Compare commits

...

45 Commits

Author SHA1 Message Date
Mathew Polzin f41521e33b Update example indentation 2019-11-16 00:06:49 -08:00
Mathew Polzin 45ec7ba753 Update SwiftPM package manifest example for version 3 2019-11-16 00:03:46 -08:00
Mathew Polzin 6ab4237c97 update podspec 2019-11-15 23:58:01 -08:00
Mathew Polzin 75711648a4 more minor documentation fixes 2019-11-15 23:48:49 -08:00
Mathew Polzin 2b4209ccb1 A couple more clarifications/corrections 2019-11-15 23:41:12 -08:00
Mathew Polzin ebd11df104 Merge pull request #54 from mattpolzin/beta/3x
Beta/3x
2019-11-15 23:30:01 -08:00
Mathew Polzin 96da1b4e21 update documentation 2019-11-15 23:15:32 -08:00
Mathew Polzin c7696d83fa update Playground pages to run 2019-11-15 17:46:53 -08:00
Mathew Polzin 8ee04d8932 generate linuxmain 2019-11-15 17:38:04 -08:00
Mathew Polzin 4a7a14b1b0 Add test coverage for resource object compare(to:) 2019-11-15 17:36:42 -08:00
Mathew Polzin a6b7d7a94a woops, abstract wrapper protocol landed in wrong module by accident 2019-11-15 17:05:45 -08:00
Mathew Polzin 1010489a02 compare(to:) bug fixes and test additions 2019-11-15 16:59:00 -08:00
Mathew Polzin ae855c85ee going through and fleshing out tests. minor adjustments and bug fixes. 2019-11-15 08:30:17 -08:00
Mathew Polzin 440b649577 Merge pull request #49 from mattpolzin/alpha/3x
Alpha/3x
2019-11-12 18:38:31 -08:00
Mathew Polzin 54551617b4 Add more errors for the resource object type property 2019-11-12 18:34:34 -08:00
Mathew Polzin 8c3a82ec23 Merge pull request #53 from mattpolzin/feature/better-error-messages
Feature/better error messages
2019-11-10 23:47:05 -08:00
Mathew Polzin e9a3b35dc7 improve error messages for poly include types 2019-11-10 23:43:35 -08:00
Mathew Polzin 2eecf95995 Adding Document Decoding Errors for some common problems 2019-11-10 23:02:26 -08:00
Mathew Polzin 4dc30ddc1c Rounding out the Resource Object errors 2019-11-10 20:46:35 -08:00
Mathew Polzin 455ff64326 update linuxmain 2019-11-09 00:34:39 -08:00
Mathew Polzin 0b4baf35d5 got some attribute cases added and tested. added some descriptions (custom string convertible) 2019-11-09 00:33:42 -08:00
Mathew Polzin 11ef050d58 most common relationship errors tested. 2019-11-08 18:47:28 -08:00
Mathew Polzin 86344ef93f trivial refactor in sparse fieldset file 2019-11-07 08:08:45 -08:00
Mathew Polzin 7fabe2574e rename some of the new public protocols in JSONAPITesting 2019-11-07 07:56:58 -08:00
Mathew Polzin 19636a47f0 Merge pull request #51 from mattpolzin/feature/primary-resource-body-improvements
Feature/primary resource body improvements
2019-11-06 23:46:13 -08:00
Mathew Polzin 0538de48cb rename PolyWrapped to CodablePolyWrapped (missed when I did other similar renaming 2019-11-06 23:41:02 -08:00
Mathew Polzin 024fe2d452 Down to one sequence of compare functions for all documents 2019-11-06 23:21:22 -08:00
Mathew Polzin 832161628b go from 3 specializations of all document related compare functions down to 2. 2019-11-06 23:05:21 -08:00
Mathew Polzin f37f44cfda add comparable protocol 2019-11-06 21:42:13 -08:00
Mathew Polzin ae7e0f528a abstract away document comparison 2019-11-06 00:19:27 -08:00
Mathew Polzin 9f42e5fa3f Merge pull request #48 from mattpolzin/feature/indentation
Feature/indentation
2019-11-05 23:03:03 -08:00
Mathew Polzin 3e96adf775 merge and fix conflicts 2019-11-05 23:00:18 -08:00
Mathew Polzin 83233a7e1a Merge pull request #46 from mattpolzin/feature/testing-comparisons
Feature/testing comparisons
2019-11-05 22:47:47 -08:00
Mathew Polzin adcc6bfb10 fix error after merge 2019-11-05 22:43:40 -08:00
Mathew Polzin 6f9a2df605 Merge remote-tracking branch 'origin/alpha/3x' into feature/testing-comparisons 2019-11-05 22:41:53 -08:00
Mathew Polzin c24b70b49f Merge pull request #45 from mattpolzin/feature/deprecate-subscript
Feature/deprecate subscript
2019-11-05 22:40:37 -08:00
Mathew Polzin 7b56e35a91 Merge pull request #47 from mattpolzin/feature/document-improvements
Feature/document improvements
2019-11-05 22:38:42 -08:00
Mathew Polzin e6f82c6052 indentation 2019-11-05 22:37:48 -08:00
Mathew Polzin d6f01d6c1d indentation 2019-11-05 22:27:59 -08:00
Mathew Polzin a7f6ed5845 update README 2019-11-05 21:03:51 -08:00
Mathew Polzin 87271b93f9 deprecate subscript attribute accessor in favor of key path dynamic member lookup 2019-11-05 20:48:04 -08:00
Mathew Polzin 706346e3a6 just reformatting and rearranging 2019-11-05 20:04:47 -08:00
Mathew Polzin 33a5ff41a0 beginning to do some breaking change cleanup 2019-11-05 19:50:52 -08:00
Mathew Polzin 0fe5c53ada generate linuxmain 2019-11-05 19:04:47 -08:00
Mathew Polzin f7bfa91ccc lots of comparison code, a few small breaking changes 2019-11-05 19:03:51 -08:00
72 changed files with 6269 additions and 2194 deletions
@@ -37,7 +37,7 @@ typealias ToManyRelationship<Entity: Relatable> = JSONAPI.ToManyRelationship<Ent
// JSON:API Documents for this particular API to have Metadata, Links, // JSON:API Documents for this particular API to have Metadata, Links,
// useful Errors, or an APIDescription (The *SPEC* calls this // useful Errors, or an APIDescription (The *SPEC* calls this
// "API Description" the "JSON:API Object"). // "API Description" the "JSON:API Object").
typealias Document<PrimaryResourceBody: JSONAPI.ResourceBody, IncludeType: JSONAPI.Include> = JSONAPI.Document<PrimaryResourceBody, NoMetadata, NoLinks, IncludeType, NoAPIDescription, BasicJSONAPIError<String>> typealias Document<PrimaryResourceBody: JSONAPI.CodableResourceBody, IncludeType: JSONAPI.Include> = JSONAPI.Document<PrimaryResourceBody, NoMetadata, NoLinks, IncludeType, NoAPIDescription, BasicJSONAPIError<String>>
// MARK: Entity Definitions // MARK: Entity Definitions
@@ -64,11 +64,11 @@ if case let .data(bodyData) = peopleResponse.body {
// MARK: - Work in the abstract // MARK: - Work in the abstract
print("-----") print("-----")
func process<T: JSONAPIDocument>(document: T) { func process<T: CodableJSONAPIDocument>(document: T) {
guard case let .data(body) = document.body else { guard let body = document.body.data else {
return return
} }
let x: T.Body.Data = body let x: T.BodyData = body
} }
process(document: peopleResponse) process(document: peopleResponse)
+2 -2
View File
@@ -16,7 +16,7 @@ Pod::Spec.new do |spec|
# #
spec.name = "MP-JSONAPI" spec.name = "MP-JSONAPI"
spec.version = "2.5.0" spec.version = "3.0.0"
spec.summary = "Swift Codable JSON API framework." spec.summary = "Swift Codable JSON API framework."
# This description is used to generate tags and improve search results. # This description is used to generate tags and improve search results.
@@ -136,6 +136,6 @@ See the JSON API Spec here: https://jsonapi.org/format/
# spec.requires_arc = true # spec.requires_arc = true
# spec.xcconfig = { "HEADER_SEARCH_PATHS" => "$(SDKROOT)/usr/include/libxml2" } # spec.xcconfig = { "HEADER_SEARCH_PATHS" => "$(SDKROOT)/usr/include/libxml2" }
spec.dependency "Poly", "~> 2.2" spec.dependency "Poly", "~> 2.3.1"
end end
+2 -2
View File
@@ -6,8 +6,8 @@
"repositoryURL": "https://github.com/mattpolzin/Poly.git", "repositoryURL": "https://github.com/mattpolzin/Poly.git",
"state": { "state": {
"branch": null, "branch": null,
"revision": "b24fd3b41bf3126d4c6dede3708135182172af60", "revision": "0c9c08204142babc480938d704a23513d11420e5",
"version": "2.2.0" "version": "2.3.1"
} }
} }
] ]
+1 -1
View File
@@ -18,7 +18,7 @@ let package = Package(
targets: ["JSONAPITesting"]) targets: ["JSONAPITesting"])
], ],
dependencies: [ dependencies: [
.package(url: "https://github.com/mattpolzin/Poly.git", .upToNextMajor(from: "2.2.0")), .package(url: "https://github.com/mattpolzin/Poly.git", .upToNextMajor(from: "2.3.1")),
], ],
targets: [ targets: [
.target( .target(
+60 -640
View File
File diff suppressed because it is too large Load Diff
+32 -32
View File
@@ -7,58 +7,58 @@
/// This is what the JSON API Spec calls the "JSON:API Object" /// This is what the JSON API Spec calls the "JSON:API Object"
public protocol APIDescriptionType: Codable, Equatable { public protocol APIDescriptionType: Codable, Equatable {
associatedtype Meta associatedtype Meta
} }
/// This is what the JSON API Spec calls the "JSON:API Object" /// This is what the JSON API Spec calls the "JSON:API Object"
public struct APIDescription<Meta: JSONAPI.Meta>: APIDescriptionType { public struct APIDescription<Meta: JSONAPI.Meta>: APIDescriptionType {
public let version: String public let version: String
public let meta: Meta public let meta: Meta
public init(version: String, meta: Meta) { public init(version: String, meta: Meta) {
self.version = version self.version = version
self.meta = meta self.meta = meta
} }
} }
/// Can be used as `APIDescriptionType` for Documents that do not /// Can be used as `APIDescriptionType` for Documents that do not
/// have any API Description (a.k.a. "JSON:API Object"). /// have any API Description (a.k.a. "JSON:API Object").
public struct NoAPIDescription: APIDescriptionType, CustomStringConvertible { public struct NoAPIDescription: APIDescriptionType, CustomStringConvertible {
public typealias Meta = NoMetadata public typealias Meta = NoMetadata
public init() {} public init() {}
public static var none: NoAPIDescription { return .init() } public static var none: NoAPIDescription { return .init() }
public var description: String { return "No JSON:API Object" } public var description: String { return "No JSON:API Object" }
} }
extension APIDescription { extension APIDescription {
private enum CodingKeys: String, CodingKey { private enum CodingKeys: String, CodingKey {
case version case version
case meta case meta
} }
public init(from decoder: Decoder) throws { public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self) let container = try decoder.container(keyedBy: CodingKeys.self)
// The spec says that if a version is not specified, it should be assumed to be at least 1.0 // The spec says that if a version is not specified, it should be assumed to be at least 1.0
version = (try? container.decode(String.self, forKey: .version)) ?? "1.0" version = (try? container.decode(String.self, forKey: .version)) ?? "1.0"
if let metaVal = NoMetadata() as? Meta { if let metaVal = NoMetadata() as? Meta {
meta = metaVal meta = metaVal
} else { } else {
meta = try container.decode(Meta.self, forKey: .meta) meta = try container.decode(Meta.self, forKey: .meta)
} }
} }
public func encode(to encoder: Encoder) throws { public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self) var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(version, forKey: .version) try container.encode(version, forKey: .version)
if Meta.self != NoMetadata.self { if Meta.self != NoMetadata.self {
try container.encode(meta, forKey: .meta) try container.encode(meta, forKey: .meta)
} }
} }
} }
File diff suppressed because it is too large Load Diff
@@ -1,11 +1,73 @@
// //
// DocumentDecodingErro.swift // DocumentDecodingError.swift
// //
// //
// Created by Mathew Polzin on 10/20/19. // Created by Mathew Polzin on 10/20/19.
// //
public enum JSONAPIDocumentDecodingError: Swift.Error { public enum DocumentDecodingError: Swift.Error, Equatable {
case primaryResource(error: ResourceObjectDecodingError, idx: Int?)
case primaryResourceMissing
case primaryResourcesMissing
case includes(error: IncludesDecodingError)
case foundErrorDocumentWhenExpectingSuccess case foundErrorDocumentWhenExpectingSuccess
case foundSuccessDocumentWhenExpectingError case foundSuccessDocumentWhenExpectingError
init(_ decodingError: ResourceObjectDecodingError) {
self = .primaryResource(error: decodingError, idx: nil)
}
init(_ decodingError: ManyResourceBodyDecodingError) {
self = .primaryResource(error: decodingError.error, idx: decodingError.idx)
}
init(_ decodingError: IncludesDecodingError) {
self = .includes(error: decodingError)
}
init?(_ decodingError: DecodingError) {
switch decodingError {
case .valueNotFound(let type, let context) where Location(context) == .data && type is AbstractResourceObject.Type:
self = .primaryResourceMissing
case .valueNotFound(let type, let context) where Location(context) == .data && type == UnkeyedDecodingContainer.self:
self = .primaryResourcesMissing
default:
return nil
}
}
private enum Location: Equatable {
case data
init?(_ context: DecodingError.Context) {
guard context.codingPath.contains(where: { $0.stringValue == "data" }) else {
return nil
}
self = .data
}
}
}
extension DocumentDecodingError: CustomStringConvertible {
public var description: String {
switch self {
case .primaryResource(error: let error, idx: let idx):
let idxString = idx.map { " \($0 + 1)" } ?? ""
return "Primary Resource\(idxString) failed to parse because \(error)"
case .primaryResourceMissing:
return "Primary Resource missing."
case .primaryResourcesMissing:
return "Primary Resources array missing."
case .includes(error: let error):
return "\(error)"
case .foundErrorDocumentWhenExpectingSuccess:
return "Expected a success document with a 'data' property but found an error document."
case .foundSuccessDocumentWhenExpectingError:
return "Expected an error document but found a success document with a 'data' property."
}
}
} }
+116 -60
View File
@@ -14,35 +14,35 @@ public typealias Include = EncodableJSONPoly
/// ///
/// If you have /// If you have
/// ///
/// `let includes: Includes<Include2<Thing1, Thing2>> = ...` /// let includes: Includes<Include2<Thing1, Thing2>> = ...
/// ///
/// then you can access all `Thing1` included resources with /// then you can access all `Thing1` included resources with
/// ///
/// `let includedThings = includes[Thing1.self]` /// let includedThings = includes[Thing1.self]
public struct Includes<I: Include>: Encodable, Equatable { public struct Includes<I: Include>: Encodable, Equatable {
public static var none: Includes { return .init(values: []) } public static var none: Includes { return .init(values: []) }
let values: [I]
public init(values: [I]) {
self.values = values
}
public func encode(to encoder: Encoder) throws { public let values: [I]
var container = encoder.unkeyedContainer()
guard I.self != NoIncludes.self else { public init(values: [I]) {
throw JSONAPIEncodingError.illegalEncoding("Attempting to encode Include0, which should be represented by the absense of an 'included' entry altogether.") self.values = values
} }
for value in values { public func encode(to encoder: Encoder) throws {
try container.encode(value) var container = encoder.unkeyedContainer()
}
} guard I.self != NoIncludes.self else {
throw JSONAPICodingError.illegalEncoding("Attempting to encode Include0, which should be represented by the absense of an 'included' entry altogether.", path: encoder.codingPath)
public var count: Int { }
return values.count
} for value in values {
try container.encode(value)
}
}
public var count: Int {
return values.count
}
} }
extension Includes: Decodable where I: Decodable { extension Includes: Decodable where I: Decodable {
@@ -56,8 +56,35 @@ extension Includes: Decodable where I: Decodable {
} }
var valueAggregator = [I]() var valueAggregator = [I]()
var idx = 0
while !container.isAtEnd { while !container.isAtEnd {
valueAggregator.append(try container.decode(I.self)) do {
valueAggregator.append(try container.decode(I.self))
idx = idx + 1
} catch let error as PolyDecodeNoTypesMatchedError {
let errors: [ResourceObjectDecodingError] = error
.individualTypeFailures
.compactMap { decodingError in
switch decodingError.error {
case .typeMismatch(_, let context),
.valueNotFound(_, let context),
.keyNotFound(_, let context),
.dataCorrupted(let context):
return context.underlyingError as? ResourceObjectDecodingError
@unknown default:
return nil
}
}
guard errors.count == error.individualTypeFailures.count else {
throw IncludesDecodingError(error: error, idx: idx)
}
throw IncludesDecodingError(
error: IncludeDecodingError(failures: errors),
idx: idx
)
} catch let error {
throw IncludesDecodingError(error: error, idx: idx)
}
} }
values = valueAggregator values = valueAggregator
@@ -65,25 +92,25 @@ extension Includes: Decodable where I: Decodable {
} }
extension Includes { extension Includes {
public func appending(_ other: Includes<I>) -> Includes { public func appending(_ other: Includes<I>) -> Includes {
return Includes(values: values + other.values) return Includes(values: values + other.values)
} }
} }
public func +<I: Include>(_ left: Includes<I>, _ right: Includes<I>) -> Includes<I> { public func +<I: Include>(_ left: Includes<I>, _ right: Includes<I>) -> Includes<I> {
return left.appending(right) return left.appending(right)
} }
extension Includes: CustomStringConvertible { extension Includes: CustomStringConvertible {
public var description: String { public var description: String {
return "Includes(\(String(describing: values))" return "Includes(\(String(describing: values))"
} }
} }
extension Includes where I == NoIncludes { extension Includes where I == NoIncludes {
public init() { public init() {
values = [] values = []
} }
} }
// MARK: - 0 includes // MARK: - 0 includes
@@ -93,73 +120,73 @@ public typealias NoIncludes = Include0
// MARK: - 1 include // MARK: - 1 include
public typealias Include1 = Poly1 public typealias Include1 = Poly1
extension Includes where I: _Poly1 { extension Includes where I: _Poly1 {
public subscript(_ lookup: I.A.Type) -> [I.A] { public subscript(_ lookup: I.A.Type) -> [I.A] {
return values.compactMap { $0.a } return values.compactMap { $0.a }
} }
} }
// MARK: - 2 includes // MARK: - 2 includes
public typealias Include2 = Poly2 public typealias Include2 = Poly2
extension Includes where I: _Poly2 { extension Includes where I: _Poly2 {
public subscript(_ lookup: I.B.Type) -> [I.B] { public subscript(_ lookup: I.B.Type) -> [I.B] {
return values.compactMap { $0.b } return values.compactMap { $0.b }
} }
} }
// MARK: - 3 includes // MARK: - 3 includes
public typealias Include3 = Poly3 public typealias Include3 = Poly3
extension Includes where I: _Poly3 { extension Includes where I: _Poly3 {
public subscript(_ lookup: I.C.Type) -> [I.C] { public subscript(_ lookup: I.C.Type) -> [I.C] {
return values.compactMap { $0.c } return values.compactMap { $0.c }
} }
} }
// MARK: - 4 includes // MARK: - 4 includes
public typealias Include4 = Poly4 public typealias Include4 = Poly4
extension Includes where I: _Poly4 { extension Includes where I: _Poly4 {
public subscript(_ lookup: I.D.Type) -> [I.D] { public subscript(_ lookup: I.D.Type) -> [I.D] {
return values.compactMap { $0.d } return values.compactMap { $0.d }
} }
} }
// MARK: - 5 includes // MARK: - 5 includes
public typealias Include5 = Poly5 public typealias Include5 = Poly5
extension Includes where I: _Poly5 { extension Includes where I: _Poly5 {
public subscript(_ lookup: I.E.Type) -> [I.E] { public subscript(_ lookup: I.E.Type) -> [I.E] {
return values.compactMap { $0.e } return values.compactMap { $0.e }
} }
} }
// MARK: - 6 includes // MARK: - 6 includes
public typealias Include6 = Poly6 public typealias Include6 = Poly6
extension Includes where I: _Poly6 { extension Includes where I: _Poly6 {
public subscript(_ lookup: I.F.Type) -> [I.F] { public subscript(_ lookup: I.F.Type) -> [I.F] {
return values.compactMap { $0.f } return values.compactMap { $0.f }
} }
} }
// MARK: - 7 includes // MARK: - 7 includes
public typealias Include7 = Poly7 public typealias Include7 = Poly7
extension Includes where I: _Poly7 { extension Includes where I: _Poly7 {
public subscript(_ lookup: I.G.Type) -> [I.G] { public subscript(_ lookup: I.G.Type) -> [I.G] {
return values.compactMap { $0.g } return values.compactMap { $0.g }
} }
} }
// MARK: - 8 includes // MARK: - 8 includes
public typealias Include8 = Poly8 public typealias Include8 = Poly8
extension Includes where I: _Poly8 { extension Includes where I: _Poly8 {
public subscript(_ lookup: I.H.Type) -> [I.H] { public subscript(_ lookup: I.H.Type) -> [I.H] {
return values.compactMap { $0.h } return values.compactMap { $0.h }
} }
} }
// MARK: - 9 includes // MARK: - 9 includes
public typealias Include9 = Poly9 public typealias Include9 = Poly9
extension Includes where I: _Poly9 { extension Includes where I: _Poly9 {
public subscript(_ lookup: I.I.Type) -> [I.I] { public subscript(_ lookup: I.I.Type) -> [I.I] {
return values.compactMap { $0.i } return values.compactMap { $0.i }
} }
} }
// MARK: - 10 includes // MARK: - 10 includes
@@ -177,3 +204,32 @@ extension Includes where I: _Poly11 {
return values.compactMap { $0.k } return values.compactMap { $0.k }
} }
} }
// MARK: - DecodingError
public struct IncludesDecodingError: Swift.Error, Equatable {
public let error: Swift.Error
public let idx: Int
public static func ==(lhs: Self, rhs: Self) -> Bool {
return lhs.idx == rhs.idx
&& String(describing: lhs) == String(describing: rhs)
}
}
extension IncludesDecodingError: CustomStringConvertible {
public var description: String {
return "Include \(idx + 1) failed to parse: \(error)"
}
}
public struct IncludeDecodingError: Swift.Error, Equatable, CustomStringConvertible {
public let failures: [ResourceObjectDecodingError]
public var description: String {
return failures
.enumerated()
.map {
"\nCould not have been Include Type \($0.offset + 1) because:\n\($0.element)"
}.joined(separator: "\n")
}
}
+69 -52
View File
@@ -10,121 +10,132 @@
/// array should be used for no results). /// array should be used for no results).
public protocol OptionalEncodablePrimaryResource: Equatable, Encodable {} public protocol OptionalEncodablePrimaryResource: Equatable, Encodable {}
/// An `EncodablePrimaryResource` is a `PrimaryResource` that only supports encoding. /// An `EncodablePrimaryResource` is a `CodablePrimaryResource` that only supports encoding.
/// This is actually more restrictave than `PrimaryResource`, which supports both encoding and
/// decoding.
public protocol EncodablePrimaryResource: OptionalEncodablePrimaryResource {} public protocol EncodablePrimaryResource: OptionalEncodablePrimaryResource {}
/// This protocol allows for `SingleResourceBody` to contain a `null` /// This protocol allows for `SingleResourceBody` to contain a `null`
/// data object where `ManyResourceBody` cannot (because an empty /// data object where `ManyResourceBody` cannot (because an empty
/// array should be used for no results). /// array should be used for no results).
public protocol OptionalPrimaryResource: OptionalEncodablePrimaryResource, Decodable {} public protocol OptionalCodablePrimaryResource: OptionalEncodablePrimaryResource, Decodable {}
/// A `PrimaryResource` is a type that can be used in the body of a JSON API /// A `CodablePrimaryResource` is a type that can be used in the body of a JSON API
/// document as the primary resource. /// document as the primary resource.
public protocol PrimaryResource: EncodablePrimaryResource, OptionalPrimaryResource {} public protocol CodablePrimaryResource: EncodablePrimaryResource, OptionalCodablePrimaryResource {}
extension Optional: OptionalEncodablePrimaryResource where Wrapped: EncodablePrimaryResource {} extension Optional: OptionalEncodablePrimaryResource where Wrapped: EncodablePrimaryResource {}
extension Optional: OptionalPrimaryResource where Wrapped: PrimaryResource {} extension Optional: OptionalCodablePrimaryResource where Wrapped: CodablePrimaryResource {}
/// An `EncodableResourceBody` is a `ResourceBody` that only supports being /// An `EncodableResourceBody` is a `ResourceBody` that only supports being
/// encoded. It is actually weaker than `ResourceBody`, which supports both encoding /// encoded. It is actually weaker than `ResourceBody`, which supports both encoding
/// and decoding. /// and decoding.
public protocol EncodableResourceBody: Equatable, Encodable {} public protocol EncodableResourceBody: Equatable, Encodable {
associatedtype PrimaryResource
}
/// A ResourceBody is a representation of the body of the JSON API Document. /// A `CodableResourceBody` is a representation of the body of the JSON:API Document.
/// It can either be one resource (which can be specified as optional or not) /// It can either be one resource (which can be specified as optional or not)
/// or it can contain many resources (and array with zero or more entries). /// or it can contain many resources (and array with zero or more entries).
public protocol ResourceBody: Decodable, EncodableResourceBody {} public protocol CodableResourceBody: Decodable, EncodableResourceBody {}
/// A `ResourceBody` that has the ability to take on more primary /// A `ResourceBody` that has the ability to take on more primary
/// resources by appending another similarly typed `ResourceBody`. /// resources by appending another similarly typed `ResourceBody`.
public protocol Appendable { public protocol ResourceBodyAppendable {
func appending(_ other: Self) -> Self func appending(_ other: Self) -> Self
} }
public func +<R: Appendable>(_ left: R, right: R) -> R { public func +<R: ResourceBodyAppendable>(_ left: R, right: R) -> R {
return left.appending(right) return left.appending(right)
} }
/// A type allowing for a document body containing 1 primary resource. /// A type allowing for a document body containing 1 primary resource.
/// If the `Entity` specialization is an `Optional` type, the body can contain /// If the `Entity` specialization is an `Optional` type, the body can contain
/// 0 or 1 primary resources. /// 0 or 1 primary resources.
public struct SingleResourceBody<Entity: JSONAPI.OptionalEncodablePrimaryResource>: EncodableResourceBody { public struct SingleResourceBody<PrimaryResource: JSONAPI.OptionalEncodablePrimaryResource>: EncodableResourceBody {
public let value: Entity public let value: PrimaryResource
public init(resourceObject: Entity) { public init(resourceObject: PrimaryResource) {
self.value = resourceObject self.value = resourceObject
} }
} }
/// A type allowing for a document body containing 0 or more primary resources. /// A type allowing for a document body containing 0 or more primary resources.
public struct ManyResourceBody<Entity: JSONAPI.EncodablePrimaryResource>: EncodableResourceBody, Appendable { public struct ManyResourceBody<PrimaryResource: JSONAPI.EncodablePrimaryResource>: EncodableResourceBody, ResourceBodyAppendable {
public let values: [Entity] public let values: [PrimaryResource]
public init(resourceObjects: [Entity]) { public init(resourceObjects: [PrimaryResource]) {
values = resourceObjects values = resourceObjects
} }
public func appending(_ other: ManyResourceBody) -> ManyResourceBody { public func appending(_ other: ManyResourceBody) -> ManyResourceBody {
return ManyResourceBody(resourceObjects: values + other.values) return ManyResourceBody(resourceObjects: values + other.values)
} }
} }
/// Use NoResourceBody to indicate you expect a JSON API document to not /// Use NoResourceBody to indicate you expect a JSON API document to not
/// contain a "data" top-level key. /// contain a "data" top-level key.
public struct NoResourceBody: ResourceBody { public struct NoResourceBody: CodableResourceBody {
public static var none: NoResourceBody { return NoResourceBody() } public typealias PrimaryResource = Void
public static var none: NoResourceBody { return NoResourceBody() }
} }
// MARK: Codable // MARK: Codable
extension SingleResourceBody { extension SingleResourceBody {
public func encode(to encoder: Encoder) throws { public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer() var container = encoder.singleValueContainer()
let anyNil: Any? = nil let anyNil: Any? = nil
let nilValue = anyNil as? Entity let nilValue = anyNil as? PrimaryResource
guard value != nilValue else { guard value != nilValue else {
try container.encodeNil() try container.encodeNil()
return return
} }
try container.encode(value) try container.encode(value)
} }
} }
extension SingleResourceBody: Decodable, ResourceBody where Entity: OptionalPrimaryResource { extension SingleResourceBody: Decodable, CodableResourceBody where PrimaryResource: OptionalCodablePrimaryResource {
public init(from decoder: Decoder) throws { public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer() let container = try decoder.singleValueContainer()
let anyNil: Any? = nil let anyNil: Any? = nil
if container.decodeNil(), if container.decodeNil(),
let val = anyNil as? Entity { let val = anyNil as? PrimaryResource {
value = val value = val
return return
} }
value = try container.decode(Entity.self) value = try container.decode(PrimaryResource.self)
} }
} }
extension ManyResourceBody { extension ManyResourceBody {
public func encode(to encoder: Encoder) throws { public func encode(to encoder: Encoder) throws {
var container = encoder.unkeyedContainer() var container = encoder.unkeyedContainer()
for value in values { for value in values {
try container.encode(value) try container.encode(value)
} }
} }
} }
extension ManyResourceBody: Decodable, ResourceBody where Entity: PrimaryResource { extension ManyResourceBody: Decodable, CodableResourceBody where PrimaryResource: CodablePrimaryResource {
public init(from decoder: Decoder) throws { public init(from decoder: Decoder) throws {
var container = try decoder.unkeyedContainer() var container = try decoder.unkeyedContainer()
var valueAggregator = [Entity]() var valueAggregator = [PrimaryResource]()
var idx = 0
while !container.isAtEnd { while !container.isAtEnd {
valueAggregator.append(try container.decode(Entity.self)) do {
valueAggregator.append(try container.decode(PrimaryResource.self))
} catch let error as ResourceObjectDecodingError {
throw ManyResourceBodyDecodingError(
error: error,
idx: idx
)
}
idx = idx + 1
} }
values = valueAggregator values = valueAggregator
} }
@@ -133,13 +144,19 @@ extension ManyResourceBody: Decodable, ResourceBody where Entity: PrimaryResourc
// MARK: CustomStringConvertible // MARK: CustomStringConvertible
extension SingleResourceBody: CustomStringConvertible { extension SingleResourceBody: CustomStringConvertible {
public var description: String { public var description: String {
return "PrimaryResourceBody(\(String(describing: value)))" return "PrimaryResourceBody(\(String(describing: value)))"
} }
} }
extension ManyResourceBody: CustomStringConvertible { extension ManyResourceBody: CustomStringConvertible {
public var description: String { public var description: String {
return "PrimaryResourceBody(\(String(describing: values)))" return "PrimaryResourceBody(\(String(describing: values)))"
} }
}
// MARK: - DecodingError
public struct ManyResourceBodyDecodingError: Swift.Error, Equatable {
public let error: ResourceObjectDecodingError
public let idx: Int
} }
-14
View File
@@ -1,14 +0,0 @@
//
// EncodingError.swift
// JSONAPI
//
// Created by Mathew Polzin on 12/7/18.
//
public enum JSONAPIEncodingError: Swift.Error {
case typeMismatch(expected: String, found: String)
case illegalEncoding(String)
case illegalDecoding(String)
case missingOrMalformedMetadata
case missingOrMalformedLinks
}
+10 -3
View File
@@ -6,10 +6,12 @@
// //
/// Most of the JSON:API Spec defined Error fields. /// Most of the JSON:API Spec defined Error fields.
public struct BasicJSONAPIErrorPayload<IdType: Codable & Equatable>: Codable, Equatable, ErrorDictType { public struct BasicJSONAPIErrorPayload<IdType: Codable & Equatable>: Codable, Equatable, ErrorDictType, CustomStringConvertible {
/// a unique identifier for this particular occurrence of the problem /// a unique identifier for this particular occurrence of the problem
public let id: IdType? public let id: IdType?
// public let links: Links? // we skip this for now to avoid adding complexity to using this basic type.
// public let links: Links? // we skip this for now to avoid adding complexity to using this basic type.
/// the HTTP status code applicable to this problem /// the HTTP status code applicable to this problem
public let status: String? public let status: String?
/// an application-specific error code /// an application-specific error code
@@ -20,7 +22,8 @@ public struct BasicJSONAPIErrorPayload<IdType: Codable & Equatable>: Codable, Eq
public let detail: String? public let detail: String?
/// an object containing references to the source of the error /// an object containing references to the source of the error
public let source: Source? public let source: Source?
// public let meta: Meta? // we skip this for now to avoid adding complexity to using this basic type
// public let meta: Meta? // we skip this for now to avoid adding complexity to using this basic type
public init(id: IdType? = nil, public init(id: IdType? = nil,
status: String? = nil, status: String? = nil,
@@ -61,6 +64,10 @@ public struct BasicJSONAPIErrorPayload<IdType: Codable & Equatable>: Codable, Eq
].compactMap { $0 } ].compactMap { $0 }
return Dictionary(uniqueKeysWithValues: keysAndValues) return Dictionary(uniqueKeysWithValues: keysAndValues)
} }
public var description: String {
return definedFields.map { "\($0.key): \($0.value)" }.sorted().joined(separator: ", ")
}
} }
/// `BasicJSONAPIError` optionally decodes many possible fields /// `BasicJSONAPIError` optionally decodes many possible fields
@@ -8,7 +8,7 @@
/// `GenericJSONAPIError` can be used to specify whatever error /// `GenericJSONAPIError` can be used to specify whatever error
/// payload you expect to need to parse in responses and handle any /// payload you expect to need to parse in responses and handle any
/// other payload structure as `.unknownError`. /// other payload structure as `.unknownError`.
public enum GenericJSONAPIError<ErrorPayload: Codable & Equatable>: JSONAPIError { public enum GenericJSONAPIError<ErrorPayload: Codable & Equatable>: JSONAPIError, CustomStringConvertible {
case unknownError case unknownError
case error(ErrorPayload) case error(ErrorPayload)
@@ -35,6 +35,15 @@ public enum GenericJSONAPIError<ErrorPayload: Codable & Equatable>: JSONAPIError
public static var unknown: Self { public static var unknown: Self {
return .unknownError return .unknownError
} }
public var description: String {
switch self {
case .unknownError:
return "unknown error"
case .error(let payload):
return String(describing: payload)
}
}
} }
public extension GenericJSONAPIError { public extension GenericJSONAPIError {
+14 -14
View File
@@ -6,7 +6,7 @@
// //
public protocol JSONAPIError: Swift.Error, Equatable, Codable { public protocol JSONAPIError: Swift.Error, Equatable, Codable {
static var unknown: Self { get } static var unknown: Self { get }
} }
/// `UnknownJSONAPIError` can actually be used in any sitaution /// `UnknownJSONAPIError` can actually be used in any sitaution
@@ -16,18 +16,18 @@ public protocol JSONAPIError: Swift.Error, Equatable, Codable {
/// information the server might be providing in the error payload, /// information the server might be providing in the error payload,
/// use `BasicJSONAPIError` instead. /// use `BasicJSONAPIError` instead.
public enum UnknownJSONAPIError: JSONAPIError { public enum UnknownJSONAPIError: JSONAPIError {
case unknownError case unknownError
public init(from decoder: Decoder) throws {
self = .unknown
}
public func encode(to encoder: Encoder) throws { public init(from decoder: Decoder) throws {
var container = encoder.singleValueContainer() self = .unknown
try container.encode("unknown") }
}
public func encode(to encoder: Encoder) throws {
public static var unknown: Self { var container = encoder.singleValueContainer()
return .unknownError try container.encode("unknown")
} }
public static var unknown: Self {
return .unknownError
}
} }
+27
View File
@@ -0,0 +1,27 @@
//
// JSONAPICodingError.swift
// JSONAPI
//
// Created by Mathew Polzin on 12/7/18.
//
public enum JSONAPICodingError: Swift.Error {
case typeMismatch(expected: String, found: String, path: [CodingKey])
case quantityMismatch(expected: Quantity, path: [CodingKey])
case illegalEncoding(String, path: [CodingKey])
case illegalDecoding(String, path: [CodingKey])
case missingOrMalformedMetadata(path: [CodingKey])
case missingOrMalformedLinks(path: [CodingKey])
public enum Quantity: String, Equatable {
case one
case many
public var other: Quantity {
switch self {
case .one: return .many
case .many: return .one
}
}
}
}
+44 -44
View File
@@ -5,63 +5,63 @@
// Created by Mathew Polzin on 11/24/18. // Created by Mathew Polzin on 11/24/18.
// //
/// A Links structure should contain nothing but JSONAPI.Link properties. /// A Links structure should contain nothing but `JSONAPI.Link` properties.
public protocol Links: Codable, Equatable {} public protocol Links: Codable, Equatable {}
/// Use NoLinks where no links should belong to a JSON API component /// Use NoLinks where no links should belong to a JSON API component
public struct NoLinks: Links, CustomStringConvertible { public struct NoLinks: Links, CustomStringConvertible {
public static var none: NoLinks { return NoLinks() } public static var none: NoLinks { return NoLinks() }
public init() {} public init() {}
public var description: String { return "No Links" } public var description: String { return "No Links" }
} }
public protocol JSONAPIURL: Codable, Equatable {} public protocol JSONAPIURL: Codable, Equatable {}
public struct Link<URL: JSONAPI.JSONAPIURL, Meta: JSONAPI.Meta>: Equatable, Codable { public struct Link<URL: JSONAPI.JSONAPIURL, Meta: JSONAPI.Meta>: Equatable, Codable {
public let url: URL public let url: URL
public let meta: Meta public let meta: Meta
public init(url: URL, meta: Meta) { public init(url: URL, meta: Meta) {
self.url = url self.url = url
self.meta = meta self.meta = meta
} }
} }
extension Link where Meta == NoMetadata { extension Link where Meta == NoMetadata {
public init(url: URL) { public init(url: URL) {
self.init(url: url, meta: .none) self.init(url: url, meta: .none)
} }
} }
public extension Link { public extension Link {
private enum CodingKeys: String, CodingKey { private enum CodingKeys: String, CodingKey {
case href case href
case meta case meta
} }
init(from decoder: Decoder) throws { init(from decoder: Decoder) throws {
guard Meta.self == NoMetadata.self, guard Meta.self == NoMetadata.self,
let noMeta = NoMetadata() as? Meta else { let noMeta = NoMetadata() as? Meta else {
let container = try decoder.container(keyedBy: CodingKeys.self) let container = try decoder.container(keyedBy: CodingKeys.self)
meta = try container.decode(Meta.self, forKey: .meta) meta = try container.decode(Meta.self, forKey: .meta)
url = try container.decode(URL.self, forKey: .href) url = try container.decode(URL.self, forKey: .href)
return return
} }
let container = try decoder.singleValueContainer() let container = try decoder.singleValueContainer()
url = try container.decode(URL.self) url = try container.decode(URL.self)
meta = noMeta meta = noMeta
} }
func encode(to encoder: Encoder) throws { func encode(to encoder: Encoder) throws {
guard Meta.self == NoMetadata.self else { guard Meta.self == NoMetadata.self else {
var container = encoder.container(keyedBy: CodingKeys.self) var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(url, forKey: .href) try container.encode(url, forKey: .href)
try container.encode(meta, forKey: .meta) try container.encode(meta, forKey: .meta)
return return
} }
var container = encoder.singleValueContainer() var container = encoder.singleValueContainer()
try container.encode(url) try container.encode(url)
} }
} }
+7 -7
View File
@@ -6,25 +6,25 @@
// //
/// Conform a type to this protocol to indicate it can be encoded to or decoded from /// Conform a type to this protocol to indicate it can be encoded to or decoded from
/// the meta data attached to a component of a JSON API document. Different meta data /// the meta data attached to a component of a JSON:API document. Different meta data
/// can be stored all over the place: On the root document, on a resource object, on /// can be stored all over the place: On the root document, on a resource object, on
/// link objects, etc. /// link objects, etc.
/// ///
/// JSON API Metadata is totally open ended. It can take whatever JSON-compliant structure /// JSON:API Metadata is totally open ended. It can take whatever JSON-compliant structure
/// the server and client agree upon. /// the server and client agree upon.
public protocol Meta: Codable, Equatable { public protocol Meta: Codable, Equatable {
} }
// We make Optional a Meta if it wraps a Meta so that Metadata can be specified as // We make Optional a Meta if it wraps a Meta so that
// nullable. // Metadata can be specified as nullable.
extension Optional: Meta where Wrapped: Meta {} extension Optional: Meta where Wrapped: Meta {}
/// Use this type when you want to specify not to encode or decode any metadata /// Use this type when you want to specify not to encode or decode any metadata
/// for a type. /// for a type.
public struct NoMetadata: Meta, CustomStringConvertible { public struct NoMetadata: Meta, CustomStringConvertible {
public static var none: NoMetadata { return NoMetadata() } public static var none: NoMetadata { return NoMetadata() }
public init() { } public init() { }
public var description: String { return "No Metadata" } public var description: String { return "No Metadata" }
} }
@@ -6,31 +6,31 @@
// //
public extension TransformedAttribute { public extension TransformedAttribute {
/// Map an Attribute to a new wrapped type. /// Map an Attribute to a new wrapped type.
/// Note that the resulting Attribute will have no transformer, even if the /// Note that the resulting Attribute will have no transformer, even if the
/// source Attribute has a transformer. /// source Attribute has a transformer.
/// You are mapping the output of the source transform into /// You are mapping the output of the source transform into
/// the RawValue of a new transformerless Attribute. /// the RawValue of a new transformerless Attribute.
/// ///
/// Generally, this is the most useful operation. The transformer gives you /// Generally, this is the most useful operation. The transformer gives you
/// control over the decoding of the Attribute, but once the Attribute exists, /// control over the decoding of the Attribute, but once the Attribute exists,
/// mapping on it is most useful for creating computed Attribute properties. /// mapping on it is most useful for creating computed Attribute properties.
func map<T: Codable>(_ transform: (Transformer.To) throws -> T) rethrows -> Attribute<T> { func map<T: Codable>(_ transform: (Transformer.To) throws -> T) rethrows -> Attribute<T> {
return Attribute<T>(value: try transform(value)) return Attribute<T>(value: try transform(value))
} }
} }
public extension Attribute { public extension Attribute {
/// Map an Attribute to a new wrapped type. /// Map an Attribute to a new wrapped type.
/// Note that the resulting Attribute will have no transformer, even if the /// Note that the resulting Attribute will have no transformer, even if the
/// source Attribute has a transformer. /// source Attribute has a transformer.
/// You are mapping the output of the source transform into /// You are mapping the output of the source transform into
/// the RawValue of a new transformerless Attribute. /// the RawValue of a new transformerless Attribute.
/// ///
/// Generally, this is the most useful operation. The transformer gives you /// Generally, this is the most useful operation. The transformer gives you
/// control over the decoding of the Attribute, but once the Attribute exists, /// control over the decoding of the Attribute, but once the Attribute exists,
/// mapping on it is most useful for creating computed Attribute properties. /// mapping on it is most useful for creating computed Attribute properties.
func map<T: Codable>(_ transform: (ValueType) throws -> T) rethrows -> Attribute<T> { func map<T: Codable>(_ transform: (ValueType) throws -> T) rethrows -> Attribute<T> {
return Attribute<T>(value: try transform(value)) return Attribute<T>(value: try transform(value))
} }
} }
+93 -84
View File
@@ -5,50 +5,58 @@
// Created by Mathew Polzin on 11/13/18. // Created by Mathew Polzin on 11/13/18.
// //
public protocol AttributeType: Codable { public protocol AbstractAttributeType {
associatedtype RawValue: Codable var rawValueType: Any.Type { get }
associatedtype ValueType }
var value: ValueType { get } public protocol AttributeType: Codable, AbstractAttributeType {
associatedtype RawValue: Codable
associatedtype ValueType
var value: ValueType { get }
}
extension AttributeType {
public var rawValueType: Any.Type { return RawValue.self }
} }
// MARK: TransformedAttribute // MARK: TransformedAttribute
/// A TransformedAttribute takes a Codable type and attempts to turn it into another type. /// A TransformedAttribute takes a Codable type and attempts to turn it into another type.
public struct TransformedAttribute<RawValue: Codable, Transformer: JSONAPI.Transformer>: AttributeType where Transformer.From == RawValue { public struct TransformedAttribute<RawValue: Codable, Transformer: JSONAPI.Transformer>: AttributeType where Transformer.From == RawValue {
public let rawValue: RawValue public let rawValue: RawValue
public let value: Transformer.To public let value: Transformer.To
public init(rawValue: RawValue) throws { public init(rawValue: RawValue) throws {
self.rawValue = rawValue self.rawValue = rawValue
value = try Transformer.transform(rawValue) value = try Transformer.transform(rawValue)
} }
} }
extension TransformedAttribute where Transformer == IdentityTransformer<RawValue> { extension TransformedAttribute where Transformer == IdentityTransformer<RawValue> {
// If we are using the identity transform, we can skip the transform and guarantee no // If we are using the identity transform, we can skip the transform and guarantee no
// error is thrown. // error is thrown.
public init(value: RawValue) { public init(value: RawValue) {
rawValue = value rawValue = value
self.value = value self.value = value
} }
} }
extension TransformedAttribute where Transformer: ReversibleTransformer { extension TransformedAttribute where Transformer: ReversibleTransformer {
/// Initialize a TransformedAttribute from its transformed value. The /// Initialize a TransformedAttribute from its transformed value. The
/// RawValue, which is what gets encoded/decoded, is determined using /// RawValue, which is what gets encoded/decoded, is determined using
/// The Transformer's reverse function. /// The Transformer's reverse function.
public init(transformedValue: Transformer.To) throws { public init(transformedValue: Transformer.To) throws {
self.value = transformedValue self.value = transformedValue
rawValue = try Transformer.reverse(value) rawValue = try Transformer.reverse(value)
} }
} }
extension TransformedAttribute: CustomStringConvertible { extension TransformedAttribute: CustomStringConvertible {
public var description: String { public var description: String {
return "Attribute<\(String(describing: Transformer.From.self)) -> \(String(describing: Transformer.To.self))>(\(String(describing: value)))" return "Attribute<\(String(describing: Transformer.From.self)) -> \(String(describing: Transformer.To.self))>(\(String(describing: value)))"
} }
} }
extension TransformedAttribute: Equatable where Transformer.From: Equatable, Transformer.To: Equatable {} extension TransformedAttribute: Equatable where Transformer.From: Equatable, Transformer.To: Equatable {}
@@ -63,85 +71,86 @@ public typealias ValidatedAttribute<RawValue: Codable, Validator: JSONAPI.Valida
/// An Attribute simply represents a type that can be encoded and decoded. /// An Attribute simply represents a type that can be encoded and decoded.
public struct Attribute<RawValue: Codable>: AttributeType { public struct Attribute<RawValue: Codable>: AttributeType {
let attribute: TransformedAttribute<RawValue, IdentityTransformer<RawValue>> let attribute: TransformedAttribute<RawValue, IdentityTransformer<RawValue>>
public var value: RawValue { public var value: RawValue {
return attribute.value return attribute.value
} }
public init(value: RawValue) { public init(value: RawValue) {
attribute = .init(value: value) attribute = .init(value: value)
} }
} }
extension Attribute: CustomStringConvertible { extension Attribute: CustomStringConvertible {
public var description: String { public var description: String {
return "Attribute<\(String(describing: RawValue.self))>(\(String(describing: value)))" return "Attribute<\(String(describing: RawValue.self))>(\(String(describing: value)))"
} }
} }
extension Attribute: Equatable where RawValue: Equatable {} extension Attribute: Equatable where RawValue: Equatable {}
// MARK: - Codable // MARK: - Codable
extension TransformedAttribute { extension TransformedAttribute {
public init(from decoder: Decoder) throws { public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer() let container = try decoder.singleValueContainer()
let rawVal: RawValue
// A little trickery follows. If the value is nil, the
// container.decode(Value.self) will fail even if Value
// is Optional. However, we can check if decoding nil
// succeeds and then attempt to coerce nil to a Value
// type at which point we can store nil in `value`.
let anyNil: Any? = nil
if container.decodeNil(),
let val = anyNil as? Transformer.From {
rawVal = val
} else {
rawVal = try container.decode(Transformer.From.self)
}
rawValue = rawVal
value = try Transformer.transform(rawVal)
}
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(rawValue) let rawVal: RawValue
}
// A little trickery follows. If the value is nil, the
// container.decode(Value.self) will fail even if Value
// is Optional. However, we can check if decoding nil
// succeeds and then attempt to coerce nil to a Value
// type at which point we can store nil in `value`.
let anyNil: Any? = nil
if container.decodeNil(),
let val = anyNil as? Transformer.From {
rawVal = val
} else {
rawVal = try container.decode(Transformer.From.self)
}
rawValue = rawVal
value = try Transformer.transform(rawVal)
}
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(rawValue)
}
} }
extension Attribute { extension Attribute {
public init(from decoder: Decoder) throws { public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer() let container = try decoder.singleValueContainer()
// A little trickery follows. If the value is nil, the // A little trickery follows. If the value is nil, the
// container.decode(Value.self) will fail even if Value // container.decode(Value.self) will fail even if Value
// is Optional. However, we can check if decoding nil // is Optional. However, we can check if decoding nil
// succeeds and then attempt to coerce nil to a Value // succeeds and then attempt to coerce nil to a Value
// type at which point we can store nil in `value`. // type at which point we can store nil in `value`.
let anyNil: Any? = nil let anyNil: Any? = nil
if container.decodeNil(), if container.decodeNil(),
let val = anyNil as? RawValue { let val = anyNil as? RawValue {
attribute = .init(value: val) attribute = .init(value: val)
} else { } else {
attribute = try container.decode(TransformedAttribute<RawValue, IdentityTransformer<RawValue>>.self) attribute = try container.decode(TransformedAttribute<RawValue, IdentityTransformer<RawValue>>.self)
} }
} }
public func encode(to encoder: Encoder) throws { public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer() var container = encoder.singleValueContainer()
try container.encode(attribute) try container.encode(attribute)
} }
} }
// MARK: Attribute decoding and encoding defaults // MARK: Attribute decoding and encoding defaults
extension AttributeType { extension AttributeType {
public static func defaultDecoding<Container: KeyedDecodingContainerProtocol>(from container: Container, forKey key: Container.Key) throws -> Self { public static func defaultDecoding<Container: KeyedDecodingContainerProtocol>(from container: Container,
return try container.decode(Self.self, forKey: key) forKey key: Container.Key) throws -> Self {
} return try container.decode(Self.self, forKey: key)
}
} }

Some files were not shown because too many files have changed in this diff Show More