Skip to content

Commit e758d7d

Browse files
authored
Merge pull request #1 from nimblehq/feature/move-decoder-from-nimble-extension
[Feature] Create JSONMapper framework
2 parents d403505 + c4545b0 commit e758d7d

19 files changed

+669
-42
lines changed

.github/PULL_REQUEST_TEMPLATE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
https://github.com/nimblehq/git-template/issues/??
1+
https://github.com/nimblehq/JSONMapper/issues/??
22

33
## What happened 👀
44

.gitignore

Lines changed: 47 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,47 @@
1-
*.gem
2-
*.rbc
3-
/.config
4-
/.idea
5-
/coverage/
6-
/InstalledFiles
7-
/node-modules
8-
/pkg/
9-
/spec/reports/
10-
/spec/examples.txt
11-
/test/tmp/
12-
/test/version_tmp/
13-
/tmp/
14-
15-
# Used by dotenv library to load environment variables.
16-
# .env
17-
18-
## Documentation cache and generated files:
19-
/.yardoc/
20-
/_yardoc/
21-
/doc/
22-
/rdoc/
23-
24-
## Environment normalization:
25-
/.bundle/
26-
/vendor/bundle
27-
/lib/bundler/man/
28-
29-
# for a library or gem, you might want to ignore these files since the code is
30-
# intended to run in multiple environments; otherwise, check them in:
31-
# Gemfile.lock
32-
# .ruby-version
33-
# .ruby-gemset
34-
35-
# unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
36-
.rvmrc
1+
# Created by https://www.gitignore.io/api/swift
2+
# Edit at https://www.gitignore.io/?templates=swift
3+
4+
### Swift ###
5+
# Xcode
6+
#
7+
# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
8+
9+
## Build generated
10+
build/
11+
DerivedData/
12+
13+
## Various settings
14+
*.pbxuser
15+
!default.pbxuser
16+
*.mode1v3
17+
!default.mode1v3
18+
*.mode2v3
19+
!default.mode2v3
20+
*.perspectivev3
21+
!default.perspectivev3
22+
xcuserdata/
23+
24+
## Other
25+
*.moved-aside
26+
*.xccheckout
27+
*.xcscmblueprint
28+
29+
## Obj-C/Swift specific
30+
*.hmap
31+
*.ipa
32+
*.dSYM.zip
33+
*.dSYM
34+
35+
## Playgrounds
36+
timeline.xctimeline
37+
playground.xcworkspace
38+
39+
# Swift Package Manager
40+
# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
41+
# Packages/
42+
# Package.pins
43+
# Package.resolved
44+
.build/
45+
46+
# End of https://www.gitignore.io/api/swift
47+
.DS_Store

Package.swift

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// swift-tools-version:5.3
2+
// The swift-tools-version declares the minimum version of Swift required to build this package.
3+
4+
import PackageDescription
5+
6+
let package = Package(
7+
name: "JSONMapper",
8+
products: [
9+
// Products define the executables and libraries a package produces, and make them visible to other packages.
10+
.library(
11+
name: "JSONMapper",
12+
targets: ["JSONMapper"]),
13+
],
14+
dependencies: [
15+
// Dependencies declare other packages that this package depends on.
16+
// .package(url: /* package url */, from: "1.0.0"),
17+
],
18+
targets: [
19+
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
20+
// Targets can depend on other targets in this package, and on products in packages this package depends on.
21+
.target(
22+
name: "JSONMapper",
23+
dependencies: []),
24+
.testTarget(
25+
name: "JSONMapperTests",
26+
dependencies: ["JSONMapper"]),
27+
]
28+
)

README.md

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,19 @@
1-
# Git Repository Template
1+
# JSONMapper
22

3-
Project repository template to set up all public projects at [Nimble](https://nimblehq.co/)
3+
An iOS library supports mapping json data
44

5-
## Usage
5+
## Installation
66

7-
Clone the repository
7+
### Swift Package Manager
8+
9+
The Swift Package Manager is a tool for automating the distribution of Swift code and is integrated into the swift compiler. It is in early development, but Alamofire does support its use on supported platforms.
10+
11+
Once you have your Swift package set up, adding Alamofire as a dependency is as easy as adding it to the dependencies value of your Package.swift.
12+
13+
dependencies: [
14+
.package(url: "https://github.com/nimblehq/JSONMapper.git", .upToNextMajor(from: "1.0.0"))
15+
]
816

9-
`git clone git@github.com:nimblehq/git-template.git`
1017

1118
## License
1219

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
//
2+
// Encodable+Dictionary.swift
3+
// NimbleExtension
4+
//
5+
// Created by Tam Nguyen on 11/17/20.
6+
// Copyright © 2020 Nimble. All rights reserved.
7+
//
8+
9+
import Foundation
10+
11+
public extension Encodable {
12+
13+
func toDictionary(_ encoder: JSONEncoder = JSONEncoder()) -> [String: Any] {
14+
guard let dictionary = try? JSONSerialization.jsonObject(
15+
with: encoder.encode(self), options: .allowFragments
16+
) as? [String: Any] else {
17+
return [:]
18+
}
19+
return dictionary
20+
}
21+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// Optional+Or.swift
2+
//
3+
// Created by Edgars Simanovskis on 27/08/2019.
4+
// Copyright © 2018 Nimble. All rights reserved.
5+
//
6+
7+
public extension Optional {
8+
9+
var isNil: Bool {
10+
return self == nil
11+
}
12+
13+
var hasValue: Bool { !isNil }
14+
15+
func or(_ otherOptional: @autoclosure () throws -> Wrapped?) rethrows -> Wrapped? {
16+
switch self {
17+
case .some(let value): return value
18+
case .none: return try otherOptional()
19+
}
20+
}
21+
22+
func or(_ otherWrapped: @autoclosure () throws -> Wrapped) rethrows -> Wrapped {
23+
switch self {
24+
case .some(let value): return value
25+
case .none: return try otherWrapped()
26+
}
27+
}
28+
29+
func resolve(with error: @autoclosure () -> Error) throws -> Wrapped {
30+
switch self {
31+
case .none: throw error()
32+
case .some(let wrapped): return wrapped
33+
}
34+
}
35+
}
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
// JSONAPIDecoder.swift
2+
//
3+
// Created by Pirush Prechathavanich on 4/4/18.
4+
// Copyright © 2018 Nimble. All rights reserved.
5+
//
6+
7+
import Foundation
8+
9+
public class JSONAPIDecoder: JSONDecoder {
10+
11+
private typealias ResourceDictionary = [ResourceIdentifier: Resource]
12+
13+
private let decoder: JSONEncoder
14+
15+
init(decoder: JSONEncoder = JSONEncoder()) {
16+
self.decoder = decoder
17+
}
18+
19+
public override func decode<T>(_ type: T.Type, from data: Data) throws -> T where T: Decodable {
20+
let jsonAPIObject = try super.decode(JSONAPIObject.self, from: data)
21+
22+
let includedData = jsonAPIObject.included ?? []
23+
let dictionary = includedDictionary(from: includedData)
24+
25+
switch jsonAPIObject.type {
26+
case .data(let data):
27+
return try decode(data, including: dictionary, into: type)
28+
case .meta(let meta):
29+
return try decode(meta, into: type)
30+
case .errors(let errors):
31+
throw errors
32+
}
33+
}
34+
35+
public func decodeWithMeta<Value: Decodable, Meta: Decodable>(
36+
value valueType: Value.Type,
37+
meta metaType: Meta.Type,
38+
from data: Data
39+
) throws -> (value: Value, meta: Meta) {
40+
let jsonAPIObject = try super.decode(JSONAPIObject.self, from: data)
41+
42+
let includedData = jsonAPIObject.included ?? []
43+
let dictionary = includedDictionary(from: includedData)
44+
45+
switch jsonAPIObject.type {
46+
case .data(let data):
47+
return (
48+
value: try decode(data, including: dictionary, into: valueType),
49+
meta: try decode(jsonAPIObject.meta ?? .nil, into: metaType)
50+
)
51+
case .meta(let meta):
52+
throw Errors.JSONAPIDecodingError.unableToDecode(
53+
reason: "No data field. Only contains meta: \(meta)"
54+
)
55+
case .errors(let errors):
56+
throw errors
57+
}
58+
}
59+
60+
}
61+
62+
// MARK: - Private
63+
64+
extension JSONAPIDecoder {
65+
66+
private func decode<T: Decodable>(_ meta: JSON, into type: T.Type) throws -> T {
67+
let data = try decoder.encode(meta)
68+
return try super.decode(type, from: data)
69+
}
70+
71+
private func decode<T: Decodable>(_ dataType: DataType<Resource>,
72+
including includedDictionary: ResourceDictionary,
73+
into type: T.Type) throws -> T {
74+
switch dataType {
75+
case .single(let resource):
76+
return try decode(resource, including: includedDictionary, into: type)
77+
case .collection(let resources):
78+
return try decodeCollection(of: resources, including: includedDictionary, into: type)
79+
}
80+
}
81+
82+
private func decode<T: Decodable>(_ resource: Resource,
83+
including includedDictionary: ResourceDictionary,
84+
into type: T.Type) throws -> T {
85+
let dictionary = try resolvedAttributes(of: resource, including: includedDictionary)
86+
let data = try decoder.encode(dictionary)
87+
return try super.decode(type, from: data)
88+
}
89+
90+
private func decodeCollection<T: Decodable>(of resources: [Resource],
91+
including includedDictionary: ResourceDictionary,
92+
into type: T.Type) throws -> T {
93+
let collection = try resources.compactMap { try resolvedAttributes(of: $0, including: includedDictionary) }
94+
let data = try decoder.encode(collection)
95+
return try super.decode(type, from: data)
96+
}
97+
98+
private func includedDictionary(from includedData: [Resource]) -> ResourceDictionary {
99+
return includedData.reduce(into: [:]) { dictionary, resource in
100+
let identifier = ResourceIdentifier(id: resource.id, type: resource.type)
101+
dictionary[identifier] = resource
102+
}
103+
}
104+
105+
private func resolvedAttributes(of resource: Resource,
106+
including includedDictionary: ResourceDictionary) throws -> JSON? {
107+
var attributes = resource.attributes?.nested ?? [:]
108+
attributes[Resource.CodingKeys.id.rawValue] = .string(resource.id)
109+
attributes[Resource.CodingKeys.type.rawValue] = .string(resource.type)
110+
111+
try resource.relationships?.forEach { key, relationship in
112+
guard let type = relationship.data else { return }
113+
switch type {
114+
case .single(let identifier):
115+
let includedResource = try getResource(from: includedDictionary, for: identifier)
116+
attributes[key] = try resolvedAttributes(of: includedResource, including: includedDictionary)
117+
118+
case .collection(let identifiers):
119+
let includedAttributes = try identifiers
120+
.map { try getResource(from: includedDictionary, for: $0) }
121+
.compactMap { try resolvedAttributes(of: $0, including: includedDictionary) }
122+
attributes[key] = .array(includedAttributes)
123+
}
124+
}
125+
return .nested(attributes)
126+
}
127+
128+
private func getResource(from includedDictionary: ResourceDictionary,
129+
for identifier: ResourceIdentifier) throws -> Resource {
130+
guard let resource = includedDictionary[identifier] else {
131+
throw Errors.JSONAPIDecodingError.resourceNotFound(identifier: identifier)
132+
}
133+
return resource
134+
}
135+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
// DataType.swift
2+
//
3+
// Created by Edgars Simanovskis on 27/08/2019.
4+
// Copyright © 2018 Nimble. All rights reserved.
5+
//
6+
7+
import Foundation
8+
9+
public enum DataType<T: Codable>: Codable {
10+
11+
case single(T)
12+
case collection([T])
13+
14+
public init(from decoder: Decoder) throws {
15+
let container = try decoder.singleValueContainer()
16+
do {
17+
self = try .single(container.decode(T.self))
18+
} catch DecodingError.typeMismatch {
19+
self = try .collection(container.decode([T].self))
20+
}
21+
}
22+
23+
public func encode(to encoder: Encoder) throws {
24+
var container = encoder.singleValueContainer()
25+
switch self {
26+
case .single(let object): try container.encode(object)
27+
case .collection(let array): try container.encode(array)
28+
}
29+
}
30+
}

0 commit comments

Comments
 (0)