From c7b97567a9fb3db03b9af08b68a2fa37bd037727 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Thu, 20 Jun 2019 22:35:55 -0700 Subject: [PATCH 001/235] update swift tools version in package file, add some property wrappers, add some tests for wrappers. its all broken but worth holding onto for now. --- Package.swift | 2 +- Sources/JSONAPI/Resource/Attribute.swift | 4 +- .../JSONAPI/Resource/PropertyWrappers.swift | 115 ++++++++++++++++++ .../Attribute/AttributeTests.swift | 91 +++++++++++++- Tests/JSONAPITests/Entity/EntityTests.swift | 2 +- 5 files changed, 209 insertions(+), 5 deletions(-) create mode 100644 Sources/JSONAPI/Resource/PropertyWrappers.swift diff --git a/Package.swift b/Package.swift index 0789b25..675e093 100644 --- a/Package.swift +++ b/Package.swift @@ -34,5 +34,5 @@ let package = Package( name: "JSONAPITestingTests", dependencies: ["JSONAPI", "JSONAPITesting"]) ], - swiftLanguageVersions: [.v5] + swiftLanguageVersions: [.version("5.1")] ) diff --git a/Sources/JSONAPI/Resource/Attribute.swift b/Sources/JSONAPI/Resource/Attribute.swift index a43c368..c05983d 100644 --- a/Sources/JSONAPI/Resource/Attribute.swift +++ b/Sources/JSONAPI/Resource/Attribute.swift @@ -17,9 +17,9 @@ public protocol AttributeType: Codable { /// A TransformedAttribute takes a Codable type and attempts to turn it into another type. public struct TransformedAttribute: AttributeType where Transformer.From == RawValue { public let rawValue: RawValue - + public let value: Transformer.To - + public init(rawValue: RawValue) throws { self.rawValue = rawValue value = try Transformer.transform(rawValue) diff --git a/Sources/JSONAPI/Resource/PropertyWrappers.swift b/Sources/JSONAPI/Resource/PropertyWrappers.swift new file mode 100644 index 0000000..6c50f18 --- /dev/null +++ b/Sources/JSONAPI/Resource/PropertyWrappers.swift @@ -0,0 +1,115 @@ +// +// PropertyWrappers.swift +// +// +// Created by Mathew Polzin on 6/20/19. +// + + +// MARK: - Transformed +@propertyWrapper +public struct Transformed { + + public typealias RawValue = Transformer.From + public typealias Value = Transformer.To + + private var _value: Value? + + public var wrappedValue: Value { + get { + guard let ret = _value else { + fatalError("Attribute read from before initialization.") + } + return ret + } + set { + _value = newValue + } + } + + public init(initialValue: Value, _ transformer: Transformer.Type) { + self._value = initialValue + } + + public init(_ transformer: Transformer.Type) { + self._value = nil + } + + public init(rawValue: RawValue, _ transformer: Transformer.Type) throws { + self._value = try Transformer.transform(rawValue) + } +} + +extension Transformed: Decodable where Transformer.From: Decodable { + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + + let rawVal = try container.decode(Transformer.From.self) + + _value = try Transformer.transform(rawVal) + } +} + +extension Transformed: Encodable where Transformer: ReversibleTransformer, Transformer.From: Encodable { + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + + guard let value = _value else { + fatalError("Attribute encoded before initialization.") + } + + try container.encode(Transformer.reverse(value)) + } +} + +// MARK: - Nullable + +public protocol _Optional { + static var nilValue: Self { get } + var isNilValue: Bool { get } +} + +extension Optional: _Optional { + public static var nilValue: Self { + return .none + } + + public var isNilValue: Bool { return self == nil } +} + +protocol _Nullable {} + +@propertyWrapper +public struct Nullable: Decodable, _Optional, _Nullable { + public var wrappedValue: T? + + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + + if container.decodeNil() { + wrappedValue = nil + return + } + + wrappedValue = try container.decode(T.self) + } + + public init(initialValue: T? = nil) { + wrappedValue = initialValue + } + + public static var nilValue: Self { + return .init() + } + + public var isNilValue: Bool { + return wrappedValue == nil + } +} + +extension Nullable: Encodable where T: Encodable { + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(wrappedValue) + } +} diff --git a/Tests/JSONAPITests/Attribute/AttributeTests.swift b/Tests/JSONAPITests/Attribute/AttributeTests.swift index 22acd71..a516f36 100644 --- a/Tests/JSONAPITests/Attribute/AttributeTests.swift +++ b/Tests/JSONAPITests/Attribute/AttributeTests.swift @@ -62,6 +62,70 @@ class AttributeTests: XCTestCase { } } +// MARK: Property Wrappers +extension AttributeTests { + func test_Transformed() { + + struct Test: Codable { + @Transformed(IntToString.self) + var value: String = "" + } + + let test = Test(value: "hello") + XCTAssertEqual(test.value, "hello") + + let test2 = try! JSONDecoder().decode(Test.self, + from: #"{"value": 12}"#.data(using: .utf8)!) + + XCTAssertEqual(test2.value, "12") + try! print(String(data: JSONEncoder().encode(test2), encoding: .utf8)!) + + let test3 = try? JSONDecoder().decode(Test.self, + from: #"{"value": null}"#.data(using: .utf8)!) + + XCTAssertNil(test3) + } + + func test_Nullable() { + struct Test: Codable { + @Nullable + var value: String? + } + + let test = Test(value: nil) + XCTAssertNil(test.value) + + let test2 = Test(value: "hello") + XCTAssertEqual(test2.value, "hello") + + let test3 = try! JSONDecoder().decode(Test.self, + from: #"{"value": "world"}"#.data(using: .utf8)!) + + XCTAssertEqual(test3.value, "world") + try! print(String(data: JSONEncoder().encode(test2), encoding: .utf8)!) + + let test4 = try? JSONDecoder().decode(Test.self, + from: #"{"value": null}"#.data(using: .utf8)!) + + XCTAssertNotNil(test4) + XCTAssertNil(test4?.value) + } + + func test_NullableTransformed() { + struct Test: Codable { +// Nullable> + let x: Transformed +// @Nullable @Transformed(IdentityTransformer.self) + @Transformed(IntToString.self) @Nullable + var value: String? + } + + let test = Test(x: .init(initialValue: "12", IntToString.self)) + + print(test.x.wrappedValue) + } +} + // MARK: Test types extension AttributeTests { enum TestTransformer: ReversibleTransformer { @@ -77,12 +141,37 @@ extension AttributeTests { } } - enum IntToString: Transformer { + enum IntToString: ReversibleTransformer { public static func transform(_ from: Int) -> String { return String(from) } + + public static func reverse(_ value: String) throws -> Int { + guard let intValue = Int(value) else { + fatalError("Reversed IntToString with invalid String value.") + } + return intValue + } } + enum OptionalIntToOptionalString: ReversibleTransformer { + public static func transform(_ from: Int?) -> String? { + return from.map(String.init) + } + + public static func reverse(_ value: String?) throws -> Int? { + guard let stringValue = value else { + return nil + } + + guard let intValue = Int(stringValue) else { + fatalError("Reversed IntToString with invalid String value.") + } + + return intValue + } + } + enum IntToInt: Transformer { public static func transform(_ from: Int) -> Int { return from + 100 diff --git a/Tests/JSONAPITests/Entity/EntityTests.swift b/Tests/JSONAPITests/Entity/EntityTests.swift index 18e313e..0110865 100644 --- a/Tests/JSONAPITests/Entity/EntityTests.swift +++ b/Tests/JSONAPITests/Entity/EntityTests.swift @@ -29,7 +29,7 @@ class EntityTests: XCTestCase { let entity1 = TestEntity1(attributes: .none, relationships: .none, meta: .none, links: .none) let entity = TestEntity9(attributes: .none, relationships: .init(one: entity1.pointer, nullableOne: .init(resourceObject: entity1, meta: .none, links: .none), optionalOne: .init(resourceObject: entity1, meta: .none, links: .none), optionalNullableOne: nil, optionalMany: .init(resourceObjects: [entity1, entity1], meta: .none, links: .none)), meta: .none, links: .none) - XCTAssertEqual(entity ~> \.optionalOne, entity1.id) + XCTAssertEqual(entity ~> \.optionalOne, Optional(entity1.id)) } func test_toMany_relationship_operator_access() { From c996e7447c89c7108d3ba64d17e01fc51aa6be34 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Tue, 23 Jul 2019 21:11:59 -0700 Subject: [PATCH 002/235] Update README.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index ea5a6ee..1bfe81e 100644 --- a/README.md +++ b/README.md @@ -462,6 +462,8 @@ A `Meta` struct is totally open-ended. It is described by the **SPEC** as a plac You can specify `NoMetadata` if the part of the document being described should not contain any `Meta`. +If you need to support metadata with structure that is not pre-determined, consider an "Any Codable" type such as that found at https://github.com/Flight-School/AnyCodable. + ### `JSONAPI.Links` A `Links` struct must contain only `Link` properties. Each `Link` property can either be a `URL` or a `URL` and some `Meta`. Each part of the document has some suggested common `Links` to include but generally any link can be included. From 7b5b17918c075ec3b5eecf402c8c3219313aaa29 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Wed, 24 Jul 2019 19:43:11 -0700 Subject: [PATCH 003/235] Update linuxmain, slightly update wording and indentation on example at bottom of README. --- .../Contents.swift | 159 +++++++++--------- README.md | 130 +++++++------- .../JSONAPITestingTests/XCTestManifests.swift | 30 +++- Tests/JSONAPITests/XCTestManifests.swift | 101 +++++++---- Tests/LinuxMain.swift | 4 +- 5 files changed, 241 insertions(+), 183 deletions(-) diff --git a/JSONAPI.playground/Pages/Full Client & Server Example.xcplaygroundpage/Contents.swift b/JSONAPI.playground/Pages/Full Client & Server Example.xcplaygroundpage/Contents.swift index 2e54355..c8cbea1 100644 --- a/JSONAPI.playground/Pages/Full Client & Server Example.xcplaygroundpage/Contents.swift +++ b/JSONAPI.playground/Pages/Full Client & Server Example.xcplaygroundpage/Contents.swift @@ -4,65 +4,66 @@ import Poly // MARK: - Preamble (setup) -// We make String a CreatableRawIdType. This is actually done in +// Make String a CreatableRawIdType. This is actually done in // this Playground's Entities.swift file, so it is commented out here. /* -var GlobalStringId: Int = 0 -extension String: CreatableRawIdType { - public static func unique() -> String { - GlobalStringId += 1 - return String(GlobalStringId) - } -} -*/ - -// We create a typealias given that we do not expect JSON:API Resource + var globalStringId: Int = 0 + extension String: CreatableRawIdType { + public static func unique() -> String { + globalStringId += 1 + return String(globalStringId) + } + } + */ + +// Create a typealias because we do not expect JSON:API Resource // Objects for this particular API to have Metadata or Links associated // with them. We also expect them to have String Identifiers. typealias JSONEntity = JSONAPI.ResourceObject -// Similarly, we create a typealias for unidentified entities. JSON:API +// Similarly, create a typealias for unidentified entities. JSON:API // only allows unidentified entities (i.e. no "id" field) for client // requests that create new entities. In these situations, the server // is expected to assign the new entity a unique ID. typealias UnidentifiedJSONEntity = JSONAPI.ResourceObject -// We create typealiases given that we do not expect JSON:API Relationships -// for this particular API to have Metadata or Links associated -// with them. +// Create relationship typealiases because we do not expect +// JSON:API Relationships for this particular API to have +// Metadata or Links associated with them. typealias ToOneRelationship = JSONAPI.ToOneRelationship typealias ToManyRelationship = JSONAPI.ToManyRelationship -// We create a typealias for a Document given that we do not expect +// Create a typealias for a Document because we do not expect // JSON:API Documents for this particular API to have Metadata, Links, -// useful Errors, or a JSON:API Object (i.e. APIDescription). +// useful Errors, or an APIDescription (The *SPEC* calls this +// "API Description" the "JSON:API Object"). typealias Document = JSONAPI.Document // MARK: Entity Definitions enum AuthorDescription: ResourceObjectDescription { - public static var jsonType: String { return "authors" } + public static var jsonType: String { return "authors" } - public struct Attributes: JSONAPI.Attributes { - public let name: Attribute - } + public struct Attributes: JSONAPI.Attributes { + public let name: Attribute + } - public typealias Relationships = NoRelationships + public typealias Relationships = NoRelationships } typealias Author = JSONEntity enum ArticleDescription: ResourceObjectDescription { - public static var jsonType: String { return "articles" } + public static var jsonType: String { return "articles" } - public struct Attributes: JSONAPI.Attributes { - public let title: Attribute - public let abstract: Attribute - } + public struct Attributes: JSONAPI.Attributes { + public let title: Attribute + public let abstract: Attribute + } - public struct Relationships: JSONAPI.Relationships { - public let author: ToOneRelationship - } + public struct Relationships: JSONAPI.Relationships { + public let author: ToOneRelationship + } } typealias Article = JSONEntity @@ -83,38 +84,38 @@ typealias SingleArticleDocument = Document, NoInclud // that creates a document. Note that this document is the entirety // of a JSON:API response body. func articleDocument(includeAuthor: Bool) -> Either { - // Let's pretend all of this is coming from a database: - - let authorId = Author.Identifier(rawValue: "1234") - - let article = Article(id: .init(rawValue: "5678"), - attributes: .init(title: .init(value: "JSON:API in Swift"), - abstract: .init(value: "Not yet written")), - relationships: .init(author: .init(id: authorId)), - meta: .none, - links: .none) - - let document = SingleArticleDocument(apiDescription: .none, - body: .init(resourceObject: article), - includes: .none, - meta: .none, - links: .none) - - switch includeAuthor { - case false: - return .a(document) - - case true: - let author = Author(id: authorId, - attributes: .init(name: .init(value: "Janice Bluff")), - relationships: .none, - meta: .none, - links: .none) - - let includes: Includes = .init(values: [.init(author)]) - - return .b(document.including(.init(values: [.init(author)]))) - } + // Let's pretend all of this is coming from a database: + + let authorId = Author.Identifier(rawValue: "1234") + + let article = Article(id: .init(rawValue: "5678"), + attributes: .init(title: .init(value: "JSON:API in Swift"), + abstract: .init(value: "Not yet written")), + relationships: .init(author: .init(id: authorId)), + meta: .none, + links: .none) + + let document = SingleArticleDocument(apiDescription: .none, + body: .init(resourceObject: article), + includes: .none, + meta: .none, + links: .none) + + switch includeAuthor { + case false: + return .init(document) + + case true: + let author = Author(id: authorId, + attributes: .init(name: .init(value: "Janice Bluff")), + relationships: .none, + meta: .none, + links: .none) + + let includes: Includes = .init(values: [.init(author)]) + + return .init(document.including(.init(values: [.init(author)]))) + } } let encoder = JSONEncoder() @@ -124,8 +125,8 @@ encoder.outputFormatting = .prettyPrinted let responseBody = articleDocument(includeAuthor: true) let responseData = try! encoder.encode(responseBody) -// Next step would be encoding and setting as the HTTP body of a response. -// we will just print it out instead: +// Next step would be setting the HTTP body of a response. +// We will just print it out instead: print("-----") print(String(data: responseData, encoding: .utf8)!) @@ -139,31 +140,31 @@ print(String(data: otherResponseData, encoding: .utf8)!) // MARK: - Client Pseudo-example enum NetworkError: Swift.Error { - case serverError - case quantityMismatch + case serverError + case quantityMismatch } // Skipping over all the API stuff, here's a chunk of code that will // decode a document. We will assume we have made a request for a // single article including the author. func docode(articleResponseData: Data) throws -> (article: Article, author: Author) { - let decoder = JSONDecoder() - decoder.keyDecodingStrategy = .convertFromSnakeCase + let decoder = JSONDecoder() + decoder.keyDecodingStrategy = .convertFromSnakeCase - let articleDocument = try decoder.decode(SingleArticleDocumentWithIncludes.self, from: articleResponseData) + let articleDocument = try decoder.decode(SingleArticleDocumentWithIncludes.self, from: articleResponseData) - switch articleDocument.body { - case .data(let data): - let authors = data.includes[Author.self] + switch articleDocument.body { + case .data(let data): + let authors = data.includes[Author.self] - guard authors.count == 1 else { - throw NetworkError.quantityMismatch - } + guard authors.count == 1 else { + throw NetworkError.quantityMismatch + } - return (article: data.primary.value, author: authors[0]) - case .errors(let errors, meta: _, links: _): - throw NetworkError.serverError - } + return (article: data.primary.value, author: authors[0]) + case .errors(let errors, meta: _, links: _): + throw NetworkError.serverError + } } let response = try! docode(articleResponseData: responseData) diff --git a/README.md b/README.md index 1bfe81e..9567cb4 100644 --- a/README.md +++ b/README.md @@ -640,38 +640,39 @@ The following serves as a sort of pseudo-example. It skips server/client impleme ### Preamble (Setup shared by server and client) ```swift -// We make String a CreatableRawIdType. -var GlobalStringId: Int = 0 +// Make String a CreatableRawIdType. +var globalStringId: Int = 0 extension String: CreatableRawIdType { public static func unique() -> String { - GlobalStringId += 1 - return String(GlobalStringId) + globalStringId += 1 + return String(globalStringId) } } -// We create a typealias given that we do not expect JSON:API Resource +// Create a typealias because we do not expect JSON:API Resource // Objects for this particular API to have Metadata or Links associated // with them. We also expect them to have String Identifiers. -typealias JSONResourceObject = JSONAPI.ResourceObject +typealias JSONEntity = JSONAPI.ResourceObject -// Similarly, we create a typealias for unidentified resource objects. JSON:API -// only allows unidentified resource objects (i.e. no "id" field) for client -// requests that create new resource objects. In these situations, the server -// is expected to assign the new resource object a unique ID. -typealias UnidentifiedJSONResourceObject = JSONAPI.ResourceObject +// Similarly, create a typealias for unidentified entities. JSON:API +// only allows unidentified entities (i.e. no "id" field) for client +// requests that create new entities. In these situations, the server +// is expected to assign the new entity a unique ID. +typealias UnidentifiedJSONEntity = JSONAPI.ResourceObject -// We create typealiases given that we do not expect JSON:API Relationships -// for this particular API to have Metadata or Links associated -// with them. -typealias ToOneRelationship = JSONAPI.ToOneRelationship -typealias ToManyRelationship = JSONAPI.ToManyRelationship +// Create relationship typealiases because we do not expect +// JSON:API Relationships for this particular API to have +// Metadata or Links associated with them. +typealias ToOneRelationship = JSONAPI.ToOneRelationship +typealias ToManyRelationship = JSONAPI.ToManyRelationship -// We create a typealias for a Document given that we do not expect +// Create a typealias for a Document because we do not expect // JSON:API Documents for this particular API to have Metadata, Links, -// useful Errors, or a JSON:API Object (i.e. APIDescription). +// useful Errors, or an APIDescription (The *SPEC* calls this +// "API Description" the "JSON:API Object"). typealias Document = JSONAPI.Document -// MARK: ResourceObject Definitions +// MARK: Entity Definitions enum AuthorDescription: ResourceObjectDescription { public static var jsonType: String { return "authors" } @@ -683,7 +684,7 @@ enum AuthorDescription: ResourceObjectDescription { public typealias Relationships = NoRelationships } -typealias Author = JSONResourceObject +typealias Author = JSONEntity enum ArticleDescription: ResourceObjectDescription { public static var jsonType: String { return "articles" } @@ -698,7 +699,7 @@ enum ArticleDescription: ResourceObjectDescription { } } -typealias Article = JSONResourceObject +typealias Article = JSONEntity // MARK: Document Definitions @@ -707,47 +708,48 @@ typealias Article = JSONResourceObject typealias SingleArticleDocumentWithIncludes = Document, Include1> // ... and a typealias to represent a document containing one Article and -// not including any related resource objects. +// not including any related entities. typealias SingleArticleDocument = Document, NoIncludes> ``` + ### Server Pseudo-example ```swift // Skipping over all the API and database stuff, here's a chunk of code // that creates a document. Note that this document is the entirety // of a JSON:API response body. func articleDocument(includeAuthor: Bool) -> Either { - // Let's pretend all of this is coming from a database: + // Let's pretend all of this is coming from a database: - let authorId = Author.Identifier(rawValue: "1234") + let authorId = Author.Identifier(rawValue: "1234") - let article = Article(id: .init(rawValue: "5678"), - attributes: .init(title: .init(value: "JSON:API in Swift"), - abstract: .init(value: "Not yet written")), - relationships: .init(author: .init(id: authorId)), - meta: .none, - links: .none) + let article = Article(id: .init(rawValue: "5678"), + attributes: .init(title: .init(value: "JSON:API in Swift"), + abstract: .init(value: "Not yet written")), + relationships: .init(author: .init(id: authorId)), + meta: .none, + links: .none) - let document = SingleArticleDocument(apiDescription: .none, - body: .init(resourceObject: article), - includes: .none, - meta: .none, - links: .none) + let document = SingleArticleDocument(apiDescription: .none, + body: .init(resourceObject: article), + includes: .none, + meta: .none, + links: .none) - switch includeAuthor { - case false: - return .a(document) + switch includeAuthor { + case false: + return .init(document) - case true: - let author = Author(id: authorId, - attributes: .init(name: .init(value: "Janice Bluff")), - relationships: .none, - meta: .none, - links: .none) + case true: + let author = Author(id: authorId, + attributes: .init(name: .init(value: "Janice Bluff")), + relationships: .none, + meta: .none, + links: .none) - let includes: Includes = .init(values: [.init(author)]) + let includes: Includes = .init(values: [.init(author)]) - return .b(document.including(.init(values: [.init(author)]))) - } + return .init(document.including(.init(values: [.init(author)]))) + } } let encoder = JSONEncoder() @@ -757,8 +759,8 @@ encoder.outputFormatting = .prettyPrinted let responseBody = articleDocument(includeAuthor: true) let responseData = try! encoder.encode(responseBody) -// Next step would be encoding and setting as the HTTP body of a response. -// we will just print it out instead: +// Next step would be setting the HTTP body of a response. +// We will just print it out instead: print("-----") print(String(data: responseData, encoding: .utf8)!) @@ -773,31 +775,31 @@ print(String(data: otherResponseData, encoding: .utf8)!) ### Client Pseudo-example ```swift enum NetworkError: Swift.Error { - case serverError - case quantityMismatch + case serverError + case quantityMismatch } // Skipping over all the API stuff, here's a chunk of code that will // decode a document. We will assume we have made a request for a // single article including the author. func docode(articleResponseData: Data) throws -> (article: Article, author: Author) { - let decoder = JSONDecoder() - decoder.keyDecodingStrategy = .convertFromSnakeCase + let decoder = JSONDecoder() + decoder.keyDecodingStrategy = .convertFromSnakeCase - let articleDocument = try decoder.decode(SingleArticleDocumentWithIncludes.self, from: articleResponseData) + let articleDocument = try decoder.decode(SingleArticleDocumentWithIncludes.self, from: articleResponseData) - switch articleDocument.body { - case .data(let data): - let authors = data.includes[Author.self] + switch articleDocument.body { + case .data(let data): + let authors = data.includes[Author.self] - guard authors.count == 1 else { - throw NetworkError.quantityMismatch - } + guard authors.count == 1 else { + throw NetworkError.quantityMismatch + } - return (article: data.primary.value, author: authors[0]) - case .errors(let errors, meta: _, links: _): - throw NetworkError.serverError - } + return (article: data.primary.value, author: authors[0]) + case .errors(let errors, meta: _, links: _): + throw NetworkError.serverError + } } let response = try! docode(articleResponseData: responseData) diff --git a/Tests/JSONAPITestingTests/XCTestManifests.swift b/Tests/JSONAPITestingTests/XCTestManifests.swift index 3da4318..c916df3 100644 --- a/Tests/JSONAPITestingTests/XCTestManifests.swift +++ b/Tests/JSONAPITestingTests/XCTestManifests.swift @@ -1,7 +1,11 @@ +#if !canImport(ObjectiveC) import XCTest extension Attribute_LiteralTests { - static let __allTests = [ + // DO NOT MODIFY: This is autogenerated, use: + // `swift test --generate-linuxmain` + // to regenerate. + static let __allTests__Attribute_LiteralTests = [ ("test_ArrayLiteral", test_ArrayLiteral), ("test_BooleanLiteral", test_BooleanLiteral), ("test_DictionaryLiteral", test_DictionaryLiteral), @@ -32,7 +36,10 @@ extension Attribute_LiteralTests { } extension EntityCheckTests { - static let __allTests = [ + // DO NOT MODIFY: This is autogenerated, use: + // `swift test --generate-linuxmain` + // to regenerate. + static let __allTests__EntityCheckTests = [ ("test_failsWithBadAttribute", test_failsWithBadAttribute), ("test_failsWithBadRelationship", test_failsWithBadRelationship), ("test_failsWithEnumAttributes", test_failsWithEnumAttributes), @@ -42,27 +49,32 @@ extension EntityCheckTests { } extension Id_LiteralTests { - static let __allTests = [ + // DO NOT MODIFY: This is autogenerated, use: + // `swift test --generate-linuxmain` + // to regenerate. + static let __allTests__Id_LiteralTests = [ ("test_IntegerLiteral", test_IntegerLiteral), ("test_StringLiteral", test_StringLiteral), ] } extension Relationship_LiteralTests { - static let __allTests = [ + // DO NOT MODIFY: This is autogenerated, use: + // `swift test --generate-linuxmain` + // to regenerate. + static let __allTests__Relationship_LiteralTests = [ ("test_ArrayLiteral", test_ArrayLiteral), ("test_NilLiteral", test_NilLiteral), ("test_StringLiteral", test_StringLiteral), ] } -#if !os(macOS) public func __allTests() -> [XCTestCaseEntry] { return [ - testCase(Attribute_LiteralTests.__allTests), - testCase(EntityCheckTests.__allTests), - testCase(Id_LiteralTests.__allTests), - testCase(Relationship_LiteralTests.__allTests), + testCase(Attribute_LiteralTests.__allTests__Attribute_LiteralTests), + testCase(EntityCheckTests.__allTests__EntityCheckTests), + testCase(Id_LiteralTests.__allTests__Id_LiteralTests), + testCase(Relationship_LiteralTests.__allTests__Relationship_LiteralTests), ] } #endif diff --git a/Tests/JSONAPITests/XCTestManifests.swift b/Tests/JSONAPITests/XCTestManifests.swift index 32b0c0a..a7b9e90 100644 --- a/Tests/JSONAPITests/XCTestManifests.swift +++ b/Tests/JSONAPITests/XCTestManifests.swift @@ -1,7 +1,11 @@ +#if !canImport(ObjectiveC) import XCTest extension APIDescriptionTests { - static let __allTests = [ + // DO NOT MODIFY: This is autogenerated, use: + // `swift test --generate-linuxmain` + // to regenerate. + static let __allTests__APIDescriptionTests = [ ("test_empty", test_empty), ("test_failsMissingMeta", test_failsMissingMeta), ("test_NoDescriptionString", test_NoDescriptionString), @@ -12,7 +16,10 @@ extension APIDescriptionTests { } extension AttributeTests { - static let __allTests = [ + // DO NOT MODIFY: This is autogenerated, use: + // `swift test --generate-linuxmain` + // to regenerate. + static let __allTests__AttributeTests = [ ("test_AttributeConstructor", test_AttributeConstructor), ("test_EncodedPrimitives", test_EncodedPrimitives), ("test_NullableIsEqualToNonNullableIfNotNil", test_NullableIsEqualToNonNullableIfNotNil), @@ -24,7 +31,10 @@ extension AttributeTests { } extension Attribute_FunctorTests { - static let __allTests = [ + // DO NOT MODIFY: This is autogenerated, use: + // `swift test --generate-linuxmain` + // to regenerate. + static let __allTests__Attribute_FunctorTests = [ ("test_mapGuaranteed", test_mapGuaranteed), ("test_mapOptionalFailure", test_mapOptionalFailure), ("test_mapOptionalSuccess", test_mapOptionalSuccess), @@ -32,7 +42,10 @@ extension Attribute_FunctorTests { } extension ComputedPropertiesTests { - static let __allTests = [ + // DO NOT MODIFY: This is autogenerated, use: + // `swift test --generate-linuxmain` + // to regenerate. + static let __allTests__ComputedPropertiesTests = [ ("test_ComputedAttributeAccess", test_ComputedAttributeAccess), ("test_ComputedNonAttributeAccess", test_ComputedNonAttributeAccess), ("test_ComputedRelationshipAccess", test_ComputedRelationshipAccess), @@ -42,7 +55,10 @@ extension ComputedPropertiesTests { } extension CustomAttributesTests { - static let __allTests = [ + // DO NOT MODIFY: This is autogenerated, use: + // `swift test --generate-linuxmain` + // to regenerate. + static let __allTests__CustomAttributesTests = [ ("test_customDecode", test_customDecode), ("test_customEncode", test_customEncode), ("test_customKeysDecode", test_customKeysDecode), @@ -51,7 +67,10 @@ extension CustomAttributesTests { } extension DocumentTests { - static let __allTests = [ + // DO NOT MODIFY: This is autogenerated, use: + // `swift test --generate-linuxmain` + // to regenerate. + static let __allTests__DocumentTests = [ ("test_errorDocumentFailsWithNoAPIDescription", test_errorDocumentFailsWithNoAPIDescription), ("test_errorDocumentNoMeta", test_errorDocumentNoMeta), ("test_errorDocumentNoMeta_encode", test_errorDocumentNoMeta_encode), @@ -154,7 +173,10 @@ extension DocumentTests { } extension EntityTests { - static let __allTests = [ + // DO NOT MODIFY: This is autogenerated, use: + // `swift test --generate-linuxmain` + // to regenerate. + static let __allTests__EntityTests = [ ("test_copyIdentifiedByType", test_copyIdentifiedByType), ("test_copyIdentifiedByValue", test_copyIdentifiedByValue), ("test_copyWithNewId", test_copyWithNewId), @@ -202,6 +224,7 @@ extension EntityTests { ("test_optional_relationship_operator_access", test_optional_relationship_operator_access), ("test_optionalNullableRelationshipNulled", test_optionalNullableRelationshipNulled), ("test_optionalNullableRelationshipNulled_encode", test_optionalNullableRelationshipNulled_encode), + ("test_optionalNullableRelationshipOmitted", test_optionalNullableRelationshipOmitted), ("test_optionalToMany_relationship_opeartor_access", test_optionalToMany_relationship_opeartor_access), ("test_optionalToManyIsNotOmitted", test_optionalToManyIsNotOmitted), ("test_optionalToManyIsNotOmitted_encode", test_optionalToManyIsNotOmitted_encode), @@ -227,7 +250,10 @@ extension EntityTests { } extension IncludedTests { - static let __allTests = [ + // DO NOT MODIFY: This is autogenerated, use: + // `swift test --generate-linuxmain` + // to regenerate. + static let __allTests__IncludedTests = [ ("test_appending", test_appending), ("test_EightDifferentIncludes", test_EightDifferentIncludes), ("test_EightDifferentIncludes_encode", test_EightDifferentIncludes_encode), @@ -256,7 +282,10 @@ extension IncludedTests { } extension LinksTests { - static let __allTests = [ + // DO NOT MODIFY: This is autogenerated, use: + // `swift test --generate-linuxmain` + // to regenerate. + static let __allTests__LinksTests = [ ("test_linkFailsIfMetaNotFound", test_linkFailsIfMetaNotFound), ("test_linkWithMetadata", test_linkWithMetadata), ("test_linkWithMetadata_encode", test_linkWithMetadata_encode), @@ -270,7 +299,10 @@ extension LinksTests { } extension NonJSONAPIRelatableTests { - static let __allTests = [ + // DO NOT MODIFY: This is autogenerated, use: + // `swift test --generate-linuxmain` + // to regenerate. + static let __allTests__NonJSONAPIRelatableTests = [ ("test_initialization1", test_initialization1), ("test_initialization2_all_relationships_missing", test_initialization2_all_relationships_missing), ("test_initialization2_all_relationships_there", test_initialization2_all_relationships_there), @@ -278,7 +310,10 @@ extension NonJSONAPIRelatableTests { } extension PolyProxyTests { - static let __allTests = [ + // DO NOT MODIFY: This is autogenerated, use: + // `swift test --generate-linuxmain` + // to regenerate. + static let __allTests__PolyProxyTests = [ ("test_AsymmetricEncodeDecodeUserA", test_AsymmetricEncodeDecodeUserA), ("test_AsymmetricEncodeDecodeUserB", test_AsymmetricEncodeDecodeUserB), ("test_generalReasonableness", test_generalReasonableness), @@ -289,7 +324,10 @@ extension PolyProxyTests { } extension PolyTests { - static let __allTests = [ + // DO NOT MODIFY: This is autogenerated, use: + // `swift test --generate-linuxmain` + // to regenerate. + static let __allTests__PolyTests = [ ("test_init_Poly0", test_init_Poly0), ("test_init_Poly1", test_init_Poly1), ("test_init_Poly2", test_init_Poly2), @@ -324,7 +362,10 @@ extension PolyTests { } extension RelationshipTests { - static let __allTests = [ + // DO NOT MODIFY: This is autogenerated, use: + // `swift test --generate-linuxmain` + // to regenerate. + static let __allTests__RelationshipTests = [ ("test_initToManyWithEntities", test_initToManyWithEntities), ("test_initToManyWithRelationships", test_initToManyWithRelationships), ("test_ToManyRelationship", test_ToManyRelationship), @@ -351,7 +392,10 @@ extension RelationshipTests { } extension ResourceBodyTests { - static let __allTests = [ + // DO NOT MODIFY: This is autogenerated, use: + // `swift test --generate-linuxmain` + // to regenerate. + static let __allTests__ResourceBodyTests = [ ("test_initializers", test_initializers), ("test_manyResourceBody", test_manyResourceBody), ("test_manyResourceBody_encode", test_manyResourceBody_encode), @@ -363,23 +407,22 @@ extension ResourceBodyTests { ] } -#if !os(macOS) public func __allTests() -> [XCTestCaseEntry] { return [ - testCase(APIDescriptionTests.__allTests), - testCase(AttributeTests.__allTests), - testCase(Attribute_FunctorTests.__allTests), - testCase(ComputedPropertiesTests.__allTests), - testCase(CustomAttributesTests.__allTests), - testCase(DocumentTests.__allTests), - testCase(EntityTests.__allTests), - testCase(IncludedTests.__allTests), - testCase(LinksTests.__allTests), - testCase(NonJSONAPIRelatableTests.__allTests), - testCase(PolyProxyTests.__allTests), - testCase(PolyTests.__allTests), - testCase(RelationshipTests.__allTests), - testCase(ResourceBodyTests.__allTests), + testCase(APIDescriptionTests.__allTests__APIDescriptionTests), + testCase(AttributeTests.__allTests__AttributeTests), + testCase(Attribute_FunctorTests.__allTests__Attribute_FunctorTests), + testCase(ComputedPropertiesTests.__allTests__ComputedPropertiesTests), + testCase(CustomAttributesTests.__allTests__CustomAttributesTests), + testCase(DocumentTests.__allTests__DocumentTests), + testCase(EntityTests.__allTests__EntityTests), + testCase(IncludedTests.__allTests__IncludedTests), + testCase(LinksTests.__allTests__LinksTests), + testCase(NonJSONAPIRelatableTests.__allTests__NonJSONAPIRelatableTests), + testCase(PolyProxyTests.__allTests__PolyProxyTests), + testCase(PolyTests.__allTests__PolyTests), + testCase(RelationshipTests.__allTests__RelationshipTests), + testCase(ResourceBodyTests.__allTests__ResourceBodyTests), ] } #endif diff --git a/Tests/LinuxMain.swift b/Tests/LinuxMain.swift index 18367a8..450cf0f 100644 --- a/Tests/LinuxMain.swift +++ b/Tests/LinuxMain.swift @@ -1,10 +1,10 @@ import XCTest -import JSONAPITests import JSONAPITestingTests +import JSONAPITests var tests = [XCTestCaseEntry]() -tests += JSONAPITests.__allTests() tests += JSONAPITestingTests.__allTests() +tests += JSONAPITests.__allTests() XCTMain(tests) From 880894d026133bc954a0d56fc749943b55907af1 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Thu, 25 Jul 2019 07:38:21 -0700 Subject: [PATCH 004/235] small fix for example in README --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 9567cb4..993e727 100644 --- a/README.md +++ b/README.md @@ -510,6 +510,8 @@ public enum ResourceObjectDescription2: JSONAPI.ResourceObjectDescription { case wholeOtherThing = "coolProperty" } } + + public typealias Relationships = NoRelationships } ``` From 0144a2ee804ac3dfecaf54dd4f4ce23ef0225060 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Thu, 25 Jul 2019 07:49:17 -0700 Subject: [PATCH 005/235] Remove prior experimentation with property wrappers -- feature was not baked when that experimentation was done. --- .../JSONAPI/Resource/PropertyWrappers.swift | 115 ------------------ .../Attribute/AttributeTests.swift | 64 ---------- 2 files changed, 179 deletions(-) delete mode 100644 Sources/JSONAPI/Resource/PropertyWrappers.swift diff --git a/Sources/JSONAPI/Resource/PropertyWrappers.swift b/Sources/JSONAPI/Resource/PropertyWrappers.swift deleted file mode 100644 index 6c50f18..0000000 --- a/Sources/JSONAPI/Resource/PropertyWrappers.swift +++ /dev/null @@ -1,115 +0,0 @@ -// -// PropertyWrappers.swift -// -// -// Created by Mathew Polzin on 6/20/19. -// - - -// MARK: - Transformed -@propertyWrapper -public struct Transformed { - - public typealias RawValue = Transformer.From - public typealias Value = Transformer.To - - private var _value: Value? - - public var wrappedValue: Value { - get { - guard let ret = _value else { - fatalError("Attribute read from before initialization.") - } - return ret - } - set { - _value = newValue - } - } - - public init(initialValue: Value, _ transformer: Transformer.Type) { - self._value = initialValue - } - - public init(_ transformer: Transformer.Type) { - self._value = nil - } - - public init(rawValue: RawValue, _ transformer: Transformer.Type) throws { - self._value = try Transformer.transform(rawValue) - } -} - -extension Transformed: Decodable where Transformer.From: Decodable { - public init(from decoder: Decoder) throws { - let container = try decoder.singleValueContainer() - - let rawVal = try container.decode(Transformer.From.self) - - _value = try Transformer.transform(rawVal) - } -} - -extension Transformed: Encodable where Transformer: ReversibleTransformer, Transformer.From: Encodable { - public func encode(to encoder: Encoder) throws { - var container = encoder.singleValueContainer() - - guard let value = _value else { - fatalError("Attribute encoded before initialization.") - } - - try container.encode(Transformer.reverse(value)) - } -} - -// MARK: - Nullable - -public protocol _Optional { - static var nilValue: Self { get } - var isNilValue: Bool { get } -} - -extension Optional: _Optional { - public static var nilValue: Self { - return .none - } - - public var isNilValue: Bool { return self == nil } -} - -protocol _Nullable {} - -@propertyWrapper -public struct Nullable: Decodable, _Optional, _Nullable { - public var wrappedValue: T? - - public init(from decoder: Decoder) throws { - let container = try decoder.singleValueContainer() - - if container.decodeNil() { - wrappedValue = nil - return - } - - wrappedValue = try container.decode(T.self) - } - - public init(initialValue: T? = nil) { - wrappedValue = initialValue - } - - public static var nilValue: Self { - return .init() - } - - public var isNilValue: Bool { - return wrappedValue == nil - } -} - -extension Nullable: Encodable where T: Encodable { - public func encode(to encoder: Encoder) throws { - var container = encoder.singleValueContainer() - try container.encode(wrappedValue) - } -} diff --git a/Tests/JSONAPITests/Attribute/AttributeTests.swift b/Tests/JSONAPITests/Attribute/AttributeTests.swift index a516f36..f68831c 100644 --- a/Tests/JSONAPITests/Attribute/AttributeTests.swift +++ b/Tests/JSONAPITests/Attribute/AttributeTests.swift @@ -62,70 +62,6 @@ class AttributeTests: XCTestCase { } } -// MARK: Property Wrappers -extension AttributeTests { - func test_Transformed() { - - struct Test: Codable { - @Transformed(IntToString.self) - var value: String = "" - } - - let test = Test(value: "hello") - XCTAssertEqual(test.value, "hello") - - let test2 = try! JSONDecoder().decode(Test.self, - from: #"{"value": 12}"#.data(using: .utf8)!) - - XCTAssertEqual(test2.value, "12") - try! print(String(data: JSONEncoder().encode(test2), encoding: .utf8)!) - - let test3 = try? JSONDecoder().decode(Test.self, - from: #"{"value": null}"#.data(using: .utf8)!) - - XCTAssertNil(test3) - } - - func test_Nullable() { - struct Test: Codable { - @Nullable - var value: String? - } - - let test = Test(value: nil) - XCTAssertNil(test.value) - - let test2 = Test(value: "hello") - XCTAssertEqual(test2.value, "hello") - - let test3 = try! JSONDecoder().decode(Test.self, - from: #"{"value": "world"}"#.data(using: .utf8)!) - - XCTAssertEqual(test3.value, "world") - try! print(String(data: JSONEncoder().encode(test2), encoding: .utf8)!) - - let test4 = try? JSONDecoder().decode(Test.self, - from: #"{"value": null}"#.data(using: .utf8)!) - - XCTAssertNotNil(test4) - XCTAssertNil(test4?.value) - } - - func test_NullableTransformed() { - struct Test: Codable { -// Nullable> - let x: Transformed -// @Nullable @Transformed(IdentityTransformer.self) - @Transformed(IntToString.self) @Nullable - var value: String? - } - - let test = Test(x: .init(initialValue: "12", IntToString.self)) - - print(test.x.wrappedValue) - } -} - // MARK: Test types extension AttributeTests { enum TestTransformer: ReversibleTransformer { From 9143281290f6b89d121d761520bfe184771d0260 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Thu, 25 Jul 2019 09:12:17 -0700 Subject: [PATCH 006/235] Introduce dynamic member keypath lookup as alternative to existing subscript keypath lookup for attributes. also did a bit of tidying up. --- .../Usage.xcplaygroundpage/Contents.swift | 6 +- JSONAPI.playground/Sources/Entities.swift | 6 +- Sources/JSONAPI/Resource/ResourceObject.swift | 59 ++++++++++++---- .../Attribute/Attribute+FunctorTests.swift | 3 + .../ComputedPropertiesTests.swift | 2 + .../CustomAttributesTests.swift | 4 ++ .../ResourceObjectTests.swift} | 67 +++++++++++++++---- .../stubs/ResourceObjectStubs.swift} | 0 8 files changed, 112 insertions(+), 35 deletions(-) rename Tests/JSONAPITests/{Entity/EntityTests.swift => ResourceObject/ResourceObjectTests.swift} (93%) rename Tests/JSONAPITests/{Entity/stubs/EntityStubs.swift => ResourceObject/stubs/ResourceObjectStubs.swift} (100%) diff --git a/JSONAPI.playground/Pages/Usage.xcplaygroundpage/Contents.swift b/JSONAPI.playground/Pages/Usage.xcplaygroundpage/Contents.swift index 22bb3c0..6850045 100644 --- a/JSONAPI.playground/Pages/Usage.xcplaygroundpage/Contents.swift +++ b/JSONAPI.playground/Pages/Usage.xcplaygroundpage/Contents.swift @@ -49,15 +49,11 @@ print("-----") // MARK: - Pass successfully parsed body to other parts of the code -/* - ---- CRASHING IN XCODE 10.2 PLAYGROUND ---- - if case let .data(bodyData) = peopleResponse.body { - print("first person's name: \(bodyData.primary.values[0][\.fullName])") + print("first person's name: \(bodyData.primary.values[0].fullName)") } else { print("no body data") } - */ // MARK: - Work in the abstract diff --git a/JSONAPI.playground/Sources/Entities.swift b/JSONAPI.playground/Sources/Entities.swift index a62c993..d01f4f8 100644 --- a/JSONAPI.playground/Sources/Entities.swift +++ b/JSONAPI.playground/Sources/Entities.swift @@ -15,11 +15,11 @@ Please enjoy these examples, but allow me the forced casting and the lack of err ********/ // MARK: - String as CreatableRawIdType -var GlobalStringId: Int = 0 +var globalStringId: Int = 0 extension String: CreatableRawIdType { public static func unique() -> String { - GlobalStringId += 1 - return String(GlobalStringId) + globalStringId += 1 + return String(globalStringId) } } diff --git a/Sources/JSONAPI/Resource/ResourceObject.swift b/Sources/JSONAPI/Resource/ResourceObject.swift index 83cef3e..1450c46 100644 --- a/Sources/JSONAPI/Resource/ResourceObject.swift +++ b/Sources/JSONAPI/Resource/ResourceObject.swift @@ -56,8 +56,9 @@ public protocol ResourceObjectProxyDescription: JSONTyped { public protocol ResourceObjectDescription: ResourceObjectProxyDescription where Attributes: JSONAPI.Attributes, Relationships: JSONAPI.Relationships {} /// ResourceObjectProxy is a protocol that can be used to create -/// types that _act_ like Entities but cannot be encoded -/// or decoded as Entities. +/// types that _act_ like ResourceObject but cannot be encoded +/// or decoded as ResourceObjects. +@dynamicMemberLookup public protocol ResourceObjectProxy: Equatable, JSONTyped { associatedtype Description: ResourceObjectProxyDescription associatedtype EntityRawIdType: JSONAPI.MaybeRawId @@ -81,7 +82,7 @@ public protocol ResourceObjectProxy: Equatable, JSONTyped { } extension ResourceObjectProxy { - /// The JSON API compliant "type" of this `Entity`. + /// The JSON API compliant "type" of this `ResourceObject`. public static var jsonType: String { return Description.jsonType } } @@ -141,7 +142,7 @@ extension ResourceObject: CustomStringConvertible { } } -// MARK: Convenience initializers +// MARK: - Convenience initializers extension ResourceObject where EntityRawIdType: CreatableRawIdType { public init(attributes: Description.Attributes, relationships: Description.Relationships, meta: MetaType, links: LinksType) { self.id = ResourceObject.Id() @@ -392,7 +393,7 @@ extension ResourceObject where MetaType == NoMetadata, LinksType == NoLinks, Ent } */ -// MARK: Pointer for Relationships use. +// MARK: - Pointer for Relationships use public extension ResourceObject where EntityRawIdType: JSONAPI.RawIdType { /// An ResourceObject.Pointer is a `ToOneRelationship` with no metadata or links. @@ -416,7 +417,7 @@ public extension ResourceObject where EntityRawIdType: JSONAPI.RawIdType { } } -// MARK: Identifying Unidentified Entities +// MARK: - Identifying Unidentified Entities public extension ResourceObject where EntityRawIdType == Unidentified { /// Create a new ResourceObject from this one with a newly created /// unique Id of the given type. @@ -437,31 +438,55 @@ public extension ResourceObject where EntityRawIdType: CreatableRawIdType { } } -// MARK: Attribute Access +// MARK: - Attribute Access public extension ResourceObjectProxy { + // MARK: Keypath Subscript Lookup /// Access the attribute at the given keypath. This just /// allows you to write `resourceObject[\.propertyName]` instead - /// of `resourceObject.attributes.propertyName`. + /// of `resourceObject.attributes.propertyName.value`. subscript(_ path: KeyPath) -> T.ValueType { return attributes[keyPath: path].value } /// Access the attribute at the given keypath. This just /// allows you to write `resourceObject[\.propertyName]` instead - /// of `resourceObject.attributes.propertyName`. + /// of `resourceObject.attributes.propertyName.value`. subscript(_ path: KeyPath) -> T.ValueType? { return attributes[keyPath: path]?.value } /// Access the attribute at the given keypath. This just /// allows you to write `resourceObject[\.propertyName]` instead - /// of `resourceObject.attributes.propertyName`. + /// of `resourceObject.attributes.propertyName.value`. subscript(_ path: KeyPath) -> U? where T.ValueType == U? { // Implementation Note: Handles Transform that returns optional // type. return attributes[keyPath: path].flatMap { $0.value } } + // MARK: Dynaminc Member Keypath Lookup + /// Access the attribute at the given keypath. This just + /// allows you to write `resourceObject[\.propertyName]` instead + /// of `resourceObject.attributes.propertyName.value`. + subscript(dynamicMember path: KeyPath) -> T.ValueType { + return attributes[keyPath: path].value + } + + /// Access the attribute at the given keypath. This just + /// allows you to write `resourceObject[\.propertyName]` instead + /// of `resourceObject.attributes.propertyName.value`. + subscript(dynamicMember path: KeyPath) -> T.ValueType? { + return attributes[keyPath: path]?.value + } + + /// Access the attribute at the given keypath. This just + /// allows you to write `resourceObject[\.propertyName]` instead + /// of `resourceObject.attributes.propertyName.value`. + subscript(dynamicMember path: KeyPath) -> U? where T.ValueType == U? { + return attributes[keyPath: path].flatMap { $0.value } + } + + // MARK: Direct Keypath Subscript Lookup /// Access the storage of the attribute at the given keypath. This just /// allows you to write `resourceObject[direct: \.propertyName]` instead /// of `resourceObject.attributes.propertyName`. @@ -475,16 +500,24 @@ public extension ResourceObjectProxy { } } -// MARK: Meta-Attribute Access +// MARK: - Meta-Attribute Access public extension ResourceObjectProxy { + // MARK: Keypath Subscript Lookup /// Access an attribute requiring a transformation on the RawValue _and_ /// a secondary transformation on this entity (self). subscript(_ path: KeyPath T>) -> T { return attributes[keyPath: path](self) } + + // MARK: Dynamic Member Keypath Lookup + /// Access an attribute requiring a transformation on the RawValue _and_ + /// a secondary transformation on this entity (self). + subscript(dynamicMember path: KeyPath T>) -> T { + return attributes[keyPath: path](self) + } } -// MARK: Relationship Access +// MARK: - Relationship Access public extension ResourceObjectProxy { /// Access to an Id of a `ToOneRelationship`. /// This allows you to write `resourceObject ~> \.other` instead @@ -526,7 +559,7 @@ public extension ResourceObjectProxy { } } -// MARK: Meta-Relationship Access +// MARK: - Meta-Relationship Access public extension ResourceObjectProxy { /// Access to an Id of a `ToOneRelationship`. /// This allows you to write `resourceObject ~> \.other` instead diff --git a/Tests/JSONAPITests/Attribute/Attribute+FunctorTests.swift b/Tests/JSONAPITests/Attribute/Attribute+FunctorTests.swift index 4386d9f..ab2d7b7 100644 --- a/Tests/JSONAPITests/Attribute/Attribute+FunctorTests.swift +++ b/Tests/JSONAPITests/Attribute/Attribute+FunctorTests.swift @@ -16,6 +16,7 @@ class Attribute_FunctorTests: XCTestCase { XCTAssertNotNil(entity) XCTAssertEqual(entity?[\.computedString], "Frankie2") + XCTAssertEqual(entity?.computedString, "Frankie2") } func test_mapOptionalSuccess() { @@ -24,6 +25,7 @@ class Attribute_FunctorTests: XCTestCase { XCTAssertNotNil(entity) XCTAssertEqual(entity?[\.computedNumber], 22) + XCTAssertEqual(entity?.computedNumber, 22) } func test_mapOptionalFailure() { @@ -32,6 +34,7 @@ class Attribute_FunctorTests: XCTestCase { XCTAssertNotNil(entity) XCTAssertNil(entity?[\.computedNumber]) + XCTAssertNil(entity?.computedNumber) } } diff --git a/Tests/JSONAPITests/Computed Properties/ComputedPropertiesTests.swift b/Tests/JSONAPITests/Computed Properties/ComputedPropertiesTests.swift index c7ebbf4..add302d 100644 --- a/Tests/JSONAPITests/Computed Properties/ComputedPropertiesTests.swift +++ b/Tests/JSONAPITests/Computed Properties/ComputedPropertiesTests.swift @@ -15,6 +15,7 @@ class ComputedPropertiesTests: XCTestCase { XCTAssertEqual(entity.id, "1234") XCTAssertEqual(entity[\.name], "Sarah") + XCTAssertEqual(entity.name, "Sarah") XCTAssertEqual(entity ~> \.other, "5678") XCTAssertNoThrow(try TestType.check(entity)) } @@ -27,6 +28,7 @@ class ComputedPropertiesTests: XCTestCase { let entity = decoded(type: TestType.self, data: computed_property_attribute) XCTAssertEqual(entity[\.computed], "Sarah2") + XCTAssertEqual(entity.computed, "Sarah2") XCTAssertEqual(entity[direct: \.directSecretsOut], "shhhh") } diff --git a/Tests/JSONAPITests/Custom Attributes Tests/CustomAttributesTests.swift b/Tests/JSONAPITests/Custom Attributes Tests/CustomAttributesTests.swift index 7d6e8f0..91e8d8f 100644 --- a/Tests/JSONAPITests/Custom Attributes Tests/CustomAttributesTests.swift +++ b/Tests/JSONAPITests/Custom Attributes Tests/CustomAttributesTests.swift @@ -14,7 +14,9 @@ class CustomAttributesTests: XCTestCase { let entity = decoded(type: CustomAttributeEntity.self, data: customAttributeEntityData) XCTAssertEqual(entity[\.firstName], "Cool") + XCTAssertEqual(entity.firstName, "Cool") XCTAssertEqual(entity[\.name], "Cool Name") + XCTAssertEqual(entity.name, "Cool Name") XCTAssertNoThrow(try CustomAttributeEntity.check(entity)) } @@ -27,7 +29,9 @@ class CustomAttributesTests: XCTestCase { let entity = decoded(type: CustomKeysEntity.self, data: customAttributeEntityData) XCTAssertEqual(entity[\.firstNameSilly], "Cool") + XCTAssertEqual(entity.firstNameSilly, "Cool") XCTAssertEqual(entity[\.lastNameSilly], "Name") + XCTAssertEqual(entity.lastNameSilly, "Name") XCTAssertNoThrow(try CustomKeysEntity.check(entity)) } diff --git a/Tests/JSONAPITests/Entity/EntityTests.swift b/Tests/JSONAPITests/ResourceObject/ResourceObjectTests.swift similarity index 93% rename from Tests/JSONAPITests/Entity/EntityTests.swift rename to Tests/JSONAPITests/ResourceObject/ResourceObjectTests.swift index 5abecae..ba3cd47 100644 --- a/Tests/JSONAPITests/Entity/EntityTests.swift +++ b/Tests/JSONAPITests/ResourceObject/ResourceObjectTests.swift @@ -1,5 +1,5 @@ // -// EntityTests.swift +// ResourceObjectTests.swift // JSONAPITests // // Created by Mathew Polzin on 7/25/18. @@ -9,7 +9,7 @@ import XCTest import JSONAPI import JSONAPITesting -class EntityTests: XCTestCase { +class ResourceObjectTests: XCTestCase { func test_relationship_access() { let entity1 = TestEntity1(attributes: .none, relationships: .none, meta: .none, links: .none) @@ -69,6 +69,7 @@ class EntityTests: XCTestCase { let entity = UnidentifiedTestEntity(attributes: .init(me: "hello"), relationships: .none, meta: .none, links: .none) XCTAssertEqual(entity[\.me], "hello") + XCTAssertEqual(entity.me, "hello") } func test_initialization() { @@ -102,7 +103,7 @@ class EntityTests: XCTestCase { } // MARK: - Identifying entity copies -extension EntityTests { +extension ResourceObjectTests { func test_copyIdentifiedByType() { let unidentifiedEntity = UnidentifiedTestEntity(attributes: .init(me: .init(value: "hello")), relationships: .none, meta: .none, links: .none) @@ -132,7 +133,7 @@ extension EntityTests { } // MARK: - Encode/Decode -extension EntityTests { +extension ResourceObjectTests { func test_EntityNoRelationshipsNoAttributes() { let entity = decoded(type: TestEntity1.self, @@ -157,6 +158,7 @@ extension EntityTests { XCTAssert(type(of: entity.relationships) == NoRelationships.self) XCTAssertEqual(entity[\.floater], 123.321) + XCTAssertEqual(entity.floater, 123.321) XCTAssertNoThrow(try TestEntity5.check(entity)) testEncoded(entity: entity) @@ -189,7 +191,9 @@ extension EntityTests { data: entity_some_relationships_some_attributes) XCTAssertEqual(entity[\.word], "coolio") + XCTAssertEqual(entity.word, "coolio") XCTAssertEqual(entity[\.number], 992299) + XCTAssertEqual(entity.number, 992299) XCTAssertEqual((entity ~> \.other).rawValue, "2DF03B69-4B0A-467F-B52E-B0C9E44FCECF") XCTAssertNoThrow(try TestEntity4.check(entity)) @@ -203,15 +207,18 @@ extension EntityTests { } // MARK: Attribute omission and nullification -extension EntityTests { +extension ResourceObjectTests { func test_entityOneOmittedAttribute() { let entity = decoded(type: TestEntity6.self, data: entity_one_omitted_attribute) XCTAssertEqual(entity[\.here], "Hello") + XCTAssertEqual(entity.here, "Hello") XCTAssertNil(entity[\.maybeHere]) + XCTAssertNil(entity.maybeHere) XCTAssertEqual(entity[\.maybeNull], "World") + XCTAssertEqual(entity.maybeNull, "World") XCTAssertNoThrow(try TestEntity6.check(entity)) testEncoded(entity: entity) @@ -227,8 +234,11 @@ extension EntityTests { data: entity_one_null_attribute) XCTAssertEqual(entity[\.here], "Hello") + XCTAssertEqual(entity.here, "Hello") XCTAssertEqual(entity[\.maybeHere], "World") + XCTAssertEqual(entity.maybeHere, "World") XCTAssertNil(entity[\.maybeNull]) + XCTAssertNil(entity.maybeNull) XCTAssertNoThrow(try TestEntity6.check(entity)) testEncoded(entity: entity) @@ -244,8 +254,11 @@ extension EntityTests { data: entity_all_attributes) XCTAssertEqual(entity[\.here], "Hello") + XCTAssertEqual(entity.here, "Hello") XCTAssertEqual(entity[\.maybeHere], "World") + XCTAssertEqual(entity.maybeHere, "World") XCTAssertEqual(entity[\.maybeNull], "!") + XCTAssertEqual(entity.maybeNull, "!") XCTAssertNoThrow(try TestEntity6.check(entity)) testEncoded(entity: entity) @@ -261,8 +274,11 @@ extension EntityTests { data: entity_one_null_and_one_missing_attribute) XCTAssertEqual(entity[\.here], "Hello") + XCTAssertEqual(entity.here, "Hello") XCTAssertNil(entity[\.maybeHere]) + XCTAssertNil(entity.maybeHere) XCTAssertNil(entity[\.maybeNull]) + XCTAssertNil(entity.maybeNull) XCTAssertNoThrow(try TestEntity6.check(entity)) testEncoded(entity: entity) @@ -283,7 +299,9 @@ extension EntityTests { data: entity_null_optional_nullable_attribute) XCTAssertEqual(entity[\.here], "Hello") + XCTAssertEqual(entity.here, "Hello") XCTAssertNil(entity[\.maybeHereMaybeNull]) + XCTAssertNil(entity.maybeHereMaybeNull) XCTAssertNoThrow(try TestEntity7.check(entity)) testEncoded(entity: entity) @@ -299,7 +317,9 @@ extension EntityTests { data: entity_non_null_optional_nullable_attribute) XCTAssertEqual(entity[\.here], "Hello") + XCTAssertEqual(entity.here, "Hello") XCTAssertEqual(entity[\.maybeHereMaybeNull], "World") + XCTAssertEqual(entity.maybeHereMaybeNull, "World") XCTAssertNoThrow(try TestEntity7.check(entity)) testEncoded(entity: entity) @@ -312,17 +332,23 @@ extension EntityTests { } // MARK: Attribute Transformation -extension EntityTests { +extension ResourceObjectTests { func test_IntToString() { let entity = decoded(type: TestEntity8.self, data: entity_int_to_string_attribute) XCTAssertEqual(entity[\.string], "22") + XCTAssertEqual(entity.string, "22") XCTAssertEqual(entity[\.int], 22) + XCTAssertEqual(entity.int, 22) XCTAssertEqual(entity[\.stringFromInt], "22") + XCTAssertEqual(entity.stringFromInt, "22") XCTAssertEqual(entity[\.plus], 122) + XCTAssertEqual(entity.plus, 122) XCTAssertEqual(entity[\.doubleFromInt], 22.0) + XCTAssertEqual(entity.doubleFromInt, 22.0) XCTAssertEqual(entity[\.nullToString], "nil") + XCTAssertEqual(entity.nullToString, "nil") XCTAssertNoThrow(try TestEntity8.check(entity)) testEncoded(entity: entity) @@ -335,7 +361,7 @@ extension EntityTests { } // MARK: Attribute Validation -extension EntityTests { +extension ResourceObjectTests { func test_IntOver10_success() { XCTAssertNoThrow(decoded(type: TestEntity11.self, data: entity_valid_validated_attribute)) } @@ -350,7 +376,7 @@ extension EntityTests { } // MARK: Relationship omission and nullification -extension EntityTests { +extension ResourceObjectTests { func test_nullableRelationshipNotNullOrOmitted() { let entity = decoded(type: TestEntity9.self, data: entity_optional_not_omitted_relationship) @@ -451,7 +477,7 @@ extension EntityTests { // MARK: Relationships of same type as root entity -extension EntityTests { +extension ResourceObjectTests { func test_RleationshipsOfSameType() { let entity = decoded(type: TestEntity10.self, data: entity_self_ref_relationship) @@ -470,12 +496,13 @@ extension EntityTests { // MARK: Unidentified -extension EntityTests { +extension ResourceObjectTests { func test_UnidentifiedEntity() { let entity = decoded(type: UnidentifiedTestEntity.self, data: entity_unidentified) XCTAssertNil(entity[\.me]) + XCTAssertNil(entity.me) XCTAssertEqual(entity.id, .unidentified) XCTAssertNoThrow(try UnidentifiedTestEntity.check(entity)) @@ -492,6 +519,7 @@ extension EntityTests { data: entity_unidentified_with_attributes) XCTAssertEqual(entity[\.me], "unknown") + XCTAssertEqual(entity.me, "unknown") XCTAssertEqual(entity.id, .unidentified) XCTAssertNoThrow(try UnidentifiedTestEntity.check(entity)) @@ -506,12 +534,13 @@ extension EntityTests { // MARK: With Meta and/or Links -extension EntityTests { +extension ResourceObjectTests { func test_UnidentifiedEntityWithAttributesAndMeta() { let entity = decoded(type: UnidentifiedTestEntityWithMeta.self, data: entity_unidentified_with_attributes_and_meta) XCTAssertEqual(entity[\.me], "unknown") + XCTAssertEqual(entity.me, "unknown") XCTAssertEqual(entity.id, .unidentified) XCTAssertEqual(entity.meta.x, "world") XCTAssertEqual(entity.meta.y, 5) @@ -530,6 +559,7 @@ extension EntityTests { data: entity_unidentified_with_attributes_and_links) XCTAssertEqual(entity[\.me], "unknown") + XCTAssertEqual(entity.me, "unknown") XCTAssertEqual(entity.id, .unidentified) XCTAssertEqual(entity.links.link1, .init(url: "https://image.com/image.png")) XCTAssertNoThrow(try UnidentifiedTestEntityWithLinks.check(entity)) @@ -547,6 +577,7 @@ extension EntityTests { data: entity_unidentified_with_attributes_and_meta_and_links) XCTAssertEqual(entity[\.me], "unknown") + XCTAssertEqual(entity.me, "unknown") XCTAssertEqual(entity.id, .unidentified) XCTAssertEqual(entity.meta.x, "world") XCTAssertEqual(entity.meta.y, 5) @@ -566,7 +597,9 @@ extension EntityTests { data: entity_some_relationships_some_attributes_with_meta) XCTAssertEqual(entity[\.word], "coolio") + XCTAssertEqual(entity.word, "coolio") XCTAssertEqual(entity[\.number], 992299) + XCTAssertEqual(entity.number, 992299) XCTAssertEqual((entity ~> \.other).rawValue, "2DF03B69-4B0A-467F-B52E-B0C9E44FCECF") XCTAssertEqual(entity.meta.x, "world") XCTAssertEqual(entity.meta.y, 5) @@ -585,7 +618,9 @@ extension EntityTests { data: entity_some_relationships_some_attributes_with_links) XCTAssertEqual(entity[\.word], "coolio") + XCTAssertEqual(entity.word, "coolio") XCTAssertEqual(entity[\.number], 992299) + XCTAssertEqual(entity.number, 992299) XCTAssertEqual((entity ~> \.other).rawValue, "2DF03B69-4B0A-467F-B52E-B0C9E44FCECF") XCTAssertEqual(entity.links.link1, .init(url: "https://image.com/image.png")) XCTAssertNoThrow(try TestEntity4WithLinks.check(entity)) @@ -603,7 +638,9 @@ extension EntityTests { data: entity_some_relationships_some_attributes_with_meta_and_links) XCTAssertEqual(entity[\.word], "coolio") + XCTAssertEqual(entity.word, "coolio") XCTAssertEqual(entity[\.number], 992299) + XCTAssertEqual(entity.number, 992299) XCTAssertEqual((entity ~> \.other).rawValue, "2DF03B69-4B0A-467F-B52E-B0C9E44FCECF") XCTAssertEqual(entity.meta.x, "world") XCTAssertEqual(entity.meta.y, 5) @@ -621,7 +658,7 @@ extension EntityTests { // MARK: With a Meta Attribute -extension EntityTests { +extension ResourceObjectTests { func test_MetaEntityAttributeAccessWorks() { let entity1 = TestEntityWithMetaAttribute(id: "even", attributes: .init(), @@ -635,13 +672,15 @@ extension EntityTests { links: .none) XCTAssertEqual(entity1[\.metaAttribute], true) + XCTAssertEqual(entity1.metaAttribute, true) XCTAssertEqual(entity2[\.metaAttribute], false) + XCTAssertEqual(entity2.metaAttribute, false) } } // MARK: With a Meta Relationship -extension EntityTests { +extension ResourceObjectTests { func test_MetaEntityRelationshipAccessWorks() { let entity1 = TestEntityWithMetaRelationship(id: "even", attributes: .none, @@ -654,7 +693,7 @@ extension EntityTests { } // MARK: - Test Types -extension EntityTests { +extension ResourceObjectTests { enum TestEntityType1: ResourceObjectDescription { static var jsonType: String { return "test_entities"} diff --git a/Tests/JSONAPITests/Entity/stubs/EntityStubs.swift b/Tests/JSONAPITests/ResourceObject/stubs/ResourceObjectStubs.swift similarity index 100% rename from Tests/JSONAPITests/Entity/stubs/EntityStubs.swift rename to Tests/JSONAPITests/ResourceObject/stubs/ResourceObjectStubs.swift From 78b9b125921fc1e506f0717fc5f53d2d776a9543 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Thu, 25 Jul 2019 09:23:01 -0700 Subject: [PATCH 007/235] Update README --- README.md | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 993e727..d2859e4 100644 --- a/README.md +++ b/README.md @@ -311,11 +311,18 @@ A resource object that does not have attributes can be described by adding the f typealias Attributes = NoAttributes ``` -`Attributes` can be accessed via the `subscript` operator of the `ResourceObject` type as follows: +As of Swift 5.1, `Attributes` can be accessed via dynamic member keypath lookup as follows: +```swift +let favoriteColor: String = person.favoriteColor +``` + +🗒 `Attributes` can also be accessed via the older `subscript` operator as follows: ```swift let favoriteColor: String = person[\.favoriteColor] ``` +In both cases you retain type-safety, although neither plays particularly nicely with code autocompletion. It is best practice to pick an attribute access syntax and stick with it. At some point in the future the syntax deemed less desirable may be deprecated. + #### `Transformer` Sometimes you need to use a type that does not encode or decode itself in the way you need to represent it as a serialized JSON object. For example, the Swift `Foundation` type `Date` can encode/decode itself to `Double` out of the box, but you might want to represent dates as ISO 8601 compliant `String`s instead. The Foundation library `JSONDecoder` has a setting to make this adjustment, but for the sake of an example, you could create a `Transformer`. @@ -354,7 +361,7 @@ You can also creator `Validators` and `ValidatedAttribute`s. A `Validator` is ju #### Computed `Attribute` -You can add computed properties to your `ResourceObjectDescription.Attributes` struct if you would like to expose attributes that are not explicitly represented by the JSON. These computed properties do not have to be wrapped in `Attribute`, `ValidatedAttribute`, or `TransformedAttribute`. This allows computed attributes to be of types that are not `Codable`. Here's an example of how you might take the `person[\.name]` attribute from the example above and create a `fullName` computed property. +You can add computed properties to your `ResourceObjectDescription.Attributes` struct if you would like to expose attributes that are not explicitly represented by the JSON. These computed properties do not have to be wrapped in `Attribute`, `ValidatedAttribute`, or `TransformedAttribute`. This allows computed attributes to be of types that are not `Codable`. Here's an example of how you might take the `person.name` attribute from the example above and create a `fullName` computed property. ```swift public var fullName: Attribute { @@ -362,7 +369,7 @@ public var fullName: Attribute { } ``` -If your computed property is wrapped in a `AttributeType` then you can still use the default subscript operator to access it (as would be the case with the `person[\.fullName]` example above). However, if you add a property to the `Attributes` `struct` that is not wrapped in an `AttributeType`, you must either access it from its full path (`person.attributes.newThing`) or with the "direct" subscript accessor (`person[direct: \.newThing]`). This keeps the subscript access unambiguous enough for the compiler to be helpful prior to explicitly casting, comparing, or storing the result. +If your computed property is wrapped in a `AttributeType` then you can still use the default subscript operator to access it (as would be the case with the `person.fullName` example above). However, if you add a property to the `Attributes` `struct` that is not wrapped in an `AttributeType`, you must either access it from its full path (`person.attributes.newThing`) or with the "direct" subscript accessor (`person[direct: \.newThing]`). This keeps the subscript access unambiguous enough for the compiler to be helpful prior to explicitly casting, comparing, or storing the result. ### Copying/Mutating `ResourceObjects` `ResourceObject` is a value type, so copying is its default behavior. There are two common mutations you might want to make when copying a `ResourceObject`: @@ -599,7 +606,7 @@ typealias User = JSONAPI.ResourceObject {Value}` where `{ResourceObject}` is the `JSONAPI.ResourceObject` described by the `ResourceObjectDescription` containing the meta-attribute. @@ -620,7 +627,7 @@ enum UserDescription: ResourceObjectDescription { struct Relationships: JSONAPI.Relationships { public var friend: (User) -> User.Identifier { return { user in - return User.Identifier(rawValue: user[\.friend_id]) + return User.Identifier(rawValue: user.friend_id) } } } From 569cec05cfc639f9c0a35da9ebf2b2515db58e74 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Thu, 25 Jul 2019 09:27:56 -0700 Subject: [PATCH 008/235] ditch language version argument in package file that is not accepted by swift test. update linuxmain. --- Package.swift | 6 +- Tests/JSONAPITests/XCTestManifests.swift | 156 +++++++++++------------ 2 files changed, 81 insertions(+), 81 deletions(-) diff --git a/Package.swift b/Package.swift index d4138a9..f88e67d 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.0 +// swift-tools-version:5.1 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription @@ -33,6 +33,6 @@ let package = Package( .testTarget( name: "JSONAPITestingTests", dependencies: ["JSONAPI", "JSONAPITesting"]) - ], - swiftLanguageVersions: [.version("5.1")] + ] +// swiftLanguageVersions: [.version("5.1")] ) diff --git a/Tests/JSONAPITests/XCTestManifests.swift b/Tests/JSONAPITests/XCTestManifests.swift index a7b9e90..2cb8f39 100644 --- a/Tests/JSONAPITests/XCTestManifests.swift +++ b/Tests/JSONAPITests/XCTestManifests.swift @@ -172,83 +172,6 @@ extension DocumentTests { ] } -extension EntityTests { - // DO NOT MODIFY: This is autogenerated, use: - // `swift test --generate-linuxmain` - // to regenerate. - static let __allTests__EntityTests = [ - ("test_copyIdentifiedByType", test_copyIdentifiedByType), - ("test_copyIdentifiedByValue", test_copyIdentifiedByValue), - ("test_copyWithNewId", test_copyWithNewId), - ("test_entityAllAttribute", test_entityAllAttribute), - ("test_entityAllAttribute_encode", test_entityAllAttribute_encode), - ("test_entityBrokenNullableOmittedAttribute", test_entityBrokenNullableOmittedAttribute), - ("test_EntityNoRelationshipsNoAttributes", test_EntityNoRelationshipsNoAttributes), - ("test_EntityNoRelationshipsNoAttributes_encode", test_EntityNoRelationshipsNoAttributes_encode), - ("test_EntityNoRelationshipsSomeAttributes", test_EntityNoRelationshipsSomeAttributes), - ("test_EntityNoRelationshipsSomeAttributes_encode", test_EntityNoRelationshipsSomeAttributes_encode), - ("test_entityOneNullAndOneOmittedAttribute", test_entityOneNullAndOneOmittedAttribute), - ("test_entityOneNullAndOneOmittedAttribute_encode", test_entityOneNullAndOneOmittedAttribute_encode), - ("test_entityOneNullAttribute", test_entityOneNullAttribute), - ("test_entityOneNullAttribute_encode", test_entityOneNullAttribute_encode), - ("test_entityOneOmittedAttribute", test_entityOneOmittedAttribute), - ("test_entityOneOmittedAttribute_encode", test_entityOneOmittedAttribute_encode), - ("test_EntitySomeRelationshipsNoAttributes", test_EntitySomeRelationshipsNoAttributes), - ("test_EntitySomeRelationshipsNoAttributes_encode", test_EntitySomeRelationshipsNoAttributes_encode), - ("test_EntitySomeRelationshipsSomeAttributes", test_EntitySomeRelationshipsSomeAttributes), - ("test_EntitySomeRelationshipsSomeAttributes_encode", test_EntitySomeRelationshipsSomeAttributes_encode), - ("test_EntitySomeRelationshipsSomeAttributesWithLinks", test_EntitySomeRelationshipsSomeAttributesWithLinks), - ("test_EntitySomeRelationshipsSomeAttributesWithLinks_encode", test_EntitySomeRelationshipsSomeAttributesWithLinks_encode), - ("test_EntitySomeRelationshipsSomeAttributesWithMeta", test_EntitySomeRelationshipsSomeAttributesWithMeta), - ("test_EntitySomeRelationshipsSomeAttributesWithMeta_encode", test_EntitySomeRelationshipsSomeAttributesWithMeta_encode), - ("test_EntitySomeRelationshipsSomeAttributesWithMetaAndLinks", test_EntitySomeRelationshipsSomeAttributesWithMetaAndLinks), - ("test_EntitySomeRelationshipsSomeAttributesWithMetaAndLinks_encode", test_EntitySomeRelationshipsSomeAttributesWithMetaAndLinks_encode), - ("test_initialization", test_initialization), - ("test_IntOver10_encode", test_IntOver10_encode), - ("test_IntOver10_failure", test_IntOver10_failure), - ("test_IntOver10_success", test_IntOver10_success), - ("test_IntToString", test_IntToString), - ("test_IntToString_encode", test_IntToString_encode), - ("test_MetaEntityAttributeAccessWorks", test_MetaEntityAttributeAccessWorks), - ("test_MetaEntityRelationshipAccessWorks", test_MetaEntityRelationshipAccessWorks), - ("test_NonNullOptionalNullableAttribute", test_NonNullOptionalNullableAttribute), - ("test_NonNullOptionalNullableAttribute_encode", test_NonNullOptionalNullableAttribute_encode), - ("test_nullableRelationshipIsNull", test_nullableRelationshipIsNull), - ("test_nullableRelationshipIsNull_encode", test_nullableRelationshipIsNull_encode), - ("test_nullableRelationshipNotNull", test_nullableRelationshipNotNull), - ("test_nullableRelationshipNotNull_encode", test_nullableRelationshipNotNull_encode), - ("test_nullableRelationshipNotNullOrOmitted", test_nullableRelationshipNotNullOrOmitted), - ("test_nullableRelationshipNotNullOrOmitted_encode", test_nullableRelationshipNotNullOrOmitted_encode), - ("test_NullOptionalNullableAttribute", test_NullOptionalNullableAttribute), - ("test_NullOptionalNullableAttribute_encode", test_NullOptionalNullableAttribute_encode), - ("test_optional_relationship_operator_access", test_optional_relationship_operator_access), - ("test_optionalNullableRelationshipNulled", test_optionalNullableRelationshipNulled), - ("test_optionalNullableRelationshipNulled_encode", test_optionalNullableRelationshipNulled_encode), - ("test_optionalNullableRelationshipOmitted", test_optionalNullableRelationshipOmitted), - ("test_optionalToMany_relationship_opeartor_access", test_optionalToMany_relationship_opeartor_access), - ("test_optionalToManyIsNotOmitted", test_optionalToManyIsNotOmitted), - ("test_optionalToManyIsNotOmitted_encode", test_optionalToManyIsNotOmitted_encode), - ("test_pointerWithMetaAndLinks", test_pointerWithMetaAndLinks), - ("test_relationship_access", test_relationship_access), - ("test_relationship_operator_access", test_relationship_operator_access), - ("test_relationshipIds", test_relationshipIds), - ("test_RleationshipsOfSameType", test_RleationshipsOfSameType), - ("test_RleationshipsOfSameType_encode", test_RleationshipsOfSameType_encode), - ("test_toMany_relationship_operator_access", test_toMany_relationship_operator_access), - ("test_UnidentifiedEntity", test_UnidentifiedEntity), - ("test_UnidentifiedEntity_encode", test_UnidentifiedEntity_encode), - ("test_unidentifiedEntityAttributeAccess", test_unidentifiedEntityAttributeAccess), - ("test_UnidentifiedEntityWithAttributes", test_UnidentifiedEntityWithAttributes), - ("test_UnidentifiedEntityWithAttributes_encode", test_UnidentifiedEntityWithAttributes_encode), - ("test_UnidentifiedEntityWithAttributesAndLinks", test_UnidentifiedEntityWithAttributesAndLinks), - ("test_UnidentifiedEntityWithAttributesAndLinks_encode", test_UnidentifiedEntityWithAttributesAndLinks_encode), - ("test_UnidentifiedEntityWithAttributesAndMeta", test_UnidentifiedEntityWithAttributesAndMeta), - ("test_UnidentifiedEntityWithAttributesAndMeta_encode", test_UnidentifiedEntityWithAttributesAndMeta_encode), - ("test_UnidentifiedEntityWithAttributesAndMetaAndLinks", test_UnidentifiedEntityWithAttributesAndMetaAndLinks), - ("test_UnidentifiedEntityWithAttributesAndMetaAndLinks_encode", test_UnidentifiedEntityWithAttributesAndMetaAndLinks_encode), - ] -} - extension IncludedTests { // DO NOT MODIFY: This is autogenerated, use: // `swift test --generate-linuxmain` @@ -407,6 +330,83 @@ extension ResourceBodyTests { ] } +extension ResourceObjectTests { + // DO NOT MODIFY: This is autogenerated, use: + // `swift test --generate-linuxmain` + // to regenerate. + static let __allTests__ResourceObjectTests = [ + ("test_copyIdentifiedByType", test_copyIdentifiedByType), + ("test_copyIdentifiedByValue", test_copyIdentifiedByValue), + ("test_copyWithNewId", test_copyWithNewId), + ("test_entityAllAttribute", test_entityAllAttribute), + ("test_entityAllAttribute_encode", test_entityAllAttribute_encode), + ("test_entityBrokenNullableOmittedAttribute", test_entityBrokenNullableOmittedAttribute), + ("test_EntityNoRelationshipsNoAttributes", test_EntityNoRelationshipsNoAttributes), + ("test_EntityNoRelationshipsNoAttributes_encode", test_EntityNoRelationshipsNoAttributes_encode), + ("test_EntityNoRelationshipsSomeAttributes", test_EntityNoRelationshipsSomeAttributes), + ("test_EntityNoRelationshipsSomeAttributes_encode", test_EntityNoRelationshipsSomeAttributes_encode), + ("test_entityOneNullAndOneOmittedAttribute", test_entityOneNullAndOneOmittedAttribute), + ("test_entityOneNullAndOneOmittedAttribute_encode", test_entityOneNullAndOneOmittedAttribute_encode), + ("test_entityOneNullAttribute", test_entityOneNullAttribute), + ("test_entityOneNullAttribute_encode", test_entityOneNullAttribute_encode), + ("test_entityOneOmittedAttribute", test_entityOneOmittedAttribute), + ("test_entityOneOmittedAttribute_encode", test_entityOneOmittedAttribute_encode), + ("test_EntitySomeRelationshipsNoAttributes", test_EntitySomeRelationshipsNoAttributes), + ("test_EntitySomeRelationshipsNoAttributes_encode", test_EntitySomeRelationshipsNoAttributes_encode), + ("test_EntitySomeRelationshipsSomeAttributes", test_EntitySomeRelationshipsSomeAttributes), + ("test_EntitySomeRelationshipsSomeAttributes_encode", test_EntitySomeRelationshipsSomeAttributes_encode), + ("test_EntitySomeRelationshipsSomeAttributesWithLinks", test_EntitySomeRelationshipsSomeAttributesWithLinks), + ("test_EntitySomeRelationshipsSomeAttributesWithLinks_encode", test_EntitySomeRelationshipsSomeAttributesWithLinks_encode), + ("test_EntitySomeRelationshipsSomeAttributesWithMeta", test_EntitySomeRelationshipsSomeAttributesWithMeta), + ("test_EntitySomeRelationshipsSomeAttributesWithMeta_encode", test_EntitySomeRelationshipsSomeAttributesWithMeta_encode), + ("test_EntitySomeRelationshipsSomeAttributesWithMetaAndLinks", test_EntitySomeRelationshipsSomeAttributesWithMetaAndLinks), + ("test_EntitySomeRelationshipsSomeAttributesWithMetaAndLinks_encode", test_EntitySomeRelationshipsSomeAttributesWithMetaAndLinks_encode), + ("test_initialization", test_initialization), + ("test_IntOver10_encode", test_IntOver10_encode), + ("test_IntOver10_failure", test_IntOver10_failure), + ("test_IntOver10_success", test_IntOver10_success), + ("test_IntToString", test_IntToString), + ("test_IntToString_encode", test_IntToString_encode), + ("test_MetaEntityAttributeAccessWorks", test_MetaEntityAttributeAccessWorks), + ("test_MetaEntityRelationshipAccessWorks", test_MetaEntityRelationshipAccessWorks), + ("test_NonNullOptionalNullableAttribute", test_NonNullOptionalNullableAttribute), + ("test_NonNullOptionalNullableAttribute_encode", test_NonNullOptionalNullableAttribute_encode), + ("test_nullableRelationshipIsNull", test_nullableRelationshipIsNull), + ("test_nullableRelationshipIsNull_encode", test_nullableRelationshipIsNull_encode), + ("test_nullableRelationshipNotNull", test_nullableRelationshipNotNull), + ("test_nullableRelationshipNotNull_encode", test_nullableRelationshipNotNull_encode), + ("test_nullableRelationshipNotNullOrOmitted", test_nullableRelationshipNotNullOrOmitted), + ("test_nullableRelationshipNotNullOrOmitted_encode", test_nullableRelationshipNotNullOrOmitted_encode), + ("test_NullOptionalNullableAttribute", test_NullOptionalNullableAttribute), + ("test_NullOptionalNullableAttribute_encode", test_NullOptionalNullableAttribute_encode), + ("test_optional_relationship_operator_access", test_optional_relationship_operator_access), + ("test_optionalNullableRelationshipNulled", test_optionalNullableRelationshipNulled), + ("test_optionalNullableRelationshipNulled_encode", test_optionalNullableRelationshipNulled_encode), + ("test_optionalNullableRelationshipOmitted", test_optionalNullableRelationshipOmitted), + ("test_optionalToMany_relationship_opeartor_access", test_optionalToMany_relationship_opeartor_access), + ("test_optionalToManyIsNotOmitted", test_optionalToManyIsNotOmitted), + ("test_optionalToManyIsNotOmitted_encode", test_optionalToManyIsNotOmitted_encode), + ("test_pointerWithMetaAndLinks", test_pointerWithMetaAndLinks), + ("test_relationship_access", test_relationship_access), + ("test_relationship_operator_access", test_relationship_operator_access), + ("test_relationshipIds", test_relationshipIds), + ("test_RleationshipsOfSameType", test_RleationshipsOfSameType), + ("test_RleationshipsOfSameType_encode", test_RleationshipsOfSameType_encode), + ("test_toMany_relationship_operator_access", test_toMany_relationship_operator_access), + ("test_UnidentifiedEntity", test_UnidentifiedEntity), + ("test_UnidentifiedEntity_encode", test_UnidentifiedEntity_encode), + ("test_unidentifiedEntityAttributeAccess", test_unidentifiedEntityAttributeAccess), + ("test_UnidentifiedEntityWithAttributes", test_UnidentifiedEntityWithAttributes), + ("test_UnidentifiedEntityWithAttributes_encode", test_UnidentifiedEntityWithAttributes_encode), + ("test_UnidentifiedEntityWithAttributesAndLinks", test_UnidentifiedEntityWithAttributesAndLinks), + ("test_UnidentifiedEntityWithAttributesAndLinks_encode", test_UnidentifiedEntityWithAttributesAndLinks_encode), + ("test_UnidentifiedEntityWithAttributesAndMeta", test_UnidentifiedEntityWithAttributesAndMeta), + ("test_UnidentifiedEntityWithAttributesAndMeta_encode", test_UnidentifiedEntityWithAttributesAndMeta_encode), + ("test_UnidentifiedEntityWithAttributesAndMetaAndLinks", test_UnidentifiedEntityWithAttributesAndMetaAndLinks), + ("test_UnidentifiedEntityWithAttributesAndMetaAndLinks_encode", test_UnidentifiedEntityWithAttributesAndMetaAndLinks_encode), + ] +} + public func __allTests() -> [XCTestCaseEntry] { return [ testCase(APIDescriptionTests.__allTests__APIDescriptionTests), @@ -415,7 +415,6 @@ public func __allTests() -> [XCTestCaseEntry] { testCase(ComputedPropertiesTests.__allTests__ComputedPropertiesTests), testCase(CustomAttributesTests.__allTests__CustomAttributesTests), testCase(DocumentTests.__allTests__DocumentTests), - testCase(EntityTests.__allTests__EntityTests), testCase(IncludedTests.__allTests__IncludedTests), testCase(LinksTests.__allTests__LinksTests), testCase(NonJSONAPIRelatableTests.__allTests__NonJSONAPIRelatableTests), @@ -423,6 +422,7 @@ public func __allTests() -> [XCTestCaseEntry] { testCase(PolyTests.__allTests__PolyTests), testCase(RelationshipTests.__allTests__RelationshipTests), testCase(ResourceBodyTests.__allTests__ResourceBodyTests), + testCase(ResourceObjectTests.__allTests__ResourceObjectTests), ] } #endif From fb72817de6eed8976b0a6eb7ccbc849f578395d9 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Thu, 25 Jul 2019 09:36:35 -0700 Subject: [PATCH 009/235] bump swift version badge to swift 5.1 --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index d2859e4..2613c5a 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,11 @@ # JSONAPI -[![MIT license](http://img.shields.io/badge/license-MIT-lightgrey.svg)](http://opensource.org/licenses/MIT) [![Swift 5.0](http://img.shields.io/badge/Swift-5.0-blue.svg)](https://swift.org) [![Build Status](https://app.bitrise.io/app/c8295b9589aa401e/status.svg?token=vzcyqWD5bQ4xqQfZsaVzNw&branch=master)](https://app.bitrise.io/app/c8295b9589aa401e) +[![MIT license](http://img.shields.io/badge/license-MIT-lightgrey.svg)](http://opensource.org/licenses/MIT) [![Swift 5.1](http://img.shields.io/badge/Swift-5.1-blue.svg)](https://swift.org) [![Build Status](https://app.bitrise.io/app/c8295b9589aa401e/status.svg?token=vzcyqWD5bQ4xqQfZsaVzNw&branch=master)](https://app.bitrise.io/app/c8295b9589aa401e) A Swift package for encoding to- and decoding from **JSON API** compliant requests and responses. See the JSON API Spec here: https://jsonapi.org/format/ -:warning: Although I find the type-safety of this framework appealing, the Swift compiler currently has enough trouble with it that it can become difficult to reason about errors produced by small typos. Similarly, auto-complete fails to provide reasonable suggestions much of the time. If you get the code right, everything compiles, otherwise it can suck to figure out what is wrong. This is mostly a concern when creating resource objects in-code (servers and test suites must do this). Writing a client that uses this framework to ingest JSON API Compliant API responses is much less painful. :warning: +:warning: Although I find the type-safety of this framework appealing, the Swift compiler currently has enough trouble with it that it can become difficult to reason about errors produced by small typos. Similarly, auto-complete fails to provide reasonable suggestions much of the time. If you get the code right, everything compiles, otherwise it can suck to figure out what is wrong. This is mostly a concern when creating resource objects in-code (servers and test suites must do this). Writing a client that uses this framework to ingest JSON API Compliant API responses is much less painful. Note that this is a compile-time concern -- test coverage of this library's behavior is very good. :warning: ## Table of Contents From 4d3597ef0e31cf19f845c34ad2fe6296476eefc8 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Sat, 27 Jul 2019 21:30:14 -0700 Subject: [PATCH 010/235] Update README.md --- README.md | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 993e727..de493ae 100644 --- a/README.md +++ b/README.md @@ -813,10 +813,18 @@ print(response.author) ``` # JSONAPI+Testing -The `JSONAPI` framework is packaged with a test library to help you test your `JSONAPI` integration. The test library is called `JSONAPITesting`. It provides literal expressibility for `Attribute`, `ToOneRelationship`, and `Id` in many situations so that you can easily write test `ResourceObject` values into your unit tests. It also provides a `check()` function for each `ResourceObject` type that can be used to catch problems with your `JSONAPI` structures that are not caught by Swift's type system. You can see the `JSONAPITesting` in action in the Playground included with the `JSONAPI` repository. +The `JSONAPI` framework is packaged with a test library to help you test your `JSONAPI` integration. + +The test library is called `JSONAPITesting`. It provides literal expressibility for `Attribute`, `ToOneRelationship`, and `Id` in many situations so that you can easily write test `ResourceObject` values into your unit tests. It also provides a `check()` function for each `ResourceObject` type that can be used to catch problems with your `JSONAPI` structures that are not caught by Swift's type system. + +You can see the `JSONAPITesting` in action in the Playground included with the `JSONAPI` repository. # JSONAPI+Arbitrary -This library has moved into its own Package. See https://github.com/mattpolzin/JSONAPI-Arbitrary +This library has moved into its own Package. See https://github.com/mattpolzin/JSONAPI-Arbitrary for more information. # JSONAPI+OpenAPI -This library has moved into its own Package. See https://github.com/mattpolzin/JSONAPI-OpenAPI +The `JSONAPI+OpenAPI` library generates OpenAPI compliant JSON Schema for models built with the `JSONAPI` library. If your Swift code is your preferred source of truth for API information, this is an easy way to document the response schemas of your API. + +`JSONAPI+OpenAPI` also has experimental support for generating `JSONAPI` Swift code from Open API documentation (this currently lives on the `feature/gen-swift` branch). + +See https://github.com/mattpolzin/JSONAPI-OpenAPI for more information. From c4f96c0376f27447d089be6530d6d1b0fd12e547 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Sun, 28 Jul 2019 13:10:42 -0700 Subject: [PATCH 011/235] Updated example in playground/documentation after realizing I was not using a variable I had created. --- .../Contents.swift | 2 +- README.md | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/JSONAPI.playground/Pages/Full Client & Server Example.xcplaygroundpage/Contents.swift b/JSONAPI.playground/Pages/Full Client & Server Example.xcplaygroundpage/Contents.swift index c8cbea1..e68aaa6 100644 --- a/JSONAPI.playground/Pages/Full Client & Server Example.xcplaygroundpage/Contents.swift +++ b/JSONAPI.playground/Pages/Full Client & Server Example.xcplaygroundpage/Contents.swift @@ -114,7 +114,7 @@ func articleDocument(includeAuthor: Bool) -> Either = .init(values: [.init(author)]) - return .init(document.including(.init(values: [.init(author)]))) + return .init(document.including(includes)) } } diff --git a/README.md b/README.md index de493ae..f00fafd 100644 --- a/README.md +++ b/README.md @@ -750,7 +750,7 @@ func articleDocument(includeAuthor: Bool) -> Either = .init(values: [.init(author)]) - return .init(document.including(.init(values: [.init(author)]))) + return .init(document.including(includes)) } } @@ -813,9 +813,9 @@ print(response.author) ``` # JSONAPI+Testing -The `JSONAPI` framework is packaged with a test library to help you test your `JSONAPI` integration. +The `JSONAPI` framework is packaged with a test library to help you test your `JSONAPI` integration. -The test library is called `JSONAPITesting`. It provides literal expressibility for `Attribute`, `ToOneRelationship`, and `Id` in many situations so that you can easily write test `ResourceObject` values into your unit tests. It also provides a `check()` function for each `ResourceObject` type that can be used to catch problems with your `JSONAPI` structures that are not caught by Swift's type system. +The test library is called `JSONAPITesting`. It provides literal expressibility for `Attribute`, `ToOneRelationship`, and `Id` in many situations so that you can easily write test `ResourceObject` values into your unit tests. It also provides a `check()` function for each `ResourceObject` type that can be used to catch problems with your `JSONAPI` structures that are not caught by Swift's type system. You can see the `JSONAPITesting` in action in the Playground included with the `JSONAPI` repository. @@ -823,8 +823,8 @@ You can see the `JSONAPITesting` in action in the Playground included with the ` This library has moved into its own Package. See https://github.com/mattpolzin/JSONAPI-Arbitrary for more information. # JSONAPI+OpenAPI -The `JSONAPI+OpenAPI` library generates OpenAPI compliant JSON Schema for models built with the `JSONAPI` library. If your Swift code is your preferred source of truth for API information, this is an easy way to document the response schemas of your API. +The `JSONAPI+OpenAPI` library generates OpenAPI compliant JSON Schema for models built with the `JSONAPI` library. If your Swift code is your preferred source of truth for API information, this is an easy way to document the response schemas of your API. -`JSONAPI+OpenAPI` also has experimental support for generating `JSONAPI` Swift code from Open API documentation (this currently lives on the `feature/gen-swift` branch). +`JSONAPI+OpenAPI` also has experimental support for generating `JSONAPI` Swift code from Open API documentation (this currently lives on the `feature/gen-swift` branch). See https://github.com/mattpolzin/JSONAPI-OpenAPI for more information. From f5eb343bd452c4fc3a9e0f6793247162a17817c3 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Mon, 29 Jul 2019 20:41:22 -0700 Subject: [PATCH 012/235] Complete test coverage of APIDescription --- .../APIDescription/APIDescriptionTests.swift | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/Tests/JSONAPITests/APIDescription/APIDescriptionTests.swift b/Tests/JSONAPITests/APIDescription/APIDescriptionTests.swift index d5706b6..9640352 100644 --- a/Tests/JSONAPITests/APIDescription/APIDescriptionTests.swift +++ b/Tests/JSONAPITests/APIDescription/APIDescriptionTests.swift @@ -10,6 +10,17 @@ import JSONAPI class APIDescriptionTests: XCTestCase { + func test_init() { + let _ = APIDescription(version: "hello", + meta: .none) + let _ = APIDescription(version: "world", + meta: .init(hello: "there", + number: 2)) + let _ = NoAPIDescription() + + XCTAssertEqual(NoAPIDescription(), NoAPIDescription.none) + } + func test_NoDescriptionString() { XCTAssertEqual(String(describing: NoAPIDescription()), "No JSON:API Object") } @@ -18,12 +29,18 @@ class APIDescriptionTests: XCTestCase { let description = decoded(type: APIDescription.self, data: api_description_empty) XCTAssertEqual(description.version, "1.0") + + test_DecodeEncodeEquality(type: APIDescription.self, + data: api_description_empty) } func test_WithVersion() { let description = decoded(type: APIDescription.self, data: api_description_with_version) XCTAssertEqual(description.version, "1.5") + + test_DecodeEncodeEquality(type: APIDescription.self, + data: api_description_with_version) } func test_WithMeta() { @@ -32,6 +49,9 @@ class APIDescriptionTests: XCTestCase { XCTAssertEqual(description.version, "1.0") XCTAssertEqual(description.meta.hello, "world") XCTAssertEqual(description.meta.number, 10) + + test_DecodeEncodeEquality(type: APIDescription.self, + data: api_description_with_meta) } func test_WithVersionAndMeta() { @@ -40,6 +60,9 @@ class APIDescriptionTests: XCTestCase { XCTAssertEqual(description.version, "2.0") XCTAssertEqual(description.meta.hello, "world") XCTAssertEqual(description.meta.number, 10) + + test_DecodeEncodeEquality(type: APIDescription.self, + data: api_description_with_version_and_meta) } func test_failsMissingMeta() { From 396f8453d169b39873bb29a0ec339804ec60c1ab Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Mon, 29 Jul 2019 21:00:44 -0700 Subject: [PATCH 013/235] swap out broken nil checks (not harming anything, but not working as evidenced by not getting hit by any test cases) --- Sources/JSONAPI/Document/ResourceBody.swift | 10 ++++++---- Sources/JSONAPI/Resource/Relationship.swift | 4 ---- .../JSONAPITests/ResourceBody/ResourceBodyTests.swift | 3 +++ 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/Sources/JSONAPI/Document/ResourceBody.swift b/Sources/JSONAPI/Document/ResourceBody.swift index eed341e..b8f170c 100644 --- a/Sources/JSONAPI/Document/ResourceBody.swift +++ b/Sources/JSONAPI/Document/ResourceBody.swift @@ -71,10 +71,12 @@ extension SingleResourceBody { public func encode(to encoder: Encoder) throws { var container = encoder.singleValueContainer() - if (value as Any?) == nil { - try container.encodeNil() - return - } + let anyNil: Any? = nil + let nilValue = anyNil as? Entity + guard value != nilValue else { + try container.encodeNil() + return + } try container.encode(value) } diff --git a/Sources/JSONAPI/Resource/Relationship.swift b/Sources/JSONAPI/Resource/Relationship.swift index f6a4bdc..43f5457 100644 --- a/Sources/JSONAPI/Resource/Relationship.swift +++ b/Sources/JSONAPI/Resource/Relationship.swift @@ -190,10 +190,6 @@ extension ToOneRelationship: Codable where Identifiable.Identifier: OptionalId { public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: ResourceLinkageCodingKeys.self) - if (id as Any?) == nil { - try container.encodeNil(forKey: .data) - } - if MetaType.self != NoMetadata.self { try container.encode(meta, forKey: .meta) } diff --git a/Tests/JSONAPITests/ResourceBody/ResourceBodyTests.swift b/Tests/JSONAPITests/ResourceBody/ResourceBodyTests.swift index a20db8c..9bc3f2c 100644 --- a/Tests/JSONAPITests/ResourceBody/ResourceBodyTests.swift +++ b/Tests/JSONAPITests/ResourceBody/ResourceBodyTests.swift @@ -100,6 +100,9 @@ class ResourceBodyTests: XCTestCase { XCTAssertEqual(combined.values.count, 3) XCTAssertEqual(combined.values, body1.values + body2.values) } +} + +extension ResourceBodyTests { enum ArticleType: ResourceObjectDescription { public static var jsonType: String { return "articles" } From 60cd515fd6c86ac77bd69c30c07dce74a1761798 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Tue, 30 Jul 2019 17:44:06 -0700 Subject: [PATCH 014/235] just filling in a couple small holes in test coverage --- Tests/JSONAPITests/Entity/EntityTests.swift | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/Tests/JSONAPITests/Entity/EntityTests.swift b/Tests/JSONAPITests/Entity/EntityTests.swift index feaf9ee..7076358 100644 --- a/Tests/JSONAPITests/Entity/EntityTests.swift +++ b/Tests/JSONAPITests/Entity/EntityTests.swift @@ -30,6 +30,7 @@ class EntityTests: XCTestCase { let entity = TestEntity9(attributes: .none, relationships: .init(one: entity1.pointer, nullableOne: .init(resourceObject: entity1, meta: .none, links: .none), optionalOne: .init(resourceObject: entity1, meta: .none, links: .none), optionalNullableOne: nil, optionalMany: .init(resourceObjects: [entity1, entity1], meta: .none, links: .none)), meta: .none, links: .none) XCTAssertEqual(entity ~> \.optionalOne, entity1.id) + XCTAssertEqual((entity ~> \.optionalOne).rawValue, entity1.id.rawValue) } func test_toMany_relationship_operator_access() { @@ -393,6 +394,7 @@ extension EntityTests { XCTAssertEqual((entity ~> \.nullableOne)?.rawValue, "3323") XCTAssertEqual((entity ~> \.one).rawValue, "4459") XCTAssertNil(entity ~> \.optionalNullableOne) + XCTAssertNil((entity ~> \.optionalNullableOne).rawValue) XCTAssertNoThrow(try TestEntity9.check(entity)) testEncoded(entity: entity) @@ -651,6 +653,16 @@ extension EntityTests { XCTAssertEqual(entity1 ~> \.metaRelationship, "hello") } + + func test_toManyMetaRelationshipAccessWorks() { + let entity1 = TestEntityWithMetaRelationship(id: "even", + attributes: .none, + relationships: .init(), + meta: .none, + links: .none) + + XCTAssertEqual(entity1 ~> \.toManyMetaRelationship, ["hello"]) + } } // MARK: - Test Types @@ -883,6 +895,12 @@ extension EntityTests { return TestEntity1.Identifier(rawValue: "hello") } } + + var toManyMetaRelationship: (TestEntityWithMetaRelationship) -> [TestEntity1.Identifier] { + return { entity in + return [TestEntity1.Identifier.id(from: "hello")] + } + } } } From e0e672322233a35823cad7705812a6d4ecefcde3 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Fri, 2 Aug 2019 07:27:17 -0700 Subject: [PATCH 015/235] Make Xcode Beta happy by explicitly creating optionals where they used to be implied. --- Tests/JSONAPITests/Entity/EntityTests.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/JSONAPITests/Entity/EntityTests.swift b/Tests/JSONAPITests/Entity/EntityTests.swift index 7076358..5fe168f 100644 --- a/Tests/JSONAPITests/Entity/EntityTests.swift +++ b/Tests/JSONAPITests/Entity/EntityTests.swift @@ -29,8 +29,8 @@ class EntityTests: XCTestCase { let entity1 = TestEntity1(attributes: .none, relationships: .none, meta: .none, links: .none) let entity = TestEntity9(attributes: .none, relationships: .init(one: entity1.pointer, nullableOne: .init(resourceObject: entity1, meta: .none, links: .none), optionalOne: .init(resourceObject: entity1, meta: .none, links: .none), optionalNullableOne: nil, optionalMany: .init(resourceObjects: [entity1, entity1], meta: .none, links: .none)), meta: .none, links: .none) - XCTAssertEqual(entity ~> \.optionalOne, entity1.id) - XCTAssertEqual((entity ~> \.optionalOne).rawValue, entity1.id.rawValue) + XCTAssertEqual(entity ~> \.optionalOne, Optional(entity1.id)) + XCTAssertEqual((entity ~> \.optionalOne).rawValue, Optional(entity1.id.rawValue)) } func test_toMany_relationship_operator_access() { From 7ec185bfe837addf84adf9b25c0b870a7438a8e4 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Fri, 2 Aug 2019 08:45:38 -0700 Subject: [PATCH 016/235] Add a couple tests and a bit of code doc. --- Sources/JSONAPI/Resource/Transformer.swift | 2 + .../Attribute/TransformerTests.swift | 51 +++++++++++++++++++ Tests/JSONAPITests/XCTestManifests.swift | 13 +++++ 3 files changed, 66 insertions(+) create mode 100644 Tests/JSONAPITests/Attribute/TransformerTests.swift diff --git a/Sources/JSONAPI/Resource/Transformer.swift b/Sources/JSONAPI/Resource/Transformer.swift index 920b9d4..04d3c04 100644 --- a/Sources/JSONAPI/Resource/Transformer.swift +++ b/Sources/JSONAPI/Resource/Transformer.swift @@ -42,6 +42,8 @@ extension Validator { return value } + /// Validate the given value and then return it if valid. + /// throws if invalid. public static func validate(_ value: To) throws -> To { let _ = try transform(value) return value diff --git a/Tests/JSONAPITests/Attribute/TransformerTests.swift b/Tests/JSONAPITests/Attribute/TransformerTests.swift new file mode 100644 index 0000000..1988f40 --- /dev/null +++ b/Tests/JSONAPITests/Attribute/TransformerTests.swift @@ -0,0 +1,51 @@ +// +// TransformerTests.swift +// +// +// Created by Mathew Polzin on 8/2/19. +// + +import XCTest +import JSONAPI +import JSONAPITesting + +class TransformerTests: XCTestCase { + func testIdentityTransform() { + let inString = "hello world" + + XCTAssertNoThrow(try IdentityTransformer.transform(inString)) + XCTAssertEqual(inString, try? IdentityTransformer.transform(inString)) + + XCTAssertNoThrow(try IdentityTransformer.reverse(inString)) + XCTAssertEqual(inString, try? IdentityTransformer.reverse(inString)) + } + + func testValidator() { + let string1 = "hello" + let string2 = "hello world" + + XCTAssertThrowsError(try MoreThanFiveCharValidator.validate(string1)) + XCTAssertThrowsError(try MoreThanFiveCharValidator.transform(string1)) + XCTAssertThrowsError(try MoreThanFiveCharValidator.reverse(string1)) + + XCTAssertNoThrow(try MoreThanFiveCharValidator.validate(string2)) + XCTAssertNoThrow(try MoreThanFiveCharValidator.transform(string2)) + XCTAssertNoThrow(try MoreThanFiveCharValidator.reverse(string2)) + + XCTAssertEqual(string2, try MoreThanFiveCharValidator.transform(string2)) + XCTAssertEqual(string2, try MoreThanFiveCharValidator.reverse(string2)) + } +} + +enum MoreThanFiveCharValidator: Validator { + public static func transform(_ value: String) throws -> String { + guard value.count > 5 else { + throw Error.fewerThanFiveChars + } + return value + } + + enum Error: Swift.Error { + case fewerThanFiveChars + } +} diff --git a/Tests/JSONAPITests/XCTestManifests.swift b/Tests/JSONAPITests/XCTestManifests.swift index a7b9e90..46edbba 100644 --- a/Tests/JSONAPITests/XCTestManifests.swift +++ b/Tests/JSONAPITests/XCTestManifests.swift @@ -8,6 +8,7 @@ extension APIDescriptionTests { static let __allTests__APIDescriptionTests = [ ("test_empty", test_empty), ("test_failsMissingMeta", test_failsMissingMeta), + ("test_init", test_init), ("test_NoDescriptionString", test_NoDescriptionString), ("test_WithMeta", test_WithMeta), ("test_WithVersion", test_WithVersion), @@ -235,6 +236,7 @@ extension EntityTests { ("test_RleationshipsOfSameType", test_RleationshipsOfSameType), ("test_RleationshipsOfSameType_encode", test_RleationshipsOfSameType_encode), ("test_toMany_relationship_operator_access", test_toMany_relationship_operator_access), + ("test_toManyMetaRelationshipAccessWorks", test_toManyMetaRelationshipAccessWorks), ("test_UnidentifiedEntity", test_UnidentifiedEntity), ("test_UnidentifiedEntity_encode", test_UnidentifiedEntity_encode), ("test_unidentifiedEntityAttributeAccess", test_unidentifiedEntityAttributeAccess), @@ -407,6 +409,16 @@ extension ResourceBodyTests { ] } +extension TransformerTests { + // DO NOT MODIFY: This is autogenerated, use: + // `swift test --generate-linuxmain` + // to regenerate. + static let __allTests__TransformerTests = [ + ("testIdentityTransform", testIdentityTransform), + ("testValidator", testValidator), + ] +} + public func __allTests() -> [XCTestCaseEntry] { return [ testCase(APIDescriptionTests.__allTests__APIDescriptionTests), @@ -423,6 +435,7 @@ public func __allTests() -> [XCTestCaseEntry] { testCase(PolyTests.__allTests__PolyTests), testCase(RelationshipTests.__allTests__RelationshipTests), testCase(ResourceBodyTests.__allTests__ResourceBodyTests), + testCase(TransformerTests.__allTests__TransformerTests), ] } #endif From 5335f24e9b9a328c70d7238b04d57327320ee05d Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Fri, 2 Aug 2019 09:30:30 -0700 Subject: [PATCH 017/235] Add test coverage for EmptyObjectDecoder's keyed container --- .../EmptyObjectDecoderTests.swift | 220 ++++++++++++++++++ 1 file changed, 220 insertions(+) create mode 100644 Tests/JSONAPITests/Empty Object Decoder/EmptyObjectDecoderTests.swift diff --git a/Tests/JSONAPITests/Empty Object Decoder/EmptyObjectDecoderTests.swift b/Tests/JSONAPITests/Empty Object Decoder/EmptyObjectDecoderTests.swift new file mode 100644 index 0000000..bda37d1 --- /dev/null +++ b/Tests/JSONAPITests/Empty Object Decoder/EmptyObjectDecoderTests.swift @@ -0,0 +1,220 @@ +// +// File.swift +// +// +// Created by Mathew Polzin on 8/2/19. +// + +import XCTest +@testable import JSONAPI + +class EmptyObjectDecoderTests: XCTestCase { + func testEmptyStruct() { + XCTAssertNoThrow(try EmptyStruct.init(from: EmptyObjectDecoder())) + XCTAssertNoThrow(try EmptyStructWithCodingKeys.init(from: EmptyObjectDecoder())) + } + + func testEmptyArray() { + XCTAssertNoThrow(try [EmptyStruct].init(from: EmptyObjectDecoder())) + XCTAssertNoThrow(try [String].init(from: EmptyObjectDecoder())) + XCTAssertNoThrow(try [Int].init(from: EmptyObjectDecoder())) + XCTAssertNoThrow(try [Double].init(from: EmptyObjectDecoder())) + XCTAssertNoThrow(try [Bool].init(from: EmptyObjectDecoder())) + XCTAssertNoThrow(try [Float].init(from: EmptyObjectDecoder())) + XCTAssertNoThrow(try [Int8].init(from: EmptyObjectDecoder())) + XCTAssertNoThrow(try [Int16].init(from: EmptyObjectDecoder())) + XCTAssertNoThrow(try [Int32].init(from: EmptyObjectDecoder())) + XCTAssertNoThrow(try [Int64].init(from: EmptyObjectDecoder())) + XCTAssertNoThrow(try [UInt].init(from: EmptyObjectDecoder())) + XCTAssertNoThrow(try [UInt8].init(from: EmptyObjectDecoder())) + XCTAssertNoThrow(try [UInt16].init(from: EmptyObjectDecoder())) + XCTAssertNoThrow(try [UInt32].init(from: EmptyObjectDecoder())) + XCTAssertNoThrow(try [UInt64].init(from: EmptyObjectDecoder())) + } + + func testNonEmptyStruct() { + XCTAssertThrowsError(try NonEmptyStruct.init(from: EmptyObjectDecoder())) + XCTAssertThrowsError(try NonEmptyStructString.init(from: EmptyObjectDecoder())) + XCTAssertThrowsError(try NonEmptyStructInt.init(from: EmptyObjectDecoder())) + XCTAssertThrowsError(try NonEmptyStructDouble.init(from: EmptyObjectDecoder())) + XCTAssertThrowsError(try NonEmptyStructBool.init(from: EmptyObjectDecoder())) + XCTAssertThrowsError(try NonEmptyStructFloat.init(from: EmptyObjectDecoder())) + XCTAssertThrowsError(try NonEmptyStructInt8.init(from: EmptyObjectDecoder())) + XCTAssertThrowsError(try NonEmptyStructInt16.init(from: EmptyObjectDecoder())) + XCTAssertThrowsError(try NonEmptyStructInt32.init(from: EmptyObjectDecoder())) + XCTAssertThrowsError(try NonEmptyStructInt64.init(from: EmptyObjectDecoder())) + XCTAssertThrowsError(try NonEmptyStructUInt.init(from: EmptyObjectDecoder())) + XCTAssertThrowsError(try NonEmptyStructUInt8.init(from: EmptyObjectDecoder())) + XCTAssertThrowsError(try NonEmptyStructUInt16.init(from: EmptyObjectDecoder())) + XCTAssertThrowsError(try NonEmptyStructUInt32.init(from: EmptyObjectDecoder())) + XCTAssertThrowsError(try NonEmptyStructUInt64.init(from: EmptyObjectDecoder())) + } + + func testWantingNil() { + XCTAssertThrowsError(try StructWithNil.init(from: EmptyObjectDecoder())) + } + + func testWantingSingleValue() { + XCTAssertThrowsError(try StructWithSingleValue.init(from: EmptyObjectDecoder())) + } + + func testWantingNestedKeyed() { + XCTAssertThrowsError(try StructWithNestedKeyedCall.init(from: EmptyObjectDecoder())) + } + + func testWantingNestedUnkeyed() { + XCTAssertThrowsError(try StructWithNestedUnkeyedCall.init(from: EmptyObjectDecoder())) + } + + func testWantsSuper() { + XCTAssertThrowsError(try StructWithUnkeyedSuper.init(from: EmptyObjectDecoder())) + + XCTAssertThrowsError(try StructWithKeyedSuper.init(from: EmptyObjectDecoder())) + } + + func testKeysAndCodingPath() { + XCTAssertEqual(try? EmptyObjectDecoder().container(keyedBy: EmptyStructWithCodingKeys.CodingKeys.self).allKeys.count, 0) + XCTAssertEqual(try? EmptyObjectDecoder().container(keyedBy: EmptyStructWithCodingKeys.CodingKeys.self).codingPath.count, 0) + } +} + +struct EmptyStruct: Decodable { + +} + +struct EmptyStructWithCodingKeys: Decodable { + enum CodingKeys: String, CodingKey { + case hello + } + + init(from decoder: Decoder) throws {} +} + +struct NonEmptyStruct: Decodable { + let value: T +} + +struct NonEmptyStructString: Decodable { + let value: String +} + +struct NonEmptyStructInt: Decodable { + let value: Int +} + +struct NonEmptyStructDouble: Decodable { + let value: Double +} + +struct NonEmptyStructBool: Decodable { + let value: Bool +} + +struct NonEmptyStructFloat: Decodable { + let value: Float +} + +struct NonEmptyStructInt8: Decodable { + let value: Int8 +} + +struct NonEmptyStructInt16: Decodable { + let value: Int16 +} + +struct NonEmptyStructInt32: Decodable { + let value: Int32 +} + +struct NonEmptyStructInt64: Decodable { + let value: Int64 +} + +struct NonEmptyStructUInt: Decodable { + let value: UInt +} + +struct NonEmptyStructUInt8: Decodable { + let value: UInt8 +} + +struct NonEmptyStructUInt16: Decodable { + let value: UInt16 +} + +struct NonEmptyStructUInt32: Decodable { + let value: UInt32 +} + +struct NonEmptyStructUInt64: Decodable { + let value: UInt64 +} + +struct StructWithNil: Decodable { + enum CodingKeys: String, CodingKey { + case hello + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + let _ = try container.decodeNil(forKey: .hello) + } +} + +struct StructWithSingleValue: Decodable { + let value: String + + init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + value = try container.decode(String.self) + } +} + +struct StructWithNestedKeyedCall: Decodable { + enum CodingKeys: String, CodingKey { + case hello + } + + enum NestedKeys: String, CodingKey { + case world + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + let _ = try container.nestedContainer(keyedBy: NestedKeys.self, forKey: .hello) + } +} + +struct StructWithNestedUnkeyedCall: Decodable { + enum CodingKeys: String, CodingKey { + case hello + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + let _ = try container.nestedUnkeyedContainer(forKey: .hello) + } +} + +struct StructWithUnkeyedSuper: Decodable { + enum CodingKeys: String, CodingKey { + case hello + } + + init(from decoder: Decoder) throws { + let _ = try decoder.container(keyedBy: CodingKeys.self).superDecoder() + } +} + +struct StructWithKeyedSuper: Decodable { + enum CodingKeys: String, CodingKey { + case hello + } + + init(from decoder: Decoder) throws { + let _ = try decoder.container(keyedBy: CodingKeys.self).superDecoder(forKey: .hello) + } +} From 049dc1f7b55903956c4fe960e6628ebfa6c57056 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Sun, 4 Aug 2019 10:28:48 -0700 Subject: [PATCH 018/235] Cover the EmptyObjectDecoder unkeyed container with tests --- .../EmptyObjectDecoderTests.swift | 175 +++++++++++++++++- 1 file changed, 174 insertions(+), 1 deletion(-) diff --git a/Tests/JSONAPITests/Empty Object Decoder/EmptyObjectDecoderTests.swift b/Tests/JSONAPITests/Empty Object Decoder/EmptyObjectDecoderTests.swift index bda37d1..5e13165 100644 --- a/Tests/JSONAPITests/Empty Object Decoder/EmptyObjectDecoderTests.swift +++ b/Tests/JSONAPITests/Empty Object Decoder/EmptyObjectDecoderTests.swift @@ -32,6 +32,24 @@ class EmptyObjectDecoderTests: XCTestCase { XCTAssertNoThrow(try [UInt64].init(from: EmptyObjectDecoder())) } + func testNonEmptyArray() { + XCTAssertThrowsError(try NonEmptyArray.init(from: EmptyObjectDecoder())) + XCTAssertThrowsError(try NonEmptyArrayString.init(from: EmptyObjectDecoder())) + XCTAssertThrowsError(try NonEmptyArrayInt.init(from: EmptyObjectDecoder())) + XCTAssertThrowsError(try NonEmptyArrayDouble.init(from: EmptyObjectDecoder())) + XCTAssertThrowsError(try NonEmptyArrayBool.init(from: EmptyObjectDecoder())) + XCTAssertThrowsError(try NonEmptyArrayFloat.init(from: EmptyObjectDecoder())) + XCTAssertThrowsError(try NonEmptyArrayInt8.init(from: EmptyObjectDecoder())) + XCTAssertThrowsError(try NonEmptyArrayInt16.init(from: EmptyObjectDecoder())) + XCTAssertThrowsError(try NonEmptyArrayInt32.init(from: EmptyObjectDecoder())) + XCTAssertThrowsError(try NonEmptyArrayInt64.init(from: EmptyObjectDecoder())) + XCTAssertThrowsError(try NonEmptyArrayUInt.init(from: EmptyObjectDecoder())) + XCTAssertThrowsError(try NonEmptyArrayUInt8.init(from: EmptyObjectDecoder())) + XCTAssertThrowsError(try NonEmptyArrayUInt16.init(from: EmptyObjectDecoder())) + XCTAssertThrowsError(try NonEmptyArrayUInt32.init(from: EmptyObjectDecoder())) + XCTAssertThrowsError(try NonEmptyArrayUInt64.init(from: EmptyObjectDecoder())) + } + func testNonEmptyStruct() { XCTAssertThrowsError(try NonEmptyStruct.init(from: EmptyObjectDecoder())) XCTAssertThrowsError(try NonEmptyStructString.init(from: EmptyObjectDecoder())) @@ -52,6 +70,7 @@ class EmptyObjectDecoderTests: XCTestCase { func testWantingNil() { XCTAssertThrowsError(try StructWithNil.init(from: EmptyObjectDecoder())) + XCTAssertThrowsError(try ArrayWithNil.init(from: EmptyObjectDecoder())) } func testWantingSingleValue() { @@ -60,24 +79,33 @@ class EmptyObjectDecoderTests: XCTestCase { func testWantingNestedKeyed() { XCTAssertThrowsError(try StructWithNestedKeyedCall.init(from: EmptyObjectDecoder())) + XCTAssertThrowsError(try ArrayWithNestedKeyedCall.init(from: EmptyObjectDecoder())) } func testWantingNestedUnkeyed() { XCTAssertThrowsError(try StructWithNestedUnkeyedCall.init(from: EmptyObjectDecoder())) + XCTAssertThrowsError(try ArrayWithNestedUnkeyedCall.init(from: EmptyObjectDecoder())) } func testWantsSuper() { XCTAssertThrowsError(try StructWithUnkeyedSuper.init(from: EmptyObjectDecoder())) - XCTAssertThrowsError(try StructWithKeyedSuper.init(from: EmptyObjectDecoder())) + + XCTAssertThrowsError(try ArrayWithUnkeyedSuper.init(from: EmptyObjectDecoder())) } func testKeysAndCodingPath() { XCTAssertEqual(try? EmptyObjectDecoder().container(keyedBy: EmptyStructWithCodingKeys.CodingKeys.self).allKeys.count, 0) XCTAssertEqual(try? EmptyObjectDecoder().container(keyedBy: EmptyStructWithCodingKeys.CodingKeys.self).codingPath.count, 0) + + XCTAssertEqual(try? EmptyObjectDecoder().unkeyedContainer().codingPath.count, 0) + XCTAssertEqual(try? EmptyObjectDecoder().unkeyedContainer().currentIndex, 0) + XCTAssertEqual(try? EmptyObjectDecoder().unkeyedContainer().count, 0) } } +// MARK: - struct + struct EmptyStruct: Decodable { } @@ -218,3 +246,148 @@ struct StructWithKeyedSuper: Decodable { let _ = try decoder.container(keyedBy: CodingKeys.self).superDecoder(forKey: .hello) } } + +// MARK: - array + +struct NonEmptyArray: Decodable { + init(from decoder: Decoder) throws { + var container = try decoder.unkeyedContainer() + let _ = try container.decode(T.self) + } +} + +struct NonEmptyArrayString: Decodable { + init(from decoder: Decoder) throws { + var container = try decoder.unkeyedContainer() + let _ = try container.decode(String.self) + } +} + +struct NonEmptyArrayInt: Decodable { + init(from decoder: Decoder) throws { + var container = try decoder.unkeyedContainer() + let _ = try container.decode(Int.self) + } +} + +struct NonEmptyArrayDouble: Decodable { + init(from decoder: Decoder) throws { + var container = try decoder.unkeyedContainer() + let _ = try container.decode(Double.self) + } +} + +struct NonEmptyArrayBool: Decodable { + init(from decoder: Decoder) throws { + var container = try decoder.unkeyedContainer() + let _ = try container.decode(Bool.self) + } +} + +struct NonEmptyArrayFloat: Decodable { + init(from decoder: Decoder) throws { + var container = try decoder.unkeyedContainer() + let _ = try container.decode(Float.self) + } +} + +struct NonEmptyArrayInt8: Decodable { + init(from decoder: Decoder) throws { + var container = try decoder.unkeyedContainer() + let _ = try container.decode(Int8.self) + } +} + +struct NonEmptyArrayInt16: Decodable { + init(from decoder: Decoder) throws { + var container = try decoder.unkeyedContainer() + let _ = try container.decode(Int16.self) + } +} + +struct NonEmptyArrayInt32: Decodable { + init(from decoder: Decoder) throws { + var container = try decoder.unkeyedContainer() + let _ = try container.decode(Int32.self) + } +} + +struct NonEmptyArrayInt64: Decodable { + init(from decoder: Decoder) throws { + var container = try decoder.unkeyedContainer() + let _ = try container.decode(Int64.self) + } +} + +struct NonEmptyArrayUInt: Decodable { + init(from decoder: Decoder) throws { + var container = try decoder.unkeyedContainer() + let _ = try container.decode(UInt.self) + } +} + +struct NonEmptyArrayUInt8: Decodable { + init(from decoder: Decoder) throws { + var container = try decoder.unkeyedContainer() + let _ = try container.decode(UInt8.self) + } +} + +struct NonEmptyArrayUInt16: Decodable { + init(from decoder: Decoder) throws { + var container = try decoder.unkeyedContainer() + let _ = try container.decode(UInt16.self) + } +} + +struct NonEmptyArrayUInt32: Decodable { + init(from decoder: Decoder) throws { + var container = try decoder.unkeyedContainer() + let _ = try container.decode(UInt32.self) + } +} + +struct NonEmptyArrayUInt64: Decodable { + init(from decoder: Decoder) throws { + var container = try decoder.unkeyedContainer() + let _ = try container.decode(UInt64.self) + } +} + +struct ArrayWithNil: Decodable { + + init(from decoder: Decoder) throws { + var container = try decoder.unkeyedContainer() + + let _ = try container.decodeNil() + } +} + +struct ArrayWithNestedKeyedCall: Decodable { + + enum NestedKeys: String, CodingKey { + case world + } + + init(from decoder: Decoder) throws { + var container = try decoder.unkeyedContainer() + + let _ = try container.nestedContainer(keyedBy: NestedKeys.self) + } +} + +struct ArrayWithNestedUnkeyedCall: Decodable { + + init(from decoder: Decoder) throws { + var container = try decoder.unkeyedContainer() + + let _ = try container.nestedUnkeyedContainer() + } +} + +struct ArrayWithUnkeyedSuper: Decodable { + init(from decoder: Decoder) throws { + var container = try decoder.unkeyedContainer() + let _ = try container.superDecoder() + } +} From 5aef44c3b3eef46c35bc0a2ec9681b8ff451707f Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Sun, 4 Aug 2019 18:44:28 -0700 Subject: [PATCH 019/235] Add Sparse Fieldset support for Attributes --- Sources/JSONAPI/Resource/ResourceObject.swift | 11 +- .../SparseFields/SparseFieldEncodable.swift | 31 +++ .../SparseFields/SparseFieldEncoder.swift | 199 ++++++++++++++++++ .../SparseFieldEncoderTests.swift | 145 +++++++++++++ 4 files changed, 384 insertions(+), 2 deletions(-) create mode 100644 Sources/JSONAPI/SparseFields/SparseFieldEncodable.swift create mode 100644 Sources/JSONAPI/SparseFields/SparseFieldEncoder.swift create mode 100644 Tests/JSONAPITests/SparseFields/SparseFieldEncoderTests.swift diff --git a/Sources/JSONAPI/Resource/ResourceObject.swift b/Sources/JSONAPI/Resource/ResourceObject.swift index 83cef3e..d1f1500 100644 --- a/Sources/JSONAPI/Resource/ResourceObject.swift +++ b/Sources/JSONAPI/Resource/ResourceObject.swift @@ -15,6 +15,12 @@ public protocol Relationships: Codable & Equatable {} /// properties of any types that are JSON encodable. public protocol Attributes: Codable & Equatable {} +/// Attributes containing publicly accessible and `Equatable` +/// CodingKeys are required to support Sparse Fieldsets. +public protocol SparsableAttributes: Attributes { + associatedtype CodingKeys: CodingKey & Equatable +} + /// Can be used as `Relationships` Type for Entities that do not /// have any Relationships. public struct NoRelationships: Relationships { @@ -48,7 +54,7 @@ public protocol ResourceObjectProxyDescription: JSONTyped { associatedtype Relationships: Equatable } -/// An `ResourceObjectDescription` describes a JSON API +/// A `ResourceObjectDescription` describes a JSON API /// Resource Object. The Resource Object /// itself is encoded and decoded as an /// `ResourceObject`, which gets specialized on an @@ -566,7 +572,8 @@ public extension ResourceObject { } if Description.Attributes.self != NoAttributes.self { - try container.encode(attributes, forKey: .attributes) + let nestedEncoder = container.superEncoder(forKey: .attributes) + try attributes.encode(to: nestedEncoder) } if Description.Relationships.self != NoRelationships.self { diff --git a/Sources/JSONAPI/SparseFields/SparseFieldEncodable.swift b/Sources/JSONAPI/SparseFields/SparseFieldEncodable.swift new file mode 100644 index 0000000..4fdec48 --- /dev/null +++ b/Sources/JSONAPI/SparseFields/SparseFieldEncodable.swift @@ -0,0 +1,31 @@ +// +// SparseField.swift +// +// +// Created by Mathew Polzin on 8/4/19. +// + +public struct SparseField< + Description: JSONAPI.ResourceObjectDescription, + MetaType: JSONAPI.Meta, + LinksType: JSONAPI.Links, + EntityRawIdType: JSONAPI.MaybeRawId +>: Encodable where Description.Attributes: SparsableAttributes { + + public typealias Resource = JSONAPI.ResourceObject + + public let resourceObject: Resource + public let fields: [Description.Attributes.CodingKeys] + + public init(_ resourceObject: Resource, fields: [Description.Attributes.CodingKeys]) { + self.resourceObject = resourceObject + self.fields = fields + } + + public func encode(to encoder: Encoder) throws { + let sparseEncoder = SparseFieldEncoder(wrapping: encoder, + encoding: fields) + + try resourceObject.encode(to: sparseEncoder) + } +} diff --git a/Sources/JSONAPI/SparseFields/SparseFieldEncoder.swift b/Sources/JSONAPI/SparseFields/SparseFieldEncoder.swift new file mode 100644 index 0000000..cbdcb33 --- /dev/null +++ b/Sources/JSONAPI/SparseFields/SparseFieldEncoder.swift @@ -0,0 +1,199 @@ +// +// SparseEncoder.swift +// +// +// Created by Mathew Polzin on 8/4/19. +// + +public class SparseFieldEncoder: Encoder { + private let wrappedEncoder: Encoder + private let allowedKeys: [SparseKey] + + public var codingPath: [CodingKey] { + return wrappedEncoder.codingPath + } + + public var userInfo: [CodingUserInfoKey : Any] { + return wrappedEncoder.userInfo + } + + public init(wrapping encoder: Encoder, encoding allowedKeys: [SparseKey]) { + wrappedEncoder = encoder + self.allowedKeys = allowedKeys + } + + public func container(keyedBy type: Key.Type) -> KeyedEncodingContainer where Key : CodingKey { + let container = SparseFieldKeyedEncodingContainer(wrapping: wrappedEncoder.container(keyedBy: type), + encoding: allowedKeys) + return KeyedEncodingContainer(container) + } + + public func container(keyedBy type: SparseKey.Type) -> KeyedEncodingContainer { + let container = SparseFieldKeyedEncodingContainer(wrapping: wrappedEncoder.container(keyedBy: type), + encoding: allowedKeys) + return KeyedEncodingContainer(container) + } + + public func unkeyedContainer() -> UnkeyedEncodingContainer { + return wrappedEncoder.unkeyedContainer() + } + + public func singleValueContainer() -> SingleValueEncodingContainer { + return wrappedEncoder.singleValueContainer() + } +} + +public struct SparseFieldKeyedEncodingContainer: KeyedEncodingContainerProtocol where SparseKey: CodingKey, SparseKey: Equatable, Key: CodingKey { + private var wrappedContainer: KeyedEncodingContainer + private let allowedKeys: [SparseKey] + + public var codingPath: [CodingKey] { + return wrappedContainer.codingPath + } + + public init(wrapping container: KeyedEncodingContainer, encoding allowedKeys: [SparseKey]) { + wrappedContainer = container + self.allowedKeys = allowedKeys + } + + private func shouldAllow(key: Key) -> Bool { + if let key = key as? SparseKey { + return allowedKeys.contains(key) + } + return true + } + + public mutating func encodeNil(forKey key: Key) throws { + guard shouldAllow(key: key) else { return } + + try wrappedContainer.encodeNil(forKey: key) + } + + public mutating func encode(_ value: Bool, forKey key: Key) throws { + guard shouldAllow(key: key) else { return } + + try wrappedContainer.encode(value, forKey: key) + } + + public mutating func encode(_ value: String, forKey key: Key) throws { + guard shouldAllow(key: key) else { return } + + try wrappedContainer.encode(value, forKey: key) + } + + public mutating func encode(_ value: Double, forKey key: Key) throws { + guard shouldAllow(key: key) else { return } + + try wrappedContainer.encode(value, forKey: key) + } + + public mutating func encode(_ value: Float, forKey key: Key) throws { + guard shouldAllow(key: key) else { return } + + try wrappedContainer.encode(value, forKey: key) + } + + public mutating func encode(_ value: Int, forKey key: Key) throws { + guard shouldAllow(key: key) else { return } + + try wrappedContainer.encode(value, forKey: key) + } + + public mutating func encode(_ value: Int8, forKey key: Key) throws { + guard shouldAllow(key: key) else { return } + + try wrappedContainer.encode(value, forKey: key) + } + + public mutating func encode(_ value: Int16, forKey key: Key) throws { + guard shouldAllow(key: key) else { return } + + try wrappedContainer.encode(value, forKey: key) + } + + public mutating func encode(_ value: Int32, forKey key: Key) throws { + guard shouldAllow(key: key) else { return } + + try wrappedContainer.encode(value, forKey: key) + } + + public mutating func encode(_ value: Int64, forKey key: Key) throws { + guard shouldAllow(key: key) else { return } + + try wrappedContainer.encode(value, forKey: key) + } + + public mutating func encode(_ value: UInt, forKey key: Key) throws { + guard shouldAllow(key: key) else { return } + + try wrappedContainer.encode(value, forKey: key) + } + + public mutating func encode(_ value: UInt8, forKey key: Key) throws { + guard shouldAllow(key: key) else { return } + + try wrappedContainer.encode(value, forKey: key) + } + + public mutating func encode(_ value: UInt16, forKey key: Key) throws { + guard shouldAllow(key: key) else { return } + + try wrappedContainer.encode(value, forKey: key) + } + + public mutating func encode(_ value: UInt32, forKey key: Key) throws { + guard shouldAllow(key: key) else { return } + + try wrappedContainer.encode(value, forKey: key) + } + + public mutating func encode(_ value: UInt64, forKey key: Key) throws { + guard shouldAllow(key: key) else { return } + + try wrappedContainer.encode(value, forKey: key) + } + + public mutating func encode(_ value: T, forKey key: Key) throws where T : Encodable { + guard shouldAllow(key: key) else { return } + + try wrappedContainer.encode(value, forKey: key) + } + + public mutating func nestedContainer(keyedBy keyType: NestedKey.Type, + forKey key: Key) -> KeyedEncodingContainer where NestedKey : CodingKey { + guard shouldAllow(key: key) else { + return KeyedEncodingContainer( + SparseFieldKeyedEncodingContainer(wrapping: wrappedContainer.nestedContainer(keyedBy: keyType, + forKey: key), + encoding: []) + ) + } + + return KeyedEncodingContainer( + SparseFieldKeyedEncodingContainer(wrapping: wrappedContainer.nestedContainer(keyedBy: keyType, + forKey: key), + encoding: allowedKeys) + ) + } + + public mutating func nestedUnkeyedContainer(forKey key: Key) -> UnkeyedEncodingContainer { + guard shouldAllow(key: key) else { + // TODO: Seems like this might not work as expected... maybe need an empty unkeyed container + return wrappedContainer.nestedUnkeyedContainer(forKey: key) + } + + return wrappedContainer.nestedUnkeyedContainer(forKey: key) + } + + public mutating func superEncoder() -> Encoder { + return wrappedContainer.superEncoder() + } + + public mutating func superEncoder(forKey key: Key) -> Encoder { + guard shouldAllow(key: key) else { + return SparseFieldEncoder(wrapping: wrappedContainer.superEncoder(forKey: key), encoding: [SparseKey]()) + } + + return SparseFieldEncoder(wrapping: wrappedContainer.superEncoder(forKey: key), encoding: allowedKeys) + } +} diff --git a/Tests/JSONAPITests/SparseFields/SparseFieldEncoderTests.swift b/Tests/JSONAPITests/SparseFields/SparseFieldEncoderTests.swift new file mode 100644 index 0000000..a7f9ccd --- /dev/null +++ b/Tests/JSONAPITests/SparseFields/SparseFieldEncoderTests.swift @@ -0,0 +1,145 @@ +// +// File.swift +// +// +// Created by Mathew Polzin on 8/4/19. +// + +import XCTest +import Foundation +import JSONAPI +import JSONAPITesting + +class SparseFieldEncoderTests: XCTestCase { + func test_FullEncode() { + let jsonEncoder = JSONEncoder() + let sparseWithEverything = SparseField(testEverythingObject, fields: EverythingTest.Attributes.CodingKeys.allCases) + + let encoded = try! jsonEncoder.encode(sparseWithEverything) + + let deserialized = try! JSONSerialization.jsonObject(with: encoded, + options: []) + + let outerDict = deserialized as? [String: Any] + let id = outerDict?["id"] as? String + let type = outerDict?["type"] as? String + let attributesDict = outerDict?["attributes"] as? [String: Any] + let relationships = outerDict?["relationships"] + + XCTAssertEqual(id, testEverythingObject.id.rawValue) + XCTAssertEqual(type, EverythingTest.jsonType) + XCTAssertNil(relationships) + + XCTAssertEqual(attributesDict?.count, 9) // note not 10 because one value is omitted + XCTAssertEqual(attributesDict?["bool"] as? Bool, + testEverythingObject[\.bool]) + XCTAssertEqual(attributesDict?["int"] as? Int, + testEverythingObject[\.int]) + XCTAssertEqual(attributesDict?["double"] as? Double, + testEverythingObject[\.double]) + XCTAssertEqual(attributesDict?["string"] as? String, + testEverythingObject[\.string]) + XCTAssertEqual((attributesDict?["nestedStruct"] as? [String: String])?["hello"], + testEverythingObject[\.nestedStruct].hello) + XCTAssertEqual(attributesDict?["nestedEnum"] as? String, + testEverythingObject[\.nestedEnum].rawValue) + XCTAssertEqual(attributesDict?["array"] as? [Bool], + testEverythingObject[\.array]) + XCTAssertNil(attributesDict?["optional"]) + XCTAssertNotNil(attributesDict?["nullable"] as? NSNull) + XCTAssertNotNil(attributesDict?["optionalNullable"] as? NSNull) + } + + func test_PartialEncode() { + let jsonEncoder = JSONEncoder() + let sparseWithEverything = SparseField(testEverythingObject, fields: [.string, .bool, .array]) + + let encoded = try! jsonEncoder.encode(sparseWithEverything) + + let deserialized = try! JSONSerialization.jsonObject(with: encoded, + options: []) + + let outerDict = deserialized as? [String: Any] + let id = outerDict?["id"] as? String + let type = outerDict?["type"] as? String + let attributesDict = outerDict?["attributes"] as? [String: Any] + let relationships = outerDict?["relationships"] + + XCTAssertEqual(id, testEverythingObject.id.rawValue) + XCTAssertEqual(type, EverythingTest.jsonType) + XCTAssertNil(relationships) + + XCTAssertEqual(attributesDict?.count, 3) + XCTAssertEqual(attributesDict?["bool"] as? Bool, + testEverythingObject[\.bool]) + XCTAssertNil(attributesDict?["int"]) + XCTAssertNil(attributesDict?["double"]) + XCTAssertEqual(attributesDict?["string"] as? String, + testEverythingObject[\.string]) + XCTAssertNil(attributesDict?["nestedStruct"]) + XCTAssertNil(attributesDict?["nestedEnum"]) + XCTAssertEqual(attributesDict?["array"] as? [Bool], + testEverythingObject[\.array]) + XCTAssertNil(attributesDict?["optional"]) + XCTAssertNil(attributesDict?["nullable"]) + XCTAssertNil(attributesDict?["optionalNullable"]) + } +} + +struct EverythingTestDescription: JSONAPI.ResourceObjectDescription { + static let jsonType: String = "everything" + + struct Attributes: JSONAPI.SparsableAttributes { + let bool: Attribute + let int: Attribute + let double: Attribute + let string: Attribute + let nestedStruct: Attribute + let nestedEnum: Attribute + + let array: Attribute<[Bool]> + let optional: Attribute? + let nullable: Attribute + let optionalNullable: Attribute? + + struct NestedStruct: Codable, Equatable { + let hello: String + } + + enum NestedEnum: String, Codable, Equatable { + case hello + case world + } + + enum CodingKeys: String, CodingKey, Equatable, CaseIterable { + case bool + case int + case double + case string + case nestedStruct + case nestedEnum + case array + case optional + case nullable + case optionalNullable + } + } + + typealias Relationships = NoRelationships +} + +typealias EverythingTest = JSONAPI.ResourceObject + +let testEverythingObject = EverythingTest(attributes: .init(bool: true, + int: 10, + double: 10.5, + string: "hello world", + nestedStruct: .init(value: .init(hello: "world")), + nestedEnum: .init(value: .hello), + array: [true, false, false], + optional: nil, + nullable: .init(value: nil), + optionalNullable: .init(value: nil)), + relationships: .none, + meta: .none, + links: .none) From 2f3a61928430ba3f448145622ce43aa9ead49161 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Sun, 4 Aug 2019 18:47:26 -0700 Subject: [PATCH 020/235] Spacing difference --- Sources/JSONAPI/Resource/ResourceObject.swift | 92 +++++++++---------- 1 file changed, 46 insertions(+), 46 deletions(-) diff --git a/Sources/JSONAPI/Resource/ResourceObject.swift b/Sources/JSONAPI/Resource/ResourceObject.swift index d1f1500..af964a7 100644 --- a/Sources/JSONAPI/Resource/ResourceObject.swift +++ b/Sources/JSONAPI/Resource/ResourceObject.swift @@ -553,63 +553,63 @@ infix operator ~> // MARK: - Codable private enum ResourceObjectCodingKeys: String, CodingKey { - case type = "type" - case id = "id" - case attributes = "attributes" - case relationships = "relationships" - case meta = "meta" - case links = "links" + case type = "type" + case id = "id" + case attributes = "attributes" + case relationships = "relationships" + case meta = "meta" + case links = "links" } public extension ResourceObject { - func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: ResourceObjectCodingKeys.self) - - try container.encode(ResourceObject.jsonType, forKey: .type) - - if EntityRawIdType.self != Unidentified.self { - try container.encode(id, forKey: .id) - } - - if Description.Attributes.self != NoAttributes.self { + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: ResourceObjectCodingKeys.self) + + try container.encode(ResourceObject.jsonType, forKey: .type) + + if EntityRawIdType.self != Unidentified.self { + try container.encode(id, forKey: .id) + } + + if Description.Attributes.self != NoAttributes.self { let nestedEncoder = container.superEncoder(forKey: .attributes) try attributes.encode(to: nestedEncoder) - } - - if Description.Relationships.self != NoRelationships.self { - try container.encode(relationships, forKey: .relationships) - } + } - if MetaType.self != NoMetadata.self { - try container.encode(meta, forKey: .meta) - } + if Description.Relationships.self != NoRelationships.self { + try container.encode(relationships, forKey: .relationships) + } - if LinksType.self != NoLinks.self { - try container.encode(links, forKey: .links) - } - } + if MetaType.self != NoMetadata.self { + try container.encode(meta, forKey: .meta) + } - init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: ResourceObjectCodingKeys.self) - - let type = try container.decode(String.self, forKey: .type) - - guard ResourceObject.jsonType == type else { - throw JSONAPIEncodingError.typeMismatch(expected: Description.jsonType, found: type) - } + if LinksType.self != NoLinks.self { + try container.encode(links, forKey: .links) + } + } - let maybeUnidentified = Unidentified() as? EntityRawIdType - id = try maybeUnidentified.map { ResourceObject.Id(rawValue: $0) } ?? container.decode(ResourceObject.Id.self, forKey: .id) + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: ResourceObjectCodingKeys.self) - attributes = try (NoAttributes() as? Description.Attributes) ?? - container.decode(Description.Attributes.self, forKey: .attributes) + let type = try container.decode(String.self, forKey: .type) - relationships = try (NoRelationships() as? Description.Relationships) - ?? container.decodeIfPresent(Description.Relationships.self, forKey: .relationships) - ?? Description.Relationships(from: EmptyObjectDecoder()) + guard ResourceObject.jsonType == type else { + throw JSONAPIEncodingError.typeMismatch(expected: Description.jsonType, found: type) + } - meta = try (NoMetadata() as? MetaType) ?? container.decode(MetaType.self, forKey: .meta) + let maybeUnidentified = Unidentified() as? EntityRawIdType + id = try maybeUnidentified.map { ResourceObject.Id(rawValue: $0) } ?? container.decode(ResourceObject.Id.self, forKey: .id) - links = try (NoLinks() as? LinksType) ?? container.decode(LinksType.self, forKey: .links) - } + attributes = try (NoAttributes() as? Description.Attributes) ?? + container.decode(Description.Attributes.self, forKey: .attributes) + + relationships = try (NoRelationships() as? Description.Relationships) + ?? container.decodeIfPresent(Description.Relationships.self, forKey: .relationships) + ?? Description.Relationships(from: EmptyObjectDecoder()) + + meta = try (NoMetadata() as? MetaType) ?? container.decode(MetaType.self, forKey: .meta) + + links = try (NoLinks() as? LinksType) ?? container.decode(LinksType.self, forKey: .links) + } } From e0b53236bbbeffe57fc8f43d5805d853f6f6df11 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Sun, 4 Aug 2019 23:03:56 -0700 Subject: [PATCH 021/235] Add sparse fields method to ResourceObject and test it. --- .../SparseFields/SparseFieldEncodable.swift | 9 ++++ ....swift => SparseFieldEncodableTests.swift} | 41 +++++++++++++++++-- 2 files changed, 47 insertions(+), 3 deletions(-) rename Tests/JSONAPITests/SparseFields/{SparseFieldEncoderTests.swift => SparseFieldEncodableTests.swift} (77%) diff --git a/Sources/JSONAPI/SparseFields/SparseFieldEncodable.swift b/Sources/JSONAPI/SparseFields/SparseFieldEncodable.swift index 4fdec48..bac54d2 100644 --- a/Sources/JSONAPI/SparseFields/SparseFieldEncodable.swift +++ b/Sources/JSONAPI/SparseFields/SparseFieldEncodable.swift @@ -29,3 +29,12 @@ public struct SparseField< try resourceObject.encode(to: sparseEncoder) } } + +public extension ResourceObject where Description.Attributes: SparsableAttributes { + + /// Get a Sparse Fieldset of this `ResourceObject` that can be encoded + /// as a `PrimaryResource`. + func sparse(with fields: [Description.Attributes.CodingKeys]) -> SparseField { + return SparseField(self, fields: fields) + } +} diff --git a/Tests/JSONAPITests/SparseFields/SparseFieldEncoderTests.swift b/Tests/JSONAPITests/SparseFields/SparseFieldEncodableTests.swift similarity index 77% rename from Tests/JSONAPITests/SparseFields/SparseFieldEncoderTests.swift rename to Tests/JSONAPITests/SparseFields/SparseFieldEncodableTests.swift index a7f9ccd..5a17075 100644 --- a/Tests/JSONAPITests/SparseFields/SparseFieldEncoderTests.swift +++ b/Tests/JSONAPITests/SparseFields/SparseFieldEncodableTests.swift @@ -1,5 +1,5 @@ // -// File.swift +// SparseFieldEncodableTests.swift // // // Created by Mathew Polzin on 8/4/19. @@ -52,9 +52,44 @@ class SparseFieldEncoderTests: XCTestCase { func test_PartialEncode() { let jsonEncoder = JSONEncoder() - let sparseWithEverything = SparseField(testEverythingObject, fields: [.string, .bool, .array]) + let sparseObject = SparseField(testEverythingObject, fields: [.string, .bool, .array]) - let encoded = try! jsonEncoder.encode(sparseWithEverything) + let encoded = try! jsonEncoder.encode(sparseObject) + + let deserialized = try! JSONSerialization.jsonObject(with: encoded, + options: []) + + let outerDict = deserialized as? [String: Any] + let id = outerDict?["id"] as? String + let type = outerDict?["type"] as? String + let attributesDict = outerDict?["attributes"] as? [String: Any] + let relationships = outerDict?["relationships"] + + XCTAssertEqual(id, testEverythingObject.id.rawValue) + XCTAssertEqual(type, EverythingTest.jsonType) + XCTAssertNil(relationships) + + XCTAssertEqual(attributesDict?.count, 3) + XCTAssertEqual(attributesDict?["bool"] as? Bool, + testEverythingObject[\.bool]) + XCTAssertNil(attributesDict?["int"]) + XCTAssertNil(attributesDict?["double"]) + XCTAssertEqual(attributesDict?["string"] as? String, + testEverythingObject[\.string]) + XCTAssertNil(attributesDict?["nestedStruct"]) + XCTAssertNil(attributesDict?["nestedEnum"]) + XCTAssertEqual(attributesDict?["array"] as? [Bool], + testEverythingObject[\.array]) + XCTAssertNil(attributesDict?["optional"]) + XCTAssertNil(attributesDict?["nullable"]) + XCTAssertNil(attributesDict?["optionalNullable"]) + } + + func test_sparseFieldsMethod() { + let jsonEncoder = JSONEncoder() + let sparseObject = testEverythingObject.sparse(with: [.string, .bool, .array]) + + let encoded = try! jsonEncoder.encode(sparseObject) let deserialized = try! JSONSerialization.jsonObject(with: encoded, options: []) From 61e00c2de56d16e6f78701b8894f472d9535d221 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Mon, 5 Aug 2019 08:01:42 -0700 Subject: [PATCH 022/235] small rename --- ...SparseFieldEncodable.swift => SparseFieldset.swift} | 10 +++++----- ...dEncodableTests.swift => SparseFieldsetTests.swift} | 8 ++++---- 2 files changed, 9 insertions(+), 9 deletions(-) rename Sources/JSONAPI/SparseFields/{SparseFieldEncodable.swift => SparseFieldset.swift} (83%) rename Tests/JSONAPITests/SparseFields/{SparseFieldEncodableTests.swift => SparseFieldsetTests.swift} (95%) diff --git a/Sources/JSONAPI/SparseFields/SparseFieldEncodable.swift b/Sources/JSONAPI/SparseFields/SparseFieldset.swift similarity index 83% rename from Sources/JSONAPI/SparseFields/SparseFieldEncodable.swift rename to Sources/JSONAPI/SparseFields/SparseFieldset.swift index bac54d2..8e0979b 100644 --- a/Sources/JSONAPI/SparseFields/SparseFieldEncodable.swift +++ b/Sources/JSONAPI/SparseFields/SparseFieldset.swift @@ -1,11 +1,11 @@ // -// SparseField.swift +// SparseFieldset.swift // // // Created by Mathew Polzin on 8/4/19. // -public struct SparseField< +public struct SparseFieldset< Description: JSONAPI.ResourceObjectDescription, MetaType: JSONAPI.Meta, LinksType: JSONAPI.Links, @@ -33,8 +33,8 @@ public struct SparseField< public extension ResourceObject where Description.Attributes: SparsableAttributes { /// Get a Sparse Fieldset of this `ResourceObject` that can be encoded - /// as a `PrimaryResource`. - func sparse(with fields: [Description.Attributes.CodingKeys]) -> SparseField { - return SparseField(self, fields: fields) + /// as a `SparsePrimaryResource`. + func sparse(with fields: [Description.Attributes.CodingKeys]) -> SparseFieldset { + return SparseFieldset(self, fields: fields) } } diff --git a/Tests/JSONAPITests/SparseFields/SparseFieldEncodableTests.swift b/Tests/JSONAPITests/SparseFields/SparseFieldsetTests.swift similarity index 95% rename from Tests/JSONAPITests/SparseFields/SparseFieldEncodableTests.swift rename to Tests/JSONAPITests/SparseFields/SparseFieldsetTests.swift index 5a17075..7845119 100644 --- a/Tests/JSONAPITests/SparseFields/SparseFieldEncodableTests.swift +++ b/Tests/JSONAPITests/SparseFields/SparseFieldsetTests.swift @@ -1,5 +1,5 @@ // -// SparseFieldEncodableTests.swift +// SparseFieldsetTests.swift // // // Created by Mathew Polzin on 8/4/19. @@ -13,7 +13,7 @@ import JSONAPITesting class SparseFieldEncoderTests: XCTestCase { func test_FullEncode() { let jsonEncoder = JSONEncoder() - let sparseWithEverything = SparseField(testEverythingObject, fields: EverythingTest.Attributes.CodingKeys.allCases) + let sparseWithEverything = SparseFieldset(testEverythingObject, fields: EverythingTest.Attributes.CodingKeys.allCases) let encoded = try! jsonEncoder.encode(sparseWithEverything) @@ -30,7 +30,7 @@ class SparseFieldEncoderTests: XCTestCase { XCTAssertEqual(type, EverythingTest.jsonType) XCTAssertNil(relationships) - XCTAssertEqual(attributesDict?.count, 9) // note not 10 because one value is omitted + XCTAssertEqual(attributesDict?.count, 9) // note not 10 because one value is omitted intentionally at initialization XCTAssertEqual(attributesDict?["bool"] as? Bool, testEverythingObject[\.bool]) XCTAssertEqual(attributesDict?["int"] as? Int, @@ -52,7 +52,7 @@ class SparseFieldEncoderTests: XCTestCase { func test_PartialEncode() { let jsonEncoder = JSONEncoder() - let sparseObject = SparseField(testEverythingObject, fields: [.string, .bool, .array]) + let sparseObject = SparseFieldset(testEverythingObject, fields: [.string, .bool, .array]) let encoded = try! jsonEncoder.encode(sparseObject) From b98fb08353862277f416925392ffee1924b3fd9d Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Mon, 5 Aug 2019 08:13:18 -0700 Subject: [PATCH 023/235] Adding a bit of code doc --- Sources/JSONAPI/Document/ResourceBody.swift | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Sources/JSONAPI/Document/ResourceBody.swift b/Sources/JSONAPI/Document/ResourceBody.swift index b8f170c..bcc4d8b 100644 --- a/Sources/JSONAPI/Document/ResourceBody.swift +++ b/Sources/JSONAPI/Document/ResourceBody.swift @@ -5,6 +5,10 @@ // Created by Mathew Polzin on 11/10/18. // +/// A `MaybePrimaryResource` is just an optional `PrimaryResource`. +/// This protocol allows for `SingleResourceBody` to contain a `null` +/// data object where `ManyResourceBody` cannot (because an empty +/// array should be used for no results). public protocol MaybePrimaryResource: Equatable, Codable {} /// A PrimaryResource is a type that can be used in the body of a JSON API @@ -19,6 +23,8 @@ extension Optional: MaybePrimaryResource where Wrapped: PrimaryResource {} public protocol ResourceBody: Codable, Equatable { } +/// A `ResourceBody` that has the ability to take on more primary +/// resources by appending another similarly typed `ResourceBody`. public protocol AppendableResourceBody: ResourceBody { func appending(_ other: Self) -> Self } From 265cffe8f041fc13e58af6ebf99e88cd4cec9651 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Mon, 5 Aug 2019 08:17:38 -0700 Subject: [PATCH 024/235] Rename MaybePrimaryResource to OptionalPrimaryResource because I use 'Maybe' elsewhere to indicate a type-level distinction whereas this is a value-level distinction that really is just 'Optional' at play. --- Sources/JSONAPI/Document/ResourceBody.swift | 9 ++++----- .../Resource/Poly+PrimaryResource.swift | 18 +++++++++--------- 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/Sources/JSONAPI/Document/ResourceBody.swift b/Sources/JSONAPI/Document/ResourceBody.swift index bcc4d8b..0752331 100644 --- a/Sources/JSONAPI/Document/ResourceBody.swift +++ b/Sources/JSONAPI/Document/ResourceBody.swift @@ -5,17 +5,16 @@ // Created by Mathew Polzin on 11/10/18. // -/// A `MaybePrimaryResource` is just an optional `PrimaryResource`. /// This protocol allows for `SingleResourceBody` to contain a `null` /// data object where `ManyResourceBody` cannot (because an empty /// array should be used for no results). -public protocol MaybePrimaryResource: Equatable, Codable {} +public protocol OptionalPrimaryResource: Equatable, Codable {} /// A PrimaryResource is a type that can be used in the body of a JSON API /// document as the primary resource. -public protocol PrimaryResource: MaybePrimaryResource {} +public protocol PrimaryResource: OptionalPrimaryResource {} -extension Optional: MaybePrimaryResource where Wrapped: PrimaryResource {} +extension Optional: OptionalPrimaryResource where Wrapped: PrimaryResource {} /// A ResourceBody 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) @@ -33,7 +32,7 @@ public func +(_ left: R, right: R) -> R { return left.appending(right) } -public struct SingleResourceBody: ResourceBody { +public struct SingleResourceBody: ResourceBody { public let value: Entity public init(resourceObject: Entity) { diff --git a/Sources/JSONAPI/Resource/Poly+PrimaryResource.swift b/Sources/JSONAPI/Resource/Poly+PrimaryResource.swift index 66aaf63..e2ed829 100644 --- a/Sources/JSONAPI/Resource/Poly+PrimaryResource.swift +++ b/Sources/JSONAPI/Resource/Poly+PrimaryResource.swift @@ -30,28 +30,28 @@ extension Poly0: PrimaryResource { } // MARK: - 1 type -extension Poly1: PrimaryResource, MaybePrimaryResource where A: PolyWrapped {} +extension Poly1: PrimaryResource, OptionalPrimaryResource where A: PolyWrapped {} // MARK: - 2 types -extension Poly2: PrimaryResource, MaybePrimaryResource where A: PolyWrapped, B: PolyWrapped {} +extension Poly2: PrimaryResource, OptionalPrimaryResource where A: PolyWrapped, B: PolyWrapped {} // MARK: - 3 types -extension Poly3: PrimaryResource, MaybePrimaryResource where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped {} +extension Poly3: PrimaryResource, OptionalPrimaryResource where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped {} // MARK: - 4 types -extension Poly4: PrimaryResource, MaybePrimaryResource where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped {} +extension Poly4: PrimaryResource, OptionalPrimaryResource where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped {} // MARK: - 5 types -extension Poly5: PrimaryResource, MaybePrimaryResource where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped, E: PolyWrapped {} +extension Poly5: PrimaryResource, OptionalPrimaryResource where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped, E: PolyWrapped {} // MARK: - 6 types -extension Poly6: PrimaryResource, MaybePrimaryResource where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped, E: PolyWrapped, F: PolyWrapped {} +extension Poly6: PrimaryResource, OptionalPrimaryResource where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped, E: PolyWrapped, F: PolyWrapped {} // MARK: - 7 types -extension Poly7: PrimaryResource, MaybePrimaryResource where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped, E: PolyWrapped, F: PolyWrapped, G: PolyWrapped {} +extension Poly7: PrimaryResource, OptionalPrimaryResource where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped, E: PolyWrapped, F: PolyWrapped, G: PolyWrapped {} // MARK: - 8 types -extension Poly8: PrimaryResource, MaybePrimaryResource where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped, E: PolyWrapped, F: PolyWrapped, G: PolyWrapped, H: PolyWrapped {} +extension Poly8: PrimaryResource, OptionalPrimaryResource where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped, E: PolyWrapped, F: PolyWrapped, G: PolyWrapped, H: PolyWrapped {} // MARK: - 9 types -extension Poly9: PrimaryResource, MaybePrimaryResource where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped, E: PolyWrapped, F: PolyWrapped, G: PolyWrapped, H: PolyWrapped, I: PolyWrapped {} +extension Poly9: PrimaryResource, OptionalPrimaryResource where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped, E: PolyWrapped, F: PolyWrapped, G: PolyWrapped, H: PolyWrapped, I: PolyWrapped {} From a596ecaecc5be01fd16186b50232f36749e536b8 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Mon, 5 Aug 2019 09:23:44 -0700 Subject: [PATCH 025/235] A stab at separating out decoding enough to make it possible to use encode-only sparse fieldsets with JSONDocument --- Sources/JSONAPI/Document/Document.swift | 144 +++++++++--------- Sources/JSONAPI/Document/Includes.swift | 40 ++--- Sources/JSONAPI/Document/ResourceBody.swift | 72 +++++---- .../Resource/Poly+PrimaryResource.swift | 23 ++- 4 files changed, 159 insertions(+), 120 deletions(-) diff --git a/Sources/JSONAPI/Document/Document.swift b/Sources/JSONAPI/Document/Document.swift index 1a0fd5f..d0a34d9 100644 --- a/Sources/JSONAPI/Document/Document.swift +++ b/Sources/JSONAPI/Document/Document.swift @@ -7,19 +7,21 @@ import Poly -public protocol JSONAPIDocument: Codable, Equatable { - associatedtype PrimaryResourceBody: JSONAPI.ResourceBody - associatedtype MetaType: JSONAPI.Meta - associatedtype LinksType: JSONAPI.Links - associatedtype IncludeType: JSONAPI.Include - associatedtype APIDescription: APIDescriptionType - associatedtype Error: JSONAPIError +public protocol EncodableJSONAPIDocument: Equatable, Encodable { + associatedtype PrimaryResourceBody: JSONAPI.EncodableResourceBody + associatedtype MetaType: JSONAPI.Meta + associatedtype LinksType: JSONAPI.Links + associatedtype IncludeType: JSONAPI.Include + associatedtype APIDescription: APIDescriptionType + associatedtype Error: JSONAPIError - typealias Body = Document.Body + typealias Body = Document.Body - var body: Body { get } + var body: Body { get } } +public protocol JSONAPIDocument: EncodableJSONAPIDocument, Decodable where PrimaryResourceBody: JSONAPI.ResourceBody, IncludeType: Decodable {} + /// A JSON API Document represents the entire body /// of a JSON API request or the entire body of /// a JSON API response. @@ -27,7 +29,7 @@ public protocol JSONAPIDocument: Codable, Equatable { /// API uses snake case, you will want to use /// a conversion such as the one offerred by the /// Foundation JSONEncoder/Decoder: `KeyDecodingStrategy` -public struct Document: JSONAPIDocument { +public struct Document: EncodableJSONAPIDocument { public typealias Include = IncludeType /// The JSON API Spec calls this the JSON:API Object. It contains version @@ -273,66 +275,6 @@ extension Document { case links case jsonapi } - - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: RootCodingKeys.self) - - if let noData = NoAPIDescription() as? APIDescription { - apiDescription = noData - } else { - apiDescription = try container.decode(APIDescription.self, forKey: .jsonapi) - } - - let errors = try container.decodeIfPresent([Error].self, forKey: .errors) - - let meta: MetaType? - if let noMeta = NoMetadata() as? MetaType { - meta = noMeta - } else { - do { - meta = try container.decode(MetaType.self, forKey: .meta) - } catch { - meta = nil - } - } - - let links: LinksType? - if let noLinks = NoLinks() as? LinksType { - links = noLinks - } else { - do { - links = try container.decode(LinksType.self, forKey: .links) - } catch { - links = nil - } - } - - // If there are errors, there cannot be a body. Return errors and any metadata found. - if let errors = errors { - body = .errors(errors, meta: meta, links: links) - return - } - - let data: PrimaryResourceBody - if let noData = NoResourceBody() as? PrimaryResourceBody { - data = noData - } else { - data = try container.decode(PrimaryResourceBody.self, forKey: .data) - } - - let maybeIncludes = try container.decodeIfPresent(Includes.self, forKey: .included) - - // TODO come back to this and make robust - - guard let metaVal = meta else { - throw JSONAPIEncodingError.missingOrMalformedMetadata - } - guard let linksVal = links else { - throw JSONAPIEncodingError.missingOrMalformedLinks - } - - body = .data(.init(primary: data, includes: maybeIncludes ?? Includes.none, meta: metaVal, links: linksVal)) - } public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: RootCodingKeys.self) @@ -377,6 +319,68 @@ extension Document { } } +extension Document: Decodable, JSONAPIDocument where PrimaryResourceBody: ResourceBody, IncludeType: Decodable { + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: RootCodingKeys.self) + + if let noData = NoAPIDescription() as? APIDescription { + apiDescription = noData + } else { + apiDescription = try container.decode(APIDescription.self, forKey: .jsonapi) + } + + let errors = try container.decodeIfPresent([Error].self, forKey: .errors) + + let meta: MetaType? + if let noMeta = NoMetadata() as? MetaType { + meta = noMeta + } else { + do { + meta = try container.decode(MetaType.self, forKey: .meta) + } catch { + meta = nil + } + } + + let links: LinksType? + if let noLinks = NoLinks() as? LinksType { + links = noLinks + } else { + do { + links = try container.decode(LinksType.self, forKey: .links) + } catch { + links = nil + } + } + + // If there are errors, there cannot be a body. Return errors and any metadata found. + if let errors = errors { + body = .errors(errors, meta: meta, links: links) + return + } + + let data: PrimaryResourceBody + if let noData = NoResourceBody() as? PrimaryResourceBody { + data = noData + } else { + data = try container.decode(PrimaryResourceBody.self, forKey: .data) + } + + let maybeIncludes = try container.decodeIfPresent(Includes.self, forKey: .included) + + // TODO come back to this and make robust + + guard let metaVal = meta else { + throw JSONAPIEncodingError.missingOrMalformedMetadata + } + guard let linksVal = links else { + throw JSONAPIEncodingError.missingOrMalformedLinks + } + + body = .data(.init(primary: data, includes: maybeIncludes ?? Includes.none, meta: metaVal, links: linksVal)) + } +} + // MARK: - CustomStringConvertible extension Document: CustomStringConvertible { diff --git a/Sources/JSONAPI/Document/Includes.swift b/Sources/JSONAPI/Document/Includes.swift index 8682ee9..1608b2d 100644 --- a/Sources/JSONAPI/Document/Includes.swift +++ b/Sources/JSONAPI/Document/Includes.swift @@ -7,9 +7,9 @@ import Poly -public typealias Include = JSONPoly +public typealias Include = EncodableJSONPoly -public struct Includes: Codable, Equatable { +public struct Includes: Encodable, Equatable { public static var none: Includes { return .init(values: []) } let values: [I] @@ -17,23 +17,6 @@ public struct Includes: Codable, Equatable { public init(values: [I]) { self.values = values } - - public init(from decoder: Decoder) throws { - var container = try decoder.unkeyedContainer() - - // If not parsing includes, no need to loop over them. - guard I.self != NoIncludes.self else { - values = [] - return - } - - var valueAggregator = [I]() - while !container.isAtEnd { - valueAggregator.append(try container.decode(I.self)) - } - - values = valueAggregator - } public func encode(to encoder: Encoder) throws { var container = encoder.unkeyedContainer() @@ -52,6 +35,25 @@ public struct Includes: Codable, Equatable { } } +extension Includes: Decodable where I: Decodable { + public init(from decoder: Decoder) throws { + var container = try decoder.unkeyedContainer() + + // If not parsing includes, no need to loop over them. + guard I.self != NoIncludes.self else { + values = [] + return + } + + var valueAggregator = [I]() + while !container.isAtEnd { + valueAggregator.append(try container.decode(I.self)) + } + + values = valueAggregator + } +} + extension Includes { public func appending(_ other: Includes) -> Includes { return Includes(values: values + other.values) diff --git a/Sources/JSONAPI/Document/ResourceBody.swift b/Sources/JSONAPI/Document/ResourceBody.swift index 0752331..3e9141d 100644 --- a/Sources/JSONAPI/Document/ResourceBody.swift +++ b/Sources/JSONAPI/Document/ResourceBody.swift @@ -5,26 +5,36 @@ // Created by Mathew Polzin on 11/10/18. // +public protocol OptionalEncodablePrimaryResource: Equatable, Encodable {} + +public protocol EncodablePrimaryResource: OptionalEncodablePrimaryResource {} + /// This protocol allows for `SingleResourceBody` to contain a `null` /// data object where `ManyResourceBody` cannot (because an empty /// array should be used for no results). -public protocol OptionalPrimaryResource: Equatable, Codable {} +public protocol OptionalPrimaryResource: OptionalEncodablePrimaryResource, Decodable {} /// A PrimaryResource is a type that can be used in the body of a JSON API /// document as the primary resource. -public protocol PrimaryResource: OptionalPrimaryResource {} +public protocol PrimaryResource: EncodablePrimaryResource, OptionalPrimaryResource {} + +extension Optional: OptionalEncodablePrimaryResource where Wrapped: EncodablePrimaryResource {} extension Optional: OptionalPrimaryResource where Wrapped: PrimaryResource {} +/// An `EncodableResourceBody` is a `ResourceBody` that only supports being +/// encoded. It is actually weaker than `ResourceBody`, which supports both encoding +/// and decoding. +public protocol EncodableResourceBody: Equatable, Encodable {} + /// A ResourceBody 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) /// or it can contain many resources (and array with zero or more entries). -public protocol ResourceBody: Codable, Equatable { -} +public protocol ResourceBody: Decodable, EncodableResourceBody {} /// A `ResourceBody` that has the ability to take on more primary /// resources by appending another similarly typed `ResourceBody`. -public protocol AppendableResourceBody: ResourceBody { +public protocol AppendableResourceBody { func appending(_ other: Self) -> Self } @@ -32,7 +42,7 @@ public func +(_ left: R, right: R) -> R { return left.appending(right) } -public struct SingleResourceBody: ResourceBody { +public struct SingleResourceBody: EncodableResourceBody { public let value: Entity public init(resourceObject: Entity) { @@ -40,7 +50,7 @@ public struct SingleResourceBody: Resou } } -public struct ManyResourceBody: AppendableResourceBody { +public struct ManyResourceBody: EncodableResourceBody, AppendableResourceBody { public let values: [Entity] public init(resourceObjects: [Entity]) { @@ -60,19 +70,6 @@ public struct NoResourceBody: ResourceBody { // MARK: Codable extension SingleResourceBody { - public init(from decoder: Decoder) throws { - let container = try decoder.singleValueContainer() - - let anyNil: Any? = nil - if container.decodeNil(), - let val = anyNil as? Entity { - value = val - return - } - - value = try container.decode(Entity.self) - } - public func encode(to encoder: Encoder) throws { var container = encoder.singleValueContainer() @@ -87,16 +84,22 @@ extension SingleResourceBody { } } -extension ManyResourceBody { - public init(from decoder: Decoder) throws { - var container = try decoder.unkeyedContainer() - var valueAggregator = [Entity]() - while !container.isAtEnd { - valueAggregator.append(try container.decode(Entity.self)) - } - values = valueAggregator - } +extension SingleResourceBody: Decodable, ResourceBody where Entity: OptionalPrimaryResource { + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + + let anyNil: Any? = nil + if container.decodeNil(), + let val = anyNil as? Entity { + value = val + return + } + value = try container.decode(Entity.self) + } +} + +extension ManyResourceBody { public func encode(to encoder: Encoder) throws { var container = encoder.unkeyedContainer() @@ -106,6 +109,17 @@ extension ManyResourceBody { } } +extension ManyResourceBody: Decodable, ResourceBody where Entity: PrimaryResource { + public init(from decoder: Decoder) throws { + var container = try decoder.unkeyedContainer() + var valueAggregator = [Entity]() + while !container.isAtEnd { + valueAggregator.append(try container.decode(Entity.self)) + } + values = valueAggregator + } +} + // MARK: CustomStringConvertible extension SingleResourceBody: CustomStringConvertible { diff --git a/Sources/JSONAPI/Resource/Poly+PrimaryResource.swift b/Sources/JSONAPI/Resource/Poly+PrimaryResource.swift index e2ed829..f1cad3a 100644 --- a/Sources/JSONAPI/Resource/Poly+PrimaryResource.swift +++ b/Sources/JSONAPI/Resource/Poly+PrimaryResource.swift @@ -15,9 +15,10 @@ import Poly /// disparate types under one roof for /// the purposes of JSON API compliant /// encoding or decoding. -public typealias JSONPoly = Poly & PrimaryResource +public typealias EncodableJSONPoly = Poly & EncodablePrimaryResource -public typealias PolyWrapped = Codable & Equatable +public typealias EncodablePolyWrapped = Encodable & Equatable +public typealias PolyWrapped = EncodablePolyWrapped & Decodable extension Poly0: PrimaryResource { public init(from decoder: Decoder) throws { @@ -30,28 +31,46 @@ extension Poly0: PrimaryResource { } // MARK: - 1 type +extension Poly1: EncodablePrimaryResource, OptionalEncodablePrimaryResource where A: EncodablePolyWrapped {} + extension Poly1: PrimaryResource, OptionalPrimaryResource where A: PolyWrapped {} // MARK: - 2 types +extension Poly2: EncodablePrimaryResource, OptionalEncodablePrimaryResource where A: EncodablePolyWrapped, B: EncodablePolyWrapped {} + extension Poly2: PrimaryResource, OptionalPrimaryResource where A: PolyWrapped, B: PolyWrapped {} // MARK: - 3 types +extension Poly3: EncodablePrimaryResource, OptionalEncodablePrimaryResource where A: EncodablePolyWrapped, B: EncodablePolyWrapped, C: EncodablePolyWrapped {} + extension Poly3: PrimaryResource, OptionalPrimaryResource where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped {} // MARK: - 4 types +extension Poly4: EncodablePrimaryResource, OptionalEncodablePrimaryResource where A: EncodablePolyWrapped, B: EncodablePolyWrapped, C: EncodablePolyWrapped, D: EncodablePolyWrapped {} + extension Poly4: PrimaryResource, OptionalPrimaryResource where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped {} // MARK: - 5 types +extension Poly5: EncodablePrimaryResource, OptionalEncodablePrimaryResource where A: EncodablePolyWrapped, B: EncodablePolyWrapped, C: EncodablePolyWrapped, D: EncodablePolyWrapped, E: EncodablePolyWrapped {} + extension Poly5: PrimaryResource, OptionalPrimaryResource where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped, E: PolyWrapped {} // MARK: - 6 types +extension Poly6: EncodablePrimaryResource, OptionalEncodablePrimaryResource where A: EncodablePolyWrapped, B: EncodablePolyWrapped, C: EncodablePolyWrapped, D: EncodablePolyWrapped, E: EncodablePolyWrapped, F: EncodablePolyWrapped {} + extension Poly6: PrimaryResource, OptionalPrimaryResource where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped, E: PolyWrapped, F: PolyWrapped {} // MARK: - 7 types +extension Poly7: EncodablePrimaryResource, OptionalEncodablePrimaryResource where A: EncodablePolyWrapped, B: EncodablePolyWrapped, C: EncodablePolyWrapped, D: EncodablePolyWrapped, E: EncodablePolyWrapped, F: EncodablePolyWrapped, G: EncodablePolyWrapped {} + extension Poly7: PrimaryResource, OptionalPrimaryResource where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped, E: PolyWrapped, F: PolyWrapped, G: PolyWrapped {} // MARK: - 8 types +extension Poly8: EncodablePrimaryResource, OptionalEncodablePrimaryResource where A: EncodablePolyWrapped, B: EncodablePolyWrapped, C: EncodablePolyWrapped, D: EncodablePolyWrapped, E: EncodablePolyWrapped, F: EncodablePolyWrapped, G: EncodablePolyWrapped, H: EncodablePolyWrapped {} + extension Poly8: PrimaryResource, OptionalPrimaryResource where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped, E: PolyWrapped, F: PolyWrapped, G: PolyWrapped, H: PolyWrapped {} // MARK: - 9 types +extension Poly9: EncodablePrimaryResource, OptionalEncodablePrimaryResource where A: EncodablePolyWrapped, B: EncodablePolyWrapped, C: EncodablePolyWrapped, D: EncodablePolyWrapped, E: EncodablePolyWrapped, F: EncodablePolyWrapped, G: EncodablePolyWrapped, H: EncodablePolyWrapped, I: EncodablePolyWrapped {} + extension Poly9: PrimaryResource, OptionalPrimaryResource where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped, E: PolyWrapped, F: PolyWrapped, G: PolyWrapped, H: PolyWrapped, I: PolyWrapped {} From 0c7c7edcab854ce25ca8c0cc889d770ba08fb40f Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Mon, 5 Aug 2019 14:48:08 -0700 Subject: [PATCH 026/235] Add sparse fieldset resource body tests --- .../JSONAPI/SparseFields/SparseFieldset.swift | 2 +- .../ResourceBody/ResourceBodyTests.swift | 93 ++++++++++++++++++- 2 files changed, 93 insertions(+), 2 deletions(-) diff --git a/Sources/JSONAPI/SparseFields/SparseFieldset.swift b/Sources/JSONAPI/SparseFields/SparseFieldset.swift index 8e0979b..9f1e579 100644 --- a/Sources/JSONAPI/SparseFields/SparseFieldset.swift +++ b/Sources/JSONAPI/SparseFields/SparseFieldset.swift @@ -10,7 +10,7 @@ public struct SparseFieldset< MetaType: JSONAPI.Meta, LinksType: JSONAPI.Links, EntityRawIdType: JSONAPI.MaybeRawId ->: Encodable where Description.Attributes: SparsableAttributes { +>: EncodablePrimaryResource where Description.Attributes: SparsableAttributes { public typealias Resource = JSONAPI.ResourceObject diff --git a/Tests/JSONAPITests/ResourceBody/ResourceBodyTests.swift b/Tests/JSONAPITests/ResourceBody/ResourceBodyTests.swift index 9bc3f2c..6b12256 100644 --- a/Tests/JSONAPITests/ResourceBody/ResourceBodyTests.swift +++ b/Tests/JSONAPITests/ResourceBody/ResourceBodyTests.swift @@ -102,6 +102,93 @@ class ResourceBodyTests: XCTestCase { } } +// MARK: - Sparse Fieldsets + +extension ResourceBodyTests { + func test_SparseSingleBodyEncode() { + let sparseArticle = Article(attributes: .init(title: "hello world"), + relationships: .none, + meta: .none, + links: .none) + .sparse(with: []) + let body = SingleResourceBody(resourceObject: sparseArticle) + + let encoded = try! JSONEncoder().encode(body) + + let deserialized = try! JSONSerialization.jsonObject(with: encoded, + options: []) + + let deserializedObj = deserialized as? [String: Any] + + XCTAssertNotNil(deserializedObj?["id"]) + XCTAssertEqual(deserializedObj?["id"] as? String, sparseArticle.resourceObject.id.rawValue) + + XCTAssertNotNil(deserializedObj?["type"]) + XCTAssertEqual(deserializedObj?["type"] as? String, Article.jsonType) + + XCTAssertEqual((deserializedObj?["attributes"] as? [String: Any])?.count, 0) + + XCTAssertNil(deserializedObj?["relationships"]) + } + + func test_SparseManyBodyEncode() { + let fields: [Article.Attributes.CodingKeys] = [.title] + let sparseArticle1 = Article(attributes: .init(title: "hello world"), + relationships: .none, + meta: .none, + links: .none) + .sparse(with: fields) + let sparseArticle2 = Article(attributes: .init(title: "hello two"), + relationships: .none, + meta: .none, + links: .none) + .sparse(with: fields) + + let body = ManyResourceBody(resourceObjects: [sparseArticle1, sparseArticle2]) + + let encoded = try! JSONEncoder().encode(body) + + let deserialized = try! JSONSerialization.jsonObject(with: encoded, + options: []) + + let deserializedObj = deserialized as? [Any] + + XCTAssertEqual(deserializedObj?.count, 2) + + guard let deserializedObj1 = deserializedObj?.first as? [String: Any], + let deserializedObj2 = deserializedObj?.last as? [String: Any] else { + XCTFail("Expected to deserialize two objects from array") + return + } + + // first article + XCTAssertNotNil(deserializedObj1["id"]) + XCTAssertEqual(deserializedObj1["id"] as? String, sparseArticle1.resourceObject.id.rawValue) + + XCTAssertNotNil(deserializedObj1["type"]) + XCTAssertEqual(deserializedObj1["type"] as? String, Article.jsonType) + + XCTAssertEqual((deserializedObj1["attributes"] as? [String: Any])?.count, 1) + XCTAssertEqual((deserializedObj1["attributes"] as? [String: Any])?["title"] as? String, "hello world") + + XCTAssertNil(deserializedObj1["relationships"]) + + // second article + XCTAssertNotNil(deserializedObj2["id"]) + XCTAssertEqual(deserializedObj2["id"] as? String, sparseArticle2.resourceObject.id.rawValue) + + XCTAssertNotNil(deserializedObj2["type"]) + XCTAssertEqual(deserializedObj2["type"] as? String, Article.jsonType) + + XCTAssertEqual((deserializedObj2["attributes"] as? [String: Any])?.count, 1) + XCTAssertEqual((deserializedObj2["attributes"] as? [String: Any])?["title"] as? String, "hello two") + + XCTAssertNil(deserializedObj2["relationships"]) + } +} + +// MARK: - Test Types + extension ResourceBodyTests { enum ArticleType: ResourceObjectDescription { @@ -109,8 +196,12 @@ extension ResourceBodyTests { typealias Relationships = NoRelationships - struct Attributes: JSONAPI.Attributes { + struct Attributes: JSONAPI.SparsableAttributes { let title: Attribute + + public enum CodingKeys: String, Equatable, CodingKey { + case title + } } } From 9a07cf7066942c68b7986f06010b5c5f57394d83 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Mon, 5 Aug 2019 14:51:18 -0700 Subject: [PATCH 027/235] Add test class placeholder --- .../SparseFields/SparseFieldEncoderTests.swift | 15 +++++++++++++++ .../SparseFields/SparseFieldsetTests.swift | 2 +- 2 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 Tests/JSONAPITests/SparseFields/SparseFieldEncoderTests.swift diff --git a/Tests/JSONAPITests/SparseFields/SparseFieldEncoderTests.swift b/Tests/JSONAPITests/SparseFields/SparseFieldEncoderTests.swift new file mode 100644 index 0000000..a9790ff --- /dev/null +++ b/Tests/JSONAPITests/SparseFields/SparseFieldEncoderTests.swift @@ -0,0 +1,15 @@ +// +// SparseFieldEncoderTests.swift +// +// +// Created by Mathew Polzin on 8/5/19. +// + +import XCTest +import JSONAPI + +class SparseFieldEncoderTests: XCTestCase { + func test_placeholder() { + // TODO: write tests + } +} diff --git a/Tests/JSONAPITests/SparseFields/SparseFieldsetTests.swift b/Tests/JSONAPITests/SparseFields/SparseFieldsetTests.swift index 7845119..003dcc5 100644 --- a/Tests/JSONAPITests/SparseFields/SparseFieldsetTests.swift +++ b/Tests/JSONAPITests/SparseFields/SparseFieldsetTests.swift @@ -10,7 +10,7 @@ import Foundation import JSONAPI import JSONAPITesting -class SparseFieldEncoderTests: XCTestCase { +class SparseFieldsetTests: XCTestCase { func test_FullEncode() { let jsonEncoder = JSONEncoder() let sparseWithEverything = SparseFieldset(testEverythingObject, fields: EverythingTest.Attributes.CodingKeys.allCases) From 32d584099f0b1640bd564755b9b485e3bc867083 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Mon, 5 Aug 2019 14:51:48 -0700 Subject: [PATCH 028/235] Update linuxmain --- Tests/JSONAPITests/XCTestManifests.swift | 43 ++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/Tests/JSONAPITests/XCTestManifests.swift b/Tests/JSONAPITests/XCTestManifests.swift index 46edbba..3011985 100644 --- a/Tests/JSONAPITests/XCTestManifests.swift +++ b/Tests/JSONAPITests/XCTestManifests.swift @@ -173,6 +173,24 @@ extension DocumentTests { ] } +extension EmptyObjectDecoderTests { + // DO NOT MODIFY: This is autogenerated, use: + // `swift test --generate-linuxmain` + // to regenerate. + static let __allTests__EmptyObjectDecoderTests = [ + ("testEmptyArray", testEmptyArray), + ("testEmptyStruct", testEmptyStruct), + ("testKeysAndCodingPath", testKeysAndCodingPath), + ("testNonEmptyArray", testNonEmptyArray), + ("testNonEmptyStruct", testNonEmptyStruct), + ("testWantingNestedKeyed", testWantingNestedKeyed), + ("testWantingNestedUnkeyed", testWantingNestedUnkeyed), + ("testWantingNil", testWantingNil), + ("testWantingSingleValue", testWantingSingleValue), + ("testWantsSuper", testWantsSuper), + ] +} + extension EntityTests { // DO NOT MODIFY: This is autogenerated, use: // `swift test --generate-linuxmain` @@ -406,6 +424,28 @@ extension ResourceBodyTests { ("test_manyResourceBodyMerge", test_manyResourceBodyMerge), ("test_singleResourceBody", test_singleResourceBody), ("test_singleResourceBody_encode", test_singleResourceBody_encode), + ("test_SparseManyBodyEncode", test_SparseManyBodyEncode), + ("test_SparseSingleBodyEncode", test_SparseSingleBodyEncode), + ] +} + +extension SparseFieldEncoderTests { + // DO NOT MODIFY: This is autogenerated, use: + // `swift test --generate-linuxmain` + // to regenerate. + static let __allTests__SparseFieldEncoderTests = [ + ("test_placeholder", test_placeholder), + ] +} + +extension SparseFieldsetTests { + // DO NOT MODIFY: This is autogenerated, use: + // `swift test --generate-linuxmain` + // to regenerate. + static let __allTests__SparseFieldsetTests = [ + ("test_FullEncode", test_FullEncode), + ("test_PartialEncode", test_PartialEncode), + ("test_sparseFieldsMethod", test_sparseFieldsMethod), ] } @@ -427,6 +467,7 @@ public func __allTests() -> [XCTestCaseEntry] { testCase(ComputedPropertiesTests.__allTests__ComputedPropertiesTests), testCase(CustomAttributesTests.__allTests__CustomAttributesTests), testCase(DocumentTests.__allTests__DocumentTests), + testCase(EmptyObjectDecoderTests.__allTests__EmptyObjectDecoderTests), testCase(EntityTests.__allTests__EntityTests), testCase(IncludedTests.__allTests__IncludedTests), testCase(LinksTests.__allTests__LinksTests), @@ -435,6 +476,8 @@ public func __allTests() -> [XCTestCaseEntry] { testCase(PolyTests.__allTests__PolyTests), testCase(RelationshipTests.__allTests__RelationshipTests), testCase(ResourceBodyTests.__allTests__ResourceBodyTests), + testCase(SparseFieldEncoderTests.__allTests__SparseFieldEncoderTests), + testCase(SparseFieldsetTests.__allTests__SparseFieldsetTests), testCase(TransformerTests.__allTests__TransformerTests), ] } From 61f2edb59a3ba5fa205fc1184e948c09f537cdf5 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Mon, 5 Aug 2019 15:21:59 -0700 Subject: [PATCH 029/235] Add tests for sparse fieldset includes --- .../JSONAPI/SparseFields/SparseFieldset.swift | 6 + .../JSONAPITests/Includes/IncludeTests.swift | 172 +++++++++++++++++- 2 files changed, 176 insertions(+), 2 deletions(-) diff --git a/Sources/JSONAPI/SparseFields/SparseFieldset.swift b/Sources/JSONAPI/SparseFields/SparseFieldset.swift index 9f1e579..3540334 100644 --- a/Sources/JSONAPI/SparseFields/SparseFieldset.swift +++ b/Sources/JSONAPI/SparseFields/SparseFieldset.swift @@ -38,3 +38,9 @@ public extension ResourceObject where Description.Attributes: SparsableAttribute return SparseFieldset(self, fields: fields) } } + +public extension ResourceObject where Description.Attributes: SparsableAttributes { + + /// The Sparse Fieldset type for this `ResourceObject` + typealias SparseType = SparseFieldset +} diff --git a/Tests/JSONAPITests/Includes/IncludeTests.swift b/Tests/JSONAPITests/Includes/IncludeTests.swift index 7f8f0f8..2254efe 100644 --- a/Tests/JSONAPITests/Includes/IncludeTests.swift +++ b/Tests/JSONAPITests/Includes/IncludeTests.swift @@ -178,6 +178,8 @@ class IncludedTests: XCTestCase { } } +// MARK: - Appending + extension IncludedTests { func test_appending() { let include1 = Includes>(values: [.init(TestEntity8(attributes: .none, relationships: .none, meta: .none, links: .none)), .init(TestEntity9(attributes: .none, relationships: .none, meta: .none, links: .none)), .init(TestEntity8(attributes: .none, relationships: .none, meta: .none, links: .none))]) @@ -190,6 +192,162 @@ extension IncludedTests { } } +// MARK: - Sparse Fieldsets + +extension IncludedTests { + func test_OneSparseIncludeType() { + let include1 = TestEntity(attributes: .init(foo: "hello", + bar: 10), + relationships: .none, + meta: .none, + links: .none) + .sparse(with: [.foo]) + + let includes: Includes> = .init(values: [.init(include1)]) + + let encoded = try! JSONEncoder().encode(includes) + + let deserialized = try! JSONSerialization.jsonObject(with: encoded, + options: []) + + let deserializedObj = deserialized as? [Any] + + XCTAssertEqual(deserializedObj?.count, 1) + + guard let deserializedObj1 = deserializedObj?.first as? [String: Any] else { + XCTFail("Expected to deserialize one object from array") + return + } + + XCTAssertNotNil(deserializedObj1["id"]) + XCTAssertEqual(deserializedObj1["id"] as? String, include1.resourceObject.id.rawValue) + + XCTAssertNotNil(deserializedObj1["type"]) + XCTAssertEqual(deserializedObj1["type"] as? String, TestEntity.jsonType) + + XCTAssertEqual((deserializedObj1["attributes"] as? [String: Any])?.count, 1) + XCTAssertEqual((deserializedObj1["attributes"] as? [String: Any])?["foo"] as? String, "hello") + + XCTAssertNil(deserializedObj1["relationships"]) + } + + func test_TwoSparseIncludeTypes() { + let include1 = TestEntity(attributes: .init(foo: "hello", + bar: 10), + relationships: .none, + meta: .none, + links: .none) + .sparse(with: [.foo]) + + let include2 = TestEntity2(attributes: .init(foo: "world", + bar: 2), + relationships: .init(entity1: "1234"), + meta: .none, + links: .none) + .sparse(with: [.bar]) + + let includes: Includes> = .init(values: [.init(include1), .init(include2)]) + + let encoded = try! JSONEncoder().encode(includes) + + let deserialized = try! JSONSerialization.jsonObject(with: encoded, + options: []) + + let deserializedObj = deserialized as? [Any] + + XCTAssertEqual(deserializedObj?.count, 2) + + guard let deserializedObj1 = deserializedObj?.first as? [String: Any], + let deserializedObj2 = deserializedObj?.last as? [String: Any] else { + XCTFail("Expected to deserialize two objects from array") + return + } + + // first include + XCTAssertNotNil(deserializedObj1["id"]) + XCTAssertEqual(deserializedObj1["id"] as? String, include1.resourceObject.id.rawValue) + + XCTAssertNotNil(deserializedObj1["type"]) + XCTAssertEqual(deserializedObj1["type"] as? String, TestEntity.jsonType) + + XCTAssertEqual((deserializedObj1["attributes"] as? [String: Any])?.count, 1) + XCTAssertEqual((deserializedObj1["attributes"] as? [String: Any])?["foo"] as? String, "hello") + + XCTAssertNil(deserializedObj1["relationships"]) + + // second include + XCTAssertNotNil(deserializedObj2["id"]) + XCTAssertEqual(deserializedObj2["id"] as? String, include2.resourceObject.id.rawValue) + + XCTAssertNotNil(deserializedObj2["type"]) + XCTAssertEqual(deserializedObj2["type"] as? String, TestEntity2.jsonType) + + XCTAssertEqual((deserializedObj2["attributes"] as? [String: Any])?.count, 1) + XCTAssertEqual((deserializedObj2["attributes"] as? [String: Any])?["bar"] as? Int, 2) + + XCTAssertNotNil(deserializedObj2["relationships"]) + XCTAssertNotNil((deserializedObj2["relationships"] as? [String: Any])?["entity1"]) + } + + func test_ComboSparseAndFullIncludeTypes() { + let include1 = TestEntity(attributes: .init(foo: "hello", + bar: 10), + relationships: .none, + meta: .none, + links: .none) + .sparse(with: [.foo]) + + let include2 = TestEntity2(attributes: .init(foo: "world", + bar: 2), + relationships: .init(entity1: "1234"), + meta: .none, + links: .none) + + let includes: Includes> = .init(values: [.init(include1), .init(include2)]) + + let encoded = try! JSONEncoder().encode(includes) + + let deserialized = try! JSONSerialization.jsonObject(with: encoded, + options: []) + + let deserializedObj = deserialized as? [Any] + + XCTAssertEqual(deserializedObj?.count, 2) + + guard let deserializedObj1 = deserializedObj?.first as? [String: Any], + let deserializedObj2 = deserializedObj?.last as? [String: Any] else { + XCTFail("Expected to deserialize two objects from array") + return + } + + // first include + XCTAssertNotNil(deserializedObj1["id"]) + XCTAssertEqual(deserializedObj1["id"] as? String, include1.resourceObject.id.rawValue) + + XCTAssertNotNil(deserializedObj1["type"]) + XCTAssertEqual(deserializedObj1["type"] as? String, TestEntity.jsonType) + + XCTAssertEqual((deserializedObj1["attributes"] as? [String: Any])?.count, 1) + XCTAssertEqual((deserializedObj1["attributes"] as? [String: Any])?["foo"] as? String, "hello") + + XCTAssertNil(deserializedObj1["relationships"]) + + // second include + XCTAssertNotNil(deserializedObj2["id"]) + XCTAssertEqual(deserializedObj2["id"] as? String, include2.id.rawValue) + + XCTAssertNotNil(deserializedObj2["type"]) + XCTAssertEqual(deserializedObj2["type"] as? String, TestEntity2.jsonType) + + XCTAssertEqual((deserializedObj2["attributes"] as? [String: Any])?.count, 2) + XCTAssertEqual((deserializedObj2["attributes"] as? [String: Any])?["foo"] as? String, "world") + XCTAssertEqual((deserializedObj2["attributes"] as? [String: Any])?["bar"] as? Int, 2) + + XCTAssertNotNil(deserializedObj2["relationships"]) + XCTAssertNotNil((deserializedObj2["relationships"] as? [String: Any])?["entity1"]) + } +} + // MARK: - Test types extension IncludedTests { enum TestEntityType: ResourceObjectDescription { @@ -198,9 +356,14 @@ extension IncludedTests { public static var jsonType: String { return "test_entity1" } - public struct Attributes: JSONAPI.Attributes { + public struct Attributes: JSONAPI.SparsableAttributes { let foo: Attribute let bar: Attribute + + public enum CodingKeys: String, Equatable, CodingKey { + case foo + case bar + } } } @@ -214,9 +377,14 @@ extension IncludedTests { let entity1: ToOneRelationship } - public struct Attributes: JSONAPI.Attributes { + public struct Attributes: JSONAPI.SparsableAttributes { let foo: Attribute let bar: Attribute + + public enum CodingKeys: String, Equatable, CodingKey { + case foo + case bar + } } } From 83f7a7b60e9b7d19dbcf75f4e05d200ae78c6f85 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Mon, 5 Aug 2019 16:02:40 -0700 Subject: [PATCH 030/235] Add sparse document tests --- Sources/JSONAPI/Resource/ResourceObject.swift | 4 +- .../JSONAPITests/Document/DocumentTests.swift | 230 ++++++++++++++++++ .../JSONAPITests/Includes/IncludeTests.swift | 26 +- 3 files changed, 246 insertions(+), 14 deletions(-) diff --git a/Sources/JSONAPI/Resource/ResourceObject.swift b/Sources/JSONAPI/Resource/ResourceObject.swift index af964a7..7fefbec 100644 --- a/Sources/JSONAPI/Resource/ResourceObject.swift +++ b/Sources/JSONAPI/Resource/ResourceObject.swift @@ -15,10 +15,12 @@ public protocol Relationships: Codable & Equatable {} /// properties of any types that are JSON encodable. public protocol Attributes: Codable & Equatable {} +public typealias SparsableCodingKey = CodingKey & Equatable + /// Attributes containing publicly accessible and `Equatable` /// CodingKeys are required to support Sparse Fieldsets. public protocol SparsableAttributes: Attributes { - associatedtype CodingKeys: CodingKey & Equatable + associatedtype CodingKeys: SparsableCodingKey } /// Can be used as `Relationships` Type for Entities that do not diff --git a/Tests/JSONAPITests/Document/DocumentTests.swift b/Tests/JSONAPITests/Document/DocumentTests.swift index 2aa8229..5f9c18b 100644 --- a/Tests/JSONAPITests/Document/DocumentTests.swift +++ b/Tests/JSONAPITests/Document/DocumentTests.swift @@ -926,6 +926,217 @@ extension DocumentTests { } } +// MARK: Sparse Fieldset Documents + +extension DocumentTests { + func test_sparsePrimaryResource() { + let primaryResource = Book(attributes: .init(pageCount: 100), + relationships: .init(author: "1234", + series: []), + meta: .none, + links: .none) + .sparse(with: [.pageCount]) + + let document = Document< + SingleResourceBody, + NoMetadata, + NoLinks, + NoIncludes, + NoAPIDescription, + UnknownJSONAPIError + >(apiDescription: .none, + body: .init(resourceObject: primaryResource), + includes: .none, + meta: .none, + links: .none) + + let encoded = try! JSONEncoder().encode(document) + + let deserialized = try! JSONSerialization.jsonObject(with: encoded, + options: []) + + let deserializedObj = deserialized as? [String: Any] + + guard let deserializedObj1 = deserializedObj?["data"] as? [String: Any] else { + XCTFail("Expected to deserialize one object from document data") + return + } + + XCTAssertNotNil(deserializedObj1["id"]) + XCTAssertEqual(deserializedObj1["id"] as? String, primaryResource.resourceObject.id.rawValue) + + XCTAssertNotNil(deserializedObj1["type"]) + XCTAssertEqual(deserializedObj1["type"] as? String, Book.jsonType) + + XCTAssertEqual((deserializedObj1["attributes"] as? [String: Any])?.count, 1) + XCTAssertEqual((deserializedObj1["attributes"] as? [String: Any])?["pageCount"] as? Int, 100) + + XCTAssertNotNil(deserializedObj1["relationships"]) + } + + func test_sparsePrimaryResourceOptionalAndNil() { + let document = Document< + SingleResourceBody, + NoMetadata, + NoLinks, + NoIncludes, + NoAPIDescription, + UnknownJSONAPIError + >(apiDescription: .none, + body: .init(resourceObject: nil), + includes: .none, + meta: .none, + links: .none) + + let encoded = try! JSONEncoder().encode(document) + + let deserialized = try! JSONSerialization.jsonObject(with: encoded, + options: []) + + let deserializedObj = deserialized as? [String: Any] + + XCTAssertNotNil(deserializedObj?["data"] as? NSNull) + } + + func test_sparseIncludeFullPrimaryResource() { + let bookInclude = Book(id: "444", + attributes: .init(pageCount: 113), + relationships: .init(author: "1234", + series: ["443"]), + meta: .none, + links: .none) + .sparse(with: []) + + let primaryResource = Book(id: "443", + attributes: .init(pageCount: 100), + relationships: .init(author: "1234", + series: ["444"]), + meta: .none, + links: .none) + + let document = Document< + SingleResourceBody, + NoMetadata, + NoLinks, + Include1, + NoAPIDescription, + UnknownJSONAPIError + >(apiDescription: .none, + body: .init(resourceObject: primaryResource), + includes: .init(values: [.init(bookInclude)]), + meta: .none, + links: .none) + + let encoded = try! JSONEncoder().encode(document) + + let deserialized = try! JSONSerialization.jsonObject(with: encoded, + options: []) + + let deserializedObj = deserialized as? [String: Any] + + guard let deserializedObj1 = deserializedObj?["data"] as? [String: Any] else { + XCTFail("Expected to deserialize one object from document data") + return + } + + XCTAssertNotNil(deserializedObj1["id"]) + XCTAssertEqual(deserializedObj1["id"] as? String, primaryResource.id.rawValue) + + XCTAssertNotNil(deserializedObj1["type"]) + XCTAssertEqual(deserializedObj1["type"] as? String, Book.jsonType) + + XCTAssertEqual((deserializedObj1["attributes"] as? [String: Any])?.count, 1) + XCTAssertEqual((deserializedObj1["attributes"] as? [String: Any])?["pageCount"] as? Int, 100) + + XCTAssertNotNil(deserializedObj1["relationships"]) + + guard let deserializedIncludes = deserializedObj?["included"] as? [Any], + let deserializedObj2 = deserializedIncludes.first as? [String: Any] else { + XCTFail("Expected to deserialize one incude object") + return + } + + XCTAssertNotNil(deserializedObj2["id"]) + XCTAssertEqual(deserializedObj2["id"] as? String, bookInclude.resourceObject.id.rawValue) + + XCTAssertNotNil(deserializedObj2["type"]) + XCTAssertEqual(deserializedObj2["type"] as? String, Book.jsonType) + + XCTAssertEqual((deserializedObj2["attributes"] as? [String: Any])?.count, 0) + + XCTAssertNotNil(deserializedObj2["relationships"]) + } + + func test_sparseIncludeSparsePrimaryResource() { + let bookInclude = Book(id: "444", + attributes: .init(pageCount: 113), + relationships: .init(author: "1234", + series: ["443"]), + meta: .none, + links: .none) + .sparse(with: []) + + let primaryResource = Book(id: "443", + attributes: .init(pageCount: 100), + relationships: .init(author: "1234", + series: ["444"]), + meta: .none, + links: .none) + .sparse(with: []) + + let document = Document< + SingleResourceBody, + NoMetadata, + NoLinks, + Include1, + NoAPIDescription, + UnknownJSONAPIError + >(apiDescription: .none, + body: .init(resourceObject: primaryResource), + includes: .init(values: [.init(bookInclude)]), + meta: .none, + links: .none) + + let encoded = try! JSONEncoder().encode(document) + + let deserialized = try! JSONSerialization.jsonObject(with: encoded, + options: []) + + let deserializedObj = deserialized as? [String: Any] + + guard let deserializedObj1 = deserializedObj?["data"] as? [String: Any] else { + XCTFail("Expected to deserialize one object from document data") + return + } + + XCTAssertNotNil(deserializedObj1["id"]) + XCTAssertEqual(deserializedObj1["id"] as? String, primaryResource.resourceObject.id.rawValue) + + XCTAssertNotNil(deserializedObj1["type"]) + XCTAssertEqual(deserializedObj1["type"] as? String, Book.jsonType) + + XCTAssertEqual((deserializedObj1["attributes"] as? [String: Any])?.count, 0) + + XCTAssertNotNil(deserializedObj1["relationships"]) + + guard let deserializedIncludes = deserializedObj?["included"] as? [Any], + let deserializedObj2 = deserializedIncludes.first as? [String: Any] else { + XCTFail("Expected to deserialize one incude object") + return + } + + XCTAssertNotNil(deserializedObj2["id"]) + XCTAssertEqual(deserializedObj2["id"] as? String, bookInclude.resourceObject.id.rawValue) + + XCTAssertNotNil(deserializedObj2["type"]) + XCTAssertEqual(deserializedObj2["type"] as? String, Book.jsonType) + + XCTAssertEqual((deserializedObj2["attributes"] as? [String: Any])?.count, 0) + + XCTAssertNotNil(deserializedObj2["relationships"]) + } +} + // MARK: Poly PrimaryResource Tests extension DocumentTests { func test_singleDocument_PolyPrimaryResource() { @@ -1115,6 +1326,25 @@ extension DocumentTests { typealias Article = BasicEntity + enum BookType: ResourceObjectDescription { + static var jsonType: String { return "books" } + + struct Attributes: JSONAPI.SparsableAttributes { + let pageCount: Attribute + + enum CodingKeys: String, SparsableCodingKey { + case pageCount + } + } + + struct Relationships: JSONAPI.Relationships { + let author: ToOneRelationship + let series: ToManyRelationship + } + } + + typealias Book = BasicEntity + struct TestPageMetadata: JSONAPI.Meta { let total: Int let limit: Int diff --git a/Tests/JSONAPITests/Includes/IncludeTests.swift b/Tests/JSONAPITests/Includes/IncludeTests.swift index 2254efe..4272083 100644 --- a/Tests/JSONAPITests/Includes/IncludeTests.swift +++ b/Tests/JSONAPITests/Includes/IncludeTests.swift @@ -12,15 +12,15 @@ class IncludedTests: XCTestCase { func test_zeroIncludes() { let includes = decoded(type: Includes.self, data: two_same_type_includes) - + XCTAssertEqual(includes.count, 0) } func test_zeroIncludes_encode() { - XCTAssertThrowsError(try JSONEncoder().encode(decoded(type: Includes.self, - data: two_same_type_includes))) - } - + XCTAssertThrowsError(try JSONEncoder().encode(decoded(type: Includes.self, + data: two_same_type_includes))) + } + func test_OneInclude() { let includes = decoded(type: Includes>.self, data: one_include) @@ -32,7 +32,7 @@ class IncludedTests: XCTestCase { test_DecodeEncodeEquality(type: Includes>.self, data: one_include) } - + func test_TwoSameIncludes() { let includes = decoded(type: Includes>.self, data: two_same_type_includes) @@ -57,7 +57,7 @@ class IncludedTests: XCTestCase { test_DecodeEncodeEquality(type: Includes>.self, data: two_different_type_includes) } - + func test_ThreeDifferentIncludes() { let includes = decoded(type: Includes>.self, data: three_different_type_includes) @@ -71,11 +71,11 @@ class IncludedTests: XCTestCase { test_DecodeEncodeEquality(type: Includes>.self, data: three_different_type_includes) } - + func test_FourDifferentIncludes() { let includes = decoded(type: Includes>.self, data: four_different_type_includes) - + XCTAssertEqual(includes[TestEntity.self].count, 1) XCTAssertEqual(includes[TestEntity2.self].count, 1) XCTAssertEqual(includes[TestEntity4.self].count, 1) @@ -86,11 +86,11 @@ class IncludedTests: XCTestCase { test_DecodeEncodeEquality(type: Includes>.self, data: four_different_type_includes) } - + func test_FiveDifferentIncludes() { let includes = decoded(type: Includes>.self, data: five_different_type_includes) - + XCTAssertEqual(includes[TestEntity.self].count, 1) XCTAssertEqual(includes[TestEntity2.self].count, 1) XCTAssertEqual(includes[TestEntity3.self].count, 1) @@ -102,11 +102,11 @@ class IncludedTests: XCTestCase { test_DecodeEncodeEquality(type: Includes>.self, data: five_different_type_includes) } - + func test_SixDifferentIncludes() { let includes = decoded(type: Includes>.self, data: six_different_type_includes) - + XCTAssertEqual(includes[TestEntity.self].count, 1) XCTAssertEqual(includes[TestEntity2.self].count, 1) XCTAssertEqual(includes[TestEntity3.self].count, 1) From fe1f4c6c198b1bec0580d91e1d89a8d65287889f Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Mon, 5 Aug 2019 16:15:56 -0700 Subject: [PATCH 031/235] A bit of code documentation --- Sources/JSONAPI/Resource/ResourceObject.swift | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/Sources/JSONAPI/Resource/ResourceObject.swift b/Sources/JSONAPI/Resource/ResourceObject.swift index 7fefbec..4d08ba6 100644 --- a/Sources/JSONAPI/Resource/ResourceObject.swift +++ b/Sources/JSONAPI/Resource/ResourceObject.swift @@ -15,6 +15,8 @@ public protocol Relationships: Codable & Equatable {} /// properties of any types that are JSON encodable. public protocol Attributes: Codable & Equatable {} +/// CodingKeys must be `CodingKey` and `Equatable` in order +/// to support Sparse Fieldsets. public typealias SparsableCodingKey = CodingKey & Equatable /// Attributes containing publicly accessible and `Equatable` @@ -403,14 +405,14 @@ extension ResourceObject where MetaType == NoMetadata, LinksType == NoLinks, Ent // MARK: Pointer for Relationships use. public extension ResourceObject where EntityRawIdType: JSONAPI.RawIdType { - /// An ResourceObject.Pointer is a `ToOneRelationship` with no metadata or links. - /// This is just a convenient way to reference an ResourceObject so that - /// other Entities' Relationships can be built up from it. + /// A `ResourceObject.Pointer` is a `ToOneRelationship` with no metadata or links. + /// This is just a convenient way to reference a `ResourceObject` so that + /// other ResourceObjects' Relationships can be built up from it. typealias Pointer = ToOneRelationship - /// ResourceObject.Pointers is a `ToManyRelationship` with no metadata or links. - /// This is just a convenient way to reference a bunch of Entities so - /// that other Entities' Relationships can be built up from them. + /// `ResourceObject.Pointers` is a `ToManyRelationship` with no metadata or links. + /// This is just a convenient way to reference a bunch of ResourceObjects so + /// that other ResourceObjects' Relationships can be built up from them. typealias Pointers = ToManyRelationship /// Get a pointer to this resource object that can be used as a @@ -419,6 +421,8 @@ public extension ResourceObject where EntityRawIdType: JSONAPI.RawIdType { return Pointer(resourceObject: self) } + /// Get a pointer (i.e. `ToOneRelationship`) to this resource + /// object with the given metadata and links attached. func pointer(withMeta meta: MType, links: LType) -> ToOneRelationship { return ToOneRelationship(resourceObject: self, meta: meta, links: links) } @@ -426,20 +430,20 @@ public extension ResourceObject where EntityRawIdType: JSONAPI.RawIdType { // MARK: Identifying Unidentified Entities public extension ResourceObject where EntityRawIdType == Unidentified { - /// Create a new ResourceObject from this one with a newly created + /// Create a new `ResourceObject` from this one with a newly created /// unique Id of the given type. func identified(byType: RawIdType.Type) -> ResourceObject { return .init(attributes: attributes, relationships: relationships, meta: meta, links: links) } - /// Create a new ResourceObject from this one with the given Id. + /// Create a new `ResourceObject` from this one with the given Id. func identified(by id: RawIdType) -> ResourceObject { return .init(id: ResourceObject.Identifier(rawValue: id), attributes: attributes, relationships: relationships, meta: meta, links: links) } } public extension ResourceObject where EntityRawIdType: CreatableRawIdType { - /// Create a copy of this ResourceObject with a new unique Id. + /// Create a copy of this `ResourceObject` with a new unique Id. func withNewIdentifier() -> ResourceObject { return ResourceObject(attributes: attributes, relationships: relationships, meta: meta, links: links) } From 045e88f4d45259a74b64a799e5f2c2c245b452c4 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Mon, 5 Aug 2019 16:28:02 -0700 Subject: [PATCH 032/235] More code documentation --- Sources/JSONAPI/Document/Document.swift | 5 +++++ Sources/JSONAPI/Document/Error.swift | 3 +++ Sources/JSONAPI/Document/ResourceBody.swift | 8 +++++++- Sources/JSONAPI/Meta/Meta.swift | 2 ++ Sources/JSONAPI/Resource/Transformer.swift | 7 ++++++- Sources/JSONAPI/SparseFields/SparseFieldset.swift | 6 +++++- 6 files changed, 28 insertions(+), 3 deletions(-) diff --git a/Sources/JSONAPI/Document/Document.swift b/Sources/JSONAPI/Document/Document.swift index d0a34d9..4034969 100644 --- a/Sources/JSONAPI/Document/Document.swift +++ b/Sources/JSONAPI/Document/Document.swift @@ -7,6 +7,9 @@ import Poly +/// An `EncodableJSONAPIDocument` supports encoding but not decoding. +/// It is actually more restrictive than `JSONAPIDocument` which supports both +/// encoding and decoding. public protocol EncodableJSONAPIDocument: Equatable, Encodable { associatedtype PrimaryResourceBody: JSONAPI.EncodableResourceBody associatedtype MetaType: JSONAPI.Meta @@ -20,6 +23,8 @@ public protocol EncodableJSONAPIDocument: Equatable, Encodable { var body: Body { get } } +/// A `JSONAPIDocument` supports encoding and decoding of a JSON:API +/// compliant Document. public protocol JSONAPIDocument: EncodableJSONAPIDocument, Decodable where PrimaryResourceBody: JSONAPI.ResourceBody, IncludeType: Decodable {} /// A JSON API Document represents the entire body diff --git a/Sources/JSONAPI/Document/Error.swift b/Sources/JSONAPI/Document/Error.swift index 6d6e36e..ab6e3ca 100644 --- a/Sources/JSONAPI/Document/Error.swift +++ b/Sources/JSONAPI/Document/Error.swift @@ -9,6 +9,9 @@ public protocol JSONAPIError: Swift.Error, Equatable, Codable { static var unknown: Self { get } } +/// `UnknownJSONAPIError` can actually be used in any sitaution +/// where you don't know what errors are possible _or_ you just don't +/// care what errors might show up. public enum UnknownJSONAPIError: JSONAPIError { case unknownError diff --git a/Sources/JSONAPI/Document/ResourceBody.swift b/Sources/JSONAPI/Document/ResourceBody.swift index 3e9141d..2eb1bb4 100644 --- a/Sources/JSONAPI/Document/ResourceBody.swift +++ b/Sources/JSONAPI/Document/ResourceBody.swift @@ -5,8 +5,14 @@ // Created by Mathew Polzin on 11/10/18. // +/// This protocol allows for a `SingleResourceBody` to contain a `null` +/// data object where `ManyResourceBody` cannot (because an empty +/// array should be used for no results). public protocol OptionalEncodablePrimaryResource: Equatable, Encodable {} +/// An `EncodablePrimaryResource` is a `PrimaryResource` that only supports encoding. +/// This is actually more restrictave than `PrimaryResource`, which supports both encoding and +/// decoding. public protocol EncodablePrimaryResource: OptionalEncodablePrimaryResource {} /// This protocol allows for `SingleResourceBody` to contain a `null` @@ -14,7 +20,7 @@ public protocol EncodablePrimaryResource: OptionalEncodablePrimaryResource {} /// array should be used for no results). public protocol OptionalPrimaryResource: OptionalEncodablePrimaryResource, Decodable {} -/// A PrimaryResource is a type that can be used in the body of a JSON API +/// A `PrimaryResource` is a type that can be used in the body of a JSON API /// document as the primary resource. public protocol PrimaryResource: EncodablePrimaryResource, OptionalPrimaryResource {} diff --git a/Sources/JSONAPI/Meta/Meta.swift b/Sources/JSONAPI/Meta/Meta.swift index 587dc9c..68b2c94 100644 --- a/Sources/JSONAPI/Meta/Meta.swift +++ b/Sources/JSONAPI/Meta/Meta.swift @@ -19,6 +19,8 @@ public protocol Meta: Codable, Equatable { // nullable. extension Optional: Meta where Wrapped: Meta {} +/// Use this type when you want to specify not to encode or decode any metadata +/// for a type. public struct NoMetadata: Meta, CustomStringConvertible { public static var none: NoMetadata { return NoMetadata() } diff --git a/Sources/JSONAPI/Resource/Transformer.swift b/Sources/JSONAPI/Resource/Transformer.swift index 04d3c04..c7aae56 100644 --- a/Sources/JSONAPI/Resource/Transformer.swift +++ b/Sources/JSONAPI/Resource/Transformer.swift @@ -10,12 +10,16 @@ public protocol Transformer { associatedtype From associatedtype To + /// Turn value of type `From` into a value of type `To` or + /// throw an error on failure. static func transform(_ value: From) throws -> To } /// ReversibleTransformers define a function that reverses the transform /// operation. public protocol ReversibleTransformer: Transformer { + /// Turn a value of type `To` into a value of type `From` or + /// throw an error on failure. static func reverse(_ value: To) throws -> From } @@ -43,7 +47,8 @@ extension Validator { } /// Validate the given value and then return it if valid. - /// throws if invalid. + /// throws an erro if invalid. + /// - returns: The same value passed in, if it was valid. public static func validate(_ value: To) throws -> To { let _ = try transform(value) return value diff --git a/Sources/JSONAPI/SparseFields/SparseFieldset.swift b/Sources/JSONAPI/SparseFields/SparseFieldset.swift index 3540334..b326f13 100644 --- a/Sources/JSONAPI/SparseFields/SparseFieldset.swift +++ b/Sources/JSONAPI/SparseFields/SparseFieldset.swift @@ -5,6 +5,9 @@ // Created by Mathew Polzin on 8/4/19. // +/// A SparseFieldset represents an `Encodable` subset of the fields +/// a `ResourceObject` would normally encode. Currently, you can +/// only apply sparse fieldset's to `ResourceObject.Attributes`. public struct SparseFieldset< Description: JSONAPI.ResourceObjectDescription, MetaType: JSONAPI.Meta, @@ -12,6 +15,7 @@ public struct SparseFieldset< EntityRawIdType: JSONAPI.MaybeRawId >: EncodablePrimaryResource where Description.Attributes: SparsableAttributes { + /// The `ResourceObject` type this `SparseFieldset` is capable of modifying. public typealias Resource = JSONAPI.ResourceObject public let resourceObject: Resource @@ -41,6 +45,6 @@ public extension ResourceObject where Description.Attributes: SparsableAttribute public extension ResourceObject where Description.Attributes: SparsableAttributes { - /// The Sparse Fieldset type for this `ResourceObject` + /// The `SparseFieldset` type for this `ResourceObject` typealias SparseType = SparseFieldset } From 3a60ac5fe22940e3f25ac64fc24c2c7d081496b7 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Mon, 5 Aug 2019 17:59:38 -0700 Subject: [PATCH 033/235] Add playground page showing off sparse fieldset encoding --- .../Contents.swift | 72 +++++++++++++++++++ .../Usage.xcplaygroundpage/Contents.swift | 9 ++- JSONAPI.playground/contents.xcplayground | 1 + Tests/JSONAPITests/XCTestManifests.swift | 7 ++ 4 files changed, 88 insertions(+), 1 deletion(-) create mode 100644 JSONAPI.playground/Pages/Sparse Fieldsets Example.xcplaygroundpage/Contents.swift diff --git a/JSONAPI.playground/Pages/Sparse Fieldsets Example.xcplaygroundpage/Contents.swift b/JSONAPI.playground/Pages/Sparse Fieldsets Example.xcplaygroundpage/Contents.swift new file mode 100644 index 0000000..e34667b --- /dev/null +++ b/JSONAPI.playground/Pages/Sparse Fieldsets Example.xcplaygroundpage/Contents.swift @@ -0,0 +1,72 @@ + +import JSONAPI +import Foundation + +// MARK: - Resource Object + +enum ThingWithPropertiesDescription: JSONAPI.ResourceObjectDescription { + static let jsonType: String = "thing" + + // + // NOTE: `JSONAPI.SparsableAttributes` as opposed to `JSONAPI.Attributes` + // + struct Attributes: JSONAPI.SparsableAttributes { + let stringThing: Attribute + let numberThing: Attribute + let boolThing: Attribute + + // + // NOTE: Special implementation of `CodingKeys` + // + enum CodingKeys: String, JSONAPI.SparsableCodingKey { + case stringThing + case numberThing + case boolThing + } + } + + typealias Relationships = NoRelationships +} + +typealias ThingWithProperties = JSONAPI.ResourceObject + +// MARK: - Document + +// +// NOTE: Using `JSONAPI.EncodableResourceBody` which means the document type will be `Encodable` but not `Decodable`. +// +typealias Document = JSONAPI.Document + +// +// NOTE: Using `JSONAPI.EncodablePrimaryResource` which means the `ResourceBody` will be `Encodable` but not `Decodable. +// +typealias SingleDocument = Document, NoIncludes> + +// MARK: - Resource Initialization + +let resource = ThingWithProperties(id: .init(rawValue: "1234"), + attributes: .init(stringThing: .init(value: "hello world"), + numberThing: .init(value: 10), + boolThing: .init(value: nil)), + relationships: .none, + meta: .none, + links: .none) +// +// NOTE: Creating a sparse resource that will only encode +// the attribute named "stringThing" +// +let sparseResource = resource.sparse(with: [.stringThing]) + +// MARK: - Encoding + +let encoder = JSONEncoder() + +let sparseResourceDoc = SingleDocument(apiDescription: .none, + body: .init(resourceObject: sparseResource), + includes: .none, + meta: .none, + links: .none) + +let data = try! encoder.encode(sparseResourceDoc) + +print(String(data: data, encoding: .utf8)!) diff --git a/JSONAPI.playground/Pages/Usage.xcplaygroundpage/Contents.swift b/JSONAPI.playground/Pages/Usage.xcplaygroundpage/Contents.swift index 22bb3c0..00b5172 100644 --- a/JSONAPI.playground/Pages/Usage.xcplaygroundpage/Contents.swift +++ b/JSONAPI.playground/Pages/Usage.xcplaygroundpage/Contents.swift @@ -8,6 +8,7 @@ Please enjoy these examples, but allow me the forced casting and the lack of err ********/ + // MARK: - Create a request or response body with one Dog in it let dogFromCode = try! Dog(name: "Buddy", owner: nil) @@ -15,17 +16,20 @@ let singleDogDocument = SingleDogDocument(apiDescription: .none, body: .init(res let singleDogData = try! JSONEncoder().encode(singleDogDocument) + // MARK: - Parse a request or response body with one Dog in it let dogResponse = try! JSONDecoder().decode(SingleDogDocument.self, from: singleDogData) let dogFromData = dogResponse.body.primaryResource?.value let dogOwner: Person.Identifier? = dogFromData.flatMap { $0 ~> \.owner } -// MARKL - Parse a request or response body with one Dog in it using an alternative model + +// MARK: - Parse a request or response body with one Dog in it using an alternative model typealias AltSingleDogDocument = JSONAPI.Document, NoMetadata, NoLinks, NoIncludes, NoAPIDescription, UnknownJSONAPIError> let altDogResponse = try! JSONDecoder().decode(AltSingleDogDocument.self, from: singleDogData) let altDogFromData = altDogResponse.body.primaryResource?.value let altDogHuman: Person.Identifier? = altDogFromData.flatMap { $0 ~> \.human } + // MARK: - Create a request or response with multiple people and dogs and houses included let personIds = [Person.Identifier(), Person.Identifier()] let dogs = try! [Dog(name: "Buddy", owner: personIds[0]), Dog(name: "Joy", owner: personIds[0]), Dog(name: "Travis", owner: personIds[1])] @@ -36,6 +40,7 @@ let includes = dogs.map { BatchPeopleDocument.Include($0) } + houses.map { Batch let batchPeopleDocument = BatchPeopleDocument(apiDescription: .none, body: .init(resourceObjects: people), includes: .init(values: includes), meta: .none, links: .none) let batchPeopleData = try! JSONEncoder().encode(batchPeopleDocument) + // MARK: - Parse a request or response body with multiple people in it and dogs and houses included let peopleResponse = try! JSONDecoder().decode(BatchPeopleDocument.self, from: batchPeopleData) @@ -47,6 +52,7 @@ print("-----") print(peopleResponse) print("-----") + // MARK: - Pass successfully parsed body to other parts of the code /* @@ -59,6 +65,7 @@ if case let .data(bodyData) = peopleResponse.body { } */ + // MARK: - Work in the abstract func process(document: T) { diff --git a/JSONAPI.playground/contents.xcplayground b/JSONAPI.playground/contents.xcplayground index 3da156e..21e919c 100644 --- a/JSONAPI.playground/contents.xcplayground +++ b/JSONAPI.playground/contents.xcplayground @@ -5,5 +5,6 @@ + \ No newline at end of file diff --git a/Tests/JSONAPITests/XCTestManifests.swift b/Tests/JSONAPITests/XCTestManifests.swift index 3011985..dc100bd 100644 --- a/Tests/JSONAPITests/XCTestManifests.swift +++ b/Tests/JSONAPITests/XCTestManifests.swift @@ -144,6 +144,10 @@ extension DocumentTests { ("test_singleDocumentSomeIncludesWithMetadata_encode", test_singleDocumentSomeIncludesWithMetadata_encode), ("test_singleDocumentSomeIncludesWithMetadataWithAPIDescription", test_singleDocumentSomeIncludesWithMetadataWithAPIDescription), ("test_singleDocumentSomeIncludesWithMetadataWithAPIDescription_encode", test_singleDocumentSomeIncludesWithMetadataWithAPIDescription_encode), + ("test_sparseIncludeFullPrimaryResource", test_sparseIncludeFullPrimaryResource), + ("test_sparseIncludeSparsePrimaryResource", test_sparseIncludeSparsePrimaryResource), + ("test_sparsePrimaryResource", test_sparsePrimaryResource), + ("test_sparsePrimaryResourceOptionalAndNil", test_sparsePrimaryResourceOptionalAndNil), ("test_unknownErrorDocumentAddIncludes", test_unknownErrorDocumentAddIncludes), ("test_unknownErrorDocumentAddIncludingType", test_unknownErrorDocumentAddIncludingType), ("test_unknownErrorDocumentMissingLinks", test_unknownErrorDocumentMissingLinks), @@ -275,6 +279,7 @@ extension IncludedTests { // to regenerate. static let __allTests__IncludedTests = [ ("test_appending", test_appending), + ("test_ComboSparseAndFullIncludeTypes", test_ComboSparseAndFullIncludeTypes), ("test_EightDifferentIncludes", test_EightDifferentIncludes), ("test_EightDifferentIncludes_encode", test_EightDifferentIncludes_encode), ("test_FiveDifferentIncludes", test_FiveDifferentIncludes), @@ -285,6 +290,7 @@ extension IncludedTests { ("test_NineDifferentIncludes_encode", test_NineDifferentIncludes_encode), ("test_OneInclude", test_OneInclude), ("test_OneInclude_encode", test_OneInclude_encode), + ("test_OneSparseIncludeType", test_OneSparseIncludeType), ("test_SevenDifferentIncludes", test_SevenDifferentIncludes), ("test_SevenDifferentIncludes_encode", test_SevenDifferentIncludes_encode), ("test_SixDifferentIncludes", test_SixDifferentIncludes), @@ -295,6 +301,7 @@ extension IncludedTests { ("test_TwoDifferentIncludes_encode", test_TwoDifferentIncludes_encode), ("test_TwoSameIncludes", test_TwoSameIncludes), ("test_TwoSameIncludes_encode", test_TwoSameIncludes_encode), + ("test_TwoSparseIncludeTypes", test_TwoSparseIncludeTypes), ("test_zeroIncludes", test_zeroIncludes), ("test_zeroIncludes_encode", test_zeroIncludes_encode), ("test_zeroIncludes_init", test_zeroIncludes_init), From 6c8646a1b4a093caf470d97721cc2ef51c657a38 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Mon, 5 Aug 2019 18:09:29 -0700 Subject: [PATCH 034/235] whitespace --- Sources/JSONAPI/Resource/Transformer.swift | 28 +++++++++++----------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/Sources/JSONAPI/Resource/Transformer.swift b/Sources/JSONAPI/Resource/Transformer.swift index c7aae56..203b9b5 100644 --- a/Sources/JSONAPI/Resource/Transformer.swift +++ b/Sources/JSONAPI/Resource/Transformer.swift @@ -7,12 +7,12 @@ /// A Transformer simply defines a static function that transforms a value. public protocol Transformer { - associatedtype From - associatedtype To + associatedtype From + associatedtype To /// Turn value of type `From` into a value of type `To` or /// throw an error on failure. - static func transform(_ value: From) throws -> To + static func transform(_ value: From) throws -> To } /// ReversibleTransformers define a function that reverses the transform @@ -20,13 +20,13 @@ public protocol Transformer { public protocol ReversibleTransformer: Transformer { /// Turn a value of type `To` into a value of type `From` or /// throw an error on failure. - static func reverse(_ value: To) throws -> From + static func reverse(_ value: To) throws -> From } /// The IdentityTransformer does not perform any transformation on a value. public enum IdentityTransformer: ReversibleTransformer { - public static func transform(_ value: T) throws -> T { return value } - public static func reverse(_ value: T) throws -> T { return value } + public static func transform(_ value: T) throws -> T { return value } + public static func reverse(_ value: T) throws -> T { return value } } // MARK: - Validator @@ -41,16 +41,16 @@ public protocol Validator: ReversibleTransformer where From == To { } extension Validator { - public static func reverse(_ value: To) throws -> To { - let _ = try transform(value) - return value - } + public static func reverse(_ value: To) throws -> To { + let _ = try transform(value) + return value + } /// Validate the given value and then return it if valid. /// throws an erro if invalid. /// - returns: The same value passed in, if it was valid. - public static func validate(_ value: To) throws -> To { - let _ = try transform(value) - return value - } + public static func validate(_ value: To) throws -> To { + let _ = try transform(value) + return value + } } From ed2334935123aa60b0ac6c9d625cb27bd9e1a653 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Mon, 5 Aug 2019 18:45:13 -0700 Subject: [PATCH 035/235] Update README --- README.md | 32 ++++++++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index f00fafd..7a128bc 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ See the JSON API Spec here: https://jsonapi.org/format/ - [`Transformer`](#transformer) - [`Validator`](#validator) - [Computed `Attribute`](#computed-attribute) - - [Copying `ResourceObjects`](#copying-resourceobjects) + - [Copying/Mutating `ResourceObjects`](#copyingmutating-resourceobjects) - [`JSONAPI.Document`](#jsonapidocument) - [`ResourceBody`](#resourcebody) - [nullable `PrimaryResource`](#nullable-primaryresource) @@ -54,6 +54,9 @@ See the JSON API Spec here: https://jsonapi.org/format/ - [`JSONAPI.Meta`](#jsonapimeta) - [`JSONAPI.Links`](#jsonapilinks) - [`JSONAPI.RawIdType`](#jsonapirawidtype) + - [Sparse Fieldsets](#sparse-fieldsets) + - [Supporting Sparse Fieldset Encoding](#supporting-sparse-fieldset-encoding) + - [Sparse Fieldset `typealias` comparisons](#sparse-fieldset-typealias-comparisons) - [Custom Attribute or Relationship Key Mapping](#custom-attribute-or-relationship-key-mapping) - [Custom Attribute Encode/Decode](#custom-attribute-encodedecode) - [Meta-Attributes](#meta-attributes) @@ -150,7 +153,7 @@ Note that Playground support for importing non-system Frameworks is still a bit ### Misc - [x] Support transforms on `Attributes` values (e.g. to support different representations of `Date`) - [x] Support validation on `Attributes`. -- [ ] Support sparse fieldsets. At the moment, not sure what this support will look like. A client can likely just define a new model to represent a sparse population of another model in a very specific use case. On the server side, it becomes much more appealing to be able to support arbitrary combinations of omitted fields. +- [x] Support sparse fieldsets (encoding only). A client can likely just define a new model to represent a sparse population of another model in a very specific use case. On the server side, sparse fieldsets of Resource Objects can be encoded without creating one model for every possible sparse fieldset. - [ ] Create more descriptive errors that are easier to use for troubleshooting. ### Testing @@ -487,6 +490,31 @@ extension String: CreatableRawIdType { } ``` +### Sparse Fieldsets +Sparse Fieldsets are currently supported when encoding only. When decoding, Sparse Fieldsets become tricker to support under the current types this library uses and it is assumed that clients will request one or maybe two sparse fieldset combinations for any given model at most so it can simply define the `JSONAPI` models needed to decode those subsets of all possible fields. A server, on the other hand, likely needs to support arbitrary combinations of sparse fieldsets and this library provides a mechanism for encoding those sparse fieldsets without too much extra footwork. + +You can use sparse fieldsets on the primary resources(s) _and_ includes of a `JSONAPI.Document`. + +There is a sparse fieldsets example included with this repository as a Playground page. + +#### Supporting Sparse Fieldset Encoding +1. The `JSONAPI` `ResourceObjectDescription`'s `Attributes` struct must conform to `JSONAPI.SparsableAttributes` rather than `JSONAPI.Attributes`. +2. The `JSONAPI` `ResourceObjectDescription`'s `Attributes` struct must contain a `CodingKeys` enum that conforms to `JSONAPI.SparsableCodingKey` instead of `Swift.CodingKey`. +3. `typealiases` you may have created for `JSONAPI.Document` that allow you to decode Documents will not support the "encode-only" nature of sparse fieldsets. See the next section for `typealias` comparisons. +4. To create a sparse fieldset from a `ResourceObject` just call its `sparse(with: fields)` method and pass an array of `Attributes.CodingKeys` values you would like included in the encoding. +5. Initialize and encode a `Document` containing one or more sparse or full primary resource(s) and any number of sparse or full includes. + +#### Sparse Fieldset `typealias` comparisons +You might have found a `typealias` like the following for encoding/decoding `JSONAPI.Document`s (note the primary resource body is a `JSONAPI.ResourceBody`): +```swift +typealias Document = JSONAPI.Document +``` + +In order to support sparse fieldsets (which are encode-only), the following companion `typealias` would be useful (note the primary resource body is a `JSONAPI.EncodableResourceBody`): +```swift +typealias SparseDocument = JSONAPI.Document +``` + ### Custom Attribute or Relationship Key Mapping There is not anything special going on at the `JSONAPI.Attributes` and `JSONAPI.Relationships` levels, so you can easily provide custom key mappings by taking advantage of `Codable`'s `CodingKeys` pattern. Here are two models that will encode/decode equivalently but offer different naming in your codebase: ```swift From e23ec090ed090ce66828a63c75d6e71d044ea19a Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Mon, 5 Aug 2019 19:44:09 -0700 Subject: [PATCH 036/235] Starting to add SparseFieldEncoder tests --- .../SparseFields/SparseFieldEncoder.swift | 6 - .../SparseFieldEncoderTests.swift | 181 +++++++++++++++++- 2 files changed, 179 insertions(+), 8 deletions(-) diff --git a/Sources/JSONAPI/SparseFields/SparseFieldEncoder.swift b/Sources/JSONAPI/SparseFields/SparseFieldEncoder.swift index cbdcb33..e5985c8 100644 --- a/Sources/JSONAPI/SparseFields/SparseFieldEncoder.swift +++ b/Sources/JSONAPI/SparseFields/SparseFieldEncoder.swift @@ -28,12 +28,6 @@ public class SparseFieldEncoder: Encoder { return KeyedEncodingContainer(container) } - public func container(keyedBy type: SparseKey.Type) -> KeyedEncodingContainer { - let container = SparseFieldKeyedEncodingContainer(wrapping: wrappedEncoder.container(keyedBy: type), - encoding: allowedKeys) - return KeyedEncodingContainer(container) - } - public func unkeyedContainer() -> UnkeyedEncodingContainer { return wrappedEncoder.unkeyedContainer() } diff --git a/Tests/JSONAPITests/SparseFields/SparseFieldEncoderTests.swift b/Tests/JSONAPITests/SparseFields/SparseFieldEncoderTests.swift index a9790ff..e6b3a9d 100644 --- a/Tests/JSONAPITests/SparseFields/SparseFieldEncoderTests.swift +++ b/Tests/JSONAPITests/SparseFields/SparseFieldEncoderTests.swift @@ -7,9 +7,186 @@ import XCTest import JSONAPI +import Foundation class SparseFieldEncoderTests: XCTestCase { - func test_placeholder() { - // TODO: write tests + func test_AccurateCodingPath() { + let encoder = JSONEncoder() + XCTAssertThrowsError(try encoder.encode(Wrapper())) + + do { + let _ = try encoder.encode(Wrapper()) + } catch let err as Wrapper.OuterFail.FailError { + print(err.path) + XCTAssertEqual(err.path.first as? Wrapper.OuterFail.CodingKeys, Wrapper.OuterFail.CodingKeys.inner) + } catch { + XCTFail("received unexpected error during test") + } + } + + func test_SkipsOmittedFields() { + let encoder = JSONEncoder() + + // does not throw because we omit the field that would have failed + XCTAssertNoThrow(try encoder.encode(Wrapper(fields: []))) + } + + func test_EverythingArsenal_allOn() { + let encoder = JSONEncoder() + + let allThingsOn = try! encoder.encode(EverythingWrapper(fields: EverythingWrapper.EverythingWrapped.CodingKeys.allCases)) + + let allThingsOnDeserialized = try! JSONSerialization.jsonObject(with: allThingsOn, + options: []) as! [String: Any] + + XCTAssertNil(allThingsOnDeserialized["omittable"]) + XCTAssertNotNil(allThingsOnDeserialized["nullable"] as? NSNull) + XCTAssertEqual(allThingsOnDeserialized["bool"] as? Bool, true) + XCTAssertEqual(allThingsOnDeserialized["double"] as? Double, 10.5) + XCTAssertEqual(allThingsOnDeserialized["string"] as? String, "hello") + XCTAssertEqual(allThingsOnDeserialized["float"] as? Float, 1.2) + XCTAssertEqual(allThingsOnDeserialized["int"] as? Int, 3) + XCTAssertEqual(allThingsOnDeserialized["int8"] as? Int8, 4) + XCTAssertEqual(allThingsOnDeserialized["int16"] as? Int16, 5) + XCTAssertEqual(allThingsOnDeserialized["int32"] as? Int32, 6) + XCTAssertEqual(allThingsOnDeserialized["int64"] as? Int64, 7) + XCTAssertEqual(allThingsOnDeserialized["uint"] as? UInt, 8) + XCTAssertEqual(allThingsOnDeserialized["uint8"] as? UInt8, 9) + XCTAssertEqual(allThingsOnDeserialized["uint16"] as? UInt16, 10) + XCTAssertEqual(allThingsOnDeserialized["uint32"] as? UInt32, 11) + XCTAssertEqual(allThingsOnDeserialized["uint64"] as? UInt64, 12) + XCTAssertEqual(allThingsOnDeserialized["nested"] as? String, "world") + } + + func test_EverythingArsenal_allOff() { + let encoder = JSONEncoder() + + let allThingsOn = try! encoder.encode(EverythingWrapper(fields: [])) + + let allThingsOnDeserialized = try! JSONSerialization.jsonObject(with: allThingsOn, + options: []) as! [String: Any] + + XCTAssertNil(allThingsOnDeserialized["omittable"]) + XCTAssertNil(allThingsOnDeserialized["nullable"]) + XCTAssertNil(allThingsOnDeserialized["bool"]) + XCTAssertNil(allThingsOnDeserialized["double"]) + XCTAssertNil(allThingsOnDeserialized["string"]) + XCTAssertNil(allThingsOnDeserialized["float"]) + XCTAssertNil(allThingsOnDeserialized["int"]) + XCTAssertNil(allThingsOnDeserialized["int8"]) + XCTAssertNil(allThingsOnDeserialized["int16"]) + XCTAssertNil(allThingsOnDeserialized["int32"]) + XCTAssertNil(allThingsOnDeserialized["int64"]) + XCTAssertNil(allThingsOnDeserialized["uint"]) + XCTAssertNil(allThingsOnDeserialized["uint8"]) + XCTAssertNil(allThingsOnDeserialized["uint16"]) + XCTAssertNil(allThingsOnDeserialized["uint32"]) + XCTAssertNil(allThingsOnDeserialized["uint64"]) + XCTAssertNil(allThingsOnDeserialized["nested"]) + XCTAssertEqual(allThingsOnDeserialized.count, 0) + } +} + +extension SparseFieldEncoderTests { + struct Wrapper: Encodable { + + let fields: [OuterFail.CodingKeys] + + init(fields: [OuterFail.CodingKeys] = OuterFail.CodingKeys.allCases) { + self.fields = fields + } + + func encode(to encoder: Encoder) throws { + let sparseEncoder = SparseFieldEncoder(wrapping: encoder, + encoding: fields) + try OuterFail(inner: .init()).encode(to: sparseEncoder) + } + + struct OuterFail: Encodable { + let inner: InnerFail + + public enum CodingKeys: String, Equatable, CaseIterable, CodingKey { + case inner + } + + struct InnerFail: Encodable { + func encode(to encoder: Encoder) throws { + + throw FailError(path: encoder.codingPath) + } + } + + struct FailError: Swift.Error { + let path: [CodingKey] + } + } + } + + struct EverythingWrapper: Encodable { + let fields: [EverythingWrapped.CodingKeys] + + func encode(to encoder: Encoder) throws { + let sparseEncoder = SparseFieldEncoder(wrapping: encoder, + encoding: fields) + + try EverythingWrapped(omittable: nil, + nullable: .init(value: nil), + bool: true, + double: 10.5, + string: "hello", + float: 1.2, + int: 3, + int8: 4, + int16: 5, + int32: 6, + int64: 7, + uint: 8, + uint8: 9, + uint16: 10, + uint32: 11, + uint64: 12, + nested: .init(value: "world")) + .encode(to: sparseEncoder) + } + + struct EverythingWrapped: Encodable { + let omittable: Int? + let nullable: Attribute + let bool: Bool + let double: Double + let string: String + let float: Float + let int: Int + let int8: Int8 + let int16: Int16 + let int32: Int32 + let int64: Int64 + let uint: UInt + let uint8: UInt8 + let uint16: UInt16 + let uint32: UInt32 + let uint64: UInt64 + let nested: Attribute + + enum CodingKeys: String, Equatable, CaseIterable, CodingKey { + case omittable + case nullable + case bool + case double + case string + case float + case int + case int8 + case int16 + case int32 + case int64 + case uint + case uint8 + case uint16 + case uint32 + case uint64 + case nested + } + } } } From 6ba217f5531374cec2c9daec50553ff6fd2c6c29 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Tue, 6 Aug 2019 08:27:22 -0700 Subject: [PATCH 037/235] More sparse field encoder tests --- .../SparseFields/SparseFieldEncoder.swift | 21 ++- .../SparseFieldEncoderTests.swift | 158 ++++++++++++++++++ 2 files changed, 174 insertions(+), 5 deletions(-) diff --git a/Sources/JSONAPI/SparseFields/SparseFieldEncoder.swift b/Sources/JSONAPI/SparseFields/SparseFieldEncoder.swift index e5985c8..a377fc9 100644 --- a/Sources/JSONAPI/SparseFields/SparseFieldEncoder.swift +++ b/Sources/JSONAPI/SparseFields/SparseFieldEncoder.swift @@ -50,7 +50,8 @@ public struct SparseFieldKeyedEncodingContainer: KeyedEncodingCo self.allowedKeys = allowedKeys } - private func shouldAllow(key: Key) -> Bool { + /// Ask the container whether the given key should be encoded. + public func shouldAllow(key: Key) -> Bool { if let key = key as? SparseKey { return allowedKeys.contains(key) } @@ -157,6 +158,9 @@ public struct SparseFieldKeyedEncodingContainer: KeyedEncodingCo forKey key: Key) -> KeyedEncodingContainer where NestedKey : CodingKey { guard shouldAllow(key: key) else { return KeyedEncodingContainer( + // TODO: not needed by JSONAPI library, but for completeness could + // add an EmptyObjectEncoder that can be returned here so that + // at least nothing gets encoded within the nested container SparseFieldKeyedEncodingContainer(wrapping: wrappedContainer.nestedContainer(keyedBy: keyType, forKey: key), encoding: []) @@ -172,7 +176,9 @@ public struct SparseFieldKeyedEncodingContainer: KeyedEncodingCo public mutating func nestedUnkeyedContainer(forKey key: Key) -> UnkeyedEncodingContainer { guard shouldAllow(key: key) else { - // TODO: Seems like this might not work as expected... maybe need an empty unkeyed container + // TODO: not needed by JSONAPI library, but for completeness could + // add an EmptyObjectEncoder that can be returned here so that + // at least nothing gets encoded within the nested container return wrappedContainer.nestedUnkeyedContainer(forKey: key) } @@ -180,14 +186,19 @@ public struct SparseFieldKeyedEncodingContainer: KeyedEncodingCo } public mutating func superEncoder() -> Encoder { - return wrappedContainer.superEncoder() + return SparseFieldEncoder(wrapping: wrappedContainer.superEncoder(), + encoding: allowedKeys) } public mutating func superEncoder(forKey key: Key) -> Encoder { guard shouldAllow(key: key) else { - return SparseFieldEncoder(wrapping: wrappedContainer.superEncoder(forKey: key), encoding: [SparseKey]()) + // NOTE: We are creating a sparse field encoder with no allowed keys + // here because the given key should not be allowed. + return SparseFieldEncoder(wrapping: wrappedContainer.superEncoder(forKey: key), + encoding: [SparseKey]()) } - return SparseFieldEncoder(wrapping: wrappedContainer.superEncoder(forKey: key), encoding: allowedKeys) + return SparseFieldEncoder(wrapping: wrappedContainer.superEncoder(forKey: key), + encoding: allowedKeys) } } diff --git a/Tests/JSONAPITests/SparseFields/SparseFieldEncoderTests.swift b/Tests/JSONAPITests/SparseFields/SparseFieldEncoderTests.swift index e6b3a9d..f0abce2 100644 --- a/Tests/JSONAPITests/SparseFields/SparseFieldEncoderTests.swift +++ b/Tests/JSONAPITests/SparseFields/SparseFieldEncoderTests.swift @@ -85,6 +85,73 @@ class SparseFieldEncoderTests: XCTestCase { XCTAssertNil(allThingsOnDeserialized["nested"]) XCTAssertEqual(allThingsOnDeserialized.count, 0) } + + func test_NilEncode() { + let encoder = JSONEncoder() + + let nilOn = try! encoder.encode(NilWrapper(fields: [.hello])) + let nilOff = try! encoder.encode(NilWrapper(fields: [])) + + let nilOnDeserialized = try! JSONSerialization.jsonObject(with: nilOn, + options: []) as! [String: Any] + + let nilOffDeserialized = try! JSONSerialization.jsonObject(with: nilOff, + options: []) as! [String: Any] + + XCTAssertEqual(nilOnDeserialized.count, 1) + XCTAssertNotNil(nilOnDeserialized["hello"] as? NSNull) + XCTAssertEqual(nilOffDeserialized.count, 0) + } + + func test_NestedContainers() { + let encoder = JSONEncoder() + + let nestedOn = try! encoder.encode(NestedWrapper(fields: [.hello, .world])) + let nestedOff = try! encoder.encode(NestedWrapper(fields: [])) + + let nestedOnDeserialized = try! JSONSerialization.jsonObject(with: nestedOn, + options: []) as! [String: Any] + let nestedOffDeserialized = try! JSONSerialization.jsonObject(with: nestedOff, + options: []) as! [String: Any] + + XCTAssertEqual(nestedOnDeserialized.count, 2) + XCTAssertEqual((nestedOnDeserialized["hello"] as? [String: Bool])?["nestedKey"], true) + XCTAssertEqual((nestedOnDeserialized["world"] as? [Bool])?.first, false) + + // NOTE: When a nested container is explicitly requested, + // the best we can do to omit the field is to encode + // nothing _within_ the nested container. + XCTAssertEqual(nestedOffDeserialized.count, 2) + // TODO: The container currently does not encode empty object + // for the keyed nested container but I think it should. + XCTAssertEqual((nestedOffDeserialized["hello"] as? [String: Bool])?.count, 1) + // TODO: The container currently does not encode empty array + // for the unkeyed nested container but I think it should. + XCTAssertEqual((nestedOffDeserialized["world"] as? [Bool])?.count, 1) + } + + func test_SuperEncoderIsStillSparse() { + let encoder = JSONEncoder() + + let superOn = try! encoder.encode(SuperWrapper(fields: [.hello, .world])) + let superOff = try! encoder.encode(SuperWrapper(fields: [])) + + let superOnDeserialized = try! JSONSerialization.jsonObject(with: superOn, + options: []) as! [String: Any] + let superOffDeserialized = try! JSONSerialization.jsonObject(with: superOff, + options: []) as! [String: Any] + + XCTAssertEqual(superOnDeserialized.count, 2) + XCTAssertEqual((superOnDeserialized["hello"] as? [String: Bool])?["hello"], true) + XCTAssertEqual((superOnDeserialized["super"] as? [String: Bool])?["world"], false) + + // NOTE: When explicitly requesting a super encoder + // the best we can do is tell the super encoder only + // to encode the same keys + XCTAssertEqual(superOffDeserialized.count, 2) + XCTAssertEqual((superOffDeserialized["hello"] as? [String: Bool])?.count, 0) + XCTAssertEqual((superOffDeserialized["super"] as? [String: Bool])?.count, 0) + } } extension SparseFieldEncoderTests { @@ -189,4 +256,95 @@ extension SparseFieldEncoderTests { } } } + + struct NilWrapper: Encodable { + let fields: [NilWrapped.CodingKeys] + + func encode(to encoder: Encoder) throws { + let sparseEncoder = SparseFieldEncoder(wrapping: encoder, + encoding: fields) + + try NilWrapped() + .encode(to: sparseEncoder) + } + + struct NilWrapped: Encodable { + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + try container.encodeNil(forKey: .hello) + } + + enum CodingKeys: String, Equatable, CodingKey { + case hello + } + } + } + + struct NestedWrapper: Encodable { + let fields: [NestedWrapped.CodingKeys] + + func encode(to encoder: Encoder) throws { + let sparseEncoder = SparseFieldEncoder(wrapping: encoder, + encoding: fields) + + try NestedWrapped() + .encode(to: sparseEncoder) + } + + struct NestedWrapped: Encodable { + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + var nestedContainer1 = container.nestedContainer(keyedBy: NestedKeys.self, forKey: .hello) + + var nestedContainer2 = container.nestedUnkeyedContainer(forKey: .world) + + try nestedContainer1.encode(true, forKey: .nestedKey) + try nestedContainer2.encode(false) + } + + enum CodingKeys: String, Equatable, CodingKey { + case hello + case world + } + + enum NestedKeys: String, CodingKey { + case nestedKey + } + } + } + + struct SuperWrapper: Encodable { + let fields: [SuperWrapped.CodingKeys] + + func encode(to encoder: Encoder) throws { + let sparseEncoder = SparseFieldEncoder(wrapping: encoder, + encoding: fields) + + try SuperWrapped() + .encode(to: sparseEncoder) + } + + struct SuperWrapped: Encodable { + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + let superEncoder1 = container.superEncoder(forKey: .hello) + + let superEncoder2 = container.superEncoder() + + var container1 = superEncoder1.container(keyedBy: CodingKeys.self) + var container2 = superEncoder2.container(keyedBy: CodingKeys.self) + + try container1.encode(true, forKey: .hello) + try container2.encode(false, forKey: .world) + } + + enum CodingKeys: String, Equatable, CodingKey { + case hello + case world + } + } + } } From 1df891ce06467c3ec78f05cd089bebf619939bd9 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Tue, 6 Aug 2019 08:55:49 -0700 Subject: [PATCH 038/235] Add test that uses JSONAPIDocument protocol in generic context --- .../JSONAPITests/Document/DocumentTests.swift | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/Tests/JSONAPITests/Document/DocumentTests.swift b/Tests/JSONAPITests/Document/DocumentTests.swift index 5f9c18b..be10f7c 100644 --- a/Tests/JSONAPITests/Document/DocumentTests.swift +++ b/Tests/JSONAPITests/Document/DocumentTests.swift @@ -11,6 +11,34 @@ import Poly class DocumentTests: XCTestCase { + func test_genericDocFunc() { + func test(_ doc: Doc) { + let _ = encoded(value: doc) + + XCTAssert(Doc.PrimaryResourceBody.self == NoResourceBody.self) + XCTAssert(Doc.MetaType.self == NoMetadata.self) + XCTAssert(Doc.LinksType.self == NoLinks.self) + XCTAssert(Doc.IncludeType.self == NoIncludes.self) + XCTAssert(Doc.APIDescription.self == NoAPIDescription.self) + XCTAssert(Doc.Error.self == UnknownJSONAPIError.self) + } + + test(JSONAPI.Document< + NoResourceBody, + NoMetadata, + NoLinks, + NoIncludes, + NoAPIDescription, + UnknownJSONAPIError + >( + apiDescription: .none, + body: .none, + includes: .none, + meta: .none, + links: .none + )) + } + func test_singleDocumentNull() { let document = decoded(type: Document, NoMetadata, NoLinks, NoIncludes, NoAPIDescription, UnknownJSONAPIError>.self, data: single_document_null) From d5b4aa70c7c0aa716e1241ffce944eb8b59008ef Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Tue, 6 Aug 2019 09:04:15 -0700 Subject: [PATCH 039/235] Update linuxmain --- Tests/JSONAPITests/XCTestManifests.swift | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/Tests/JSONAPITests/XCTestManifests.swift b/Tests/JSONAPITests/XCTestManifests.swift index dc100bd..97b52b0 100644 --- a/Tests/JSONAPITests/XCTestManifests.swift +++ b/Tests/JSONAPITests/XCTestManifests.swift @@ -77,6 +77,7 @@ extension DocumentTests { ("test_errorDocumentNoMeta_encode", test_errorDocumentNoMeta_encode), ("test_errorDocumentNoMetaWithAPIDescription", test_errorDocumentNoMetaWithAPIDescription), ("test_errorDocumentNoMetaWithAPIDescription_encode", test_errorDocumentNoMetaWithAPIDescription_encode), + ("test_genericDocFunc", test_genericDocFunc), ("test_manyDocumentNoIncludes", test_manyDocumentNoIncludes), ("test_manyDocumentNoIncludes_encode", test_manyDocumentNoIncludes_encode), ("test_manyDocumentNoIncludesWithAPIDescription", test_manyDocumentNoIncludesWithAPIDescription), @@ -441,7 +442,13 @@ extension SparseFieldEncoderTests { // `swift test --generate-linuxmain` // to regenerate. static let __allTests__SparseFieldEncoderTests = [ - ("test_placeholder", test_placeholder), + ("test_AccurateCodingPath", test_AccurateCodingPath), + ("test_EverythingArsenal_allOff", test_EverythingArsenal_allOff), + ("test_EverythingArsenal_allOn", test_EverythingArsenal_allOn), + ("test_NestedContainers", test_NestedContainers), + ("test_NilEncode", test_NilEncode), + ("test_SkipsOmittedFields", test_SkipsOmittedFields), + ("test_SuperEncoderIsStillSparse", test_SuperEncoderIsStillSparse), ] } From 9a0bba8d070aa7fee680fa477ab16070d6ff1cd9 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Tue, 6 Aug 2019 09:12:02 -0700 Subject: [PATCH 040/235] Rename AppendableResourceBody to Appendable. It is used in conjunction with ResourceBody but does not conform to it anymore. --- Sources/JSONAPI/Document/Document.swift | 4 ++-- Sources/JSONAPI/Document/ResourceBody.swift | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Sources/JSONAPI/Document/Document.swift b/Sources/JSONAPI/Document/Document.swift index 4034969..caad138 100644 --- a/Sources/JSONAPI/Document/Document.swift +++ b/Sources/JSONAPI/Document/Document.swift @@ -203,7 +203,7 @@ extension Document where IncludeType == NoIncludes, MetaType == NoMetadata, Link } */ -extension Document.Body.Data where PrimaryResourceBody: AppendableResourceBody { +extension Document.Body.Data where PrimaryResourceBody: Appendable { public func merging(_ other: Document.Body.Data, combiningMetaWith metaMerge: (MetaType, MetaType) -> MetaType, combiningLinksWith linksMerge: (LinksType, LinksType) -> LinksType) -> Document.Body.Data { @@ -214,7 +214,7 @@ extension Document.Body.Data where PrimaryResourceBody: AppendableResourceBody { } } -extension Document.Body.Data where PrimaryResourceBody: AppendableResourceBody, MetaType == NoMetadata, LinksType == NoLinks { +extension Document.Body.Data where PrimaryResourceBody: Appendable, MetaType == NoMetadata, LinksType == NoLinks { public func merging(_ other: Document.Body.Data) -> Document.Body.Data { return merging(other, combiningMetaWith: { _, _ in .none }, diff --git a/Sources/JSONAPI/Document/ResourceBody.swift b/Sources/JSONAPI/Document/ResourceBody.swift index 2eb1bb4..4dbac87 100644 --- a/Sources/JSONAPI/Document/ResourceBody.swift +++ b/Sources/JSONAPI/Document/ResourceBody.swift @@ -40,11 +40,11 @@ public protocol ResourceBody: Decodable, EncodableResourceBody {} /// A `ResourceBody` that has the ability to take on more primary /// resources by appending another similarly typed `ResourceBody`. -public protocol AppendableResourceBody { +public protocol Appendable { func appending(_ other: Self) -> Self } -public func +(_ left: R, right: R) -> R { +public func +(_ left: R, right: R) -> R { return left.appending(right) } @@ -56,7 +56,7 @@ public struct SingleResourceBody: EncodableResourceBody, AppendableResourceBody { +public struct ManyResourceBody: EncodableResourceBody, Appendable { public let values: [Entity] public init(resourceObjects: [Entity]) { From 453ce4b3a8a2b50fb541f8627096cb4d2739d0ba Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Tue, 6 Aug 2019 09:19:06 -0700 Subject: [PATCH 041/235] whitespace changes -- trying to switch from one whitespace standard to another in cohesive chunks to fit Xcode 11 default. --- .../JSONAPITests/Includes/IncludeTests.swift | 40 +++++++++---------- .../ResourceBody/ResourceBodyTests.swift | 16 ++++---- 2 files changed, 28 insertions(+), 28 deletions(-) diff --git a/Tests/JSONAPITests/Includes/IncludeTests.swift b/Tests/JSONAPITests/Includes/IncludeTests.swift index 4272083..1bca6f5 100644 --- a/Tests/JSONAPITests/Includes/IncludeTests.swift +++ b/Tests/JSONAPITests/Includes/IncludeTests.swift @@ -350,45 +350,45 @@ extension IncludedTests { // MARK: - Test types extension IncludedTests { - enum TestEntityType: ResourceObjectDescription { + enum TestEntityType: ResourceObjectDescription { - typealias Relationships = NoRelationships + typealias Relationships = NoRelationships - public static var jsonType: String { return "test_entity1" } + public static var jsonType: String { return "test_entity1" } - public struct Attributes: JSONAPI.SparsableAttributes { - let foo: Attribute - let bar: Attribute + public struct Attributes: JSONAPI.SparsableAttributes { + let foo: Attribute + let bar: Attribute public enum CodingKeys: String, Equatable, CodingKey { case foo case bar } - } - } + } + } - typealias TestEntity = BasicEntity + typealias TestEntity = BasicEntity - enum TestEntityType2: ResourceObjectDescription { + enum TestEntityType2: ResourceObjectDescription { - public static var jsonType: String { return "test_entity2" } + public static var jsonType: String { return "test_entity2" } - public struct Relationships: JSONAPI.Relationships { - let entity1: ToOneRelationship - } + public struct Relationships: JSONAPI.Relationships { + let entity1: ToOneRelationship + } - public struct Attributes: JSONAPI.SparsableAttributes { - let foo: Attribute - let bar: Attribute + public struct Attributes: JSONAPI.SparsableAttributes { + let foo: Attribute + let bar: Attribute public enum CodingKeys: String, Equatable, CodingKey { case foo case bar } - } - } + } + } - typealias TestEntity2 = BasicEntity + typealias TestEntity2 = BasicEntity enum TestEntityType3: ResourceObjectDescription { diff --git a/Tests/JSONAPITests/ResourceBody/ResourceBodyTests.swift b/Tests/JSONAPITests/ResourceBody/ResourceBodyTests.swift index 6b12256..ad4c3fb 100644 --- a/Tests/JSONAPITests/ResourceBody/ResourceBodyTests.swift +++ b/Tests/JSONAPITests/ResourceBody/ResourceBodyTests.swift @@ -191,19 +191,19 @@ extension ResourceBodyTests { extension ResourceBodyTests { - enum ArticleType: ResourceObjectDescription { - public static var jsonType: String { return "articles" } + enum ArticleType: ResourceObjectDescription { + public static var jsonType: String { return "articles" } - typealias Relationships = NoRelationships + typealias Relationships = NoRelationships - struct Attributes: JSONAPI.SparsableAttributes { - let title: Attribute + struct Attributes: JSONAPI.SparsableAttributes { + let title: Attribute public enum CodingKeys: String, Equatable, CodingKey { case title } - } - } + } + } - typealias Article = BasicEntity + typealias Article = BasicEntity } From 8f9ec11f270734309b3441c0eaf8d1206763fe9a Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Wed, 14 Aug 2019 08:57:57 -0700 Subject: [PATCH 042/235] Housekeeping in the README --- README.md | 58 ++++++++++++++++++++----------------------------------- 1 file changed, 21 insertions(+), 37 deletions(-) diff --git a/README.md b/README.md index 7a128bc..ff5baeb 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ A Swift package for encoding to- and decoding from **JSON API** compliant reques See the JSON API Spec here: https://jsonapi.org/format/ -:warning: Although I find the type-safety of this framework appealing, the Swift compiler currently has enough trouble with it that it can become difficult to reason about errors produced by small typos. Similarly, auto-complete fails to provide reasonable suggestions much of the time. If you get the code right, everything compiles, otherwise it can suck to figure out what is wrong. This is mostly a concern when creating resource objects in-code (servers and test suites must do this). Writing a client that uses this framework to ingest JSON API Compliant API responses is much less painful. :warning: +:warning: This library provides well-tested type safety when working with JSON:API 1.0, however the Swift compiler can sometimes have difficulty tracking down small typos when initializing `ResourceObjects`. Once the code is written correctly, it will compile, but tracking down the source of programmer errors can be an annoyance. This is mostly a concern when creating resource objects in-code (servers and test cases must do this). Writing a client that uses this framework to ingest JSON API Compliant API responses is much less painful. :warning: ## Table of Contents @@ -109,52 +109,34 @@ Note that Playground support for importing non-system Frameworks is still a bit ### JSON:API #### Document -- `data` - - [x] Encoding/Decoding -- `included` - - [x] Encoding/Decoding -- `errors` - - [x] Encoding/Decoding -- `meta` - - [x] Encoding/Decoding -- `jsonapi` (i.e. API Information) - - [x] Encoding/Decoding -- `links` - - [x] Encoding/Decoding +- [x] `data` +- [x] `included` +- [x] `errors` +- [x] `meta` +- [x] `jsonapi` (i.e. API Information) +- [x] `links` #### Resource Object -- `id` - - [x] Encoding/Decoding -- `type` - - [x] Encoding/Decoding -- `attributes` - - [x] Encoding/Decoding -- `relationships` - - [x] Encoding/Decoding -- `links` - - [x] Encoding/Decoding -- `meta` - - [x] Encoding/Decoding +- [x] `id` +- [x] `type` +- [x] `attributes` +- [x] `relationships` +- [x] `links` +- [x] `meta` #### Relationship Object -- `data` - - [x] Encoding/Decoding -- `links` - - [x] Encoding/Decoding -- `meta` - - [x] Encoding/Decoding +- [x] `data` +- [x] `links` +- [x] `meta` #### Links Object -- `href` - - [x] Encoding/Decoding -- `meta` - - [x] Encoding/Decoding +- [x] `href` +- [x] `meta` ### Misc - [x] Support transforms on `Attributes` values (e.g. to support different representations of `Date`) - [x] Support validation on `Attributes`. -- [x] Support sparse fieldsets (encoding only). A client can likely just define a new model to represent a sparse population of another model in a very specific use case. On the server side, sparse fieldsets of Resource Objects can be encoded without creating one model for every possible sparse fieldset. -- [ ] Create more descriptive errors that are easier to use for troubleshooting. +- [x] Support sparse fieldsets (encoding only). A client can likely just define a new model to represent a sparse population of another model in a very specific use case for decoding purposes. On the server side, sparse fieldsets of Resource Objects can be encoded without creating one model for every possible sparse fieldset. ### Testing #### Resource Object Validator @@ -163,6 +145,8 @@ Note that Playground support for importing non-system Frameworks is still a bit - [x] Only allow `ToManyRelationship` and `ToOneRelationship` within `Relationships` struct. ### Potential Improvements +These ideas could be implemented in future versions. + - [ ] (Maybe) Use `KeyPath` to specify `Includes` thus creating type safety around the relationship between a primary resource type and the types of included resources. - [ ] (Maybe) Replace `SingleResourceBody` and `ManyResourceBody` with support at the `Document` level to just interpret `PrimaryResource`, `PrimaryResource?`, or `[PrimaryResource]` as the same decoding/encoding strategies. - [ ] Support sideposting. JSONAPI spec might become opinionated in the future (https://github.com/json-api/json-api/pull/1197, https://github.com/json-api/json-api/issues/1215, https://github.com/json-api/json-api/issues/1216) but there is also an existing implementation to consider (https://jsonapi-suite.github.io/jsonapi_suite/ruby/writes/nested-writes). At this time, any sidepost implementation would be an awesome tertiary library to be used alongside the primary JSONAPI library. Maybe `JSONAPISideloading`. From 86a9345fdd2f85d67d6338045628664663d18346 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Wed, 14 Aug 2019 09:22:06 -0700 Subject: [PATCH 043/235] Adding some documentation and making SparseFieldEncoder internal because it does not need to be public. --- Sources/JSONAPI/Document/Document.swift | 4 ++++ Sources/JSONAPI/Document/Includes.swift | 10 ++++++++++ Sources/JSONAPI/Document/ResourceBody.swift | 4 ++++ Sources/JSONAPI/Resource/Id.swift | 3 +++ Sources/JSONAPI/SparseFields/SparseFieldEncoder.swift | 4 ++-- .../SparseFields/SparseFieldEncoderTests.swift | 2 +- 6 files changed, 24 insertions(+), 3 deletions(-) diff --git a/Sources/JSONAPI/Document/Document.swift b/Sources/JSONAPI/Document/Document.swift index caad138..aedb87a 100644 --- a/Sources/JSONAPI/Document/Document.swift +++ b/Sources/JSONAPI/Document/Document.swift @@ -53,7 +53,9 @@ public struct Document public let meta: MetaType public let links: LinksType @@ -66,6 +68,8 @@ public struct Document> = ...` +/// +/// then you can access all `Thing1` included resources with +/// +/// `let includedThings = includes[Thing1.self]` public struct Includes: Encodable, Equatable { public static var none: Includes { return .init(values: []) } diff --git a/Sources/JSONAPI/Document/ResourceBody.swift b/Sources/JSONAPI/Document/ResourceBody.swift index 4dbac87..3fdef10 100644 --- a/Sources/JSONAPI/Document/ResourceBody.swift +++ b/Sources/JSONAPI/Document/ResourceBody.swift @@ -48,6 +48,9 @@ public func +(_ left: R, right: R) -> R { return left.appending(right) } +/// A type allowing for a document body containing 1 primary resource. +/// If the `Entity` specialization is an `Optional` type, the body can contain +/// 0 or 1 primary resources. public struct SingleResourceBody: EncodableResourceBody { public let value: Entity @@ -56,6 +59,7 @@ public struct SingleResourceBody: EncodableResourceBody, Appendable { public let values: [Entity] diff --git a/Sources/JSONAPI/Resource/Id.swift b/Sources/JSONAPI/Resource/Id.swift index 7265a18..a375c08 100644 --- a/Sources/JSONAPI/Resource/Id.swift +++ b/Sources/JSONAPI/Resource/Id.swift @@ -28,6 +28,9 @@ public protocol CreatableRawIdType: RawIdType { extension String: RawIdType {} +/// A type that can be used as the `MaybeRawId` for a `ResourceObject` that does not +/// have an Id (most likely because it was created by a client and the server will be responsible +/// for assigning it an Id). public struct Unidentified: MaybeRawId, CustomStringConvertible { public init() {} diff --git a/Sources/JSONAPI/SparseFields/SparseFieldEncoder.swift b/Sources/JSONAPI/SparseFields/SparseFieldEncoder.swift index a377fc9..36a1a95 100644 --- a/Sources/JSONAPI/SparseFields/SparseFieldEncoder.swift +++ b/Sources/JSONAPI/SparseFields/SparseFieldEncoder.swift @@ -5,7 +5,7 @@ // Created by Mathew Polzin on 8/4/19. // -public class SparseFieldEncoder: Encoder { +class SparseFieldEncoder: Encoder { private let wrappedEncoder: Encoder private let allowedKeys: [SparseKey] @@ -37,7 +37,7 @@ public class SparseFieldEncoder: Encoder { } } -public struct SparseFieldKeyedEncodingContainer: KeyedEncodingContainerProtocol where SparseKey: CodingKey, SparseKey: Equatable, Key: CodingKey { +struct SparseFieldKeyedEncodingContainer: KeyedEncodingContainerProtocol where SparseKey: CodingKey, SparseKey: Equatable, Key: CodingKey { private var wrappedContainer: KeyedEncodingContainer private let allowedKeys: [SparseKey] diff --git a/Tests/JSONAPITests/SparseFields/SparseFieldEncoderTests.swift b/Tests/JSONAPITests/SparseFields/SparseFieldEncoderTests.swift index f0abce2..c6192f8 100644 --- a/Tests/JSONAPITests/SparseFields/SparseFieldEncoderTests.swift +++ b/Tests/JSONAPITests/SparseFields/SparseFieldEncoderTests.swift @@ -6,7 +6,7 @@ // import XCTest -import JSONAPI +@testable import JSONAPI import Foundation class SparseFieldEncoderTests: XCTestCase { From 89217f7187e9118d988f921325015addcfb870bc Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Wed, 14 Aug 2019 17:31:43 -0700 Subject: [PATCH 044/235] bump podspec version --- JSONAPI.podspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/JSONAPI.podspec b/JSONAPI.podspec index d884b6c..615b1c1 100644 --- a/JSONAPI.podspec +++ b/JSONAPI.podspec @@ -16,7 +16,7 @@ Pod::Spec.new do |spec| # spec.name = "JSONAPI" - spec.version = "0.31.1" + spec.version = "1.0.0" spec.summary = "Swift Codable JSON API framework." # This description is used to generate tags and improve search results. From 9db534dea07f6e94c9bbf3856b8103d9da9ec827 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Tue, 27 Aug 2019 00:48:11 -0700 Subject: [PATCH 045/235] Update README.md Add basic example Google Colab. --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index ff5baeb..44b8d59 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,10 @@ See the JSON API Spec here: https://jsonapi.org/format/ :warning: This library provides well-tested type safety when working with JSON:API 1.0, however the Swift compiler can sometimes have difficulty tracking down small typos when initializing `ResourceObjects`. Once the code is written correctly, it will compile, but tracking down the source of programmer errors can be an annoyance. This is mostly a concern when creating resource objects in-code (servers and test cases must do this). Writing a client that uses this framework to ingest JSON API Compliant API responses is much less painful. :warning: +## Quick Start + +Basic Example Colab: https://colab.research.google.com/drive/1IS7lRSBGoiW02Vd1nN_rfdDbZvTDj6Te + ## Table of Contents From 198e5a9800db9bd981936e1fe04d86dfae6703be Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Wed, 28 Aug 2019 18:55:11 -0700 Subject: [PATCH 046/235] Update README.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 44b8d59..ac41d6a 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,9 @@ See the JSON API Spec here: https://jsonapi.org/format/ ## Quick Start +### Clientside Basic Example Colab: https://colab.research.google.com/drive/1IS7lRSBGoiW02Vd1nN_rfdDbZvTDj6Te +Includes Example Colab: https://colab.research.google.com/drive/1BdF0Kc7l2ixDfBZEL16FY6palweDszQU ## Table of Contents From 771641b82ce29f056177155ec3079f0c5d482a48 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Wed, 28 Aug 2019 18:55:42 -0700 Subject: [PATCH 047/235] Update README.md --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index ac41d6a..0b38d4e 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,8 @@ See the JSON API Spec here: https://jsonapi.org/format/ ## Quick Start ### Clientside -Basic Example Colab: https://colab.research.google.com/drive/1IS7lRSBGoiW02Vd1nN_rfdDbZvTDj6Te +Basic Example Colab: https://colab.research.google.com/drive/1IS7lRSBGoiW02Vd1nN_rfdDbZvTDj6Te + Includes Example Colab: https://colab.research.google.com/drive/1BdF0Kc7l2ixDfBZEL16FY6palweDszQU ## Table of Contents From 7bd40217906d6b250f78f03462dc0ef83aae88c2 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Wed, 28 Aug 2019 20:21:50 -0700 Subject: [PATCH 048/235] Update README.md --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 0b38d4e..8ad6ff0 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,9 @@ See the JSON API Spec here: https://jsonapi.org/format/ ### Clientside Basic Example Colab: https://colab.research.google.com/drive/1IS7lRSBGoiW02Vd1nN_rfdDbZvTDj6Te -Includes Example Colab: https://colab.research.google.com/drive/1BdF0Kc7l2ixDfBZEL16FY6palweDszQU +Includes Example Colab: https://colab.research.google.com/drive/1BdF0Kc7l2ixDfBZEL16FY6palweDszQU + +Metadata Example Colab: https://colab.research.google.com/drive/10dEESwiE9I3YoyfzVeOVwOKUTEgLT3qr ## Table of Contents From bf8245c7b8c90eb3ae9f6e4931afe06895021b78 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Thu, 29 Aug 2019 09:30:59 -0700 Subject: [PATCH 049/235] Update README.md --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 8ad6ff0..f107a5d 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,9 @@ Basic Example Colab: https://colab.research.google.com/drive/1IS7lRSBGoiW02Vd1nN Includes Example Colab: https://colab.research.google.com/drive/1BdF0Kc7l2ixDfBZEL16FY6palweDszQU -Metadata Example Colab: https://colab.research.google.com/drive/10dEESwiE9I3YoyfzVeOVwOKUTEgLT3qr +Metadata Example Colab: https://colab.research.google.com/drive/10dEESwiE9I3YoyfzVeOVwOKUTEgLT3qr + +Errors Example Colab: https://colab.research.google.com/drive/1TIv6STzlHrkTf_-9Eu8sv8NoaxhZcFZH ## Table of Contents From b33c7f4ab56fa289ccac0f48e81ba4f0eb0bab0b Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Thu, 29 Aug 2019 09:33:03 -0700 Subject: [PATCH 050/235] Update README.md --- README.md | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index f107a5d..07301ee 100644 --- a/README.md +++ b/README.md @@ -10,13 +10,10 @@ See the JSON API Spec here: https://jsonapi.org/format/ ## Quick Start ### Clientside -Basic Example Colab: https://colab.research.google.com/drive/1IS7lRSBGoiW02Vd1nN_rfdDbZvTDj6Te - -Includes Example Colab: https://colab.research.google.com/drive/1BdF0Kc7l2ixDfBZEL16FY6palweDszQU - -Metadata Example Colab: https://colab.research.google.com/drive/10dEESwiE9I3YoyfzVeOVwOKUTEgLT3qr - -Errors Example Colab: https://colab.research.google.com/drive/1TIv6STzlHrkTf_-9Eu8sv8NoaxhZcFZH +- [Basic Example](https://colab.research.google.com/drive/1IS7lRSBGoiW02Vd1nN_rfdDbZvTDj6Te) +- [Includes Example](https://colab.research.google.com/drive/1BdF0Kc7l2ixDfBZEL16FY6palweDszQU) +- [Metadata Example](https://colab.research.google.com/drive/10dEESwiE9I3YoyfzVeOVwOKUTEgLT3qr) +- [Errors Example](https://colab.research.google.com/drive/1TIv6STzlHrkTf_-9Eu8sv8NoaxhZcFZH) ## Table of Contents From 51b981c10864d2fb3431afe2f590ea0902acc688 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Mon, 2 Sep 2019 13:25:57 -0700 Subject: [PATCH 051/235] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 07301ee..0602192 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ See the JSON API Spec here: https://jsonapi.org/format/ ### Clientside - [Basic Example](https://colab.research.google.com/drive/1IS7lRSBGoiW02Vd1nN_rfdDbZvTDj6Te) -- [Includes Example](https://colab.research.google.com/drive/1BdF0Kc7l2ixDfBZEL16FY6palweDszQU) +- [Compound Example](https://colab.research.google.com/drive/1BdF0Kc7l2ixDfBZEL16FY6palweDszQU) - [Metadata Example](https://colab.research.google.com/drive/10dEESwiE9I3YoyfzVeOVwOKUTEgLT3qr) - [Errors Example](https://colab.research.google.com/drive/1TIv6STzlHrkTf_-9Eu8sv8NoaxhZcFZH) From 6c548455256b3153cdf4471309ad1311c7e9acd9 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Mon, 2 Sep 2019 15:00:07 -0700 Subject: [PATCH 052/235] Update README.md --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 0602192..3617ee7 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,9 @@ See the JSON API Spec here: https://jsonapi.org/format/ - [Metadata Example](https://colab.research.google.com/drive/10dEESwiE9I3YoyfzVeOVwOKUTEgLT3qr) - [Errors Example](https://colab.research.google.com/drive/1TIv6STzlHrkTf_-9Eu8sv8NoaxhZcFZH) +### Serverside +- [Basic Example](https://colab.research.google.com/drive/1krbhzSfz8mwkBTQQnKUZJLEtYsJKSfYX) + ## Table of Contents From 0cc2368331b791e0b88fca89ec6dc9f7adcf6c97 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Mon, 2 Sep 2019 15:05:23 -0700 Subject: [PATCH 053/235] Update README.md Add instructions for adding as Swift Package Manager dependency. --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.md b/README.md index 3617ee7..e447620 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,7 @@ See the JSON API Spec here: https://jsonapi.org/format/ - [Caveat](#caveat) - [Dev Environment](#dev-environment) - [Prerequisites](#prerequisites) + - [Swift Package Manager](#swift-package-manager) - [CocoaPods](#cocoapods) - [Xcode project](#xcode-project) - [Running the Playground](#running-the-playground) @@ -100,6 +101,12 @@ If you find something wrong with this library and it isn't already mentioned und 1. Swift 4.2+ 2. Swift Package Manager *OR* Cocoapods +### Swift Package Manager +Just include the following in your package's dependencies and add `JSONAPI` to the dependencies for any of your targets. +``` + .package(url: "https://github.com/mattpolzin/JSONAPI.git", .upToNextMajor(from: "1.0.0")) +``` + ### CocoaPods To use this framework in your project via Cocoapods instead of Swift Package Manager, add the following dependencies to your Podfile. ``` From af7eaeb68433aa2d740e2a9e35f4eea0d6252505 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Mon, 2 Sep 2019 15:10:43 -0700 Subject: [PATCH 054/235] Update README.md --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index e447620..c8d1bec 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,10 @@ See the JSON API Spec here: https://jsonapi.org/format/ - [Errors Example](https://colab.research.google.com/drive/1TIv6STzlHrkTf_-9Eu8sv8NoaxhZcFZH) ### Serverside -- [Basic Example](https://colab.research.google.com/drive/1krbhzSfz8mwkBTQQnKUZJLEtYsJKSfYX) +- [Basic GET Response Example](https://colab.research.google.com/drive/1krbhzSfz8mwkBTQQnKUZJLEtYsJKSfYX) + +### Combined +This library works well when used by both the server responsible for serialization and the client responsible for deserialization. Check out the [example](#example) further down in this README. ## Table of Contents From f7f36c6e7a3143631abfc85f1701034990ff4049 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Mon, 2 Sep 2019 15:59:24 -0700 Subject: [PATCH 055/235] Update README.md --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index c8d1bec..91a5515 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,8 @@ See the JSON API Spec here: https://jsonapi.org/format/ - [Errors Example](https://colab.research.google.com/drive/1TIv6STzlHrkTf_-9Eu8sv8NoaxhZcFZH) ### Serverside -- [Basic GET Response Example](https://colab.research.google.com/drive/1krbhzSfz8mwkBTQQnKUZJLEtYsJKSfYX) +- [GET Example](https://colab.research.google.com/drive/1krbhzSfz8mwkBTQQnKUZJLEtYsJKSfYX) +- [POST Example](https://colab.research.google.com/drive/1z3n70LwRY7vLIgbsMghvnfHA67QiuqpQ) ### Combined This library works well when used by both the server responsible for serialization and the client responsible for deserialization. Check out the [example](#example) further down in this README. From 44f21b44302bec7f847fdd321f2b9bc858663d95 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Mon, 2 Sep 2019 16:04:06 -0700 Subject: [PATCH 056/235] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 91a5515..22c4f7e 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ A Swift package for encoding to- and decoding from **JSON API** compliant reques See the JSON API Spec here: https://jsonapi.org/format/ -:warning: This library provides well-tested type safety when working with JSON:API 1.0, however the Swift compiler can sometimes have difficulty tracking down small typos when initializing `ResourceObjects`. Once the code is written correctly, it will compile, but tracking down the source of programmer errors can be an annoyance. This is mostly a concern when creating resource objects in-code (servers and test cases must do this). Writing a client that uses this framework to ingest JSON API Compliant API responses is much less painful. :warning: +:warning: This library provides well-tested type safety when working with JSON:API 1.0. However, the Swift compiler can sometimes have difficulty tracking down small typos when initializing `ResourceObjects`. Once the code is written correctly, it will compile, but tracking down the source of programmer errors can be an annoyance. This is mostly a concern when creating resource objects in-code (servers and test cases must do this). Writing a client that uses this framework to ingest JSON API Compliant API responses is much less painful. :warning: ## Quick Start From baee85fb1b9492fd062a514a7c86abe374e4e14a Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Mon, 2 Sep 2019 16:07:10 -0700 Subject: [PATCH 057/235] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 22c4f7e..f582bac 100644 --- a/README.md +++ b/README.md @@ -112,7 +112,7 @@ Just include the following in your package's dependencies and add `JSONAPI` to t ``` ### CocoaPods -To use this framework in your project via Cocoapods instead of Swift Package Manager, add the following dependencies to your Podfile. +To use this framework in your project via Cocoapods, add the following dependencies to your Podfile. ``` pod 'Poly', :git => 'https://github.com/mattpolzin/Poly.git' pod 'JSONAPI', :git => 'https://github.com/mattpolzin/JSONAPI.git' From ff06c36b362c0f5695b02dfb150ea17ee58739e1 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Thu, 5 Sep 2019 22:22:55 -0700 Subject: [PATCH 058/235] uncomment out language version declaration in package manifest, 'fix' indentation. --- Package.swift | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/Package.swift b/Package.swift index f88e67d..33d743d 100644 --- a/Package.swift +++ b/Package.swift @@ -6,33 +6,33 @@ import PackageDescription let package = Package( name: "JSONAPI", platforms: [ - .macOS(.v10_10), - .iOS(.v10) + .macOS(.v10_10), + .iOS(.v10) ], products: [ .library( name: "JSONAPI", targets: ["JSONAPI"]), - .library( - name: "JSONAPITesting", - targets: ["JSONAPITesting"]) + .library( + name: "JSONAPITesting", + targets: ["JSONAPITesting"]) ], dependencies: [ - .package(url: "https://github.com/mattpolzin/Poly.git", .upToNextMajor(from: "2.0.0")), + .package(url: "https://github.com/mattpolzin/Poly.git", .upToNextMajor(from: "2.0.0")), ], targets: [ .target( name: "JSONAPI", dependencies: ["Poly"]), - .target( - name: "JSONAPITesting", - dependencies: ["JSONAPI"]), + .target( + name: "JSONAPITesting", + dependencies: ["JSONAPI"]), .testTarget( name: "JSONAPITests", dependencies: ["JSONAPI", "JSONAPITesting"]), .testTarget( name: "JSONAPITestingTests", dependencies: ["JSONAPI", "JSONAPITesting"]) - ] -// swiftLanguageVersions: [.version("5.1")] + ], + swiftLanguageVersions: [.v5] ) From e9048a4861eb227db0475a5dee1316ecdcdf5e07 Mon Sep 17 00:00:00 2001 From: Chris Ballinger Date: Fri, 13 Sep 2019 15:07:37 -0700 Subject: [PATCH 059/235] Change Pod name to avoid conflict. Resolves #32 --- JSONAPI.podspec | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/JSONAPI.podspec b/JSONAPI.podspec index 615b1c1..f5cac91 100644 --- a/JSONAPI.podspec +++ b/JSONAPI.podspec @@ -15,7 +15,7 @@ Pod::Spec.new do |spec| # summary should be tweet-length, and the description more in depth. # - spec.name = "JSONAPI" + spec.name = "MP-JSONAPI" spec.version = "1.0.0" spec.summary = "Swift Codable JSON API framework." @@ -132,6 +132,7 @@ See the JSON API Spec here: https://jsonapi.org/format/ # you can include multiple dependencies to ensure it works. spec.swift_version = "5.0" + spec.module_name = "JSONAPI" # spec.requires_arc = true # spec.xcconfig = { "HEADER_SEARCH_PATHS" => "$(SDKROOT)/usr/include/libxml2" } From e99c6625f881d95f04fa9c005e9727c9abe20f25 Mon Sep 17 00:00:00 2001 From: Chris Ballinger Date: Sat, 14 Sep 2019 10:07:53 -0700 Subject: [PATCH 060/235] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f582bac..508e82c 100644 --- a/README.md +++ b/README.md @@ -115,7 +115,7 @@ Just include the following in your package's dependencies and add `JSONAPI` to t To use this framework in your project via Cocoapods, add the following dependencies to your Podfile. ``` pod 'Poly', :git => 'https://github.com/mattpolzin/Poly.git' - pod 'JSONAPI', :git => 'https://github.com/mattpolzin/JSONAPI.git' + pod 'MP-JSONAPI', :git => 'https://github.com/mattpolzin/JSONAPI.git' ``` ### Xcode project From 3ff1b867cab4ca77fa0f409096f906c720ad12da Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Sat, 14 Sep 2019 15:53:48 -0700 Subject: [PATCH 061/235] Bump spec version to account for breaking changes with configs and swift tools version --- JSONAPI.podspec | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/JSONAPI.podspec b/JSONAPI.podspec index f5cac91..caad3a6 100644 --- a/JSONAPI.podspec +++ b/JSONAPI.podspec @@ -16,7 +16,7 @@ Pod::Spec.new do |spec| # spec.name = "MP-JSONAPI" - spec.version = "1.0.0" + spec.version = "2.0.0" spec.summary = "Swift Codable JSON API framework." # This description is used to generate tags and improve search results. @@ -131,7 +131,7 @@ See the JSON API Spec here: https://jsonapi.org/format/ # where they will only apply to your library. If you depend on other Podspecs # you can include multiple dependencies to ensure it works. - spec.swift_version = "5.0" + spec.swift_version = "5.1" spec.module_name = "JSONAPI" # spec.requires_arc = true From 5ed45078a15ca0670b3e58ceead626f08d8f8c05 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Sat, 14 Sep 2019 16:44:46 -0700 Subject: [PATCH 062/235] Update README.md Fix incorrect Swift version requirement and suggested SPM dependency version. --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index b9c7a5d..6c0a231 100644 --- a/README.md +++ b/README.md @@ -102,13 +102,13 @@ If you find something wrong with this library and it isn't already mentioned und ## Dev Environment ### Prerequisites -1. Swift 4.2+ +1. Swift 5.1+ 2. Swift Package Manager *OR* Cocoapods ### Swift Package Manager Just include the following in your package's dependencies and add `JSONAPI` to the dependencies for any of your targets. ``` - .package(url: "https://github.com/mattpolzin/JSONAPI.git", .upToNextMajor(from: "1.0.0")) + .package(url: "https://github.com/mattpolzin/JSONAPI.git", .upToNextMajor(from: "2.0.0")) ``` ### CocoaPods From 87e9ee060668c3752dd7036537720b1710d6382b Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Mon, 16 Sep 2019 09:37:34 -0700 Subject: [PATCH 063/235] remove tests that are duplicates of those in the Poly package. I just never deleted this file when the Poly stuff moved into its own package. Add support for Include10. --- Package.resolved | 4 +- Package.swift | 2 +- Sources/JSONAPI/Document/Includes.swift | 8 + .../Resource/Poly+PrimaryResource.swift | 5 + .../JSONAPITests/Includes/IncludeTests.swift | 32 + .../Includes/stubs/IncludeStubs.swift | 85 +++ Tests/JSONAPITests/Poly/PolyTests.swift | 683 ------------------ 7 files changed, 133 insertions(+), 686 deletions(-) delete mode 100644 Tests/JSONAPITests/Poly/PolyTests.swift diff --git a/Package.resolved b/Package.resolved index 4a251e7..f7356ed 100644 --- a/Package.resolved +++ b/Package.resolved @@ -6,8 +6,8 @@ "repositoryURL": "https://github.com/mattpolzin/Poly.git", "state": { "branch": null, - "revision": "38051821d7ef49e590e26e819a2fe447e50be9ff", - "version": "2.0.1" + "revision": "4a08517b24f8e9f6dd8c02ec7da316aac5c00e2e", + "version": "2.1.0" } } ] diff --git a/Package.swift b/Package.swift index 33d743d..e0f38ab 100644 --- a/Package.swift +++ b/Package.swift @@ -18,7 +18,7 @@ let package = Package( targets: ["JSONAPITesting"]) ], dependencies: [ - .package(url: "https://github.com/mattpolzin/Poly.git", .upToNextMajor(from: "2.0.0")), + .package(url: "https://github.com/mattpolzin/Poly.git", .upToNextMajor(from: "2.1.0")), ], targets: [ .target( diff --git a/Sources/JSONAPI/Document/Includes.swift b/Sources/JSONAPI/Document/Includes.swift index dc9eb02..65ccee0 100644 --- a/Sources/JSONAPI/Document/Includes.swift +++ b/Sources/JSONAPI/Document/Includes.swift @@ -161,3 +161,11 @@ extension Includes where I: _Poly9 { return values.compactMap { $0.i } } } + +// MARK: - 10 includes +public typealias Include10 = Poly10 +extension Includes where I: _Poly10 { + public subscript(_ lookup: I.J.Type) -> [I.J] { + return values.compactMap { $0.j } + } +} diff --git a/Sources/JSONAPI/Resource/Poly+PrimaryResource.swift b/Sources/JSONAPI/Resource/Poly+PrimaryResource.swift index f1cad3a..d1e1e5f 100644 --- a/Sources/JSONAPI/Resource/Poly+PrimaryResource.swift +++ b/Sources/JSONAPI/Resource/Poly+PrimaryResource.swift @@ -74,3 +74,8 @@ extension Poly8: PrimaryResource, OptionalPrimaryResource where A: PolyWrapped, extension Poly9: EncodablePrimaryResource, OptionalEncodablePrimaryResource where A: EncodablePolyWrapped, B: EncodablePolyWrapped, C: EncodablePolyWrapped, D: EncodablePolyWrapped, E: EncodablePolyWrapped, F: EncodablePolyWrapped, G: EncodablePolyWrapped, H: EncodablePolyWrapped, I: EncodablePolyWrapped {} extension Poly9: PrimaryResource, OptionalPrimaryResource where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped, E: PolyWrapped, F: PolyWrapped, G: PolyWrapped, H: PolyWrapped, I: PolyWrapped {} + +// MARK: - 10 types +extension Poly10: EncodablePrimaryResource, OptionalEncodablePrimaryResource where A: EncodablePolyWrapped, B: EncodablePolyWrapped, C: EncodablePolyWrapped, D: EncodablePolyWrapped, E: EncodablePolyWrapped, F: EncodablePolyWrapped, G: EncodablePolyWrapped, H: EncodablePolyWrapped, I: EncodablePolyWrapped, J: EncodablePolyWrapped {} + +extension Poly10: PrimaryResource, OptionalPrimaryResource where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped, E: PolyWrapped, F: PolyWrapped, G: PolyWrapped, H: PolyWrapped, I: PolyWrapped, J: PolyWrapped {} diff --git a/Tests/JSONAPITests/Includes/IncludeTests.swift b/Tests/JSONAPITests/Includes/IncludeTests.swift index 1bca6f5..ed94e89 100644 --- a/Tests/JSONAPITests/Includes/IncludeTests.swift +++ b/Tests/JSONAPITests/Includes/IncludeTests.swift @@ -176,6 +176,27 @@ class IncludedTests: XCTestCase { test_DecodeEncodeEquality(type: Includes>.self, data: nine_different_type_includes) } + + func test_TenDifferentIncludes() { + let includes = decoded(type: Includes>.self, + data: ten_different_type_includes) + + XCTAssertEqual(includes[TestEntity.self].count, 1) + XCTAssertEqual(includes[TestEntity2.self].count, 1) + XCTAssertEqual(includes[TestEntity3.self].count, 1) + XCTAssertEqual(includes[TestEntity4.self].count, 1) + XCTAssertEqual(includes[TestEntity5.self].count, 1) + XCTAssertEqual(includes[TestEntity6.self].count, 1) + XCTAssertEqual(includes[TestEntity7.self].count, 1) + XCTAssertEqual(includes[TestEntity8.self].count, 1) + XCTAssertEqual(includes[TestEntity9.self].count, 1) + XCTAssertEqual(includes[TestEntity10.self].count, 1) + } + + func test_TenDifferentIncludes_encode() { + test_DecodeEncodeEquality(type: Includes>.self, + data: ten_different_type_includes) + } } // MARK: - Appending @@ -471,4 +492,15 @@ extension IncludedTests { } typealias TestEntity9 = BasicEntity + + enum TestEntityType10: ResourceObjectDescription { + + typealias Attributes = NoAttributes + + public static var jsonType: String { return "test_entity10" } + + typealias Relationships = NoRelationships + } + + typealias TestEntity10 = BasicEntity } diff --git a/Tests/JSONAPITests/Includes/stubs/IncludeStubs.swift b/Tests/JSONAPITests/Includes/stubs/IncludeStubs.swift index 10fd6a3..80f1bfa 100644 --- a/Tests/JSONAPITests/Includes/stubs/IncludeStubs.swift +++ b/Tests/JSONAPITests/Includes/stubs/IncludeStubs.swift @@ -512,3 +512,88 @@ let nine_different_type_includes = """ } ] """.data(using: .utf8)! + +let ten_different_type_includes = """ +[ + { + "type": "test_entity1", + "id": "2DF03B69-4B0A-467F-B52E-B0C9E44FCECF", + "attributes": { + "foo": "Hello", + "bar": 123 + } + }, + { + "type": "test_entity2", + "id": "90F03B69-4DF1-467F-B52E-B0C9E44FC333", + "attributes": { + "foo": "World", + "bar": 456 + }, + "relationships": { + "entity1": { + "data": { + "type": "test_entity1", + "id": "2DF03B69-4B0A-467F-B52E-B0C9E44FCECF" + } + } + } + }, + { + "type": "test_entity3", + "id": "11223B69-4DF1-467F-B52E-B0C9E44FC443", + "relationships": { + "entity1": { + "data": { + "type": "test_entity1", + "id": "2DF03B69-4B0A-467F-B52E-B0C9E44FCECF" + } + }, + "entity2": { + "data": [ + { + "type": "test_entity2", + "id": "90F03B69-4DF1-467F-B52E-B0C9E44FC333" + } + ] + } + } + }, + { + "type": "test_entity6", + "id": "11113B69-4DF1-467F-B52E-B0C9E44FC444", + "relationships": { + "entity4": { + "data": { + "type": "test_entity4", + "id": "364B3B69-4DF1-467F-B52E-B0C9E44F666E" + } + } + } + }, + { + "type": "test_entity5", + "id": "A24B3B69-4DF1-467F-B52E-B0C9E44F436A" + }, + { + "type": "test_entity4", + "id": "364B3B69-4DF1-467F-B52E-B0C9E44F666E" + }, + { + "type": "test_entity7", + "id": "364B3B69-4DF1-222F-B52E-B0C9E44F666E" + }, + { + "type": "test_entity8", + "id": "364B3B69-4DF1-222F-B52E-B0C9E44F266F" + }, + { + "type": "test_entity9", + "id": "364B3B69-4DF1-218F-B52E-B0C9E44F2661" + }, + { + "type": "test_entity10", + "id": "264B3B69-4DF1-212F-B52E-B0C9E44F2660" + } +] +""".data(using: .utf8)! diff --git a/Tests/JSONAPITests/Poly/PolyTests.swift b/Tests/JSONAPITests/Poly/PolyTests.swift deleted file mode 100644 index 0662ba0..0000000 --- a/Tests/JSONAPITests/Poly/PolyTests.swift +++ /dev/null @@ -1,683 +0,0 @@ -// -// PolyTests.swift -// JSONAPITests -// -// Created by Mathew Polzin on 11/23/18. -// - -import XCTest -import JSONAPI -import Poly - -// MARK: - init -class PolyTests: XCTestCase { - func test_init_Poly0() { - let _ = Poly0() - } - - func test_init_Poly1() { - let entity = TestEntity5(attributes: .none, relationships: .none, meta: .none, links: .none) - let poly = Poly1(entity) - XCTAssertEqual(poly.a, entity) - } - - func test_init_Poly2() { - let entity = TestEntity5(attributes: .none, relationships: .none, meta: .none, links: .none) - let poly = Poly2(entity) - XCTAssertEqual(poly.a, entity) - XCTAssertNil(poly.b) - - let poly2 = Poly2(entity) - XCTAssertEqual(poly2.b, entity) - XCTAssertNil(poly2.a) - } - - func test_init_Poly3() { - let entity = TestEntity5(attributes: .none, relationships: .none, meta: .none, links: .none) - let poly = Poly3(entity) - XCTAssertEqual(poly.a, entity) - XCTAssertNil(poly.b) - XCTAssertNil(poly.c) - - let poly2 = Poly3(entity) - XCTAssertEqual(poly2.b, entity) - XCTAssertNil(poly2.a) - XCTAssertNil(poly2.c) - - let poly3 = Poly3(entity) - XCTAssertEqual(poly3.c, entity) - XCTAssertNil(poly3.a) - XCTAssertNil(poly3.b) - } - - func test_init_Poly4() { - let entity = TestEntity5(attributes: .none, relationships: .none, meta: .none, links: .none) - let poly = Poly4(entity) - XCTAssertEqual(poly.a, entity) - XCTAssertNil(poly.b) - XCTAssertNil(poly.c) - XCTAssertNil(poly.d) - - let poly2 = Poly4(entity) - XCTAssertEqual(poly2.b, entity) - XCTAssertNil(poly2.a) - XCTAssertNil(poly2.c) - XCTAssertNil(poly2.d) - - let poly3 = Poly4(entity) - XCTAssertEqual(poly3.c, entity) - XCTAssertNil(poly3.a) - XCTAssertNil(poly3.b) - XCTAssertNil(poly3.d) - - let poly4 = Poly4(entity) - XCTAssertEqual(poly4.d, entity) - XCTAssertNil(poly4.a) - XCTAssertNil(poly4.b) - XCTAssertNil(poly4.c) - } - - func test_init_Poly5() { - let entity = TestEntity5(attributes: .none, relationships: .none, meta: .none, links: .none) - let poly = Poly5(entity) - XCTAssertEqual(poly.a, entity) - XCTAssertNil(poly.b) - XCTAssertNil(poly.c) - XCTAssertNil(poly.d) - XCTAssertNil(poly.e) - - let poly2 = Poly5(entity) - XCTAssertEqual(poly2.b, entity) - XCTAssertNil(poly2.a) - XCTAssertNil(poly2.c) - XCTAssertNil(poly2.d) - XCTAssertNil(poly2.e) - - let poly3 = Poly5(entity) - XCTAssertEqual(poly3.c, entity) - XCTAssertNil(poly3.a) - XCTAssertNil(poly3.b) - XCTAssertNil(poly3.d) - XCTAssertNil(poly3.e) - - let poly4 = Poly5(entity) - XCTAssertEqual(poly4.d, entity) - XCTAssertNil(poly4.a) - XCTAssertNil(poly4.b) - XCTAssertNil(poly4.c) - XCTAssertNil(poly4.e) - - let poly5 = Poly5(entity) - XCTAssertEqual(poly5.e, entity) - XCTAssertNil(poly5.a) - XCTAssertNil(poly5.b) - XCTAssertNil(poly5.c) - XCTAssertNil(poly5.d) - } - - func test_init_Poly6() { - let entity = TestEntity5(attributes: .none, relationships: .none, meta: .none, links: .none) - let poly = Poly6(entity) - XCTAssertEqual(poly.a, entity) - XCTAssertNil(poly.b) - XCTAssertNil(poly.c) - XCTAssertNil(poly.d) - XCTAssertNil(poly.e) - XCTAssertNil(poly.f) - - let poly2 = Poly6(entity) - XCTAssertEqual(poly2.b, entity) - XCTAssertNil(poly2.a) - XCTAssertNil(poly2.c) - XCTAssertNil(poly2.d) - XCTAssertNil(poly2.e) - XCTAssertNil(poly2.f) - - let poly3 = Poly6(entity) - XCTAssertEqual(poly3.c, entity) - XCTAssertNil(poly3.a) - XCTAssertNil(poly3.b) - XCTAssertNil(poly3.d) - XCTAssertNil(poly3.e) - XCTAssertNil(poly3.f) - - let poly4 = Poly6(entity) - XCTAssertEqual(poly4.d, entity) - XCTAssertNil(poly4.a) - XCTAssertNil(poly4.b) - XCTAssertNil(poly4.c) - XCTAssertNil(poly4.e) - XCTAssertNil(poly4.f) - - let poly5 = Poly6(entity) - XCTAssertEqual(poly5.e, entity) - XCTAssertNil(poly5.a) - XCTAssertNil(poly5.b) - XCTAssertNil(poly5.c) - XCTAssertNil(poly5.d) - XCTAssertNil(poly5.f) - - let poly6 = Poly6(entity) - XCTAssertEqual(poly6.f, entity) - XCTAssertNil(poly6.a) - XCTAssertNil(poly6.b) - XCTAssertNil(poly6.c) - XCTAssertNil(poly6.d) - XCTAssertNil(poly6.e) - } - - func test_init_Poly7() { - let entity = TestEntity5(attributes: .none, relationships: .none, meta: .none, links: .none) - let poly = Poly7(entity) - XCTAssertEqual(poly.a, entity) - XCTAssertNil(poly.b) - XCTAssertNil(poly.c) - XCTAssertNil(poly.d) - XCTAssertNil(poly.e) - XCTAssertNil(poly.f) - XCTAssertNil(poly.g) - - let poly2 = Poly7(entity) - XCTAssertEqual(poly2.b, entity) - XCTAssertNil(poly2.a) - XCTAssertNil(poly2.c) - XCTAssertNil(poly2.d) - XCTAssertNil(poly2.e) - XCTAssertNil(poly2.f) - XCTAssertNil(poly2.g) - - let poly3 = Poly7(entity) - XCTAssertEqual(poly3.c, entity) - XCTAssertNil(poly3.a) - XCTAssertNil(poly3.b) - XCTAssertNil(poly3.d) - XCTAssertNil(poly3.e) - XCTAssertNil(poly3.f) - XCTAssertNil(poly3.g) - - let poly4 = Poly7(entity) - XCTAssertEqual(poly4.d, entity) - XCTAssertNil(poly4.a) - XCTAssertNil(poly4.b) - XCTAssertNil(poly4.c) - XCTAssertNil(poly4.e) - XCTAssertNil(poly4.f) - XCTAssertNil(poly4.g) - - let poly5 = Poly7(entity) - XCTAssertEqual(poly5.e, entity) - XCTAssertNil(poly5.a) - XCTAssertNil(poly5.b) - XCTAssertNil(poly5.c) - XCTAssertNil(poly5.d) - XCTAssertNil(poly5.f) - XCTAssertNil(poly5.g) - - let poly6 = Poly7(entity) - XCTAssertEqual(poly6.f, entity) - XCTAssertNil(poly6.a) - XCTAssertNil(poly6.b) - XCTAssertNil(poly6.c) - XCTAssertNil(poly6.d) - XCTAssertNil(poly6.e) - XCTAssertNil(poly6.g) - - let poly7 = Poly7(entity) - XCTAssertEqual(poly7.g, entity) - XCTAssertNil(poly7.a) - XCTAssertNil(poly7.b) - XCTAssertNil(poly7.c) - XCTAssertNil(poly7.d) - XCTAssertNil(poly7.e) - XCTAssertNil(poly7.f) - } - - func test_init_Poly8() { - let entity = TestEntity5(attributes: .none, relationships: .none, meta: .none, links: .none) - let poly = Poly8(entity) - XCTAssertEqual(poly.a, entity) - XCTAssertNil(poly.b) - XCTAssertNil(poly.c) - XCTAssertNil(poly.d) - XCTAssertNil(poly.e) - XCTAssertNil(poly.f) - XCTAssertNil(poly.g) - XCTAssertNil(poly.h) - - let poly2 = Poly8(entity) - XCTAssertEqual(poly2.b, entity) - XCTAssertNil(poly2.a) - XCTAssertNil(poly2.c) - XCTAssertNil(poly2.d) - XCTAssertNil(poly2.e) - XCTAssertNil(poly2.f) - XCTAssertNil(poly2.g) - XCTAssertNil(poly2.h) - - let poly3 = Poly8(entity) - XCTAssertEqual(poly3.c, entity) - XCTAssertNil(poly3.a) - XCTAssertNil(poly3.b) - XCTAssertNil(poly3.d) - XCTAssertNil(poly3.e) - XCTAssertNil(poly3.f) - XCTAssertNil(poly3.g) - XCTAssertNil(poly3.h) - - let poly4 = Poly8(entity) - XCTAssertEqual(poly4.d, entity) - XCTAssertNil(poly4.a) - XCTAssertNil(poly4.b) - XCTAssertNil(poly4.c) - XCTAssertNil(poly4.e) - XCTAssertNil(poly4.f) - XCTAssertNil(poly4.g) - XCTAssertNil(poly4.h) - - let poly5 = Poly8(entity) - XCTAssertEqual(poly5.e, entity) - XCTAssertNil(poly5.a) - XCTAssertNil(poly5.b) - XCTAssertNil(poly5.c) - XCTAssertNil(poly5.d) - XCTAssertNil(poly5.f) - XCTAssertNil(poly5.g) - XCTAssertNil(poly5.h) - - let poly6 = Poly8(entity) - XCTAssertEqual(poly6.f, entity) - XCTAssertNil(poly6.a) - XCTAssertNil(poly6.b) - XCTAssertNil(poly6.c) - XCTAssertNil(poly6.d) - XCTAssertNil(poly6.e) - XCTAssertNil(poly6.g) - XCTAssertNil(poly6.h) - - let poly7 = Poly8(entity) - XCTAssertEqual(poly7.g, entity) - XCTAssertNil(poly7.a) - XCTAssertNil(poly7.b) - XCTAssertNil(poly7.c) - XCTAssertNil(poly7.d) - XCTAssertNil(poly7.e) - XCTAssertNil(poly7.f) - XCTAssertNil(poly7.h) - - let poly8 = Poly8(entity) - XCTAssertEqual(poly8.h, entity) - XCTAssertNil(poly8.a) - XCTAssertNil(poly8.b) - XCTAssertNil(poly8.c) - XCTAssertNil(poly8.d) - XCTAssertNil(poly8.e) - XCTAssertNil(poly8.f) - XCTAssertNil(poly8.g) - } - - func test_init_Poly9() { - let entity = TestEntity5(attributes: .none, relationships: .none, meta: .none, links: .none) - let poly = Poly9(entity) - XCTAssertEqual(poly.a, entity) - XCTAssertNil(poly.b) - XCTAssertNil(poly.c) - XCTAssertNil(poly.d) - XCTAssertNil(poly.e) - XCTAssertNil(poly.f) - XCTAssertNil(poly.g) - XCTAssertNil(poly.h) - XCTAssertNil(poly.i) - - let poly2 = Poly9(entity) - XCTAssertEqual(poly2.b, entity) - XCTAssertNil(poly2.a) - XCTAssertNil(poly2.c) - XCTAssertNil(poly2.d) - XCTAssertNil(poly2.e) - XCTAssertNil(poly2.f) - XCTAssertNil(poly2.g) - XCTAssertNil(poly2.h) - XCTAssertNil(poly2.i) - - let poly3 = Poly9(entity) - XCTAssertEqual(poly3.c, entity) - XCTAssertNil(poly3.a) - XCTAssertNil(poly3.b) - XCTAssertNil(poly3.d) - XCTAssertNil(poly3.e) - XCTAssertNil(poly3.f) - XCTAssertNil(poly3.g) - XCTAssertNil(poly3.h) - XCTAssertNil(poly3.i) - - let poly4 = Poly9(entity) - XCTAssertEqual(poly4.d, entity) - XCTAssertNil(poly4.a) - XCTAssertNil(poly4.b) - XCTAssertNil(poly4.c) - XCTAssertNil(poly4.e) - XCTAssertNil(poly4.f) - XCTAssertNil(poly4.g) - XCTAssertNil(poly4.h) - XCTAssertNil(poly4.i) - - let poly5 = Poly9(entity) - XCTAssertEqual(poly5.e, entity) - XCTAssertNil(poly5.a) - XCTAssertNil(poly5.b) - XCTAssertNil(poly5.c) - XCTAssertNil(poly5.d) - XCTAssertNil(poly5.f) - XCTAssertNil(poly5.g) - XCTAssertNil(poly5.h) - XCTAssertNil(poly5.i) - - let poly6 = Poly9(entity) - XCTAssertEqual(poly6.f, entity) - XCTAssertNil(poly6.a) - XCTAssertNil(poly6.b) - XCTAssertNil(poly6.c) - XCTAssertNil(poly6.d) - XCTAssertNil(poly6.e) - XCTAssertNil(poly6.g) - XCTAssertNil(poly6.h) - XCTAssertNil(poly6.i) - - let poly7 = Poly9(entity) - XCTAssertEqual(poly7.g, entity) - XCTAssertNil(poly7.a) - XCTAssertNil(poly7.b) - XCTAssertNil(poly7.c) - XCTAssertNil(poly7.d) - XCTAssertNil(poly7.e) - XCTAssertNil(poly7.f) - XCTAssertNil(poly7.h) - XCTAssertNil(poly7.i) - - let poly8 = Poly9(entity) - XCTAssertEqual(poly8.h, entity) - XCTAssertNil(poly8.a) - XCTAssertNil(poly8.b) - XCTAssertNil(poly8.c) - XCTAssertNil(poly8.d) - XCTAssertNil(poly8.e) - XCTAssertNil(poly8.f) - XCTAssertNil(poly8.g) - XCTAssertNil(poly8.i) - - let poly9 = Poly9(entity) - XCTAssertEqual(poly9.i, entity) - XCTAssertNil(poly9.a) - XCTAssertNil(poly9.b) - XCTAssertNil(poly9.c) - XCTAssertNil(poly9.d) - XCTAssertNil(poly9.e) - XCTAssertNil(poly9.f) - XCTAssertNil(poly9.g) - XCTAssertNil(poly9.h) - } -} - -// MARK: - subscript lookup -extension PolyTests { - func test_Poly1_lookup() { - let entity = decoded(type: TestEntity.self, data: poly_entity1) - let poly = decoded(type: Poly1.self, data: poly_entity1) - - XCTAssertEqual(entity, poly[TestEntity.self]) - } - - func test_Poly2_lookup() { - let entity = decoded(type: TestEntity2.self, data: poly_entity2) - let poly = decoded(type: Poly2.self, data: poly_entity2) - - XCTAssertNil(poly[TestEntity.self]) - XCTAssertEqual(entity, poly[TestEntity2.self]) - } - - func test_Poly3_lookup() { - let entity = decoded(type: TestEntity3.self, data: poly_entity3) - let poly = decoded(type: Poly3.self, data: poly_entity3) - - XCTAssertNil(poly[TestEntity.self]) - XCTAssertNil(poly[TestEntity2.self]) - XCTAssertEqual(entity, poly[TestEntity3.self]) - } - - func test_Poly4_lookup() { - let entity = decoded(type: TestEntity4.self, data: poly_entity4) - let poly = decoded(type: Poly4.self, data: poly_entity4) - - XCTAssertNil(poly[TestEntity.self]) - XCTAssertNil(poly[TestEntity2.self]) - XCTAssertNil(poly[TestEntity3.self]) - XCTAssertEqual(entity, poly[TestEntity4.self]) - } - - func test_Poly5_lookup() { - let entity = decoded(type: TestEntity5.self, data: poly_entity5) - let poly = decoded(type: Poly5.self, data: poly_entity5) - - XCTAssertNil(poly[TestEntity.self]) - XCTAssertNil(poly[TestEntity2.self]) - XCTAssertNil(poly[TestEntity3.self]) - XCTAssertNil(poly[TestEntity4.self]) - XCTAssertEqual(entity, poly[TestEntity5.self]) - } - - func test_Poly6_lookup() { - let entity = decoded(type: TestEntity6.self, data: poly_entity6) - let poly = decoded(type: Poly6.self, data: poly_entity6) - - XCTAssertNil(poly[TestEntity.self]) - XCTAssertNil(poly[TestEntity2.self]) - XCTAssertNil(poly[TestEntity3.self]) - XCTAssertNil(poly[TestEntity4.self]) - XCTAssertNil(poly[TestEntity5.self]) - XCTAssertEqual(entity, poly[TestEntity6.self]) - } - - func test_Poly7_lookup() { - let entity = decoded(type: TestEntity7.self, data: poly_entity7) - let poly = decoded(type: Poly7.self, data: poly_entity7) - - XCTAssertNil(poly[TestEntity.self]) - XCTAssertNil(poly[TestEntity2.self]) - XCTAssertNil(poly[TestEntity3.self]) - XCTAssertNil(poly[TestEntity4.self]) - XCTAssertNil(poly[TestEntity5.self]) - XCTAssertNil(poly[TestEntity6.self]) - XCTAssertEqual(entity, poly[TestEntity7.self]) - } - - func test_Poly8_lookup() { - let entity = decoded(type: TestEntity8.self, data: poly_entity8) - let poly = decoded(type: Poly8.self, data: poly_entity8) - - XCTAssertNil(poly[TestEntity.self]) - XCTAssertNil(poly[TestEntity2.self]) - XCTAssertNil(poly[TestEntity3.self]) - XCTAssertNil(poly[TestEntity4.self]) - XCTAssertNil(poly[TestEntity5.self]) - XCTAssertNil(poly[TestEntity6.self]) - XCTAssertNil(poly[TestEntity7.self]) - XCTAssertEqual(entity, poly[TestEntity8.self]) - } - - func test_Poly9_lookup() { - let entity = decoded(type: TestEntity9.self, data: poly_entity9) - let poly = decoded(type: Poly9.self, data: poly_entity9) - - XCTAssertNil(poly[TestEntity.self]) - XCTAssertNil(poly[TestEntity2.self]) - XCTAssertNil(poly[TestEntity3.self]) - XCTAssertNil(poly[TestEntity4.self]) - XCTAssertNil(poly[TestEntity5.self]) - XCTAssertNil(poly[TestEntity6.self]) - XCTAssertNil(poly[TestEntity7.self]) - XCTAssertNil(poly[TestEntity8.self]) - XCTAssertEqual(entity, poly[TestEntity9.self]) - } -} - -// MARK: - failures -extension PolyTests { - func test_Poly0_encode_throws() { - XCTAssertThrowsError(try JSONEncoder().encode(Poly0())) - } - - func test_Poly0_decode_throws() { - XCTAssertThrowsError(try JSONDecoder().decode(Poly0.self, from: poly_entity1)) - } - - func test_Poly1_decode_throws_typeNotFound() { - XCTAssertThrowsError(try JSONDecoder().decode(Poly1.self, from: poly_entity2)) - } - - func test_Poly2_decode_throws_typeNotFound() { - XCTAssertThrowsError(try JSONDecoder().decode(Poly2.self, from: poly_entity3)) - } - - func test_Poly3_decode_throws_typeNotFound() { - XCTAssertThrowsError(try JSONDecoder().decode(Poly3.self, from: poly_entity4)) - } - - func test_Poly4_decode_throws_typeNotFound() { - XCTAssertThrowsError(try JSONDecoder().decode(Poly4.self, from: poly_entity5)) - } - - func test_Poly5_decode_throws_typeNotFound() { - XCTAssertThrowsError(try JSONDecoder().decode(Poly5.self, from: poly_entity6)) - } - - func test_Poly6_decode_throws_typeNotFound() { - XCTAssertThrowsError(try JSONDecoder().decode(Poly6.self, from: poly_entity7)) - } - - func test_Poly7_decode_throws_typeNotFound() { - XCTAssertThrowsError(try JSONDecoder().decode(Poly7.self, from: poly_entity8)) - } - - func test_Poly8_decode_throws_typeNotFound() { - XCTAssertThrowsError(try JSONDecoder().decode(Poly8.self, from: poly_entity9)) - } - - func test_Poly9_decode_throws_typeNotFound() { - XCTAssertThrowsError(try JSONDecoder().decode(Poly9.self, from: poly_entity10)) - } -} - -// MARK: - Test types -extension PolyTests { - enum TestEntityType: ResourceObjectDescription { - - typealias Relationships = NoRelationships - - public static var jsonType: String { return "test_entity1" } - - public struct Attributes: JSONAPI.Attributes { - let foo: Attribute - let bar: Attribute - } - } - - typealias TestEntity = BasicEntity - - enum TestEntityType2: ResourceObjectDescription { - - public static var jsonType: String { return "test_entity2" } - - public struct Relationships: JSONAPI.Relationships { - let entity1: ToOneRelationship - } - - public struct Attributes: JSONAPI.Attributes { - let foo: Attribute - let bar: Attribute - } - } - - typealias TestEntity2 = BasicEntity - - enum TestEntityType3: ResourceObjectDescription { - - typealias Attributes = NoAttributes - - public static var jsonType: String { return "test_entity3" } - - public struct Relationships: JSONAPI.Relationships { - let entity1: ToOneRelationship - let entity2: ToManyRelationship - } - } - - typealias TestEntity3 = BasicEntity - - enum TestEntityType4: ResourceObjectDescription { - - typealias Attributes = NoAttributes - - typealias Relationships = NoRelationships - - public static var jsonType: String { return "test_entity4" } - } - - typealias TestEntity4 = BasicEntity - - enum TestEntityType5: ResourceObjectDescription { - - typealias Attributes = NoAttributes - - typealias Relationships = NoRelationships - - public static var jsonType: String { return "test_entity5" } - } - - typealias TestEntity5 = BasicEntity - - enum TestEntityType6: ResourceObjectDescription { - - typealias Attributes = NoAttributes - - public static var jsonType: String { return "test_entity6" } - - struct Relationships: JSONAPI.Relationships { - let entity4: ToOneRelationship - } - } - - typealias TestEntity6 = BasicEntity - - enum TestEntityType7: ResourceObjectDescription { - - typealias Attributes = NoAttributes - - public static var jsonType: String { return "test_entity7" } - - typealias Relationships = NoRelationships - } - - typealias TestEntity7 = BasicEntity - - enum TestEntityType8: ResourceObjectDescription { - - typealias Attributes = NoAttributes - - public static var jsonType: String { return "test_entity8" } - - typealias Relationships = NoRelationships - } - - typealias TestEntity8 = BasicEntity - - enum TestEntityType9: ResourceObjectDescription { - - typealias Attributes = NoAttributes - - public static var jsonType: String { return "test_entity9" } - - typealias Relationships = NoRelationships - } - - typealias TestEntity9 = BasicEntity -} From a24f15dc4e53cc2aeb876886ac8856dab47a767c Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Mon, 16 Sep 2019 09:38:36 -0700 Subject: [PATCH 064/235] regenerate linuxmain --- Tests/JSONAPITests/XCTestManifests.swift | 41 ++---------------------- 1 file changed, 2 insertions(+), 39 deletions(-) diff --git a/Tests/JSONAPITests/XCTestManifests.swift b/Tests/JSONAPITests/XCTestManifests.swift index 43dad79..4cde925 100644 --- a/Tests/JSONAPITests/XCTestManifests.swift +++ b/Tests/JSONAPITests/XCTestManifests.swift @@ -218,6 +218,8 @@ extension IncludedTests { ("test_SevenDifferentIncludes_encode", test_SevenDifferentIncludes_encode), ("test_SixDifferentIncludes", test_SixDifferentIncludes), ("test_SixDifferentIncludes_encode", test_SixDifferentIncludes_encode), + ("test_TenDifferentIncludes", test_TenDifferentIncludes), + ("test_TenDifferentIncludes_encode", test_TenDifferentIncludes_encode), ("test_ThreeDifferentIncludes", test_ThreeDifferentIncludes), ("test_ThreeDifferentIncludes_encode", test_ThreeDifferentIncludes_encode), ("test_TwoDifferentIncludes", test_TwoDifferentIncludes), @@ -273,44 +275,6 @@ extension PolyProxyTests { ] } -extension PolyTests { - // DO NOT MODIFY: This is autogenerated, use: - // `swift test --generate-linuxmain` - // to regenerate. - static let __allTests__PolyTests = [ - ("test_init_Poly0", test_init_Poly0), - ("test_init_Poly1", test_init_Poly1), - ("test_init_Poly2", test_init_Poly2), - ("test_init_Poly3", test_init_Poly3), - ("test_init_Poly4", test_init_Poly4), - ("test_init_Poly5", test_init_Poly5), - ("test_init_Poly6", test_init_Poly6), - ("test_init_Poly7", test_init_Poly7), - ("test_init_Poly8", test_init_Poly8), - ("test_init_Poly9", test_init_Poly9), - ("test_Poly0_decode_throws", test_Poly0_decode_throws), - ("test_Poly0_encode_throws", test_Poly0_encode_throws), - ("test_Poly1_decode_throws_typeNotFound", test_Poly1_decode_throws_typeNotFound), - ("test_Poly1_lookup", test_Poly1_lookup), - ("test_Poly2_decode_throws_typeNotFound", test_Poly2_decode_throws_typeNotFound), - ("test_Poly2_lookup", test_Poly2_lookup), - ("test_Poly3_decode_throws_typeNotFound", test_Poly3_decode_throws_typeNotFound), - ("test_Poly3_lookup", test_Poly3_lookup), - ("test_Poly4_decode_throws_typeNotFound", test_Poly4_decode_throws_typeNotFound), - ("test_Poly4_lookup", test_Poly4_lookup), - ("test_Poly5_decode_throws_typeNotFound", test_Poly5_decode_throws_typeNotFound), - ("test_Poly5_lookup", test_Poly5_lookup), - ("test_Poly6_decode_throws_typeNotFound", test_Poly6_decode_throws_typeNotFound), - ("test_Poly6_lookup", test_Poly6_lookup), - ("test_Poly7_decode_throws_typeNotFound", test_Poly7_decode_throws_typeNotFound), - ("test_Poly7_lookup", test_Poly7_lookup), - ("test_Poly8_decode_throws_typeNotFound", test_Poly8_decode_throws_typeNotFound), - ("test_Poly8_lookup", test_Poly8_lookup), - ("test_Poly9_decode_throws_typeNotFound", test_Poly9_decode_throws_typeNotFound), - ("test_Poly9_lookup", test_Poly9_lookup), - ] -} - extension RelationshipTests { // DO NOT MODIFY: This is autogenerated, use: // `swift test --generate-linuxmain` @@ -486,7 +450,6 @@ public func __allTests() -> [XCTestCaseEntry] { testCase(LinksTests.__allTests__LinksTests), testCase(NonJSONAPIRelatableTests.__allTests__NonJSONAPIRelatableTests), testCase(PolyProxyTests.__allTests__PolyProxyTests), - testCase(PolyTests.__allTests__PolyTests), testCase(RelationshipTests.__allTests__RelationshipTests), testCase(ResourceBodyTests.__allTests__ResourceBodyTests), testCase(ResourceObjectTests.__allTests__ResourceObjectTests), From cf6fa39548c0c75dfa2818520fcd1ce8841e0aff Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Mon, 16 Sep 2019 09:58:57 -0700 Subject: [PATCH 065/235] bump Podspec version --- JSONAPI.podspec | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/JSONAPI.podspec b/JSONAPI.podspec index caad3a6..852317d 100644 --- a/JSONAPI.podspec +++ b/JSONAPI.podspec @@ -16,7 +16,7 @@ Pod::Spec.new do |spec| # spec.name = "MP-JSONAPI" - spec.version = "2.0.0" + spec.version = "2.1.0" spec.summary = "Swift Codable JSON API framework." # 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.xcconfig = { "HEADER_SEARCH_PATHS" => "$(SDKROOT)/usr/include/libxml2" } - spec.dependency "Poly", "~> 2.0" + spec.dependency "Poly", "~> 2.1" end From 6cd5aeaba6958a3b202c21493511509a66221964 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Mon, 16 Sep 2019 17:16:42 -0700 Subject: [PATCH 066/235] Update README.md Update max number of `Include` types in documentation. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6c0a231..3d0eb2b 100644 --- a/README.md +++ b/README.md @@ -452,7 +452,7 @@ The third generic type of a `JSONAPIDocument` is a `Links` struct. `Links` are d #### `IncludeType` -The fourth generic type of a `JSONAPIDocument` is an `Include`. This type controls which types of `ResourceObject` are looked for when decoding the "included" part of the JSON API document. If you do not expect any included resource objects to be in the document, `NoIncludes` is the way to go. The `JSONAPI` framework provides `Include`s for up to six types of included resource objects. These are named `Include1`, `Include2`, `Include3`, and so on. +The fourth generic type of a `JSONAPIDocument` is an `Include`. This type controls which types of `ResourceObject` are looked for when decoding the "included" part of the JSON API document. If you do not expect any included resource objects to be in the document, `NoIncludes` is the way to go. The `JSONAPI` framework provides `Include`s for up to 10 types of included resource objects. These are named `Include1`, `Include2`, `Include3`, and so on. **IMPORTANT**: The number trailing "Include" in these type names does not indicate a number of included resource objects, it indicates a number of _types_ of included resource objects. `Include1` can be used to decode any number of included resource objects as long as all the resource objects are of the same _type_. From 88c5d400aa8a9e3c5249bbc1383297d374e812aa Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Sun, 29 Sep 2019 14:56:04 -0700 Subject: [PATCH 067/235] Add generic and basic error types. add tests for generic type. --- Sources/JSONAPI/Error/BasicJSONAPIError.swift | 73 +++++++++ .../JSONAPI/Error/GenericJSONAPIError.swift | 67 +++++++++ .../Error.swift => Error/JSONAPIError.swift} | 7 +- .../Error/GenericJSONAPIErrorTests.swift | 139 ++++++++++++++++++ 4 files changed, 284 insertions(+), 2 deletions(-) create mode 100644 Sources/JSONAPI/Error/BasicJSONAPIError.swift create mode 100644 Sources/JSONAPI/Error/GenericJSONAPIError.swift rename Sources/JSONAPI/{Document/Error.swift => Error/JSONAPIError.swift} (69%) create mode 100644 Tests/JSONAPITests/Error/GenericJSONAPIErrorTests.swift diff --git a/Sources/JSONAPI/Error/BasicJSONAPIError.swift b/Sources/JSONAPI/Error/BasicJSONAPIError.swift new file mode 100644 index 0000000..345af26 --- /dev/null +++ b/Sources/JSONAPI/Error/BasicJSONAPIError.swift @@ -0,0 +1,73 @@ +// +// BasicError.swift +// JSONAPI +// +// Created by Mathew Polzin on 9/29/19. +// + +import Foundation + +/// Most of the JSON:API Spec defined Error fields. +public struct BasicJSONAPIErrorPayload: Codable, Equatable, ErrorDictType { + /// a unique identifier for this particular occurrence of the problem + let id: IdType? +// 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 + let status: String? + /// an application-specific error code + let code: String? + /// a short, human-readable summary of the problem that SHOULD NOT change from occurrence to occurrence of the problem, except for purposes of localization + let title: String? + /// a human-readable explanation specific to this occurrence of the problem. Like `title`, this field’s value can be localized + let detail: String? + /// an object containing references to the source of the error + let source: Source? +// let meta: Meta? // we skip this for now to avoid adding complexity to using this basic type + + public struct Source: Codable, Equatable { + /// a JSON Pointer [RFC6901] to the associated entity in the request document [e.g. "/data" for a primary data object, or "/data/attributes/title" for a specific attribute]. + let pointer: String? + /// which URI query parameter caused the error + let parameter: String? + } + + public var definedFields: [String: String] { + let keysAndValues = [ + id.map { ("id", String(describing: $0)) }, + status.map { ("status", $0) }, + code.map { ("code", $0) }, + title.map { ("title", $0) }, + detail.map { ("detail", $0) }, + source.flatMap { $0.pointer.map { ("pointer", $0) } }, + source.flatMap { $0.parameter.map { ("parameter", $0) } } + ].compactMap { $0 } + return Dictionary(uniqueKeysWithValues: keysAndValues) + } +} + +/// `BasicJSONAPIError` optionally decodes many possible fields +/// specified by the JSON:API 1.0 Spec. It gives no type-guarantees of what +/// will be non-nil, but could provide good diagnostic information when +/// you do not know what error structure to expect. +/// +/// ``` +/// Fields: +/// - id +/// - status +/// - code +/// - title +/// - detail +/// - source +/// - pointer +/// - parameter +/// ``` +/// +/// The JSON:API Spec does not dictate the type of this particular Id field, +/// so you must specify whether to expect, for example, an `Int` or a `String` +/// in the id field. +/// +/// Something like `AnyCodable` from *Flight-School* could be +/// a good option if you do not know what to expect. You could also use +/// `Either` (provided by the `Poly` package that is +/// already a dependency of `JSONAPI`). +public typealias BasicJSONAPIError = GenericJSONAPIError> diff --git a/Sources/JSONAPI/Error/GenericJSONAPIError.swift b/Sources/JSONAPI/Error/GenericJSONAPIError.swift new file mode 100644 index 0000000..e151311 --- /dev/null +++ b/Sources/JSONAPI/Error/GenericJSONAPIError.swift @@ -0,0 +1,67 @@ +// +// GenericError.swift +// JSONAPI +// +// Created by Mathew Polzin on 9/29/19. +// + +import Foundation + +/// `GenericJSONAPIError` can be used to specify whatever error +/// payload you expect to need to parse in responses and handle any +/// other payload structure as `.unknownError`. +public enum GenericJSONAPIError: JSONAPIError { + case unknownError + case error(ErrorPayload) + + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + + do { + self = .error(try container.decode(ErrorPayload.self)) + } catch { + self = .unknown + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + switch self { + case .error(let payload): + try container.encode(payload) + case .unknownError: + try container.encode("unknown") + } + } + + public static var unknown: Self { + return .unknownError + } +} + +public extension GenericJSONAPIError { + var payload: ErrorPayload? { + switch self { + case .unknownError: + return nil + case .error(let payload): + return payload + } + } +} + +public protocol ErrorDictType { + var definedFields: [String: String] { get } +} + +extension GenericJSONAPIError: ErrorDictType where ErrorPayload: ErrorDictType { + /// Get a dictionary of all defined fields and their values. + public var definedFields: [String: String] { + switch self { + case .unknownError: + return [:] + case .error(let basicPayload): + return basicPayload.definedFields + } + } +} diff --git a/Sources/JSONAPI/Document/Error.swift b/Sources/JSONAPI/Error/JSONAPIError.swift similarity index 69% rename from Sources/JSONAPI/Document/Error.swift rename to Sources/JSONAPI/Error/JSONAPIError.swift index ab6e3ca..f1dbfe9 100644 --- a/Sources/JSONAPI/Document/Error.swift +++ b/Sources/JSONAPI/Error/JSONAPIError.swift @@ -11,7 +11,10 @@ public protocol JSONAPIError: Swift.Error, Equatable, Codable { /// `UnknownJSONAPIError` can actually be used in any sitaution /// where you don't know what errors are possible _or_ you just don't -/// care what errors might show up. +/// care what errors might show up. If you don't know how the error +/// will be structured but you would like to have access to more +/// information the server might be providing in the error payload, +/// use `BasicJSONAPIError` instead. public enum UnknownJSONAPIError: JSONAPIError { case unknownError @@ -24,7 +27,7 @@ public enum UnknownJSONAPIError: JSONAPIError { try container.encode("unknown") } - public static var unknown: UnknownJSONAPIError { + public static var unknown: Self { return .unknownError } } diff --git a/Tests/JSONAPITests/Error/GenericJSONAPIErrorTests.swift b/Tests/JSONAPITests/Error/GenericJSONAPIErrorTests.swift new file mode 100644 index 0000000..e1fca6e --- /dev/null +++ b/Tests/JSONAPITests/Error/GenericJSONAPIErrorTests.swift @@ -0,0 +1,139 @@ +// +// GenericJSONAPIErrorTests.swift +// JSONAPITests +// +// Created by Mathew Polzin on 9/29/19. +// + +import Foundation +import JSONAPI +import XCTest + +final class GenericJSONAPIErrorTests: XCTestCase { + func test_initAndEquality() { + let unknown1 = TestGenericJSONAPIError.unknown + let unknown2 = TestGenericJSONAPIError.unknownError + XCTAssertEqual(unknown1, unknown2) + + let known1 = TestGenericJSONAPIError.error(.init(hello: "there", world: 3)) + let known2 = TestGenericJSONAPIError.error(.init(hello: "there", world: nil)) + XCTAssertNotEqual(unknown1, known1) + XCTAssertNotEqual(unknown1, known2) + XCTAssertNotEqual(known1, known2) + } + + func test_decodeKnown() { + let datas = [ +""" +{ + "hello": "world" +} +""", +""" +{ + "hello": "there", + "world": 2 +} +""", +""" +{ + "hello": "three", + "world": null +} +""" + ].map { $0.data(using: .utf8)! } + + let errors = datas + .map { decoded(type: TestGenericJSONAPIError.self, data: $0) } + + XCTAssertEqual(errors[0], .error(TestPayload(hello: "world", world: nil))) + + XCTAssertEqual(errors[1], .error(TestPayload(hello: "there", world: 2))) + + XCTAssertEqual(errors[2], .error(TestPayload(hello: "three", world: nil))) + } + + func test_decodeUnknown() { + let data = +""" +{ + "world": 2 +} +""".data(using: .utf8)! + + let error = decoded(type: TestGenericJSONAPIError.self, data: data) + + XCTAssertEqual(error, .unknown) + } + + func test_encode() { + let datas = [ +""" +{ + "hello": "world" +} +""", +""" +{ + "hello": "there", + "world": 2 +} +""", +""" +{ + "hello": "three", + "world": null +} +""" + ].map { $0.data(using: .utf8)! } + + datas.forEach { data in + test_DecodeEncodeEquality(type: TestGenericJSONAPIError.self, data: data) + } + } + + func test_payloadAccess() { + let error1 = TestGenericJSONAPIError.error(.init(hello: "world", world: 3)) + let error2 = TestGenericJSONAPIError.error(.init(hello: "there", world: nil)) + let error3 = TestGenericJSONAPIError.unknown + + XCTAssertEqual(error1.payload?.hello, "world") + XCTAssertEqual(error1.payload?.world, 3) + XCTAssertEqual(error2.payload?.hello, "there") + XCTAssertNil(error2.payload?.world) + XCTAssertNil(error3.payload?.hello) + XCTAssertNil(error3.payload?.world) + } + + func test_definedFields() { + let error1 = TestGenericJSONAPIError.error(.init(hello: "world", world: 3)) + let error2 = TestGenericJSONAPIError.error(.init(hello: "there", world: nil)) + let error3 = TestGenericJSONAPIError.unknown + + XCTAssertEqual(error1.definedFields.count, 2) + XCTAssertEqual(error2.definedFields.count, 1) + XCTAssertEqual(error3.definedFields.count, 0) + + XCTAssertEqual(error1.definedFields["hello"], "world") + XCTAssertEqual(error1.definedFields["world"], "3") + XCTAssertEqual(error2.definedFields["hello"], "there") + XCTAssertNil(error2.definedFields["world"]) + XCTAssertNil(error3.definedFields["hello"]) + XCTAssertNil(error3.definedFields["world"]) + } +} + +private struct TestPayload: Codable, Equatable, ErrorDictType { + let hello: String + let world: Int? + + public var definedFields: [String : String] { + let keysAndValues = [ + ("hello", hello), + world.map { ("world", String($0)) } + ].compactMap { $0 } + return Dictionary(uniqueKeysWithValues: keysAndValues) + } +} + +private typealias TestGenericJSONAPIError = GenericJSONAPIError From b0801f7cee7aa2127501ebb4ba1070b1200b2766 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Sun, 29 Sep 2019 15:20:08 -0700 Subject: [PATCH 068/235] Add tests for BasicJSONAPIError and tweak documentation --- Sources/JSONAPI/Error/BasicJSONAPIError.swift | 5 + .../Error/BasicJSONAPIErrorTests.swift | 92 +++++++++++++++++++ .../Error/GenericJSONAPIErrorTests.swift | 8 ++ 3 files changed, 105 insertions(+) create mode 100644 Tests/JSONAPITests/Error/BasicJSONAPIErrorTests.swift diff --git a/Sources/JSONAPI/Error/BasicJSONAPIError.swift b/Sources/JSONAPI/Error/BasicJSONAPIError.swift index 345af26..fa0158d 100644 --- a/Sources/JSONAPI/Error/BasicJSONAPIError.swift +++ b/Sources/JSONAPI/Error/BasicJSONAPIError.swift @@ -70,4 +70,9 @@ public struct BasicJSONAPIErrorPayload: Codable, Eq /// a good option if you do not know what to expect. You could also use /// `Either` (provided by the `Poly` package that is /// already a dependency of `JSONAPI`). +/// +/// - Important: The `definedFields` property will include fields +/// with non-nil values in a flattened way. There will be no `source` key +/// but there will be `pointer` and `parameter` keys (if those values +/// are non-nil). public typealias BasicJSONAPIError = GenericJSONAPIError> diff --git a/Tests/JSONAPITests/Error/BasicJSONAPIErrorTests.swift b/Tests/JSONAPITests/Error/BasicJSONAPIErrorTests.swift new file mode 100644 index 0000000..e726143 --- /dev/null +++ b/Tests/JSONAPITests/Error/BasicJSONAPIErrorTests.swift @@ -0,0 +1,92 @@ +// +// BasicJSONAPIErrorTests.swift +// JSONAPITests +// +// Created by Mathew Polzin on 9/29/19. +// + +import Foundation +@testable import JSONAPI +import XCTest + +final class BasicJSONAPIErrorTests: XCTestCase { + func test_initAndEquality() { + let unknown1 = BasicJSONAPIError.unknown + let unknown2 = BasicJSONAPIError.unknownError + XCTAssertEqual(unknown1, unknown2) + let unknown3 = BasicJSONAPIError.unknownError + XCTAssertEqual(unknown3, .unknown) + + let _ = BasicJSONAPIError.error(.init(id: nil, + status: nil, + code: nil, + title: nil, + detail: nil, + source: nil)) + let _ = BasicJSONAPIError.error(.init(id: nil, + status: nil, + code: nil, + title: nil, + detail: nil, + source: nil)) + + let intError = BasicJSONAPIError.error(.init(id: 2, + status: nil, + code: nil, + title: nil, + detail: nil, + source: nil)) + XCTAssertEqual(intError.payload?.id, 2) + XCTAssertNotEqual(intError, unknown3) + + let stringError = BasicJSONAPIError.error(.init(id: "hello", + status: nil, + code: nil, + title: nil, + detail: nil, + source: nil)) + XCTAssertEqual(stringError.payload?.id, "hello") + XCTAssertNotEqual(stringError, unknown1) + + let wellPopulatedError = BasicJSONAPIError.error(.init(id: 10, + status: "404", + code: "12", + title: "Missing", + detail: "Resource was not found", + source: .init(pointer: "/data/attributes/id", parameter: "id"))) + XCTAssertEqual(wellPopulatedError.payload?.id, 10) + XCTAssertEqual(wellPopulatedError.payload?.status, "404") + XCTAssertEqual(wellPopulatedError.payload?.code, "12") + XCTAssertEqual(wellPopulatedError.payload?.title, "Missing") + XCTAssertEqual(wellPopulatedError.payload?.detail, "Resource was not found") + XCTAssertEqual(wellPopulatedError.payload?.source?.pointer, "/data/attributes/id") + XCTAssertEqual(wellPopulatedError.payload?.source?.parameter, "id") + + XCTAssertNotEqual(wellPopulatedError, intError) + } + + func test_definedFields() { + let unpopulatedError = BasicJSONAPIError.error(.init(id: nil, + status: nil, + code: nil, + title: nil, + detail: nil, + source: nil)) + XCTAssertEqual(unpopulatedError.definedFields.count, 0) + + let wellPopulatedError = BasicJSONAPIError.error(.init(id: 10, + status: "404", + code: "12", + title: "Missing", + detail: "Resource was not found", + source: .init(pointer: "/data/attributes/id", parameter: "id"))) + XCTAssertEqual(wellPopulatedError.definedFields.count, 7) + XCTAssertEqual(wellPopulatedError.definedFields["id"], "10") + XCTAssertEqual(wellPopulatedError.definedFields["status"], "404") + XCTAssertEqual(wellPopulatedError.definedFields["code"], "12") + XCTAssertEqual(wellPopulatedError.definedFields["title"], "Missing") + XCTAssertEqual(wellPopulatedError.definedFields["detail"], "Resource was not found") + XCTAssertEqual(wellPopulatedError.definedFields["pointer"], "/data/attributes/id") + XCTAssertEqual(wellPopulatedError.definedFields["parameter"], "id") + } +} diff --git a/Tests/JSONAPITests/Error/GenericJSONAPIErrorTests.swift b/Tests/JSONAPITests/Error/GenericJSONAPIErrorTests.swift index e1fca6e..21172ed 100644 --- a/Tests/JSONAPITests/Error/GenericJSONAPIErrorTests.swift +++ b/Tests/JSONAPITests/Error/GenericJSONAPIErrorTests.swift @@ -92,6 +92,14 @@ final class GenericJSONAPIErrorTests: XCTestCase { } } + func test_encodeUnknown() { + let error = TestGenericJSONAPIError.unknownError + + let encodedError = encoded(value: ["errors": [error]]) + + XCTAssertEqual(String(data: encodedError, encoding: .utf8)!, #"{"errors":["unknown"]}"#) + } + func test_payloadAccess() { let error1 = TestGenericJSONAPIError.error(.init(hello: "world", world: 3)) let error2 = TestGenericJSONAPIError.error(.init(hello: "there", world: nil)) From d4806ff557dfe9ceff0a6b350d17db5b29ccf225 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Sun, 29 Sep 2019 15:36:16 -0700 Subject: [PATCH 069/235] Add a few decode examples --- .../Error/BasicJSONAPIErrorTests.swift | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/Tests/JSONAPITests/Error/BasicJSONAPIErrorTests.swift b/Tests/JSONAPITests/Error/BasicJSONAPIErrorTests.swift index e726143..05bcf2f 100644 --- a/Tests/JSONAPITests/Error/BasicJSONAPIErrorTests.swift +++ b/Tests/JSONAPITests/Error/BasicJSONAPIErrorTests.swift @@ -8,6 +8,7 @@ import Foundation @testable import JSONAPI import XCTest +import Poly final class BasicJSONAPIErrorTests: XCTestCase { func test_initAndEquality() { @@ -89,4 +90,42 @@ final class BasicJSONAPIErrorTests: XCTestCase { XCTAssertEqual(wellPopulatedError.definedFields["pointer"], "/data/attributes/id") XCTAssertEqual(wellPopulatedError.definedFields["parameter"], "id") } + + func test_decodeAFewExamples() { + let datas = [ +""" +{ + "id": "hello" +} +""", +""" +{ + "id": 1234 +} +""", +""" +{ + "status": "404", + "title": "Missing", + "links": { + "about": "https://google.com" + } +} +""", +""" +{ + "status": 404 +} +""" + ].map { $0.data(using: .utf8)! } + + let errors = datas + .map { decoded(type: BasicJSONAPIError>.self, data: $0) } + + XCTAssertEqual(errors[0].payload?.id, .init("hello")) + XCTAssertEqual(errors[1].payload?.id, .init(1234)) + XCTAssertEqual(errors[2].payload?.status, "404") + XCTAssertEqual(errors[2].payload?.title, "Missing") + XCTAssertEqual(errors[3], .unknown) + } } From 305799234898f9d43636df8eae7c8f96f3c2d5d1 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Sun, 29 Sep 2019 15:57:54 -0700 Subject: [PATCH 070/235] bump podspec version, update Playground examples --- .../Contents.swift | 2 +- .../Contents.swift | 2 +- JSONAPI.playground/Sources/Entities.swift | 4 ++-- JSONAPI.podspec | 2 +- README.md | 8 ++++---- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/JSONAPI.playground/Pages/Full Client & Server Example.xcplaygroundpage/Contents.swift b/JSONAPI.playground/Pages/Full Client & Server Example.xcplaygroundpage/Contents.swift index e68aaa6..eabd561 100644 --- a/JSONAPI.playground/Pages/Full Client & Server Example.xcplaygroundpage/Contents.swift +++ b/JSONAPI.playground/Pages/Full Client & Server Example.xcplaygroundpage/Contents.swift @@ -37,7 +37,7 @@ typealias ToManyRelationship = JSONAPI.ToManyRelationship = JSONAPI.Document +typealias Document = JSONAPI.Document> // MARK: Entity Definitions diff --git a/JSONAPI.playground/Pages/Sparse Fieldsets Example.xcplaygroundpage/Contents.swift b/JSONAPI.playground/Pages/Sparse Fieldsets Example.xcplaygroundpage/Contents.swift index e34667b..5a9dc6c 100644 --- a/JSONAPI.playground/Pages/Sparse Fieldsets Example.xcplaygroundpage/Contents.swift +++ b/JSONAPI.playground/Pages/Sparse Fieldsets Example.xcplaygroundpage/Contents.swift @@ -35,7 +35,7 @@ typealias ThingWithProperties = JSONAPI.ResourceObject = JSONAPI.Document +typealias Document = JSONAPI.Document> // // NOTE: Using `JSONAPI.EncodablePrimaryResource` which means the `ResourceBody` will be `Encodable` but not `Decodable. diff --git a/JSONAPI.playground/Sources/Entities.swift b/JSONAPI.playground/Sources/Entities.swift index d01f4f8..4ae56c9 100644 --- a/JSONAPI.playground/Sources/Entities.swift +++ b/JSONAPI.playground/Sources/Entities.swift @@ -139,6 +139,6 @@ public enum HouseDescription: ResourceObjectDescription { public typealias House = ExampleEntity -public typealias SingleDogDocument = JSONAPI.Document, NoMetadata, NoLinks, NoIncludes, NoAPIDescription, UnknownJSONAPIError> +public typealias SingleDogDocument = JSONAPI.Document, NoMetadata, NoLinks, NoIncludes, NoAPIDescription, BasicJSONAPIError> -public typealias BatchPeopleDocument = JSONAPI.Document, NoMetadata, NoLinks, Include2, NoAPIDescription, UnknownJSONAPIError> +public typealias BatchPeopleDocument = JSONAPI.Document, NoMetadata, NoLinks, Include2, NoAPIDescription, BasicJSONAPIError> diff --git a/JSONAPI.podspec b/JSONAPI.podspec index 852317d..b84fef0 100644 --- a/JSONAPI.podspec +++ b/JSONAPI.podspec @@ -16,7 +16,7 @@ Pod::Spec.new do |spec| # spec.name = "MP-JSONAPI" - spec.version = "2.1.0" + spec.version = "2.2.0" spec.summary = "Swift Codable JSON API framework." # This description is used to generate tags and improve search results. diff --git a/README.md b/README.md index 3d0eb2b..733e9ca 100644 --- a/README.md +++ b/README.md @@ -10,9 +10,9 @@ See the JSON API Spec here: https://jsonapi.org/format/ ## Quick Start ### Clientside -- [Basic Example](https://colab.research.google.com/drive/1IS7lRSBGoiW02Vd1nN_rfdDbZvTDj6Te) -- [Compound Example](https://colab.research.google.com/drive/1BdF0Kc7l2ixDfBZEL16FY6palweDszQU) -- [Metadata Example](https://colab.research.google.com/drive/10dEESwiE9I3YoyfzVeOVwOKUTEgLT3qr) +- [Basic Example](https://colab.research.google.com/drive/1IS7lRSBGoiW02Vd1nN_rfdDbZvTDj6Te) +- [Compound Example](https://colab.research.google.com/drive/1BdF0Kc7l2ixDfBZEL16FY6palweDszQU) +- [Metadata Example](https://colab.research.google.com/drive/10dEESwiE9I3YoyfzVeOVwOKUTEgLT3qr) - [Errors Example](https://colab.research.google.com/drive/1TIv6STzlHrkTf_-9Eu8sv8NoaxhZcFZH) ### Serverside @@ -108,7 +108,7 @@ If you find something wrong with this library and it isn't already mentioned und ### Swift Package Manager Just include the following in your package's dependencies and add `JSONAPI` to the dependencies for any of your targets. ``` - .package(url: "https://github.com/mattpolzin/JSONAPI.git", .upToNextMajor(from: "2.0.0")) + .package(url: "https://github.com/mattpolzin/JSONAPI.git", .upToNextMajor(from: "2.2.0")) ``` ### CocoaPods From f1d6b22f61edc6f09d10e7dda5fa8b7050a17939 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Sun, 29 Sep 2019 16:49:38 -0700 Subject: [PATCH 071/235] Add playground example, add/update documentation, correct visibility of new error payload properties to public. --- .../Usage.xcplaygroundpage/Contents.swift | 30 +++++++++- README.md | 59 +++++++++++++++---- Sources/JSONAPI/Error/BasicJSONAPIError.swift | 42 +++++++++---- .../JSONAPI/Error/GenericJSONAPIError.swift | 2 - .../Error/BasicJSONAPIErrorTests.swift | 2 +- 5 files changed, 108 insertions(+), 27 deletions(-) diff --git a/JSONAPI.playground/Pages/Usage.xcplaygroundpage/Contents.swift b/JSONAPI.playground/Pages/Usage.xcplaygroundpage/Contents.swift index a50f24f..e5434e5 100644 --- a/JSONAPI.playground/Pages/Usage.xcplaygroundpage/Contents.swift +++ b/JSONAPI.playground/Pages/Usage.xcplaygroundpage/Contents.swift @@ -24,7 +24,7 @@ let dogOwner: Person.Identifier? = dogFromData.flatMap { $0 ~> \.owner } // MARK: - Parse a request or response body with one Dog in it using an alternative model -typealias AltSingleDogDocument = JSONAPI.Document, NoMetadata, NoLinks, NoIncludes, NoAPIDescription, UnknownJSONAPIError> +typealias AltSingleDogDocument = JSONAPI.Document, NoMetadata, NoLinks, NoIncludes, NoAPIDescription, BasicJSONAPIError> let altDogResponse = try! JSONDecoder().decode(AltSingleDogDocument.self, from: singleDogData) let altDogFromData = altDogResponse.body.primaryResource?.value let altDogHuman: Person.Identifier? = altDogFromData.flatMap { $0 ~> \.human } @@ -63,7 +63,7 @@ if case let .data(bodyData) = peopleResponse.body { // MARK: - Work in the abstract - +print("-----") func process(document: T) { guard case let .data(body) = document.body else { return @@ -71,3 +71,29 @@ func process(document: T) { let x: T.Body.Data = body } process(document: peopleResponse) + +// MARK: - Work with errors +typealias ErrorDoc = JSONAPI.Document> + +let mockErrorData = +""" +{ + "errors": [ + { + "status": "500", + "title": "Internal Server Error", + "detail": "Server fell over while parsing your request." + } + ] +} +""".data(using: .utf8)! + +let errorResponse = try! JSONDecoder().decode(ErrorDoc.self, from: mockErrorData) + +switch errorResponse.body { +case .data: + print("cool, data!") +case .errors(let errors, let meta, let links): + let errorDetails = errors.compactMap { $0.payload?.detail } + print("error details: \(errorDetails)") +} diff --git a/README.md b/README.md index 733e9ca..91a58f8 100644 --- a/README.md +++ b/README.md @@ -23,10 +23,8 @@ See the JSON API Spec here: https://jsonapi.org/format/ This library works well when used by both the server responsible for serialization and the client responsible for deserialization. Check out the [example](#example) further down in this README. ## Table of Contents - - [JSONAPI](#jsonapi) - - [Table of Contents](#table-of-contents) - [Primary Goals](#primary-goals) - [Caveat](#caveat) - [Dev Environment](#dev-environment) @@ -67,6 +65,9 @@ This library works well when used by both the server responsible for serializati - [`IncludeType`](#includetype) - [`APIDescriptionType`](#apidescriptiontype) - [`Error`](#error) + - [`UnknownJSONAPIError`](#unknownjsonapierror) + - [`BasicJSONAPIError`](#basicjsonapierror) + - [`GenericJSONAPIError`](#genericjsonapierror) - [`JSONAPI.Meta`](#jsonapimeta) - [`JSONAPI.Links`](#jsonapilinks) - [`JSONAPI.RawIdType`](#jsonapirawidtype) @@ -85,8 +86,6 @@ This library works well when used by both the server responsible for serializati - [JSONAPI+Arbitrary](#jsonapiarbitrary) - [JSONAPI+OpenAPI](#jsonapiopenapi) - - ## Primary Goals The primary goals of this framework are: @@ -122,6 +121,8 @@ To use this framework in your project via Cocoapods, add the following dependenc To create an Xcode project for JSONAPI, run `swift package generate-xcodeproj` +With Xcode 11+ you can also just open the folder containing your clone of this repository and begin working. + ### Running the Playground To run the included Playground files, create an Xcode project using Swift Package Manager, then create an Xcode Workspace in the root of the repository and add both the generated Xcode project and the playground to the Workspace. @@ -330,7 +331,7 @@ let favoriteColor: String = person.favoriteColor let favoriteColor: String = person[\.favoriteColor] ``` -In both cases you retain type-safety, although neither plays particularly nicely with code autocompletion. It is best practice to pick an attribute access syntax and stick with it. At some point in the future the syntax deemed less desirable may be deprecated. +In both cases you retain type-safety. It is best practice to pick an attribute access syntax and stick with it. At some point in the future the syntax deemed less desirable may be deprecated. #### `Transformer` @@ -403,7 +404,7 @@ The entirety of a JSON API request or response is encoded or decoded from- or to ```swift let decoder = JSONDecoder() -let responseStructure = JSONAPI.Document, NoMetadata, NoLinks, NoIncludes, UnknownJSONAPIError>.self +let responseStructure = JSONAPI.Document, NoMetadata, NoLinks, NoIncludes, BasicJSONAPIError>.self let document = try decoder.decode(responseStructure, from: data) ``` @@ -470,7 +471,45 @@ You can supply any `JSONAPI.Meta` type as the metadata type of the API descripti #### `Error` -The final generic type of a `JSONAPIDocument` is the `Error`. You should create an error type that can decode all the errors you expect your `JSONAPIDocument` to be able to decode. As prescribed by the **SPEC**, these errors will be found in the root document member `errors`. +The final generic type of a `JSONAPIDocument` is the `Error`. + +You can either create an error type that can handle all the errors you expect your `JSONAPIDocument` to be able to encode/decode or use an out-of-box error type described here. As prescribed by the **SPEC**, these errors will be found under the root document key `errors`. + +##### `UnknownJSONAPIError` +The `UnknownJSONAPIError` type will always succeed in parsing errors but it will not give you any information about what error occurred. You will generally get more bang for your buck out of the next error type described. + +##### `BasicJSONAPIError` +The `BasicJSONAPIError` type will always succeed unless it is faced with an `id` field of an unexpected type, although it still "succeeds" in falling back to its `.unknown` case when that happens. This type extracts _most_ of the fields the **SPEC** describes [here](https://jsonapi.org/format/#error-objects). Because all of these fields are optional in the **SPEC**, they are optional on the `BasicJSONAPIError` type. You will have to create your own error type if you want to define certain fields as non-optional or parse metadata or links out of error objects. + +🗒Metadata and links are supported at the Document level for error responses, the are just not supported hanging off of the individual errors in the `errors` array of the response when using this error type. + +The `BasicJSONAPIError` type is generic on one thing: The type it expects for the `id` field. If you expect integer `ids` back, you use `BasicJSONAPIError`. The same can be done for `String` or any other type that is both `Codable` and `Equatable`. You can even employ something like `AnyCodable` from *Flight-School* as your id field type. If you only need to handle a small subset of possible `id` field types, you can also use the `Poly` library that is already a dependency of `JSONAPI`. For example, you might expect a mix of `String` and `Int` ids for some reason: `BasicJSONAPIError>`. + +The two easiest ways to access the available properties of an error response are under the `payload` property of the error (this property is `nil` if the error was parsed as `.unknown`) or by asking the error for its `definedFields` dictionary. + +As an example, let's say you have the following `Document` type that is destined for errors: +```swift +typealias ErrorDoc = JSONAPI.Document> +``` +And you've parsed an error response +```swift +let errorResponse = try! JSONDecoder().decode(ErrorDoc.self, from: mockErrorData) +``` +You can get at the `Document` body and errors in a couple of different ways, but for one you can switch on the body: +```swift +switch errorResponse.body { +case .data: + print("cool, data!") + +case .errors(let errors, let meta, let links): + let errorDetails = errors.compactMap { $0.payload?.detail } + + print("error details: \(errorDetails)") +} +``` + +##### `GenericJSONAPIError` +This type makes it simple to use your own error payload structures as `JSONAPIError` types. Simply define a `Codable` and `Equatable` struct and then use `GenericJSONAPIError` as the error type for a `Document`. ### `JSONAPI.Meta` @@ -520,12 +559,12 @@ There is a sparse fieldsets example included with this repository as a Playgroun #### Sparse Fieldset `typealias` comparisons You might have found a `typealias` like the following for encoding/decoding `JSONAPI.Document`s (note the primary resource body is a `JSONAPI.ResourceBody`): ```swift -typealias Document = JSONAPI.Document +typealias Document = JSONAPI.Document> ``` In order to support sparse fieldsets (which are encode-only), the following companion `typealias` would be useful (note the primary resource body is a `JSONAPI.EncodableResourceBody`): ```swift -typealias SparseDocument = JSONAPI.Document +typealias SparseDocument = JSONAPI.Document> ``` ### Custom Attribute or Relationship Key Mapping @@ -713,7 +752,7 @@ typealias ToManyRelationship = JSONAPI.ToManyRelationship = JSONAPI.Document +typealias Document = JSONAPI.Document> // MARK: Entity Definitions diff --git a/Sources/JSONAPI/Error/BasicJSONAPIError.swift b/Sources/JSONAPI/Error/BasicJSONAPIError.swift index fa0158d..d3859eb 100644 --- a/Sources/JSONAPI/Error/BasicJSONAPIError.swift +++ b/Sources/JSONAPI/Error/BasicJSONAPIError.swift @@ -5,30 +5,48 @@ // Created by Mathew Polzin on 9/29/19. // -import Foundation - /// Most of the JSON:API Spec defined Error fields. public struct BasicJSONAPIErrorPayload: Codable, Equatable, ErrorDictType { /// a unique identifier for this particular occurrence of the problem - let id: IdType? -// let links: Links? // we skip this for now to avoid adding complexity to using this basic type. + public let id: IdType? +// 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 - let status: String? + public let status: String? /// an application-specific error code - let code: String? + public let code: String? /// a short, human-readable summary of the problem that SHOULD NOT change from occurrence to occurrence of the problem, except for purposes of localization - let title: String? + public let title: String? /// a human-readable explanation specific to this occurrence of the problem. Like `title`, this field’s value can be localized - let detail: String? + public let detail: String? /// an object containing references to the source of the error - let source: Source? -// let meta: Meta? // we skip this for now to avoid adding complexity to using this basic type + public let source: Source? +// public let meta: Meta? // we skip this for now to avoid adding complexity to using this basic type + + public init(id: IdType? = nil, + status: String? = nil, + code: String? = nil, + title: String? = nil, + detail: String? = nil, + source: Source? = nil) { + self.id = id + self.status = status + self.code = code + self.title = title + self.detail = detail + self.source = source + } public struct Source: Codable, Equatable { /// a JSON Pointer [RFC6901] to the associated entity in the request document [e.g. "/data" for a primary data object, or "/data/attributes/title" for a specific attribute]. - let pointer: String? + public let pointer: String? /// which URI query parameter caused the error - let parameter: String? + public let parameter: String? + + public init(pointer: String? = nil, + parameter: String? = nil) { + self.pointer = pointer + self.parameter = parameter + } } public var definedFields: [String: String] { diff --git a/Sources/JSONAPI/Error/GenericJSONAPIError.swift b/Sources/JSONAPI/Error/GenericJSONAPIError.swift index e151311..91ce2b8 100644 --- a/Sources/JSONAPI/Error/GenericJSONAPIError.swift +++ b/Sources/JSONAPI/Error/GenericJSONAPIError.swift @@ -5,8 +5,6 @@ // Created by Mathew Polzin on 9/29/19. // -import Foundation - /// `GenericJSONAPIError` can be used to specify whatever error /// payload you expect to need to parse in responses and handle any /// other payload structure as `.unknownError`. diff --git a/Tests/JSONAPITests/Error/BasicJSONAPIErrorTests.swift b/Tests/JSONAPITests/Error/BasicJSONAPIErrorTests.swift index 05bcf2f..89dc188 100644 --- a/Tests/JSONAPITests/Error/BasicJSONAPIErrorTests.swift +++ b/Tests/JSONAPITests/Error/BasicJSONAPIErrorTests.swift @@ -6,7 +6,7 @@ // import Foundation -@testable import JSONAPI +import JSONAPI import XCTest import Poly From 0e6b2a77711d455ebaa03fd3a5e0485d30841a02 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Sun, 29 Sep 2019 17:14:25 -0700 Subject: [PATCH 072/235] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 91a58f8..933bca5 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ See the JSON API Spec here: https://jsonapi.org/format/ - [Basic Example](https://colab.research.google.com/drive/1IS7lRSBGoiW02Vd1nN_rfdDbZvTDj6Te) - [Compound Example](https://colab.research.google.com/drive/1BdF0Kc7l2ixDfBZEL16FY6palweDszQU) - [Metadata Example](https://colab.research.google.com/drive/10dEESwiE9I3YoyfzVeOVwOKUTEgLT3qr) -- [Errors Example](https://colab.research.google.com/drive/1TIv6STzlHrkTf_-9Eu8sv8NoaxhZcFZH) +- [Custom Errors Example](https://colab.research.google.com/drive/1TIv6STzlHrkTf_-9Eu8sv8NoaxhZcFZH) ### Serverside - [GET Example](https://colab.research.google.com/drive/1krbhzSfz8mwkBTQQnKUZJLEtYsJKSfYX) From abae975f59a6c8e53e64bca9e125b5ca7552fcd2 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Sun, 29 Sep 2019 17:20:42 -0700 Subject: [PATCH 073/235] Update README.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 933bca5..24b2c7e 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,8 @@ See the JSON API Spec here: https://jsonapi.org/format/ ## Quick Start +:warning: The following Google Colab examples have correct code, but there appears to be an bug in the branch of the Swift compiler currently being used by the Google Colab Swift notebooks so the `JSONAPI` package cannot be pulled in and you cannot run the examples in-browser. + ### Clientside - [Basic Example](https://colab.research.google.com/drive/1IS7lRSBGoiW02Vd1nN_rfdDbZvTDj6Te) - [Compound Example](https://colab.research.google.com/drive/1BdF0Kc7l2ixDfBZEL16FY6palweDszQU) From 43dcc4fb12d36b3d3a04c929b4d9357c726f2f6c Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Sun, 29 Sep 2019 17:21:08 -0700 Subject: [PATCH 074/235] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 24b2c7e..6f4efde 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ A Swift package for encoding to- and decoding from **JSON API** compliant reques See the JSON API Spec here: https://jsonapi.org/format/ -:warning: This library provides well-tested type safety when working with JSON:API 1.0. However, the Swift compiler can sometimes have difficulty tracking down small typos when initializing `ResourceObjects`. Once the code is written correctly, it will compile, but tracking down the source of programmer errors can be an annoyance. This is mostly a concern when creating resource objects in-code (servers and test cases must do this). Writing a client that uses this framework to ingest JSON API Compliant API responses is much less painful. :warning: +:warning: This library provides well-tested type safety when working with JSON:API 1.0. However, the Swift compiler can sometimes have difficulty tracking down small typos when initializing `ResourceObjects`. Once the code is written correctly, it will compile, but tracking down the source of programmer errors can be an annoyance. This is mostly a concern when creating resource objects in-code (servers and test cases must do this). Writing a client that uses this framework to ingest JSON API Compliant API responses is much less painful. ## Quick Start From c26d6d99c0c9ad4ab4dba1558a882687636f3590 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Sun, 29 Sep 2019 17:21:43 -0700 Subject: [PATCH 075/235] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6f4efde..1120097 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ See the JSON API Spec here: https://jsonapi.org/format/ ## Quick Start -:warning: The following Google Colab examples have correct code, but there appears to be an bug in the branch of the Swift compiler currently being used by the Google Colab Swift notebooks so the `JSONAPI` package cannot be pulled in and you cannot run the examples in-browser. +:warning: The following Google Colab examples have correct code, but there appears to be an bug in the branch of the Swift compiler currently being used by the Google Colab Swift notebooks such that the `JSONAPI` package cannot be pulled in and you cannot run the examples in-browser. ### Clientside - [Basic Example](https://colab.research.google.com/drive/1IS7lRSBGoiW02Vd1nN_rfdDbZvTDj6Te) From 654d3bfd2bbf58d99f5ce38d140abe3abe1d86d5 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Wed, 2 Oct 2019 20:02:11 -0700 Subject: [PATCH 076/235] Update Poly, add Include11 type. --- Package.resolved | 4 +- Package.swift | 2 +- Sources/JSONAPI/Document/Includes.swift | 8 ++ Sources/JSONAPI/Error/BasicJSONAPIError.swift | 2 +- .../JSONAPI/Error/GenericJSONAPIError.swift | 2 +- Sources/JSONAPI/Error/JSONAPIError.swift | 2 +- .../Resource/Poly+PrimaryResource.swift | 5 ++ .../JSONAPITests/Includes/IncludeTests.swift | 33 +++++++ .../Includes/stubs/IncludeStubs.swift | 89 +++++++++++++++++++ .../Includes/stubs/one_include.json | 10 --- .../stubs/three_different_type_includes.json | 30 ------- .../stubs/two_different_type_includes.json | 26 ------ .../stubs/two_same_type_includes.json | 18 ---- Tests/JSONAPITests/Poly/stubs/PolyStubs.swift | 7 ++ 14 files changed, 148 insertions(+), 90 deletions(-) delete mode 100644 Tests/JSONAPITests/Includes/stubs/one_include.json delete mode 100644 Tests/JSONAPITests/Includes/stubs/three_different_type_includes.json delete mode 100644 Tests/JSONAPITests/Includes/stubs/two_different_type_includes.json delete mode 100644 Tests/JSONAPITests/Includes/stubs/two_same_type_includes.json diff --git a/Package.resolved b/Package.resolved index f7356ed..b1970b8 100644 --- a/Package.resolved +++ b/Package.resolved @@ -6,8 +6,8 @@ "repositoryURL": "https://github.com/mattpolzin/Poly.git", "state": { "branch": null, - "revision": "4a08517b24f8e9f6dd8c02ec7da316aac5c00e2e", - "version": "2.1.0" + "revision": "b24fd3b41bf3126d4c6dede3708135182172af60", + "version": "2.2.0" } } ] diff --git a/Package.swift b/Package.swift index e0f38ab..2093673 100644 --- a/Package.swift +++ b/Package.swift @@ -18,7 +18,7 @@ let package = Package( targets: ["JSONAPITesting"]) ], dependencies: [ - .package(url: "https://github.com/mattpolzin/Poly.git", .upToNextMajor(from: "2.1.0")), + .package(url: "https://github.com/mattpolzin/Poly.git", .upToNextMajor(from: "2.2.0")), ], targets: [ .target( diff --git a/Sources/JSONAPI/Document/Includes.swift b/Sources/JSONAPI/Document/Includes.swift index 65ccee0..b6d93e0 100644 --- a/Sources/JSONAPI/Document/Includes.swift +++ b/Sources/JSONAPI/Document/Includes.swift @@ -169,3 +169,11 @@ extension Includes where I: _Poly10 { return values.compactMap { $0.j } } } + +// MARK: - 11 includes +public typealias Include11 = Poly11 +extension Includes where I: _Poly11 { + public subscript(_ lookup: I.K.Type) -> [I.K] { + return values.compactMap { $0.k } + } +} diff --git a/Sources/JSONAPI/Error/BasicJSONAPIError.swift b/Sources/JSONAPI/Error/BasicJSONAPIError.swift index d3859eb..28795b9 100644 --- a/Sources/JSONAPI/Error/BasicJSONAPIError.swift +++ b/Sources/JSONAPI/Error/BasicJSONAPIError.swift @@ -1,5 +1,5 @@ // -// BasicError.swift +// BasicJSONAPIError.swift // JSONAPI // // Created by Mathew Polzin on 9/29/19. diff --git a/Sources/JSONAPI/Error/GenericJSONAPIError.swift b/Sources/JSONAPI/Error/GenericJSONAPIError.swift index 91ce2b8..09383e7 100644 --- a/Sources/JSONAPI/Error/GenericJSONAPIError.swift +++ b/Sources/JSONAPI/Error/GenericJSONAPIError.swift @@ -1,5 +1,5 @@ // -// GenericError.swift +// GenericJSONAPIError.swift // JSONAPI // // Created by Mathew Polzin on 9/29/19. diff --git a/Sources/JSONAPI/Error/JSONAPIError.swift b/Sources/JSONAPI/Error/JSONAPIError.swift index f1dbfe9..b997a69 100644 --- a/Sources/JSONAPI/Error/JSONAPIError.swift +++ b/Sources/JSONAPI/Error/JSONAPIError.swift @@ -1,5 +1,5 @@ // -// Error.swift +// JSONAPIError.swift // JSONAPI // // Created by Mathew Polzin on 11/10/18. diff --git a/Sources/JSONAPI/Resource/Poly+PrimaryResource.swift b/Sources/JSONAPI/Resource/Poly+PrimaryResource.swift index d1e1e5f..284e10a 100644 --- a/Sources/JSONAPI/Resource/Poly+PrimaryResource.swift +++ b/Sources/JSONAPI/Resource/Poly+PrimaryResource.swift @@ -79,3 +79,8 @@ extension Poly9: PrimaryResource, OptionalPrimaryResource where A: PolyWrapped, extension Poly10: EncodablePrimaryResource, OptionalEncodablePrimaryResource where A: EncodablePolyWrapped, B: EncodablePolyWrapped, C: EncodablePolyWrapped, D: EncodablePolyWrapped, E: EncodablePolyWrapped, F: EncodablePolyWrapped, G: EncodablePolyWrapped, H: EncodablePolyWrapped, I: EncodablePolyWrapped, J: EncodablePolyWrapped {} extension Poly10: PrimaryResource, OptionalPrimaryResource where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped, E: PolyWrapped, F: PolyWrapped, G: PolyWrapped, H: PolyWrapped, I: PolyWrapped, J: PolyWrapped {} + +// MARK: - 11 types +extension Poly11: EncodablePrimaryResource, OptionalEncodablePrimaryResource where A: EncodablePolyWrapped, B: EncodablePolyWrapped, C: EncodablePolyWrapped, D: EncodablePolyWrapped, E: EncodablePolyWrapped, F: EncodablePolyWrapped, G: EncodablePolyWrapped, H: EncodablePolyWrapped, I: EncodablePolyWrapped, J: EncodablePolyWrapped, K: EncodablePolyWrapped {} + +extension Poly11: PrimaryResource, OptionalPrimaryResource where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped, E: PolyWrapped, F: PolyWrapped, G: PolyWrapped, H: PolyWrapped, I: PolyWrapped, J: PolyWrapped, K: PolyWrapped {} diff --git a/Tests/JSONAPITests/Includes/IncludeTests.swift b/Tests/JSONAPITests/Includes/IncludeTests.swift index ed94e89..bed7d22 100644 --- a/Tests/JSONAPITests/Includes/IncludeTests.swift +++ b/Tests/JSONAPITests/Includes/IncludeTests.swift @@ -197,6 +197,28 @@ class IncludedTests: XCTestCase { test_DecodeEncodeEquality(type: Includes>.self, data: ten_different_type_includes) } + + func test_ElevenDifferentIncludes() { + let includes = decoded(type: Includes>.self, + data: eleven_different_type_includes) + + XCTAssertEqual(includes[TestEntity.self].count, 1) + XCTAssertEqual(includes[TestEntity2.self].count, 1) + XCTAssertEqual(includes[TestEntity3.self].count, 1) + XCTAssertEqual(includes[TestEntity4.self].count, 1) + XCTAssertEqual(includes[TestEntity5.self].count, 1) + XCTAssertEqual(includes[TestEntity6.self].count, 1) + XCTAssertEqual(includes[TestEntity7.self].count, 1) + XCTAssertEqual(includes[TestEntity8.self].count, 1) + XCTAssertEqual(includes[TestEntity9.self].count, 1) + XCTAssertEqual(includes[TestEntity10.self].count, 1) + XCTAssertEqual(includes[TestEntity11.self].count, 1) + } + + func test_ElevenDifferentIncludes_encode() { + test_DecodeEncodeEquality(type: Includes>.self, + data: eleven_different_type_includes) + } } // MARK: - Appending @@ -503,4 +525,15 @@ extension IncludedTests { } typealias TestEntity10 = BasicEntity + + enum TestEntityType11: ResourceObjectDescription { + + typealias Attributes = NoAttributes + + public static var jsonType: String { return "test_entity11" } + + typealias Relationships = NoRelationships + } + + typealias TestEntity11 = BasicEntity } diff --git a/Tests/JSONAPITests/Includes/stubs/IncludeStubs.swift b/Tests/JSONAPITests/Includes/stubs/IncludeStubs.swift index 80f1bfa..5e5f593 100644 --- a/Tests/JSONAPITests/Includes/stubs/IncludeStubs.swift +++ b/Tests/JSONAPITests/Includes/stubs/IncludeStubs.swift @@ -597,3 +597,92 @@ let ten_different_type_includes = """ } ] """.data(using: .utf8)! + +let eleven_different_type_includes = """ +[ + { + "type": "test_entity1", + "id": "2DF03B69-4B0A-467F-B52E-B0C9E44FCECF", + "attributes": { + "foo": "Hello", + "bar": 123 + } + }, + { + "type": "test_entity2", + "id": "90F03B69-4DF1-467F-B52E-B0C9E44FC333", + "attributes": { + "foo": "World", + "bar": 456 + }, + "relationships": { + "entity1": { + "data": { + "type": "test_entity1", + "id": "2DF03B69-4B0A-467F-B52E-B0C9E44FCECF" + } + } + } + }, + { + "type": "test_entity3", + "id": "11223B69-4DF1-467F-B52E-B0C9E44FC443", + "relationships": { + "entity1": { + "data": { + "type": "test_entity1", + "id": "2DF03B69-4B0A-467F-B52E-B0C9E44FCECF" + } + }, + "entity2": { + "data": [ + { + "type": "test_entity2", + "id": "90F03B69-4DF1-467F-B52E-B0C9E44FC333" + } + ] + } + } + }, + { + "type": "test_entity6", + "id": "11113B69-4DF1-467F-B52E-B0C9E44FC444", + "relationships": { + "entity4": { + "data": { + "type": "test_entity4", + "id": "364B3B69-4DF1-467F-B52E-B0C9E44F666E" + } + } + } + }, + { + "type": "test_entity5", + "id": "A24B3B69-4DF1-467F-B52E-B0C9E44F436A" + }, + { + "type": "test_entity4", + "id": "364B3B69-4DF1-467F-B52E-B0C9E44F666E" + }, + { + "type": "test_entity7", + "id": "364B3B69-4DF1-222F-B52E-B0C9E44F666E" + }, + { + "type": "test_entity8", + "id": "364B3B69-4DF1-222F-B52E-B0C9E44F266F" + }, + { + "type": "test_entity9", + "id": "364B3B69-4DF1-218F-B52E-B0C9E44F2661" + }, + { + "type": "test_entity10", + "id": "264B3B69-4DF1-212F-B52E-B0C9E44F2660" + }, + { + "type": "test_entity11", + "id": "264B3B69-4DF3-212F-B32E-A0C9E44F26C0B" + } +] +""".data(using: .utf8)! diff --git a/Tests/JSONAPITests/Includes/stubs/one_include.json b/Tests/JSONAPITests/Includes/stubs/one_include.json deleted file mode 100644 index 9d2a3a4..0000000 --- a/Tests/JSONAPITests/Includes/stubs/one_include.json +++ /dev/null @@ -1,10 +0,0 @@ -[ - { - "type": "test_entity1", - "id": "2DF03B69-4B0A-467F-B52E-B0C9E44FCECF", - "attributes": { - "foo": "Hello", - "bar": 123 - } - } -] diff --git a/Tests/JSONAPITests/Includes/stubs/three_different_type_includes.json b/Tests/JSONAPITests/Includes/stubs/three_different_type_includes.json deleted file mode 100644 index edd1ba8..0000000 --- a/Tests/JSONAPITests/Includes/stubs/three_different_type_includes.json +++ /dev/null @@ -1,30 +0,0 @@ -[ - { - "type": "test_entity1", - "id": "2DF03B69-4B0A-467F-B52E-B0C9E44FCECF", - "attributes": { - "foo": "Hello", - "bar": 123 - } - }, - { - "type": "test_entity2", - "id": "90F03B69-4DF1-467F-B52E-B0C9E44FC333", - "attributes": { - "foo": "World", - "bar": 456 - }, - "relationships": { - "entity1": { - "data": { - "type": "test_entity1", - "id": "2DF03B69-4B0A-467F-B52E-B0C9E44FCECF" - } - } - } - }, - { - "type": "test_entity4", - "id": "364B3B69-4DF1-467F-B52E-B0C9E44F666E" - } -] diff --git a/Tests/JSONAPITests/Includes/stubs/two_different_type_includes.json b/Tests/JSONAPITests/Includes/stubs/two_different_type_includes.json deleted file mode 100644 index 6ef8e14..0000000 --- a/Tests/JSONAPITests/Includes/stubs/two_different_type_includes.json +++ /dev/null @@ -1,26 +0,0 @@ -[ - { - "type": "test_entity1", - "id": "2DF03B69-4B0A-467F-B52E-B0C9E44FCECF", - "attributes": { - "foo": "Hello", - "bar": 123 - } - }, - { - "type": "test_entity2", - "id": "90F03B69-4DF1-467F-B52E-B0C9E44FC333", - "attributes": { - "foo": "World", - "bar": 456 - }, - "relationships": { - "entity1": { - "data": { - "type": "test_entity1", - "id": "2DF03B69-4B0A-467F-B52E-B0C9E44FCECF" - } - } - } - } -] diff --git a/Tests/JSONAPITests/Includes/stubs/two_same_type_includes.json b/Tests/JSONAPITests/Includes/stubs/two_same_type_includes.json deleted file mode 100644 index d1dfb42..0000000 --- a/Tests/JSONAPITests/Includes/stubs/two_same_type_includes.json +++ /dev/null @@ -1,18 +0,0 @@ -[ - { - "type": "test_entity1", - "id": "2DF03B69-4B0A-467F-B52E-B0C9E44FCECF", - "attributes": { - "foo": "Hello", - "bar": 123 - } - }, - { - "type": "test_entity1", - "id": "90F03B69-4DF1-467F-B52E-B0C9E44FC333", - "attributes": { - "foo": "World", - "bar": 456 - } - } -] diff --git a/Tests/JSONAPITests/Poly/stubs/PolyStubs.swift b/Tests/JSONAPITests/Poly/stubs/PolyStubs.swift index fa4ac4d..b1254cf 100644 --- a/Tests/JSONAPITests/Poly/stubs/PolyStubs.swift +++ b/Tests/JSONAPITests/Poly/stubs/PolyStubs.swift @@ -114,3 +114,10 @@ let poly_entity10 = """ "id": "A24B3444-4DF1-467F-B52E-B0C9E12F436A" } """.data(using: .utf8)! + +let poly_entity11 = """ +{ + "type": "test_entity11", + "id": "A24B3444-4DF1-467F-B52E-B0C9E12F4440" +} +""".data(using: .utf8)! From 774b53b9b6b26cdbed080da0479450726027ad78 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Wed, 2 Oct 2019 20:05:28 -0700 Subject: [PATCH 077/235] bump podspec version and bump podspec Poly dependency. --- JSONAPI.podspec | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/JSONAPI.podspec b/JSONAPI.podspec index b84fef0..780cb69 100644 --- a/JSONAPI.podspec +++ b/JSONAPI.podspec @@ -16,7 +16,7 @@ Pod::Spec.new do |spec| # spec.name = "MP-JSONAPI" - spec.version = "2.2.0" + spec.version = "2.3.0" spec.summary = "Swift Codable JSON API framework." # 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.xcconfig = { "HEADER_SEARCH_PATHS" => "$(SDKROOT)/usr/include/libxml2" } - spec.dependency "Poly", "~> 2.1" + spec.dependency "Poly", "~> 2.2" end From 662a84ccf0a045fa4b30c2a40f74c485d0807bb5 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Sat, 12 Oct 2019 09:35:01 -0700 Subject: [PATCH 078/235] Add examples around modifying attributes/relationships on resource object and then preparing a new request (i.e. for PATCHing) --- .../PATCHing.xcplaygroundpage/Contents.swift | 147 ++++++++++++++++++ JSONAPI.playground/Sources/Entities.swift | 25 +++ JSONAPI.playground/contents.xcplayground | 1 + 3 files changed, 173 insertions(+) create mode 100644 JSONAPI.playground/Pages/PATCHing.xcplaygroundpage/Contents.swift diff --git a/JSONAPI.playground/Pages/PATCHing.xcplaygroundpage/Contents.swift b/JSONAPI.playground/Pages/PATCHing.xcplaygroundpage/Contents.swift new file mode 100644 index 0000000..937b873 --- /dev/null +++ b/JSONAPI.playground/Pages/PATCHing.xcplaygroundpage/Contents.swift @@ -0,0 +1,147 @@ +//: [Previous](@previous) + +import Foundation +import JSONAPI + +/******* + + Please enjoy these examples, but allow me the forced casting and the lack of error checking for the sake of brevity. + + This playground focuses on receiving a resource, making some changes, and then creating a request body for a PATCH request. + As with all examples in these playround pages, no actual networking code will be provided. + + ********/ + +// Mapping functions (will be included in future version of library) +extension JSONAPI.ResourceObject { + func mapAttributes(_ transform: (Description.Attributes) -> Description.Attributes) -> Self { + return Self(id: id, + attributes: transform(attributes), + relationships: relationships, + meta: meta, + links: links) + } + + func mapRelationships(_ transform: (Description.Relationships) -> Description.Relationships) -> Self { + return Self(id: id, + attributes: attributes, + relationships: transform(relationships), + meta: meta, + links: links) + } +} + +// Mock up a server response +let mockDogData = """ +{ + "data": { + "id": "1234", + "type": "dogs", + "attributes": { + "name": "Sparky" + }, + "relationships": { + "owner": { + "data": null + } + } + } +} +""".data(using: .utf8)! + +// +// MARK: - EXAMPLE 1 (Mutable Attributes) +// + +// pretend to have requested a Dog and received the mock data +// now parse it. +let parsedResponse = try! JSONDecoder().decode(MutableDogDocument.self, from: mockDogData) + +// extract our Dog (skipping over any robustness to handle errors) +var dog = parsedResponse.body.primaryResource!.value +print("Received dog named: \(dog.name)") + +// change the dog's name +let changedDog = dog.mapAttributes { currentAttributes in + var ret = currentAttributes + ret.name = .init(value: "Julia") + return ret +} + +// create a document to be used as a request body for a PATCH request +let patchRequest = MutableDogDocument(apiDescription: .none, + body: .init(resourceObject: changedDog), + includes: .none, + meta: .none, + links: .none) + +// encode and send off to server +let encodedPatchRequest = try! JSONEncoder().encode(patchRequest) +print("----") +print(String(data: encodedPatchRequest, encoding:.utf8)!) + + +// +// MARK: - EXAMPLE 2 (Immutable Attributes) +// +print() +print("####") +print() + +// pretend to have requested a Dog and received the mock data +// now parse it. +let parsedResponse2 = try! JSONDecoder().decode(SingleDogDocument.self, from: mockDogData) + +// extract our Dog (skipping over any robustness to handle errors) +var dog2 = parsedResponse2.body.primaryResource!.value +print("Received dog named: \(dog2.name)") + +// change the dog's name +let changedDog2 = dog2.mapAttributes { _ in + return .init(name: .init(value: "Nigel")) +} + +// create a document to be used as a request body for a PATCH request +let patchRequest2 = SingleDogDocument(apiDescription: .none, + body: .init(resourceObject: changedDog2), + includes: .none, + meta: .none, + links: .none) + +// encode and send off to server +let encodedPatchRequest2 = try! JSONEncoder().encode(patchRequest2) +print("----") +print(String(data: encodedPatchRequest2, encoding:.utf8)!) + + +// +// MARK: - EXAMPLE 3 (Change relationship) +// +print() +print("####") +print() + +// pretend to have requested a Dog and received the mock data +// now parse it. +let parsedResponse3 = try! JSONDecoder().decode(SingleDogDocument.self, from: mockDogData) + +// extract our Dog (skipping over any robustness to handle errors) +var dog3 = parsedResponse2.body.primaryResource!.value +print("Received dog with owner: \(dog3 ~> \.owner)") + +// give the dog an owner +let changedDog3 = dog3.mapRelationships { _ in + return .init(owner: .init(id: Id(rawValue: "1"))) +} + +// create a document to be used as a request body for a PATCH request +let patchRequest3 = SingleDogDocument(apiDescription: .none, + body: .init(resourceObject: changedDog3), + includes: .none, + meta: .none, + links: .none) + +// encode and send off to server +let encodedPatchRequest3 = try! JSONEncoder().encode(patchRequest3) +print("----") +print(String(data: encodedPatchRequest3, encoding:.utf8)!) diff --git a/JSONAPI.playground/Sources/Entities.swift b/JSONAPI.playground/Sources/Entities.swift index 4ae56c9..1bd6109 100644 --- a/JSONAPI.playground/Sources/Entities.swift +++ b/JSONAPI.playground/Sources/Entities.swift @@ -119,6 +119,29 @@ public enum AlternativeDogDescription: ResourceObjectDescription { public typealias AlternativeDog = ExampleEntity +public enum MutableDogDescription: ResourceObjectDescription { + + public static var jsonType: String { return "dogs" } + + public struct Attributes: JSONAPI.Attributes { + public var name: Attribute + + public init(name: Attribute) { + self.name = name + } + } + + public struct Relationships: JSONAPI.Relationships { + public var owner: ToOne + + public init(owner: ToOne) { + self.owner = owner + } + } +} + +public typealias MutableDog = ExampleEntity + public extension ResourceObject where Description == DogDescription, MetaType == NoMetadata, LinksType == NoLinks, EntityRawIdType == String { init(name: String, owner: Person?) throws { self = Dog(attributes: .init(name: .init(value: name)), relationships: DogDescription.Relationships(owner: .init(resourceObject: owner)), meta: .none, links: .none) @@ -141,4 +164,6 @@ public typealias House = ExampleEntity public typealias SingleDogDocument = JSONAPI.Document, NoMetadata, NoLinks, NoIncludes, NoAPIDescription, BasicJSONAPIError> +public typealias MutableDogDocument = JSONAPI.Document, NoMetadata, NoLinks, NoIncludes, NoAPIDescription, BasicJSONAPIError> + public typealias BatchPeopleDocument = JSONAPI.Document, NoMetadata, NoLinks, Include2, NoAPIDescription, BasicJSONAPIError> diff --git a/JSONAPI.playground/contents.xcplayground b/JSONAPI.playground/contents.xcplayground index 21e919c..e777e3c 100644 --- a/JSONAPI.playground/contents.xcplayground +++ b/JSONAPI.playground/contents.xcplayground @@ -6,5 +6,6 @@ + \ No newline at end of file From 0b307bd3bcd9dbb03d83c5cb412012d3bdf0b0ef Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Sat, 12 Oct 2019 17:54:28 -0700 Subject: [PATCH 079/235] Add replacement and tapping functions for attributes and relationships. --- .../PATCHing.xcplaygroundpage/Contents.swift | 29 +--- .../ResourceObject+Replacing.swift | 74 +++++++++ .../ResourceObject.swift | 0 .../ResourceObject+ReplacingTests.swift | 155 ++++++++++++++++++ 4 files changed, 232 insertions(+), 26 deletions(-) create mode 100644 Sources/JSONAPI/Resource/Resource Object/ResourceObject+Replacing.swift rename Sources/JSONAPI/Resource/{ => Resource Object}/ResourceObject.swift (100%) create mode 100644 Tests/JSONAPITests/ResourceObject/ResourceObject+ReplacingTests.swift diff --git a/JSONAPI.playground/Pages/PATCHing.xcplaygroundpage/Contents.swift b/JSONAPI.playground/Pages/PATCHing.xcplaygroundpage/Contents.swift index 937b873..f916054 100644 --- a/JSONAPI.playground/Pages/PATCHing.xcplaygroundpage/Contents.swift +++ b/JSONAPI.playground/Pages/PATCHing.xcplaygroundpage/Contents.swift @@ -12,25 +12,6 @@ import JSONAPI ********/ -// Mapping functions (will be included in future version of library) -extension JSONAPI.ResourceObject { - func mapAttributes(_ transform: (Description.Attributes) -> Description.Attributes) -> Self { - return Self(id: id, - attributes: transform(attributes), - relationships: relationships, - meta: meta, - links: links) - } - - func mapRelationships(_ transform: (Description.Relationships) -> Description.Relationships) -> Self { - return Self(id: id, - attributes: attributes, - relationships: transform(relationships), - meta: meta, - links: links) - } -} - // Mock up a server response let mockDogData = """ { @@ -62,11 +43,7 @@ var dog = parsedResponse.body.primaryResource!.value print("Received dog named: \(dog.name)") // change the dog's name -let changedDog = dog.mapAttributes { currentAttributes in - var ret = currentAttributes - ret.name = .init(value: "Julia") - return ret -} +let changedDog = dog.tappingAttributes { $0.name = .init(value: "Julia") } // create a document to be used as a request body for a PATCH request let patchRequest = MutableDogDocument(apiDescription: .none, @@ -97,7 +74,7 @@ var dog2 = parsedResponse2.body.primaryResource!.value print("Received dog named: \(dog2.name)") // change the dog's name -let changedDog2 = dog2.mapAttributes { _ in +let changedDog2 = dog2.replacingAttributes { _ in return .init(name: .init(value: "Nigel")) } @@ -130,7 +107,7 @@ var dog3 = parsedResponse2.body.primaryResource!.value print("Received dog with owner: \(dog3 ~> \.owner)") // give the dog an owner -let changedDog3 = dog3.mapRelationships { _ in +let changedDog3 = dog3.replacingRelationships { _ in return .init(owner: .init(id: Id(rawValue: "1"))) } diff --git a/Sources/JSONAPI/Resource/Resource Object/ResourceObject+Replacing.swift b/Sources/JSONAPI/Resource/Resource Object/ResourceObject+Replacing.swift new file mode 100644 index 0000000..98ef37c --- /dev/null +++ b/Sources/JSONAPI/Resource/Resource Object/ResourceObject+Replacing.swift @@ -0,0 +1,74 @@ +// +// ResourceObject+Replacing.swift +// JSONAPI +// +// Created by Mathew Polzin on 10/12/19. +// + +public extension JSONAPI.ResourceObject { + /// Return a new `ResourceObject`, having replaced `self`'s + /// `attributes` with the attributes returned by the given + /// replacement function. + /// + /// - important: `self` is not mutated. A copy of self is returned. + /// + /// - parameters: + /// - replacement: A function that takes the existing `attributes` and returns the replacement. + func replacingAttributes(_ replacement: (Description.Attributes) -> Description.Attributes) -> Self { + return Self(id: id, + attributes: replacement(attributes), + relationships: relationships, + meta: meta, + links: links) + } + + /// Return a new `ResourceObject`, having updated `self`'s + /// `attributes` with the tap function given. + /// + /// - important: `self` is not mutated. A copy of self is returned. + /// + /// - parameters: + /// - tap: A function that takes a copy of the existing `attributes` and mutates them. + func tappingAttributes(_ tap: (inout Description.Attributes) -> Void) -> Self { + var newAttributes = attributes + tap(&newAttributes) + return Self(id: id, + attributes: newAttributes, + relationships: relationships, + meta: meta, + links: links) + } + + /// Return a new `ResourceObject`, having replaced `self`'s + /// `relationships` with the `relationships` returned by the given + /// replacement function. + /// + /// - important: `self` is not mutated. A copy of self is returned. + /// + /// - parameters: + /// - replacement: A function that takes the existing relationships and returns the replacement. + func replacingRelationships(_ replacement: (Description.Relationships) -> Description.Relationships) -> Self { + return Self(id: id, + attributes: attributes, + relationships: replacement(relationships), + meta: meta, + links: links) + } + + /// Return a new `ResourceObject`, having updated `self`'s + /// `relationships` with the tap function given. + /// + /// - important: `self` is not mutated. A copy of self is returned. + /// + /// - parameters: + /// - tap: A function that takes a copy of the existing `relationships` and mutates them. + func tappingRelationships(_ tap: (inout Description.Relationships) -> Void) -> Self { + var newRelationships = relationships + tap(&newRelationships) + return Self(id: id, + attributes: attributes, + relationships: newRelationships, + meta: meta, + links: links) + } +} diff --git a/Sources/JSONAPI/Resource/ResourceObject.swift b/Sources/JSONAPI/Resource/Resource Object/ResourceObject.swift similarity index 100% rename from Sources/JSONAPI/Resource/ResourceObject.swift rename to Sources/JSONAPI/Resource/Resource Object/ResourceObject.swift diff --git a/Tests/JSONAPITests/ResourceObject/ResourceObject+ReplacingTests.swift b/Tests/JSONAPITests/ResourceObject/ResourceObject+ReplacingTests.swift new file mode 100644 index 0000000..337a5b6 --- /dev/null +++ b/Tests/JSONAPITests/ResourceObject/ResourceObject+ReplacingTests.swift @@ -0,0 +1,155 @@ +// +// ResourceObject+ReplacingTests.swift +// JSONAPITests +// +// Created by Mathew Polzin on 10/12/19. +// + +import XCTest +import JSONAPI + +final class ResourceObjectReplacingTests: XCTestCase { + func test_replaceMutableAttributes() { + let testResource = MutableTestType(attributes: .init(name: .init(value: "Matt")), + relationships: .init(other: .init(id: .init(rawValue: "2"))), + meta: .none, + links: .none) + + let mutatedResource = testResource + .replacingAttributes { + var newAttributes = $0 + newAttributes.name = .init(value: "Matt 2") + return newAttributes + } + + XCTAssertEqual(testResource.name, "Matt") + XCTAssertEqual(mutatedResource.name, "Matt 2") + } + + func test_tapMutableAttributes() { + let testResource = MutableTestType(attributes: .init(name: .init(value: "Matt")), + relationships: .init(other: .init(id: .init(rawValue: "2"))), + meta: .none, + links: .none) + + let mutatedResource = testResource + .tappingAttributes { $0.name = .init(value: "Matt 2") } + + XCTAssertEqual(testResource.name, "Matt") + XCTAssertEqual(mutatedResource.name, "Matt 2") + } + + func test_replaceImmutableAttributes() { + let testResource = ImmutableTestType(attributes: .init(name: .init(value: "Matt")), + relationships: .init(other: .init(id: .init(rawValue: "2"))), + meta: .none, + links: .none) + + let mutatedResource = testResource + .replacingAttributes { + return .init(name: $0.name.map { $0 + " 2" }) + } + + XCTAssertEqual(testResource.name, "Matt") + XCTAssertEqual(mutatedResource.name, "Matt 2") + } + + func test_tapImmutableAttributes() { + let testResource = ImmutableTestType(attributes: .init(name: .init(value: "Matt")), + relationships: .init(other: .init(id: .init(rawValue: "2"))), + meta: .none, + links: .none) + + let mutatedResource = testResource + .tappingAttributes { $0 = .init(name: $0.name.map { $0 + " 2" }) } + + XCTAssertEqual(testResource.name, "Matt") + XCTAssertEqual(mutatedResource.name, "Matt 2") + } + + func test_replaceMutableRelationships() { + let testResource = MutableTestType(attributes: .init(name: .init(value: "Matt")), + relationships: .init(other: .init(id: .init(rawValue: "2"))), + meta: .none, + links: .none) + + let mutatedResource = testResource + .replacingRelationships { + var newRelationships = $0 + newRelationships.other = .init(id: .init(rawValue: "3")) + return newRelationships + } + + XCTAssertEqual(testResource ~> \.other, "2") + XCTAssertEqual(mutatedResource ~> \.other, "3") + } + + func test_tapMutableRelationships() { + let testResource = MutableTestType(attributes: .init(name: .init(value: "Matt")), + relationships: .init(other: .init(id: .init(rawValue: "2"))), + meta: .none, + links: .none) + + let mutatedResource = testResource + .tappingRelationships { $0.other = .init(id: .init(rawValue: "3")) } + + XCTAssertEqual(testResource ~> \.other, "2") + XCTAssertEqual(mutatedResource ~> \.other, "3") + } + + func test_replaceImmutableRelationships() { + let testResource = ImmutableTestType(attributes: .init(name: .init(value: "Matt")), + relationships: .init(other: .init(id: .init(rawValue: "2"))), + meta: .none, + links: .none) + + let mutatedResource = testResource + .replacingRelationships { _ in + return .init(other: .init(id: .init(rawValue: "3"))) + } + + XCTAssertEqual(testResource ~> \.other, "2") + XCTAssertEqual(mutatedResource ~> \.other, "3") + } + + func test_tapImmutableRelationships() { + let testResource = ImmutableTestType(attributes: .init(name: .init(value: "Matt")), + relationships: .init(other: .init(id: .init(rawValue: "2"))), + meta: .none, + links: .none) + + let mutatedResource = testResource + .tappingRelationships { $0 = .init(other: .init(id: .init(rawValue: "3"))) } + + XCTAssertEqual(testResource ~> \.other, "2") + XCTAssertEqual(mutatedResource ~> \.other, "3") + } +} + +private enum MutableTestDescription: JSONAPI.ResourceObjectDescription { + static let jsonType: String = "test" + + struct Attributes: JSONAPI.Attributes { + var name: Attribute + } + + struct Relationships: JSONAPI.Relationships { + var other: ToOneRelationship + } +} + +private typealias MutableTestType = JSONAPI.ResourceObject + +private enum ImmutableTestDescription: JSONAPI.ResourceObjectDescription { + static let jsonType: String = "test2" + + struct Attributes: JSONAPI.Attributes { + let name: Attribute + } + + struct Relationships: JSONAPI.Relationships { + let other: ToOneRelationship + } +} + +private typealias ImmutableTestType = JSONAPI.ResourceObject From 43e02351deef20425154b9b8e634ebe1a58e4a6d Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Sat, 12 Oct 2019 18:22:48 -0700 Subject: [PATCH 080/235] Add documentation on replacing and tapping attributes and relationships --- README.md | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/README.md b/README.md index 1120097..c4d238c 100644 --- a/README.md +++ b/README.md @@ -76,6 +76,9 @@ This library works well when used by both the server responsible for serializati - [Sparse Fieldsets](#sparse-fieldsets) - [Supporting Sparse Fieldset Encoding](#supporting-sparse-fieldset-encoding) - [Sparse Fieldset `typealias` comparisons](#sparse-fieldset-typealias-comparisons) + - [Replacing and Tapping Attributes/Relationships](#replacing-and-tapping-attributesrelationships) + - [Tapping](#tapping) + - [Replacing](#replacing) - [Custom Attribute or Relationship Key Mapping](#custom-attribute-or-relationship-key-mapping) - [Custom Attribute Encode/Decode](#custom-attribute-encodedecode) - [Meta-Attributes](#meta-attributes) @@ -569,6 +572,35 @@ In order to support sparse fieldsets (which are encode-only), the following comp typealias SparseDocument = JSONAPI.Document> ``` +### Replacing and Tapping Attributes/Relationships +When you are working with an immutable Resource Object, it can be useful to replace its attributes or relationships. As a client, you might receive a resource from the server, update something, and then send the server a PATCH request. + +`ResourceObject` is immutable, but you can create a new copy of a `ResourceObject` having updated attributes or relationships. + +#### Tapping +If your `Attributes` or `Relationships` struct is mutable (i.e. its properties are `var`s) then you may find `ResourceObject`'s `tappingAttributes()` and `tappingRelationships()` functions useful. For both, you pass a function that takes an `inout` copy of the respective object or value that you can mutate. The mutated value is then used to create a new `ResourceObject`. + +For example, to take a hypothetical `Dog` resource object and change the name attribute: +```swift +let resourceObject = Dog(...) + +let newResourceObject = resourceObject + .tappingAttributes { $0.name = .init(value: "Charlie") } +``` + +#### Replacing +If your `Attributes` or `Relationships` struct is immutable (i.e. its properties are `let`s) then you may find `ResourceObject`'s `replacingAttributes()` and `replacingRelationships()` functions useful. For both, you pass a function that takes the current attributes or relationships and you return a new value. The new value is then used to create a new `ResourceObject`. + +For example, to take a hypothetical `Dog` resource object and change the name attribute: +```swift +let resourceObject = Dog(...) + +let newResourceObject = resourceObject + .replacingAttributes { _ in + return Dog.Attributes(name: .init(value: "Charlie")) +} +``` + ### Custom Attribute or Relationship Key Mapping There is not anything special going on at the `JSONAPI.Attributes` and `JSONAPI.Relationships` levels, so you can easily provide custom key mappings by taking advantage of `Codable`'s `CodingKeys` pattern. Here are two models that will encode/decode equivalently but offer different naming in your codebase: ```swift From dbbacab105a46af08c386137331ce4bf73ff3de2 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Sat, 12 Oct 2019 19:30:24 -0700 Subject: [PATCH 081/235] Bump podspec version --- JSONAPI.podspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/JSONAPI.podspec b/JSONAPI.podspec index 780cb69..1d3e68f 100644 --- a/JSONAPI.podspec +++ b/JSONAPI.podspec @@ -16,7 +16,7 @@ Pod::Spec.new do |spec| # spec.name = "MP-JSONAPI" - spec.version = "2.3.0" + spec.version = "2.4.0" spec.summary = "Swift Codable JSON API framework." # This description is used to generate tags and improve search results. From dbc6ecc6d82fce1162fc37472fe0424541de5bf9 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Sat, 12 Oct 2019 21:39:29 -0700 Subject: [PATCH 082/235] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index c4d238c..556d192 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ See the JSON API Spec here: https://jsonapi.org/format/ - [Compound Example](https://colab.research.google.com/drive/1BdF0Kc7l2ixDfBZEL16FY6palweDszQU) - [Metadata Example](https://colab.research.google.com/drive/10dEESwiE9I3YoyfzVeOVwOKUTEgLT3qr) - [Custom Errors Example](https://colab.research.google.com/drive/1TIv6STzlHrkTf_-9Eu8sv8NoaxhZcFZH) +- [PATCH Example](https://colab.research.google.com/drive/16KY-0BoLQKiSUh9G7nYmHzB8b2vhXA2U) ### Serverside - [GET Example](https://colab.research.google.com/drive/1krbhzSfz8mwkBTQQnKUZJLEtYsJKSfYX) From b46429a0adaacaa3e0e7d8230460ce091c260c57 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Tue, 15 Oct 2019 23:37:02 -0700 Subject: [PATCH 083/235] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 556d192..688b5fa 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ See the JSON API Spec here: https://jsonapi.org/format/ - [GET Example](https://colab.research.google.com/drive/1krbhzSfz8mwkBTQQnKUZJLEtYsJKSfYX) - [POST Example](https://colab.research.google.com/drive/1z3n70LwRY7vLIgbsMghvnfHA67QiuqpQ) -### Combined +### Client+Server This library works well when used by both the server responsible for serialization and the client responsible for deserialization. Check out the [example](#example) further down in this README. ## Table of Contents From ea5c0b8601e0b1f0a5fc3952c260cf0154d310bf Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Sun, 20 Oct 2019 22:23:52 -0700 Subject: [PATCH 084/235] Add Document.ErrorDocument and Document.SuccessDocument types --- Sources/JSONAPI/Document/Document.swift | 134 ++++++++++++++++++ .../Document/DocumentDecodingError.swift | 11 ++ .../JSONAPITests/Document/DocumentTests.swift | 132 +++++++++++++++++ 3 files changed, 277 insertions(+) create mode 100644 Sources/JSONAPI/Document/DocumentDecodingError.swift diff --git a/Sources/JSONAPI/Document/Document.swift b/Sources/JSONAPI/Document/Document.swift index aedb87a..2cbf4e7 100644 --- a/Sources/JSONAPI/Document/Document.swift +++ b/Sources/JSONAPI/Document/Document.swift @@ -414,3 +414,137 @@ extension Document.Body.Data: CustomStringConvertible { return "primary: \(String(describing: primary)), includes: \(String(describing: includes)), meta: \(String(describing: meta)), links: \(String(describing: links))" } } + +// MARK: - Error and Success Document Types + +extension Document { + /// A Document that only supports error bodies. This is useful if you wish to pass around a + /// Document type but you wish to constrain it to error values. + @dynamicMemberLookup + public struct ErrorDocument: EncodableJSONAPIDocument { + public var body: Document.Body { return document.body } + + private let document: Document + + public init(apiDescription: APIDescription, errors: [Error], meta: MetaType? = nil, links: LinksType? = nil) { + document = .init(apiDescription: apiDescription, errors: errors, meta: meta, links: links) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + + try container.encode(document) + } + + public subscript(dynamicMember path: KeyPath) -> T { + return document[keyPath: path] + } + + public static func ==(lhs: Document, rhs: ErrorDocument) -> Bool { + return lhs == rhs.document + } + } + + /// A Document that only supports success bodies. This is useful if you wish to pass around a + /// Document type but you wish to constrain it to success values. + @dynamicMemberLookup + public struct SuccessDocument: EncodableJSONAPIDocument { + public var body: Document.Body { return document.body } + + private let document: Document + + public init(apiDescription: APIDescription, + body: PrimaryResourceBody, + includes: Includes, + meta: MetaType, + links: LinksType) { + document = .init(apiDescription: apiDescription, + body: body, + includes: includes, + meta: meta, + links: links) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + + try container.encode(document) + } + + public subscript(dynamicMember path: KeyPath) -> T { + return document[keyPath: path] + } + + public static func ==(lhs: Document, rhs: SuccessDocument) -> Bool { + return lhs == rhs.document + } + } +} + +extension Document.ErrorDocument: Decodable, JSONAPIDocument + where PrimaryResourceBody: ResourceBody, IncludeType: Decodable { + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + + document = try container.decode(Document.self) + + guard document.body.isError else { + throw JSONAPIDocumentDecodingError.foundSuccessDocumentWhenExpectingError + } + } +} + +extension Document.SuccessDocument: Decodable, JSONAPIDocument + where PrimaryResourceBody: ResourceBody, IncludeType: Decodable { + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + + document = try container.decode(Document.self) + + guard !document.body.isError else { + throw JSONAPIDocumentDecodingError.foundErrorDocumentWhenExpectingSuccess + } + } +} + +extension Document.SuccessDocument where IncludeType == NoIncludes { + /// Create a new Document with the given includes. + public func including(_ includes: Includes) -> Document { + // Note that if IncludeType is NoIncludes, then we allow anything + // to be included, but if IncludeType already specifies a type + // of thing to be expected then we lock that down. + // See: Document.including() where IncludeType: _Poly1 + switch document.body { + case .data(let data): + return .init(apiDescription: document.apiDescription, + body: data.primary, + includes: includes, + meta: data.meta, + links: data.links) + case .errors: + fatalError("SuccessDocument cannot end up in an error state") + } + } +} + +// extending where _Poly1 means all non-zero _Poly arities are included +extension Document.SuccessDocument where IncludeType: _Poly1 { + /// Create a new Document adding the given includes. This does not + /// remove existing includes; it is additive. + public func including(_ includes: Includes) -> Document { + // Note that if IncludeType is NoIncludes, then we allow anything + // to be included, but if IncludeType already specifies a type + // of thing to be expected then we lock that down. + // See: Document.including() where IncludeType == NoIncludes + switch document.body { + case .data(let data): + return .init(apiDescription: document.apiDescription, + body: data.primary, + includes: data.includes + includes, + meta: data.meta, + links: data.links) + case .errors: + fatalError("SuccessDocument cannot end up in an error state") + } + } +} diff --git a/Sources/JSONAPI/Document/DocumentDecodingError.swift b/Sources/JSONAPI/Document/DocumentDecodingError.swift new file mode 100644 index 0000000..912a8f8 --- /dev/null +++ b/Sources/JSONAPI/Document/DocumentDecodingError.swift @@ -0,0 +1,11 @@ +// +// DocumentDecodingErro.swift +// +// +// Created by Mathew Polzin on 10/20/19. +// + +public enum JSONAPIDocumentDecodingError: Swift.Error { + case foundErrorDocumentWhenExpectingSuccess + case foundSuccessDocumentWhenExpectingError +} diff --git a/Tests/JSONAPITests/Document/DocumentTests.swift b/Tests/JSONAPITests/Document/DocumentTests.swift index be10f7c..48ed1c8 100644 --- a/Tests/JSONAPITests/Document/DocumentTests.swift +++ b/Tests/JSONAPITests/Document/DocumentTests.swift @@ -23,6 +23,7 @@ class DocumentTests: XCTestCase { XCTAssert(Doc.Error.self == UnknownJSONAPIError.self) } + // Document test(JSONAPI.Document< NoResourceBody, NoMetadata, @@ -37,6 +38,35 @@ class DocumentTests: XCTestCase { meta: .none, links: .none )) + + // Document.SuccessDocument + test(JSONAPI.Document< + NoResourceBody, + NoMetadata, + NoLinks, + NoIncludes, + NoAPIDescription, + UnknownJSONAPIError + >.SuccessDocument( + apiDescription: .none, + body: .none, + includes: .none, + meta: .none, + links: .none + )) + + // Document.ErrorDocument + test(JSONAPI.Document< + NoResourceBody, + NoMetadata, + NoLinks, + NoIncludes, + NoAPIDescription, + UnknownJSONAPIError + >.ErrorDocument( + apiDescription: .none, + errors: [] + )) } func test_singleDocumentNull() { @@ -51,11 +81,34 @@ class DocumentTests: XCTestCase { XCTAssertEqual(document.body.includes?.count, 0) XCTAssertEqual(document.body.links, NoLinks()) XCTAssertEqual(document.apiDescription, .none) + + // SuccessDocument + let document2 = decoded(type: Document, NoMetadata, NoLinks, NoIncludes, NoAPIDescription, UnknownJSONAPIError>.SuccessDocument.self, + data: single_document_null) + + XCTAssert(document == document2) + + XCTAssertFalse(document2.body.isError) + XCTAssertNil(document2.body.errors) + XCTAssertNotNil(document2.body.primaryResource) + XCTAssertEqual(document2.body.meta, NoMetadata()) + XCTAssertNil(document2.body.primaryResource?.value) + XCTAssertEqual(document2.body.includes?.count, 0) + XCTAssertEqual(document2.body.links, NoLinks()) + XCTAssertEqual(document2.apiDescription, .none) + + // ErrorDocument + XCTAssertThrowsError(try JSONDecoder().decode(Document, NoMetadata, NoLinks, NoIncludes, NoAPIDescription, UnknownJSONAPIError>.ErrorDocument.self, + from: single_document_null)) } func test_singleDocumentNull_encode() { test_DecodeEncodeEquality(type: Document, NoMetadata, NoLinks, NoIncludes, NoAPIDescription, UnknownJSONAPIError>.self, data: single_document_null) + + // SuccessDocument + test_DecodeEncodeEquality(type: Document, NoMetadata, NoLinks, NoIncludes, NoAPIDescription, UnknownJSONAPIError>.SuccessDocument.self, + data: single_document_null) } func test_singleDocumentNullWithAPIDescription() { @@ -94,6 +147,14 @@ extension DocumentTests { func test_errorDocumentFailsWithNoAPIDescription() { XCTAssertThrowsError(try JSONDecoder().decode(Document.self, from: error_document_no_metadata)) + + // SuccessDocument + XCTAssertThrowsError(try JSONDecoder().decode(Document.SuccessDocument.self, + from: error_document_no_metadata)) + + // ErrorDocument + XCTAssertThrowsError(try JSONDecoder().decode(Document.ErrorDocument.self, + from: error_document_no_metadata)) } func test_unknownErrorDocumentNoMeta() { @@ -115,6 +176,32 @@ extension DocumentTests { XCTAssertEqual(errors.0, document.body.errors) XCTAssertEqual(errors.0[0], .unknown) XCTAssertEqual(errors.meta, NoMetadata()) + + // SuccessDocument + XCTAssertThrowsError(try JSONDecoder().decode(Document.SuccessDocument.self, + from: error_document_no_metadata)) + + // ErrorDocument + let document2 = decoded(type: Document.ErrorDocument.self, + data: error_document_no_metadata) + + XCTAssert(document == document2) + + XCTAssertTrue(document2.body.isError) + XCTAssertEqual(document2.body.meta, NoMetadata()) + XCTAssertNil(document2.body.data) + XCTAssertNil(document2.body.primaryResource) + XCTAssertNil(document2.body.includes) + + guard case let .errors(errors2) = document2.body else { + XCTFail("Needed body to be in errors case but it was not.") + return + } + + XCTAssertEqual(errors2.0.count, 1) + XCTAssertEqual(errors2.0, document2.body.errors) + XCTAssertEqual(errors2.0[0], .unknown) + XCTAssertEqual(errors2.meta, NoMetadata()) } func test_unknownErrorDocumentAddIncludingType() { @@ -620,6 +707,26 @@ extension DocumentTests { XCTAssertEqual(documentWithIncludes.body.includes?[Author.self], [author]) } + func test_singleSuccessDocumentNoIncludesAddIncludingType() { + // NOTE distinct from above for being Document.SuccessDocument + let author = Author(id: "1", + attributes: .none, + relationships: .none, + meta: .none, + links: .none) + + let document = decoded(type: Document.SuccessDocument.self, + data: single_document_no_includes) + + let documentWithIncludes = document.including(Includes>(values: [.init(author)])) + + XCTAssertEqual(document.body.errors, documentWithIncludes.body.errors) + XCTAssertEqual(document.body.meta, documentWithIncludes.body.meta) + XCTAssertEqual(document.body.links, documentWithIncludes.body.links) + XCTAssertEqual(document.body.includes, Includes.none) + XCTAssertEqual(documentWithIncludes.body.includes?[Author.self], [author]) + } + func test_singleDocumentNoIncludesWithAPIDescription() { let document = decoded(type: Document, NoMetadata, NoLinks, NoIncludes, TestAPIDescription, UnknownJSONAPIError>.self, data: single_document_no_includes_with_api_description) @@ -848,6 +955,31 @@ extension DocumentTests { XCTAssertEqual(documentWithIncludes.body.includes?[Author.self], [existingAuthor, newAuthor]) } + func test_singleSuccessDocumentSomeIncludesAddIncludes() { + // NOTE distinct from above for being Document.SuccessDocument + let existingAuthor = Author(id: "33", + attributes: .none, + relationships: .none, + meta: .none, + links: .none) + + let newAuthor = Author(id: "1", + attributes: .none, + relationships: .none, + meta: .none, + links: .none) + + let document = decoded(type: Document, NoMetadata, NoLinks, Include1, NoAPIDescription, UnknownJSONAPIError>.SuccessDocument.self, + data: single_document_some_includes) + + let documentWithIncludes = document.including(.init(values: [.init(newAuthor)])) + + XCTAssertEqual(document.body.errors, documentWithIncludes.body.errors) + XCTAssertEqual(document.body.meta, documentWithIncludes.body.meta) + XCTAssertEqual(document.body.links, documentWithIncludes.body.links) + XCTAssertEqual(documentWithIncludes.body.includes?[Author.self], [existingAuthor, newAuthor]) + } + func test_singleDocumentSomeIncludesWithAPIDescription() { let document = decoded(type: Document, NoMetadata, NoLinks, Include1, TestAPIDescription, UnknownJSONAPIError>.self, data: single_document_some_includes_with_api_description) From 57f85476c0fd7720d5a21ab32d235231714762b8 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Sun, 20 Oct 2019 22:29:22 -0700 Subject: [PATCH 085/235] Bump podspec version --- JSONAPI.podspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/JSONAPI.podspec b/JSONAPI.podspec index 1d3e68f..c6e5573 100644 --- a/JSONAPI.podspec +++ b/JSONAPI.podspec @@ -16,7 +16,7 @@ Pod::Spec.new do |spec| # spec.name = "MP-JSONAPI" - spec.version = "2.4.0" + spec.version = "2.5.0" spec.summary = "Swift Codable JSON API framework." # This description is used to generate tags and improve search results. From eb8562037968800396d50d53e01597f8195f7783 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Sun, 20 Oct 2019 22:30:15 -0700 Subject: [PATCH 086/235] regenerate swift test manifest --- Tests/JSONAPITests/XCTestManifests.swift | 49 ++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/Tests/JSONAPITests/XCTestManifests.swift b/Tests/JSONAPITests/XCTestManifests.swift index 4cde925..cd6323d 100644 --- a/Tests/JSONAPITests/XCTestManifests.swift +++ b/Tests/JSONAPITests/XCTestManifests.swift @@ -42,6 +42,17 @@ extension Attribute_FunctorTests { ] } +extension BasicJSONAPIErrorTests { + // DO NOT MODIFY: This is autogenerated, use: + // `swift test --generate-linuxmain` + // to regenerate. + static let __allTests__BasicJSONAPIErrorTests = [ + ("test_decodeAFewExamples", test_decodeAFewExamples), + ("test_definedFields", test_definedFields), + ("test_initAndEquality", test_initAndEquality), + ] +} + extension ComputedPropertiesTests { // DO NOT MODIFY: This is autogenerated, use: // `swift test --generate-linuxmain` @@ -145,6 +156,8 @@ extension DocumentTests { ("test_singleDocumentSomeIncludesWithMetadata_encode", test_singleDocumentSomeIncludesWithMetadata_encode), ("test_singleDocumentSomeIncludesWithMetadataWithAPIDescription", test_singleDocumentSomeIncludesWithMetadataWithAPIDescription), ("test_singleDocumentSomeIncludesWithMetadataWithAPIDescription_encode", test_singleDocumentSomeIncludesWithMetadataWithAPIDescription_encode), + ("test_singleSuccessDocumentNoIncludesAddIncludingType", test_singleSuccessDocumentNoIncludesAddIncludingType), + ("test_singleSuccessDocumentSomeIncludesAddIncludes", test_singleSuccessDocumentSomeIncludesAddIncludes), ("test_sparseIncludeFullPrimaryResource", test_sparseIncludeFullPrimaryResource), ("test_sparseIncludeSparsePrimaryResource", test_sparseIncludeSparsePrimaryResource), ("test_sparsePrimaryResource", test_sparsePrimaryResource), @@ -196,6 +209,21 @@ extension EmptyObjectDecoderTests { ] } +extension GenericJSONAPIErrorTests { + // DO NOT MODIFY: This is autogenerated, use: + // `swift test --generate-linuxmain` + // to regenerate. + static let __allTests__GenericJSONAPIErrorTests = [ + ("test_decodeKnown", test_decodeKnown), + ("test_decodeUnknown", test_decodeUnknown), + ("test_definedFields", test_definedFields), + ("test_encode", test_encode), + ("test_encodeUnknown", test_encodeUnknown), + ("test_initAndEquality", test_initAndEquality), + ("test_payloadAccess", test_payloadAccess), + ] +} + extension IncludedTests { // DO NOT MODIFY: This is autogenerated, use: // `swift test --generate-linuxmain` @@ -205,6 +233,8 @@ extension IncludedTests { ("test_ComboSparseAndFullIncludeTypes", test_ComboSparseAndFullIncludeTypes), ("test_EightDifferentIncludes", test_EightDifferentIncludes), ("test_EightDifferentIncludes_encode", test_EightDifferentIncludes_encode), + ("test_ElevenDifferentIncludes", test_ElevenDifferentIncludes), + ("test_ElevenDifferentIncludes_encode", test_ElevenDifferentIncludes_encode), ("test_FiveDifferentIncludes", test_FiveDifferentIncludes), ("test_FiveDifferentIncludes_encode", test_FiveDifferentIncludes_encode), ("test_FourDifferentIncludes", test_FourDifferentIncludes), @@ -323,6 +353,22 @@ extension ResourceBodyTests { ] } +extension ResourceObjectReplacingTests { + // DO NOT MODIFY: This is autogenerated, use: + // `swift test --generate-linuxmain` + // to regenerate. + static let __allTests__ResourceObjectReplacingTests = [ + ("test_replaceImmutableAttributes", test_replaceImmutableAttributes), + ("test_replaceImmutableRelationships", test_replaceImmutableRelationships), + ("test_replaceMutableAttributes", test_replaceMutableAttributes), + ("test_replaceMutableRelationships", test_replaceMutableRelationships), + ("test_tapImmutableAttributes", test_tapImmutableAttributes), + ("test_tapImmutableRelationships", test_tapImmutableRelationships), + ("test_tapMutableAttributes", test_tapMutableAttributes), + ("test_tapMutableRelationships", test_tapMutableRelationships), + ] +} + extension ResourceObjectTests { // DO NOT MODIFY: This is autogenerated, use: // `swift test --generate-linuxmain` @@ -442,16 +488,19 @@ public func __allTests() -> [XCTestCaseEntry] { testCase(APIDescriptionTests.__allTests__APIDescriptionTests), testCase(AttributeTests.__allTests__AttributeTests), testCase(Attribute_FunctorTests.__allTests__Attribute_FunctorTests), + testCase(BasicJSONAPIErrorTests.__allTests__BasicJSONAPIErrorTests), testCase(ComputedPropertiesTests.__allTests__ComputedPropertiesTests), testCase(CustomAttributesTests.__allTests__CustomAttributesTests), testCase(DocumentTests.__allTests__DocumentTests), testCase(EmptyObjectDecoderTests.__allTests__EmptyObjectDecoderTests), + testCase(GenericJSONAPIErrorTests.__allTests__GenericJSONAPIErrorTests), testCase(IncludedTests.__allTests__IncludedTests), testCase(LinksTests.__allTests__LinksTests), testCase(NonJSONAPIRelatableTests.__allTests__NonJSONAPIRelatableTests), testCase(PolyProxyTests.__allTests__PolyProxyTests), testCase(RelationshipTests.__allTests__RelationshipTests), testCase(ResourceBodyTests.__allTests__ResourceBodyTests), + testCase(ResourceObjectReplacingTests.__allTests__ResourceObjectReplacingTests), testCase(ResourceObjectTests.__allTests__ResourceObjectTests), testCase(SparseFieldEncoderTests.__allTests__SparseFieldEncoderTests), testCase(SparseFieldsetTests.__allTests__SparseFieldsetTests), From 1a15ab6f9de9f75c4eab398f19701878ca1e70f1 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Sun, 27 Oct 2019 16:21:41 -0700 Subject: [PATCH 087/235] Fix return type of success document after adding includes --- Sources/JSONAPI/Document/Document.swift | 4 ++-- Tests/JSONAPITests/Document/DocumentTests.swift | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Sources/JSONAPI/Document/Document.swift b/Sources/JSONAPI/Document/Document.swift index 2cbf4e7..5c8160b 100644 --- a/Sources/JSONAPI/Document/Document.swift +++ b/Sources/JSONAPI/Document/Document.swift @@ -509,7 +509,7 @@ extension Document.SuccessDocument: Decodable, JSONAPIDocument extension Document.SuccessDocument where IncludeType == NoIncludes { /// Create a new Document with the given includes. - public func including(_ includes: Includes) -> Document { + public func including(_ includes: Includes) -> Document.SuccessDocument { // Note that if IncludeType is NoIncludes, then we allow anything // to be included, but if IncludeType already specifies a type // of thing to be expected then we lock that down. @@ -531,7 +531,7 @@ extension Document.SuccessDocument where IncludeType == NoIncludes { extension Document.SuccessDocument where IncludeType: _Poly1 { /// Create a new Document adding the given includes. This does not /// remove existing includes; it is additive. - public func including(_ includes: Includes) -> Document { + public func including(_ includes: Includes) -> Document.SuccessDocument { // Note that if IncludeType is NoIncludes, then we allow anything // to be included, but if IncludeType already specifies a type // of thing to be expected then we lock that down. diff --git a/Tests/JSONAPITests/Document/DocumentTests.swift b/Tests/JSONAPITests/Document/DocumentTests.swift index 48ed1c8..bbf36e9 100644 --- a/Tests/JSONAPITests/Document/DocumentTests.swift +++ b/Tests/JSONAPITests/Document/DocumentTests.swift @@ -720,6 +720,7 @@ extension DocumentTests { let documentWithIncludes = document.including(Includes>(values: [.init(author)])) + XCTAssert(type(of: documentWithIncludes) == Document, NoAPIDescription, UnknownJSONAPIError>.SuccessDocument.self) XCTAssertEqual(document.body.errors, documentWithIncludes.body.errors) XCTAssertEqual(document.body.meta, documentWithIncludes.body.meta) XCTAssertEqual(document.body.links, documentWithIncludes.body.links) From f7bfa91ccca76231fb097c87cc63ba489c8de8ac Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Tue, 5 Nov 2019 19:03:51 -0800 Subject: [PATCH 088/235] lots of comparison code, a few small breaking changes --- Package.resolved | 4 +- Package.swift | 2 +- Sources/JSONAPI/Document/Document.swift | 21 +- Sources/JSONAPI/Document/Includes.swift | 6 +- Sources/JSONAPI/Error/BasicJSONAPIError.swift | 6 +- .../JSONAPI/Error/GenericJSONAPIError.swift | 11 +- .../Resource Object/ResourceObject.swift | 6 + .../Comparisons/ArrayCompare.swift | 78 ++++++ .../Comparisons/AttributesCompare.swift | 78 ++++++ .../Comparisons/Comparison.swift | 68 +++++ .../Comparisons/DocumentCompare.swift | 180 +++++++++++++ .../Comparisons/DocumentDataCompare.swift | 167 ++++++++++++ .../Comparisons/IncludesCompare.swift | 66 +++++ .../Comparisons/RelationshipsCompare.swift | 105 ++++++++ .../Comparisons/ResourceObjectCompare.swift | 71 ++++++ Sources/JSONAPITesting/Optional+ZipWith.swift | 12 + .../Comparisons/AttributesCompareTests.swift | 74 ++++++ .../Comparisons/DocumentCompareTests.swift | 170 +++++++++++++ .../Comparisons/IncludesCompareTests.swift | 239 ++++++++++++++++++ .../RelationshipsCompareTests.swift | 14 + .../ResourceObjectCompareTests.swift | 68 +++++ 21 files changed, 1431 insertions(+), 15 deletions(-) create mode 100644 Sources/JSONAPITesting/Comparisons/ArrayCompare.swift create mode 100644 Sources/JSONAPITesting/Comparisons/AttributesCompare.swift create mode 100644 Sources/JSONAPITesting/Comparisons/Comparison.swift create mode 100644 Sources/JSONAPITesting/Comparisons/DocumentCompare.swift create mode 100644 Sources/JSONAPITesting/Comparisons/DocumentDataCompare.swift create mode 100644 Sources/JSONAPITesting/Comparisons/IncludesCompare.swift create mode 100644 Sources/JSONAPITesting/Comparisons/RelationshipsCompare.swift create mode 100644 Sources/JSONAPITesting/Comparisons/ResourceObjectCompare.swift create mode 100644 Sources/JSONAPITesting/Optional+ZipWith.swift create mode 100644 Tests/JSONAPITestingTests/Comparisons/AttributesCompareTests.swift create mode 100644 Tests/JSONAPITestingTests/Comparisons/DocumentCompareTests.swift create mode 100644 Tests/JSONAPITestingTests/Comparisons/IncludesCompareTests.swift create mode 100644 Tests/JSONAPITestingTests/Comparisons/RelationshipsCompareTests.swift create mode 100644 Tests/JSONAPITestingTests/Comparisons/ResourceObjectCompareTests.swift diff --git a/Package.resolved b/Package.resolved index b1970b8..cc81a53 100644 --- a/Package.resolved +++ b/Package.resolved @@ -6,8 +6,8 @@ "repositoryURL": "https://github.com/mattpolzin/Poly.git", "state": { "branch": null, - "revision": "b24fd3b41bf3126d4c6dede3708135182172af60", - "version": "2.2.0" + "revision": "18cd995be5c28c4dfdc1464e54ee0efb03e215bf", + "version": "2.3.0" } } ] diff --git a/Package.swift b/Package.swift index 2093673..5b780cb 100644 --- a/Package.swift +++ b/Package.swift @@ -18,7 +18,7 @@ let package = Package( targets: ["JSONAPITesting"]) ], 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.0")), ], targets: [ .target( diff --git a/Sources/JSONAPI/Document/Document.swift b/Sources/JSONAPI/Document/Document.swift index 5c8160b..00d1ce0 100644 --- a/Sources/JSONAPI/Document/Document.swift +++ b/Sources/JSONAPI/Document/Document.swift @@ -20,7 +20,16 @@ public protocol EncodableJSONAPIDocument: Equatable, Encodable { typealias Body = Document.Body + /// The Body of the Document. This body is either one or more errors + /// with links and metadata attempted to parse but not guaranteed or + /// it is a successful data struct containing all the primary and + /// included resources, the metadata, and the links that this + /// document type specifies. var body: Body { get } + + /// The JSON API Spec calls this the JSON:API Object. It contains version + /// and metadata information about the API itself. + var apiDescription: APIDescription { get } } /// A `JSONAPIDocument` supports encoding and decoding of a JSON:API @@ -30,6 +39,7 @@ public protocol JSONAPIDocument: EncodableJSONAPIDocument, Decodable where Prima /// A JSON API Document represents the entire body /// of a JSON API request or the entire body of /// a JSON API response. +/// /// Note that this type uses Camel case. If your /// API uses snake case, you will want to use /// a conversion such as the one offerred by the @@ -37,15 +47,10 @@ public protocol JSONAPIDocument: EncodableJSONAPIDocument, Decodable where Prima public struct Document: EncodableJSONAPIDocument { public typealias Include = IncludeType - /// The JSON API Spec calls this the JSON:API Object. It contains version - /// and metadata information about the API itself. + // See `EncodableJSONAPIDocument` for documentation. public let apiDescription: APIDescription - /// The Body of the Document. This body is either one or more errors - /// with links and metadata attempted to parse but not guaranteed or - /// it is a successful data struct containing all the primary and - /// included resources, the metadata, and the links that this - /// document type specifies. + // See `EncodableJSONAPIDocument` for documentation. public let body: Body public enum Body: Equatable { @@ -423,6 +428,7 @@ extension Document { @dynamicMemberLookup public struct ErrorDocument: EncodableJSONAPIDocument { public var body: Document.Body { return document.body } + public var apiDescription: APIDescription { return document.apiDescription } private let document: Document @@ -450,6 +456,7 @@ extension Document { @dynamicMemberLookup public struct SuccessDocument: EncodableJSONAPIDocument { public var body: Document.Body { return document.body } + public var apiDescription: APIDescription { return document.apiDescription } private let document: Document diff --git a/Sources/JSONAPI/Document/Includes.swift b/Sources/JSONAPI/Document/Includes.swift index b6d93e0..d74171f 100644 --- a/Sources/JSONAPI/Document/Includes.swift +++ b/Sources/JSONAPI/Document/Includes.swift @@ -14,15 +14,15 @@ public typealias Include = EncodableJSONPoly /// /// If you have /// -/// `let includes: Includes> = ...` +/// let includes: Includes> = ... /// /// then you can access all `Thing1` included resources with /// -/// `let includedThings = includes[Thing1.self]` +/// let includedThings = includes[Thing1.self] public struct Includes: Encodable, Equatable { public static var none: Includes { return .init(values: []) } - let values: [I] + public let values: [I] public init(values: [I]) { self.values = values diff --git a/Sources/JSONAPI/Error/BasicJSONAPIError.swift b/Sources/JSONAPI/Error/BasicJSONAPIError.swift index 28795b9..cdabc07 100644 --- a/Sources/JSONAPI/Error/BasicJSONAPIError.swift +++ b/Sources/JSONAPI/Error/BasicJSONAPIError.swift @@ -6,7 +6,7 @@ // /// Most of the JSON:API Spec defined Error fields. -public struct BasicJSONAPIErrorPayload: Codable, Equatable, ErrorDictType { +public struct BasicJSONAPIErrorPayload: Codable, Equatable, ErrorDictType, CustomStringConvertible { /// a unique identifier for this particular occurrence of the problem public let id: IdType? // public let links: Links? // we skip this for now to avoid adding complexity to using this basic type. @@ -61,6 +61,10 @@ public struct BasicJSONAPIErrorPayload: Codable, Eq ].compactMap { $0 } return Dictionary(uniqueKeysWithValues: keysAndValues) } + + public var description: String { + return definedFields.map { "\($0.key): \($0.value)" }.sorted().joined(separator: ", ") + } } /// `BasicJSONAPIError` optionally decodes many possible fields diff --git a/Sources/JSONAPI/Error/GenericJSONAPIError.swift b/Sources/JSONAPI/Error/GenericJSONAPIError.swift index 09383e7..473caac 100644 --- a/Sources/JSONAPI/Error/GenericJSONAPIError.swift +++ b/Sources/JSONAPI/Error/GenericJSONAPIError.swift @@ -8,7 +8,7 @@ /// `GenericJSONAPIError` can be used to specify whatever error /// payload you expect to need to parse in responses and handle any /// other payload structure as `.unknownError`. -public enum GenericJSONAPIError: JSONAPIError { +public enum GenericJSONAPIError: JSONAPIError, CustomStringConvertible { case unknownError case error(ErrorPayload) @@ -35,6 +35,15 @@ public enum GenericJSONAPIError: JSONAPIError public static var unknown: Self { return .unknownError } + + public var description: String { + switch self { + case .unknownError: + return "unknown error" + case .error(let payload): + return String(describing: payload) + } + } } public extension GenericJSONAPIError { diff --git a/Sources/JSONAPI/Resource/Resource Object/ResourceObject.swift b/Sources/JSONAPI/Resource/Resource Object/ResourceObject.swift index a62d271..4c59e88 100644 --- a/Sources/JSONAPI/Resource/Resource Object/ResourceObject.swift +++ b/Sources/JSONAPI/Resource/Resource Object/ResourceObject.swift @@ -102,6 +102,12 @@ extension ResourceObjectProxy { public protocol ResourceObjectType: ResourceObjectProxy, PrimaryResource where Description: ResourceObjectDescription { associatedtype Meta: JSONAPI.Meta associatedtype Links: JSONAPI.Links + + /// Any additional metadata packaged with the entity. + var meta: Meta { get } + + /// Links related to the entity. + var links: Links { get } } public protocol IdentifiableResourceObjectType: ResourceObjectType, Relatable where EntityRawIdType: JSONAPI.RawIdType {} diff --git a/Sources/JSONAPITesting/Comparisons/ArrayCompare.swift b/Sources/JSONAPITesting/Comparisons/ArrayCompare.swift new file mode 100644 index 0000000..d14ced7 --- /dev/null +++ b/Sources/JSONAPITesting/Comparisons/ArrayCompare.swift @@ -0,0 +1,78 @@ +// +// File.swift +// +// +// Created by Mathew Polzin on 11/5/19. +// + +import JSONAPI + +public enum ArrayElementComparison: Equatable, CustomStringConvertible { + case same + case missing + case differentTypes(String, String) + case differentValues(String, String) + case prebuilt(String) + + public init(sameTypeComparison: Comparison) { + switch sameTypeComparison { + case .same: + self = .same + case .different(let one, let two): + self = .differentValues(one, two) + case .prebuilt(let str): + self = .prebuilt(str) + } + } + + public init(resourceObjectComparison: ResourceObjectComparison) { + guard !resourceObjectComparison.isSame else { + self = .same + return + } + + self = .prebuilt( + resourceObjectComparison + .differences + .sorted { $0.key < $1.key } + .map { "\($0.key): \($0.value)" } + .joined(separator: ", ") + ) + } + + public var description: String { + switch self { + case .same: + return "same" + case .missing: + return "missing" + case .differentTypes(let one, let two), + .differentValues(let one, let two): + return "\(one) ≠ \(two)" + case .prebuilt(let description): + return description + } + } + + public var rawValue: String { description } +} + +extension Array { + func compare(to other: Self, using compare: (Element, Element) -> ArrayElementComparison) -> [ArrayElementComparison] { + let isSelfLonger = count >= other.count + + let longer = isSelfLonger ? self : other + let shorter = isSelfLonger ? other : self + + return longer.indices.map { idx in + guard shorter.indices.contains(idx) else { + return .missing + } + + let this = longer[idx] + let other = shorter[idx] + + return compare(this, other) + } + } +} diff --git a/Sources/JSONAPITesting/Comparisons/AttributesCompare.swift b/Sources/JSONAPITesting/Comparisons/AttributesCompare.swift new file mode 100644 index 0000000..541c52e --- /dev/null +++ b/Sources/JSONAPITesting/Comparisons/AttributesCompare.swift @@ -0,0 +1,78 @@ +// +// File.swift +// +// +// Created by Mathew Polzin on 11/3/19. +// + +import JSONAPI + +extension Attributes { + public func compare(to other: Self) -> [String: Comparison] { + let mirror1 = Mirror(reflecting: self) + let mirror2 = Mirror(reflecting: other) + + var comparisons = [String: Comparison]() + + for child in mirror1.children { + guard let childLabel = child.label else { continue } + + let childDescription = attributeDescription(of: child.value) + + guard let otherChild = mirror2.children.first(where: { $0.label == childLabel }) else { + comparisons[childLabel] = .different(childDescription, "missing") + continue + } + + if (attributesEqual(child.value, otherChild.value)) { + comparisons[childLabel] = .same + } else { + let otherChildDescription = attributeDescription(of: otherChild.value) + + comparisons[childLabel] = .different(childDescription, otherChildDescription) + } + } + + return comparisons + } +} + +fileprivate func attributesEqual(_ one: Any, _ two: Any) -> Bool { + guard let attr = one as? AbstractAttribute else { + return false + } + + return attr.equals(two) +} + +fileprivate func attributeDescription(of thing: Any) -> String { + return (thing as? AbstractAttribute)?.abstractDescription ?? String(describing: thing) +} + +protocol AbstractAttribute { + var abstractDescription: String { get } + + func equals(_ other: Any) -> Bool +} + +extension Attribute: AbstractAttribute { + var abstractDescription: String { String(describing: value) } + + func equals(_ other: Any) -> Bool { + guard let attributeB = other as? Self else { + return false + } + return abstractDescription == attributeB.abstractDescription + } +} + +extension TransformedAttribute: AbstractAttribute { + var abstractDescription: String { String(describing: value) } + + func equals(_ other: Any) -> Bool { + guard let attributeB = other as? Self else { + return false + } + return abstractDescription == attributeB.abstractDescription + } +} diff --git a/Sources/JSONAPITesting/Comparisons/Comparison.swift b/Sources/JSONAPITesting/Comparisons/Comparison.swift new file mode 100644 index 0000000..a6b97ee --- /dev/null +++ b/Sources/JSONAPITesting/Comparisons/Comparison.swift @@ -0,0 +1,68 @@ +// +// Comparison.swift +// +// +// Created by Mathew Polzin on 11/3/19. +// + +public enum Comparison: Equatable, CustomStringConvertible { + case same + case different(String, String) + case prebuilt(String) + + init(_ one: T, _ two: T) { + guard one == two else { + self = .different(String(describing: one), String(describing: two)) + return + } + self = .same + } + + init(reducing other: ArrayElementComparison) { + switch other { + case .same: + self = .same + case .differentTypes(let one, let two), + .differentValues(let one, let two): + self = .different(one, two) + case .missing: + self = .different("array length 1", "array length 2") + case .prebuilt(let str): + self = .prebuilt(str) + } + } + + public var description: String { + switch self { + case .same: + return "same" + case .different(let one, let two): + return "\(one) ≠ \(two)" + case .prebuilt(let str): + return str + } + } + + public var rawValue: String { description } + + public var isSame: Bool { self == .same } +} + +public typealias NamedDifferences = [String: String] + +public protocol PropertyComparable: CustomStringConvertible { + var differences: NamedDifferences { get } +} + +extension PropertyComparable { + public var description: String { + return differences + .map { "(\($0): \($1))" } + .sorted() + .joined(separator: ", ") + } + + public var rawValue: String { description } + + public var isSame: Bool { differences.isEmpty } +} diff --git a/Sources/JSONAPITesting/Comparisons/DocumentCompare.swift b/Sources/JSONAPITesting/Comparisons/DocumentCompare.swift new file mode 100644 index 0000000..15c417f --- /dev/null +++ b/Sources/JSONAPITesting/Comparisons/DocumentCompare.swift @@ -0,0 +1,180 @@ +// +// DocumentCompare.swift +// JSONAPITesting +// +// Created by Mathew Polzin on 11/4/19. +// + +import JSONAPI + +public struct DocumentComparison: Equatable, PropertyComparable { + public let apiDescription: Comparison + public let body: BodyComparison + + init(apiDescription: Comparison, body: BodyComparison) { + self.apiDescription = apiDescription + self.body = body + } + + public var differences: NamedDifferences { + return Dictionary( + [ + apiDescription != .same ? ("API Description", apiDescription.rawValue) : nil, + body != .same ? ("Body", body.rawValue) : nil + ].compactMap { $0 }, + uniquingKeysWith: { $1 } + ) + } +} + +public enum BodyComparison: Equatable, CustomStringConvertible { + case same + case dataErrorMismatch(errorOnLeft: Bool) + case differentErrors(ErrorComparison) + case differentData(DocumentDataComparison) + + public typealias ErrorComparison = [Comparison] + + static func compare(errors errors1: [E], _ meta1: M?, _ links1: L?, with errors2: [E], _ meta2: M?, _ links2: L?) -> ErrorComparison { + return errors1.compare( + to: errors2, + using: { error1, error2 in + guard error1 != error2 else { + return .same + } + + return .differentValues( + String(describing: error1), + String(describing: error2) + ) + } + ).map(Comparison.init) + [ + Comparison(meta1, meta2), + Comparison(links1, links2) + ] + } + + public var description: String { + switch self { + case .same: + return "same" + case .dataErrorMismatch(errorOnLeft: let errorOnLeft): + let errorString = "error response" + let dataString = "data response" + let left = errorOnLeft ? errorString : dataString + let right = errorOnLeft ? dataString : errorString + + return "\(left) ≠ \(right)" + case .differentErrors(let comparisons): + return comparisons + .filter { !$0.isSame } + .map { $0.rawValue } + .sorted() + .joined(separator: ", ") + case .differentData(let comparison): + return comparison.rawValue + } + } + + public var rawValue: String { description } +} + +extension Document { + public func compare(to other: Self) -> DocumentComparison where PrimaryResourceBody == SingleResourceBody, T: ResourceObjectType { + return DocumentComparison( + apiDescription: Comparison( + String(describing: apiDescription), + String(describing: other.apiDescription) + ), + body: body.compare(to: other.body) + ) + } + + public func compare(to other: Self) -> DocumentComparison where PrimaryResourceBody == SingleResourceBody, T: ResourceObjectType { + return DocumentComparison( + apiDescription: Comparison( + String(describing: apiDescription), + String(describing: other.apiDescription) + ), + body: body.compare(to: other.body) + ) + } + + public func compare(to other: Self) -> DocumentComparison where PrimaryResourceBody == ManyResourceBody, T: ResourceObjectType { + return DocumentComparison( + apiDescription: Comparison( + String(describing: apiDescription), + String(describing: other.apiDescription) + ), + body: body.compare(to: other.body) + ) + } +} + +extension Document.Body { + public func compare(to other: Self) -> BodyComparison where T: ResourceObjectType, PrimaryResourceBody == SingleResourceBody { + guard self != other else { + return .same + } + + switch (self, other) { + case (.errors(let errors1), .errors(let errors2)): + return .differentErrors(BodyComparison.compare(errors: errors1.0, + errors1.meta, + errors1.links, + with: errors2.0, + errors2.meta, + errors2.links)) + case (.errors, .data): + return .dataErrorMismatch(errorOnLeft: true) + case (.data, .errors): + return .dataErrorMismatch(errorOnLeft: false) + case (.data(let data1), .data(let data2)): + return .differentData(data1.compare(to: data2)) + } + } + + public func compare(to other: Self) -> BodyComparison where T: ResourceObjectType, PrimaryResourceBody == SingleResourceBody { + guard self != other else { + return .same + } + + switch (self, other) { + case (.errors(let errors1), .errors(let errors2)): + return .differentErrors(BodyComparison.compare(errors: errors1.0, + errors1.meta, + errors1.links, + with: errors2.0, + errors2.meta, + errors2.links)) + case (.errors, .data): + return .dataErrorMismatch(errorOnLeft: true) + case (.data, .errors): + return .dataErrorMismatch(errorOnLeft: false) + case (.data(let data1), .data(let data2)): + return .differentData(data1.compare(to: data2)) + } + } + + public func compare(to other: Self) -> BodyComparison where T: ResourceObjectType, PrimaryResourceBody == ManyResourceBody { + guard self != other else { + return .same + } + + switch (self, other) { + case (.errors(let errors1), .errors(let errors2)): + return .differentErrors(BodyComparison.compare(errors: errors1.0, + errors1.meta, + errors1.links, + with: errors2.0, + errors2.meta, + errors2.links)) + case (.errors, .data): + return .dataErrorMismatch(errorOnLeft: true) + case (.data, .errors): + return .dataErrorMismatch(errorOnLeft: false) + case (.data(let data1), .data(let data2)): + return .differentData(data1.compare(to: data2)) + } + } +} diff --git a/Sources/JSONAPITesting/Comparisons/DocumentDataCompare.swift b/Sources/JSONAPITesting/Comparisons/DocumentDataCompare.swift new file mode 100644 index 0000000..392bab0 --- /dev/null +++ b/Sources/JSONAPITesting/Comparisons/DocumentDataCompare.swift @@ -0,0 +1,167 @@ +// +// DocumentDataCompare.swift +// +// +// Created by Mathew Polzin on 11/5/19. +// + +import JSONAPI + +public struct DocumentDataComparison: Equatable, PropertyComparable { + public let primary: PrimaryResourceBodyComparison + public let includes: IncludesComparison + public let meta: Comparison + public let links: Comparison + + init(primary: PrimaryResourceBodyComparison, includes: IncludesComparison, meta: Comparison, links: Comparison) { + self.primary = primary + self.includes = includes + self.meta = meta + self.links = links + } + + public var differences: NamedDifferences { + return Dictionary( + [ + !primary.isSame ? ("Primary Resource", primary.rawValue) : nil, + !includes.isSame ? ("Includes", includes.rawValue) : nil, + !meta.isSame ? ("Meta", meta.rawValue) : nil, + !links.isSame ? ("Links", links.rawValue) : nil + ].compactMap { $0 }, + uniquingKeysWith: { $1 } + ) + } +} + +extension Document.Body.Data { + public func compare(to other: Self) -> DocumentDataComparison where T: ResourceObjectType, PrimaryResourceBody == SingleResourceBody { + return .init( + primary: primary.compare(to: other.primary), + includes: includes.compare(to: other.includes), + meta: Comparison(meta, other.meta), + links: Comparison(links, other.links) + ) + } + + public func compare(to other: Self) -> DocumentDataComparison where T: ResourceObjectType, PrimaryResourceBody == SingleResourceBody { + return .init( + primary: primary.compare(to: other.primary), + includes: includes.compare(to: other.includes), + meta: Comparison(meta, other.meta), + links: Comparison(links, other.links) + ) + } + + public func compare(to other: Self) -> DocumentDataComparison where T: ResourceObjectType, PrimaryResourceBody == ManyResourceBody { + return .init( + primary: primary.compare(to: other.primary), + includes: includes.compare(to: other.includes), + meta: Comparison(meta, other.meta), + links: Comparison(links, other.links) + ) + } +} + +public enum PrimaryResourceBodyComparison: Equatable, CustomStringConvertible { + case single(ResourceObjectComparison) + case many(ManyResourceObjectComparison) + case other(Comparison) + + public var isSame: Bool { + switch self { + case .other(let comparison): + return comparison == .same + case .single(let comparison): + return comparison.isSame + case .many(let comparison): + return comparison.isSame + } + } + + public var description: String { + switch self { + case .other(let comparison): + return comparison.rawValue + case .single(let comparison): + return comparison.rawValue + case .many(let comparison): + return comparison.rawValue + } + } + + public var rawValue: String { return description } +} + +public struct ManyResourceObjectComparison: Equatable, PropertyComparable { + public let comparisons: [ArrayElementComparison] + + public init(_ comparisons: [ArrayElementComparison]) { + self.comparisons = comparisons + } + + public var differences: NamedDifferences { + return comparisons + .enumerated() + .filter { $0.element != .same } + .reduce(into: [String: String]()) { hash, next in + hash["resource \(next.offset + 1)"] = next.element.rawValue + } + } +} + +extension SingleResourceBody where Entity: ResourceObjectType { + public func compare(to other: Self) -> PrimaryResourceBodyComparison { + return .single(.init(value, other.value)) + } +} + +public protocol _OptionalResourceObjectType { + associatedtype Wrapped: ResourceObjectType + + var maybeValue: Wrapped? { get } +} + +extension Optional: _OptionalResourceObjectType where Wrapped: ResourceObjectType { + public var maybeValue: Wrapped? { + switch self { + case .none: + return nil + case .some(let value): + return value + } + } +} + +extension SingleResourceBody where Entity: _OptionalResourceObjectType { + public func compare(to other: Self) -> PrimaryResourceBodyComparison { + guard let one = value.maybeValue, + let two = other.value.maybeValue else { + return .other(Comparison(value, other.value)) + } + return .single(.init(one, two)) + } +} + +extension ManyResourceBody where Entity: ResourceObjectType { + public func compare(to other: Self) -> PrimaryResourceBodyComparison { + return .many(.init(values.compare(to: other.values, using: { r1, r2 in + let r1AsResource = r1 as? AbstractResourceObjectType + + let maybeComparison = r1AsResource + .flatMap { resource in + try? ArrayElementComparison( + resourceObjectComparison: resource.abstractCompare(to: r2) + ) + } + + guard let comparison = maybeComparison else { + return .differentValues( + String(describing: r1), + String(describing: r2) + ) + } + + return comparison + }))) + } +} diff --git a/Sources/JSONAPITesting/Comparisons/IncludesCompare.swift b/Sources/JSONAPITesting/Comparisons/IncludesCompare.swift new file mode 100644 index 0000000..c638628 --- /dev/null +++ b/Sources/JSONAPITesting/Comparisons/IncludesCompare.swift @@ -0,0 +1,66 @@ +// +// IncludesCompare.swift +// +// +// Created by Mathew Polzin on 11/4/19. +// + +import JSONAPI +import Poly + +public struct IncludesComparison: Equatable, PropertyComparable { + public let comparisons: [ArrayElementComparison] + + public init(_ comparisons: [ArrayElementComparison]) { + self.comparisons = comparisons + } + + public var differences: NamedDifferences { + return comparisons + .enumerated() + .filter { $0.element != .same } + .reduce(into: [String: String]()) { hash, next in + hash["include \(next.offset + 1)"] = next.element.rawValue + } + } +} + +extension Includes { + public func compare(to other: Self) -> IncludesComparison { + + return IncludesComparison( + values.compare(to: other.values) { thisInclude, otherInclude in + guard thisInclude != otherInclude else { + return .same + } + + let thisWrappedValue = thisInclude.value + let otherWrappedValue = otherInclude.value + guard type(of: thisWrappedValue) == type(of: otherWrappedValue) else { + return .differentTypes( + String(describing: type(of: thisWrappedValue)), + String(describing: type(of: otherWrappedValue)) + ) + } + + let thisAsAResource = thisWrappedValue as? AbstractResourceObjectType + + let maybeComparison = thisAsAResource + .flatMap { resource in + try? ArrayElementComparison( + resourceObjectComparison: resource.abstractCompare(to: otherWrappedValue) + ) + } + + guard let comparison = maybeComparison else { + return .differentValues( + String(describing: thisWrappedValue), + String(describing: otherWrappedValue) + ) + } + + return comparison + } + ) + } +} diff --git a/Sources/JSONAPITesting/Comparisons/RelationshipsCompare.swift b/Sources/JSONAPITesting/Comparisons/RelationshipsCompare.swift new file mode 100644 index 0000000..c217cb9 --- /dev/null +++ b/Sources/JSONAPITesting/Comparisons/RelationshipsCompare.swift @@ -0,0 +1,105 @@ +// +// File.swift +// +// +// Created by Mathew Polzin on 11/3/19. +// + +import JSONAPI + +extension Relationships { + public func compare(to other: Self) -> [String: Comparison] { + let mirror1 = Mirror(reflecting: self) + let mirror2 = Mirror(reflecting: other) + + var comparisons = [String: Comparison]() + + for child in mirror1.children { + guard let childLabel = child.label else { continue } + + let childDescription = relationshipDescription(of: child.value) + + guard let otherChild = mirror2.children.first(where: { $0.label == childLabel }) else { + comparisons[childLabel] = .different(childDescription, "missing") + continue + } + + if (relationshipsEqual(child.value, otherChild.value)) { + comparisons[childLabel] = .same + } else { + let otherChildDescription = relationshipDescription(of: otherChild.value) + + comparisons[childLabel] = .different(childDescription, otherChildDescription) + } + } + + return comparisons + } +} + +fileprivate func relationshipsEqual(_ one: Any, _ two: Any) -> Bool { + guard let attr = one as? AbstractRelationship else { + return false + } + + return attr.equals(two) +} + +fileprivate func relationshipDescription(of thing: Any) -> String { + return (thing as? AbstractRelationship)?.abstractDescription ?? String(describing: thing) +} + +protocol AbstractRelationship { + var abstractDescription: String { get } + + func equals(_ other: Any) -> Bool +} + +extension ToOneRelationship: AbstractRelationship { + var abstractDescription: String { + if meta is NoMetadata && links is NoLinks { + return String(describing: id) + } + + return String(describing: + ( + String(describing: id), + String(describing: meta), + String(describing: links) + ) + ) + } + + func equals(_ other: Any) -> Bool { + guard let attributeB = other as? Self else { + return false + } + return abstractDescription == attributeB.abstractDescription + } +} + +extension ToManyRelationship: AbstractRelationship { + var abstractDescription: String { + + let idsString = ids.map { String.init(describing: $0.rawValue) }.joined(separator: ", ") + + if meta is NoMetadata && links is NoLinks { + return idsString + } + + return String(describing: + ( + idsString, + String(describing: meta), + String(describing: links) + ) + ) + } + + func equals(_ other: Any) -> Bool { + guard let attributeB = other as? Self else { + return false + } + return abstractDescription == attributeB.abstractDescription + } +} diff --git a/Sources/JSONAPITesting/Comparisons/ResourceObjectCompare.swift b/Sources/JSONAPITesting/Comparisons/ResourceObjectCompare.swift new file mode 100644 index 0000000..2619008 --- /dev/null +++ b/Sources/JSONAPITesting/Comparisons/ResourceObjectCompare.swift @@ -0,0 +1,71 @@ +// +// ResourceObjectCompare.swift +// +// +// Created by Mathew Polzin on 11/3/19. +// + +import JSONAPI + +public struct ResourceObjectComparison: Equatable, PropertyComparable { + public typealias ComparisonHash = [String: Comparison] + + public let id: Comparison + public let attributes: ComparisonHash + public let relationships: ComparisonHash + public let meta: Comparison + public let links: Comparison + + public init(_ one: T, _ two: T) { + id = Comparison(one.id.rawValue, two.id.rawValue) + attributes = one.attributes.compare(to: two.attributes) + relationships = one.relationships.compare(to: two.relationships) + meta = Comparison(one.meta, two.meta) + links = Comparison(one.links, two.links) + } + + public var differences: NamedDifferences { + return attributes.reduce(into: ComparisonHash()) { hash, next in + hash["'\(next.key)' attribute"] = next.value + } + .merging( + relationships.reduce(into: ComparisonHash()) { hash, next in + hash["'\(next.key)' relationship"] = next.value + }, + uniquingKeysWith: { $1 } + ) + .merging( + [ + "id": id, + "meta": meta, + "links": links + ], + uniquingKeysWith: { $1 } + ) + .filter { $1 != .same } + .mapValues { $0.rawValue } + } +} + +extension ResourceObjectType { + public func compare(to other: Self) -> ResourceObjectComparison { + return ResourceObjectComparison(self, other) + } +} + +protocol AbstractResourceObjectType { + func abstractCompare(to other: Any) throws -> ResourceObjectComparison +} + +enum AbstractCompareError: Swift.Error { + case typeMismatch +} + +extension ResourceObject: AbstractResourceObjectType { + func abstractCompare(to other: Any) throws -> ResourceObjectComparison { + guard let otherResource = other as? Self else { + throw AbstractCompareError.typeMismatch + } + return self.compare(to: otherResource) + } +} diff --git a/Sources/JSONAPITesting/Optional+ZipWith.swift b/Sources/JSONAPITesting/Optional+ZipWith.swift new file mode 100644 index 0000000..aacbb7b --- /dev/null +++ b/Sources/JSONAPITesting/Optional+ZipWith.swift @@ -0,0 +1,12 @@ +// +// Optional+ZipWith.swift +// +// Created by Mathew Polzin on 1/19/19. +// + +/// Zip two optionals together with the given operation performed on +/// the unwrapped contents. If either optional is nil, the zip +/// yields nil. +func zip(_ left: X?, _ right: Y?, with fn: (X, Y) -> Z) -> Z? { + return left.flatMap { lft in right.map { rght in fn(lft, rght) }} +} diff --git a/Tests/JSONAPITestingTests/Comparisons/AttributesCompareTests.swift b/Tests/JSONAPITestingTests/Comparisons/AttributesCompareTests.swift new file mode 100644 index 0000000..e7a7dca --- /dev/null +++ b/Tests/JSONAPITestingTests/Comparisons/AttributesCompareTests.swift @@ -0,0 +1,74 @@ +// +// File.swift +// +// +// Created by Mathew Polzin on 11/3/19. +// + +import XCTest +import JSONAPI +import JSONAPITesting + +final class AttributesCompareTests: XCTestCase { + func test_sameAttributes() { + let attr1 = TestAttributes( + string: "hello world", + int: 10, + bool: true, + double: 105.4, + struct: .init(value: .init()) + ) + let attr2 = attr1 + + XCTAssertEqual(attr1.compare(to: attr2), [ + "string": .same, + "int": .same, + "bool": .same, + "double": .same, + "struct": .same + ]) + } + + func test_differentAttributes() { + let attr1 = TestAttributes( + string: "hello world", + int: 10, + bool: true, + double: 105.4, + struct: .init(value: .init()) + ) + let attr2 = TestAttributes( + string: "hello", + int: 11, + bool: false, + double: 1.4, + struct: .init(value: .init(val: "there")) + ) + + XCTAssertEqual(attr1.compare(to: attr2), [ + "string": .different("hello world", "hello"), + "int": .different("10", "11"), + "bool": .different("true", "false"), + "double": .different("105.4", "1.4"), + "struct": .different("string: hello", "string: there") + ]) + } +} + +private struct TestAttributes: JSONAPI.Attributes { + let string: Attribute + let int: Attribute + let bool: Attribute + let double: Attribute + let `struct`: Attribute + + struct Struct: Equatable, Codable, CustomStringConvertible { + let string: String + + init(val: String = "hello") { + self.string = val + } + + var description: String { return "string: \(string)" } + } +} diff --git a/Tests/JSONAPITestingTests/Comparisons/DocumentCompareTests.swift b/Tests/JSONAPITestingTests/Comparisons/DocumentCompareTests.swift new file mode 100644 index 0000000..b2e14af --- /dev/null +++ b/Tests/JSONAPITestingTests/Comparisons/DocumentCompareTests.swift @@ -0,0 +1,170 @@ +// +// DocumentCompareTests.swift +// +// +// Created by Mathew Polzin on 11/4/19. +// + +import XCTest +import JSONAPI +import JSONAPITesting + +final class DocumentCompareTests: XCTestCase { + func test_same() { + XCTAssertTrue(d1.compare(to: d1).differences.isEmpty) + XCTAssertTrue(d2.compare(to: d2).differences.isEmpty) + XCTAssertTrue(d3.compare(to: d3).differences.isEmpty) + XCTAssertTrue(d4.compare(to: d4).differences.isEmpty) + } + + func test_errorAndData() { + XCTAssertEqual(d1.compare(to: d2).differences, [ + "Body": "data response ≠ error response" + ]) + + XCTAssertEqual(d2.compare(to: d1).differences, [ + "Body": "error response ≠ data response" + ]) + } + + func test_differentErrors() { + XCTAssertEqual(d2.compare(to: d4).differences, [ + "Body": "status: 500, title: Internal Error ≠ status: 404, title: Not Found" + ]) + } + + func test_differentData() { + XCTAssertEqual(d3.compare(to: d5).differences, [ + "Body": "(Includes: (include 2: missing)), (Primary Resource: (resource 2: missing))" + ]) + + XCTAssertEqual(d3.compare(to: d6).differences, [ + "Body": ##"(Includes: (include 2: missing)), (Primary Resource: (resource 2: 'age' attribute: 10 ≠ 12, 'bestFriend' relationship: Optional(Id(2)) ≠ nil, 'favoriteColor' attribute: nil ≠ Optional("blue"), 'name' attribute: name ≠ Fig, id: 1 ≠ 5), (resource 3: missing))"## + ]) + } +} + +fileprivate enum TestDescription: JSONAPI.ResourceObjectDescription { + static let jsonType: String = "test_type" + + struct Attributes: JSONAPI.Attributes { + let name: Attribute + let age: Attribute + let favoriteColor: Attribute + } + + struct Relationships: JSONAPI.Relationships { + let bestFriend: ToOneRelationship + let parents: ToManyRelationship + } +} + +fileprivate typealias TestType = ResourceObject + +fileprivate enum TestDescription2: JSONAPI.ResourceObjectDescription { + static let jsonType: String = "test_type2" + + struct Attributes: JSONAPI.Attributes { + let name: Attribute + let age: Attribute + let favoriteColor: Attribute + } + + struct Relationships: JSONAPI.Relationships { + let bestFriend: ToOneRelationship + let parents: ToManyRelationship + } +} + +fileprivate typealias TestType2 = ResourceObject + +fileprivate typealias SingleDocument = JSONAPI.Document, NoMetadata, NoLinks, Include2, NoAPIDescription, BasicJSONAPIError> + +fileprivate typealias ManyDocument = JSONAPI.Document, NoMetadata, NoLinks, Include2, NoAPIDescription, BasicJSONAPIError> + +fileprivate let r1 = TestType( + id: "1", + attributes: .init( + name: "name", + age: 10, + favoriteColor: nil + ), + relationships: .init( + bestFriend: "2", + parents: ["3", "4"] + ), + meta: .none, + links: .none +) + +fileprivate let r2 = TestType( + id: "5", + attributes: .init( + name: "Fig", + age: 12, + favoriteColor: "blue" + ), + relationships: .init( + bestFriend: nil, + parents: ["3", "4"] + ), + meta: .none, + links: .none +) + +fileprivate let r3 = TestType2( + id: "2", + attributes: .init( + name: "Tully", + age: 100, + favoriteColor: "red" + ), + relationships: .init( + bestFriend: nil, + parents: [] + ), + meta: .none, + links: .none +) + +fileprivate let d1 = SingleDocument( + apiDescription: .none, + body: .init(resourceObject: r1), + includes: .none, + meta: .none, + links: .none +) + +fileprivate let d2 = SingleDocument( + apiDescription: .none, + errors: [.error(.init(id: nil, status: "500", title: "Internal Error"))] +) + +fileprivate let d3 = ManyDocument( + apiDescription: .none, + body: .init(resourceObjects: [r1, r2]), + includes: .init(values: [.init(r3)]), + meta: .none, + links: .none +) + +fileprivate let d4 = SingleDocument( + apiDescription: .none, + errors: [.error(.init(id: nil, status: "404", title: "Not Found"))] +) + +fileprivate let d5 = ManyDocument( + apiDescription: .none, + body: .init(resourceObjects: [r1]), + includes: .init(values: [.init(r3), .init(r2)]), + meta: .none, + links: .none +) + +fileprivate let d6 = ManyDocument( + apiDescription: .none, + body: .init(resourceObjects: [r1, r1, r2]), + includes: .init(values: [.init(r3), .init(r2)]), + meta: .none, + links: .none +) diff --git a/Tests/JSONAPITestingTests/Comparisons/IncludesCompareTests.swift b/Tests/JSONAPITestingTests/Comparisons/IncludesCompareTests.swift new file mode 100644 index 0000000..7d34521 --- /dev/null +++ b/Tests/JSONAPITestingTests/Comparisons/IncludesCompareTests.swift @@ -0,0 +1,239 @@ +// +// IncludeCompareTests.swift +// +// +// Created by Mathew Polzin on 11/4/19. +// + +import XCTest +import JSONAPI +import JSONAPITesting +import Poly + +final class IncludesCompareTests: XCTestCase { + func test_same() { + let includes1 = Includes(values: justTypeOnes) + let includes2 = Includes(values: justTypeOnes) + XCTAssertTrue(includes1.compare(to: includes2).differences.isEmpty) + + let includes3 = Includes(values: longerTypeOnes) + let includes4 = Includes(values: longerTypeOnes) + XCTAssertTrue(includes3.compare(to: includes4).differences.isEmpty) + + let includes5 = Includes(values: onesAndTwos) + let includes6 = Includes(values: onesAndTwos) + XCTAssertTrue(includes5.compare(to: includes6).differences.isEmpty) + } + + func test_missing() { + let includes1 = Includes(values: justTypeOnes) + let includes2 = Includes(values: longerTypeOnes) + XCTAssertEqual(includes1.compare(to: includes2).differences, ["include 3": "missing"]) + XCTAssertEqual(includes2.compare(to: includes1).differences, ["include 3": "missing"]) + } + + func test_typeMismatch() { + let includes1 = Includes(values: onesAndTwos) + let includes2 = Includes(values: justTypeOnes) + XCTAssertEqual(includes1.compare(to: includes2).differences, ["include 2": "ResourceObject ≠ ResourceObject"]) + XCTAssertEqual(includes2.compare(to: includes1).differences, ["include 2": "ResourceObject ≠ ResourceObject"]) + } + + func test_valueMismatch() { + let includes1 = Includes(values: onesAndTwos) + let includes2 = Includes(values: differentOnesAndTwos) + XCTAssertEqual(includes1.compare(to: includes2).differences, [ + "include 1": #"'favoriteColor' attribute: Optional("red") ≠ nil, 'name' attribute: Matt ≠ Todd, 'parents' relationship: 4, 5 ≠ 7, 8, id: 1 ≠ 2"# + ]) + } + + fileprivate let justTypeOnes: [Poly2] = [ + .a( + TestType1( + id: "1", + attributes: .init( + name: "Matt", + age: 23, + favoriteColor: "red" + ), + relationships: .init( + bestFriend: "3", + parents: ["4", "5"] + ), + meta: .none, + links: .none + ) + ), + .a( + TestType1( + id: "3", + attributes: .init( + name: "Helen", + age: 24, + favoriteColor: nil + ), + relationships: .init( + bestFriend: nil, + parents: ["2"] + ), + meta: .none, + links: .none + ) + ) + ] + + fileprivate let longerTypeOnes: [Poly2] = [ + .a( + TestType1( + id: "1", + attributes: .init( + name: "Matt", + age: 23, + favoriteColor: "red" + ), + relationships: .init( + bestFriend: "3", + parents: ["4", "5"] + ), + meta: .none, + links: .none + ) + ), + .a( + TestType1( + id: "3", + attributes: .init( + name: "Helen", + age: 24, + favoriteColor: nil + ), + relationships: .init( + bestFriend: nil, + parents: ["2"] + ), + meta: .none, + links: .none + ) + ), + .a( + TestType1( + id: "2", + attributes: .init( + name: "Troy", + age: 45, + favoriteColor: "blue" + ), + relationships: .init( + bestFriend: nil, + parents: [] + ), + meta: .none, + links: .none + ) + ) + ] + + fileprivate let onesAndTwos: [Poly2] = [ + .a( + TestType1( + id: "1", + attributes: .init( + name: "Matt", + age: 23, + favoriteColor: "red" + ), + relationships: .init( + bestFriend: "3", + parents: ["4", "5"] + ), + meta: .none, + links: .none + ) + ), + .b( + TestType2( + id: "1", + attributes: .init( + name: "Lucy", + age: 33, + favoriteColor: nil + ), + relationships: .init( + bestFriend: nil, + parents: [] + ), + meta: .none, + links: .none + ) + ) + ] + + fileprivate let differentOnesAndTwos: [Poly2] = [ + .a( + TestType1( + id: "2", + attributes: .init( + name: "Todd", + age: 23, + favoriteColor: nil + ), + relationships: .init( + bestFriend: "3", + parents: ["7", "8"] + ), + meta: .none, + links: .none + ) + ), + .b( + TestType2( + id: "1", + attributes: .init( + name: "Lucy", + age: 33, + favoriteColor: nil + ), + relationships: .init( + bestFriend: nil, + parents: [] + ), + meta: .none, + links: .none + ) + ) + ] +} + +private enum TestDescription1: JSONAPI.ResourceObjectDescription { + static let jsonType: String = "test_type1" + + struct Attributes: JSONAPI.Attributes { + let name: Attribute + let age: Attribute + let favoriteColor: Attribute + } + + struct Relationships: JSONAPI.Relationships { + let bestFriend: ToOneRelationship + let parents: ToManyRelationship + } +} + +private typealias TestType1 = ResourceObject + +private enum TestDescription2: JSONAPI.ResourceObjectDescription { + static let jsonType: String = "test_type2" + + struct Attributes: JSONAPI.Attributes { + let name: Attribute + let age: Attribute + let favoriteColor: Attribute + } + + struct Relationships: JSONAPI.Relationships { + let bestFriend: ToOneRelationship + let parents: ToManyRelationship + } +} + +private typealias TestType2 = ResourceObject diff --git a/Tests/JSONAPITestingTests/Comparisons/RelationshipsCompareTests.swift b/Tests/JSONAPITestingTests/Comparisons/RelationshipsCompareTests.swift new file mode 100644 index 0000000..cdb7fda --- /dev/null +++ b/Tests/JSONAPITestingTests/Comparisons/RelationshipsCompareTests.swift @@ -0,0 +1,14 @@ +// +// File.swift +// +// +// Created by Mathew Polzin on 11/5/19. +// + +import XCTest +import JSONAPI +import JSONAPITesting + +final class RelationshipsCompareTests: XCTestCase { + // TODO: write tests +} diff --git a/Tests/JSONAPITestingTests/Comparisons/ResourceObjectCompareTests.swift b/Tests/JSONAPITestingTests/Comparisons/ResourceObjectCompareTests.swift new file mode 100644 index 0000000..2c40e87 --- /dev/null +++ b/Tests/JSONAPITestingTests/Comparisons/ResourceObjectCompareTests.swift @@ -0,0 +1,68 @@ +// +// File.swift +// +// +// Created by Mathew Polzin on 11/3/19. +// + +import XCTest +import JSONAPI +import JSONAPITesting + +final class ResourceObjectCompareTests: XCTestCase { + func test_same() { + print(test1.compare(to: test1).differences) + XCTAssertTrue(test1.compare(to: test1).differences.isEmpty) + XCTAssertTrue(test2.compare(to: test2).differences.isEmpty) + } + + func test_different() { + // TODO: write actual test + print(test1.compare(to: test2).differences.map { "\($0): \($1)" }.joined(separator: ", ")) + } + + fileprivate let test1 = TestType( + id: "2", + attributes: .init( + name: "James", + age: 12, + favoriteColor: "red"), + relationships: .init( + bestFriend: "3", + parents: ["4", "5"] + ), + meta: .none, + links: .none + ) + + fileprivate let test2 = TestType( + id: "3", + attributes: .init( + name: "Fred", + age: 10, + favoriteColor: .init(value: nil)), + relationships: .init( + bestFriend: nil, + parents: ["1"] + ), + meta: .none, + links: .none + ) +} + +private enum TestDescription: JSONAPI.ResourceObjectDescription { + static let jsonType: String = "test_type" + + struct Attributes: JSONAPI.Attributes { + let name: Attribute + let age: Attribute + let favoriteColor: Attribute + } + + struct Relationships: JSONAPI.Relationships { + let bestFriend: ToOneRelationship + let parents: ToManyRelationship + } +} + +private typealias TestType = ResourceObject From 0fe5c53ada020b2709825be93a7c2cf58cec3d2a Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Tue, 5 Nov 2019 19:04:47 -0800 Subject: [PATCH 089/235] generate linuxmain --- .../JSONAPITestingTests/XCTestManifests.swift | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/Tests/JSONAPITestingTests/XCTestManifests.swift b/Tests/JSONAPITestingTests/XCTestManifests.swift index c916df3..b1b07fb 100644 --- a/Tests/JSONAPITestingTests/XCTestManifests.swift +++ b/Tests/JSONAPITestingTests/XCTestManifests.swift @@ -35,6 +35,28 @@ extension Attribute_LiteralTests { ] } +extension AttributesCompareTests { + // DO NOT MODIFY: This is autogenerated, use: + // `swift test --generate-linuxmain` + // to regenerate. + static let __allTests__AttributesCompareTests = [ + ("test_differentAttributes", test_differentAttributes), + ("test_sameAttributes", test_sameAttributes), + ] +} + +extension DocumentCompareTests { + // DO NOT MODIFY: This is autogenerated, use: + // `swift test --generate-linuxmain` + // to regenerate. + static let __allTests__DocumentCompareTests = [ + ("test_differentData", test_differentData), + ("test_differentErrors", test_differentErrors), + ("test_errorAndData", test_errorAndData), + ("test_same", test_same), + ] +} + extension EntityCheckTests { // DO NOT MODIFY: This is autogenerated, use: // `swift test --generate-linuxmain` @@ -58,6 +80,18 @@ extension Id_LiteralTests { ] } +extension IncludesCompareTests { + // DO NOT MODIFY: This is autogenerated, use: + // `swift test --generate-linuxmain` + // to regenerate. + static let __allTests__IncludesCompareTests = [ + ("test_missing", test_missing), + ("test_same", test_same), + ("test_typeMismatch", test_typeMismatch), + ("test_valueMismatch", test_valueMismatch), + ] +} + extension Relationship_LiteralTests { // DO NOT MODIFY: This is autogenerated, use: // `swift test --generate-linuxmain` @@ -69,12 +103,26 @@ extension Relationship_LiteralTests { ] } +extension ResourceObjectCompareTests { + // DO NOT MODIFY: This is autogenerated, use: + // `swift test --generate-linuxmain` + // to regenerate. + static let __allTests__ResourceObjectCompareTests = [ + ("test_different", test_different), + ("test_same", test_same), + ] +} + public func __allTests() -> [XCTestCaseEntry] { return [ testCase(Attribute_LiteralTests.__allTests__Attribute_LiteralTests), + testCase(AttributesCompareTests.__allTests__AttributesCompareTests), + testCase(DocumentCompareTests.__allTests__DocumentCompareTests), testCase(EntityCheckTests.__allTests__EntityCheckTests), testCase(Id_LiteralTests.__allTests__Id_LiteralTests), + testCase(IncludesCompareTests.__allTests__IncludesCompareTests), testCase(Relationship_LiteralTests.__allTests__Relationship_LiteralTests), + testCase(ResourceObjectCompareTests.__allTests__ResourceObjectCompareTests), ] } #endif From 33a5ff41a029333dd5a9af8fb1a2d0bf61fea4f9 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Tue, 5 Nov 2019 19:50:52 -0800 Subject: [PATCH 090/235] beginning to do some breaking change cleanup --- Sources/JSONAPI/Document/Document.swift | 257 +++++++++++------- Sources/JSONAPI/Document/ResourceBody.swift | 6 +- .../JSONAPITests/Document/DocumentTests.swift | 2 +- 3 files changed, 164 insertions(+), 101 deletions(-) diff --git a/Sources/JSONAPI/Document/Document.swift b/Sources/JSONAPI/Document/Document.swift index 5c8160b..cc907eb 100644 --- a/Sources/JSONAPI/Document/Document.swift +++ b/Sources/JSONAPI/Document/Document.swift @@ -1,5 +1,5 @@ // -// JSONAPIDocument.swift +// Document.swift // JSONAPI // // Created by Mathew Polzin on 11/5/18. @@ -7,25 +7,96 @@ import Poly -/// An `EncodableJSONAPIDocument` supports encoding but not decoding. -/// It is actually more restrictive than `JSONAPIDocument` which supports both -/// encoding and decoding. -public protocol EncodableJSONAPIDocument: Equatable, Encodable { +public protocol DocumentBodyDataContext { associatedtype PrimaryResourceBody: JSONAPI.EncodableResourceBody associatedtype MetaType: JSONAPI.Meta associatedtype LinksType: JSONAPI.Links associatedtype IncludeType: JSONAPI.Include - associatedtype APIDescription: APIDescriptionType +} + +public protocol DocumentBodyContext: DocumentBodyDataContext { associatedtype Error: JSONAPIError + associatedtype BodyData: DocumentBodyData + where + BodyData.PrimaryResourceBody == PrimaryResourceBody, + BodyData.MetaType == MetaType, + BodyData.LinksType == LinksType, + BodyData.IncludeType == IncludeType +} - typealias Body = Document.Body +public protocol DocumentBodyData: DocumentBodyDataContext { + /// The document's primary resource body + /// (contains one or many resource objects) + var primary: PrimaryResourceBody { get } + + /// The document's included objects + var includes: Includes { get } + var meta: MetaType { get } + var links: LinksType { get } +} + +public protocol DocumentBody: DocumentBodyContext { + /// `true` if the document represents one or more errors. `false` if the + /// document represents JSON:API data and/or metadata. + var isError: Bool { get } + + /// Get all errors in the document, if any. + /// + /// `nil` if the Document is _not_ an error response. Otherwise, + /// an array containing all errors. + var errors: [Error]? { get } + + /// Get the document data + /// + /// `nil` if the Document is an error response. Otherwise, + /// a structure containing the primary resource, any included + /// resources, metadata, and links. + var data: BodyData? { get } + + /// Quick access to the `data`'s primary resource. + /// + /// `nil` if the Document is an error document. Otherwise, + /// the primary resource body, which will contain zero/one, one/many + /// resources dependening on the `PrimaryResourceBody` type. + /// + /// See `SingleResourceBody` and `ManyResourceBody`. + var primaryResource: PrimaryResourceBody? { get } + + /// Quick access to the `data`'s includes. + /// + /// `nil` if the Document is an error document. Otherwise, + /// zero or more includes. + var includes: Includes? { get } + + /// The metadata for the error or data document or `nil` if + /// no metadata is found. + var meta: MetaType? { get } + + /// The links for the error or data document or `nil` if + /// no links are found. + var links: LinksType? { get } +} + +/// An `EncodableJSONAPIDocument` supports encoding but not decoding. +/// It is actually more restrictive than `JSONAPIDocument` which supports both +/// encoding and decoding. +public protocol EncodableJSONAPIDocument: Equatable, Encodable, DocumentBodyContext { + associatedtype APIDescription: APIDescriptionType + associatedtype Body: DocumentBody + where + Body.PrimaryResourceBody == PrimaryResourceBody, + Body.MetaType == MetaType, + Body.LinksType == LinksType, + Body.IncludeType == IncludeType, + Body.Error == Error, + Body.BodyData == BodyData var body: Body { get } } -/// A `JSONAPIDocument` supports encoding and decoding of a JSON:API +/// A `CodableJSONAPIDocument` supports encoding and decoding of a JSON:API /// compliant Document. -public protocol JSONAPIDocument: EncodableJSONAPIDocument, Decodable where PrimaryResourceBody: JSONAPI.ResourceBody, IncludeType: Decodable {} +public protocol CodableJSONAPIDocument: EncodableJSONAPIDocument, Decodable where PrimaryResourceBody: JSONAPI.ResourceBody, IncludeType: Decodable {} /// A JSON API Document represents the entire body /// of a JSON API request or the entire body of @@ -36,6 +107,7 @@ public protocol JSONAPIDocument: EncodableJSONAPIDocument, Decodable where Prima /// Foundation JSONEncoder/Decoder: `KeyDecodingStrategy` public struct Document: EncodableJSONAPIDocument { public typealias Include = IncludeType + public typealias BodyData = Body.Data /// The JSON API Spec calls this the JSON:API Object. It contains version /// and metadata information about the API itself. @@ -47,12 +119,14 @@ public struct Document, links: LinksType) { - self.init(apiDescription: apiDescription, body: body, includes: includes, meta: .none, links: links) - } -} - -extension Document where LinksType == NoLinks { - public init(apiDescription: APIDescription, body: PrimaryResourceBody, includes: Includes, meta: MetaType) { - self.init(apiDescription: apiDescription, body: body, includes: includes, meta: meta, links: .none) - } -} - -extension Document where APIDescription == NoAPIDescription { - public init(body: PrimaryResourceBody, includes: Includes, meta: MetaType, links: LinksType) { - self.init(apiDescription: .none, body: body, includes: includes, meta: meta, links: links) - } -} - -extension Document where IncludeType == NoIncludes, LinksType == NoLinks { - public init(apiDescription: APIDescription, body: PrimaryResourceBody, meta: MetaType) { - self.init(apiDescription: apiDescription, body: body, meta: meta, links: .none) - } -} - -extension Document where IncludeType == NoIncludes, MetaType == NoMetadata { - public init(apiDescription: APIDescription, body: PrimaryResourceBody, links: LinksType) { - self.init(apiDescription: apiDescription, body: body, meta: .none, links: links) - } -} - -extension Document where IncludeType == NoIncludes, APIDescription == NoAPIDescription { - public init(body: PrimaryResourceBody, meta: MetaType, links: LinksType) { - self.init(apiDescription: .none, body: body, meta: meta, links: links) - } -} - -extension Document where MetaType == NoMetadata, LinksType == NoLinks { - public init(apiDescription: APIDescription, body: PrimaryResourceBody, includes: Includes) { - self.init(apiDescription: apiDescription, body: body, includes: includes, links: .none) - } -} - -extension Document where MetaType == NoMetadata, APIDescription == NoAPIDescription { - public init(body: PrimaryResourceBody, includes: Includes, links: LinksType) { - self.init(apiDescription: .none, body: body, includes: includes, links: links) - } -} - -extension Document where IncludeType == NoIncludes, MetaType == NoMetadata, LinksType == NoLinks { - public init(apiDescription: APIDescription, body: PrimaryResourceBody) { - self.init(apiDescription: apiDescription, body: body, includes: .none) - } -} - -extension Document where MetaType == NoMetadata, LinksType == NoLinks, APIDescription == NoAPIDescription { - public init(body: PrimaryResourceBody, includes: Includes) { - self.init(apiDescription: .none, body: body, includes: includes) - } -} - -extension Document where IncludeType == NoIncludes, MetaType == NoMetadata, LinksType == NoLinks, APIDescription == NoAPIDescription { - public init(body: PrimaryResourceBody) { - self.init(apiDescription: .none, body: body) - } -} -*/ - -extension Document.Body.Data where PrimaryResourceBody: Appendable { +extension Document.Body.Data where PrimaryResourceBody: ResourceBodyAppendable { public func merging(_ other: Document.Body.Data, combiningMetaWith metaMerge: (MetaType, MetaType) -> MetaType, combiningLinksWith linksMerge: (LinksType, LinksType) -> LinksType) -> Document.Body.Data { @@ -218,7 +218,7 @@ extension Document.Body.Data where PrimaryResourceBody: Appendable { } } -extension Document.Body.Data where PrimaryResourceBody: Appendable, MetaType == NoMetadata, LinksType == NoLinks { +extension Document.Body.Data where PrimaryResourceBody: ResourceBodyAppendable, MetaType == NoMetadata, LinksType == NoLinks { public func merging(_ other: Document.Body.Data) -> Document.Body.Data { return merging(other, combiningMetaWith: { _, _ in .none }, @@ -328,7 +328,7 @@ extension Document { } } -extension Document: Decodable, JSONAPIDocument where PrimaryResourceBody: ResourceBody, IncludeType: Decodable { +extension Document: Decodable, CodableJSONAPIDocument where PrimaryResourceBody: ResourceBody, IncludeType: Decodable { public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: RootCodingKeys.self) @@ -420,8 +420,9 @@ extension Document.Body.Data: CustomStringConvertible { extension Document { /// A Document that only supports error bodies. This is useful if you wish to pass around a /// Document type but you wish to constrain it to error values. - @dynamicMemberLookup public struct ErrorDocument: EncodableJSONAPIDocument { + public typealias BodyData = Document.BodyData + public var body: Document.Body { return document.body } private let document: Document @@ -436,8 +437,27 @@ extension Document { try container.encode(document) } - public subscript(dynamicMember path: KeyPath) -> T { - return document[keyPath: path] + /// The JSON API Spec calls this the JSON:API Object. It contains version + /// and metadata information about the API itself. + public var apiDescription: APIDescription { + return document.apiDescription + } + + /// Get all errors in the document, if any. + public var errors: [Error] { + return document.body.errors ?? [] + } + + /// The metadata for the error or data document or `nil` if + /// no metadata is found. + public var meta: MetaType? { + return document.body.meta + } + + /// The links for the error or data document or `nil` if + /// no links are found. + public var links: LinksType? { + return document.body.links } public static func ==(lhs: Document, rhs: ErrorDocument) -> Bool { @@ -447,8 +467,9 @@ extension Document { /// A Document that only supports success bodies. This is useful if you wish to pass around a /// Document type but you wish to constrain it to success values. - @dynamicMemberLookup public struct SuccessDocument: EncodableJSONAPIDocument { + public typealias BodyData = Document.BodyData + public var body: Document.Body { return document.body } private let document: Document @@ -471,8 +492,50 @@ extension Document { try container.encode(document) } - public subscript(dynamicMember path: KeyPath) -> T { - return document[keyPath: path] + /// The JSON API Spec calls this the JSON:API Object. It contains version + /// and metadata information about the API itself. + public var apiDescription: APIDescription { + return document.apiDescription + } + + /// Get the document data + /// + /// `nil` if the Document is an error response. Otherwise, + /// a structure containing the primary resource, any included + /// resources, metadata, and links. + var data: BodyData? { + return document.body.data + } + + /// Quick access to the `data`'s primary resource. + /// + /// `nil` if the Document is an error document. Otherwise, + /// the primary resource body, which will contain zero/one, one/many + /// resources dependening on the `PrimaryResourceBody` type. + /// + /// See `SingleResourceBody` and `ManyResourceBody`. + var primaryResource: PrimaryResourceBody? { + return document.body.primaryResource + } + + /// Quick access to the `data`'s includes. + /// + /// `nil` if the Document is an error document. Otherwise, + /// zero or more includes. + var includes: Includes? { + return document.body.includes + } + + /// The metadata for the error or data document or `nil` if + /// no metadata is found. + var meta: MetaType? { + return document.body.meta + } + + /// The links for the error or data document or `nil` if + /// no links are found. + var links: LinksType? { + return document.body.links } public static func ==(lhs: Document, rhs: SuccessDocument) -> Bool { @@ -481,7 +544,7 @@ extension Document { } } -extension Document.ErrorDocument: Decodable, JSONAPIDocument +extension Document.ErrorDocument: Decodable, CodableJSONAPIDocument where PrimaryResourceBody: ResourceBody, IncludeType: Decodable { public init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer() @@ -494,7 +557,7 @@ extension Document.ErrorDocument: Decodable, JSONAPIDocument } } -extension Document.SuccessDocument: Decodable, JSONAPIDocument +extension Document.SuccessDocument: Decodable, CodableJSONAPIDocument where PrimaryResourceBody: ResourceBody, IncludeType: Decodable { public init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer() diff --git a/Sources/JSONAPI/Document/ResourceBody.swift b/Sources/JSONAPI/Document/ResourceBody.swift index 3fdef10..7d8b9e2 100644 --- a/Sources/JSONAPI/Document/ResourceBody.swift +++ b/Sources/JSONAPI/Document/ResourceBody.swift @@ -40,11 +40,11 @@ public protocol ResourceBody: Decodable, EncodableResourceBody {} /// A `ResourceBody` that has the ability to take on more primary /// resources by appending another similarly typed `ResourceBody`. -public protocol Appendable { +public protocol ResourceBodyAppendable { func appending(_ other: Self) -> Self } -public func +(_ left: R, right: R) -> R { +public func +(_ left: R, right: R) -> R { return left.appending(right) } @@ -60,7 +60,7 @@ public struct SingleResourceBody: EncodableResourceBody, Appendable { +public struct ManyResourceBody: EncodableResourceBody, ResourceBodyAppendable { public let values: [Entity] public init(resourceObjects: [Entity]) { diff --git a/Tests/JSONAPITests/Document/DocumentTests.swift b/Tests/JSONAPITests/Document/DocumentTests.swift index bbf36e9..d6f6351 100644 --- a/Tests/JSONAPITests/Document/DocumentTests.swift +++ b/Tests/JSONAPITests/Document/DocumentTests.swift @@ -12,7 +12,7 @@ import Poly class DocumentTests: XCTestCase { func test_genericDocFunc() { - func test(_ doc: Doc) { + func test(_ doc: Doc) { let _ = encoded(value: doc) XCTAssert(Doc.PrimaryResourceBody.self == NoResourceBody.self) From 706346e3a60fb8dd34e52ab9bcc485a61b8d749f Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Tue, 5 Nov 2019 20:04:47 -0800 Subject: [PATCH 091/235] just reformatting and rearranging --- Sources/JSONAPI/Document/Document.swift | 152 +++++++++++++----------- 1 file changed, 82 insertions(+), 70 deletions(-) diff --git a/Sources/JSONAPI/Document/Document.swift b/Sources/JSONAPI/Document/Document.swift index cc907eb..98b4445 100644 --- a/Sources/JSONAPI/Document/Document.swift +++ b/Sources/JSONAPI/Document/Document.swift @@ -120,91 +120,103 @@ public struct Document, + meta: MetaType, + links: LinksType) { + self.body = .data( + .init( + primary: body, + includes: includes, + meta: meta, + links: links + ) + ) + self.apiDescription = apiDescription + } +} + +extension Document { public enum Body: DocumentBody, Equatable { - case errors([Error], meta: MetaType?, links: LinksType?) - case data(Data) + case errors([Error], meta: MetaType?, links: LinksType?) + case data(Data) public typealias BodyData = Data - public struct Data: DocumentBodyData, Equatable { + public struct Data: DocumentBodyData, Equatable { /// The document's Primary Resource object(s) - public let primary: PrimaryResourceBody + public let primary: PrimaryResourceBody /// The document's included objects - public let includes: Includes - public let meta: MetaType - public let links: LinksType - - public init(primary: PrimaryResourceBody, includes: Includes, meta: MetaType, links: LinksType) { - self.primary = primary - self.includes = includes - self.meta = meta - self.links = links - } - } + public let includes: Includes + public let meta: MetaType + public let links: LinksType + + public init(primary: PrimaryResourceBody, includes: Includes, meta: MetaType, links: LinksType) { + self.primary = primary + self.includes = includes + self.meta = meta + self.links = links + } + } /// `true` if the document represents one or more errors. `false` if the /// document represents JSON:API data and/or metadata. - public var isError: Bool { - guard case .errors = self else { return false } - return true - } - - public var errors: [Error]? { - guard case let .errors(errors, meta: _, links: _) = self else { return nil } - return errors - } + public var isError: Bool { + guard case .errors = self else { return false } + return true + } - public var data: Data? { - guard case let .data(data) = self else { return nil } - return data - } - - public var primaryResource: PrimaryResourceBody? { - guard case let .data(data) = self else { return nil } - return data.primary - } + public var errors: [Error]? { + guard case let .errors(errors, meta: _, links: _) = self else { return nil } + return errors + } - public var includes: Includes? { - guard case let .data(data) = self else { return nil } - return data.includes - } + public var data: Data? { + guard case let .data(data) = self else { return nil } + return data + } - public var meta: MetaType? { - switch self { - case .data(let data): - return data.meta - case .errors(_, meta: let metadata?, links: _): - return metadata - default: - return nil - } - } + public var primaryResource: PrimaryResourceBody? { + guard case let .data(data) = self else { return nil } + return data.primary + } - public var links: LinksType? { - switch self { - case .data(let data): - return data.links - case .errors(_, meta: _, links: let links?): - return links - default: - return nil - } - } - } + public var includes: Includes? { + guard case let .data(data) = self else { return nil } + return data.includes + } - public init(apiDescription: APIDescription, errors: [Error], meta: MetaType? = nil, links: LinksType? = nil) { - body = .errors(errors, meta: meta, links: links) - self.apiDescription = apiDescription - } + public var meta: MetaType? { + switch self { + case .data(let data): + return data.meta + case .errors(_, meta: let metadata?, links: _): + return metadata + default: + return nil + } + } - public init(apiDescription: APIDescription, - body: PrimaryResourceBody, - includes: Includes, - meta: MetaType, - links: LinksType) { - self.body = .data(.init(primary: body, includes: includes, meta: meta, links: links)) - self.apiDescription = apiDescription - } + public var links: LinksType? { + switch self { + case .data(let data): + return data.links + case .errors(_, meta: _, links: let links?): + return links + default: + return nil + } + } + } } extension Document.Body.Data where PrimaryResourceBody: ResourceBodyAppendable { From 87271b93f9273e1bb801ea364e1d7b21dde68c7b Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Tue, 5 Nov 2019 20:48:04 -0800 Subject: [PATCH 092/235] deprecate subscript attribute accessor in favor of key path dynamic member lookup --- Sources/JSONAPI/Document/Document.swift | 8 +- Sources/JSONAPI/Document/ResourceBody.swift | 22 +- Sources/JSONAPI/Meta/Meta.swift | 8 +- .../Resource/Poly+PrimaryResource.swift | 24 +- .../Resource Object/ResourceObject.swift | 236 +----------------- .../Attribute/Attribute+FunctorTests.swift | 25 +- .../ComputedPropertiesTests.swift | 16 +- .../CustomAttributesTests.swift | 20 +- Tests/JSONAPITests/Poly/PolyProxyTests.swift | 22 +- .../ResourceObject/ResourceObjectTests.swift | 217 +++++++++++++--- .../SparseFields/SparseFieldsetTests.swift | 103 +++++++- 11 files changed, 377 insertions(+), 324 deletions(-) diff --git a/Sources/JSONAPI/Document/Document.swift b/Sources/JSONAPI/Document/Document.swift index 98b4445..6256265 100644 --- a/Sources/JSONAPI/Document/Document.swift +++ b/Sources/JSONAPI/Document/Document.swift @@ -96,7 +96,7 @@ public protocol EncodableJSONAPIDocument: Equatable, Encodable, DocumentBodyCont /// A `CodableJSONAPIDocument` supports encoding and decoding of a JSON:API /// compliant Document. -public protocol CodableJSONAPIDocument: EncodableJSONAPIDocument, Decodable where PrimaryResourceBody: JSONAPI.ResourceBody, IncludeType: Decodable {} +public protocol CodableJSONAPIDocument: EncodableJSONAPIDocument, Decodable where PrimaryResourceBody: JSONAPI.CodableResourceBody, IncludeType: Decodable {} /// A JSON API Document represents the entire body /// of a JSON API request or the entire body of @@ -340,7 +340,7 @@ extension Document { } } -extension Document: Decodable, CodableJSONAPIDocument where PrimaryResourceBody: ResourceBody, IncludeType: Decodable { +extension Document: Decodable, CodableJSONAPIDocument where PrimaryResourceBody: CodableResourceBody, IncludeType: Decodable { public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: RootCodingKeys.self) @@ -557,7 +557,7 @@ extension Document { } extension Document.ErrorDocument: Decodable, CodableJSONAPIDocument - where PrimaryResourceBody: ResourceBody, IncludeType: Decodable { + where PrimaryResourceBody: CodableResourceBody, IncludeType: Decodable { public init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer() @@ -570,7 +570,7 @@ extension Document.ErrorDocument: Decodable, CodableJSONAPIDocument } extension Document.SuccessDocument: Decodable, CodableJSONAPIDocument - where PrimaryResourceBody: ResourceBody, IncludeType: Decodable { + where PrimaryResourceBody: CodableResourceBody, IncludeType: Decodable { public init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer() diff --git a/Sources/JSONAPI/Document/ResourceBody.swift b/Sources/JSONAPI/Document/ResourceBody.swift index 7d8b9e2..8f0005e 100644 --- a/Sources/JSONAPI/Document/ResourceBody.swift +++ b/Sources/JSONAPI/Document/ResourceBody.swift @@ -10,33 +10,31 @@ /// array should be used for no results). public protocol OptionalEncodablePrimaryResource: Equatable, Encodable {} -/// An `EncodablePrimaryResource` is a `PrimaryResource` that only supports encoding. -/// This is actually more restrictave than `PrimaryResource`, which supports both encoding and -/// decoding. +/// An `EncodablePrimaryResource` is a `CodablePrimaryResource` that only supports encoding. public protocol EncodablePrimaryResource: OptionalEncodablePrimaryResource {} /// This protocol allows for `SingleResourceBody` to contain a `null` /// data object where `ManyResourceBody` cannot (because an empty /// 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. -public protocol PrimaryResource: EncodablePrimaryResource, OptionalPrimaryResource {} +public protocol CodablePrimaryResource: EncodablePrimaryResource, OptionalCodablePrimaryResource {} 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 /// encoded. It is actually weaker than `ResourceBody`, which supports both encoding /// and decoding. public protocol EncodableResourceBody: Equatable, Encodable {} -/// 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) /// 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 /// resources by appending another similarly typed `ResourceBody`. @@ -74,7 +72,7 @@ public struct ManyResourceBody: Encoda /// Use NoResourceBody to indicate you expect a JSON API document to not /// contain a "data" top-level key. -public struct NoResourceBody: ResourceBody { +public struct NoResourceBody: CodableResourceBody { public static var none: NoResourceBody { return NoResourceBody() } } @@ -94,7 +92,7 @@ extension SingleResourceBody { } } -extension SingleResourceBody: Decodable, ResourceBody where Entity: OptionalPrimaryResource { +extension SingleResourceBody: Decodable, CodableResourceBody where Entity: OptionalCodablePrimaryResource { public init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer() @@ -119,7 +117,7 @@ extension ManyResourceBody { } } -extension ManyResourceBody: Decodable, ResourceBody where Entity: PrimaryResource { +extension ManyResourceBody: Decodable, CodableResourceBody where Entity: CodablePrimaryResource { public init(from decoder: Decoder) throws { var container = try decoder.unkeyedContainer() var valueAggregator = [Entity]() diff --git a/Sources/JSONAPI/Meta/Meta.swift b/Sources/JSONAPI/Meta/Meta.swift index 68b2c94..b985477 100644 --- a/Sources/JSONAPI/Meta/Meta.swift +++ b/Sources/JSONAPI/Meta/Meta.swift @@ -6,17 +6,17 @@ // /// 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 /// 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. public protocol Meta: Codable, Equatable { } -// We make Optional a Meta if it wraps a Meta so that Metadata can be specified as -// nullable. +// We make Optional a Meta if it wraps a Meta so that +// Metadata can be specified as nullable. extension Optional: Meta where Wrapped: Meta {} /// Use this type when you want to specify not to encode or decode any metadata diff --git a/Sources/JSONAPI/Resource/Poly+PrimaryResource.swift b/Sources/JSONAPI/Resource/Poly+PrimaryResource.swift index 284e10a..b11634d 100644 --- a/Sources/JSONAPI/Resource/Poly+PrimaryResource.swift +++ b/Sources/JSONAPI/Resource/Poly+PrimaryResource.swift @@ -20,7 +20,7 @@ public typealias EncodableJSONPoly = Poly & EncodablePrimaryResource public typealias EncodablePolyWrapped = Encodable & Equatable public typealias PolyWrapped = EncodablePolyWrapped & Decodable -extension Poly0: PrimaryResource { +extension Poly0: CodablePrimaryResource { public init(from decoder: Decoder) throws { throw JSONAPIEncodingError.illegalDecoding("Attempted to decode Poly0, which should represent a thing that is not expected to be found in a document.") } @@ -33,54 +33,54 @@ extension Poly0: PrimaryResource { // MARK: - 1 type extension Poly1: EncodablePrimaryResource, OptionalEncodablePrimaryResource where A: EncodablePolyWrapped {} -extension Poly1: PrimaryResource, OptionalPrimaryResource where A: PolyWrapped {} +extension Poly1: CodablePrimaryResource, OptionalCodablePrimaryResource where A: PolyWrapped {} // MARK: - 2 types extension Poly2: EncodablePrimaryResource, OptionalEncodablePrimaryResource where A: EncodablePolyWrapped, B: EncodablePolyWrapped {} -extension Poly2: PrimaryResource, OptionalPrimaryResource where A: PolyWrapped, B: PolyWrapped {} +extension Poly2: CodablePrimaryResource, OptionalCodablePrimaryResource where A: PolyWrapped, B: PolyWrapped {} // MARK: - 3 types extension Poly3: EncodablePrimaryResource, OptionalEncodablePrimaryResource where A: EncodablePolyWrapped, B: EncodablePolyWrapped, C: EncodablePolyWrapped {} -extension Poly3: PrimaryResource, OptionalPrimaryResource where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped {} +extension Poly3: CodablePrimaryResource, OptionalCodablePrimaryResource where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped {} // MARK: - 4 types extension Poly4: EncodablePrimaryResource, OptionalEncodablePrimaryResource where A: EncodablePolyWrapped, B: EncodablePolyWrapped, C: EncodablePolyWrapped, D: EncodablePolyWrapped {} -extension Poly4: PrimaryResource, OptionalPrimaryResource where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped {} +extension Poly4: CodablePrimaryResource, OptionalCodablePrimaryResource where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped {} // MARK: - 5 types extension Poly5: EncodablePrimaryResource, OptionalEncodablePrimaryResource where A: EncodablePolyWrapped, B: EncodablePolyWrapped, C: EncodablePolyWrapped, D: EncodablePolyWrapped, E: EncodablePolyWrapped {} -extension Poly5: PrimaryResource, OptionalPrimaryResource where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped, E: PolyWrapped {} +extension Poly5: CodablePrimaryResource, OptionalCodablePrimaryResource where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped, E: PolyWrapped {} // MARK: - 6 types extension Poly6: EncodablePrimaryResource, OptionalEncodablePrimaryResource where A: EncodablePolyWrapped, B: EncodablePolyWrapped, C: EncodablePolyWrapped, D: EncodablePolyWrapped, E: EncodablePolyWrapped, F: EncodablePolyWrapped {} -extension Poly6: PrimaryResource, OptionalPrimaryResource where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped, E: PolyWrapped, F: PolyWrapped {} +extension Poly6: CodablePrimaryResource, OptionalCodablePrimaryResource where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped, E: PolyWrapped, F: PolyWrapped {} // MARK: - 7 types extension Poly7: EncodablePrimaryResource, OptionalEncodablePrimaryResource where A: EncodablePolyWrapped, B: EncodablePolyWrapped, C: EncodablePolyWrapped, D: EncodablePolyWrapped, E: EncodablePolyWrapped, F: EncodablePolyWrapped, G: EncodablePolyWrapped {} -extension Poly7: PrimaryResource, OptionalPrimaryResource where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped, E: PolyWrapped, F: PolyWrapped, G: PolyWrapped {} +extension Poly7: CodablePrimaryResource, OptionalCodablePrimaryResource where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped, E: PolyWrapped, F: PolyWrapped, G: PolyWrapped {} // MARK: - 8 types extension Poly8: EncodablePrimaryResource, OptionalEncodablePrimaryResource where A: EncodablePolyWrapped, B: EncodablePolyWrapped, C: EncodablePolyWrapped, D: EncodablePolyWrapped, E: EncodablePolyWrapped, F: EncodablePolyWrapped, G: EncodablePolyWrapped, H: EncodablePolyWrapped {} -extension Poly8: PrimaryResource, OptionalPrimaryResource where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped, E: PolyWrapped, F: PolyWrapped, G: PolyWrapped, H: PolyWrapped {} +extension Poly8: CodablePrimaryResource, OptionalCodablePrimaryResource where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped, E: PolyWrapped, F: PolyWrapped, G: PolyWrapped, H: PolyWrapped {} // MARK: - 9 types extension Poly9: EncodablePrimaryResource, OptionalEncodablePrimaryResource where A: EncodablePolyWrapped, B: EncodablePolyWrapped, C: EncodablePolyWrapped, D: EncodablePolyWrapped, E: EncodablePolyWrapped, F: EncodablePolyWrapped, G: EncodablePolyWrapped, H: EncodablePolyWrapped, I: EncodablePolyWrapped {} -extension Poly9: PrimaryResource, OptionalPrimaryResource where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped, E: PolyWrapped, F: PolyWrapped, G: PolyWrapped, H: PolyWrapped, I: PolyWrapped {} +extension Poly9: CodablePrimaryResource, OptionalCodablePrimaryResource where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped, E: PolyWrapped, F: PolyWrapped, G: PolyWrapped, H: PolyWrapped, I: PolyWrapped {} // MARK: - 10 types extension Poly10: EncodablePrimaryResource, OptionalEncodablePrimaryResource where A: EncodablePolyWrapped, B: EncodablePolyWrapped, C: EncodablePolyWrapped, D: EncodablePolyWrapped, E: EncodablePolyWrapped, F: EncodablePolyWrapped, G: EncodablePolyWrapped, H: EncodablePolyWrapped, I: EncodablePolyWrapped, J: EncodablePolyWrapped {} -extension Poly10: PrimaryResource, OptionalPrimaryResource where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped, E: PolyWrapped, F: PolyWrapped, G: PolyWrapped, H: PolyWrapped, I: PolyWrapped, J: PolyWrapped {} +extension Poly10: CodablePrimaryResource, OptionalCodablePrimaryResource where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped, E: PolyWrapped, F: PolyWrapped, G: PolyWrapped, H: PolyWrapped, I: PolyWrapped, J: PolyWrapped {} // MARK: - 11 types extension Poly11: EncodablePrimaryResource, OptionalEncodablePrimaryResource where A: EncodablePolyWrapped, B: EncodablePolyWrapped, C: EncodablePolyWrapped, D: EncodablePolyWrapped, E: EncodablePolyWrapped, F: EncodablePolyWrapped, G: EncodablePolyWrapped, H: EncodablePolyWrapped, I: EncodablePolyWrapped, J: EncodablePolyWrapped, K: EncodablePolyWrapped {} -extension Poly11: PrimaryResource, OptionalPrimaryResource where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped, E: PolyWrapped, F: PolyWrapped, G: PolyWrapped, H: PolyWrapped, I: PolyWrapped, J: PolyWrapped, K: PolyWrapped {} +extension Poly11: CodablePrimaryResource, OptionalCodablePrimaryResource where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped, E: PolyWrapped, F: PolyWrapped, G: PolyWrapped, H: PolyWrapped, I: PolyWrapped, J: PolyWrapped, K: PolyWrapped {} diff --git a/Sources/JSONAPI/Resource/Resource Object/ResourceObject.swift b/Sources/JSONAPI/Resource/Resource Object/ResourceObject.swift index a62d271..2418f3a 100644 --- a/Sources/JSONAPI/Resource/Resource Object/ResourceObject.swift +++ b/Sources/JSONAPI/Resource/Resource Object/ResourceObject.swift @@ -99,7 +99,7 @@ extension ResourceObjectProxy { /// ResourceObjectType is the protocol that ResourceObject conforms to. This /// protocol lets other types accept any ResourceObject as a generic /// specialization. -public protocol ResourceObjectType: ResourceObjectProxy, PrimaryResource where Description: ResourceObjectDescription { +public protocol ResourceObjectType: ResourceObjectProxy, CodablePrimaryResource where Description: ResourceObjectDescription { associatedtype Meta: JSONAPI.Meta associatedtype Links: JSONAPI.Links } @@ -173,236 +173,6 @@ extension ResourceObject where EntityRawIdType == Unidentified { } } -/* -extension ResourceObject where Description.Attributes == NoAttributes { - public init(id: ResourceObject.Id, relationships: Description.Relationships, meta: MetaType, links: LinksType) { - self.init(id: id, attributes: NoAttributes(), relationships: relationships, meta: meta, links: links) - } -} - -extension ResourceObject where Description.Attributes == NoAttributes, MetaType == NoMetadata { - public init(id: ResourceObject.Id, relationships: Description.Relationships, links: LinksType) { - self.init(id: id, relationships: relationships, meta: .none, links: links) - } -} - -extension ResourceObject where Description.Attributes == NoAttributes, LinksType == NoLinks { - public init(id: ResourceObject.Id, relationships: Description.Relationships, meta: MetaType) { - self.init(id: id, relationships: relationships, meta: meta, links: .none) - } -} - -extension ResourceObject where Description.Attributes == NoAttributes, MetaType == NoMetadata, LinksType == NoLinks { - public init(id: ResourceObject.Id, relationships: Description.Relationships) { - self.init(id: id, relationships: relationships, links: .none) - } -} - -extension ResourceObject where Description.Attributes == NoAttributes, EntityRawIdType: CreatableRawIdType { - public init(relationships: Description.Relationships, meta: MetaType, links: LinksType) { - self.init(attributes: NoAttributes(), relationships: relationships, meta: meta, links: links) - } -} - -extension ResourceObject where Description.Attributes == NoAttributes, MetaType == NoMetadata, EntityRawIdType: CreatableRawIdType { - public init(relationships: Description.Relationships, links: LinksType) { - self.init(attributes: NoAttributes(), relationships: relationships, meta: .none, links: links) - } -} - -extension ResourceObject where Description.Attributes == NoAttributes, LinksType == NoLinks, EntityRawIdType: CreatableRawIdType { - public init(relationships: Description.Relationships, meta: MetaType) { - self.init(attributes: NoAttributes(), relationships: relationships, meta: meta, links: .none) - } -} - -extension ResourceObject where Description.Attributes == NoAttributes, MetaType == NoMetadata, LinksType == NoLinks, EntityRawIdType: CreatableRawIdType { - public init(relationships: Description.Relationships) { - self.init(attributes: NoAttributes(), relationships: relationships, meta: .none, links: .none) - } -} - -extension ResourceObject where Description.Attributes == NoAttributes, EntityRawIdType == Unidentified { - public init(relationships: Description.Relationships, meta: MetaType, links: LinksType) { - self.init(attributes: NoAttributes(), relationships: relationships, meta: meta, links: links) - } -} - -extension ResourceObject where Description.Relationships == NoRelationships { - public init(id: ResourceObject.Id, attributes: Description.Attributes, meta: MetaType, links: LinksType) { - self.init(id: id, attributes: attributes, relationships: NoRelationships(), meta: meta, links: links) - } -} - -extension ResourceObject where Description.Relationships == NoRelationships, MetaType == NoMetadata { - public init(id: ResourceObject.Id, attributes: Description.Attributes, links: LinksType) { - self.init(id: id, attributes: attributes, meta: .none, links: links) - } -} - -extension ResourceObject where Description.Relationships == NoRelationships, LinksType == NoLinks { - public init(id: ResourceObject.Id, attributes: Description.Attributes, meta: MetaType) { - self.init(id: id, attributes: attributes, meta: meta, links: .none) - } -} - -extension ResourceObject where Description.Relationships == NoRelationships, MetaType == NoMetadata, LinksType == NoLinks { - public init(id: ResourceObject.Id, attributes: Description.Attributes) { - self.init(id: id, attributes: attributes, meta: .none, links: .none) - } -} - -extension ResourceObject where Description.Relationships == NoRelationships, EntityRawIdType: CreatableRawIdType { - public init(attributes: Description.Attributes, meta: MetaType, links: LinksType) { - self.init(attributes: attributes, relationships: NoRelationships(), meta: meta, links: links) - } -} - -extension ResourceObject where Description.Relationships == NoRelationships, MetaType == NoMetadata, EntityRawIdType: CreatableRawIdType { - public init(attributes: Description.Attributes, links: LinksType) { - self.init(attributes: attributes, relationships: NoRelationships(), meta: .none, links: links) - } -} - -extension ResourceObject where Description.Relationships == NoRelationships, LinksType == NoLinks, EntityRawIdType: CreatableRawIdType { - public init(attributes: Description.Attributes, meta: MetaType) { - self.init(attributes: attributes, relationships: NoRelationships(), meta: meta, links: .none) - } -} - -extension ResourceObject where Description.Relationships == NoRelationships, MetaType == NoMetadata, LinksType == NoLinks, EntityRawIdType: CreatableRawIdType { - public init(attributes: Description.Attributes) { - self.init(attributes: attributes, relationships: NoRelationships(), meta: .none, links: .none) - } -} - -extension ResourceObject where Description.Relationships == NoRelationships, EntityRawIdType == Unidentified { - public init(attributes: Description.Attributes, meta: MetaType, links: LinksType) { - self.init(attributes: attributes, relationships: NoRelationships(), meta: meta, links: links) - } -} - -extension ResourceObject where Description.Relationships == NoRelationships, MetaType == NoMetadata, EntityRawIdType == Unidentified { - public init(attributes: Description.Attributes, links: LinksType) { - self.init(attributes: attributes, relationships: NoRelationships(), meta: .none, links: links) - } -} - -extension ResourceObject where Description.Relationships == NoRelationships, LinksType == NoLinks, EntityRawIdType == Unidentified { - public init(attributes: Description.Attributes, meta: MetaType) { - self.init(attributes: attributes, relationships: NoRelationships(), meta: meta, links: .none) - } -} - -extension ResourceObject where Description.Relationships == NoRelationships, MetaType == NoMetadata, LinksType == NoLinks, EntityRawIdType == Unidentified { - public init(attributes: Description.Attributes) { - self.init(attributes: attributes, relationships: NoRelationships(), meta: .none, links: .none) - } -} - -extension ResourceObject where Description.Attributes == NoAttributes, Description.Relationships == NoRelationships { - public init(id: ResourceObject.Id, meta: MetaType, links: LinksType) { - self.init(id: id, attributes: NoAttributes(), relationships: NoRelationships(), meta: meta, links: links) - } -} - -extension ResourceObject where Description.Attributes == NoAttributes, Description.Relationships == NoRelationships, MetaType == NoMetadata { - public init(id: ResourceObject.Id, links: LinksType) { - self.init(id: id, attributes: NoAttributes(), relationships: NoRelationships(), meta: .none, links: links) - } -} - -extension ResourceObject where Description.Attributes == NoAttributes, Description.Relationships == NoRelationships, LinksType == NoLinks { - public init(id: ResourceObject.Id, meta: MetaType) { - self.init(id: id, attributes: NoAttributes(), relationships: NoRelationships(), meta: meta, links: .none) - } -} - -extension ResourceObject where Description.Attributes == NoAttributes, Description.Relationships == NoRelationships, MetaType == NoMetadata, LinksType == NoLinks { - public init(id: ResourceObject.Id) { - self.init(id: id, attributes: NoAttributes(), relationships: NoRelationships(), meta: .none, links: .none) - } -} - -extension ResourceObject where Description.Attributes == NoAttributes, Description.Relationships == NoRelationships, EntityRawIdType: CreatableRawIdType { - public init(meta: MetaType, links: LinksType) { - self.init(attributes: NoAttributes(), relationships: NoRelationships(), meta: meta, links: links) - } -} - -extension ResourceObject where Description.Attributes == NoAttributes, Description.Relationships == NoRelationships, MetaType == NoMetadata, EntityRawIdType: CreatableRawIdType { - public init(links: LinksType) { - self.init(attributes: NoAttributes(), relationships: NoRelationships(), meta: .none, links: links) - } -} - -extension ResourceObject where Description.Attributes == NoAttributes, Description.Relationships == NoRelationships, LinksType == NoLinks, EntityRawIdType: CreatableRawIdType { - public init(meta: MetaType) { - self.init(attributes: NoAttributes(), relationships: NoRelationships(), meta: meta, links: .none) - } -} - -extension ResourceObject where Description.Attributes == NoAttributes, Description.Relationships == NoRelationships, MetaType == NoMetadata, LinksType == NoLinks, EntityRawIdType: CreatableRawIdType { - public init() { - self.init(attributes: NoAttributes(), relationships: NoRelationships(), meta: .none, links: .none) - } -} - -extension ResourceObject where MetaType == NoMetadata { - public init(id: ResourceObject.Id, attributes: Description.Attributes, relationships: Description.Relationships, links: LinksType) { - self.init(id: id, attributes: attributes, relationships: relationships, meta: .none, links: links) - } -} - -extension ResourceObject where MetaType == NoMetadata, EntityRawIdType: CreatableRawIdType { - public init(attributes: Description.Attributes, relationships: Description.Relationships, links: LinksType) { - self.init(attributes: attributes, relationships: relationships, meta: .none, links: links) - } -} - -extension ResourceObject where MetaType == NoMetadata, EntityRawIdType == Unidentified { - public init(attributes: Description.Attributes, relationships: Description.Relationships, links: LinksType) { - self.init(attributes: attributes, relationships: relationships, meta: .none, links: links) - } -} - -extension ResourceObject where LinksType == NoLinks { - public init(id: ResourceObject.Id, attributes: Description.Attributes, relationships: Description.Relationships, meta: MetaType) { - self.init(id: id, attributes: attributes, relationships: relationships, meta: meta, links: .none) - } -} - -extension ResourceObject where LinksType == NoLinks, EntityRawIdType: CreatableRawIdType { - public init(attributes: Description.Attributes, relationships: Description.Relationships, meta: MetaType) { - self.init(attributes: attributes, relationships: relationships, meta: meta, links: .none) - } -} - -extension ResourceObject where LinksType == NoLinks, EntityRawIdType == Unidentified { - public init(attributes: Description.Attributes, relationships: Description.Relationships, meta: MetaType) { - self.init(attributes: attributes, relationships: relationships, meta: meta, links: .none) - } -} - -extension ResourceObject where MetaType == NoMetadata, LinksType == NoLinks { - public init(id: ResourceObject.Id, attributes: Description.Attributes, relationships: Description.Relationships) { - self.init(id: id, attributes: attributes, relationships: relationships, meta: .none, links: .none) - } -} - -extension ResourceObject where MetaType == NoMetadata, LinksType == NoLinks, EntityRawIdType: CreatableRawIdType { - public init(attributes: Description.Attributes, relationships: Description.Relationships) { - self.init(attributes: attributes, relationships: relationships, meta: .none, links: .none) - } -} - -extension ResourceObject where MetaType == NoMetadata, LinksType == NoLinks, EntityRawIdType == Unidentified { - public init(attributes: Description.Attributes, relationships: Description.Relationships) { - self.init(attributes: attributes, relationships: relationships, meta: .none, links: .none) - } -} -*/ - // MARK: - Pointer for Relationships use public extension ResourceObject where EntityRawIdType: JSONAPI.RawIdType { @@ -456,6 +226,7 @@ public extension ResourceObjectProxy { /// Access the attribute at the given keypath. This just /// allows you to write `resourceObject[\.propertyName]` instead /// of `resourceObject.attributes.propertyName.value`. + @available(*, deprecated, message: "This will be removed in a future version in favor of `resource.` (dynamic member lookup)") subscript(_ path: KeyPath) -> T.ValueType { return attributes[keyPath: path].value } @@ -463,6 +234,7 @@ public extension ResourceObjectProxy { /// Access the attribute at the given keypath. This just /// allows you to write `resourceObject[\.propertyName]` instead /// of `resourceObject.attributes.propertyName.value`. + @available(*, deprecated, message: "This will be removed in a future version in favor of `resource.` (dynamic member lookup)") subscript(_ path: KeyPath) -> T.ValueType? { return attributes[keyPath: path]?.value } @@ -470,6 +242,7 @@ public extension ResourceObjectProxy { /// Access the attribute at the given keypath. This just /// allows you to write `resourceObject[\.propertyName]` instead /// of `resourceObject.attributes.propertyName.value`. + @available(*, deprecated, message: "This will be removed in a future version in favor of `resource.` (dynamic member lookup)") subscript(_ path: KeyPath) -> U? where T.ValueType == U? { // Implementation Note: Handles Transform that returns optional // type. @@ -517,6 +290,7 @@ public extension ResourceObjectProxy { // MARK: Keypath Subscript Lookup /// Access an attribute requiring a transformation on the RawValue _and_ /// a secondary transformation on this entity (self). + @available(*, deprecated, message: "This will be removed in a future version in favor of `resource.` (dynamic member lookup)") subscript(_ path: KeyPath T>) -> T { return attributes[keyPath: path](self) } diff --git a/Tests/JSONAPITests/Attribute/Attribute+FunctorTests.swift b/Tests/JSONAPITests/Attribute/Attribute+FunctorTests.swift index ab2d7b7..78f217f 100644 --- a/Tests/JSONAPITests/Attribute/Attribute+FunctorTests.swift +++ b/Tests/JSONAPITests/Attribute/Attribute+FunctorTests.swift @@ -15,27 +15,46 @@ class Attribute_FunctorTests: XCTestCase { XCTAssertNotNil(entity) - XCTAssertEqual(entity?[\.computedString], "Frankie2") + XCTAssertEqual(entity?.computedString, "Frankie2") } + @available(*, deprecated, message: "remove next major version") + func test_mapGuaranteed_deprecated() { + let entity = try? TestType(attributes: .init(name: "Frankie", number: .init(rawValue: 22.0)), relationships: .none, meta: .none, links: .none) + + XCTAssertEqual(entity?[\.computedString], "Frankie2") + } + func test_mapOptionalSuccess() { let entity = try? TestType(attributes: .init(name: "Frankie", number: .init(rawValue: 22.0)), relationships: .none, meta: .none, links: .none) XCTAssertNotNil(entity) - XCTAssertEqual(entity?[\.computedNumber], 22) XCTAssertEqual(entity?.computedNumber, 22) } + @available(*, deprecated, message: "remove next major version") + func test_mapOptionalSuccess_deprecated() { + let entity = try? TestType(attributes: .init(name: "Frankie", number: .init(rawValue: 22.0)), relationships: .none, meta: .none, links: .none) + + XCTAssertEqual(entity?[\.computedNumber], 22) + } + func test_mapOptionalFailure() { let entity = try? TestType(attributes: .init(name: "Frankie", number: .init(rawValue: 22.5)), relationships: .none, meta: .none, links: .none) XCTAssertNotNil(entity) - XCTAssertNil(entity?[\.computedNumber]) XCTAssertNil(entity?.computedNumber) } + + @available(*, deprecated, message: "remove next major version") + func test_mapOptionalFailure_deprecated() { + let entity = try? TestType(attributes: .init(name: "Frankie", number: .init(rawValue: 22.5)), relationships: .none, meta: .none, links: .none) + + XCTAssertNil(entity?[\.computedNumber]) + } } // MARK: Test types diff --git a/Tests/JSONAPITests/Computed Properties/ComputedPropertiesTests.swift b/Tests/JSONAPITests/Computed Properties/ComputedPropertiesTests.swift index add302d..39ea74d 100644 --- a/Tests/JSONAPITests/Computed Properties/ComputedPropertiesTests.swift +++ b/Tests/JSONAPITests/Computed Properties/ComputedPropertiesTests.swift @@ -14,12 +14,18 @@ class ComputedPropertiesTests: XCTestCase { let entity = decoded(type: TestType.self, data: computed_property_attribute) XCTAssertEqual(entity.id, "1234") - XCTAssertEqual(entity[\.name], "Sarah") XCTAssertEqual(entity.name, "Sarah") XCTAssertEqual(entity ~> \.other, "5678") XCTAssertNoThrow(try TestType.check(entity)) } + @available(*, deprecated, message: "remove next major version") + func test_DecodeIgnoresComputed_deprecated() { + let entity = decoded(type: TestType.self, data: computed_property_attribute) + + XCTAssertEqual(entity[\.name], "Sarah") + } + func test_EncodeIgnoresComputed() { test_DecodeEncodeEquality(type: TestType.self, data: computed_property_attribute) } @@ -27,11 +33,17 @@ class ComputedPropertiesTests: XCTestCase { func test_ComputedAttributeAccess() { let entity = decoded(type: TestType.self, data: computed_property_attribute) - XCTAssertEqual(entity[\.computed], "Sarah2") XCTAssertEqual(entity.computed, "Sarah2") XCTAssertEqual(entity[direct: \.directSecretsOut], "shhhh") } + @available(*, deprecated, message: "remove next major version") + func test_ComputedAttributeAccess_deprecated() { + let entity = decoded(type: TestType.self, data: computed_property_attribute) + + XCTAssertEqual(entity[\.computed], "Sarah2") + } + func test_ComputedNonAttributeAccess() { let entity = decoded(type: TestType.self, data: computed_property_attribute) diff --git a/Tests/JSONAPITests/Custom Attributes Tests/CustomAttributesTests.swift b/Tests/JSONAPITests/Custom Attributes Tests/CustomAttributesTests.swift index 91e8d8f..ec0e7f1 100644 --- a/Tests/JSONAPITests/Custom Attributes Tests/CustomAttributesTests.swift +++ b/Tests/JSONAPITests/Custom Attributes Tests/CustomAttributesTests.swift @@ -13,13 +13,19 @@ class CustomAttributesTests: XCTestCase { func test_customDecode() { let entity = decoded(type: CustomAttributeEntity.self, data: customAttributeEntityData) - XCTAssertEqual(entity[\.firstName], "Cool") XCTAssertEqual(entity.firstName, "Cool") - XCTAssertEqual(entity[\.name], "Cool Name") XCTAssertEqual(entity.name, "Cool Name") XCTAssertNoThrow(try CustomAttributeEntity.check(entity)) } + @available(*, deprecated, message: "remove next major version") + func test_customDecode_deprecated() { + let entity = decoded(type: CustomAttributeEntity.self, data: customAttributeEntityData) + + XCTAssertEqual(entity[\.firstName], "Cool") + XCTAssertEqual(entity[\.name], "Cool Name") + } + func test_customEncode() { test_DecodeEncodeEquality(type: CustomAttributeEntity.self, data: customAttributeEntityData) @@ -28,13 +34,19 @@ class CustomAttributesTests: XCTestCase { func test_customKeysDecode() { let entity = decoded(type: CustomKeysEntity.self, data: customAttributeEntityData) - XCTAssertEqual(entity[\.firstNameSilly], "Cool") XCTAssertEqual(entity.firstNameSilly, "Cool") - XCTAssertEqual(entity[\.lastNameSilly], "Name") XCTAssertEqual(entity.lastNameSilly, "Name") XCTAssertNoThrow(try CustomKeysEntity.check(entity)) } + @available(*, deprecated, message: "remove next major version") + func test_customKeysDecode_deprecated() { + let entity = decoded(type: CustomKeysEntity.self, data: customAttributeEntityData) + + XCTAssertEqual(entity[\.firstNameSilly], "Cool") + XCTAssertEqual(entity[\.lastNameSilly], "Name") + } + func test_customKeysEncode() { test_DecodeEncodeEquality(type: CustomKeysEntity.self, data: customAttributeEntityData) diff --git a/Tests/JSONAPITests/Poly/PolyProxyTests.swift b/Tests/JSONAPITests/Poly/PolyProxyTests.swift index 3d12259..582a2fc 100644 --- a/Tests/JSONAPITests/Poly/PolyProxyTests.swift +++ b/Tests/JSONAPITests/Poly/PolyProxyTests.swift @@ -21,12 +21,19 @@ public class PolyProxyTests: XCTestCase { XCTAssertEqual(polyUserA.userA, userA) XCTAssertNil(polyUserA.userB) - XCTAssertEqual(polyUserA[\.name], "Ken Moore") + XCTAssertEqual(polyUserA.name, "Ken Moore") XCTAssertEqual(polyUserA.id, "1") XCTAssertEqual(polyUserA.relationships, .none) XCTAssertEqual(polyUserA[direct: \.x], .init(x: "y")) } + @available(*, deprecated, message: "remove next major version") + func test_UserADecode_deprecated() { + let polyUserA = decoded(type: User.self, data: poly_user_stub_1) + + XCTAssertEqual(polyUserA[\.name], "Ken Moore") + } + func test_UserAAndBEncodeEquality() { test_DecodeEncodeEquality(type: User.self, data: poly_user_stub_1) test_DecodeEncodeEquality(type: User.self, data: poly_user_stub_2) @@ -56,11 +63,18 @@ public class PolyProxyTests: XCTestCase { XCTAssertEqual(polyUserB.userB, userB) XCTAssertNil(polyUserB.userA) - XCTAssertEqual(polyUserB[\.name], "Ken Less") + XCTAssertEqual(polyUserB.name, "Ken Less") XCTAssertEqual(polyUserB.id, "2") XCTAssertEqual(polyUserB.relationships, .none) XCTAssertEqual(polyUserB[direct: \.x], .init(x: "y")) } + + @available(*, deprecated, message: "remove next major version") + func test_UserBDecode_deprecated() { + let polyUserB = decoded(type: User.self, data: poly_user_stub_2) + + XCTAssertEqual(polyUserB[\.name], "Ken Less") + } } // MARK: - Test types @@ -114,9 +128,9 @@ extension Poly2: ResourceObjectProxy, JSONTyped where A == PolyProxyTests.UserA, public var attributes: SharedUserDescription.Attributes { switch self { case .a(let a): - return .init(name: .init(value: "\(a[\.firstName]) \(a[\.lastName])"), x: .init(x: "y")) + return .init(name: .init(value: "\(a.firstName) \(a.lastName)"), x: .init(x: "y")) case .b(let b): - return .init(name: .init(value: b[\.name].joined(separator: " ")), x: .init(x: "y")) + return .init(name: .init(value: b.name.joined(separator: " ")), x: .init(x: "y")) } } diff --git a/Tests/JSONAPITests/ResourceObject/ResourceObjectTests.swift b/Tests/JSONAPITests/ResourceObject/ResourceObjectTests.swift index 2e023a0..c061f1e 100644 --- a/Tests/JSONAPITests/ResourceObject/ResourceObjectTests.swift +++ b/Tests/JSONAPITests/ResourceObject/ResourceObjectTests.swift @@ -69,10 +69,16 @@ class ResourceObjectTests: XCTestCase { func test_unidentifiedEntityAttributeAccess() { let entity = UnidentifiedTestEntity(attributes: .init(me: "hello"), relationships: .none, meta: .none, links: .none) - XCTAssertEqual(entity[\.me], "hello") XCTAssertEqual(entity.me, "hello") } + @available(*, deprecated, message: "remove next major version") + func test_unidentifiedEntityAttributeAccess_deprecated() { + let entity = UnidentifiedTestEntity(attributes: .init(me: "hello"), relationships: .none, meta: .none, links: .none) + + XCTAssertEqual(entity[\.me], "hello") + } + func test_initialization() { let entity1 = TestEntity1(id: .init(rawValue: "wow"), attributes: .none, relationships: .none, meta: .none, links: .none) let entity2 = TestEntity2(id: .init(rawValue: "cool"), attributes: .none, relationships: .init(other: .init(resourceObject: entity1)), meta: .none, links: .none) @@ -158,13 +164,19 @@ extension ResourceObjectTests { XCTAssert(type(of: entity.relationships) == NoRelationships.self) - XCTAssertEqual(entity[\.floater], 123.321) XCTAssertEqual(entity.floater, 123.321) XCTAssertNoThrow(try TestEntity5.check(entity)) testEncoded(entity: entity) } + @available(*, deprecated, message: "remove next major version") + func test_EntityNoRelationshipsSomeAttributes_deprecated() { + let entity = decoded(type: TestEntity5.self, + data: entity_no_relationships_some_attributes) + XCTAssertEqual(entity[\.floater], 123.321) + } + func test_EntityNoRelationshipsSomeAttributes_encode() { test_DecodeEncodeEquality(type: TestEntity5.self, data: entity_no_relationships_some_attributes) @@ -191,9 +203,7 @@ extension ResourceObjectTests { let entity = decoded(type: TestEntity4.self, data: entity_some_relationships_some_attributes) - XCTAssertEqual(entity[\.word], "coolio") XCTAssertEqual(entity.word, "coolio") - XCTAssertEqual(entity[\.number], 992299) XCTAssertEqual(entity.number, 992299) XCTAssertEqual((entity ~> \.other).rawValue, "2DF03B69-4B0A-467F-B52E-B0C9E44FCECF") XCTAssertNoThrow(try TestEntity4.check(entity)) @@ -201,6 +211,15 @@ extension ResourceObjectTests { testEncoded(entity: entity) } + @available(*, deprecated, message: "remove next major version") + func test_EntitySomeRelationshipsSomeAttributes_deprecated() { + let entity = decoded(type: TestEntity4.self, + data: entity_some_relationships_some_attributes) + + XCTAssertEqual(entity[\.word], "coolio") + XCTAssertEqual(entity[\.number], 992299) + } + func test_EntitySomeRelationshipsSomeAttributes_encode() { test_DecodeEncodeEquality(type: TestEntity4.self, data: entity_some_relationships_some_attributes) @@ -214,17 +233,24 @@ extension ResourceObjectTests { let entity = decoded(type: TestEntity6.self, data: entity_one_omitted_attribute) - XCTAssertEqual(entity[\.here], "Hello") XCTAssertEqual(entity.here, "Hello") - XCTAssertNil(entity[\.maybeHere]) XCTAssertNil(entity.maybeHere) - XCTAssertEqual(entity[\.maybeNull], "World") XCTAssertEqual(entity.maybeNull, "World") XCTAssertNoThrow(try TestEntity6.check(entity)) testEncoded(entity: entity) } + @available(*, deprecated, message: "remove next major version") + func test_entityOneOmittedAttribute_deprecated() { + let entity = decoded(type: TestEntity6.self, + data: entity_one_omitted_attribute) + + XCTAssertEqual(entity[\.here], "Hello") + XCTAssertNil(entity[\.maybeHere]) + XCTAssertEqual(entity[\.maybeNull], "World") + } + func test_entityOneOmittedAttribute_encode() { test_DecodeEncodeEquality(type: TestEntity6.self, data: entity_one_omitted_attribute) @@ -234,17 +260,24 @@ extension ResourceObjectTests { let entity = decoded(type: TestEntity6.self, data: entity_one_null_attribute) - XCTAssertEqual(entity[\.here], "Hello") XCTAssertEqual(entity.here, "Hello") - XCTAssertEqual(entity[\.maybeHere], "World") XCTAssertEqual(entity.maybeHere, "World") - XCTAssertNil(entity[\.maybeNull]) XCTAssertNil(entity.maybeNull) XCTAssertNoThrow(try TestEntity6.check(entity)) testEncoded(entity: entity) } + @available(*, deprecated, message: "remove next major version") + func test_entityOneNullAttribute_deprecated() { + let entity = decoded(type: TestEntity6.self, + data: entity_one_null_attribute) + + XCTAssertEqual(entity[\.here], "Hello") + XCTAssertEqual(entity[\.maybeHere], "World") + XCTAssertNil(entity[\.maybeNull]) + } + func test_entityOneNullAttribute_encode() { test_DecodeEncodeEquality(type: TestEntity6.self, data: entity_one_null_attribute) @@ -254,17 +287,24 @@ extension ResourceObjectTests { let entity = decoded(type: TestEntity6.self, data: entity_all_attributes) - XCTAssertEqual(entity[\.here], "Hello") XCTAssertEqual(entity.here, "Hello") - XCTAssertEqual(entity[\.maybeHere], "World") XCTAssertEqual(entity.maybeHere, "World") - XCTAssertEqual(entity[\.maybeNull], "!") XCTAssertEqual(entity.maybeNull, "!") XCTAssertNoThrow(try TestEntity6.check(entity)) testEncoded(entity: entity) } + @available(*, deprecated, message: "remove next major version") + func test_entityAllAttribute_deprecated() { + let entity = decoded(type: TestEntity6.self, + data: entity_all_attributes) + + XCTAssertEqual(entity[\.here], "Hello") + XCTAssertEqual(entity[\.maybeHere], "World") + XCTAssertEqual(entity[\.maybeNull], "!") + } + func test_entityAllAttribute_encode() { test_DecodeEncodeEquality(type: TestEntity6.self, data: entity_all_attributes) @@ -274,17 +314,24 @@ extension ResourceObjectTests { let entity = decoded(type: TestEntity6.self, data: entity_one_null_and_one_missing_attribute) - XCTAssertEqual(entity[\.here], "Hello") XCTAssertEqual(entity.here, "Hello") - XCTAssertNil(entity[\.maybeHere]) XCTAssertNil(entity.maybeHere) - XCTAssertNil(entity[\.maybeNull]) XCTAssertNil(entity.maybeNull) XCTAssertNoThrow(try TestEntity6.check(entity)) testEncoded(entity: entity) } + @available(*, deprecated, message: "remove next major version") + func test_entityOneNullAndOneOmittedAttribute_deprecated() { + let entity = decoded(type: TestEntity6.self, + data: entity_one_null_and_one_missing_attribute) + + XCTAssertEqual(entity[\.here], "Hello") + XCTAssertNil(entity[\.maybeHere]) + XCTAssertNil(entity[\.maybeNull]) + } + func test_entityOneNullAndOneOmittedAttribute_encode() { test_DecodeEncodeEquality(type: TestEntity6.self, data: entity_one_null_and_one_missing_attribute) @@ -299,15 +346,22 @@ extension ResourceObjectTests { let entity = decoded(type: TestEntity7.self, data: entity_null_optional_nullable_attribute) - XCTAssertEqual(entity[\.here], "Hello") XCTAssertEqual(entity.here, "Hello") - XCTAssertNil(entity[\.maybeHereMaybeNull]) XCTAssertNil(entity.maybeHereMaybeNull) XCTAssertNoThrow(try TestEntity7.check(entity)) testEncoded(entity: entity) } + @available(*, deprecated, message: "remove next major version") + func test_NullOptionalNullableAttribute_deprecated() { + let entity = decoded(type: TestEntity7.self, + data: entity_null_optional_nullable_attribute) + + XCTAssertEqual(entity[\.here], "Hello") + XCTAssertNil(entity[\.maybeHereMaybeNull]) + } + func test_NullOptionalNullableAttribute_encode() { test_DecodeEncodeEquality(type: TestEntity7.self, data: entity_null_optional_nullable_attribute) @@ -317,15 +371,22 @@ extension ResourceObjectTests { let entity = decoded(type: TestEntity7.self, data: entity_non_null_optional_nullable_attribute) - XCTAssertEqual(entity[\.here], "Hello") XCTAssertEqual(entity.here, "Hello") - XCTAssertEqual(entity[\.maybeHereMaybeNull], "World") XCTAssertEqual(entity.maybeHereMaybeNull, "World") XCTAssertNoThrow(try TestEntity7.check(entity)) testEncoded(entity: entity) } + @available(*, deprecated, message: "remove next major version") + func test_NonNullOptionalNullableAttribute_deprecated() { + let entity = decoded(type: TestEntity7.self, + data: entity_non_null_optional_nullable_attribute) + + XCTAssertEqual(entity[\.here], "Hello") + XCTAssertEqual(entity[\.maybeHereMaybeNull], "World") + } + func test_NonNullOptionalNullableAttribute_encode() { test_DecodeEncodeEquality(type: TestEntity7.self, data: entity_non_null_optional_nullable_attribute) @@ -338,23 +399,30 @@ extension ResourceObjectTests { let entity = decoded(type: TestEntity8.self, data: entity_int_to_string_attribute) - XCTAssertEqual(entity[\.string], "22") XCTAssertEqual(entity.string, "22") - XCTAssertEqual(entity[\.int], 22) XCTAssertEqual(entity.int, 22) - XCTAssertEqual(entity[\.stringFromInt], "22") XCTAssertEqual(entity.stringFromInt, "22") - XCTAssertEqual(entity[\.plus], 122) XCTAssertEqual(entity.plus, 122) - XCTAssertEqual(entity[\.doubleFromInt], 22.0) XCTAssertEqual(entity.doubleFromInt, 22.0) - XCTAssertEqual(entity[\.nullToString], "nil") XCTAssertEqual(entity.nullToString, "nil") XCTAssertNoThrow(try TestEntity8.check(entity)) testEncoded(entity: entity) } + @available(*, deprecated, message: "remove next major version") + func test_IntToString_deprecated() { + let entity = decoded(type: TestEntity8.self, + data: entity_int_to_string_attribute) + + XCTAssertEqual(entity[\.string], "22") + XCTAssertEqual(entity[\.int], 22) + XCTAssertEqual(entity[\.stringFromInt], "22") + XCTAssertEqual(entity[\.plus], 122) + XCTAssertEqual(entity[\.doubleFromInt], 22.0) + XCTAssertEqual(entity[\.nullToString], "nil") + } + func test_IntToString_encode() { test_DecodeEncodeEquality(type: TestEntity8.self, data: entity_int_to_string_attribute) @@ -503,7 +571,6 @@ extension ResourceObjectTests { let entity = decoded(type: UnidentifiedTestEntity.self, data: entity_unidentified) - XCTAssertNil(entity[\.me]) XCTAssertNil(entity.me) XCTAssertEqual(entity.id, .unidentified) XCTAssertNoThrow(try UnidentifiedTestEntity.check(entity)) @@ -511,6 +578,14 @@ extension ResourceObjectTests { testEncoded(entity: entity) } + @available(*, deprecated, message: "remove next major version") + func test_UnidentifiedEntity_deprecated() { + let entity = decoded(type: UnidentifiedTestEntity.self, + data: entity_unidentified) + + XCTAssertNil(entity[\.me]) + } + func test_UnidentifiedEntity_encode() { test_DecodeEncodeEquality(type: UnidentifiedTestEntity.self, data: entity_unidentified) @@ -520,7 +595,6 @@ extension ResourceObjectTests { let entity = decoded(type: UnidentifiedTestEntity.self, data: entity_unidentified_with_attributes) - XCTAssertEqual(entity[\.me], "unknown") XCTAssertEqual(entity.me, "unknown") XCTAssertEqual(entity.id, .unidentified) XCTAssertNoThrow(try UnidentifiedTestEntity.check(entity)) @@ -528,6 +602,14 @@ extension ResourceObjectTests { testEncoded(entity: entity) } + @available(*, deprecated, message: "remove next major version") + func test_UnidentifiedEntityWithAttributes_deprecated() { + let entity = decoded(type: UnidentifiedTestEntity.self, + data: entity_unidentified_with_attributes) + + XCTAssertEqual(entity[\.me], "unknown") + } + func test_UnidentifiedEntityWithAttributes_encode() { test_DecodeEncodeEquality(type: UnidentifiedTestEntity.self, data: entity_unidentified_with_attributes) @@ -541,7 +623,6 @@ extension ResourceObjectTests { let entity = decoded(type: UnidentifiedTestEntityWithMeta.self, data: entity_unidentified_with_attributes_and_meta) - XCTAssertEqual(entity[\.me], "unknown") XCTAssertEqual(entity.me, "unknown") XCTAssertEqual(entity.id, .unidentified) XCTAssertEqual(entity.meta.x, "world") @@ -551,6 +632,14 @@ extension ResourceObjectTests { testEncoded(entity: entity) } + @available(*, deprecated, message: "remove next major version") + func test_UnidentifiedEntityWithAttributesAndMeta_deprecated() { + let entity = decoded(type: UnidentifiedTestEntityWithMeta.self, + data: entity_unidentified_with_attributes_and_meta) + + XCTAssertEqual(entity[\.me], "unknown") + } + func test_UnidentifiedEntityWithAttributesAndMeta_encode() { test_DecodeEncodeEquality(type: UnidentifiedTestEntityWithMeta.self, data: entity_unidentified_with_attributes_and_meta) @@ -560,7 +649,6 @@ extension ResourceObjectTests { let entity = decoded(type: UnidentifiedTestEntityWithLinks.self, data: entity_unidentified_with_attributes_and_links) - XCTAssertEqual(entity[\.me], "unknown") XCTAssertEqual(entity.me, "unknown") XCTAssertEqual(entity.id, .unidentified) XCTAssertEqual(entity.links.link1, .init(url: "https://image.com/image.png")) @@ -569,6 +657,14 @@ extension ResourceObjectTests { testEncoded(entity: entity) } + @available(*, deprecated, message: "remove next major version") + func test_UnidentifiedEntityWithAttributesAndLinks_deprecated() { + let entity = decoded(type: UnidentifiedTestEntityWithLinks.self, + data: entity_unidentified_with_attributes_and_links) + + XCTAssertEqual(entity[\.me], "unknown") + } + func test_UnidentifiedEntityWithAttributesAndLinks_encode() { test_DecodeEncodeEquality(type: UnidentifiedTestEntityWithLinks.self, data: entity_unidentified_with_attributes_and_links) @@ -578,7 +674,6 @@ extension ResourceObjectTests { let entity = decoded(type: UnidentifiedTestEntityWithMetaAndLinks.self, data: entity_unidentified_with_attributes_and_meta_and_links) - XCTAssertEqual(entity[\.me], "unknown") XCTAssertEqual(entity.me, "unknown") XCTAssertEqual(entity.id, .unidentified) XCTAssertEqual(entity.meta.x, "world") @@ -589,6 +684,14 @@ extension ResourceObjectTests { testEncoded(entity: entity) } + @available(*, deprecated, message: "remove next major version") + func test_UnidentifiedEntityWithAttributesAndMetaAndLinks_deprecated() { + let entity = decoded(type: UnidentifiedTestEntityWithMetaAndLinks.self, + data: entity_unidentified_with_attributes_and_meta_and_links) + + XCTAssertEqual(entity[\.me], "unknown") + } + func test_UnidentifiedEntityWithAttributesAndMetaAndLinks_encode() { test_DecodeEncodeEquality(type: UnidentifiedTestEntityWithMetaAndLinks.self, data: entity_unidentified_with_attributes_and_meta_and_links) @@ -598,9 +701,7 @@ extension ResourceObjectTests { let entity = decoded(type: TestEntity4WithMeta.self, data: entity_some_relationships_some_attributes_with_meta) - XCTAssertEqual(entity[\.word], "coolio") XCTAssertEqual(entity.word, "coolio") - XCTAssertEqual(entity[\.number], 992299) XCTAssertEqual(entity.number, 992299) XCTAssertEqual((entity ~> \.other).rawValue, "2DF03B69-4B0A-467F-B52E-B0C9E44FCECF") XCTAssertEqual(entity.meta.x, "world") @@ -610,6 +711,15 @@ extension ResourceObjectTests { testEncoded(entity: entity) } + @available(*, deprecated, message: "remove next major version") + func test_EntitySomeRelationshipsSomeAttributesWithMeta_deprecated() { + let entity = decoded(type: TestEntity4WithMeta.self, + data: entity_some_relationships_some_attributes_with_meta) + + XCTAssertEqual(entity[\.word], "coolio") + XCTAssertEqual(entity[\.number], 992299) + } + func test_EntitySomeRelationshipsSomeAttributesWithMeta_encode() { test_DecodeEncodeEquality(type: TestEntity4WithMeta.self, data: entity_some_relationships_some_attributes_with_meta) @@ -619,9 +729,7 @@ extension ResourceObjectTests { let entity = decoded(type: TestEntity4WithLinks.self, data: entity_some_relationships_some_attributes_with_links) - XCTAssertEqual(entity[\.word], "coolio") XCTAssertEqual(entity.word, "coolio") - XCTAssertEqual(entity[\.number], 992299) XCTAssertEqual(entity.number, 992299) XCTAssertEqual((entity ~> \.other).rawValue, "2DF03B69-4B0A-467F-B52E-B0C9E44FCECF") XCTAssertEqual(entity.links.link1, .init(url: "https://image.com/image.png")) @@ -630,6 +738,15 @@ extension ResourceObjectTests { testEncoded(entity: entity) } + @available(*, deprecated, message: "remove next major version") + func test_EntitySomeRelationshipsSomeAttributesWithLinks_deprecated() { + let entity = decoded(type: TestEntity4WithLinks.self, + data: entity_some_relationships_some_attributes_with_links) + + XCTAssertEqual(entity[\.word], "coolio") + XCTAssertEqual(entity[\.number], 992299) + } + func test_EntitySomeRelationshipsSomeAttributesWithLinks_encode() { test_DecodeEncodeEquality(type: TestEntity4WithLinks.self, data: entity_some_relationships_some_attributes_with_links) @@ -639,9 +756,7 @@ extension ResourceObjectTests { let entity = decoded(type: TestEntity4WithMetaAndLinks.self, data: entity_some_relationships_some_attributes_with_meta_and_links) - XCTAssertEqual(entity[\.word], "coolio") XCTAssertEqual(entity.word, "coolio") - XCTAssertEqual(entity[\.number], 992299) XCTAssertEqual(entity.number, 992299) XCTAssertEqual((entity ~> \.other).rawValue, "2DF03B69-4B0A-467F-B52E-B0C9E44FCECF") XCTAssertEqual(entity.meta.x, "world") @@ -652,6 +767,15 @@ extension ResourceObjectTests { testEncoded(entity: entity) } + @available(*, deprecated, message: "remove next major version") + func test_EntitySomeRelationshipsSomeAttributesWithMetaAndLinks_deprecated() { + let entity = decoded(type: TestEntity4WithMetaAndLinks.self, + data: entity_some_relationships_some_attributes_with_meta_and_links) + + XCTAssertEqual(entity[\.word], "coolio") + XCTAssertEqual(entity[\.number], 992299) + } + func test_EntitySomeRelationshipsSomeAttributesWithMetaAndLinks_encode() { test_DecodeEncodeEquality(type: TestEntity4WithMetaAndLinks.self, data: entity_some_relationships_some_attributes_with_meta_and_links) @@ -673,11 +797,26 @@ extension ResourceObjectTests { meta: .none, links: .none) - XCTAssertEqual(entity1[\.metaAttribute], true) XCTAssertEqual(entity1.metaAttribute, true) - XCTAssertEqual(entity2[\.metaAttribute], false) XCTAssertEqual(entity2.metaAttribute, false) } + + @available(*, deprecated, message: "remove next major version") + func test_MetaEntityAttributeAccessWorks_deprecated() { + let entity1 = TestEntityWithMetaAttribute(id: "even", + attributes: .init(), + relationships: .none, + meta: .none, + links: .none) + let entity2 = TestEntityWithMetaAttribute(id: "odd", + attributes: .init(), + relationships: .none, + meta: .none, + links: .none) + + XCTAssertEqual(entity1[\.metaAttribute], true) + XCTAssertEqual(entity2[\.metaAttribute], false) + } } // MARK: With a Meta Relationship diff --git a/Tests/JSONAPITests/SparseFields/SparseFieldsetTests.swift b/Tests/JSONAPITests/SparseFields/SparseFieldsetTests.swift index 003dcc5..bffe9dd 100644 --- a/Tests/JSONAPITests/SparseFields/SparseFieldsetTests.swift +++ b/Tests/JSONAPITests/SparseFields/SparseFieldsetTests.swift @@ -31,6 +31,38 @@ class SparseFieldsetTests: XCTestCase { XCTAssertNil(relationships) XCTAssertEqual(attributesDict?.count, 9) // note not 10 because one value is omitted intentionally at initialization + XCTAssertEqual(attributesDict?["bool"] as? Bool, + testEverythingObject.bool) + XCTAssertEqual(attributesDict?["int"] as? Int, + testEverythingObject.int) + XCTAssertEqual(attributesDict?["double"] as? Double, + testEverythingObject.double) + XCTAssertEqual(attributesDict?["string"] as? String, + testEverythingObject.string) + XCTAssertEqual((attributesDict?["nestedStruct"] as? [String: String])?["hello"], + testEverythingObject.nestedStruct.hello) + XCTAssertEqual(attributesDict?["nestedEnum"] as? String, + testEverythingObject.nestedEnum.rawValue) + XCTAssertEqual(attributesDict?["array"] as? [Bool], + testEverythingObject.array) + XCTAssertNil(attributesDict?["optional"]) + XCTAssertNotNil(attributesDict?["nullable"] as? NSNull) + XCTAssertNotNil(attributesDict?["optionalNullable"] as? NSNull) + } + + @available(*, deprecated, message: "remove next major version") + func test_FullEncode_deprecated() { + let jsonEncoder = JSONEncoder() + let sparseWithEverything = SparseFieldset(testEverythingObject, fields: EverythingTest.Attributes.CodingKeys.allCases) + + let encoded = try! jsonEncoder.encode(sparseWithEverything) + + let deserialized = try! JSONSerialization.jsonObject(with: encoded, + options: []) + + let outerDict = deserialized as? [String: Any] + let attributesDict = outerDict?["attributes"] as? [String: Any] + XCTAssertEqual(attributesDict?["bool"] as? Bool, testEverythingObject[\.bool]) XCTAssertEqual(attributesDict?["int"] as? Int, @@ -45,9 +77,6 @@ class SparseFieldsetTests: XCTestCase { testEverythingObject[\.nestedEnum].rawValue) XCTAssertEqual(attributesDict?["array"] as? [Bool], testEverythingObject[\.array]) - XCTAssertNil(attributesDict?["optional"]) - XCTAssertNotNil(attributesDict?["nullable"] as? NSNull) - XCTAssertNotNil(attributesDict?["optionalNullable"] as? NSNull) } func test_PartialEncode() { @@ -71,20 +100,48 @@ class SparseFieldsetTests: XCTestCase { XCTAssertEqual(attributesDict?.count, 3) XCTAssertEqual(attributesDict?["bool"] as? Bool, - testEverythingObject[\.bool]) + testEverythingObject.bool) XCTAssertNil(attributesDict?["int"]) XCTAssertNil(attributesDict?["double"]) XCTAssertEqual(attributesDict?["string"] as? String, - testEverythingObject[\.string]) + testEverythingObject.string) XCTAssertNil(attributesDict?["nestedStruct"]) XCTAssertNil(attributesDict?["nestedEnum"]) XCTAssertEqual(attributesDict?["array"] as? [Bool], - testEverythingObject[\.array]) + testEverythingObject.array) XCTAssertNil(attributesDict?["optional"]) XCTAssertNil(attributesDict?["nullable"]) XCTAssertNil(attributesDict?["optionalNullable"]) } + @available(*, deprecated, message: "remove next major version") + func test_PartialEncode_deprecated() { + let jsonEncoder = JSONEncoder() + let sparseObject = SparseFieldset(testEverythingObject, fields: [.string, .bool, .array]) + + let encoded = try! jsonEncoder.encode(sparseObject) + + let deserialized = try! JSONSerialization.jsonObject(with: encoded, + options: []) + + let outerDict = deserialized as? [String: Any] + let id = outerDict?["id"] as? String + let type = outerDict?["type"] as? String + let attributesDict = outerDict?["attributes"] as? [String: Any] + let relationships = outerDict?["relationships"] + + XCTAssertEqual(id, testEverythingObject.id.rawValue) + XCTAssertEqual(type, EverythingTest.jsonType) + XCTAssertNil(relationships) + + XCTAssertEqual(attributesDict?["bool"] as? Bool, + testEverythingObject[\.bool]) + XCTAssertEqual(attributesDict?["string"] as? String, + testEverythingObject[\.string]) + XCTAssertEqual(attributesDict?["array"] as? [Bool], + testEverythingObject[\.array]) + } + func test_sparseFieldsMethod() { let jsonEncoder = JSONEncoder() let sparseObject = testEverythingObject.sparse(with: [.string, .bool, .array]) @@ -106,19 +163,47 @@ class SparseFieldsetTests: XCTestCase { XCTAssertEqual(attributesDict?.count, 3) XCTAssertEqual(attributesDict?["bool"] as? Bool, - testEverythingObject[\.bool]) + testEverythingObject.bool) XCTAssertNil(attributesDict?["int"]) XCTAssertNil(attributesDict?["double"]) XCTAssertEqual(attributesDict?["string"] as? String, - testEverythingObject[\.string]) + testEverythingObject.string) XCTAssertNil(attributesDict?["nestedStruct"]) XCTAssertNil(attributesDict?["nestedEnum"]) XCTAssertEqual(attributesDict?["array"] as? [Bool], - testEverythingObject[\.array]) + testEverythingObject.array) XCTAssertNil(attributesDict?["optional"]) XCTAssertNil(attributesDict?["nullable"]) XCTAssertNil(attributesDict?["optionalNullable"]) } + + @available(*, deprecated, message: "remove next major version") + func test_sparseFieldsMethod_deprecated() { + let jsonEncoder = JSONEncoder() + let sparseObject = testEverythingObject.sparse(with: [.string, .bool, .array]) + + let encoded = try! jsonEncoder.encode(sparseObject) + + let deserialized = try! JSONSerialization.jsonObject(with: encoded, + options: []) + + let outerDict = deserialized as? [String: Any] + let id = outerDict?["id"] as? String + let type = outerDict?["type"] as? String + let attributesDict = outerDict?["attributes"] as? [String: Any] + let relationships = outerDict?["relationships"] + + XCTAssertEqual(id, testEverythingObject.id.rawValue) + XCTAssertEqual(type, EverythingTest.jsonType) + XCTAssertNil(relationships) + + XCTAssertEqual(attributesDict?["bool"] as? Bool, + testEverythingObject[\.bool]) + XCTAssertEqual(attributesDict?["string"] as? String, + testEverythingObject[\.string]) + XCTAssertEqual(attributesDict?["array"] as? [Bool], + testEverythingObject[\.array]) + } } struct EverythingTestDescription: JSONAPI.ResourceObjectDescription { From a7f6ed584530b138a72695566f147dfcd97db861 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Tue, 5 Nov 2019 21:03:51 -0800 Subject: [PATCH 093/235] update README --- README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/README.md b/README.md index 688b5fa..c535288 100644 --- a/README.md +++ b/README.md @@ -332,13 +332,11 @@ As of Swift 5.1, `Attributes` can be accessed via dynamic member keypath lookup let favoriteColor: String = person.favoriteColor ``` -🗒 `Attributes` can also be accessed via the older `subscript` operator as follows: +:warning: `Attributes` can also be accessed via the older `subscript` operator, but this is a deprecated feature that will be removed in the next major version: ```swift let favoriteColor: String = person[\.favoriteColor] ``` -In both cases you retain type-safety. It is best practice to pick an attribute access syntax and stick with it. At some point in the future the syntax deemed less desirable may be deprecated. - #### `Transformer` Sometimes you need to use a type that does not encode or decode itself in the way you need to represent it as a serialized JSON object. For example, the Swift `Foundation` type `Date` can encode/decode itself to `Double` out of the box, but you might want to represent dates as ISO 8601 compliant `String`s instead. The Foundation library `JSONDecoder` has a setting to make this adjustment, but for the sake of an example, you could create a `Transformer`. From d6f01d6c1d13315d5314307fc813b4fc4383238e Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Tue, 5 Nov 2019 22:27:59 -0800 Subject: [PATCH 094/235] indentation --- Sources/JSONAPI/Document/Document.swift | 282 ++++++++++++------------ 1 file changed, 141 insertions(+), 141 deletions(-) diff --git a/Sources/JSONAPI/Document/Document.swift b/Sources/JSONAPI/Document/Document.swift index 98b4445..60030c3 100644 --- a/Sources/JSONAPI/Document/Document.swift +++ b/Sources/JSONAPI/Document/Document.swift @@ -106,34 +106,34 @@ public protocol CodableJSONAPIDocument: EncodableJSONAPIDocument, Decodable wher /// a conversion such as the one offerred by the /// Foundation JSONEncoder/Decoder: `KeyDecodingStrategy` public struct Document: EncodableJSONAPIDocument { - public typealias Include = IncludeType + public typealias Include = IncludeType public typealias BodyData = Body.Data - /// The JSON API Spec calls this the JSON:API Object. It contains version - /// and metadata information about the API itself. - public let apiDescription: APIDescription + /// The JSON API Spec calls this the JSON:API Object. It contains version + /// and metadata information about the API itself. + public let apiDescription: APIDescription - /// The Body of the Document. This body is either one or more errors - /// with links and metadata attempted to parse but not guaranteed or - /// it is a successful data struct containing all the primary and - /// included resources, the metadata, and the links that this - /// document type specifies. - public let body: Body + /// The Body of the Document. This body is either one or more errors + /// with links and metadata attempted to parse but not guaranteed or + /// it is a successful data struct containing all the primary and + /// included resources, the metadata, and the links that this + /// document type specifies. + public let body: Body - public init(apiDescription: APIDescription, + public init(apiDescription: APIDescription, errors: [Error], meta: MetaType? = nil, links: LinksType? = nil) { - body = .errors(errors, meta: meta, links: links) - self.apiDescription = apiDescription - } - - public init(apiDescription: APIDescription, - body: PrimaryResourceBody, - includes: Includes, - meta: MetaType, - links: LinksType) { - self.body = .data( + body = .errors(errors, meta: meta, links: links) + self.apiDescription = apiDescription + } + + public init(apiDescription: APIDescription, + body: PrimaryResourceBody, + includes: Includes, + meta: MetaType, + links: LinksType) { + self.body = .data( .init( primary: body, includes: includes, @@ -141,8 +141,8 @@ public struct Document MetaType, - combiningLinksWith linksMerge: (LinksType, LinksType) -> LinksType) -> Document.Body.Data { - return Document.Body.Data(primary: primary.appending(other.primary), - includes: includes.appending(other.includes), - meta: metaMerge(meta, other.meta), - links: linksMerge(links, other.links)) - } + public func merging(_ other: Document.Body.Data, + combiningMetaWith metaMerge: (MetaType, MetaType) -> MetaType, + combiningLinksWith linksMerge: (LinksType, LinksType) -> LinksType) -> Document.Body.Data { + return Document.Body.Data(primary: primary.appending(other.primary), + includes: includes.appending(other.includes), + meta: metaMerge(meta, other.meta), + links: linksMerge(links, other.links)) + } } extension Document.Body.Data where PrimaryResourceBody: ResourceBodyAppendable, MetaType == NoMetadata, LinksType == NoLinks { - public func merging(_ other: Document.Body.Data) -> Document.Body.Data { - return merging(other, - combiningMetaWith: { _, _ in .none }, - combiningLinksWith: { _, _ in .none }) - } + public func merging(_ other: Document.Body.Data) -> Document.Body.Data { + return merging(other, + combiningMetaWith: { _, _ in .none }, + combiningLinksWith: { _, _ in .none }) + } } extension Document where IncludeType == NoIncludes { - /// Create a new Document with the given includes. - public func including(_ includes: Includes) -> Document { - // Note that if IncludeType is NoIncludes, then we allow anything - // to be included, but if IncludeType already specifies a type - // of thing to be expected then we lock that down. - // See: Document.including() where IncludeType: _Poly1 - switch body { - case .data(let data): - return .init(apiDescription: apiDescription, - body: data.primary, - includes: includes, - meta: data.meta, - links: data.links) - case .errors(let errors, meta: let meta, links: let links): - return .init(apiDescription: apiDescription, - errors: errors, - meta: meta, - links: links) - } - } + /// Create a new Document with the given includes. + public func including(_ includes: Includes) -> Document { + // Note that if IncludeType is NoIncludes, then we allow anything + // to be included, but if IncludeType already specifies a type + // of thing to be expected then we lock that down. + // See: Document.including() where IncludeType: _Poly1 + switch body { + case .data(let data): + return .init(apiDescription: apiDescription, + body: data.primary, + includes: includes, + meta: data.meta, + links: data.links) + case .errors(let errors, meta: let meta, links: let links): + return .init(apiDescription: apiDescription, + errors: errors, + meta: meta, + links: links) + } + } } // extending where _Poly1 means all non-zero _Poly arities are included extension Document where IncludeType: _Poly1 { - /// Create a new Document adding the given includes. This does not - /// remove existing includes; it is additive. - public func including(_ includes: Includes) -> Document { - // Note that if IncludeType is NoIncludes, then we allow anything - // to be included, but if IncludeType already specifies a type - // of thing to be expected then we lock that down. - // See: Document.including() where IncludeType == NoIncludes - switch body { - case .data(let data): - return .init(apiDescription: apiDescription, - body: data.primary, - includes: data.includes + includes, - meta: data.meta, - links: data.links) - case .errors(let errors, meta: let meta, links: let links): - return .init(apiDescription: apiDescription, - errors: errors, - meta: meta, - links: links) - } - } + /// Create a new Document adding the given includes. This does not + /// remove existing includes; it is additive. + public func including(_ includes: Includes) -> Document { + // Note that if IncludeType is NoIncludes, then we allow anything + // to be included, but if IncludeType already specifies a type + // of thing to be expected then we lock that down. + // See: Document.including() where IncludeType == NoIncludes + switch body { + case .data(let data): + return .init(apiDescription: apiDescription, + body: data.primary, + includes: data.includes + includes, + meta: data.meta, + links: data.links) + case .errors(let errors, meta: let meta, links: let links): + return .init(apiDescription: apiDescription, + errors: errors, + meta: meta, + links: links) + } + } } // MARK: - Codable extension Document { - private enum RootCodingKeys: String, CodingKey { - case data - case errors - case included - case meta - case links - case jsonapi - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: RootCodingKeys.self) - - switch body { - case .errors(let errors, meta: let meta, links: let links): - var errContainer = container.nestedUnkeyedContainer(forKey: .errors) - - for error in errors { - try errContainer.encode(error) - } - - if MetaType.self != NoMetadata.self, - let metaVal = meta { - try container.encode(metaVal, forKey: .meta) - } - - if LinksType.self != NoLinks.self, - let linksVal = links { - try container.encode(linksVal, forKey: .links) - } - - case .data(let data): - try container.encode(data.primary, forKey: .data) - - if Include.self != NoIncludes.self { - try container.encode(data.includes, forKey: .included) - } - - if MetaType.self != NoMetadata.self { - try container.encode(data.meta, forKey: .meta) - } - - if LinksType.self != NoLinks.self { - try container.encode(data.links, forKey: .links) - } - } - - if APIDescription.self != NoAPIDescription.self { - try container.encode(apiDescription, forKey: .jsonapi) - } - } + private enum RootCodingKeys: String, CodingKey { + case data + case errors + case included + case meta + case links + case jsonapi + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: RootCodingKeys.self) + + switch body { + case .errors(let errors, meta: let meta, links: let links): + var errContainer = container.nestedUnkeyedContainer(forKey: .errors) + + for error in errors { + try errContainer.encode(error) + } + + if MetaType.self != NoMetadata.self, + let metaVal = meta { + try container.encode(metaVal, forKey: .meta) + } + + if LinksType.self != NoLinks.self, + let linksVal = links { + try container.encode(linksVal, forKey: .links) + } + + case .data(let data): + try container.encode(data.primary, forKey: .data) + + if Include.self != NoIncludes.self { + try container.encode(data.includes, forKey: .included) + } + + if MetaType.self != NoMetadata.self { + try container.encode(data.meta, forKey: .meta) + } + + if LinksType.self != NoLinks.self { + try container.encode(data.links, forKey: .links) + } + } + + if APIDescription.self != NoAPIDescription.self { + try container.encode(apiDescription, forKey: .jsonapi) + } + } } extension Document: Decodable, CodableJSONAPIDocument where PrimaryResourceBody: ResourceBody, IncludeType: Decodable { @@ -405,26 +405,26 @@ extension Document: Decodable, CodableJSONAPIDocument where PrimaryResourceBody: // MARK: - CustomStringConvertible extension Document: CustomStringConvertible { - public var description: String { - return "Document(\(String(describing: body)))" - } + public var description: String { + return "Document(\(String(describing: body)))" + } } extension Document.Body: CustomStringConvertible { - public var description: String { - switch self { - case .errors(let errors, meta: let meta, links: let links): - return "errors: \(String(describing: errors)), meta: \(String(describing: meta)), links: \(String(describing: links))" - case .data(let data): - return String(describing: data) - } - } + public var description: String { + switch self { + case .errors(let errors, meta: let meta, links: let links): + return "errors: \(String(describing: errors)), meta: \(String(describing: meta)), links: \(String(describing: links))" + case .data(let data): + return String(describing: data) + } + } } extension Document.Body.Data: CustomStringConvertible { - public var description: String { - return "primary: \(String(describing: primary)), includes: \(String(describing: includes)), meta: \(String(describing: meta)), links: \(String(describing: links))" - } + public var description: String { + return "primary: \(String(describing: primary)), includes: \(String(describing: includes)), meta: \(String(describing: meta)), links: \(String(describing: links))" + } } // MARK: - Error and Success Document Types From e6f82c6052e6d156bc95ad491c8857d26facd0b3 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Tue, 5 Nov 2019 22:37:48 -0800 Subject: [PATCH 095/235] indentation --- Sources/JSONAPI/Document/APIDescription.swift | 64 +- Sources/JSONAPI/Document/Includes.swift | 120 +-- Sources/JSONAPI/Document/ResourceBody.swift | 60 +- Sources/JSONAPI/EncodingError.swift | 10 +- Sources/JSONAPI/Error/BasicJSONAPIError.swift | 7 +- Sources/JSONAPI/Error/JSONAPIError.swift | 28 +- Sources/JSONAPI/Meta/Links.swift | 86 +- Sources/JSONAPI/Meta/Meta.swift | 6 +- .../JSONAPI/Resource/Attribute+Functor.swift | 48 +- Sources/JSONAPI/Resource/Attribute.swift | 175 ++-- Sources/JSONAPI/Resource/Id.swift | 92 +- .../Resource/Poly+PrimaryResource.swift | 144 ++- Sources/JSONAPI/Resource/Relationship.swift | 374 ++++---- .../Resource Object/ResourceObject.swift | 828 +++++++++--------- .../JSONAPITesting/Attribute+Literal.swift | 126 +-- Sources/JSONAPITesting/Id+Literal.swift | 40 +- Sources/JSONAPITesting/Optional+Literal.swift | 24 +- .../JSONAPITesting/Relationship+Literal.swift | 38 +- .../JSONAPITesting/ResourceObjectCheck.swift | 104 +-- 19 files changed, 1228 insertions(+), 1146 deletions(-) diff --git a/Sources/JSONAPI/Document/APIDescription.swift b/Sources/JSONAPI/Document/APIDescription.swift index a0ba2ff..cb417a6 100644 --- a/Sources/JSONAPI/Document/APIDescription.swift +++ b/Sources/JSONAPI/Document/APIDescription.swift @@ -7,58 +7,58 @@ /// This is what the JSON API Spec calls the "JSON:API Object" public protocol APIDescriptionType: Codable, Equatable { - associatedtype Meta + associatedtype Meta } /// This is what the JSON API Spec calls the "JSON:API Object" public struct APIDescription: APIDescriptionType { - public let version: String - public let meta: Meta + public let version: String + public let meta: Meta - public init(version: String, meta: Meta) { - self.version = version - self.meta = meta - } + public init(version: String, meta: Meta) { + self.version = version + self.meta = meta + } } /// Can be used as `APIDescriptionType` for Documents that do not /// have any API Description (a.k.a. "JSON:API Object"). 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 { - private enum CodingKeys: String, CodingKey { - case version - case meta - } + private enum CodingKeys: String, CodingKey { + case version + case meta + } - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) + public init(from decoder: Decoder) throws { + 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 - version = (try? container.decode(String.self, forKey: .version)) ?? "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" - if let metaVal = NoMetadata() as? Meta { - meta = metaVal - } else { - meta = try container.decode(Meta.self, forKey: .meta) - } - } + if let metaVal = NoMetadata() as? Meta { + meta = metaVal + } else { + meta = try container.decode(Meta.self, forKey: .meta) + } + } - public func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) - try container.encode(version, forKey: .version) + try container.encode(version, forKey: .version) - if Meta.self != NoMetadata.self { - try container.encode(meta, forKey: .meta) - } - } + if Meta.self != NoMetadata.self { + try container.encode(meta, forKey: .meta) + } + } } diff --git a/Sources/JSONAPI/Document/Includes.swift b/Sources/JSONAPI/Document/Includes.swift index b6d93e0..087b2b5 100644 --- a/Sources/JSONAPI/Document/Includes.swift +++ b/Sources/JSONAPI/Document/Includes.swift @@ -20,29 +20,29 @@ public typealias Include = EncodableJSONPoly /// /// `let includedThings = includes[Thing1.self]` public struct Includes: Encodable, Equatable { - 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 { - var container = encoder.unkeyedContainer() - - guard I.self != NoIncludes.self else { - throw JSONAPIEncodingError.illegalEncoding("Attempting to encode Include0, which should be represented by the absense of an 'included' entry altogether.") - } - - for value in values { - try container.encode(value) - } - } - - public var count: Int { - return values.count - } + 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 { + var container = encoder.unkeyedContainer() + + guard I.self != NoIncludes.self else { + throw JSONAPIEncodingError.illegalEncoding("Attempting to encode Include0, which should be represented by the absense of an 'included' entry altogether.") + } + + for value in values { + try container.encode(value) + } + } + + public var count: Int { + return values.count + } } extension Includes: Decodable where I: Decodable { @@ -65,25 +65,25 @@ extension Includes: Decodable where I: Decodable { } extension Includes { - public func appending(_ other: Includes) -> Includes { - return Includes(values: values + other.values) - } + public func appending(_ other: Includes) -> Includes { + return Includes(values: values + other.values) + } } public func +(_ left: Includes, _ right: Includes) -> Includes { - return left.appending(right) + return left.appending(right) } extension Includes: CustomStringConvertible { - public var description: String { - return "Includes(\(String(describing: values))" - } + public var description: String { + return "Includes(\(String(describing: values))" + } } extension Includes where I == NoIncludes { - public init() { - values = [] - } + public init() { + values = [] + } } // MARK: - 0 includes @@ -93,73 +93,73 @@ public typealias NoIncludes = Include0 // MARK: - 1 include public typealias Include1 = Poly1 extension Includes where I: _Poly1 { - public subscript(_ lookup: I.A.Type) -> [I.A] { - return values.compactMap { $0.a } - } + public subscript(_ lookup: I.A.Type) -> [I.A] { + return values.compactMap { $0.a } + } } // MARK: - 2 includes public typealias Include2 = Poly2 extension Includes where I: _Poly2 { - public subscript(_ lookup: I.B.Type) -> [I.B] { - return values.compactMap { $0.b } - } + public subscript(_ lookup: I.B.Type) -> [I.B] { + return values.compactMap { $0.b } + } } // MARK: - 3 includes public typealias Include3 = Poly3 extension Includes where I: _Poly3 { - public subscript(_ lookup: I.C.Type) -> [I.C] { - return values.compactMap { $0.c } - } + public subscript(_ lookup: I.C.Type) -> [I.C] { + return values.compactMap { $0.c } + } } // MARK: - 4 includes public typealias Include4 = Poly4 extension Includes where I: _Poly4 { - public subscript(_ lookup: I.D.Type) -> [I.D] { - return values.compactMap { $0.d } - } + public subscript(_ lookup: I.D.Type) -> [I.D] { + return values.compactMap { $0.d } + } } // MARK: - 5 includes public typealias Include5 = Poly5 extension Includes where I: _Poly5 { - public subscript(_ lookup: I.E.Type) -> [I.E] { - return values.compactMap { $0.e } - } + public subscript(_ lookup: I.E.Type) -> [I.E] { + return values.compactMap { $0.e } + } } // MARK: - 6 includes public typealias Include6 = Poly6 extension Includes where I: _Poly6 { - public subscript(_ lookup: I.F.Type) -> [I.F] { - return values.compactMap { $0.f } - } + public subscript(_ lookup: I.F.Type) -> [I.F] { + return values.compactMap { $0.f } + } } // MARK: - 7 includes public typealias Include7 = Poly7 extension Includes where I: _Poly7 { - public subscript(_ lookup: I.G.Type) -> [I.G] { - return values.compactMap { $0.g } - } + public subscript(_ lookup: I.G.Type) -> [I.G] { + return values.compactMap { $0.g } + } } // MARK: - 8 includes public typealias Include8 = Poly8 extension Includes where I: _Poly8 { - public subscript(_ lookup: I.H.Type) -> [I.H] { - return values.compactMap { $0.h } - } + public subscript(_ lookup: I.H.Type) -> [I.H] { + return values.compactMap { $0.h } + } } // MARK: - 9 includes public typealias Include9 = Poly9 extension Includes where I: _Poly9 { - public subscript(_ lookup: I.I.Type) -> [I.I] { - return values.compactMap { $0.i } - } + public subscript(_ lookup: I.I.Type) -> [I.I] { + return values.compactMap { $0.i } + } } // MARK: - 10 includes diff --git a/Sources/JSONAPI/Document/ResourceBody.swift b/Sources/JSONAPI/Document/ResourceBody.swift index 7d8b9e2..8fd20d6 100644 --- a/Sources/JSONAPI/Document/ResourceBody.swift +++ b/Sources/JSONAPI/Document/ResourceBody.swift @@ -41,47 +41,47 @@ public protocol ResourceBody: Decodable, EncodableResourceBody {} /// A `ResourceBody` that has the ability to take on more primary /// resources by appending another similarly typed `ResourceBody`. public protocol ResourceBodyAppendable { - func appending(_ other: Self) -> Self + func appending(_ other: Self) -> Self } public func +(_ left: R, right: R) -> R { - return left.appending(right) + return left.appending(right) } /// A type allowing for a document body containing 1 primary resource. /// If the `Entity` specialization is an `Optional` type, the body can contain /// 0 or 1 primary resources. public struct SingleResourceBody: EncodableResourceBody { - public let value: Entity + public let value: Entity - public init(resourceObject: Entity) { - self.value = resourceObject - } + public init(resourceObject: Entity) { + self.value = resourceObject + } } /// A type allowing for a document body containing 0 or more primary resources. public struct ManyResourceBody: EncodableResourceBody, ResourceBodyAppendable { - public let values: [Entity] + public let values: [Entity] - public init(resourceObjects: [Entity]) { - values = resourceObjects - } + public init(resourceObjects: [Entity]) { + values = resourceObjects + } - public func appending(_ other: ManyResourceBody) -> ManyResourceBody { - return ManyResourceBody(resourceObjects: values + other.values) - } + public func appending(_ other: ManyResourceBody) -> ManyResourceBody { + return ManyResourceBody(resourceObjects: values + other.values) + } } /// Use NoResourceBody to indicate you expect a JSON API document to not /// contain a "data" top-level key. public struct NoResourceBody: ResourceBody { - public static var none: NoResourceBody { return NoResourceBody() } + public static var none: NoResourceBody { return NoResourceBody() } } // MARK: Codable extension SingleResourceBody { - public func encode(to encoder: Encoder) throws { - var container = encoder.singleValueContainer() + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() let anyNil: Any? = nil let nilValue = anyNil as? Entity @@ -90,8 +90,8 @@ extension SingleResourceBody { return } - try container.encode(value) - } + try container.encode(value) + } } extension SingleResourceBody: Decodable, ResourceBody where Entity: OptionalPrimaryResource { @@ -110,13 +110,13 @@ extension SingleResourceBody: Decodable, ResourceBody where Entity: OptionalPrim } extension ManyResourceBody { - public func encode(to encoder: Encoder) throws { - var container = encoder.unkeyedContainer() + public func encode(to encoder: Encoder) throws { + var container = encoder.unkeyedContainer() - for value in values { - try container.encode(value) - } - } + for value in values { + try container.encode(value) + } + } } extension ManyResourceBody: Decodable, ResourceBody where Entity: PrimaryResource { @@ -133,13 +133,13 @@ extension ManyResourceBody: Decodable, ResourceBody where Entity: PrimaryResourc // MARK: CustomStringConvertible extension SingleResourceBody: CustomStringConvertible { - public var description: String { - return "PrimaryResourceBody(\(String(describing: value)))" - } + public var description: String { + return "PrimaryResourceBody(\(String(describing: value)))" + } } extension ManyResourceBody: CustomStringConvertible { - public var description: String { - return "PrimaryResourceBody(\(String(describing: values)))" - } + public var description: String { + return "PrimaryResourceBody(\(String(describing: values)))" + } } diff --git a/Sources/JSONAPI/EncodingError.swift b/Sources/JSONAPI/EncodingError.swift index 1d8145f..8b461cd 100644 --- a/Sources/JSONAPI/EncodingError.swift +++ b/Sources/JSONAPI/EncodingError.swift @@ -6,9 +6,9 @@ // public enum JSONAPIEncodingError: Swift.Error { - case typeMismatch(expected: String, found: String) - case illegalEncoding(String) - case illegalDecoding(String) - case missingOrMalformedMetadata - case missingOrMalformedLinks + case typeMismatch(expected: String, found: String) + case illegalEncoding(String) + case illegalDecoding(String) + case missingOrMalformedMetadata + case missingOrMalformedLinks } diff --git a/Sources/JSONAPI/Error/BasicJSONAPIError.swift b/Sources/JSONAPI/Error/BasicJSONAPIError.swift index 28795b9..88ed95e 100644 --- a/Sources/JSONAPI/Error/BasicJSONAPIError.swift +++ b/Sources/JSONAPI/Error/BasicJSONAPIError.swift @@ -9,7 +9,9 @@ public struct BasicJSONAPIErrorPayload: Codable, Equatable, ErrorDictType { /// a unique identifier for this particular occurrence of the problem 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 public let status: String? /// an application-specific error code @@ -20,7 +22,8 @@ public struct BasicJSONAPIErrorPayload: Codable, Eq public let detail: String? /// an object containing references to the source of the error 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, status: String? = nil, diff --git a/Sources/JSONAPI/Error/JSONAPIError.swift b/Sources/JSONAPI/Error/JSONAPIError.swift index b997a69..6f19105 100644 --- a/Sources/JSONAPI/Error/JSONAPIError.swift +++ b/Sources/JSONAPI/Error/JSONAPIError.swift @@ -6,7 +6,7 @@ // 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 @@ -16,18 +16,18 @@ public protocol JSONAPIError: Swift.Error, Equatable, Codable { /// information the server might be providing in the error payload, /// use `BasicJSONAPIError` instead. public enum UnknownJSONAPIError: JSONAPIError { - case unknownError - - public init(from decoder: Decoder) throws { - self = .unknown - } + case unknownError - public func encode(to encoder: Encoder) throws { - var container = encoder.singleValueContainer() - try container.encode("unknown") - } - - public static var unknown: Self { - return .unknownError - } + public init(from decoder: Decoder) throws { + self = .unknown + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode("unknown") + } + + public static var unknown: Self { + return .unknownError + } } diff --git a/Sources/JSONAPI/Meta/Links.swift b/Sources/JSONAPI/Meta/Links.swift index 00658cb..876995f 100644 --- a/Sources/JSONAPI/Meta/Links.swift +++ b/Sources/JSONAPI/Meta/Links.swift @@ -10,58 +10,58 @@ public protocol Links: Codable, Equatable {} /// Use NoLinks where no links should belong to a JSON API component public struct NoLinks: Links, CustomStringConvertible { - public static var none: NoLinks { return NoLinks() } - public init() {} - - public var description: String { return "No Links" } + public static var none: NoLinks { return NoLinks() } + public init() {} + + public var description: String { return "No Links" } } public protocol JSONAPIURL: Codable, Equatable {} public struct Link: Equatable, Codable { - public let url: URL - public let meta: Meta - - public init(url: URL, meta: Meta) { - self.url = url - self.meta = meta - } + public let url: URL + public let meta: Meta + + public init(url: URL, meta: Meta) { + self.url = url + self.meta = meta + } } extension Link where Meta == NoMetadata { - public init(url: URL) { - self.init(url: url, meta: .none) - } + public init(url: URL) { + self.init(url: url, meta: .none) + } } public extension Link { - private enum CodingKeys: String, CodingKey { - case href - case meta - } - - init(from decoder: Decoder) throws { - guard Meta.self == NoMetadata.self, - let noMeta = NoMetadata() as? Meta else { - let container = try decoder.container(keyedBy: CodingKeys.self) - meta = try container.decode(Meta.self, forKey: .meta) - url = try container.decode(URL.self, forKey: .href) - return - } - let container = try decoder.singleValueContainer() - url = try container.decode(URL.self) - meta = noMeta - } - - func encode(to encoder: Encoder) throws { - guard Meta.self == NoMetadata.self else { - var container = encoder.container(keyedBy: CodingKeys.self) - try container.encode(url, forKey: .href) - try container.encode(meta, forKey: .meta) - return - } - var container = encoder.singleValueContainer() - - try container.encode(url) - } + private enum CodingKeys: String, CodingKey { + case href + case meta + } + + init(from decoder: Decoder) throws { + guard Meta.self == NoMetadata.self, + let noMeta = NoMetadata() as? Meta else { + let container = try decoder.container(keyedBy: CodingKeys.self) + meta = try container.decode(Meta.self, forKey: .meta) + url = try container.decode(URL.self, forKey: .href) + return + } + let container = try decoder.singleValueContainer() + url = try container.decode(URL.self) + meta = noMeta + } + + func encode(to encoder: Encoder) throws { + guard Meta.self == NoMetadata.self else { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(url, forKey: .href) + try container.encode(meta, forKey: .meta) + return + } + var container = encoder.singleValueContainer() + + try container.encode(url) + } } diff --git a/Sources/JSONAPI/Meta/Meta.swift b/Sources/JSONAPI/Meta/Meta.swift index 68b2c94..b49149f 100644 --- a/Sources/JSONAPI/Meta/Meta.swift +++ b/Sources/JSONAPI/Meta/Meta.swift @@ -22,9 +22,9 @@ extension Optional: Meta where Wrapped: Meta {} /// Use this type when you want to specify not to encode or decode any metadata /// for a type. 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" } } diff --git a/Sources/JSONAPI/Resource/Attribute+Functor.swift b/Sources/JSONAPI/Resource/Attribute+Functor.swift index 1da6ff0..6f4b4dc 100644 --- a/Sources/JSONAPI/Resource/Attribute+Functor.swift +++ b/Sources/JSONAPI/Resource/Attribute+Functor.swift @@ -6,31 +6,31 @@ // public extension TransformedAttribute { - /// Map an Attribute to a new wrapped type. - /// Note that the resulting Attribute will have no transformer, even if the - /// source Attribute has a transformer. - /// You are mapping the output of the source transform into - /// the RawValue of a new transformerless Attribute. - /// - /// Generally, this is the most useful operation. The transformer gives you - /// control over the decoding of the Attribute, but once the Attribute exists, - /// mapping on it is most useful for creating computed Attribute properties. - func map(_ transform: (Transformer.To) throws -> T) rethrows -> Attribute { - return Attribute(value: try transform(value)) - } + /// Map an Attribute to a new wrapped type. + /// Note that the resulting Attribute will have no transformer, even if the + /// source Attribute has a transformer. + /// You are mapping the output of the source transform into + /// the RawValue of a new transformerless Attribute. + /// + /// Generally, this is the most useful operation. The transformer gives you + /// control over the decoding of the Attribute, but once the Attribute exists, + /// mapping on it is most useful for creating computed Attribute properties. + func map(_ transform: (Transformer.To) throws -> T) rethrows -> Attribute { + return Attribute(value: try transform(value)) + } } public extension Attribute { - /// Map an Attribute to a new wrapped type. - /// Note that the resulting Attribute will have no transformer, even if the - /// source Attribute has a transformer. - /// You are mapping the output of the source transform into - /// the RawValue of a new transformerless Attribute. - /// - /// Generally, this is the most useful operation. The transformer gives you - /// control over the decoding of the Attribute, but once the Attribute exists, - /// mapping on it is most useful for creating computed Attribute properties. - func map(_ transform: (ValueType) throws -> T) rethrows -> Attribute { - return Attribute(value: try transform(value)) - } + /// Map an Attribute to a new wrapped type. + /// Note that the resulting Attribute will have no transformer, even if the + /// source Attribute has a transformer. + /// You are mapping the output of the source transform into + /// the RawValue of a new transformerless Attribute. + /// + /// Generally, this is the most useful operation. The transformer gives you + /// control over the decoding of the Attribute, but once the Attribute exists, + /// mapping on it is most useful for creating computed Attribute properties. + func map(_ transform: (ValueType) throws -> T) rethrows -> Attribute { + return Attribute(value: try transform(value)) + } } diff --git a/Sources/JSONAPI/Resource/Attribute.swift b/Sources/JSONAPI/Resource/Attribute.swift index c05983d..f73d672 100644 --- a/Sources/JSONAPI/Resource/Attribute.swift +++ b/Sources/JSONAPI/Resource/Attribute.swift @@ -6,49 +6,49 @@ // public protocol AttributeType: Codable { - associatedtype RawValue: Codable - associatedtype ValueType + associatedtype RawValue: Codable + associatedtype ValueType - var value: ValueType { get } + var value: ValueType { get } } // MARK: TransformedAttribute /// A TransformedAttribute takes a Codable type and attempts to turn it into another type. public struct TransformedAttribute: 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 { - self.rawValue = rawValue - value = try Transformer.transform(rawValue) - } + public init(rawValue: RawValue) throws { + self.rawValue = rawValue + value = try Transformer.transform(rawValue) + } } extension TransformedAttribute where Transformer == IdentityTransformer { - // If we are using the identity transform, we can skip the transform and guarantee no - // error is thrown. - public init(value: RawValue) { - rawValue = value - self.value = value - } + // If we are using the identity transform, we can skip the transform and guarantee no + // error is thrown. + public init(value: RawValue) { + rawValue = value + self.value = value + } } extension TransformedAttribute where Transformer: ReversibleTransformer { - /// Initialize a TransformedAttribute from its transformed value. The - /// RawValue, which is what gets encoded/decoded, is determined using - /// The Transformer's reverse function. - public init(transformedValue: Transformer.To) throws { - self.value = transformedValue - rawValue = try Transformer.reverse(value) - } + /// Initialize a TransformedAttribute from its transformed value. The + /// RawValue, which is what gets encoded/decoded, is determined using + /// The Transformer's reverse function. + public init(transformedValue: Transformer.To) throws { + self.value = transformedValue + rawValue = try Transformer.reverse(value) + } } extension TransformedAttribute: CustomStringConvertible { - public var description: String { - return "Attribute<\(String(describing: Transformer.From.self)) -> \(String(describing: Transformer.To.self))>(\(String(describing: value)))" - } + public var description: String { + 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 {} @@ -63,85 +63,86 @@ public typealias ValidatedAttribute: AttributeType { - let attribute: TransformedAttribute> + let attribute: TransformedAttribute> - public var value: RawValue { - return attribute.value - } + public var value: RawValue { + return attribute.value + } - public init(value: RawValue) { - attribute = .init(value: value) - } + public init(value: RawValue) { + attribute = .init(value: value) + } } extension Attribute: CustomStringConvertible { - public var description: String { - return "Attribute<\(String(describing: RawValue.self))>(\(String(describing: value)))" - } + public var description: String { + return "Attribute<\(String(describing: RawValue.self))>(\(String(describing: value)))" + } } extension Attribute: Equatable where RawValue: Equatable {} // MARK: - Codable extension TransformedAttribute { - public init(from decoder: Decoder) throws { - 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) - } + public init(from decoder: Decoder) throws { + 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) + } } extension Attribute { - public init(from decoder: Decoder) throws { - let container = try decoder.singleValueContainer() - - // 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? RawValue { - attribute = .init(value: val) - } else { - attribute = try container.decode(TransformedAttribute>.self) - } - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.singleValueContainer() - - try container.encode(attribute) - } + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + + // 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? RawValue { + attribute = .init(value: val) + } else { + attribute = try container.decode(TransformedAttribute>.self) + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + + try container.encode(attribute) + } } // MARK: Attribute decoding and encoding defaults extension AttributeType { - public static func defaultDecoding(from container: Container, forKey key: Container.Key) throws -> Self { - return try container.decode(Self.self, forKey: key) - } + public static func defaultDecoding(from container: Container, + forKey key: Container.Key) throws -> Self { + return try container.decode(Self.self, forKey: key) + } } diff --git a/Sources/JSONAPI/Resource/Id.swift b/Sources/JSONAPI/Resource/Id.swift index a375c08..d66e3d9 100644 --- a/Sources/JSONAPI/Resource/Id.swift +++ b/Sources/JSONAPI/Resource/Id.swift @@ -23,7 +23,7 @@ public protocol RawIdType: MaybeRawId, Hashable {} /// Conformances for `String` and `UUID` /// are given in the README for this library. public protocol CreatableRawIdType: RawIdType { - static func unique() -> Self + static func unique() -> Self } extension String: RawIdType {} @@ -32,80 +32,80 @@ extension String: RawIdType {} /// have an Id (most likely because it was created by a client and the server will be responsible /// for assigning it an Id). public struct Unidentified: MaybeRawId, CustomStringConvertible { - public init() {} - - public var description: String { return "Unidentified" } + public init() {} + + public var description: String { return "Unidentified" } } public protocol OptionalId: Codable { - associatedtype IdentifiableType: JSONAPI.JSONTyped - associatedtype RawType: MaybeRawId + associatedtype IdentifiableType: JSONAPI.JSONTyped + associatedtype RawType: MaybeRawId - var rawValue: RawType { get } - init(rawValue: RawType) + var rawValue: RawType { get } + init(rawValue: RawType) } public protocol IdType: OptionalId, CustomStringConvertible, Hashable where RawType: RawIdType {} extension Optional: MaybeRawId where Wrapped: Codable & Equatable {} extension Optional: OptionalId where Wrapped: IdType { - public typealias IdentifiableType = Wrapped.IdentifiableType - public typealias RawType = Wrapped.RawType? - - public var rawValue: Wrapped.RawType? { - guard case .some(let value) = self else { - return nil - } - return value.rawValue - } - - public init(rawValue: Wrapped.RawType?) { - self = rawValue.map { Wrapped(rawValue: $0) } - } + public typealias IdentifiableType = Wrapped.IdentifiableType + public typealias RawType = Wrapped.RawType? + + public var rawValue: Wrapped.RawType? { + guard case .some(let value) = self else { + return nil + } + return value.rawValue + } + + public init(rawValue: Wrapped.RawType?) { + self = rawValue.map { Wrapped(rawValue: $0) } + } } public extension IdType { - var description: String { return "Id(\(String(describing: rawValue)))" } + var description: String { return "Id(\(String(describing: rawValue)))" } } public protocol CreatableIdType: IdType { - init() + init() } /// An ResourceObject ID. These IDs can be encoded to or decoded from /// JSON API IDs. public struct Id: Equatable, OptionalId { - public let rawValue: RawType - - public init(rawValue: RawType) { - self.rawValue = rawValue - } - - public init(from decoder: Decoder) throws { - let container = try decoder.singleValueContainer() - let rawValue = try container.decode(RawType.self) - self.init(rawValue: rawValue) - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.singleValueContainer() - try container.encode(rawValue) - } + public let rawValue: RawType + + public init(rawValue: RawType) { + self.rawValue = rawValue + } + + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + let rawValue = try container.decode(RawType.self) + self.init(rawValue: rawValue) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(rawValue) + } } extension Id: Hashable, CustomStringConvertible, IdType where RawType: RawIdType { - public static func id(from rawValue: RawType) -> Id { - return Id(rawValue: rawValue) - } + public static func id(from rawValue: RawType) -> Id { + return Id(rawValue: rawValue) + } } extension Id: CreatableIdType where RawType: CreatableRawIdType { - public init() { - rawValue = .unique() - } + public init() { + rawValue = .unique() + } } extension Id where RawType == Unidentified { - public static var unidentified: Id { return .init(rawValue: Unidentified()) } + public static var unidentified: Id { return .init(rawValue: Unidentified()) } } diff --git a/Sources/JSONAPI/Resource/Poly+PrimaryResource.swift b/Sources/JSONAPI/Resource/Poly+PrimaryResource.swift index 284e10a..3ee23a1 100644 --- a/Sources/JSONAPI/Resource/Poly+PrimaryResource.swift +++ b/Sources/JSONAPI/Resource/Poly+PrimaryResource.swift @@ -21,66 +21,144 @@ public typealias EncodablePolyWrapped = Encodable & Equatable public typealias PolyWrapped = EncodablePolyWrapped & Decodable extension Poly0: PrimaryResource { - public init(from decoder: Decoder) throws { - throw JSONAPIEncodingError.illegalDecoding("Attempted to decode Poly0, which should represent a thing that is not expected to be found in a document.") - } + public init(from decoder: Decoder) throws { + throw JSONAPIEncodingError.illegalDecoding("Attempted to decode Poly0, which should represent a thing that is not expected to be found in a document.") + } - public func encode(to encoder: Encoder) throws { - throw JSONAPIEncodingError.illegalEncoding("Attempted to encode Poly0, which should represent a thing that is not expected to be found in a document.") - } + public func encode(to encoder: Encoder) throws { + throw JSONAPIEncodingError.illegalEncoding("Attempted to encode Poly0, which should represent a thing that is not expected to be found in a document.") + } } // MARK: - 1 type -extension Poly1: EncodablePrimaryResource, OptionalEncodablePrimaryResource where A: EncodablePolyWrapped {} +extension Poly1: EncodablePrimaryResource, OptionalEncodablePrimaryResource + where A: EncodablePolyWrapped {} -extension Poly1: PrimaryResource, OptionalPrimaryResource where A: PolyWrapped {} +extension Poly1: PrimaryResource, OptionalPrimaryResource + where A: PolyWrapped {} // MARK: - 2 types -extension Poly2: EncodablePrimaryResource, OptionalEncodablePrimaryResource where A: EncodablePolyWrapped, B: EncodablePolyWrapped {} +extension Poly2: EncodablePrimaryResource, OptionalEncodablePrimaryResource + where A: EncodablePolyWrapped, B: EncodablePolyWrapped {} -extension Poly2: PrimaryResource, OptionalPrimaryResource where A: PolyWrapped, B: PolyWrapped {} +extension Poly2: PrimaryResource, OptionalPrimaryResource + where A: PolyWrapped, B: PolyWrapped {} // MARK: - 3 types -extension Poly3: EncodablePrimaryResource, OptionalEncodablePrimaryResource where A: EncodablePolyWrapped, B: EncodablePolyWrapped, C: EncodablePolyWrapped {} +extension Poly3: EncodablePrimaryResource, OptionalEncodablePrimaryResource + where A: EncodablePolyWrapped, B: EncodablePolyWrapped, C: EncodablePolyWrapped {} -extension Poly3: PrimaryResource, OptionalPrimaryResource where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped {} +extension Poly3: PrimaryResource, OptionalPrimaryResource + where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped {} // MARK: - 4 types -extension Poly4: EncodablePrimaryResource, OptionalEncodablePrimaryResource where A: EncodablePolyWrapped, B: EncodablePolyWrapped, C: EncodablePolyWrapped, D: EncodablePolyWrapped {} +extension Poly4: EncodablePrimaryResource, OptionalEncodablePrimaryResource + where A: EncodablePolyWrapped, B: EncodablePolyWrapped, C: EncodablePolyWrapped, D: EncodablePolyWrapped {} -extension Poly4: PrimaryResource, OptionalPrimaryResource where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped {} +extension Poly4: PrimaryResource, OptionalPrimaryResource + where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped {} // MARK: - 5 types -extension Poly5: EncodablePrimaryResource, OptionalEncodablePrimaryResource where A: EncodablePolyWrapped, B: EncodablePolyWrapped, C: EncodablePolyWrapped, D: EncodablePolyWrapped, E: EncodablePolyWrapped {} +extension Poly5: EncodablePrimaryResource, OptionalEncodablePrimaryResource + where A: EncodablePolyWrapped, B: EncodablePolyWrapped, C: EncodablePolyWrapped, D: EncodablePolyWrapped, E: EncodablePolyWrapped {} -extension Poly5: PrimaryResource, OptionalPrimaryResource where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped, E: PolyWrapped {} +extension Poly5: PrimaryResource, OptionalPrimaryResource + where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped, E: PolyWrapped {} // MARK: - 6 types -extension Poly6: EncodablePrimaryResource, OptionalEncodablePrimaryResource where A: EncodablePolyWrapped, B: EncodablePolyWrapped, C: EncodablePolyWrapped, D: EncodablePolyWrapped, E: EncodablePolyWrapped, F: EncodablePolyWrapped {} +extension Poly6: EncodablePrimaryResource, OptionalEncodablePrimaryResource + where A: EncodablePolyWrapped, B: EncodablePolyWrapped, C: EncodablePolyWrapped, D: EncodablePolyWrapped, E: EncodablePolyWrapped, F: EncodablePolyWrapped {} -extension Poly6: PrimaryResource, OptionalPrimaryResource where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped, E: PolyWrapped, F: PolyWrapped {} +extension Poly6: PrimaryResource, OptionalPrimaryResource + where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped, E: PolyWrapped, F: PolyWrapped {} // MARK: - 7 types -extension Poly7: EncodablePrimaryResource, OptionalEncodablePrimaryResource where A: EncodablePolyWrapped, B: EncodablePolyWrapped, C: EncodablePolyWrapped, D: EncodablePolyWrapped, E: EncodablePolyWrapped, F: EncodablePolyWrapped, G: EncodablePolyWrapped {} - -extension Poly7: PrimaryResource, OptionalPrimaryResource where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped, E: PolyWrapped, F: PolyWrapped, G: PolyWrapped {} +extension Poly7: EncodablePrimaryResource, OptionalEncodablePrimaryResource + where + A: EncodablePolyWrapped, + B: EncodablePolyWrapped, + C: EncodablePolyWrapped, + D: EncodablePolyWrapped, + E: EncodablePolyWrapped, + F: EncodablePolyWrapped, + G: EncodablePolyWrapped {} + +extension Poly7: PrimaryResource, OptionalPrimaryResource + where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped, E: PolyWrapped, F: PolyWrapped, G: PolyWrapped {} // MARK: - 8 types -extension Poly8: EncodablePrimaryResource, OptionalEncodablePrimaryResource where A: EncodablePolyWrapped, B: EncodablePolyWrapped, C: EncodablePolyWrapped, D: EncodablePolyWrapped, E: EncodablePolyWrapped, F: EncodablePolyWrapped, G: EncodablePolyWrapped, H: EncodablePolyWrapped {} - -extension Poly8: PrimaryResource, OptionalPrimaryResource where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped, E: PolyWrapped, F: PolyWrapped, G: PolyWrapped, H: PolyWrapped {} +extension Poly8: EncodablePrimaryResource, OptionalEncodablePrimaryResource + where + A: EncodablePolyWrapped, + B: EncodablePolyWrapped, + C: EncodablePolyWrapped, + D: EncodablePolyWrapped, + E: EncodablePolyWrapped, + F: EncodablePolyWrapped, + G: EncodablePolyWrapped, + H: EncodablePolyWrapped {} + +extension Poly8: PrimaryResource, OptionalPrimaryResource + where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped, E: PolyWrapped, F: PolyWrapped, G: PolyWrapped, H: PolyWrapped {} // MARK: - 9 types -extension Poly9: EncodablePrimaryResource, OptionalEncodablePrimaryResource where A: EncodablePolyWrapped, B: EncodablePolyWrapped, C: EncodablePolyWrapped, D: EncodablePolyWrapped, E: EncodablePolyWrapped, F: EncodablePolyWrapped, G: EncodablePolyWrapped, H: EncodablePolyWrapped, I: EncodablePolyWrapped {} - -extension Poly9: PrimaryResource, OptionalPrimaryResource where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped, E: PolyWrapped, F: PolyWrapped, G: PolyWrapped, H: PolyWrapped, I: PolyWrapped {} +extension Poly9: EncodablePrimaryResource, OptionalEncodablePrimaryResource + where + A: EncodablePolyWrapped, + B: EncodablePolyWrapped, + C: EncodablePolyWrapped, + D: EncodablePolyWrapped, + E: EncodablePolyWrapped, + F: EncodablePolyWrapped, + G: EncodablePolyWrapped, + H: EncodablePolyWrapped, + I: EncodablePolyWrapped {} + +extension Poly9: PrimaryResource, OptionalPrimaryResource + where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped, E: PolyWrapped, F: PolyWrapped, G: PolyWrapped, H: PolyWrapped, I: PolyWrapped {} // MARK: - 10 types -extension Poly10: EncodablePrimaryResource, OptionalEncodablePrimaryResource where A: EncodablePolyWrapped, B: EncodablePolyWrapped, C: EncodablePolyWrapped, D: EncodablePolyWrapped, E: EncodablePolyWrapped, F: EncodablePolyWrapped, G: EncodablePolyWrapped, H: EncodablePolyWrapped, I: EncodablePolyWrapped, J: EncodablePolyWrapped {} - -extension Poly10: PrimaryResource, OptionalPrimaryResource where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped, E: PolyWrapped, F: PolyWrapped, G: PolyWrapped, H: PolyWrapped, I: PolyWrapped, J: PolyWrapped {} +extension Poly10: EncodablePrimaryResource, OptionalEncodablePrimaryResource + where + A: EncodablePolyWrapped, + B: EncodablePolyWrapped, + C: EncodablePolyWrapped, + D: EncodablePolyWrapped, + E: EncodablePolyWrapped, + F: EncodablePolyWrapped, + G: EncodablePolyWrapped, + H: EncodablePolyWrapped, + I: EncodablePolyWrapped, + J: EncodablePolyWrapped {} + +extension Poly10: PrimaryResource, OptionalPrimaryResource + where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped, E: PolyWrapped, F: PolyWrapped, G: PolyWrapped, H: PolyWrapped, I: PolyWrapped, J: PolyWrapped {} // MARK: - 11 types -extension Poly11: EncodablePrimaryResource, OptionalEncodablePrimaryResource where A: EncodablePolyWrapped, B: EncodablePolyWrapped, C: EncodablePolyWrapped, D: EncodablePolyWrapped, E: EncodablePolyWrapped, F: EncodablePolyWrapped, G: EncodablePolyWrapped, H: EncodablePolyWrapped, I: EncodablePolyWrapped, J: EncodablePolyWrapped, K: EncodablePolyWrapped {} - -extension Poly11: PrimaryResource, OptionalPrimaryResource where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped, E: PolyWrapped, F: PolyWrapped, G: PolyWrapped, H: PolyWrapped, I: PolyWrapped, J: PolyWrapped, K: PolyWrapped {} +extension Poly11: EncodablePrimaryResource, OptionalEncodablePrimaryResource + where + A: EncodablePolyWrapped, + B: EncodablePolyWrapped, + C: EncodablePolyWrapped, + D: EncodablePolyWrapped, + E: EncodablePolyWrapped, + F: EncodablePolyWrapped, + G: EncodablePolyWrapped, + H: EncodablePolyWrapped, + I: EncodablePolyWrapped, + J: EncodablePolyWrapped, + K: EncodablePolyWrapped {} + +extension Poly11: PrimaryResource, OptionalPrimaryResource + where + A: PolyWrapped, + B: PolyWrapped, + C: PolyWrapped, + D: PolyWrapped, + E: PolyWrapped, + F: PolyWrapped, + G: PolyWrapped, + H: PolyWrapped, + I: PolyWrapped, + J: PolyWrapped, + K: PolyWrapped {} diff --git a/Sources/JSONAPI/Resource/Relationship.swift b/Sources/JSONAPI/Resource/Relationship.swift index 43f5457..69fabd9 100644 --- a/Sources/JSONAPI/Resource/Relationship.swift +++ b/Sources/JSONAPI/Resource/Relationship.swift @@ -6,11 +6,11 @@ // public protocol RelationshipType { - associatedtype LinksType - associatedtype MetaType + associatedtype LinksType + associatedtype MetaType - var links: LinksType { get } - var meta: MetaType { get } + var links: LinksType { get } + var meta: MetaType { get } } /// An ResourceObject relationship that can be encoded to or decoded from @@ -19,46 +19,46 @@ public protocol RelationshipType { /// A convenient typealias might make your code much more legible: `One` public struct ToOneRelationship: RelationshipType, Equatable { - public let id: Identifiable.Identifier + public let id: Identifiable.Identifier - public let meta: MetaType - public let links: LinksType + public let meta: MetaType + public let links: LinksType - public init(id: Identifiable.Identifier, meta: MetaType, links: LinksType) { - self.id = id - self.meta = meta - self.links = links - } + public init(id: Identifiable.Identifier, meta: MetaType, links: LinksType) { + self.id = id + self.meta = meta + self.links = links + } } extension ToOneRelationship where MetaType == NoMetadata, LinksType == NoLinks { - public init(id: Identifiable.Identifier) { - self.init(id: id, meta: .none, links: .none) - } + public init(id: Identifiable.Identifier) { + self.init(id: id, meta: .none, links: .none) + } } extension ToOneRelationship { - public init(resourceObject: T, meta: MetaType, links: LinksType) where T.Id == Identifiable.Identifier { - self.init(id: resourceObject.id, meta: meta, links: links) - } + public init(resourceObject: T, meta: MetaType, links: LinksType) where T.Id == Identifiable.Identifier { + self.init(id: resourceObject.id, meta: meta, links: links) + } } extension ToOneRelationship where MetaType == NoMetadata, LinksType == NoLinks { - public init(resourceObject: T) where T.Id == Identifiable.Identifier { - self.init(id: resourceObject.id, meta: .none, links: .none) - } + public init(resourceObject: T) where T.Id == Identifiable.Identifier { + self.init(id: resourceObject.id, meta: .none, links: .none) + } } extension ToOneRelationship where Identifiable: OptionalRelatable { - public init(resourceObject: T?, meta: MetaType, links: LinksType) where T.Id == Identifiable.Wrapped.Identifier { - self.init(id: resourceObject?.id, meta: meta, links: links) - } + public init(resourceObject: T?, meta: MetaType, links: LinksType) where T.Id == Identifiable.Wrapped.Identifier { + self.init(id: resourceObject?.id, meta: meta, links: links) + } } extension ToOneRelationship where Identifiable: OptionalRelatable, MetaType == NoMetadata, LinksType == NoLinks { - public init(resourceObject: T?) where T.Id == Identifiable.Wrapped.Identifier { - self.init(id: resourceObject?.id, meta: .none, links: .none) - } + public init(resourceObject: T?) where T.Id == Identifiable.Wrapped.Identifier { + self.init(id: resourceObject?.id, meta: .none, links: .none) + } } /// An ResourceObject relationship that can be encoded to or decoded from @@ -67,57 +67,57 @@ extension ToOneRelationship where Identifiable: OptionalRelatable, MetaType == N /// A convenient typealias might make your code much more legible: `Many` public struct ToManyRelationship: RelationshipType, Equatable { - public let ids: [Relatable.Identifier] + public let ids: [Relatable.Identifier] - public let meta: MetaType - public let links: LinksType + public let meta: MetaType + public let links: LinksType - public init(ids: [Relatable.Identifier], meta: MetaType, links: LinksType) { - self.ids = ids - self.meta = meta - self.links = links - } + public init(ids: [Relatable.Identifier], meta: MetaType, links: LinksType) { + self.ids = ids + self.meta = meta + self.links = links + } - public init(pointers: [ToOneRelationship], meta: MetaType, links: LinksType) where T.Identifier == Relatable.Identifier { - ids = pointers.map { $0.id } - self.meta = meta - self.links = links - } + public init(pointers: [ToOneRelationship], meta: MetaType, links: LinksType) where T.Identifier == Relatable.Identifier { + ids = pointers.map { $0.id } + self.meta = meta + self.links = links + } - public init(resourceObjects: [T], meta: MetaType, links: LinksType) where T.Id == Relatable.Identifier { - self.init(ids: resourceObjects.map { $0.id }, meta: meta, links: links) - } + public init(resourceObjects: [T], meta: MetaType, links: LinksType) where T.Id == Relatable.Identifier { + self.init(ids: resourceObjects.map { $0.id }, meta: meta, links: links) + } - private init(meta: MetaType, links: LinksType) { - self.init(ids: [], meta: meta, links: links) - } + private init(meta: MetaType, links: LinksType) { + self.init(ids: [], meta: meta, links: links) + } - public static func none(withMeta meta: MetaType, links: LinksType) -> ToManyRelationship { - return ToManyRelationship(meta: meta, links: links) - } + public static func none(withMeta meta: MetaType, links: LinksType) -> ToManyRelationship { + return ToManyRelationship(meta: meta, links: links) + } } extension ToManyRelationship where MetaType == NoMetadata, LinksType == NoLinks { - public init(ids: [Relatable.Identifier]) { - self.init(ids: ids, meta: .none, links: .none) - } + public init(ids: [Relatable.Identifier]) { + self.init(ids: ids, meta: .none, links: .none) + } - public init(pointers: [ToOneRelationship]) where T.Identifier == Relatable.Identifier { - self.init(pointers: pointers, meta: .none, links: .none) - } + public init(pointers: [ToOneRelationship]) where T.Identifier == Relatable.Identifier { + self.init(pointers: pointers, meta: .none, links: .none) + } - public static var none: ToManyRelationship { - return .none(withMeta: .none, links: .none) - } + public static var none: ToManyRelationship { + return .none(withMeta: .none, links: .none) + } - public init(resourceObjects: [T]) where T.Id == Relatable.Identifier { - self.init(resourceObjects: resourceObjects, meta: .none, links: .none) - } + public init(resourceObjects: [T]) where T.Id == Relatable.Identifier { + self.init(resourceObjects: resourceObjects, meta: .none, links: .none) + } } public protocol Identifiable: JSONTyped { - associatedtype Identifier: Equatable + associatedtype Identifier: Equatable } /// The Relatable protocol describes anything that @@ -128,152 +128,152 @@ public protocol Relatable: Identifiable where Identifier: JSONAPI.IdType { /// OptionalRelatable just describes an Optional /// with a Reltable Wrapped type. public protocol OptionalRelatable: Identifiable where Identifier == Wrapped.Identifier? { - associatedtype Wrapped: JSONAPI.Relatable + associatedtype Wrapped: JSONAPI.Relatable } extension Optional: Identifiable, OptionalRelatable, JSONTyped where Wrapped: JSONAPI.Relatable { - public typealias Identifier = Wrapped.Identifier? + public typealias Identifier = Wrapped.Identifier? - public static var jsonType: String { return Wrapped.jsonType } + public static var jsonType: String { return Wrapped.jsonType } } // MARK: Codable private enum ResourceLinkageCodingKeys: String, CodingKey { - case data = "data" - case meta = "meta" - case links = "links" + case data = "data" + case meta = "meta" + case links = "links" } private enum ResourceIdentifierCodingKeys: String, CodingKey { - case id = "id" - case entityType = "type" + case id = "id" + case entityType = "type" } extension ToOneRelationship: Codable where Identifiable.Identifier: OptionalId { - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: ResourceLinkageCodingKeys.self) - - if let noMeta = NoMetadata() as? MetaType { - meta = noMeta - } else { - meta = try container.decode(MetaType.self, forKey: .meta) - } - - if let noLinks = NoLinks() as? LinksType { - links = noLinks - } else { - links = try container.decode(LinksType.self, forKey: .links) - } - - // A little trickery follows. If the id is nil, the - // container.decode(Identifier.self) will fail even if Identifier - // is Optional. However, we can check if decoding nil - // succeeds and then attempt to coerce nil to a Identifier - // type at which point we can store nil in `id`. - let anyNil: Any? = nil - if try container.decodeNil(forKey: .data), - let val = anyNil as? Identifiable.Identifier { - id = val - return - } - - let identifier = try container.nestedContainer(keyedBy: ResourceIdentifierCodingKeys.self, forKey: .data) - - let type = try identifier.decode(String.self, forKey: .entityType) - - guard type == Identifiable.jsonType else { - throw JSONAPIEncodingError.typeMismatch(expected: Identifiable.jsonType, found: type) - } - - id = Identifiable.Identifier(rawValue: try identifier.decode(Identifiable.Identifier.RawType.self, forKey: .id)) - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: ResourceLinkageCodingKeys.self) - - if MetaType.self != NoMetadata.self { - try container.encode(meta, forKey: .meta) - } - - if LinksType.self != NoLinks.self { - try container.encode(links, forKey: .links) - } - - // If id is nil, instead of {id: , type: } we will just - // encode `null` - let anyNil: Any? = nil - let nilId = anyNil as? Identifiable.Identifier - guard id != nilId else { - try container.encodeNil(forKey: .data) - return - } - - var identifier = container.nestedContainer(keyedBy: ResourceIdentifierCodingKeys.self, forKey: .data) - - try identifier.encode(id.rawValue, forKey: .id) - try identifier.encode(Identifiable.jsonType, forKey: .entityType) - } + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: ResourceLinkageCodingKeys.self) + + if let noMeta = NoMetadata() as? MetaType { + meta = noMeta + } else { + meta = try container.decode(MetaType.self, forKey: .meta) + } + + if let noLinks = NoLinks() as? LinksType { + links = noLinks + } else { + links = try container.decode(LinksType.self, forKey: .links) + } + + // A little trickery follows. If the id is nil, the + // container.decode(Identifier.self) will fail even if Identifier + // is Optional. However, we can check if decoding nil + // succeeds and then attempt to coerce nil to a Identifier + // type at which point we can store nil in `id`. + let anyNil: Any? = nil + if try container.decodeNil(forKey: .data), + let val = anyNil as? Identifiable.Identifier { + id = val + return + } + + let identifier = try container.nestedContainer(keyedBy: ResourceIdentifierCodingKeys.self, forKey: .data) + + let type = try identifier.decode(String.self, forKey: .entityType) + + guard type == Identifiable.jsonType else { + throw JSONAPIEncodingError.typeMismatch(expected: Identifiable.jsonType, found: type) + } + + id = Identifiable.Identifier(rawValue: try identifier.decode(Identifiable.Identifier.RawType.self, forKey: .id)) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: ResourceLinkageCodingKeys.self) + + if MetaType.self != NoMetadata.self { + try container.encode(meta, forKey: .meta) + } + + if LinksType.self != NoLinks.self { + try container.encode(links, forKey: .links) + } + + // If id is nil, instead of {id: , type: } we will just + // encode `null` + let anyNil: Any? = nil + let nilId = anyNil as? Identifiable.Identifier + guard id != nilId else { + try container.encodeNil(forKey: .data) + return + } + + var identifier = container.nestedContainer(keyedBy: ResourceIdentifierCodingKeys.self, forKey: .data) + + try identifier.encode(id.rawValue, forKey: .id) + try identifier.encode(Identifiable.jsonType, forKey: .entityType) + } } extension ToManyRelationship: Codable { - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: ResourceLinkageCodingKeys.self) - - if let noMeta = NoMetadata() as? MetaType { - meta = noMeta - } else { - meta = try container.decode(MetaType.self, forKey: .meta) - } - - if let noLinks = NoLinks() as? LinksType { - links = noLinks - } else { - links = try container.decode(LinksType.self, forKey: .links) - } - - var identifiers = try container.nestedUnkeyedContainer(forKey: .data) - - var newIds = [Relatable.Identifier]() - while !identifiers.isAtEnd { - let identifier = try identifiers.nestedContainer(keyedBy: ResourceIdentifierCodingKeys.self) - - let type = try identifier.decode(String.self, forKey: .entityType) - - guard type == Relatable.jsonType else { - throw JSONAPIEncodingError.typeMismatch(expected: Relatable.jsonType, found: type) - } - - newIds.append(Relatable.Identifier(rawValue: try identifier.decode(Relatable.Identifier.RawType.self, forKey: .id))) - } - ids = newIds - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: ResourceLinkageCodingKeys.self) - - if MetaType.self != NoMetadata.self { - try container.encode(meta, forKey: .meta) - } - - if LinksType.self != NoLinks.self { - try container.encode(links, forKey: .links) - } - - var identifiers = container.nestedUnkeyedContainer(forKey: .data) - - for id in ids { - var identifier = identifiers.nestedContainer(keyedBy: ResourceIdentifierCodingKeys.self) - - try identifier.encode(id.rawValue, forKey: .id) - try identifier.encode(Relatable.jsonType, forKey: .entityType) - } - } + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: ResourceLinkageCodingKeys.self) + + if let noMeta = NoMetadata() as? MetaType { + meta = noMeta + } else { + meta = try container.decode(MetaType.self, forKey: .meta) + } + + if let noLinks = NoLinks() as? LinksType { + links = noLinks + } else { + links = try container.decode(LinksType.self, forKey: .links) + } + + var identifiers = try container.nestedUnkeyedContainer(forKey: .data) + + var newIds = [Relatable.Identifier]() + while !identifiers.isAtEnd { + let identifier = try identifiers.nestedContainer(keyedBy: ResourceIdentifierCodingKeys.self) + + let type = try identifier.decode(String.self, forKey: .entityType) + + guard type == Relatable.jsonType else { + throw JSONAPIEncodingError.typeMismatch(expected: Relatable.jsonType, found: type) + } + + newIds.append(Relatable.Identifier(rawValue: try identifier.decode(Relatable.Identifier.RawType.self, forKey: .id))) + } + ids = newIds + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: ResourceLinkageCodingKeys.self) + + if MetaType.self != NoMetadata.self { + try container.encode(meta, forKey: .meta) + } + + if LinksType.self != NoLinks.self { + try container.encode(links, forKey: .links) + } + + var identifiers = container.nestedUnkeyedContainer(forKey: .data) + + for id in ids { + var identifier = identifiers.nestedContainer(keyedBy: ResourceIdentifierCodingKeys.self) + + try identifier.encode(id.rawValue, forKey: .id) + try identifier.encode(Relatable.jsonType, forKey: .entityType) + } + } } // MARK: CustomStringDescribable extension ToOneRelationship: CustomStringConvertible { - public var description: String { return "Relationship(\(String(describing: id)))" } + public var description: String { return "Relationship(\(String(describing: id)))" } } extension ToManyRelationship: CustomStringConvertible { - public var description: String { return "Relationship([\(ids.map(String.init(describing:)).joined(separator: ", "))])" } + public var description: String { return "Relationship([\(ids.map(String.init(describing:)).joined(separator: ", "))])" } } diff --git a/Sources/JSONAPI/Resource/Resource Object/ResourceObject.swift b/Sources/JSONAPI/Resource/Resource Object/ResourceObject.swift index a62d271..b242778 100644 --- a/Sources/JSONAPI/Resource/Resource Object/ResourceObject.swift +++ b/Sources/JSONAPI/Resource/Resource Object/ResourceObject.swift @@ -28,34 +28,34 @@ public protocol SparsableAttributes: Attributes { /// Can be used as `Relationships` Type for Entities that do not /// have any Relationships. public struct NoRelationships: Relationships { - public static var none: NoRelationships { return .init() } + public static var none: NoRelationships { return .init() } } extension NoRelationships: CustomStringConvertible { - public var description: String { return "No Relationships" } + public var description: String { return "No Relationships" } } /// Can be used as `Attributes` Type for Entities that do not /// have any Attributes. public struct NoAttributes: Attributes { - public static var none: NoAttributes { return .init() } + public static var none: NoAttributes { return .init() } } extension NoAttributes: CustomStringConvertible { - public var description: String { return "No Attributes" } + public var description: String { return "No Attributes" } } /// Something that is JSONTyped provides a String representation /// of its type. public protocol JSONTyped { - static var jsonType: String { get } + static var jsonType: String { get } } /// A `ResourceObjectProxyDescription` is an `ResourceObjectDescription` /// without Codable conformance. public protocol ResourceObjectProxyDescription: JSONTyped { - associatedtype Attributes: Equatable - associatedtype Relationships: Equatable + associatedtype Attributes: Equatable + associatedtype Relationships: Equatable } /// A `ResourceObjectDescription` describes a JSON API @@ -70,38 +70,38 @@ public protocol ResourceObjectDescription: ResourceObjectProxyDescription where /// or decoded as ResourceObjects. @dynamicMemberLookup public protocol ResourceObjectProxy: Equatable, JSONTyped { - associatedtype Description: ResourceObjectProxyDescription - associatedtype EntityRawIdType: JSONAPI.MaybeRawId + associatedtype Description: ResourceObjectProxyDescription + associatedtype EntityRawIdType: JSONAPI.MaybeRawId - typealias Id = JSONAPI.Id + typealias Id = JSONAPI.Id - typealias Attributes = Description.Attributes - typealias Relationships = Description.Relationships + typealias Attributes = Description.Attributes + typealias Relationships = Description.Relationships - /// The `Entity`'s Id. This can be of type `Unidentified` if - /// the entity is being created clientside and the - /// server is being asked to create a unique Id. Otherwise, - /// this should be of a type conforming to `IdType`. - var id: Id { get } + /// The `Entity`'s Id. This can be of type `Unidentified` if + /// the entity is being created clientside and the + /// server is being asked to create a unique Id. Otherwise, + /// this should be of a type conforming to `IdType`. + var id: Id { get } - /// The JSON API compliant attributes of this `Entity`. - var attributes: Attributes { get } + /// The JSON API compliant attributes of this `Entity`. + var attributes: Attributes { get } - /// The JSON API compliant relationships of this `Entity`. - var relationships: Relationships { get } + /// The JSON API compliant relationships of this `Entity`. + var relationships: Relationships { get } } extension ResourceObjectProxy { - /// The JSON API compliant "type" of this `ResourceObject`. - public static var jsonType: String { return Description.jsonType } + /// The JSON API compliant "type" of this `ResourceObject`. + public static var jsonType: String { return Description.jsonType } } /// ResourceObjectType is the protocol that ResourceObject conforms to. This /// protocol lets other types accept any ResourceObject as a generic /// specialization. public protocol ResourceObjectType: ResourceObjectProxy, PrimaryResource where Description: ResourceObjectDescription { - associatedtype Meta: JSONAPI.Meta - associatedtype Links: JSONAPI.Links + associatedtype Meta: JSONAPI.Meta + associatedtype Links: JSONAPI.Links } public protocol IdentifiableResourceObjectType: ResourceObjectType, Relatable where EntityRawIdType: JSONAPI.RawIdType {} @@ -112,369 +112,369 @@ public protocol IdentifiableResourceObjectType: ResourceObjectType, Relatable wh /// See https://jsonapi.org/format/#document-resource-objects public struct ResourceObject: ResourceObjectType { - public typealias Meta = MetaType - public typealias Links = LinksType - - /// The `ResourceObject`'s Id. This can be of type `Unidentified` if - /// the entity is being created clientside and the - /// server is being asked to create a unique Id. Otherwise, - /// this should be of a type conforming to `IdType`. - public let id: ResourceObject.Id - - /// The JSON API compliant attributes of this `ResourceObject`. - public let attributes: Description.Attributes - - /// The JSON API compliant relationships of this `ResourceObject`. - public let relationships: Description.Relationships - - /// Any additional metadata packaged with the entity. - public let meta: MetaType - - /// Links related to the entity. - public let links: LinksType - - public init(id: ResourceObject.Id, attributes: Description.Attributes, relationships: Description.Relationships, meta: MetaType, links: LinksType) { - self.id = id - self.attributes = attributes - self.relationships = relationships - self.meta = meta - self.links = links - } -} - -extension ResourceObject: Identifiable, IdentifiableResourceObjectType, Relatable where EntityRawIdType: JSONAPI.RawIdType { - public typealias Identifier = ResourceObject.Id -} - -extension ResourceObject: CustomStringConvertible { - public var description: String { - return "ResourceObject<\(ResourceObject.jsonType)>(id: \(String(describing: id)), attributes: \(String(describing: attributes)), relationships: \(String(describing: relationships)))" - } -} - -// MARK: - Convenience initializers -extension ResourceObject where EntityRawIdType: CreatableRawIdType { - public init(attributes: Description.Attributes, relationships: Description.Relationships, meta: MetaType, links: LinksType) { - self.id = ResourceObject.Id() - self.attributes = attributes - self.relationships = relationships - self.meta = meta - self.links = links - } -} - -extension ResourceObject where EntityRawIdType == Unidentified { - public init(attributes: Description.Attributes, relationships: Description.Relationships, meta: MetaType, links: LinksType) { - self.id = .unidentified - self.attributes = attributes - self.relationships = relationships - self.meta = meta - self.links = links - } -} - -/* -extension ResourceObject where Description.Attributes == NoAttributes { - public init(id: ResourceObject.Id, relationships: Description.Relationships, meta: MetaType, links: LinksType) { - self.init(id: id, attributes: NoAttributes(), relationships: relationships, meta: meta, links: links) - } -} - -extension ResourceObject where Description.Attributes == NoAttributes, MetaType == NoMetadata { - public init(id: ResourceObject.Id, relationships: Description.Relationships, links: LinksType) { - self.init(id: id, relationships: relationships, meta: .none, links: links) - } -} - -extension ResourceObject where Description.Attributes == NoAttributes, LinksType == NoLinks { - public init(id: ResourceObject.Id, relationships: Description.Relationships, meta: MetaType) { - self.init(id: id, relationships: relationships, meta: meta, links: .none) - } -} - -extension ResourceObject where Description.Attributes == NoAttributes, MetaType == NoMetadata, LinksType == NoLinks { - public init(id: ResourceObject.Id, relationships: Description.Relationships) { - self.init(id: id, relationships: relationships, links: .none) - } -} - -extension ResourceObject where Description.Attributes == NoAttributes, EntityRawIdType: CreatableRawIdType { - public init(relationships: Description.Relationships, meta: MetaType, links: LinksType) { - self.init(attributes: NoAttributes(), relationships: relationships, meta: meta, links: links) - } -} - -extension ResourceObject where Description.Attributes == NoAttributes, MetaType == NoMetadata, EntityRawIdType: CreatableRawIdType { - public init(relationships: Description.Relationships, links: LinksType) { - self.init(attributes: NoAttributes(), relationships: relationships, meta: .none, links: links) - } -} + public typealias Meta = MetaType + public typealias Links = LinksType -extension ResourceObject where Description.Attributes == NoAttributes, LinksType == NoLinks, EntityRawIdType: CreatableRawIdType { - public init(relationships: Description.Relationships, meta: MetaType) { - self.init(attributes: NoAttributes(), relationships: relationships, meta: meta, links: .none) - } -} + /// The `ResourceObject`'s Id. This can be of type `Unidentified` if + /// the entity is being created clientside and the + /// server is being asked to create a unique Id. Otherwise, + /// this should be of a type conforming to `IdType`. + public let id: ResourceObject.Id -extension ResourceObject where Description.Attributes == NoAttributes, MetaType == NoMetadata, LinksType == NoLinks, EntityRawIdType: CreatableRawIdType { - public init(relationships: Description.Relationships) { - self.init(attributes: NoAttributes(), relationships: relationships, meta: .none, links: .none) - } -} + /// The JSON API compliant attributes of this `ResourceObject`. + public let attributes: Description.Attributes -extension ResourceObject where Description.Attributes == NoAttributes, EntityRawIdType == Unidentified { - public init(relationships: Description.Relationships, meta: MetaType, links: LinksType) { - self.init(attributes: NoAttributes(), relationships: relationships, meta: meta, links: links) - } -} + /// The JSON API compliant relationships of this `ResourceObject`. + public let relationships: Description.Relationships -extension ResourceObject where Description.Relationships == NoRelationships { - public init(id: ResourceObject.Id, attributes: Description.Attributes, meta: MetaType, links: LinksType) { - self.init(id: id, attributes: attributes, relationships: NoRelationships(), meta: meta, links: links) - } -} + /// Any additional metadata packaged with the entity. + public let meta: MetaType -extension ResourceObject where Description.Relationships == NoRelationships, MetaType == NoMetadata { - public init(id: ResourceObject.Id, attributes: Description.Attributes, links: LinksType) { - self.init(id: id, attributes: attributes, meta: .none, links: links) - } -} + /// Links related to the entity. + public let links: LinksType -extension ResourceObject where Description.Relationships == NoRelationships, LinksType == NoLinks { - public init(id: ResourceObject.Id, attributes: Description.Attributes, meta: MetaType) { - self.init(id: id, attributes: attributes, meta: meta, links: .none) - } -} - -extension ResourceObject where Description.Relationships == NoRelationships, MetaType == NoMetadata, LinksType == NoLinks { - public init(id: ResourceObject.Id, attributes: Description.Attributes) { - self.init(id: id, attributes: attributes, meta: .none, links: .none) - } -} - -extension ResourceObject where Description.Relationships == NoRelationships, EntityRawIdType: CreatableRawIdType { - public init(attributes: Description.Attributes, meta: MetaType, links: LinksType) { - self.init(attributes: attributes, relationships: NoRelationships(), meta: meta, links: links) - } -} - -extension ResourceObject where Description.Relationships == NoRelationships, MetaType == NoMetadata, EntityRawIdType: CreatableRawIdType { - public init(attributes: Description.Attributes, links: LinksType) { - self.init(attributes: attributes, relationships: NoRelationships(), meta: .none, links: links) - } -} - -extension ResourceObject where Description.Relationships == NoRelationships, LinksType == NoLinks, EntityRawIdType: CreatableRawIdType { - public init(attributes: Description.Attributes, meta: MetaType) { - self.init(attributes: attributes, relationships: NoRelationships(), meta: meta, links: .none) - } -} - -extension ResourceObject where Description.Relationships == NoRelationships, MetaType == NoMetadata, LinksType == NoLinks, EntityRawIdType: CreatableRawIdType { - public init(attributes: Description.Attributes) { - self.init(attributes: attributes, relationships: NoRelationships(), meta: .none, links: .none) - } -} - -extension ResourceObject where Description.Relationships == NoRelationships, EntityRawIdType == Unidentified { - public init(attributes: Description.Attributes, meta: MetaType, links: LinksType) { - self.init(attributes: attributes, relationships: NoRelationships(), meta: meta, links: links) - } -} - -extension ResourceObject where Description.Relationships == NoRelationships, MetaType == NoMetadata, EntityRawIdType == Unidentified { - public init(attributes: Description.Attributes, links: LinksType) { - self.init(attributes: attributes, relationships: NoRelationships(), meta: .none, links: links) - } -} - -extension ResourceObject where Description.Relationships == NoRelationships, LinksType == NoLinks, EntityRawIdType == Unidentified { - public init(attributes: Description.Attributes, meta: MetaType) { - self.init(attributes: attributes, relationships: NoRelationships(), meta: meta, links: .none) - } -} - -extension ResourceObject where Description.Relationships == NoRelationships, MetaType == NoMetadata, LinksType == NoLinks, EntityRawIdType == Unidentified { - public init(attributes: Description.Attributes) { - self.init(attributes: attributes, relationships: NoRelationships(), meta: .none, links: .none) - } -} - -extension ResourceObject where Description.Attributes == NoAttributes, Description.Relationships == NoRelationships { - public init(id: ResourceObject.Id, meta: MetaType, links: LinksType) { - self.init(id: id, attributes: NoAttributes(), relationships: NoRelationships(), meta: meta, links: links) - } -} - -extension ResourceObject where Description.Attributes == NoAttributes, Description.Relationships == NoRelationships, MetaType == NoMetadata { - public init(id: ResourceObject.Id, links: LinksType) { - self.init(id: id, attributes: NoAttributes(), relationships: NoRelationships(), meta: .none, links: links) - } -} - -extension ResourceObject where Description.Attributes == NoAttributes, Description.Relationships == NoRelationships, LinksType == NoLinks { - public init(id: ResourceObject.Id, meta: MetaType) { - self.init(id: id, attributes: NoAttributes(), relationships: NoRelationships(), meta: meta, links: .none) - } -} - -extension ResourceObject where Description.Attributes == NoAttributes, Description.Relationships == NoRelationships, MetaType == NoMetadata, LinksType == NoLinks { - public init(id: ResourceObject.Id) { - self.init(id: id, attributes: NoAttributes(), relationships: NoRelationships(), meta: .none, links: .none) - } -} - -extension ResourceObject where Description.Attributes == NoAttributes, Description.Relationships == NoRelationships, EntityRawIdType: CreatableRawIdType { - public init(meta: MetaType, links: LinksType) { - self.init(attributes: NoAttributes(), relationships: NoRelationships(), meta: meta, links: links) - } -} - -extension ResourceObject where Description.Attributes == NoAttributes, Description.Relationships == NoRelationships, MetaType == NoMetadata, EntityRawIdType: CreatableRawIdType { - public init(links: LinksType) { - self.init(attributes: NoAttributes(), relationships: NoRelationships(), meta: .none, links: links) - } -} - -extension ResourceObject where Description.Attributes == NoAttributes, Description.Relationships == NoRelationships, LinksType == NoLinks, EntityRawIdType: CreatableRawIdType { - public init(meta: MetaType) { - self.init(attributes: NoAttributes(), relationships: NoRelationships(), meta: meta, links: .none) - } -} - -extension ResourceObject where Description.Attributes == NoAttributes, Description.Relationships == NoRelationships, MetaType == NoMetadata, LinksType == NoLinks, EntityRawIdType: CreatableRawIdType { - public init() { - self.init(attributes: NoAttributes(), relationships: NoRelationships(), meta: .none, links: .none) - } -} - -extension ResourceObject where MetaType == NoMetadata { - public init(id: ResourceObject.Id, attributes: Description.Attributes, relationships: Description.Relationships, links: LinksType) { - self.init(id: id, attributes: attributes, relationships: relationships, meta: .none, links: links) - } -} - -extension ResourceObject where MetaType == NoMetadata, EntityRawIdType: CreatableRawIdType { - public init(attributes: Description.Attributes, relationships: Description.Relationships, links: LinksType) { - self.init(attributes: attributes, relationships: relationships, meta: .none, links: links) - } -} - -extension ResourceObject where MetaType == NoMetadata, EntityRawIdType == Unidentified { - public init(attributes: Description.Attributes, relationships: Description.Relationships, links: LinksType) { - self.init(attributes: attributes, relationships: relationships, meta: .none, links: links) - } -} - -extension ResourceObject where LinksType == NoLinks { - public init(id: ResourceObject.Id, attributes: Description.Attributes, relationships: Description.Relationships, meta: MetaType) { - self.init(id: id, attributes: attributes, relationships: relationships, meta: meta, links: .none) - } + public init(id: ResourceObject.Id, attributes: Description.Attributes, relationships: Description.Relationships, meta: MetaType, links: LinksType) { + self.id = id + self.attributes = attributes + self.relationships = relationships + self.meta = meta + self.links = links + } } -extension ResourceObject where LinksType == NoLinks, EntityRawIdType: CreatableRawIdType { - public init(attributes: Description.Attributes, relationships: Description.Relationships, meta: MetaType) { - self.init(attributes: attributes, relationships: relationships, meta: meta, links: .none) - } +extension ResourceObject: Identifiable, IdentifiableResourceObjectType, Relatable where EntityRawIdType: JSONAPI.RawIdType { + public typealias Identifier = ResourceObject.Id } -extension ResourceObject where LinksType == NoLinks, EntityRawIdType == Unidentified { - public init(attributes: Description.Attributes, relationships: Description.Relationships, meta: MetaType) { - self.init(attributes: attributes, relationships: relationships, meta: meta, links: .none) - } +extension ResourceObject: CustomStringConvertible { + public var description: String { + return "ResourceObject<\(ResourceObject.jsonType)>(id: \(String(describing: id)), attributes: \(String(describing: attributes)), relationships: \(String(describing: relationships)))" + } } -extension ResourceObject where MetaType == NoMetadata, LinksType == NoLinks { - public init(id: ResourceObject.Id, attributes: Description.Attributes, relationships: Description.Relationships) { - self.init(id: id, attributes: attributes, relationships: relationships, meta: .none, links: .none) - } +// MARK: - Convenience initializers +extension ResourceObject where EntityRawIdType: CreatableRawIdType { + public init(attributes: Description.Attributes, relationships: Description.Relationships, meta: MetaType, links: LinksType) { + self.id = ResourceObject.Id() + self.attributes = attributes + self.relationships = relationships + self.meta = meta + self.links = links + } } -extension ResourceObject where MetaType == NoMetadata, LinksType == NoLinks, EntityRawIdType: CreatableRawIdType { - public init(attributes: Description.Attributes, relationships: Description.Relationships) { - self.init(attributes: attributes, relationships: relationships, meta: .none, links: .none) - } +extension ResourceObject where EntityRawIdType == Unidentified { + public init(attributes: Description.Attributes, relationships: Description.Relationships, meta: MetaType, links: LinksType) { + self.id = .unidentified + self.attributes = attributes + self.relationships = relationships + self.meta = meta + self.links = links + } } -extension ResourceObject where MetaType == NoMetadata, LinksType == NoLinks, EntityRawIdType == Unidentified { - public init(attributes: Description.Attributes, relationships: Description.Relationships) { - self.init(attributes: attributes, relationships: relationships, meta: .none, links: .none) - } -} -*/ +/* + extension ResourceObject where Description.Attributes == NoAttributes { + public init(id: ResourceObject.Id, relationships: Description.Relationships, meta: MetaType, links: LinksType) { + self.init(id: id, attributes: NoAttributes(), relationships: relationships, meta: meta, links: links) + } + } + + extension ResourceObject where Description.Attributes == NoAttributes, MetaType == NoMetadata { + public init(id: ResourceObject.Id, relationships: Description.Relationships, links: LinksType) { + self.init(id: id, relationships: relationships, meta: .none, links: links) + } + } + + extension ResourceObject where Description.Attributes == NoAttributes, LinksType == NoLinks { + public init(id: ResourceObject.Id, relationships: Description.Relationships, meta: MetaType) { + self.init(id: id, relationships: relationships, meta: meta, links: .none) + } + } + + extension ResourceObject where Description.Attributes == NoAttributes, MetaType == NoMetadata, LinksType == NoLinks { + public init(id: ResourceObject.Id, relationships: Description.Relationships) { + self.init(id: id, relationships: relationships, links: .none) + } + } + + extension ResourceObject where Description.Attributes == NoAttributes, EntityRawIdType: CreatableRawIdType { + public init(relationships: Description.Relationships, meta: MetaType, links: LinksType) { + self.init(attributes: NoAttributes(), relationships: relationships, meta: meta, links: links) + } + } + + extension ResourceObject where Description.Attributes == NoAttributes, MetaType == NoMetadata, EntityRawIdType: CreatableRawIdType { + public init(relationships: Description.Relationships, links: LinksType) { + self.init(attributes: NoAttributes(), relationships: relationships, meta: .none, links: links) + } + } + + extension ResourceObject where Description.Attributes == NoAttributes, LinksType == NoLinks, EntityRawIdType: CreatableRawIdType { + public init(relationships: Description.Relationships, meta: MetaType) { + self.init(attributes: NoAttributes(), relationships: relationships, meta: meta, links: .none) + } + } + + extension ResourceObject where Description.Attributes == NoAttributes, MetaType == NoMetadata, LinksType == NoLinks, EntityRawIdType: CreatableRawIdType { + public init(relationships: Description.Relationships) { + self.init(attributes: NoAttributes(), relationships: relationships, meta: .none, links: .none) + } + } + + extension ResourceObject where Description.Attributes == NoAttributes, EntityRawIdType == Unidentified { + public init(relationships: Description.Relationships, meta: MetaType, links: LinksType) { + self.init(attributes: NoAttributes(), relationships: relationships, meta: meta, links: links) + } + } + + extension ResourceObject where Description.Relationships == NoRelationships { + public init(id: ResourceObject.Id, attributes: Description.Attributes, meta: MetaType, links: LinksType) { + self.init(id: id, attributes: attributes, relationships: NoRelationships(), meta: meta, links: links) + } + } + + extension ResourceObject where Description.Relationships == NoRelationships, MetaType == NoMetadata { + public init(id: ResourceObject.Id, attributes: Description.Attributes, links: LinksType) { + self.init(id: id, attributes: attributes, meta: .none, links: links) + } + } + + extension ResourceObject where Description.Relationships == NoRelationships, LinksType == NoLinks { + public init(id: ResourceObject.Id, attributes: Description.Attributes, meta: MetaType) { + self.init(id: id, attributes: attributes, meta: meta, links: .none) + } + } + + extension ResourceObject where Description.Relationships == NoRelationships, MetaType == NoMetadata, LinksType == NoLinks { + public init(id: ResourceObject.Id, attributes: Description.Attributes) { + self.init(id: id, attributes: attributes, meta: .none, links: .none) + } + } + + extension ResourceObject where Description.Relationships == NoRelationships, EntityRawIdType: CreatableRawIdType { + public init(attributes: Description.Attributes, meta: MetaType, links: LinksType) { + self.init(attributes: attributes, relationships: NoRelationships(), meta: meta, links: links) + } + } + + extension ResourceObject where Description.Relationships == NoRelationships, MetaType == NoMetadata, EntityRawIdType: CreatableRawIdType { + public init(attributes: Description.Attributes, links: LinksType) { + self.init(attributes: attributes, relationships: NoRelationships(), meta: .none, links: links) + } + } + + extension ResourceObject where Description.Relationships == NoRelationships, LinksType == NoLinks, EntityRawIdType: CreatableRawIdType { + public init(attributes: Description.Attributes, meta: MetaType) { + self.init(attributes: attributes, relationships: NoRelationships(), meta: meta, links: .none) + } + } + + extension ResourceObject where Description.Relationships == NoRelationships, MetaType == NoMetadata, LinksType == NoLinks, EntityRawIdType: CreatableRawIdType { + public init(attributes: Description.Attributes) { + self.init(attributes: attributes, relationships: NoRelationships(), meta: .none, links: .none) + } + } + + extension ResourceObject where Description.Relationships == NoRelationships, EntityRawIdType == Unidentified { + public init(attributes: Description.Attributes, meta: MetaType, links: LinksType) { + self.init(attributes: attributes, relationships: NoRelationships(), meta: meta, links: links) + } + } + + extension ResourceObject where Description.Relationships == NoRelationships, MetaType == NoMetadata, EntityRawIdType == Unidentified { + public init(attributes: Description.Attributes, links: LinksType) { + self.init(attributes: attributes, relationships: NoRelationships(), meta: .none, links: links) + } + } + + extension ResourceObject where Description.Relationships == NoRelationships, LinksType == NoLinks, EntityRawIdType == Unidentified { + public init(attributes: Description.Attributes, meta: MetaType) { + self.init(attributes: attributes, relationships: NoRelationships(), meta: meta, links: .none) + } + } + + extension ResourceObject where Description.Relationships == NoRelationships, MetaType == NoMetadata, LinksType == NoLinks, EntityRawIdType == Unidentified { + public init(attributes: Description.Attributes) { + self.init(attributes: attributes, relationships: NoRelationships(), meta: .none, links: .none) + } + } + + extension ResourceObject where Description.Attributes == NoAttributes, Description.Relationships == NoRelationships { + public init(id: ResourceObject.Id, meta: MetaType, links: LinksType) { + self.init(id: id, attributes: NoAttributes(), relationships: NoRelationships(), meta: meta, links: links) + } + } + + extension ResourceObject where Description.Attributes == NoAttributes, Description.Relationships == NoRelationships, MetaType == NoMetadata { + public init(id: ResourceObject.Id, links: LinksType) { + self.init(id: id, attributes: NoAttributes(), relationships: NoRelationships(), meta: .none, links: links) + } + } + + extension ResourceObject where Description.Attributes == NoAttributes, Description.Relationships == NoRelationships, LinksType == NoLinks { + public init(id: ResourceObject.Id, meta: MetaType) { + self.init(id: id, attributes: NoAttributes(), relationships: NoRelationships(), meta: meta, links: .none) + } + } + + extension ResourceObject where Description.Attributes == NoAttributes, Description.Relationships == NoRelationships, MetaType == NoMetadata, LinksType == NoLinks { + public init(id: ResourceObject.Id) { + self.init(id: id, attributes: NoAttributes(), relationships: NoRelationships(), meta: .none, links: .none) + } + } + + extension ResourceObject where Description.Attributes == NoAttributes, Description.Relationships == NoRelationships, EntityRawIdType: CreatableRawIdType { + public init(meta: MetaType, links: LinksType) { + self.init(attributes: NoAttributes(), relationships: NoRelationships(), meta: meta, links: links) + } + } + + extension ResourceObject where Description.Attributes == NoAttributes, Description.Relationships == NoRelationships, MetaType == NoMetadata, EntityRawIdType: CreatableRawIdType { + public init(links: LinksType) { + self.init(attributes: NoAttributes(), relationships: NoRelationships(), meta: .none, links: links) + } + } + + extension ResourceObject where Description.Attributes == NoAttributes, Description.Relationships == NoRelationships, LinksType == NoLinks, EntityRawIdType: CreatableRawIdType { + public init(meta: MetaType) { + self.init(attributes: NoAttributes(), relationships: NoRelationships(), meta: meta, links: .none) + } + } + + extension ResourceObject where Description.Attributes == NoAttributes, Description.Relationships == NoRelationships, MetaType == NoMetadata, LinksType == NoLinks, EntityRawIdType: CreatableRawIdType { + public init() { + self.init(attributes: NoAttributes(), relationships: NoRelationships(), meta: .none, links: .none) + } + } + + extension ResourceObject where MetaType == NoMetadata { + public init(id: ResourceObject.Id, attributes: Description.Attributes, relationships: Description.Relationships, links: LinksType) { + self.init(id: id, attributes: attributes, relationships: relationships, meta: .none, links: links) + } + } + + extension ResourceObject where MetaType == NoMetadata, EntityRawIdType: CreatableRawIdType { + public init(attributes: Description.Attributes, relationships: Description.Relationships, links: LinksType) { + self.init(attributes: attributes, relationships: relationships, meta: .none, links: links) + } + } + + extension ResourceObject where MetaType == NoMetadata, EntityRawIdType == Unidentified { + public init(attributes: Description.Attributes, relationships: Description.Relationships, links: LinksType) { + self.init(attributes: attributes, relationships: relationships, meta: .none, links: links) + } + } + + extension ResourceObject where LinksType == NoLinks { + public init(id: ResourceObject.Id, attributes: Description.Attributes, relationships: Description.Relationships, meta: MetaType) { + self.init(id: id, attributes: attributes, relationships: relationships, meta: meta, links: .none) + } + } + + extension ResourceObject where LinksType == NoLinks, EntityRawIdType: CreatableRawIdType { + public init(attributes: Description.Attributes, relationships: Description.Relationships, meta: MetaType) { + self.init(attributes: attributes, relationships: relationships, meta: meta, links: .none) + } + } + + extension ResourceObject where LinksType == NoLinks, EntityRawIdType == Unidentified { + public init(attributes: Description.Attributes, relationships: Description.Relationships, meta: MetaType) { + self.init(attributes: attributes, relationships: relationships, meta: meta, links: .none) + } + } + + extension ResourceObject where MetaType == NoMetadata, LinksType == NoLinks { + public init(id: ResourceObject.Id, attributes: Description.Attributes, relationships: Description.Relationships) { + self.init(id: id, attributes: attributes, relationships: relationships, meta: .none, links: .none) + } + } + + extension ResourceObject where MetaType == NoMetadata, LinksType == NoLinks, EntityRawIdType: CreatableRawIdType { + public init(attributes: Description.Attributes, relationships: Description.Relationships) { + self.init(attributes: attributes, relationships: relationships, meta: .none, links: .none) + } + } + + extension ResourceObject where MetaType == NoMetadata, LinksType == NoLinks, EntityRawIdType == Unidentified { + public init(attributes: Description.Attributes, relationships: Description.Relationships) { + self.init(attributes: attributes, relationships: relationships, meta: .none, links: .none) + } + } + */ // MARK: - Pointer for Relationships use public extension ResourceObject where EntityRawIdType: JSONAPI.RawIdType { - /// A `ResourceObject.Pointer` is a `ToOneRelationship` with no metadata or links. - /// This is just a convenient way to reference a `ResourceObject` so that - /// other ResourceObjects' Relationships can be built up from it. - typealias Pointer = ToOneRelationship + /// A `ResourceObject.Pointer` is a `ToOneRelationship` with no metadata or links. + /// This is just a convenient way to reference a `ResourceObject` so that + /// other ResourceObjects' Relationships can be built up from it. + typealias Pointer = ToOneRelationship - /// `ResourceObject.Pointers` is a `ToManyRelationship` with no metadata or links. - /// This is just a convenient way to reference a bunch of ResourceObjects so - /// that other ResourceObjects' Relationships can be built up from them. - typealias Pointers = ToManyRelationship + /// `ResourceObject.Pointers` is a `ToManyRelationship` with no metadata or links. + /// This is just a convenient way to reference a bunch of ResourceObjects so + /// that other ResourceObjects' Relationships can be built up from them. + typealias Pointers = ToManyRelationship - /// Get a pointer to this resource object that can be used as a - /// relationship to another resource object. - var pointer: Pointer { - return Pointer(resourceObject: self) - } + /// Get a pointer to this resource object that can be used as a + /// relationship to another resource object. + var pointer: Pointer { + return Pointer(resourceObject: self) + } /// Get a pointer (i.e. `ToOneRelationship`) to this resource /// object with the given metadata and links attached. - func pointer(withMeta meta: MType, links: LType) -> ToOneRelationship { - return ToOneRelationship(resourceObject: self, meta: meta, links: links) - } + func pointer(withMeta meta: MType, links: LType) -> ToOneRelationship { + return ToOneRelationship(resourceObject: self, meta: meta, links: links) + } } // MARK: - Identifying Unidentified Entities public extension ResourceObject where EntityRawIdType == Unidentified { - /// Create a new `ResourceObject` from this one with a newly created - /// unique Id of the given type. - func identified(byType: RawIdType.Type) -> ResourceObject { - return .init(attributes: attributes, relationships: relationships, meta: meta, links: links) - } - - /// Create a new `ResourceObject` from this one with the given Id. - func identified(by id: RawIdType) -> ResourceObject { - return .init(id: ResourceObject.Identifier(rawValue: id), attributes: attributes, relationships: relationships, meta: meta, links: links) - } + /// Create a new `ResourceObject` from this one with a newly created + /// unique Id of the given type. + func identified(byType: RawIdType.Type) -> ResourceObject { + return .init(attributes: attributes, relationships: relationships, meta: meta, links: links) + } + + /// Create a new `ResourceObject` from this one with the given Id. + func identified(by id: RawIdType) -> ResourceObject { + return .init(id: ResourceObject.Identifier(rawValue: id), attributes: attributes, relationships: relationships, meta: meta, links: links) + } } public extension ResourceObject where EntityRawIdType: CreatableRawIdType { - /// Create a copy of this `ResourceObject` with a new unique Id. - func withNewIdentifier() -> ResourceObject { - return ResourceObject(attributes: attributes, relationships: relationships, meta: meta, links: links) - } + /// Create a copy of this `ResourceObject` with a new unique Id. + func withNewIdentifier() -> ResourceObject { + return ResourceObject(attributes: attributes, relationships: relationships, meta: meta, links: links) + } } // MARK: - Attribute Access public extension ResourceObjectProxy { // MARK: Keypath Subscript Lookup - /// Access the attribute at the given keypath. This just - /// allows you to write `resourceObject[\.propertyName]` instead - /// of `resourceObject.attributes.propertyName.value`. - subscript(_ path: KeyPath) -> T.ValueType { - return attributes[keyPath: path].value - } - - /// Access the attribute at the given keypath. This just - /// allows you to write `resourceObject[\.propertyName]` instead - /// of `resourceObject.attributes.propertyName.value`. - subscript(_ path: KeyPath) -> T.ValueType? { - return attributes[keyPath: path]?.value - } - - /// Access the attribute at the given keypath. This just - /// allows you to write `resourceObject[\.propertyName]` instead - /// of `resourceObject.attributes.propertyName.value`. - subscript(_ path: KeyPath) -> U? where T.ValueType == U? { - // Implementation Note: Handles Transform that returns optional - // type. - return attributes[keyPath: path].flatMap { $0.value } - } + /// Access the attribute at the given keypath. This just + /// allows you to write `resourceObject[\.propertyName]` instead + /// of `resourceObject.attributes.propertyName.value`. + subscript(_ path: KeyPath) -> T.ValueType { + return attributes[keyPath: path].value + } + + /// Access the attribute at the given keypath. This just + /// allows you to write `resourceObject[\.propertyName]` instead + /// of `resourceObject.attributes.propertyName.value`. + subscript(_ path: KeyPath) -> T.ValueType? { + return attributes[keyPath: path]?.value + } + + /// Access the attribute at the given keypath. This just + /// allows you to write `resourceObject[\.propertyName]` instead + /// of `resourceObject.attributes.propertyName.value`. + subscript(_ path: KeyPath) -> U? where T.ValueType == U? { + // Implementation Note: Handles Transform that returns optional + // type. + return attributes[keyPath: path].flatMap { $0.value } + } // MARK: Dynaminc Member Keypath Lookup /// Access the attribute at the given keypath. This just @@ -499,27 +499,27 @@ public extension ResourceObjectProxy { } // MARK: Direct Keypath Subscript Lookup - /// Access the storage of the attribute at the given keypath. This just + /// Access the storage of the attribute at the given keypath. This just /// allows you to write `resourceObject[direct: \.propertyName]` instead - /// of `resourceObject.attributes.propertyName`. + /// of `resourceObject.attributes.propertyName`. /// Most of the subscripts dig into an `AttributeType`. This subscript /// returns the `AttributeType` (or another type, if you are accessing /// an attribute that is not stored in an `AttributeType`). - subscript(direct path: KeyPath) -> T { - // Implementation Note: Handles attributes that are not - // AttributeType. These should only exist as computed properties. - return attributes[keyPath: path] - } + subscript(direct path: KeyPath) -> T { + // Implementation Note: Handles attributes that are not + // AttributeType. These should only exist as computed properties. + return attributes[keyPath: path] + } } // MARK: - Meta-Attribute Access public extension ResourceObjectProxy { // MARK: Keypath Subscript Lookup - /// Access an attribute requiring a transformation on the RawValue _and_ - /// a secondary transformation on this entity (self). - subscript(_ path: KeyPath T>) -> T { - return attributes[keyPath: path](self) - } + /// Access an attribute requiring a transformation on the RawValue _and_ + /// a secondary transformation on this entity (self). + subscript(_ path: KeyPath T>) -> T { + return attributes[keyPath: path](self) + } // MARK: Dynamic Member Keypath Lookup /// Access an attribute requiring a transformation on the RawValue _and_ @@ -531,61 +531,61 @@ public extension ResourceObjectProxy { // MARK: - Relationship Access public extension ResourceObjectProxy { - /// Access to an Id of a `ToOneRelationship`. - /// This allows you to write `resourceObject ~> \.other` instead - /// of `resourceObject.relationships.other.id`. - static func ~>(entity: Self, path: KeyPath>) -> OtherEntity.Identifier { - return entity.relationships[keyPath: path].id - } - - /// Access to an Id of an optional `ToOneRelationship`. - /// This allows you to write `resourceObject ~> \.other` instead - /// of `resourceObject.relationships.other?.id`. - static func ~>(entity: Self, path: KeyPath?>) -> OtherEntity.Identifier { - // Implementation Note: This signature applies to `ToOneRelationship?` - // whereas the one below applies to `ToOneRelationship?` - return entity.relationships[keyPath: path]?.id - } - - /// Access to an Id of an optional `ToOneRelationship`. - /// This allows you to write `resourceObject ~> \.other` instead - /// of `resourceObject.relationships.other?.id`. - static func ~>(entity: Self, path: KeyPath?>) -> OtherEntity.Identifier? { - // Implementation Note: This signature applies to `ToOneRelationship?` - // whereas the one above applies to `ToOneRelationship?` - return entity.relationships[keyPath: path]?.id - } - - /// Access to all Ids of a `ToManyRelationship`. - /// This allows you to write `resourceObject ~> \.others` instead - /// of `resourceObject.relationships.others.ids`. - static func ~>(entity: Self, path: KeyPath>) -> [OtherEntity.Identifier] { - return entity.relationships[keyPath: path].ids - } - - /// Access to all Ids of an optional `ToManyRelationship`. - /// This allows you to write `resourceObject ~> \.others` instead - /// of `resourceObject.relationships.others?.ids`. - static func ~>(entity: Self, path: KeyPath?>) -> [OtherEntity.Identifier]? { - return entity.relationships[keyPath: path]?.ids - } + /// Access to an Id of a `ToOneRelationship`. + /// This allows you to write `resourceObject ~> \.other` instead + /// of `resourceObject.relationships.other.id`. + static func ~>(entity: Self, path: KeyPath>) -> OtherEntity.Identifier { + return entity.relationships[keyPath: path].id + } + + /// Access to an Id of an optional `ToOneRelationship`. + /// This allows you to write `resourceObject ~> \.other` instead + /// of `resourceObject.relationships.other?.id`. + static func ~>(entity: Self, path: KeyPath?>) -> OtherEntity.Identifier { + // Implementation Note: This signature applies to `ToOneRelationship?` + // whereas the one below applies to `ToOneRelationship?` + return entity.relationships[keyPath: path]?.id + } + + /// Access to an Id of an optional `ToOneRelationship`. + /// This allows you to write `resourceObject ~> \.other` instead + /// of `resourceObject.relationships.other?.id`. + static func ~>(entity: Self, path: KeyPath?>) -> OtherEntity.Identifier? { + // Implementation Note: This signature applies to `ToOneRelationship?` + // whereas the one above applies to `ToOneRelationship?` + return entity.relationships[keyPath: path]?.id + } + + /// Access to all Ids of a `ToManyRelationship`. + /// This allows you to write `resourceObject ~> \.others` instead + /// of `resourceObject.relationships.others.ids`. + static func ~>(entity: Self, path: KeyPath>) -> [OtherEntity.Identifier] { + return entity.relationships[keyPath: path].ids + } + + /// Access to all Ids of an optional `ToManyRelationship`. + /// This allows you to write `resourceObject ~> \.others` instead + /// of `resourceObject.relationships.others?.ids`. + static func ~>(entity: Self, path: KeyPath?>) -> [OtherEntity.Identifier]? { + return entity.relationships[keyPath: path]?.ids + } } // MARK: - Meta-Relationship Access public extension ResourceObjectProxy { - /// Access to an Id of a `ToOneRelationship`. - /// This allows you to write `resourceObject ~> \.other` instead - /// of `resourceObject.relationships.other.id`. - static func ~>(entity: Self, path: KeyPath Identifier>) -> Identifier { - return entity.relationships[keyPath: path](entity) - } - - /// Access to all Ids of a `ToManyRelationship`. - /// This allows you to write `resourceObject ~> \.others` instead - /// of `resourceObject.relationships.others.ids`. - static func ~>(entity: Self, path: KeyPath [Identifier]>) -> [Identifier] { - return entity.relationships[keyPath: path](entity) - } + /// Access to an Id of a `ToOneRelationship`. + /// This allows you to write `resourceObject ~> \.other` instead + /// of `resourceObject.relationships.other.id`. + static func ~>(entity: Self, path: KeyPath Identifier>) -> Identifier { + return entity.relationships[keyPath: path](entity) + } + + /// Access to all Ids of a `ToManyRelationship`. + /// This allows you to write `resourceObject ~> \.others` instead + /// of `resourceObject.relationships.others.ids`. + static func ~>(entity: Self, path: KeyPath [Identifier]>) -> [Identifier] { + return entity.relationships[keyPath: path](entity) + } } infix operator ~> diff --git a/Sources/JSONAPITesting/Attribute+Literal.swift b/Sources/JSONAPITesting/Attribute+Literal.swift index 706b6e4..b631bab 100644 --- a/Sources/JSONAPITesting/Attribute+Literal.swift +++ b/Sources/JSONAPITesting/Attribute+Literal.swift @@ -2,81 +2,81 @@ import JSONAPI extension Attribute: ExpressibleByUnicodeScalarLiteral where RawValue: ExpressibleByUnicodeScalarLiteral { - public typealias UnicodeScalarLiteralType = RawValue.UnicodeScalarLiteralType + public typealias UnicodeScalarLiteralType = RawValue.UnicodeScalarLiteralType - public init(unicodeScalarLiteral value: RawValue.UnicodeScalarLiteralType) { - self.init(value: RawValue(unicodeScalarLiteral: value)) - } + public init(unicodeScalarLiteral value: RawValue.UnicodeScalarLiteralType) { + self.init(value: RawValue(unicodeScalarLiteral: value)) + } } extension Attribute: ExpressibleByExtendedGraphemeClusterLiteral where RawValue: ExpressibleByExtendedGraphemeClusterLiteral { - public typealias ExtendedGraphemeClusterLiteralType = RawValue.ExtendedGraphemeClusterLiteralType + public typealias ExtendedGraphemeClusterLiteralType = RawValue.ExtendedGraphemeClusterLiteralType - public init(extendedGraphemeClusterLiteral value: RawValue.ExtendedGraphemeClusterLiteralType) { - self.init(value: RawValue(extendedGraphemeClusterLiteral: value)) - } + public init(extendedGraphemeClusterLiteral value: RawValue.ExtendedGraphemeClusterLiteralType) { + self.init(value: RawValue(extendedGraphemeClusterLiteral: value)) + } } extension Attribute: ExpressibleByStringLiteral where RawValue: ExpressibleByStringLiteral { - public typealias StringLiteralType = RawValue.StringLiteralType + public typealias StringLiteralType = RawValue.StringLiteralType - public init(stringLiteral value: RawValue.StringLiteralType) { - self.init(value: RawValue(stringLiteral: value)) - } + public init(stringLiteral value: RawValue.StringLiteralType) { + self.init(value: RawValue(stringLiteral: value)) + } } extension Attribute: ExpressibleByNilLiteral where RawValue: ExpressibleByNilLiteral { - public init(nilLiteral: ()) { - self.init(value: RawValue(nilLiteral: ())) - } + public init(nilLiteral: ()) { + self.init(value: RawValue(nilLiteral: ())) + } } extension Attribute: ExpressibleByFloatLiteral where RawValue: ExpressibleByFloatLiteral { - public typealias FloatLiteralType = RawValue.FloatLiteralType + public typealias FloatLiteralType = RawValue.FloatLiteralType - public init(floatLiteral value: RawValue.FloatLiteralType) { - self.init(value: RawValue(floatLiteral: value)) - } + public init(floatLiteral value: RawValue.FloatLiteralType) { + self.init(value: RawValue(floatLiteral: value)) + } } extension Optional: ExpressibleByFloatLiteral where Wrapped: ExpressibleByFloatLiteral { - public typealias FloatLiteralType = Wrapped.FloatLiteralType + public typealias FloatLiteralType = Wrapped.FloatLiteralType - public init(floatLiteral value: FloatLiteralType) { - self = .some(Wrapped(floatLiteral: value)) - } + public init(floatLiteral value: FloatLiteralType) { + self = .some(Wrapped(floatLiteral: value)) + } } extension Attribute: ExpressibleByBooleanLiteral where RawValue: ExpressibleByBooleanLiteral { - public typealias BooleanLiteralType = RawValue.BooleanLiteralType + public typealias BooleanLiteralType = RawValue.BooleanLiteralType - public init(booleanLiteral value: BooleanLiteralType) { - self.init(value: RawValue(booleanLiteral: value)) - } + public init(booleanLiteral value: BooleanLiteralType) { + self.init(value: RawValue(booleanLiteral: value)) + } } extension Optional: ExpressibleByBooleanLiteral where Wrapped: ExpressibleByBooleanLiteral { - public typealias BooleanLiteralType = Wrapped.BooleanLiteralType + public typealias BooleanLiteralType = Wrapped.BooleanLiteralType - public init(booleanLiteral value: BooleanLiteralType) { - self = .some(Wrapped(booleanLiteral: value)) - } + public init(booleanLiteral value: BooleanLiteralType) { + self = .some(Wrapped(booleanLiteral: value)) + } } extension Attribute: ExpressibleByIntegerLiteral where RawValue: ExpressibleByIntegerLiteral { - public typealias IntegerLiteralType = RawValue.IntegerLiteralType + public typealias IntegerLiteralType = RawValue.IntegerLiteralType - public init(integerLiteral value: IntegerLiteralType) { - self.init(value: RawValue(integerLiteral: value)) - } + public init(integerLiteral value: IntegerLiteralType) { + self.init(value: RawValue(integerLiteral: value)) + } } extension Optional: ExpressibleByIntegerLiteral where Wrapped: ExpressibleByIntegerLiteral { - public typealias IntegerLiteralType = Wrapped.IntegerLiteralType + public typealias IntegerLiteralType = Wrapped.IntegerLiteralType - public init(integerLiteral value: IntegerLiteralType) { - self = .some(Wrapped(integerLiteral: value)) - } + public init(integerLiteral value: IntegerLiteralType) { + self = .some(Wrapped(integerLiteral: value)) + } } // regretably, array and dictionary literals are not so easy because Dictionaries and Arrays @@ -84,55 +84,55 @@ extension Optional: ExpressibleByIntegerLiteral where Wrapped: ExpressibleByInte // we can still provide a case for the Array and Dictionary types, though. public protocol DictionaryType { - associatedtype Key: Hashable - associatedtype Value + associatedtype Key: Hashable + associatedtype Value - init(_ keysAndValues: S, uniquingKeysWith combine: (Dictionary.Value, Dictionary.Value) throws -> Dictionary.Value) rethrows where S : Sequence, S.Element == (Key, Value) + init(_ keysAndValues: S, uniquingKeysWith combine: (Dictionary.Value, Dictionary.Value) throws -> Dictionary.Value) rethrows where S : Sequence, S.Element == (Key, Value) } extension Dictionary: DictionaryType {} extension Attribute: ExpressibleByDictionaryLiteral where RawValue: DictionaryType { - public typealias Key = RawValue.Key + public typealias Key = RawValue.Key - public typealias Value = RawValue.Value + public typealias Value = RawValue.Value - public init(dictionaryLiteral elements: (RawValue.Key, RawValue.Value)...) { + public init(dictionaryLiteral elements: (RawValue.Key, RawValue.Value)...) { - // we arbitrarily keep the first value if two values are assigned to the same key - self.init(value: RawValue(elements, uniquingKeysWith: { val, _ in val })) - } + // we arbitrarily keep the first value if two values are assigned to the same key + self.init(value: RawValue(elements, uniquingKeysWith: { val, _ in val })) + } } extension Optional: DictionaryType where Wrapped: DictionaryType { - public typealias Key = Wrapped.Key + public typealias Key = Wrapped.Key - public typealias Value = Wrapped.Value + public typealias Value = Wrapped.Value - public init(_ keysAndValues: S, uniquingKeysWith combine: (Dictionary.Value, Dictionary.Value) throws -> Dictionary.Value) rethrows where S : Sequence, S.Element == (Key, Value) { - self = try .some(Wrapped(keysAndValues, uniquingKeysWith: combine)) - } + public init(_ keysAndValues: S, uniquingKeysWith combine: (Dictionary.Value, Dictionary.Value) throws -> Dictionary.Value) rethrows where S : Sequence, S.Element == (Key, Value) { + self = try .some(Wrapped(keysAndValues, uniquingKeysWith: combine)) + } } public protocol ArrayType { - associatedtype Element + associatedtype Element - init(_ s: S) where Element == S.Element, S : Sequence + init(_ s: S) where Element == S.Element, S : Sequence } extension Array: ArrayType {} extension ArraySlice: ArrayType {} extension Attribute: ExpressibleByArrayLiteral where RawValue: ArrayType { - public typealias ArrayLiteralElement = RawValue.Element + public typealias ArrayLiteralElement = RawValue.Element - public init(arrayLiteral elements: ArrayLiteralElement...) { - self.init(value: RawValue(elements)) - } + public init(arrayLiteral elements: ArrayLiteralElement...) { + self.init(value: RawValue(elements)) + } } extension Optional: ArrayType where Wrapped: ArrayType { - public typealias Element = Wrapped.Element + public typealias Element = Wrapped.Element - public init(_ s: S) where Element == S.Element, S : Sequence { - self = .some(Wrapped(s)) - } + public init(_ s: S) where Element == S.Element, S : Sequence { + self = .some(Wrapped(s)) + } } diff --git a/Sources/JSONAPITesting/Id+Literal.swift b/Sources/JSONAPITesting/Id+Literal.swift index b05b8b1..92121f9 100644 --- a/Sources/JSONAPITesting/Id+Literal.swift +++ b/Sources/JSONAPITesting/Id+Literal.swift @@ -8,33 +8,33 @@ import JSONAPI extension Id: ExpressibleByUnicodeScalarLiteral where RawType: ExpressibleByUnicodeScalarLiteral { - public typealias UnicodeScalarLiteralType = RawType.UnicodeScalarLiteralType - - public init(unicodeScalarLiteral value: RawType.UnicodeScalarLiteralType) { - self.init(rawValue: RawType(unicodeScalarLiteral: value)) - } + public typealias UnicodeScalarLiteralType = RawType.UnicodeScalarLiteralType + + public init(unicodeScalarLiteral value: RawType.UnicodeScalarLiteralType) { + self.init(rawValue: RawType(unicodeScalarLiteral: value)) + } } extension Id: ExpressibleByExtendedGraphemeClusterLiteral where RawType: ExpressibleByExtendedGraphemeClusterLiteral { - public typealias ExtendedGraphemeClusterLiteralType = RawType.ExtendedGraphemeClusterLiteralType - - public init(extendedGraphemeClusterLiteral value: RawType.ExtendedGraphemeClusterLiteralType) { - self.init(rawValue: RawType(extendedGraphemeClusterLiteral: value)) - } + public typealias ExtendedGraphemeClusterLiteralType = RawType.ExtendedGraphemeClusterLiteralType + + public init(extendedGraphemeClusterLiteral value: RawType.ExtendedGraphemeClusterLiteralType) { + self.init(rawValue: RawType(extendedGraphemeClusterLiteral: value)) + } } extension Id: ExpressibleByStringLiteral where RawType: ExpressibleByStringLiteral { - public typealias StringLiteralType = RawType.StringLiteralType - - public init(stringLiteral value: RawType.StringLiteralType) { - self.init(rawValue: RawType(stringLiteral: value)) - } + public typealias StringLiteralType = RawType.StringLiteralType + + public init(stringLiteral value: RawType.StringLiteralType) { + self.init(rawValue: RawType(stringLiteral: value)) + } } extension Id: ExpressibleByIntegerLiteral where RawType: ExpressibleByIntegerLiteral { - public typealias IntegerLiteralType = RawType.IntegerLiteralType - - public init(integerLiteral value: IntegerLiteralType) { - self.init(rawValue: RawType(integerLiteral: value)) - } + public typealias IntegerLiteralType = RawType.IntegerLiteralType + + public init(integerLiteral value: IntegerLiteralType) { + self.init(rawValue: RawType(integerLiteral: value)) + } } diff --git a/Sources/JSONAPITesting/Optional+Literal.swift b/Sources/JSONAPITesting/Optional+Literal.swift index e0d81b5..8c87fab 100644 --- a/Sources/JSONAPITesting/Optional+Literal.swift +++ b/Sources/JSONAPITesting/Optional+Literal.swift @@ -6,25 +6,25 @@ // extension Optional: ExpressibleByUnicodeScalarLiteral where Wrapped: ExpressibleByUnicodeScalarLiteral { - public typealias UnicodeScalarLiteralType = Wrapped.UnicodeScalarLiteralType + public typealias UnicodeScalarLiteralType = Wrapped.UnicodeScalarLiteralType - public init(unicodeScalarLiteral value: UnicodeScalarLiteralType) { - self = .some(Wrapped(unicodeScalarLiteral: value)) - } + public init(unicodeScalarLiteral value: UnicodeScalarLiteralType) { + self = .some(Wrapped(unicodeScalarLiteral: value)) + } } extension Optional: ExpressibleByExtendedGraphemeClusterLiteral where Wrapped: ExpressibleByExtendedGraphemeClusterLiteral { - public typealias ExtendedGraphemeClusterLiteralType = Wrapped.ExtendedGraphemeClusterLiteralType + public typealias ExtendedGraphemeClusterLiteralType = Wrapped.ExtendedGraphemeClusterLiteralType - public init(extendedGraphemeClusterLiteral value: ExtendedGraphemeClusterLiteralType) { - self = .some(Wrapped(extendedGraphemeClusterLiteral: value)) - } + public init(extendedGraphemeClusterLiteral value: ExtendedGraphemeClusterLiteralType) { + self = .some(Wrapped(extendedGraphemeClusterLiteral: value)) + } } extension Optional: ExpressibleByStringLiteral where Wrapped: ExpressibleByStringLiteral { - public typealias StringLiteralType = Wrapped.StringLiteralType + public typealias StringLiteralType = Wrapped.StringLiteralType - public init(stringLiteral value: StringLiteralType) { - self = .some(Wrapped(stringLiteral: value)) - } + public init(stringLiteral value: StringLiteralType) { + self = .some(Wrapped(stringLiteral: value)) + } } diff --git a/Sources/JSONAPITesting/Relationship+Literal.swift b/Sources/JSONAPITesting/Relationship+Literal.swift index 9af692c..551fd88 100644 --- a/Sources/JSONAPITesting/Relationship+Literal.swift +++ b/Sources/JSONAPITesting/Relationship+Literal.swift @@ -8,40 +8,40 @@ import JSONAPI extension ToOneRelationship: ExpressibleByNilLiteral where Identifiable.Identifier: ExpressibleByNilLiteral, MetaType == NoMetadata, LinksType == NoLinks { - public init(nilLiteral: ()) { + public init(nilLiteral: ()) { - self.init(id: Identifiable.Identifier(nilLiteral: ())) - } + self.init(id: Identifiable.Identifier(nilLiteral: ())) + } } extension ToOneRelationship: ExpressibleByUnicodeScalarLiteral where Identifiable.Identifier: ExpressibleByUnicodeScalarLiteral, MetaType == NoMetadata, LinksType == NoLinks { - public typealias UnicodeScalarLiteralType = Identifiable.Identifier.UnicodeScalarLiteralType + public typealias UnicodeScalarLiteralType = Identifiable.Identifier.UnicodeScalarLiteralType - public init(unicodeScalarLiteral value: UnicodeScalarLiteralType) { - self.init(id: Identifiable.Identifier(unicodeScalarLiteral: value)) - } + public init(unicodeScalarLiteral value: UnicodeScalarLiteralType) { + self.init(id: Identifiable.Identifier(unicodeScalarLiteral: value)) + } } extension ToOneRelationship: ExpressibleByExtendedGraphemeClusterLiteral where Identifiable.Identifier: ExpressibleByExtendedGraphemeClusterLiteral, MetaType == NoMetadata, LinksType == NoLinks { - public typealias ExtendedGraphemeClusterLiteralType = Identifiable.Identifier.ExtendedGraphemeClusterLiteralType + public typealias ExtendedGraphemeClusterLiteralType = Identifiable.Identifier.ExtendedGraphemeClusterLiteralType - public init(extendedGraphemeClusterLiteral value: ExtendedGraphemeClusterLiteralType) { - self.init(id: Identifiable.Identifier(extendedGraphemeClusterLiteral: value)) - } + public init(extendedGraphemeClusterLiteral value: ExtendedGraphemeClusterLiteralType) { + self.init(id: Identifiable.Identifier(extendedGraphemeClusterLiteral: value)) + } } extension ToOneRelationship: ExpressibleByStringLiteral where Identifiable.Identifier: ExpressibleByStringLiteral, MetaType == NoMetadata, LinksType == NoLinks { - public typealias StringLiteralType = Identifiable.Identifier.StringLiteralType + public typealias StringLiteralType = Identifiable.Identifier.StringLiteralType - public init(stringLiteral value: StringLiteralType) { - self.init(id: Identifiable.Identifier(stringLiteral: value)) - } + public init(stringLiteral value: StringLiteralType) { + self.init(id: Identifiable.Identifier(stringLiteral: value)) + } } extension ToManyRelationship: ExpressibleByArrayLiteral where MetaType == NoMetadata, LinksType == NoLinks { - public typealias ArrayLiteralElement = Relatable.Identifier + public typealias ArrayLiteralElement = Relatable.Identifier - public init(arrayLiteral elements: ArrayLiteralElement...) { - self.init(ids: elements) - } + public init(arrayLiteral elements: ArrayLiteralElement...) { + self.init(ids: elements) + } } diff --git a/Sources/JSONAPITesting/ResourceObjectCheck.swift b/Sources/JSONAPITesting/ResourceObjectCheck.swift index 0749ec8..13c1724 100644 --- a/Sources/JSONAPITesting/ResourceObjectCheck.swift +++ b/Sources/JSONAPITesting/ResourceObjectCheck.swift @@ -8,29 +8,29 @@ import JSONAPI public enum ResourceObjectCheckError: Swift.Error { - /// The attributes should live in a struct, not - /// another type class. - case attributesNotStruct + /// The attributes should live in a struct, not + /// another type class. + case attributesNotStruct - /// The relationships should live in a struct, not - /// another type class. - case relationshipsNotStruct + /// The relationships should live in a struct, not + /// another type class. + case relationshipsNotStruct - /// All stored properties on an Attributes struct should - /// be one of the supplied Attribute types. - case nonAttribute(named: String) + /// All stored properties on an Attributes struct should + /// be one of the supplied Attribute types. + case nonAttribute(named: String) - /// All stored properties on a Relationships struct should - /// be one of the supplied Relationship types. - case nonRelationship(named: String) + /// All stored properties on a Relationships struct should + /// be one of the supplied Relationship types. + case nonRelationship(named: String) - /// It is explicitly stated by the JSON spec - /// a "none" value for arrays is an empty array, not `nil`. - case nullArray(named: String) + /// It is explicitly stated by the JSON spec + /// a "none" value for arrays is an empty array, not `nil`. + case nullArray(named: String) } public struct ResourceObjectCheckErrors: Swift.Error { - let problems: [ResourceObjectCheckError] + let problems: [ResourceObjectCheckError] } private protocol OptionalAttributeType {} @@ -55,40 +55,40 @@ extension TransformedAttribute: _AttributeType {} extension Attribute: _AttributeType {} public extension ResourceObject { - static func check(_ entity: ResourceObject) throws { - var problems = [ResourceObjectCheckError]() - - let attributesMirror = Mirror(reflecting: entity.attributes) - - if attributesMirror.displayStyle != .`struct` { - problems.append(.attributesNotStruct) - } - - for attribute in attributesMirror.children { - if attribute.value as? _AttributeType == nil, - attribute.value as? OptionalAttributeType == nil { - problems.append(.nonAttribute(named: attribute.label ?? "unnamed")) - } - if attribute.value as? AttributeTypeWithOptionalArray != nil { - problems.append(.nullArray(named: attribute.label ?? "unnamed")) - } - } - - let relationshipsMirror = Mirror(reflecting: entity.relationships) - - if relationshipsMirror.displayStyle != .`struct` { - problems.append(.relationshipsNotStruct) - } - - for relationship in relationshipsMirror.children { - if relationship.value as? _RelationshipType == nil, - relationship.value as? OptionalRelationshipType == nil { - problems.append(.nonRelationship(named: relationship.label ?? "unnamed")) - } - } - - guard problems.count == 0 else { - throw ResourceObjectCheckErrors(problems: problems) - } - } + static func check(_ entity: ResourceObject) throws { + var problems = [ResourceObjectCheckError]() + + let attributesMirror = Mirror(reflecting: entity.attributes) + + if attributesMirror.displayStyle != .`struct` { + problems.append(.attributesNotStruct) + } + + for attribute in attributesMirror.children { + if attribute.value as? _AttributeType == nil, + attribute.value as? OptionalAttributeType == nil { + problems.append(.nonAttribute(named: attribute.label ?? "unnamed")) + } + if attribute.value as? AttributeTypeWithOptionalArray != nil { + problems.append(.nullArray(named: attribute.label ?? "unnamed")) + } + } + + let relationshipsMirror = Mirror(reflecting: entity.relationships) + + if relationshipsMirror.displayStyle != .`struct` { + problems.append(.relationshipsNotStruct) + } + + for relationship in relationshipsMirror.children { + if relationship.value as? _RelationshipType == nil, + relationship.value as? OptionalRelationshipType == nil { + problems.append(.nonRelationship(named: relationship.label ?? "unnamed")) + } + } + + guard problems.count == 0 else { + throw ResourceObjectCheckErrors(problems: problems) + } + } } From adcc6bfb108d661f6af658c25eb36a4f1bf33266 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Tue, 5 Nov 2019 22:43:40 -0800 Subject: [PATCH 096/235] fix error after merge --- Sources/JSONAPI/Document/Document.swift | 2 -- 1 file changed, 2 deletions(-) diff --git a/Sources/JSONAPI/Document/Document.swift b/Sources/JSONAPI/Document/Document.swift index 08c5d9b..bfe3268 100644 --- a/Sources/JSONAPI/Document/Document.swift +++ b/Sources/JSONAPI/Document/Document.swift @@ -441,7 +441,6 @@ extension Document { public typealias BodyData = Document.BodyData public var body: Document.Body { return document.body } - public var apiDescription: APIDescription { return document.apiDescription } private let document: Document @@ -489,7 +488,6 @@ extension Document { public typealias BodyData = Document.BodyData public var body: Document.Body { return document.body } - public var apiDescription: APIDescription { return document.apiDescription } private let document: Document From ae7e0f528a9080f85abb47ceb95fb519276d7da5 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Wed, 6 Nov 2019 00:19:27 -0800 Subject: [PATCH 097/235] abstract away document comparison --- .../Comparisons/DocumentCompare.swift | 100 +++++++++++------- .../Comparisons/DocumentDataCompare.swift | 2 +- 2 files changed, 60 insertions(+), 42 deletions(-) diff --git a/Sources/JSONAPITesting/Comparisons/DocumentCompare.swift b/Sources/JSONAPITesting/Comparisons/DocumentCompare.swift index 15c417f..587d6a0 100644 --- a/Sources/JSONAPITesting/Comparisons/DocumentCompare.swift +++ b/Sources/JSONAPITesting/Comparisons/DocumentCompare.swift @@ -79,7 +79,7 @@ public enum BodyComparison: Equatable, CustomStringConvertible { public var rawValue: String { description } } -extension Document { +extension EncodableJSONAPIDocument where Body: Equatable { public func compare(to other: Self) -> DocumentComparison where PrimaryResourceBody == SingleResourceBody, T: ResourceObjectType { return DocumentComparison( apiDescription: Comparison( @@ -111,70 +111,88 @@ extension Document { } } -extension Document.Body { +extension DocumentBody where Self: Equatable { public func compare(to other: Self) -> BodyComparison where T: ResourceObjectType, PrimaryResourceBody == SingleResourceBody { + + // rule out case where they are the same guard self != other else { return .same } - switch (self, other) { - case (.errors(let errors1), .errors(let errors2)): - return .differentErrors(BodyComparison.compare(errors: errors1.0, - errors1.meta, - errors1.links, - with: errors2.0, - errors2.meta, - errors2.links)) - case (.errors, .data): - return .dataErrorMismatch(errorOnLeft: true) - case (.data, .errors): - return .dataErrorMismatch(errorOnLeft: false) - case (.data(let data1), .data(let data2)): + // rule out case where they are both error bodies + if let errors1 = errors, let errors2 = other.errors { + return .differentErrors( + BodyComparison.compare( + errors: errors1, meta, links, + with: errors2, meta, links + ) + ) + } + + // rule out the case where they are both data + if let data1 = data, let data2 = other.data { return .differentData(data1.compare(to: data2)) } + + // we are left with the case where one is data and the + // other is an error if self.isError, then "the error + // is on the left" + return .dataErrorMismatch(errorOnLeft: isError) } public func compare(to other: Self) -> BodyComparison where T: ResourceObjectType, PrimaryResourceBody == SingleResourceBody { + + // rule out case where they are the same guard self != other else { return .same } - switch (self, other) { - case (.errors(let errors1), .errors(let errors2)): - return .differentErrors(BodyComparison.compare(errors: errors1.0, - errors1.meta, - errors1.links, - with: errors2.0, - errors2.meta, - errors2.links)) - case (.errors, .data): - return .dataErrorMismatch(errorOnLeft: true) - case (.data, .errors): - return .dataErrorMismatch(errorOnLeft: false) - case (.data(let data1), .data(let data2)): + // rule out case where they are both error bodies + if let errors1 = errors, let errors2 = other.errors { + return .differentErrors( + BodyComparison.compare( + errors: errors1, meta, links, + with: errors2, meta, links + ) + ) + } + + // rule out the case where they are both data + if let data1 = data, let data2 = other.data { return .differentData(data1.compare(to: data2)) } + + // we are left with the case where one is data and the + // other is an error if self.isError, then "the error + // is on the left" + return .dataErrorMismatch(errorOnLeft: isError) } public func compare(to other: Self) -> BodyComparison where T: ResourceObjectType, PrimaryResourceBody == ManyResourceBody { + + // rule out case where they are the same guard self != other else { return .same } - switch (self, other) { - case (.errors(let errors1), .errors(let errors2)): - return .differentErrors(BodyComparison.compare(errors: errors1.0, - errors1.meta, - errors1.links, - with: errors2.0, - errors2.meta, - errors2.links)) - case (.errors, .data): - return .dataErrorMismatch(errorOnLeft: true) - case (.data, .errors): - return .dataErrorMismatch(errorOnLeft: false) - case (.data(let data1), .data(let data2)): + // rule out case where they are both error bodies + if let errors1 = errors, let errors2 = other.errors { + return .differentErrors( + BodyComparison.compare( + errors: errors1, meta, links, + with: errors2, meta, links + ) + ) + } + + // rule out the case where they are both data + if let data1 = data, let data2 = other.data { return .differentData(data1.compare(to: data2)) } + + // we are left with the case where one is data and the + // other is an error if self.isError, then "the error + // is on the left" + return .dataErrorMismatch(errorOnLeft: isError) } } diff --git a/Sources/JSONAPITesting/Comparisons/DocumentDataCompare.swift b/Sources/JSONAPITesting/Comparisons/DocumentDataCompare.swift index 392bab0..d268572 100644 --- a/Sources/JSONAPITesting/Comparisons/DocumentDataCompare.swift +++ b/Sources/JSONAPITesting/Comparisons/DocumentDataCompare.swift @@ -33,7 +33,7 @@ public struct DocumentDataComparison: Equatable, PropertyComparable { } } -extension Document.Body.Data { +extension DocumentBodyData { public func compare(to other: Self) -> DocumentDataComparison where T: ResourceObjectType, PrimaryResourceBody == SingleResourceBody { return .init( primary: primary.compare(to: other.primary), From f37f44cfda87e23a2607feeb2c6b5af5001ed2c8 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Wed, 6 Nov 2019 21:42:13 -0800 Subject: [PATCH 098/235] add comparable protocol --- Sources/JSONAPITesting/Comparisons/Comparison.swift | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/Sources/JSONAPITesting/Comparisons/Comparison.swift b/Sources/JSONAPITesting/Comparisons/Comparison.swift index a6b97ee..e55af29 100644 --- a/Sources/JSONAPITesting/Comparisons/Comparison.swift +++ b/Sources/JSONAPITesting/Comparisons/Comparison.swift @@ -5,7 +5,13 @@ // Created by Mathew Polzin on 11/3/19. // -public enum Comparison: Equatable, CustomStringConvertible { +public protocol Comparable: CustomStringConvertible { + var rawValue: String { get } + + var isSame: Bool { get } +} + +public enum Comparison: Comparable, Equatable { case same case different(String, String) case prebuilt(String) @@ -50,7 +56,7 @@ public enum Comparison: Equatable, CustomStringConvertible { public typealias NamedDifferences = [String: String] -public protocol PropertyComparable: CustomStringConvertible { +public protocol PropertyComparable: Comparable { var differences: NamedDifferences { get } } From 832161628b64bb68e7fc842c20371da656185a7f Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Wed, 6 Nov 2019 23:05:21 -0800 Subject: [PATCH 099/235] go from 3 specializations of all document related compare functions down to 2. --- Sources/JSONAPI/Document/ResourceBody.swift | 32 ++++--- .../Comparisons/DocumentCompare.swift | 54 ++--------- .../Comparisons/DocumentDataCompare.swift | 95 +++++++++++-------- .../Comparisons/DocumentCompareTests.swift | 52 ++++++++++ 4 files changed, 136 insertions(+), 97 deletions(-) diff --git a/Sources/JSONAPI/Document/ResourceBody.swift b/Sources/JSONAPI/Document/ResourceBody.swift index c8408de..25090c8 100644 --- a/Sources/JSONAPI/Document/ResourceBody.swift +++ b/Sources/JSONAPI/Document/ResourceBody.swift @@ -29,7 +29,9 @@ extension Optional: OptionalCodablePrimaryResource where Wrapped: CodablePrimary /// An `EncodableResourceBody` is a `ResourceBody` that only supports being /// encoded. It is actually weaker than `ResourceBody`, which supports both encoding /// and decoding. -public protocol EncodableResourceBody: Equatable, Encodable {} +public protocol EncodableResourceBody: Equatable, Encodable { + associatedtype PrimaryResource +} /// 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) @@ -49,19 +51,19 @@ public func +(_ left: R, right: R) -> R { /// A type allowing for a document body containing 1 primary resource. /// If the `Entity` specialization is an `Optional` type, the body can contain /// 0 or 1 primary resources. -public struct SingleResourceBody: EncodableResourceBody { - public let value: Entity +public struct SingleResourceBody: EncodableResourceBody { + public let value: PrimaryResource - public init(resourceObject: Entity) { + public init(resourceObject: PrimaryResource) { self.value = resourceObject } } /// A type allowing for a document body containing 0 or more primary resources. -public struct ManyResourceBody: EncodableResourceBody, ResourceBodyAppendable { - public let values: [Entity] +public struct ManyResourceBody: EncodableResourceBody, ResourceBodyAppendable { + public let values: [PrimaryResource] - public init(resourceObjects: [Entity]) { + public init(resourceObjects: [PrimaryResource]) { values = resourceObjects } @@ -73,6 +75,8 @@ public struct ManyResourceBody: Encoda /// Use NoResourceBody to indicate you expect a JSON API document to not /// contain a "data" top-level key. public struct NoResourceBody: CodableResourceBody { + public typealias PrimaryResource = Void + public static var none: NoResourceBody { return NoResourceBody() } } @@ -82,7 +86,7 @@ extension SingleResourceBody { var container = encoder.singleValueContainer() let anyNil: Any? = nil - let nilValue = anyNil as? Entity + let nilValue = anyNil as? PrimaryResource guard value != nilValue else { try container.encodeNil() return @@ -92,18 +96,18 @@ extension SingleResourceBody { } } -extension SingleResourceBody: Decodable, CodableResourceBody where Entity: OptionalCodablePrimaryResource { +extension SingleResourceBody: Decodable, CodableResourceBody where PrimaryResource: OptionalCodablePrimaryResource { public init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer() let anyNil: Any? = nil if container.decodeNil(), - let val = anyNil as? Entity { + let val = anyNil as? PrimaryResource { value = val return } - value = try container.decode(Entity.self) + value = try container.decode(PrimaryResource.self) } } @@ -117,12 +121,12 @@ extension ManyResourceBody { } } -extension ManyResourceBody: Decodable, CodableResourceBody where Entity: CodablePrimaryResource { +extension ManyResourceBody: Decodable, CodableResourceBody where PrimaryResource: CodablePrimaryResource { public init(from decoder: Decoder) throws { var container = try decoder.unkeyedContainer() - var valueAggregator = [Entity]() + var valueAggregator = [PrimaryResource]() while !container.isAtEnd { - valueAggregator.append(try container.decode(Entity.self)) + valueAggregator.append(try container.decode(PrimaryResource.self)) } values = valueAggregator } diff --git a/Sources/JSONAPITesting/Comparisons/DocumentCompare.swift b/Sources/JSONAPITesting/Comparisons/DocumentCompare.swift index 587d6a0..3f3ee23 100644 --- a/Sources/JSONAPITesting/Comparisons/DocumentCompare.swift +++ b/Sources/JSONAPITesting/Comparisons/DocumentCompare.swift @@ -79,18 +79,8 @@ public enum BodyComparison: Equatable, CustomStringConvertible { public var rawValue: String { description } } -extension EncodableJSONAPIDocument where Body: Equatable { - public func compare(to other: Self) -> DocumentComparison where PrimaryResourceBody == SingleResourceBody, T: ResourceObjectType { - return DocumentComparison( - apiDescription: Comparison( - String(describing: apiDescription), - String(describing: other.apiDescription) - ), - body: body.compare(to: other.body) - ) - } - - public func compare(to other: Self) -> DocumentComparison where PrimaryResourceBody == SingleResourceBody, T: ResourceObjectType { +extension EncodableJSONAPIDocument where Body: Equatable, PrimaryResourceBody: _ResourceBody { + public func compare(to other: Self) -> DocumentComparison { return DocumentComparison( apiDescription: Comparison( String(describing: apiDescription), @@ -99,8 +89,10 @@ extension EncodableJSONAPIDocument where Body: Equatable { body: body.compare(to: other.body) ) } +} - public func compare(to other: Self) -> DocumentComparison where PrimaryResourceBody == ManyResourceBody, T: ResourceObjectType { +extension EncodableJSONAPIDocument where Body: Equatable, PrimaryResourceBody: _OptionalResourceBody { + public func compare(to other: Self) -> DocumentComparison { return DocumentComparison( apiDescription: Comparison( String(describing: apiDescription), @@ -111,36 +103,8 @@ extension EncodableJSONAPIDocument where Body: Equatable { } } -extension DocumentBody where Self: Equatable { - public func compare(to other: Self) -> BodyComparison where T: ResourceObjectType, PrimaryResourceBody == SingleResourceBody { - - // rule out case where they are the same - guard self != other else { - return .same - } - - // rule out case where they are both error bodies - if let errors1 = errors, let errors2 = other.errors { - return .differentErrors( - BodyComparison.compare( - errors: errors1, meta, links, - with: errors2, meta, links - ) - ) - } - - // rule out the case where they are both data - if let data1 = data, let data2 = other.data { - return .differentData(data1.compare(to: data2)) - } - - // we are left with the case where one is data and the - // other is an error if self.isError, then "the error - // is on the left" - return .dataErrorMismatch(errorOnLeft: isError) - } - - public func compare(to other: Self) -> BodyComparison where T: ResourceObjectType, PrimaryResourceBody == SingleResourceBody { +extension DocumentBody where Self: Equatable, PrimaryResourceBody: _ResourceBody { + public func compare(to other: Self) -> BodyComparison { // rule out case where they are the same guard self != other else { @@ -167,8 +131,10 @@ extension DocumentBody where Self: Equatable { // is on the left" return .dataErrorMismatch(errorOnLeft: isError) } +} - public func compare(to other: Self) -> BodyComparison where T: ResourceObjectType, PrimaryResourceBody == ManyResourceBody { +extension DocumentBody where Self: Equatable, PrimaryResourceBody: _OptionalResourceBody { + public func compare(to other: Self) -> BodyComparison { // rule out case where they are the same guard self != other else { diff --git a/Sources/JSONAPITesting/Comparisons/DocumentDataCompare.swift b/Sources/JSONAPITesting/Comparisons/DocumentDataCompare.swift index d268572..87b6819 100644 --- a/Sources/JSONAPITesting/Comparisons/DocumentDataCompare.swift +++ b/Sources/JSONAPITesting/Comparisons/DocumentDataCompare.swift @@ -33,17 +33,8 @@ public struct DocumentDataComparison: Equatable, PropertyComparable { } } -extension DocumentBodyData { - public func compare(to other: Self) -> DocumentDataComparison where T: ResourceObjectType, PrimaryResourceBody == SingleResourceBody { - return .init( - primary: primary.compare(to: other.primary), - includes: includes.compare(to: other.includes), - meta: Comparison(meta, other.meta), - links: Comparison(links, other.links) - ) - } - - public func compare(to other: Self) -> DocumentDataComparison where T: ResourceObjectType, PrimaryResourceBody == SingleResourceBody { +extension DocumentBodyData where PrimaryResourceBody: _ResourceBody { + public func compare(to other: Self) -> DocumentDataComparison { return .init( primary: primary.compare(to: other.primary), includes: includes.compare(to: other.includes), @@ -51,8 +42,10 @@ extension DocumentBodyData { links: Comparison(links, other.links) ) } +} - public func compare(to other: Self) -> DocumentDataComparison where T: ResourceObjectType, PrimaryResourceBody == ManyResourceBody { +extension DocumentBodyData where PrimaryResourceBody: _OptionalResourceBody { + public func compare(to other: Self) -> DocumentDataComparison { return .init( primary: primary.compare(to: other.primary), includes: includes.compare(to: other.includes), @@ -109,42 +102,24 @@ public struct ManyResourceObjectComparison: Equatable, PropertyComparable { } } -extension SingleResourceBody where Entity: ResourceObjectType { +extension _OptionalResourceBody where WrappedPrimaryResourceType: ResourceObjectType { public func compare(to other: Self) -> PrimaryResourceBodyComparison { - return .single(.init(value, other.value)) - } -} - -public protocol _OptionalResourceObjectType { - associatedtype Wrapped: ResourceObjectType + guard let one = optionalResourceObject, + let two = other.optionalResourceObject else { - var maybeValue: Wrapped? { get } -} - -extension Optional: _OptionalResourceObjectType where Wrapped: ResourceObjectType { - public var maybeValue: Wrapped? { - switch self { - case .none: - return nil - case .some(let value): - return value - } - } -} + func nilOrName(_ resObj: T?) -> String { + resObj.map { String(describing: type(of: $0)) } ?? "nil" + } -extension SingleResourceBody where Entity: _OptionalResourceObjectType { - public func compare(to other: Self) -> PrimaryResourceBodyComparison { - guard let one = value.maybeValue, - let two = other.value.maybeValue else { - return .other(Comparison(value, other.value)) + return .other(Comparison(nilOrName(optionalResourceObject), nilOrName(other.optionalResourceObject))) } return .single(.init(one, two)) } } -extension ManyResourceBody where Entity: ResourceObjectType { +extension _ResourceBody where PrimaryResourceType: ResourceObjectType { public func compare(to other: Self) -> PrimaryResourceBodyComparison { - return .many(.init(values.compare(to: other.values, using: { r1, r2 in + return .many(.init(resourceObjects.compare(to: other.resourceObjects, using: { r1, r2 in let r1AsResource = r1 as? AbstractResourceObjectType let maybeComparison = r1AsResource @@ -165,3 +140,45 @@ extension ManyResourceBody where Entity: ResourceObjectType { }))) } } + +public protocol _ResourceBody { + associatedtype PrimaryResourceType: ResourceObjectType + var resourceObjects: [PrimaryResourceType] { get } +} + +public protocol _OptionalResourceBody { + associatedtype WrappedPrimaryResourceType: ResourceObjectType + var optionalResourceObject: WrappedPrimaryResourceType? { get } +} + +public protocol _OptionalResourceObjectType { + associatedtype Wrapped: ResourceObjectType + + var maybeValue: Wrapped? { get } +} + +extension Optional: _OptionalResourceObjectType where Wrapped: ResourceObjectType { + public var maybeValue: Wrapped? { + switch self { + case .none: + return nil + case .some(let value): + return value + } + } +} + +extension ManyResourceBody: _ResourceBody where PrimaryResource: ResourceObjectType { + public var resourceObjects: [PrimaryResource] { values } +} + +extension SingleResourceBody: _ResourceBody where PrimaryResource: ResourceObjectType { + public typealias PrimaryResourceType = PrimaryResource + public var resourceObjects: [PrimaryResource] { [value] } +} + +extension SingleResourceBody: _OptionalResourceBody where PrimaryResource: _OptionalResourceObjectType { + public typealias WrappedPrimaryResourceType = PrimaryResource.Wrapped + + public var optionalResourceObject: WrappedPrimaryResourceType? { value.maybeValue } +} diff --git a/Tests/JSONAPITestingTests/Comparisons/DocumentCompareTests.swift b/Tests/JSONAPITestingTests/Comparisons/DocumentCompareTests.swift index b2e14af..7c00e38 100644 --- a/Tests/JSONAPITestingTests/Comparisons/DocumentCompareTests.swift +++ b/Tests/JSONAPITestingTests/Comparisons/DocumentCompareTests.swift @@ -15,6 +15,12 @@ final class DocumentCompareTests: XCTestCase { XCTAssertTrue(d2.compare(to: d2).differences.isEmpty) XCTAssertTrue(d3.compare(to: d3).differences.isEmpty) XCTAssertTrue(d4.compare(to: d4).differences.isEmpty) + XCTAssertTrue(d5.compare(to: d5).differences.isEmpty) + XCTAssertTrue(d6.compare(to: d6).differences.isEmpty) + XCTAssertTrue(d7.compare(to: d7).differences.isEmpty) + XCTAssertTrue(d8.compare(to: d8).differences.isEmpty) + XCTAssertTrue(d9.compare(to: d9).differences.isEmpty) + XCTAssertTrue(d10.compare(to: d10).differences.isEmpty) } func test_errorAndData() { @@ -41,6 +47,18 @@ final class DocumentCompareTests: XCTestCase { XCTAssertEqual(d3.compare(to: d6).differences, [ "Body": ##"(Includes: (include 2: missing)), (Primary Resource: (resource 2: 'age' attribute: 10 ≠ 12, 'bestFriend' relationship: Optional(Id(2)) ≠ nil, 'favoriteColor' attribute: nil ≠ Optional("blue"), 'name' attribute: name ≠ Fig, id: 1 ≠ 5), (resource 3: missing))"## ]) + + XCTAssertEqual(d7.compare(to: d8).differences, [ + "Body": ##"(Primary Resource: nil ≠ ResourceObject)"## + ]) + + XCTAssertEqual(d8.compare(to: d9).differences, [ + "Body": ##"(Primary Resource: ('age' attribute: 10 ≠ 12), ('bestFriend' relationship: Optional(Id(2)) ≠ nil), ('favoriteColor' attribute: nil ≠ Optional("blue")), ('name' attribute: name ≠ Fig), (id: 1 ≠ 5))"## + ]) + + XCTAssertEqual(d1.compare(to: d10).differences, [ + "Body": ##"(Primary Resource: (resource 1: 'age' attribute: 10 ≠ 12, 'bestFriend' relationship: Optional(Id(2)) ≠ nil, 'favoriteColor' attribute: nil ≠ Optional("blue"), 'name' attribute: name ≠ Fig, id: 1 ≠ 5))"## + ]) } } @@ -80,6 +98,8 @@ fileprivate typealias TestType2 = ResourceObject, NoMetadata, NoLinks, Include2, NoAPIDescription, BasicJSONAPIError> +fileprivate typealias OptionalSingleDocument = JSONAPI.Document, NoMetadata, NoLinks, Include2, NoAPIDescription, BasicJSONAPIError> + fileprivate typealias ManyDocument = JSONAPI.Document, NoMetadata, NoLinks, Include2, NoAPIDescription, BasicJSONAPIError> fileprivate let r1 = TestType( @@ -168,3 +188,35 @@ fileprivate let d6 = ManyDocument( meta: .none, links: .none ) + +fileprivate let d7 = OptionalSingleDocument( + apiDescription: .none, + body: .init(resourceObject: nil), + includes: .none, + meta: .none, + links: .none +) + +fileprivate let d8 = OptionalSingleDocument( + apiDescription: .none, + body: .init(resourceObject: r1), + includes: .none, + meta: .none, + links: .none +) + +fileprivate let d9 = OptionalSingleDocument( + apiDescription: .none, + body: .init(resourceObject: r2), + includes: .none, + meta: .none, + links: .none +) + +fileprivate let d10 = SingleDocument( + apiDescription: .none, + body: .init(resourceObject: r2), + includes: .none, + meta: .none, + links: .none +) From 024fe2d4528e93b091e5a3900a2ca258bf40b911 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Wed, 6 Nov 2019 23:21:22 -0800 Subject: [PATCH 100/235] Down to one sequence of compare functions for all documents --- .../Comparisons/DocumentCompare.swift | 42 ------------- .../Comparisons/DocumentDataCompare.swift | 59 +++++-------------- .../Comparisons/DocumentCompareTests.swift | 2 +- 3 files changed, 17 insertions(+), 86 deletions(-) diff --git a/Sources/JSONAPITesting/Comparisons/DocumentCompare.swift b/Sources/JSONAPITesting/Comparisons/DocumentCompare.swift index 3f3ee23..2ffc37e 100644 --- a/Sources/JSONAPITesting/Comparisons/DocumentCompare.swift +++ b/Sources/JSONAPITesting/Comparisons/DocumentCompare.swift @@ -79,18 +79,6 @@ public enum BodyComparison: Equatable, CustomStringConvertible { public var rawValue: String { description } } -extension EncodableJSONAPIDocument where Body: Equatable, PrimaryResourceBody: _ResourceBody { - public func compare(to other: Self) -> DocumentComparison { - return DocumentComparison( - apiDescription: Comparison( - String(describing: apiDescription), - String(describing: other.apiDescription) - ), - body: body.compare(to: other.body) - ) - } -} - extension EncodableJSONAPIDocument where Body: Equatable, PrimaryResourceBody: _OptionalResourceBody { public func compare(to other: Self) -> DocumentComparison { return DocumentComparison( @@ -103,36 +91,6 @@ extension EncodableJSONAPIDocument where Body: Equatable, PrimaryResourceBody: _ } } -extension DocumentBody where Self: Equatable, PrimaryResourceBody: _ResourceBody { - public func compare(to other: Self) -> BodyComparison { - - // rule out case where they are the same - guard self != other else { - return .same - } - - // rule out case where they are both error bodies - if let errors1 = errors, let errors2 = other.errors { - return .differentErrors( - BodyComparison.compare( - errors: errors1, meta, links, - with: errors2, meta, links - ) - ) - } - - // rule out the case where they are both data - if let data1 = data, let data2 = other.data { - return .differentData(data1.compare(to: data2)) - } - - // we are left with the case where one is data and the - // other is an error if self.isError, then "the error - // is on the left" - return .dataErrorMismatch(errorOnLeft: isError) - } -} - extension DocumentBody where Self: Equatable, PrimaryResourceBody: _OptionalResourceBody { public func compare(to other: Self) -> BodyComparison { diff --git a/Sources/JSONAPITesting/Comparisons/DocumentDataCompare.swift b/Sources/JSONAPITesting/Comparisons/DocumentDataCompare.swift index 87b6819..607bfa7 100644 --- a/Sources/JSONAPITesting/Comparisons/DocumentDataCompare.swift +++ b/Sources/JSONAPITesting/Comparisons/DocumentDataCompare.swift @@ -33,17 +33,6 @@ public struct DocumentDataComparison: Equatable, PropertyComparable { } } -extension DocumentBodyData where PrimaryResourceBody: _ResourceBody { - public func compare(to other: Self) -> DocumentDataComparison { - return .init( - primary: primary.compare(to: other.primary), - includes: includes.compare(to: other.includes), - meta: Comparison(meta, other.meta), - links: Comparison(links, other.links) - ) - } -} - extension DocumentBodyData where PrimaryResourceBody: _OptionalResourceBody { public func compare(to other: Self) -> DocumentDataComparison { return .init( @@ -56,28 +45,23 @@ extension DocumentBodyData where PrimaryResourceBody: _OptionalResourceBody { } public enum PrimaryResourceBodyComparison: Equatable, CustomStringConvertible { - case single(ResourceObjectComparison) - case many(ManyResourceObjectComparison) - case other(Comparison) + case oneOrMore(ManyResourceObjectComparison) + case optionalSingle(Comparison) public var isSame: Bool { switch self { - case .other(let comparison): + case .optionalSingle(let comparison): return comparison == .same - case .single(let comparison): - return comparison.isSame - case .many(let comparison): + case .oneOrMore(let comparison): return comparison.isSame } } public var description: String { switch self { - case .other(let comparison): + case .optionalSingle(let comparison): return comparison.rawValue - case .single(let comparison): - return comparison.rawValue - case .many(let comparison): + case .oneOrMore(let comparison): return comparison.rawValue } } @@ -107,19 +91,14 @@ extension _OptionalResourceBody where WrappedPrimaryResourceType: ResourceObject guard let one = optionalResourceObject, let two = other.optionalResourceObject else { - func nilOrName(_ resObj: T?) -> String { - resObj.map { String(describing: type(of: $0)) } ?? "nil" + func nilOrName(_ resObj: [T]?) -> String { + resObj.map { _ in String(describing: T.self) } ?? "nil" } - return .other(Comparison(nilOrName(optionalResourceObject), nilOrName(other.optionalResourceObject))) + return .optionalSingle(Comparison(nilOrName(optionalResourceObject), nilOrName(other.optionalResourceObject))) } - return .single(.init(one, two)) - } -} -extension _ResourceBody where PrimaryResourceType: ResourceObjectType { - public func compare(to other: Self) -> PrimaryResourceBodyComparison { - return .many(.init(resourceObjects.compare(to: other.resourceObjects, using: { r1, r2 in + return .oneOrMore(.init(one.compare(to: two, using: { r1, r2 in let r1AsResource = r1 as? AbstractResourceObjectType let maybeComparison = r1AsResource @@ -141,14 +120,9 @@ extension _ResourceBody where PrimaryResourceType: ResourceObjectType { } } -public protocol _ResourceBody { - associatedtype PrimaryResourceType: ResourceObjectType - var resourceObjects: [PrimaryResourceType] { get } -} - public protocol _OptionalResourceBody { associatedtype WrappedPrimaryResourceType: ResourceObjectType - var optionalResourceObject: WrappedPrimaryResourceType? { get } + var optionalResourceObject: [WrappedPrimaryResourceType]? { get } } public protocol _OptionalResourceObjectType { @@ -168,17 +142,16 @@ extension Optional: _OptionalResourceObjectType where Wrapped: ResourceObjectTyp } } -extension ManyResourceBody: _ResourceBody where PrimaryResource: ResourceObjectType { - public var resourceObjects: [PrimaryResource] { values } +extension ResourceObject: _OptionalResourceObjectType { + public var maybeValue: Self? { self } } -extension SingleResourceBody: _ResourceBody where PrimaryResource: ResourceObjectType { - public typealias PrimaryResourceType = PrimaryResource - public var resourceObjects: [PrimaryResource] { [value] } +extension ManyResourceBody: _OptionalResourceBody where PrimaryResource: ResourceObjectType { + public var optionalResourceObject: [PrimaryResource]? { values } } extension SingleResourceBody: _OptionalResourceBody where PrimaryResource: _OptionalResourceObjectType { public typealias WrappedPrimaryResourceType = PrimaryResource.Wrapped - public var optionalResourceObject: WrappedPrimaryResourceType? { value.maybeValue } + public var optionalResourceObject: [WrappedPrimaryResourceType]? { value.maybeValue.map { [$0] } } } diff --git a/Tests/JSONAPITestingTests/Comparisons/DocumentCompareTests.swift b/Tests/JSONAPITestingTests/Comparisons/DocumentCompareTests.swift index 7c00e38..0d19a5c 100644 --- a/Tests/JSONAPITestingTests/Comparisons/DocumentCompareTests.swift +++ b/Tests/JSONAPITestingTests/Comparisons/DocumentCompareTests.swift @@ -53,7 +53,7 @@ final class DocumentCompareTests: XCTestCase { ]) XCTAssertEqual(d8.compare(to: d9).differences, [ - "Body": ##"(Primary Resource: ('age' attribute: 10 ≠ 12), ('bestFriend' relationship: Optional(Id(2)) ≠ nil), ('favoriteColor' attribute: nil ≠ Optional("blue")), ('name' attribute: name ≠ Fig), (id: 1 ≠ 5))"## + "Body": ##"(Primary Resource: (resource 1: 'age' attribute: 10 ≠ 12, 'bestFriend' relationship: Optional(Id(2)) ≠ nil, 'favoriteColor' attribute: nil ≠ Optional("blue"), 'name' attribute: name ≠ Fig, id: 1 ≠ 5))"## ]) XCTAssertEqual(d1.compare(to: d10).differences, [ From 0538de48cbbcdec391f02c21d8f78f166856d953 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Wed, 6 Nov 2019 23:41:02 -0800 Subject: [PATCH 101/235] rename PolyWrapped to CodablePolyWrapped (missed when I did other similar renaming --- .../Resource/Poly+PrimaryResource.swift | 44 +++++++++---------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/Sources/JSONAPI/Resource/Poly+PrimaryResource.swift b/Sources/JSONAPI/Resource/Poly+PrimaryResource.swift index 0ca0256..7f1fa1c 100644 --- a/Sources/JSONAPI/Resource/Poly+PrimaryResource.swift +++ b/Sources/JSONAPI/Resource/Poly+PrimaryResource.swift @@ -18,7 +18,7 @@ import Poly public typealias EncodableJSONPoly = Poly & EncodablePrimaryResource public typealias EncodablePolyWrapped = Encodable & Equatable -public typealias PolyWrapped = EncodablePolyWrapped & Decodable +public typealias CodablePolyWrapped = EncodablePolyWrapped & Decodable extension Poly0: CodablePrimaryResource { public init(from decoder: Decoder) throws { @@ -35,42 +35,42 @@ extension Poly1: EncodablePrimaryResource, OptionalEncodablePrimaryResource where A: EncodablePolyWrapped {} extension Poly1: CodablePrimaryResource, OptionalCodablePrimaryResource - where A: PolyWrapped {} + where A: CodablePolyWrapped {} // MARK: - 2 types extension Poly2: EncodablePrimaryResource, OptionalEncodablePrimaryResource where A: EncodablePolyWrapped, B: EncodablePolyWrapped {} extension Poly2: CodablePrimaryResource, OptionalCodablePrimaryResource - where A: PolyWrapped, B: PolyWrapped {} + where A: CodablePolyWrapped, B: CodablePolyWrapped {} // MARK: - 3 types extension Poly3: EncodablePrimaryResource, OptionalEncodablePrimaryResource where A: EncodablePolyWrapped, B: EncodablePolyWrapped, C: EncodablePolyWrapped {} extension Poly3: CodablePrimaryResource, OptionalCodablePrimaryResource - where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped {} + where A: CodablePolyWrapped, B: CodablePolyWrapped, C: CodablePolyWrapped {} // MARK: - 4 types extension Poly4: EncodablePrimaryResource, OptionalEncodablePrimaryResource where A: EncodablePolyWrapped, B: EncodablePolyWrapped, C: EncodablePolyWrapped, D: EncodablePolyWrapped {} extension Poly4: CodablePrimaryResource, OptionalCodablePrimaryResource - where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped {} + where A: CodablePolyWrapped, B: CodablePolyWrapped, C: CodablePolyWrapped, D: CodablePolyWrapped {} // MARK: - 5 types extension Poly5: EncodablePrimaryResource, OptionalEncodablePrimaryResource where A: EncodablePolyWrapped, B: EncodablePolyWrapped, C: EncodablePolyWrapped, D: EncodablePolyWrapped, E: EncodablePolyWrapped {} extension Poly5: CodablePrimaryResource, OptionalCodablePrimaryResource - where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped, E: PolyWrapped {} + where A: CodablePolyWrapped, B: CodablePolyWrapped, C: CodablePolyWrapped, D: CodablePolyWrapped, E: CodablePolyWrapped {} // MARK: - 6 types extension Poly6: EncodablePrimaryResource, OptionalEncodablePrimaryResource where A: EncodablePolyWrapped, B: EncodablePolyWrapped, C: EncodablePolyWrapped, D: EncodablePolyWrapped, E: EncodablePolyWrapped, F: EncodablePolyWrapped {} extension Poly6: CodablePrimaryResource, OptionalCodablePrimaryResource - where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped, E: PolyWrapped, F: PolyWrapped {} + where A: CodablePolyWrapped, B: CodablePolyWrapped, C: CodablePolyWrapped, D: CodablePolyWrapped, E: CodablePolyWrapped, F: CodablePolyWrapped {} // MARK: - 7 types extension Poly7: EncodablePrimaryResource, OptionalEncodablePrimaryResource @@ -84,7 +84,7 @@ extension Poly7: EncodablePrimaryResource, OptionalEncodablePrimaryResource G: EncodablePolyWrapped {} extension Poly7: CodablePrimaryResource, OptionalCodablePrimaryResource - where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped, E: PolyWrapped, F: PolyWrapped, G: PolyWrapped {} + where A: CodablePolyWrapped, B: CodablePolyWrapped, C: CodablePolyWrapped, D: CodablePolyWrapped, E: CodablePolyWrapped, F: CodablePolyWrapped, G: CodablePolyWrapped {} // MARK: - 8 types extension Poly8: EncodablePrimaryResource, OptionalEncodablePrimaryResource @@ -99,7 +99,7 @@ extension Poly8: EncodablePrimaryResource, OptionalEncodablePrimaryResource H: EncodablePolyWrapped {} extension Poly8: CodablePrimaryResource, OptionalCodablePrimaryResource - where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped, E: PolyWrapped, F: PolyWrapped, G: PolyWrapped, H: PolyWrapped {} + where A: CodablePolyWrapped, B: CodablePolyWrapped, C: CodablePolyWrapped, D: CodablePolyWrapped, E: CodablePolyWrapped, F: CodablePolyWrapped, G: CodablePolyWrapped, H: CodablePolyWrapped {} // MARK: - 9 types extension Poly9: EncodablePrimaryResource, OptionalEncodablePrimaryResource @@ -115,7 +115,7 @@ extension Poly9: EncodablePrimaryResource, OptionalEncodablePrimaryResource I: EncodablePolyWrapped {} extension Poly9: CodablePrimaryResource, OptionalCodablePrimaryResource - where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped, E: PolyWrapped, F: PolyWrapped, G: PolyWrapped, H: PolyWrapped, I: PolyWrapped {} + where A: CodablePolyWrapped, B: CodablePolyWrapped, C: CodablePolyWrapped, D: CodablePolyWrapped, E: CodablePolyWrapped, F: CodablePolyWrapped, G: CodablePolyWrapped, H: CodablePolyWrapped, I: CodablePolyWrapped {} // MARK: - 10 types extension Poly10: EncodablePrimaryResource, OptionalEncodablePrimaryResource @@ -132,7 +132,7 @@ extension Poly10: EncodablePrimaryResource, OptionalEncodablePrimaryResource J: EncodablePolyWrapped {} extension Poly10: CodablePrimaryResource, OptionalCodablePrimaryResource - where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped, E: PolyWrapped, F: PolyWrapped, G: PolyWrapped, H: PolyWrapped, I: PolyWrapped, J: PolyWrapped {} + where A: CodablePolyWrapped, B: CodablePolyWrapped, C: CodablePolyWrapped, D: CodablePolyWrapped, E: CodablePolyWrapped, F: CodablePolyWrapped, G: CodablePolyWrapped, H: CodablePolyWrapped, I: CodablePolyWrapped, J: CodablePolyWrapped {} // MARK: - 11 types extension Poly11: EncodablePrimaryResource, OptionalEncodablePrimaryResource @@ -151,14 +151,14 @@ extension Poly11: EncodablePrimaryResource, OptionalEncodablePrimaryResource extension Poly11: CodablePrimaryResource, OptionalCodablePrimaryResource where - A: PolyWrapped, - B: PolyWrapped, - C: PolyWrapped, - D: PolyWrapped, - E: PolyWrapped, - F: PolyWrapped, - G: PolyWrapped, - H: PolyWrapped, - I: PolyWrapped, - J: PolyWrapped, - K: PolyWrapped {} + A: CodablePolyWrapped, + B: CodablePolyWrapped, + C: CodablePolyWrapped, + D: CodablePolyWrapped, + E: CodablePolyWrapped, + F: CodablePolyWrapped, + G: CodablePolyWrapped, + H: CodablePolyWrapped, + I: CodablePolyWrapped, + J: CodablePolyWrapped, + K: CodablePolyWrapped {} From 7fabe2574e044aad0781f48d6dc4e5867f9b8382 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Thu, 7 Nov 2019 07:56:58 -0800 Subject: [PATCH 102/235] rename some of the new public protocols in JSONAPITesting --- .../Comparisons/ArrayCompare.swift | 2 +- .../Comparisons/AttributesCompare.swift | 4 +- .../Comparisons/Comparison.swift | 8 ++-- .../Comparisons/DocumentCompare.swift | 20 ++++---- .../Comparisons/DocumentDataCompare.swift | 48 +++++++++---------- .../Comparisons/IncludesCompare.swift | 2 +- .../Comparisons/RelationshipsCompare.swift | 4 +- .../Comparisons/ResourceObjectCompare.swift | 16 +++---- 8 files changed, 52 insertions(+), 52 deletions(-) diff --git a/Sources/JSONAPITesting/Comparisons/ArrayCompare.swift b/Sources/JSONAPITesting/Comparisons/ArrayCompare.swift index d14ced7..a1f7e5c 100644 --- a/Sources/JSONAPITesting/Comparisons/ArrayCompare.swift +++ b/Sources/JSONAPITesting/Comparisons/ArrayCompare.swift @@ -14,7 +14,7 @@ public enum ArrayElementComparison: Equatable, CustomStringConvertible { case differentValues(String, String) case prebuilt(String) - public init(sameTypeComparison: Comparison) { + public init(sameTypeComparison: BasicComparison) { switch sameTypeComparison { case .same: self = .same diff --git a/Sources/JSONAPITesting/Comparisons/AttributesCompare.swift b/Sources/JSONAPITesting/Comparisons/AttributesCompare.swift index 541c52e..147432c 100644 --- a/Sources/JSONAPITesting/Comparisons/AttributesCompare.swift +++ b/Sources/JSONAPITesting/Comparisons/AttributesCompare.swift @@ -8,11 +8,11 @@ import JSONAPI extension Attributes { - public func compare(to other: Self) -> [String: Comparison] { + public func compare(to other: Self) -> [String: BasicComparison] { let mirror1 = Mirror(reflecting: self) let mirror2 = Mirror(reflecting: other) - var comparisons = [String: Comparison]() + var comparisons = [String: BasicComparison]() for child in mirror1.children { guard let childLabel = child.label else { continue } diff --git a/Sources/JSONAPITesting/Comparisons/Comparison.swift b/Sources/JSONAPITesting/Comparisons/Comparison.swift index e55af29..c0f3240 100644 --- a/Sources/JSONAPITesting/Comparisons/Comparison.swift +++ b/Sources/JSONAPITesting/Comparisons/Comparison.swift @@ -5,13 +5,13 @@ // Created by Mathew Polzin on 11/3/19. // -public protocol Comparable: CustomStringConvertible { +public protocol Comparison: CustomStringConvertible { var rawValue: String { get } var isSame: Bool { get } } -public enum Comparison: Comparable, Equatable { +public enum BasicComparison: Comparison, Equatable { case same case different(String, String) case prebuilt(String) @@ -56,11 +56,11 @@ public enum Comparison: Comparable, Equatable { public typealias NamedDifferences = [String: String] -public protocol PropertyComparable: Comparable { +public protocol PropertyComparison: Comparison { var differences: NamedDifferences { get } } -extension PropertyComparable { +extension PropertyComparison { public var description: String { return differences .map { "(\($0): \($1))" } diff --git a/Sources/JSONAPITesting/Comparisons/DocumentCompare.swift b/Sources/JSONAPITesting/Comparisons/DocumentCompare.swift index 2ffc37e..d56c84d 100644 --- a/Sources/JSONAPITesting/Comparisons/DocumentCompare.swift +++ b/Sources/JSONAPITesting/Comparisons/DocumentCompare.swift @@ -7,11 +7,11 @@ import JSONAPI -public struct DocumentComparison: Equatable, PropertyComparable { - public let apiDescription: Comparison +public struct DocumentComparison: Equatable, PropertyComparison { + public let apiDescription: BasicComparison public let body: BodyComparison - init(apiDescription: Comparison, body: BodyComparison) { + init(apiDescription: BasicComparison, body: BodyComparison) { self.apiDescription = apiDescription self.body = body } @@ -33,7 +33,7 @@ public enum BodyComparison: Equatable, CustomStringConvertible { case differentErrors(ErrorComparison) case differentData(DocumentDataComparison) - public typealias ErrorComparison = [Comparison] + public typealias ErrorComparison = [BasicComparison] static func compare(errors errors1: [E], _ meta1: M?, _ links1: L?, with errors2: [E], _ meta2: M?, _ links2: L?) -> ErrorComparison { return errors1.compare( @@ -48,9 +48,9 @@ public enum BodyComparison: Equatable, CustomStringConvertible { String(describing: error2) ) } - ).map(Comparison.init) + [ - Comparison(meta1, meta2), - Comparison(links1, links2) + ).map(BasicComparison.init) + [ + BasicComparison(meta1, meta2), + BasicComparison(links1, links2) ] } @@ -79,10 +79,10 @@ public enum BodyComparison: Equatable, CustomStringConvertible { public var rawValue: String { description } } -extension EncodableJSONAPIDocument where Body: Equatable, PrimaryResourceBody: _OptionalResourceBody { +extension EncodableJSONAPIDocument where Body: Equatable, PrimaryResourceBody: TestableResourceBody { public func compare(to other: Self) -> DocumentComparison { return DocumentComparison( - apiDescription: Comparison( + apiDescription: BasicComparison( String(describing: apiDescription), String(describing: other.apiDescription) ), @@ -91,7 +91,7 @@ extension EncodableJSONAPIDocument where Body: Equatable, PrimaryResourceBody: _ } } -extension DocumentBody where Self: Equatable, PrimaryResourceBody: _OptionalResourceBody { +extension DocumentBody where Self: Equatable, PrimaryResourceBody: TestableResourceBody { public func compare(to other: Self) -> BodyComparison { // rule out case where they are the same diff --git a/Sources/JSONAPITesting/Comparisons/DocumentDataCompare.swift b/Sources/JSONAPITesting/Comparisons/DocumentDataCompare.swift index 607bfa7..da7f27e 100644 --- a/Sources/JSONAPITesting/Comparisons/DocumentDataCompare.swift +++ b/Sources/JSONAPITesting/Comparisons/DocumentDataCompare.swift @@ -7,13 +7,13 @@ import JSONAPI -public struct DocumentDataComparison: Equatable, PropertyComparable { +public struct DocumentDataComparison: Equatable, PropertyComparison { public let primary: PrimaryResourceBodyComparison public let includes: IncludesComparison - public let meta: Comparison - public let links: Comparison + public let meta: BasicComparison + public let links: BasicComparison - init(primary: PrimaryResourceBodyComparison, includes: IncludesComparison, meta: Comparison, links: Comparison) { + init(primary: PrimaryResourceBodyComparison, includes: IncludesComparison, meta: BasicComparison, links: BasicComparison) { self.primary = primary self.includes = includes self.meta = meta @@ -33,20 +33,20 @@ public struct DocumentDataComparison: Equatable, PropertyComparable { } } -extension DocumentBodyData where PrimaryResourceBody: _OptionalResourceBody { +extension DocumentBodyData where PrimaryResourceBody: TestableResourceBody { public func compare(to other: Self) -> DocumentDataComparison { return .init( primary: primary.compare(to: other.primary), includes: includes.compare(to: other.includes), - meta: Comparison(meta, other.meta), - links: Comparison(links, other.links) + meta: BasicComparison(meta, other.meta), + links: BasicComparison(links, other.links) ) } } public enum PrimaryResourceBodyComparison: Equatable, CustomStringConvertible { case oneOrMore(ManyResourceObjectComparison) - case optionalSingle(Comparison) + case optionalSingle(BasicComparison) public var isSame: Bool { switch self { @@ -69,7 +69,7 @@ public enum PrimaryResourceBodyComparison: Equatable, CustomStringConvertible { public var rawValue: String { return description } } -public struct ManyResourceObjectComparison: Equatable, PropertyComparable { +public struct ManyResourceObjectComparison: Equatable, PropertyComparison { public let comparisons: [ArrayElementComparison] public init(_ comparisons: [ArrayElementComparison]) { @@ -86,16 +86,16 @@ public struct ManyResourceObjectComparison: Equatable, PropertyComparable { } } -extension _OptionalResourceBody where WrappedPrimaryResourceType: ResourceObjectType { +extension TestableResourceBody where TestablePrimaryResourceType: ResourceObjectType { public func compare(to other: Self) -> PrimaryResourceBodyComparison { - guard let one = optionalResourceObject, - let two = other.optionalResourceObject else { + guard let one = testableResourceObject, + let two = other.testableResourceObject else { func nilOrName(_ resObj: [T]?) -> String { resObj.map { _ in String(describing: T.self) } ?? "nil" } - return .optionalSingle(Comparison(nilOrName(optionalResourceObject), nilOrName(other.optionalResourceObject))) + return .optionalSingle(BasicComparison(nilOrName(testableResourceObject), nilOrName(other.testableResourceObject))) } return .oneOrMore(.init(one.compare(to: two, using: { r1, r2 in @@ -120,18 +120,18 @@ extension _OptionalResourceBody where WrappedPrimaryResourceType: ResourceObject } } -public protocol _OptionalResourceBody { - associatedtype WrappedPrimaryResourceType: ResourceObjectType - var optionalResourceObject: [WrappedPrimaryResourceType]? { get } +public protocol TestableResourceBody { + associatedtype TestablePrimaryResourceType: ResourceObjectType + var testableResourceObject: [TestablePrimaryResourceType]? { get } } -public protocol _OptionalResourceObjectType { +public protocol OptionalResourceObjectType { associatedtype Wrapped: ResourceObjectType var maybeValue: Wrapped? { get } } -extension Optional: _OptionalResourceObjectType where Wrapped: ResourceObjectType { +extension Optional: OptionalResourceObjectType where Wrapped: ResourceObjectType { public var maybeValue: Wrapped? { switch self { case .none: @@ -142,16 +142,16 @@ extension Optional: _OptionalResourceObjectType where Wrapped: ResourceObjectTyp } } -extension ResourceObject: _OptionalResourceObjectType { +extension ResourceObject: OptionalResourceObjectType { public var maybeValue: Self? { self } } -extension ManyResourceBody: _OptionalResourceBody where PrimaryResource: ResourceObjectType { - public var optionalResourceObject: [PrimaryResource]? { values } +extension ManyResourceBody: TestableResourceBody where PrimaryResource: ResourceObjectType { + public var testableResourceObject: [PrimaryResource]? { values } } -extension SingleResourceBody: _OptionalResourceBody where PrimaryResource: _OptionalResourceObjectType { - public typealias WrappedPrimaryResourceType = PrimaryResource.Wrapped +extension SingleResourceBody: TestableResourceBody where PrimaryResource: OptionalResourceObjectType { + public typealias TestablePrimaryResourceType = PrimaryResource.Wrapped - public var optionalResourceObject: [WrappedPrimaryResourceType]? { value.maybeValue.map { [$0] } } + public var testableResourceObject: [TestablePrimaryResourceType]? { value.maybeValue.map { [$0] } } } diff --git a/Sources/JSONAPITesting/Comparisons/IncludesCompare.swift b/Sources/JSONAPITesting/Comparisons/IncludesCompare.swift index c638628..13b293d 100644 --- a/Sources/JSONAPITesting/Comparisons/IncludesCompare.swift +++ b/Sources/JSONAPITesting/Comparisons/IncludesCompare.swift @@ -8,7 +8,7 @@ import JSONAPI import Poly -public struct IncludesComparison: Equatable, PropertyComparable { +public struct IncludesComparison: Equatable, PropertyComparison { public let comparisons: [ArrayElementComparison] public init(_ comparisons: [ArrayElementComparison]) { diff --git a/Sources/JSONAPITesting/Comparisons/RelationshipsCompare.swift b/Sources/JSONAPITesting/Comparisons/RelationshipsCompare.swift index c217cb9..3bfa7d6 100644 --- a/Sources/JSONAPITesting/Comparisons/RelationshipsCompare.swift +++ b/Sources/JSONAPITesting/Comparisons/RelationshipsCompare.swift @@ -8,11 +8,11 @@ import JSONAPI extension Relationships { - public func compare(to other: Self) -> [String: Comparison] { + public func compare(to other: Self) -> [String: BasicComparison] { let mirror1 = Mirror(reflecting: self) let mirror2 = Mirror(reflecting: other) - var comparisons = [String: Comparison]() + var comparisons = [String: BasicComparison]() for child in mirror1.children { guard let childLabel = child.label else { continue } diff --git a/Sources/JSONAPITesting/Comparisons/ResourceObjectCompare.swift b/Sources/JSONAPITesting/Comparisons/ResourceObjectCompare.swift index 2619008..ddfeb49 100644 --- a/Sources/JSONAPITesting/Comparisons/ResourceObjectCompare.swift +++ b/Sources/JSONAPITesting/Comparisons/ResourceObjectCompare.swift @@ -7,21 +7,21 @@ import JSONAPI -public struct ResourceObjectComparison: Equatable, PropertyComparable { - public typealias ComparisonHash = [String: Comparison] +public struct ResourceObjectComparison: Equatable, PropertyComparison { + public typealias ComparisonHash = [String: BasicComparison] - public let id: Comparison + public let id: BasicComparison public let attributes: ComparisonHash public let relationships: ComparisonHash - public let meta: Comparison - public let links: Comparison + public let meta: BasicComparison + public let links: BasicComparison public init(_ one: T, _ two: T) { - id = Comparison(one.id.rawValue, two.id.rawValue) + id = BasicComparison(one.id.rawValue, two.id.rawValue) attributes = one.attributes.compare(to: two.attributes) relationships = one.relationships.compare(to: two.relationships) - meta = Comparison(one.meta, two.meta) - links = Comparison(one.links, two.links) + meta = BasicComparison(one.meta, two.meta) + links = BasicComparison(one.links, two.links) } public var differences: NamedDifferences { From 86344ef93f16bc21725de8b301991994dabc9869 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Thu, 7 Nov 2019 08:08:45 -0800 Subject: [PATCH 103/235] trivial refactor in sparse fieldset file --- Sources/JSONAPI/SparseFields/SparseFieldset.swift | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/Sources/JSONAPI/SparseFields/SparseFieldset.swift b/Sources/JSONAPI/SparseFields/SparseFieldset.swift index b326f13..f7fca89 100644 --- a/Sources/JSONAPI/SparseFields/SparseFieldset.swift +++ b/Sources/JSONAPI/SparseFields/SparseFieldset.swift @@ -36,15 +36,12 @@ public struct SparseFieldset< public extension ResourceObject where Description.Attributes: SparsableAttributes { + /// The `SparseFieldset` type for this `ResourceObject` + typealias SparseType = SparseFieldset + /// Get a Sparse Fieldset of this `ResourceObject` that can be encoded /// as a `SparsePrimaryResource`. - func sparse(with fields: [Description.Attributes.CodingKeys]) -> SparseFieldset { + func sparse(with fields: [Description.Attributes.CodingKeys]) -> SparseType { return SparseFieldset(self, fields: fields) } } - -public extension ResourceObject where Description.Attributes: SparsableAttributes { - - /// The `SparseFieldset` type for this `ResourceObject` - typealias SparseType = SparseFieldset -} From 11ef050d58afb91098c1fb486af28f744963564d Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Fri, 8 Nov 2019 18:47:28 -0800 Subject: [PATCH 104/235] most common relationship errors tested. --- Sources/JSONAPI/Document/Document.swift | 4 +- Sources/JSONAPI/Document/Includes.swift | 2 +- Sources/JSONAPI/EncodingError.swift | 10 +- Sources/JSONAPI/Resource/Attribute.swift | 10 +- .../Resource/Poly+PrimaryResource.swift | 4 +- Sources/JSONAPI/Resource/Relationship.swift | 16 ++- .../Resource Object/ResourceObject.swift | 105 +++++++++++++++- .../ResourceObjectDecodingErrorTests.swift | 114 ++++++++++++++++++ .../stubs/ResourceObjectStubs.swift | 53 ++++++++ .../Test Helpers/EncodeDecode.swift | 4 +- 10 files changed, 300 insertions(+), 22 deletions(-) create mode 100644 Tests/JSONAPITests/ResourceObject/ResourceObjectDecodingErrorTests.swift diff --git a/Sources/JSONAPI/Document/Document.swift b/Sources/JSONAPI/Document/Document.swift index e1d1e69..ff28502 100644 --- a/Sources/JSONAPI/Document/Document.swift +++ b/Sources/JSONAPI/Document/Document.swift @@ -397,10 +397,10 @@ extension Document: Decodable, CodableJSONAPIDocument where PrimaryResourceBody: // TODO come back to this and make robust guard let metaVal = meta else { - throw JSONAPIEncodingError.missingOrMalformedMetadata + throw JSONAPIEncodingError.missingOrMalformedMetadata(path: decoder.codingPath) } guard let linksVal = links else { - throw JSONAPIEncodingError.missingOrMalformedLinks + throw JSONAPIEncodingError.missingOrMalformedLinks(path: decoder.codingPath) } body = .data(.init(primary: data, includes: maybeIncludes ?? Includes.none, meta: metaVal, links: linksVal)) diff --git a/Sources/JSONAPI/Document/Includes.swift b/Sources/JSONAPI/Document/Includes.swift index 5c0ab3f..b8ff4b2 100644 --- a/Sources/JSONAPI/Document/Includes.swift +++ b/Sources/JSONAPI/Document/Includes.swift @@ -32,7 +32,7 @@ public struct Includes: Encodable, Equatable { var container = encoder.unkeyedContainer() guard I.self != NoIncludes.self else { - throw JSONAPIEncodingError.illegalEncoding("Attempting to encode Include0, which should be represented by the absense of an 'included' entry altogether.") + throw JSONAPIEncodingError.illegalEncoding("Attempting to encode Include0, which should be represented by the absense of an 'included' entry altogether.", path: encoder.codingPath) } for value in values { diff --git a/Sources/JSONAPI/EncodingError.swift b/Sources/JSONAPI/EncodingError.swift index 8b461cd..d2423be 100644 --- a/Sources/JSONAPI/EncodingError.swift +++ b/Sources/JSONAPI/EncodingError.swift @@ -6,9 +6,9 @@ // public enum JSONAPIEncodingError: Swift.Error { - case typeMismatch(expected: String, found: String) - case illegalEncoding(String) - case illegalDecoding(String) - case missingOrMalformedMetadata - case missingOrMalformedLinks + case typeMismatch(expected: String, found: String, path: [CodingKey]) + case illegalEncoding(String, path: [CodingKey]) + case illegalDecoding(String, path: [CodingKey]) + case missingOrMalformedMetadata(path: [CodingKey]) + case missingOrMalformedLinks(path: [CodingKey]) } diff --git a/Sources/JSONAPI/Resource/Attribute.swift b/Sources/JSONAPI/Resource/Attribute.swift index f73d672..5b0ed41 100644 --- a/Sources/JSONAPI/Resource/Attribute.swift +++ b/Sources/JSONAPI/Resource/Attribute.swift @@ -5,13 +5,21 @@ // Created by Mathew Polzin on 11/13/18. // -public protocol AttributeType: Codable { +public protocol AbstractAttributeType { + var rawValueType: Any.Type { 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 /// A TransformedAttribute takes a Codable type and attempts to turn it into another type. diff --git a/Sources/JSONAPI/Resource/Poly+PrimaryResource.swift b/Sources/JSONAPI/Resource/Poly+PrimaryResource.swift index 7f1fa1c..92719b5 100644 --- a/Sources/JSONAPI/Resource/Poly+PrimaryResource.swift +++ b/Sources/JSONAPI/Resource/Poly+PrimaryResource.swift @@ -22,11 +22,11 @@ public typealias CodablePolyWrapped = EncodablePolyWrapped & Decodable extension Poly0: CodablePrimaryResource { public init(from decoder: Decoder) throws { - throw JSONAPIEncodingError.illegalDecoding("Attempted to decode Poly0, which should represent a thing that is not expected to be found in a document.") + throw JSONAPIEncodingError.illegalDecoding("Attempted to decode Poly0, which should represent a thing that is not expected to be found in a document.", path: decoder.codingPath) } public func encode(to encoder: Encoder) throws { - throw JSONAPIEncodingError.illegalEncoding("Attempted to encode Poly0, which should represent a thing that is not expected to be found in a document.") + throw JSONAPIEncodingError.illegalEncoding("Attempted to encode Poly0, which should represent a thing that is not expected to be found in a document.", path: encoder.codingPath) } } diff --git a/Sources/JSONAPI/Resource/Relationship.swift b/Sources/JSONAPI/Resource/Relationship.swift index 69fabd9..d7202bc 100644 --- a/Sources/JSONAPI/Resource/Relationship.swift +++ b/Sources/JSONAPI/Resource/Relationship.swift @@ -170,8 +170,16 @@ extension ToOneRelationship: Codable where Identifiable.Identifier: OptionalId { // succeeds and then attempt to coerce nil to a Identifier // type at which point we can store nil in `id`. let anyNil: Any? = nil - if try container.decodeNil(forKey: .data), - let val = anyNil as? Identifiable.Identifier { + if try container.decodeNil(forKey: .data) { + guard let val = anyNil as? Identifiable.Identifier else { + throw DecodingError.valueNotFound( + Self.self, + DecodingError.Context( + codingPath: decoder.codingPath, + debugDescription: "Expected non-null relationship data." + ) + ) + } id = val return } @@ -181,7 +189,7 @@ extension ToOneRelationship: Codable where Identifiable.Identifier: OptionalId { let type = try identifier.decode(String.self, forKey: .entityType) guard type == Identifiable.jsonType else { - throw JSONAPIEncodingError.typeMismatch(expected: Identifiable.jsonType, found: type) + throw JSONAPIEncodingError.typeMismatch(expected: Identifiable.jsonType, found: type, path: decoder.codingPath) } id = Identifiable.Identifier(rawValue: try identifier.decode(Identifiable.Identifier.RawType.self, forKey: .id)) @@ -239,7 +247,7 @@ extension ToManyRelationship: Codable { let type = try identifier.decode(String.self, forKey: .entityType) guard type == Relatable.jsonType else { - throw JSONAPIEncodingError.typeMismatch(expected: Relatable.jsonType, found: type) + throw JSONAPIEncodingError.typeMismatch(expected: Relatable.jsonType, found: type, path: decoder.codingPath) } newIds.append(Relatable.Identifier(rawValue: try identifier.decode(Relatable.Identifier.RawType.self, forKey: .id))) diff --git a/Sources/JSONAPI/Resource/Resource Object/ResourceObject.swift b/Sources/JSONAPI/Resource/Resource Object/ResourceObject.swift index a746e6e..b336655 100644 --- a/Sources/JSONAPI/Resource/Resource Object/ResourceObject.swift +++ b/Sources/JSONAPI/Resource/Resource Object/ResourceObject.swift @@ -414,21 +414,114 @@ public extension ResourceObject { let type = try container.decode(String.self, forKey: .type) guard ResourceObject.jsonType == type else { - throw JSONAPIEncodingError.typeMismatch(expected: Description.jsonType, found: type) + throw JSONAPIEncodingError.typeMismatch(expected: Description.jsonType, found: type, path: decoder.codingPath) } let maybeUnidentified = Unidentified() as? EntityRawIdType id = try maybeUnidentified.map { ResourceObject.Id(rawValue: $0) } ?? container.decode(ResourceObject.Id.self, forKey: .id) - attributes = try (NoAttributes() as? Description.Attributes) ?? - container.decode(Description.Attributes.self, forKey: .attributes) + do { + attributes = try (NoAttributes() as? Description.Attributes) ?? + container.decode(Description.Attributes.self, forKey: .attributes) + } catch let decodingError as DecodingError { + throw ResourceObjectDecodingError(decodingError) + ?? decodingError + } catch let decodingError as JSONAPIEncodingError { + throw ResourceObjectDecodingError(decodingError) + ?? decodingError + } - relationships = try (NoRelationships() as? Description.Relationships) - ?? container.decodeIfPresent(Description.Relationships.self, forKey: .relationships) - ?? Description.Relationships(from: EmptyObjectDecoder()) + do { + relationships = try (NoRelationships() as? Description.Relationships) + ?? container.decodeIfPresent(Description.Relationships.self, forKey: .relationships) + ?? Description.Relationships(from: EmptyObjectDecoder()) + } catch let decodingError as DecodingError { + throw ResourceObjectDecodingError(decodingError) + ?? decodingError + } catch let decodingError as JSONAPIEncodingError { + throw ResourceObjectDecodingError(decodingError) + ?? decodingError + } catch _ as EmptyObjectDecodingError { + throw ResourceObjectDecodingError( + subjectName: ResourceObjectDecodingError.entireObject, + cause: .keyNotFound, + location: .relationships + ) + } meta = try (NoMetadata() as? MetaType) ?? container.decode(MetaType.self, forKey: .meta) links = try (NoLinks() as? LinksType) ?? container.decode(LinksType.self, forKey: .links) } } + +public struct ResourceObjectDecodingError: Swift.Error, Equatable { + public let subjectName: String + public let cause: Cause + public let location: Location + + static let entireObject = "entire object" + + public enum Cause: Equatable { + case keyNotFound + case valueNotFound + case typeMismatch(expectedTypeName: String) + case jsonTypeMismatch(expectedType: String, foundType: String) + } + + public enum Location: Equatable { + case attributes + case relationships + } + + init?(_ decodingError: DecodingError) { + switch decodingError { + case .typeMismatch(let expectedType, let ctx): + (location, subjectName) = Self.context(ctx) + let typeString: String + if let attrType = expectedType as? AbstractAttributeType { + typeString = String(describing: attrType.rawValueType) + } else { + typeString = String(describing: expectedType) + } + cause = .typeMismatch(expectedTypeName: typeString) + case .valueNotFound(_, let ctx): + (location, subjectName) = Self.context(ctx) + cause = .valueNotFound + case .keyNotFound(let missingKey, let ctx): + (location, _) = Self.context(ctx) + subjectName = missingKey.stringValue + cause = .keyNotFound + default: + return nil + } + } + + init?(_ jsonAPIError: JSONAPIEncodingError) { + switch jsonAPIError { + case .typeMismatch(expected: let expected, found: let found, path: let path): + (location, subjectName) = Self.context(path: path) + cause = .jsonTypeMismatch(expectedType: expected, foundType: found) + default: + return nil + } + } + + init(subjectName: String, cause: Cause, location: Location) { + self.subjectName = subjectName + self.cause = cause + self.location = location + } + + static func context(_ decodingContext: DecodingError.Context) -> (Location, name: String) { + + return context(path: decodingContext.codingPath) + } + + static func context(path: [CodingKey]) -> (Location, name: String) { + return ( + path.contains { $0.stringValue == "attributes" } ? .attributes : .relationships, + name: path.last?.stringValue ?? "unnamed" + ) + } +} diff --git a/Tests/JSONAPITests/ResourceObject/ResourceObjectDecodingErrorTests.swift b/Tests/JSONAPITests/ResourceObject/ResourceObjectDecodingErrorTests.swift new file mode 100644 index 0000000..a0d28b0 --- /dev/null +++ b/Tests/JSONAPITests/ResourceObject/ResourceObjectDecodingErrorTests.swift @@ -0,0 +1,114 @@ +// +// ResourceObjectDecodingErrorTests.swift +// +// +// Created by Mathew Polzin on 11/8/19. +// + +import XCTest +@testable import JSONAPI + +// MARK: - Relationships +final class ResourceObjectDecodingErrorTests: XCTestCase { + func test_missingRelationshipsObject() { + XCTAssertThrowsError(try testDecoder.decode( + TestEntity.self, + from: entity_relationships_entirely_missing + )) { error in + XCTAssertEqual( + error as? ResourceObjectDecodingError, + ResourceObjectDecodingError( + subjectName: ResourceObjectDecodingError.entireObject, + cause: .keyNotFound, + location: .relationships + ) + ) + } + } + + func test_required_relationship() { + XCTAssertThrowsError(try testDecoder.decode( + TestEntity.self, + from: entity_required_relationship_is_omitted + )) { error in + XCTAssertEqual( + error as? ResourceObjectDecodingError, + ResourceObjectDecodingError( + subjectName: "required", + cause: .keyNotFound, + location: .relationships + ) + ) + } + } + + func test_NonNullable_relationship() { + XCTAssertThrowsError(try testDecoder.decode( + TestEntity.self, + from: entity_nonNullable_relationship_is_null + )) { error in + XCTAssertEqual( + error as? ResourceObjectDecodingError, + ResourceObjectDecodingError( + subjectName: "required", + cause: .valueNotFound, + location: .relationships + ) + ) + } + } + + func test_NonNullable_relationship2() { + XCTAssertThrowsError(try testDecoder.decode( + TestEntity.self, + from: entity_nonNullable_relationship_is_null2 + )) { error in + XCTAssertEqual( + error as? ResourceObjectDecodingError, + ResourceObjectDecodingError( + subjectName: "required", + cause: .valueNotFound, + location: .relationships + ) + ) + } + } + + func test_oneTypeVsAnother_relationship() { + XCTAssertThrowsError(try testDecoder.decode( + TestEntity.self, + from: entity_relationship_is_wrong_type + )) { error in + print(error) + XCTAssertEqual( + error as? ResourceObjectDecodingError, + ResourceObjectDecodingError( + subjectName: "required", + cause: .jsonTypeMismatch(expectedType: "thirteenth_test_entities", foundType: "not_the_same"), + location: .relationships + ) + ) + } + } +} + +// MARK: - Attributes +extension ResourceObjectDecodingErrorTests { + // TODO: write tests +} + +// MARK: - Test Types +extension ResourceObjectDecodingErrorTests { + enum TestEntityType: ResourceObjectDescription { + public static var jsonType: String { return "thirteenth_test_entities" } + + typealias Attributes = NoAttributes + + public struct Relationships: JSONAPI.Relationships { + + let required: ToOneRelationship + } + } + + typealias TestEntity = BasicEntity +} diff --git a/Tests/JSONAPITests/ResourceObject/stubs/ResourceObjectStubs.swift b/Tests/JSONAPITests/ResourceObject/stubs/ResourceObjectStubs.swift index 58343ce..06cf6cd 100644 --- a/Tests/JSONAPITests/ResourceObject/stubs/ResourceObjectStubs.swift +++ b/Tests/JSONAPITests/ResourceObject/stubs/ResourceObjectStubs.swift @@ -393,6 +393,59 @@ let entity_all_relationships_optional_and_omitted = """ } """.data(using: .utf8)! +let entity_nonNullable_relationship_is_null = """ +{ + "id": "1", + "type": "thirteenth_test_entities", + "relationships": { + "required": null + } +} +""".data(using: .utf8)! + +let entity_nonNullable_relationship_is_null2 = """ +{ + "id": "1", + "type": "thirteenth_test_entities", + "relationships": { + "required": { + "data": null + } + } +} +""".data(using: .utf8)! + +let entity_required_relationship_is_omitted = """ +{ + "id": "1", + "type": "thirteenth_test_entities", + "relationships": { + } +} +""".data(using: .utf8)! + +let entity_relationship_is_wrong_type = """ +{ + "id": "1", + "type": "thirteenth_test_entities", + "relationships": { + "required": { + "data": { + "id": "123", + "type": "not_the_same" + } + } + } +} +""".data(using: .utf8)! + +let entity_relationships_entirely_missing = """ +{ + "id": "1", + "type": "thirteenth_test_entities", +} +""".data(using: .utf8)! + let entity_unidentified = """ { "type": "unidentified_test_entities", diff --git a/Tests/JSONAPITests/Test Helpers/EncodeDecode.swift b/Tests/JSONAPITests/Test Helpers/EncodeDecode.swift index a2eeb56..d0b1c56 100644 --- a/Tests/JSONAPITests/Test Helpers/EncodeDecode.swift +++ b/Tests/JSONAPITests/Test Helpers/EncodeDecode.swift @@ -8,8 +8,10 @@ import Foundation import XCTest +let testDecoder = JSONDecoder() + func decoded(type: T.Type, data: Data) -> T { - return try! JSONDecoder().decode(T.self, from: data) + return try! testDecoder.decode(T.self, from: data) } func encoded(value: T) -> Data { From 0b4baf35d52df475d0c854f02988edc45ec5ab56 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Sat, 9 Nov 2019 00:33:42 -0800 Subject: [PATCH 105/235] got some attribute cases added and tested. added some descriptions (custom string convertible) --- Sources/JSONAPI/Document/Document.swift | 4 +- Sources/JSONAPI/Document/Includes.swift | 2 +- ...ngError.swift => JSONAPICodingError.swift} | 4 +- .../Resource/Poly+PrimaryResource.swift | 4 +- Sources/JSONAPI/Resource/Relationship.swift | 4 +- .../Resource Object/ResourceObject.swift | 54 ++++-- .../ResourceObjectDecodingErrorTests.swift | 170 +++++++++++++++++- .../stubs/ResourceObjectStubs.swift | 58 ++++++ 8 files changed, 275 insertions(+), 25 deletions(-) rename Sources/JSONAPI/{EncodingError.swift => JSONAPICodingError.swift} (82%) diff --git a/Sources/JSONAPI/Document/Document.swift b/Sources/JSONAPI/Document/Document.swift index ff28502..7c387be 100644 --- a/Sources/JSONAPI/Document/Document.swift +++ b/Sources/JSONAPI/Document/Document.swift @@ -397,10 +397,10 @@ extension Document: Decodable, CodableJSONAPIDocument where PrimaryResourceBody: // TODO come back to this and make robust guard let metaVal = meta else { - throw JSONAPIEncodingError.missingOrMalformedMetadata(path: decoder.codingPath) + throw JSONAPICodingError.missingOrMalformedMetadata(path: decoder.codingPath) } guard let linksVal = links else { - throw JSONAPIEncodingError.missingOrMalformedLinks(path: decoder.codingPath) + throw JSONAPICodingError.missingOrMalformedLinks(path: decoder.codingPath) } body = .data(.init(primary: data, includes: maybeIncludes ?? Includes.none, meta: metaVal, links: linksVal)) diff --git a/Sources/JSONAPI/Document/Includes.swift b/Sources/JSONAPI/Document/Includes.swift index b8ff4b2..e7a701b 100644 --- a/Sources/JSONAPI/Document/Includes.swift +++ b/Sources/JSONAPI/Document/Includes.swift @@ -32,7 +32,7 @@ public struct Includes: Encodable, Equatable { var container = encoder.unkeyedContainer() guard I.self != NoIncludes.self else { - throw JSONAPIEncodingError.illegalEncoding("Attempting to encode Include0, which should be represented by the absense of an 'included' entry altogether.", path: encoder.codingPath) + throw JSONAPICodingError.illegalEncoding("Attempting to encode Include0, which should be represented by the absense of an 'included' entry altogether.", path: encoder.codingPath) } for value in values { diff --git a/Sources/JSONAPI/EncodingError.swift b/Sources/JSONAPI/JSONAPICodingError.swift similarity index 82% rename from Sources/JSONAPI/EncodingError.swift rename to Sources/JSONAPI/JSONAPICodingError.swift index d2423be..281f1de 100644 --- a/Sources/JSONAPI/EncodingError.swift +++ b/Sources/JSONAPI/JSONAPICodingError.swift @@ -1,11 +1,11 @@ // -// EncodingError.swift +// JSONAPICodingError.swift // JSONAPI // // Created by Mathew Polzin on 12/7/18. // -public enum JSONAPIEncodingError: Swift.Error { +public enum JSONAPICodingError: Swift.Error { case typeMismatch(expected: String, found: String, path: [CodingKey]) case illegalEncoding(String, path: [CodingKey]) case illegalDecoding(String, path: [CodingKey]) diff --git a/Sources/JSONAPI/Resource/Poly+PrimaryResource.swift b/Sources/JSONAPI/Resource/Poly+PrimaryResource.swift index 92719b5..73c1246 100644 --- a/Sources/JSONAPI/Resource/Poly+PrimaryResource.swift +++ b/Sources/JSONAPI/Resource/Poly+PrimaryResource.swift @@ -22,11 +22,11 @@ public typealias CodablePolyWrapped = EncodablePolyWrapped & Decodable extension Poly0: CodablePrimaryResource { public init(from decoder: Decoder) throws { - throw JSONAPIEncodingError.illegalDecoding("Attempted to decode Poly0, which should represent a thing that is not expected to be found in a document.", path: decoder.codingPath) + throw JSONAPICodingError.illegalDecoding("Attempted to decode Poly0, which should represent a thing that is not expected to be found in a document.", path: decoder.codingPath) } public func encode(to encoder: Encoder) throws { - throw JSONAPIEncodingError.illegalEncoding("Attempted to encode Poly0, which should represent a thing that is not expected to be found in a document.", path: encoder.codingPath) + throw JSONAPICodingError.illegalEncoding("Attempted to encode Poly0, which should represent a thing that is not expected to be found in a document.", path: encoder.codingPath) } } diff --git a/Sources/JSONAPI/Resource/Relationship.swift b/Sources/JSONAPI/Resource/Relationship.swift index d7202bc..92b43f2 100644 --- a/Sources/JSONAPI/Resource/Relationship.swift +++ b/Sources/JSONAPI/Resource/Relationship.swift @@ -189,7 +189,7 @@ extension ToOneRelationship: Codable where Identifiable.Identifier: OptionalId { let type = try identifier.decode(String.self, forKey: .entityType) guard type == Identifiable.jsonType else { - throw JSONAPIEncodingError.typeMismatch(expected: Identifiable.jsonType, found: type, path: decoder.codingPath) + throw JSONAPICodingError.typeMismatch(expected: Identifiable.jsonType, found: type, path: decoder.codingPath) } id = Identifiable.Identifier(rawValue: try identifier.decode(Identifiable.Identifier.RawType.self, forKey: .id)) @@ -247,7 +247,7 @@ extension ToManyRelationship: Codable { let type = try identifier.decode(String.self, forKey: .entityType) guard type == Relatable.jsonType else { - throw JSONAPIEncodingError.typeMismatch(expected: Relatable.jsonType, found: type, path: decoder.codingPath) + throw JSONAPICodingError.typeMismatch(expected: Relatable.jsonType, found: type, path: decoder.codingPath) } newIds.append(Relatable.Identifier(rawValue: try identifier.decode(Relatable.Identifier.RawType.self, forKey: .id))) diff --git a/Sources/JSONAPI/Resource/Resource Object/ResourceObject.swift b/Sources/JSONAPI/Resource/Resource Object/ResourceObject.swift index b336655..6305ead 100644 --- a/Sources/JSONAPI/Resource/Resource Object/ResourceObject.swift +++ b/Sources/JSONAPI/Resource/Resource Object/ResourceObject.swift @@ -414,21 +414,25 @@ public extension ResourceObject { let type = try container.decode(String.self, forKey: .type) guard ResourceObject.jsonType == type else { - throw JSONAPIEncodingError.typeMismatch(expected: Description.jsonType, found: type, path: decoder.codingPath) + throw JSONAPICodingError.typeMismatch(expected: Description.jsonType, found: type, path: decoder.codingPath) } let maybeUnidentified = Unidentified() as? EntityRawIdType id = try maybeUnidentified.map { ResourceObject.Id(rawValue: $0) } ?? container.decode(ResourceObject.Id.self, forKey: .id) do { - attributes = try (NoAttributes() as? Description.Attributes) ?? - container.decode(Description.Attributes.self, forKey: .attributes) + attributes = try (NoAttributes() as? Description.Attributes) + ?? container.decodeIfPresent(Description.Attributes.self, forKey: .attributes) + ?? Description.Attributes(from: EmptyObjectDecoder()) } catch let decodingError as DecodingError { throw ResourceObjectDecodingError(decodingError) ?? decodingError - } catch let decodingError as JSONAPIEncodingError { - throw ResourceObjectDecodingError(decodingError) - ?? decodingError + } catch _ as EmptyObjectDecodingError { + throw ResourceObjectDecodingError( + subjectName: ResourceObjectDecodingError.entireObject, + cause: .keyNotFound, + location: .attributes + ) } do { @@ -438,7 +442,7 @@ public extension ResourceObject { } catch let decodingError as DecodingError { throw ResourceObjectDecodingError(decodingError) ?? decodingError - } catch let decodingError as JSONAPIEncodingError { + } catch let decodingError as JSONAPICodingError { throw ResourceObjectDecodingError(decodingError) ?? decodingError } catch _ as EmptyObjectDecodingError { @@ -469,21 +473,23 @@ public struct ResourceObjectDecodingError: Swift.Error, Equatable { case jsonTypeMismatch(expectedType: String, foundType: String) } - public enum Location: Equatable { + public enum Location: String, Equatable { case attributes case relationships + + var singular: String { + switch self { + case .attributes: return "attribute" + case .relationships: return "relationship" + } + } } init?(_ decodingError: DecodingError) { switch decodingError { case .typeMismatch(let expectedType, let ctx): (location, subjectName) = Self.context(ctx) - let typeString: String - if let attrType = expectedType as? AbstractAttributeType { - typeString = String(describing: attrType.rawValueType) - } else { - typeString = String(describing: expectedType) - } + let typeString = String(describing: expectedType) cause = .typeMismatch(expectedTypeName: typeString) case .valueNotFound(_, let ctx): (location, subjectName) = Self.context(ctx) @@ -497,7 +503,7 @@ public struct ResourceObjectDecodingError: Swift.Error, Equatable { } } - init?(_ jsonAPIError: JSONAPIEncodingError) { + init?(_ jsonAPIError: JSONAPICodingError) { switch jsonAPIError { case .typeMismatch(expected: let expected, found: let found, path: let path): (location, subjectName) = Self.context(path: path) @@ -525,3 +531,21 @@ public struct ResourceObjectDecodingError: Swift.Error, Equatable { ) } } + +extension ResourceObjectDecodingError: CustomStringConvertible { + public var description: String { + switch cause { + case .keyNotFound: + if subjectName == ResourceObjectDecodingError.entireObject { + return "\(location) object is required and missing." + } + return "'\(subjectName)' \(location.singular) is required and missing." + case .valueNotFound: + return "'\(subjectName)' \(location.singular) is not nullable but null." + case .typeMismatch(expectedTypeName: let expected): + return "'\(subjectName)' \(location.singular) is not a \(expected) as expected." + case .jsonTypeMismatch(expectedType: let expected, foundType: let found): + return "'\(subjectName)' \(location.singular) is of JSON:API type \"\(found)\" but it was expected to be \"\(expected)\"" + } + } +} diff --git a/Tests/JSONAPITests/ResourceObject/ResourceObjectDecodingErrorTests.swift b/Tests/JSONAPITests/ResourceObject/ResourceObjectDecodingErrorTests.swift index a0d28b0..3dbb971 100644 --- a/Tests/JSONAPITests/ResourceObject/ResourceObjectDecodingErrorTests.swift +++ b/Tests/JSONAPITests/ResourceObject/ResourceObjectDecodingErrorTests.swift @@ -23,6 +23,11 @@ final class ResourceObjectDecodingErrorTests: XCTestCase { location: .relationships ) ) + + XCTAssertEqual( + (error as? ResourceObjectDecodingError)?.description, + "relationships object is required and missing." + ) } } @@ -39,6 +44,11 @@ final class ResourceObjectDecodingErrorTests: XCTestCase { location: .relationships ) ) + + XCTAssertEqual( + (error as? ResourceObjectDecodingError)?.description, + "'required' relationship is required and missing." + ) } } @@ -55,6 +65,11 @@ final class ResourceObjectDecodingErrorTests: XCTestCase { location: .relationships ) ) + + XCTAssertEqual( + (error as? ResourceObjectDecodingError)?.description, + "'required' relationship is not nullable but null." + ) } } @@ -71,6 +86,11 @@ final class ResourceObjectDecodingErrorTests: XCTestCase { location: .relationships ) ) + + XCTAssertEqual( + (error as? ResourceObjectDecodingError)?.description, + "'required' relationship is not nullable but null." + ) } } @@ -88,13 +108,146 @@ final class ResourceObjectDecodingErrorTests: XCTestCase { location: .relationships ) ) + + XCTAssertEqual( + (error as? ResourceObjectDecodingError)?.description, + #"'required' relationship is of JSON:API type "not_the_same" but it was expected to be "thirteenth_test_entities""# + ) } } + + func test_twoOneVsToMany_relationship() { + // TODO: write test + } } // MARK: - Attributes extension ResourceObjectDecodingErrorTests { - // TODO: write tests + func test_missingAttributesObject() { + XCTAssertThrowsError(try testDecoder.decode( + TestEntity2.self, + from: entity_attributes_entirely_missing + )) { error in + XCTAssertEqual( + error as? ResourceObjectDecodingError, + ResourceObjectDecodingError( + subjectName: ResourceObjectDecodingError.entireObject, + cause: .keyNotFound, + location: .attributes + ) + ) + + XCTAssertEqual( + (error as? ResourceObjectDecodingError)?.description, + "attributes object is required and missing." + ) + } + } + + func test_required_attribute() { + XCTAssertThrowsError(try testDecoder.decode( + TestEntity2.self, + from: entity_required_attribute_is_omitted + )) { error in + XCTAssertEqual( + error as? ResourceObjectDecodingError, + ResourceObjectDecodingError( + subjectName: "required", + cause: .keyNotFound, + location: .attributes + ) + ) + + XCTAssertEqual( + (error as? ResourceObjectDecodingError)?.description, + "'required' attribute is required and missing." + ) + } + } + + func test_NonNullable_attribute() { + XCTAssertThrowsError(try testDecoder.decode( + TestEntity2.self, + from: entity_nonNullable_attribute_is_null + )) { error in + XCTAssertEqual( + error as? ResourceObjectDecodingError, + ResourceObjectDecodingError( + subjectName: "required", + cause: .valueNotFound, + location: .attributes + ) + ) + + XCTAssertEqual( + (error as? ResourceObjectDecodingError)?.description, + "'required' attribute is not nullable but null." + ) + } + } + + func test_oneTypeVsAnother_attribute() { + XCTAssertThrowsError(try testDecoder.decode( + TestEntity2.self, + from: entity_attribute_is_wrong_type + )) { error in + XCTAssertEqual( + error as? ResourceObjectDecodingError, + ResourceObjectDecodingError( + subjectName: "required", + cause: .typeMismatch(expectedTypeName: String(describing: String.self)), + location: .attributes + ) + ) + + XCTAssertEqual( + (error as? ResourceObjectDecodingError)?.description, + "'required' attribute is not a String as expected." + ) + } + } + + func test_oneTypeVsAnother_attribute2() { + XCTAssertThrowsError(try testDecoder.decode( + TestEntity2.self, + from: entity_attribute_is_wrong_type2 + )) { error in + XCTAssertEqual( + error as? ResourceObjectDecodingError, + ResourceObjectDecodingError( + subjectName: "other", + cause: .typeMismatch(expectedTypeName: String(describing: Int.self)), + location: .attributes + ) + ) + + XCTAssertEqual( + (error as? ResourceObjectDecodingError)?.description, + "'other' attribute is not a Int as expected." + ) + } + } + + func test_oneTypeVsAnother_attribute3() { + XCTAssertThrowsError(try testDecoder.decode( + TestEntity2.self, + from: entity_attribute_is_wrong_type3 + )) { error in + XCTAssertEqual( + error as? ResourceObjectDecodingError, + ResourceObjectDecodingError( + subjectName: "yetAnother", + cause: .typeMismatch(expectedTypeName: String(describing: Bool.self)), + location: .attributes + ) + ) + + XCTAssertEqual( + (error as? ResourceObjectDecodingError)?.description, + "'yetAnother' attribute is not a Bool as expected." + ) + } + } } // MARK: - Test Types @@ -111,4 +264,19 @@ extension ResourceObjectDecodingErrorTests { } typealias TestEntity = BasicEntity + + enum TestEntityType2: ResourceObjectDescription { + public static var jsonType: String { return "thirteenth_test_entities" } + + public struct Attributes: JSONAPI.Attributes { + + let required: Attribute + let other: Attribute? + let yetAnother: Attribute? + } + + typealias Relationships = NoRelationships + } + + typealias TestEntity2 = BasicEntity } diff --git a/Tests/JSONAPITests/ResourceObject/stubs/ResourceObjectStubs.swift b/Tests/JSONAPITests/ResourceObject/stubs/ResourceObjectStubs.swift index 06cf6cd..6817bff 100644 --- a/Tests/JSONAPITests/ResourceObject/stubs/ResourceObjectStubs.swift +++ b/Tests/JSONAPITests/ResourceObject/stubs/ResourceObjectStubs.swift @@ -446,6 +446,64 @@ let entity_relationships_entirely_missing = """ } """.data(using: .utf8)! +let entity_required_attribute_is_omitted = """ +{ + "id": "1", + "type": "thirteenth_test_entities", + "attributes": { + } +} +""".data(using: .utf8)! + +let entity_nonNullable_attribute_is_null = """ +{ + "id": "1", + "type": "thirteenth_test_entities", + "attributes": { + "required": null + } +} +""".data(using: .utf8)! + +let entity_attribute_is_wrong_type = """ +{ + "id": "1", + "type": "thirteenth_test_entities", + "attributes": { + "required": 10 + } +} +""".data(using: .utf8)! + +let entity_attribute_is_wrong_type2 = """ +{ + "id": "1", + "type": "thirteenth_test_entities", + "attributes": { + "required": "hello", + "other": "world" + } +} +""".data(using: .utf8)! + +let entity_attribute_is_wrong_type3 = """ +{ + "id": "1", + "type": "thirteenth_test_entities", + "attributes": { + "required": "hello", + "yetAnother": 101 + } +} +""".data(using: .utf8)! + +let entity_attributes_entirely_missing = """ +{ + "id": "1", + "type": "thirteenth_test_entities" +} +""".data(using: .utf8)! + let entity_unidentified = """ { "type": "unidentified_test_entities", From 455ff64326c4d8be79e5b8ac5a7318c0450cee16 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Sat, 9 Nov 2019 00:34:39 -0800 Subject: [PATCH 106/235] update linuxmain --- Tests/JSONAPITests/XCTestManifests.swift | 52 ++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/Tests/JSONAPITests/XCTestManifests.swift b/Tests/JSONAPITests/XCTestManifests.swift index cd6323d..0b2c004 100644 --- a/Tests/JSONAPITests/XCTestManifests.swift +++ b/Tests/JSONAPITests/XCTestManifests.swift @@ -37,8 +37,11 @@ extension Attribute_FunctorTests { // to regenerate. static let __allTests__Attribute_FunctorTests = [ ("test_mapGuaranteed", test_mapGuaranteed), + ("test_mapGuaranteed_deprecated", test_mapGuaranteed_deprecated), ("test_mapOptionalFailure", test_mapOptionalFailure), + ("test_mapOptionalFailure_deprecated", test_mapOptionalFailure_deprecated), ("test_mapOptionalSuccess", test_mapOptionalSuccess), + ("test_mapOptionalSuccess_deprecated", test_mapOptionalSuccess_deprecated), ] } @@ -59,9 +62,11 @@ extension ComputedPropertiesTests { // to regenerate. static let __allTests__ComputedPropertiesTests = [ ("test_ComputedAttributeAccess", test_ComputedAttributeAccess), + ("test_ComputedAttributeAccess_deprecated", test_ComputedAttributeAccess_deprecated), ("test_ComputedNonAttributeAccess", test_ComputedNonAttributeAccess), ("test_ComputedRelationshipAccess", test_ComputedRelationshipAccess), ("test_DecodeIgnoresComputed", test_DecodeIgnoresComputed), + ("test_DecodeIgnoresComputed_deprecated", test_DecodeIgnoresComputed_deprecated), ("test_EncodeIgnoresComputed", test_EncodeIgnoresComputed), ] } @@ -72,8 +77,10 @@ extension CustomAttributesTests { // to regenerate. static let __allTests__CustomAttributesTests = [ ("test_customDecode", test_customDecode), + ("test_customDecode_deprecated", test_customDecode_deprecated), ("test_customEncode", test_customEncode), ("test_customKeysDecode", test_customKeysDecode), + ("test_customKeysDecode_deprecated", test_customKeysDecode_deprecated), ("test_customKeysEncode", test_customKeysEncode), ] } @@ -301,7 +308,9 @@ extension PolyProxyTests { ("test_generalReasonableness", test_generalReasonableness), ("test_UserAAndBEncodeEquality", test_UserAAndBEncodeEquality), ("test_UserADecode", test_UserADecode), + ("test_UserADecode_deprecated", test_UserADecode_deprecated), ("test_UserBDecode", test_UserBDecode), + ("test_UserBDecode_deprecated", test_UserBDecode_deprecated), ] } @@ -353,6 +362,26 @@ extension ResourceBodyTests { ] } +extension ResourceObjectDecodingErrorTests { + // DO NOT MODIFY: This is autogenerated, use: + // `swift test --generate-linuxmain` + // to regenerate. + static let __allTests__ResourceObjectDecodingErrorTests = [ + ("test_missingAttributesObject", test_missingAttributesObject), + ("test_missingRelationshipsObject", test_missingRelationshipsObject), + ("test_NonNullable_attribute", test_NonNullable_attribute), + ("test_NonNullable_relationship", test_NonNullable_relationship), + ("test_NonNullable_relationship2", test_NonNullable_relationship2), + ("test_oneTypeVsAnother_attribute", test_oneTypeVsAnother_attribute), + ("test_oneTypeVsAnother_attribute2", test_oneTypeVsAnother_attribute2), + ("test_oneTypeVsAnother_attribute3", test_oneTypeVsAnother_attribute3), + ("test_oneTypeVsAnother_relationship", test_oneTypeVsAnother_relationship), + ("test_required_attribute", test_required_attribute), + ("test_required_relationship", test_required_relationship), + ("test_twoOneVsToMany_relationship", test_twoOneVsToMany_relationship), + ] +} + extension ResourceObjectReplacingTests { // DO NOT MODIFY: This is autogenerated, use: // `swift test --generate-linuxmain` @@ -378,37 +407,49 @@ extension ResourceObjectTests { ("test_copyIdentifiedByValue", test_copyIdentifiedByValue), ("test_copyWithNewId", test_copyWithNewId), ("test_entityAllAttribute", test_entityAllAttribute), + ("test_entityAllAttribute_deprecated", test_entityAllAttribute_deprecated), ("test_entityAllAttribute_encode", test_entityAllAttribute_encode), ("test_entityBrokenNullableOmittedAttribute", test_entityBrokenNullableOmittedAttribute), ("test_EntityNoRelationshipsNoAttributes", test_EntityNoRelationshipsNoAttributes), ("test_EntityNoRelationshipsNoAttributes_encode", test_EntityNoRelationshipsNoAttributes_encode), ("test_EntityNoRelationshipsSomeAttributes", test_EntityNoRelationshipsSomeAttributes), + ("test_EntityNoRelationshipsSomeAttributes_deprecated", test_EntityNoRelationshipsSomeAttributes_deprecated), ("test_EntityNoRelationshipsSomeAttributes_encode", test_EntityNoRelationshipsSomeAttributes_encode), ("test_entityOneNullAndOneOmittedAttribute", test_entityOneNullAndOneOmittedAttribute), + ("test_entityOneNullAndOneOmittedAttribute_deprecated", test_entityOneNullAndOneOmittedAttribute_deprecated), ("test_entityOneNullAndOneOmittedAttribute_encode", test_entityOneNullAndOneOmittedAttribute_encode), ("test_entityOneNullAttribute", test_entityOneNullAttribute), + ("test_entityOneNullAttribute_deprecated", test_entityOneNullAttribute_deprecated), ("test_entityOneNullAttribute_encode", test_entityOneNullAttribute_encode), ("test_entityOneOmittedAttribute", test_entityOneOmittedAttribute), + ("test_entityOneOmittedAttribute_deprecated", test_entityOneOmittedAttribute_deprecated), ("test_entityOneOmittedAttribute_encode", test_entityOneOmittedAttribute_encode), ("test_EntitySomeRelationshipsNoAttributes", test_EntitySomeRelationshipsNoAttributes), ("test_EntitySomeRelationshipsNoAttributes_encode", test_EntitySomeRelationshipsNoAttributes_encode), ("test_EntitySomeRelationshipsSomeAttributes", test_EntitySomeRelationshipsSomeAttributes), + ("test_EntitySomeRelationshipsSomeAttributes_deprecated", test_EntitySomeRelationshipsSomeAttributes_deprecated), ("test_EntitySomeRelationshipsSomeAttributes_encode", test_EntitySomeRelationshipsSomeAttributes_encode), ("test_EntitySomeRelationshipsSomeAttributesWithLinks", test_EntitySomeRelationshipsSomeAttributesWithLinks), + ("test_EntitySomeRelationshipsSomeAttributesWithLinks_deprecated", test_EntitySomeRelationshipsSomeAttributesWithLinks_deprecated), ("test_EntitySomeRelationshipsSomeAttributesWithLinks_encode", test_EntitySomeRelationshipsSomeAttributesWithLinks_encode), ("test_EntitySomeRelationshipsSomeAttributesWithMeta", test_EntitySomeRelationshipsSomeAttributesWithMeta), + ("test_EntitySomeRelationshipsSomeAttributesWithMeta_deprecated", test_EntitySomeRelationshipsSomeAttributesWithMeta_deprecated), ("test_EntitySomeRelationshipsSomeAttributesWithMeta_encode", test_EntitySomeRelationshipsSomeAttributesWithMeta_encode), ("test_EntitySomeRelationshipsSomeAttributesWithMetaAndLinks", test_EntitySomeRelationshipsSomeAttributesWithMetaAndLinks), + ("test_EntitySomeRelationshipsSomeAttributesWithMetaAndLinks_deprecated", test_EntitySomeRelationshipsSomeAttributesWithMetaAndLinks_deprecated), ("test_EntitySomeRelationshipsSomeAttributesWithMetaAndLinks_encode", test_EntitySomeRelationshipsSomeAttributesWithMetaAndLinks_encode), ("test_initialization", test_initialization), ("test_IntOver10_encode", test_IntOver10_encode), ("test_IntOver10_failure", test_IntOver10_failure), ("test_IntOver10_success", test_IntOver10_success), ("test_IntToString", test_IntToString), + ("test_IntToString_deprecated", test_IntToString_deprecated), ("test_IntToString_encode", test_IntToString_encode), ("test_MetaEntityAttributeAccessWorks", test_MetaEntityAttributeAccessWorks), + ("test_MetaEntityAttributeAccessWorks_deprecated", test_MetaEntityAttributeAccessWorks_deprecated), ("test_MetaEntityRelationshipAccessWorks", test_MetaEntityRelationshipAccessWorks), ("test_NonNullOptionalNullableAttribute", test_NonNullOptionalNullableAttribute), + ("test_NonNullOptionalNullableAttribute_deprecated", test_NonNullOptionalNullableAttribute_deprecated), ("test_NonNullOptionalNullableAttribute_encode", test_NonNullOptionalNullableAttribute_encode), ("test_nullableRelationshipIsNull", test_nullableRelationshipIsNull), ("test_nullableRelationshipIsNull_encode", test_nullableRelationshipIsNull_encode), @@ -417,6 +458,7 @@ extension ResourceObjectTests { ("test_nullableRelationshipNotNullOrOmitted", test_nullableRelationshipNotNullOrOmitted), ("test_nullableRelationshipNotNullOrOmitted_encode", test_nullableRelationshipNotNullOrOmitted_encode), ("test_NullOptionalNullableAttribute", test_NullOptionalNullableAttribute), + ("test_NullOptionalNullableAttribute_deprecated", test_NullOptionalNullableAttribute_deprecated), ("test_NullOptionalNullableAttribute_encode", test_NullOptionalNullableAttribute_encode), ("test_optional_relationship_operator_access", test_optional_relationship_operator_access), ("test_optionalNullableRelationshipNulled", test_optionalNullableRelationshipNulled), @@ -434,15 +476,21 @@ extension ResourceObjectTests { ("test_toMany_relationship_operator_access", test_toMany_relationship_operator_access), ("test_toManyMetaRelationshipAccessWorks", test_toManyMetaRelationshipAccessWorks), ("test_UnidentifiedEntity", test_UnidentifiedEntity), + ("test_UnidentifiedEntity_deprecated", test_UnidentifiedEntity_deprecated), ("test_UnidentifiedEntity_encode", test_UnidentifiedEntity_encode), ("test_unidentifiedEntityAttributeAccess", test_unidentifiedEntityAttributeAccess), + ("test_unidentifiedEntityAttributeAccess_deprecated", test_unidentifiedEntityAttributeAccess_deprecated), ("test_UnidentifiedEntityWithAttributes", test_UnidentifiedEntityWithAttributes), + ("test_UnidentifiedEntityWithAttributes_deprecated", test_UnidentifiedEntityWithAttributes_deprecated), ("test_UnidentifiedEntityWithAttributes_encode", test_UnidentifiedEntityWithAttributes_encode), ("test_UnidentifiedEntityWithAttributesAndLinks", test_UnidentifiedEntityWithAttributesAndLinks), + ("test_UnidentifiedEntityWithAttributesAndLinks_deprecated", test_UnidentifiedEntityWithAttributesAndLinks_deprecated), ("test_UnidentifiedEntityWithAttributesAndLinks_encode", test_UnidentifiedEntityWithAttributesAndLinks_encode), ("test_UnidentifiedEntityWithAttributesAndMeta", test_UnidentifiedEntityWithAttributesAndMeta), + ("test_UnidentifiedEntityWithAttributesAndMeta_deprecated", test_UnidentifiedEntityWithAttributesAndMeta_deprecated), ("test_UnidentifiedEntityWithAttributesAndMeta_encode", test_UnidentifiedEntityWithAttributesAndMeta_encode), ("test_UnidentifiedEntityWithAttributesAndMetaAndLinks", test_UnidentifiedEntityWithAttributesAndMetaAndLinks), + ("test_UnidentifiedEntityWithAttributesAndMetaAndLinks_deprecated", test_UnidentifiedEntityWithAttributesAndMetaAndLinks_deprecated), ("test_UnidentifiedEntityWithAttributesAndMetaAndLinks_encode", test_UnidentifiedEntityWithAttributesAndMetaAndLinks_encode), ] } @@ -468,8 +516,11 @@ extension SparseFieldsetTests { // to regenerate. static let __allTests__SparseFieldsetTests = [ ("test_FullEncode", test_FullEncode), + ("test_FullEncode_deprecated", test_FullEncode_deprecated), ("test_PartialEncode", test_PartialEncode), + ("test_PartialEncode_deprecated", test_PartialEncode_deprecated), ("test_sparseFieldsMethod", test_sparseFieldsMethod), + ("test_sparseFieldsMethod_deprecated", test_sparseFieldsMethod_deprecated), ] } @@ -500,6 +551,7 @@ public func __allTests() -> [XCTestCaseEntry] { testCase(PolyProxyTests.__allTests__PolyProxyTests), testCase(RelationshipTests.__allTests__RelationshipTests), testCase(ResourceBodyTests.__allTests__ResourceBodyTests), + testCase(ResourceObjectDecodingErrorTests.__allTests__ResourceObjectDecodingErrorTests), testCase(ResourceObjectReplacingTests.__allTests__ResourceObjectReplacingTests), testCase(ResourceObjectTests.__allTests__ResourceObjectTests), testCase(SparseFieldEncoderTests.__allTests__SparseFieldEncoderTests), From 4dc30ddc1c579075bd359b841d5f50223d19789e Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Sun, 10 Nov 2019 20:46:35 -0800 Subject: [PATCH 107/235] Rounding out the Resource Object errors --- Sources/JSONAPI/JSONAPICodingError.swift | 13 ++ Sources/JSONAPI/Resource/Relationship.swift | 30 ++++- .../Resource Object/ResourceObject.swift | 91 -------------- .../ResourceObjectDecodingError.swift | 111 ++++++++++++++++++ .../ResourceObjectDecodingErrorTests.swift | 42 ++++++- .../stubs/ResourceObjectStubs.swift | 48 +++++++- 6 files changed, 234 insertions(+), 101 deletions(-) create mode 100644 Sources/JSONAPI/Resource/Resource Object/ResourceObjectDecodingError.swift diff --git a/Sources/JSONAPI/JSONAPICodingError.swift b/Sources/JSONAPI/JSONAPICodingError.swift index 281f1de..da0705d 100644 --- a/Sources/JSONAPI/JSONAPICodingError.swift +++ b/Sources/JSONAPI/JSONAPICodingError.swift @@ -7,8 +7,21 @@ 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 + } + } + } } diff --git a/Sources/JSONAPI/Resource/Relationship.swift b/Sources/JSONAPI/Resource/Relationship.swift index 92b43f2..a11031e 100644 --- a/Sources/JSONAPI/Resource/Relationship.swift +++ b/Sources/JSONAPI/Resource/Relationship.swift @@ -184,7 +184,17 @@ extension ToOneRelationship: Codable where Identifiable.Identifier: OptionalId { return } - let identifier = try container.nestedContainer(keyedBy: ResourceIdentifierCodingKeys.self, forKey: .data) + let identifier: KeyedDecodingContainer + do { + identifier = try container.nestedContainer(keyedBy: ResourceIdentifierCodingKeys.self, forKey: .data) + } catch let error as DecodingError { + guard case let .typeMismatch(type, context) = error, + type is _DictionaryType.Type else { + throw error + } + throw JSONAPICodingError.quantityMismatch(expected: .one, + path: context.codingPath) + } let type = try identifier.decode(String.self, forKey: .entityType) @@ -238,7 +248,17 @@ extension ToManyRelationship: Codable { links = try container.decode(LinksType.self, forKey: .links) } - var identifiers = try container.nestedUnkeyedContainer(forKey: .data) + var identifiers: UnkeyedDecodingContainer + do { + identifiers = try container.nestedUnkeyedContainer(forKey: .data) + } catch let error as DecodingError { + guard case let .typeMismatch(type, context) = error, + type is _ArrayType.Type else { + throw error + } + throw JSONAPICodingError.quantityMismatch(expected: .many, + path: context.codingPath) + } var newIds = [Relatable.Identifier]() while !identifiers.isAtEnd { @@ -285,3 +305,9 @@ extension ToOneRelationship: CustomStringConvertible { extension ToManyRelationship: CustomStringConvertible { public var description: String { return "Relationship([\(ids.map(String.init(describing:)).joined(separator: ", "))])" } } + +private protocol _DictionaryType {} +extension Dictionary: _DictionaryType {} + +private protocol _ArrayType {} +extension Array: _ArrayType {} diff --git a/Sources/JSONAPI/Resource/Resource Object/ResourceObject.swift b/Sources/JSONAPI/Resource/Resource Object/ResourceObject.swift index 6305ead..e85d8f0 100644 --- a/Sources/JSONAPI/Resource/Resource Object/ResourceObject.swift +++ b/Sources/JSONAPI/Resource/Resource Object/ResourceObject.swift @@ -458,94 +458,3 @@ public extension ResourceObject { links = try (NoLinks() as? LinksType) ?? container.decode(LinksType.self, forKey: .links) } } - -public struct ResourceObjectDecodingError: Swift.Error, Equatable { - public let subjectName: String - public let cause: Cause - public let location: Location - - static let entireObject = "entire object" - - public enum Cause: Equatable { - case keyNotFound - case valueNotFound - case typeMismatch(expectedTypeName: String) - case jsonTypeMismatch(expectedType: String, foundType: String) - } - - public enum Location: String, Equatable { - case attributes - case relationships - - var singular: String { - switch self { - case .attributes: return "attribute" - case .relationships: return "relationship" - } - } - } - - init?(_ decodingError: DecodingError) { - switch decodingError { - case .typeMismatch(let expectedType, let ctx): - (location, subjectName) = Self.context(ctx) - let typeString = String(describing: expectedType) - cause = .typeMismatch(expectedTypeName: typeString) - case .valueNotFound(_, let ctx): - (location, subjectName) = Self.context(ctx) - cause = .valueNotFound - case .keyNotFound(let missingKey, let ctx): - (location, _) = Self.context(ctx) - subjectName = missingKey.stringValue - cause = .keyNotFound - default: - return nil - } - } - - init?(_ jsonAPIError: JSONAPICodingError) { - switch jsonAPIError { - case .typeMismatch(expected: let expected, found: let found, path: let path): - (location, subjectName) = Self.context(path: path) - cause = .jsonTypeMismatch(expectedType: expected, foundType: found) - default: - return nil - } - } - - init(subjectName: String, cause: Cause, location: Location) { - self.subjectName = subjectName - self.cause = cause - self.location = location - } - - static func context(_ decodingContext: DecodingError.Context) -> (Location, name: String) { - - return context(path: decodingContext.codingPath) - } - - static func context(path: [CodingKey]) -> (Location, name: String) { - return ( - path.contains { $0.stringValue == "attributes" } ? .attributes : .relationships, - name: path.last?.stringValue ?? "unnamed" - ) - } -} - -extension ResourceObjectDecodingError: CustomStringConvertible { - public var description: String { - switch cause { - case .keyNotFound: - if subjectName == ResourceObjectDecodingError.entireObject { - return "\(location) object is required and missing." - } - return "'\(subjectName)' \(location.singular) is required and missing." - case .valueNotFound: - return "'\(subjectName)' \(location.singular) is not nullable but null." - case .typeMismatch(expectedTypeName: let expected): - return "'\(subjectName)' \(location.singular) is not a \(expected) as expected." - case .jsonTypeMismatch(expectedType: let expected, foundType: let found): - return "'\(subjectName)' \(location.singular) is of JSON:API type \"\(found)\" but it was expected to be \"\(expected)\"" - } - } -} diff --git a/Sources/JSONAPI/Resource/Resource Object/ResourceObjectDecodingError.swift b/Sources/JSONAPI/Resource/Resource Object/ResourceObjectDecodingError.swift new file mode 100644 index 0000000..d1ac49c --- /dev/null +++ b/Sources/JSONAPI/Resource/Resource Object/ResourceObjectDecodingError.swift @@ -0,0 +1,111 @@ +// +// ResourceObjectDecodingError.swift +// +// +// Created by Mathew Polzin on 11/10/19. +// + +public struct ResourceObjectDecodingError: Swift.Error, Equatable { + public let subjectName: String + public let cause: Cause + public let location: Location + + static let entireObject = "entire object" + + public enum Cause: Equatable { + case keyNotFound + case valueNotFound + case typeMismatch(expectedTypeName: String) + case jsonTypeMismatch(expectedType: String, foundType: String) + case quantityMismatch(expected: JSONAPICodingError.Quantity) + } + + public enum Location: String, Equatable { + case attributes + case relationships + + var singular: String { + switch self { + case .attributes: return "attribute" + case .relationships: return "relationship" + } + } + } + + init?(_ decodingError: DecodingError) { + switch decodingError { + case .typeMismatch(let expectedType, let ctx): + (location, subjectName) = Self.context(ctx) + let typeString = String(describing: expectedType) + cause = .typeMismatch(expectedTypeName: typeString) + case .valueNotFound(_, let ctx): + (location, subjectName) = Self.context(ctx) + cause = .valueNotFound + case .keyNotFound(let missingKey, let ctx): + (location, _) = Self.context(ctx) + subjectName = missingKey.stringValue + cause = .keyNotFound + default: + return nil + } + } + + init?(_ jsonAPIError: JSONAPICodingError) { + switch jsonAPIError { + case .typeMismatch(expected: let expected, found: let found, path: let path): + (location, subjectName) = Self.context(path: path) + cause = .jsonTypeMismatch(expectedType: expected, foundType: found) + case .quantityMismatch(expected: let expected, path: let path): + (location, subjectName) = Self.context(path: path) + cause = .quantityMismatch(expected: expected) + default: + return nil + } + } + + init(subjectName: String, cause: Cause, location: Location) { + self.subjectName = subjectName + self.cause = cause + self.location = location + } + + static func context(_ decodingContext: DecodingError.Context) -> (Location, name: String) { + + return context(path: decodingContext.codingPath) + } + + static func context(path: [CodingKey]) -> (Location, name: String) { + return ( + path.contains { $0.stringValue == "attributes" } ? .attributes : .relationships, + name: path.last?.stringValue ?? "unnamed" + ) + } +} + +extension ResourceObjectDecodingError: CustomStringConvertible { + public var description: String { + switch cause { + case .keyNotFound: + if subjectName == ResourceObjectDecodingError.entireObject { + return "\(location) object is required and missing." + } + return "'\(subjectName)' \(location.singular) is required and missing." + case .valueNotFound: + return "'\(subjectName)' \(location.singular) is not nullable but null." + case .typeMismatch(expectedTypeName: let expected): + return "'\(subjectName)' \(location.singular) is not a \(expected) as expected." + case .jsonTypeMismatch(expectedType: let expected, foundType: let found): + return "'\(subjectName)' \(location.singular) is of JSON:API type \"\(found)\" but it was expected to be \"\(expected)\"" + case .quantityMismatch(expected: let expected): + let expecation: String = { + switch expected { + case .many: + return "\(expected) values" + case .one: + return "\(expected) value" + } + }() + return "'\(subjectName)' \(location.singular) should contain \(expecation) but found \(expected.other)" + } + } +} diff --git a/Tests/JSONAPITests/ResourceObject/ResourceObjectDecodingErrorTests.swift b/Tests/JSONAPITests/ResourceObject/ResourceObjectDecodingErrorTests.swift index 3dbb971..0891d23 100644 --- a/Tests/JSONAPITests/ResourceObject/ResourceObjectDecodingErrorTests.swift +++ b/Tests/JSONAPITests/ResourceObject/ResourceObjectDecodingErrorTests.swift @@ -117,7 +117,44 @@ final class ResourceObjectDecodingErrorTests: XCTestCase { } func test_twoOneVsToMany_relationship() { - // TODO: write test + + XCTAssertThrowsError(try testDecoder.decode( + TestEntity.self, + from: entity_single_relationship_is_many + )) { error in + XCTAssertEqual( + error as? ResourceObjectDecodingError, + ResourceObjectDecodingError( + subjectName: "required", + cause: .quantityMismatch(expected: .one), + location: .relationships + ) + ) + + XCTAssertEqual( + (error as? ResourceObjectDecodingError)?.description, + "'required' relationship should contain one value but found many" + ) + } + + XCTAssertThrowsError(try testDecoder.decode( + TestEntity.self, + from: entity_many_relationship_is_single + )) { error in + XCTAssertEqual( + error as? ResourceObjectDecodingError, + ResourceObjectDecodingError( + subjectName: "omittable", + cause: .quantityMismatch(expected: .many), + location: .relationships + ) + ) + + XCTAssertEqual( + (error as? ResourceObjectDecodingError)?.description, + "'omittable' relationship should contain many values but found one" + ) + } } } @@ -260,13 +297,14 @@ extension ResourceObjectDecodingErrorTests { public struct Relationships: JSONAPI.Relationships { let required: ToOneRelationship + let omittable: ToManyRelationship? } } typealias TestEntity = BasicEntity enum TestEntityType2: ResourceObjectDescription { - public static var jsonType: String { return "thirteenth_test_entities" } + public static var jsonType: String { return "fourteenth_test_entities" } public struct Attributes: JSONAPI.Attributes { diff --git a/Tests/JSONAPITests/ResourceObject/stubs/ResourceObjectStubs.swift b/Tests/JSONAPITests/ResourceObject/stubs/ResourceObjectStubs.swift index 6817bff..9fe7355 100644 --- a/Tests/JSONAPITests/ResourceObject/stubs/ResourceObjectStubs.swift +++ b/Tests/JSONAPITests/ResourceObject/stubs/ResourceObjectStubs.swift @@ -439,6 +439,42 @@ let entity_relationship_is_wrong_type = """ } """.data(using: .utf8)! +let entity_single_relationship_is_many = """ +{ + "id": "1", + "type": "thirteenth_test_entities", + "relationships": { + "required": { + "data": [{ + "id": "123", + "type": "thirteenth_test_entities" + }] + } + } +} +""".data(using: .utf8)! + +let entity_many_relationship_is_single = """ +{ + "id": "1", + "type": "thirteenth_test_entities", + "relationships": { + "required": { + "data": { + "id": "123", + "type": "thirteenth_test_entities" + } + }, + "omittable": { + "data": { + "id": "456", + "type": "thirteenth_test_entities" + } + } + } +} +""".data(using: .utf8)! + let entity_relationships_entirely_missing = """ { "id": "1", @@ -449,7 +485,7 @@ let entity_relationships_entirely_missing = """ let entity_required_attribute_is_omitted = """ { "id": "1", - "type": "thirteenth_test_entities", + "type": "fourteenth_test_entities", "attributes": { } } @@ -458,7 +494,7 @@ let entity_required_attribute_is_omitted = """ let entity_nonNullable_attribute_is_null = """ { "id": "1", - "type": "thirteenth_test_entities", + "type": "fourteenth_test_entities", "attributes": { "required": null } @@ -468,7 +504,7 @@ let entity_nonNullable_attribute_is_null = """ let entity_attribute_is_wrong_type = """ { "id": "1", - "type": "thirteenth_test_entities", + "type": "fourteenth_test_entities", "attributes": { "required": 10 } @@ -478,7 +514,7 @@ let entity_attribute_is_wrong_type = """ let entity_attribute_is_wrong_type2 = """ { "id": "1", - "type": "thirteenth_test_entities", + "type": "fourteenth_test_entities", "attributes": { "required": "hello", "other": "world" @@ -489,7 +525,7 @@ let entity_attribute_is_wrong_type2 = """ let entity_attribute_is_wrong_type3 = """ { "id": "1", - "type": "thirteenth_test_entities", + "type": "fourteenth_test_entities", "attributes": { "required": "hello", "yetAnother": 101 @@ -500,7 +536,7 @@ let entity_attribute_is_wrong_type3 = """ let entity_attributes_entirely_missing = """ { "id": "1", - "type": "thirteenth_test_entities" + "type": "fourteenth_test_entities" } """.data(using: .utf8)! From 2eecf959953f18d8cd1dffb068a2dc60dbcbed30 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Sun, 10 Nov 2019 23:02:26 -0800 Subject: [PATCH 108/235] Adding Document Decoding Errors for some common problems --- Sources/JSONAPI/Document/Document.swift | 26 ++- .../Document/DocumentDecodingError.swift | 68 ++++++- Sources/JSONAPI/Document/Includes.swift | 25 ++- Sources/JSONAPI/Document/ResourceBody.swift | 17 +- Sources/JSONAPI/Resource/Id.swift | 7 +- .../Resource Object/ResourceObject.swift | 10 +- .../ResourceObjectDecodingError.swift | 21 +- .../Document/DocumentDecodingErrorTests.swift | 189 ++++++++++++++++++ .../Document/stubs/DocumentStubs.swift | 85 ++++++++ .../ResourceObjectDecodingErrorTests.swift | 24 +++ .../stubs/ResourceObjectStubs.swift | 11 + Tests/JSONAPITests/XCTestManifests.swift | 15 ++ 12 files changed, 483 insertions(+), 15 deletions(-) create mode 100644 Tests/JSONAPITests/Document/DocumentDecodingErrorTests.swift diff --git a/Sources/JSONAPI/Document/Document.swift b/Sources/JSONAPI/Document/Document.swift index 7c387be..34c32a4 100644 --- a/Sources/JSONAPI/Document/Document.swift +++ b/Sources/JSONAPI/Document/Document.swift @@ -349,8 +349,8 @@ extension Document: Decodable, CodableJSONAPIDocument where PrimaryResourceBody: public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: RootCodingKeys.self) - if let noData = NoAPIDescription() as? APIDescription { - apiDescription = noData + if let noAPIDescription = NoAPIDescription() as? APIDescription { + apiDescription = noAPIDescription } else { apiDescription = try container.decode(APIDescription.self, forKey: .jsonapi) } @@ -389,10 +389,24 @@ extension Document: Decodable, CodableJSONAPIDocument where PrimaryResourceBody: if let noData = NoResourceBody() as? PrimaryResourceBody { data = noData } else { - data = try container.decode(PrimaryResourceBody.self, forKey: .data) + do { + data = try container.decode(PrimaryResourceBody.self, forKey: .data) + } catch let error as ResourceObjectDecodingError { + throw DocumentDecodingError(error) + } catch let error as ManyResourceBodyDecodingError { + throw DocumentDecodingError(error) + } catch let error as DecodingError { + throw DocumentDecodingError(error) + ?? error + } } - let maybeIncludes = try container.decodeIfPresent(Includes.self, forKey: .included) + let maybeIncludes: Includes? + do { + maybeIncludes = try container.decodeIfPresent(Includes.self, forKey: .included) + } catch let error as IncludesDecodingError { + throw DocumentDecodingError(error) + } // TODO come back to this and make robust @@ -569,7 +583,7 @@ extension Document.ErrorDocument: Decodable, CodableJSONAPIDocument document = try container.decode(Document.self) guard document.body.isError else { - throw JSONAPIDocumentDecodingError.foundSuccessDocumentWhenExpectingError + throw DocumentDecodingError.foundSuccessDocumentWhenExpectingError } } } @@ -582,7 +596,7 @@ extension Document.SuccessDocument: Decodable, CodableJSONAPIDocument document = try container.decode(Document.self) guard !document.body.isError else { - throw JSONAPIDocumentDecodingError.foundErrorDocumentWhenExpectingSuccess + throw DocumentDecodingError.foundErrorDocumentWhenExpectingSuccess } } } diff --git a/Sources/JSONAPI/Document/DocumentDecodingError.swift b/Sources/JSONAPI/Document/DocumentDecodingError.swift index 912a8f8..0e58fe3 100644 --- a/Sources/JSONAPI/Document/DocumentDecodingError.swift +++ b/Sources/JSONAPI/Document/DocumentDecodingError.swift @@ -1,11 +1,75 @@ // -// DocumentDecodingErro.swift +// DocumentDecodingError.swift // // // 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 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 + case other + + init(_ context: DecodingError.Context) { + if context.codingPath.contains(where: { $0.stringValue == "data" }) { + self = .data + } else { + self = .other + } + } + } +} + +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." + } + } } diff --git a/Sources/JSONAPI/Document/Includes.swift b/Sources/JSONAPI/Document/Includes.swift index e7a701b..2174c67 100644 --- a/Sources/JSONAPI/Document/Includes.swift +++ b/Sources/JSONAPI/Document/Includes.swift @@ -56,8 +56,14 @@ extension Includes: Decodable where I: Decodable { } var valueAggregator = [I]() + var idx = 0 while !container.isAtEnd { - valueAggregator.append(try container.decode(I.self)) + do { + valueAggregator.append(try container.decode(I.self)) + idx = idx + 1 + } catch let error { + throw IncludesDecodingError(error: error, idx: idx) + } } values = valueAggregator @@ -177,3 +183,20 @@ extension Includes where I: _Poly11 { 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)" + } +} diff --git a/Sources/JSONAPI/Document/ResourceBody.swift b/Sources/JSONAPI/Document/ResourceBody.swift index 25090c8..5a7c96f 100644 --- a/Sources/JSONAPI/Document/ResourceBody.swift +++ b/Sources/JSONAPI/Document/ResourceBody.swift @@ -125,8 +125,17 @@ extension ManyResourceBody: Decodable, CodableResourceBody where PrimaryResource public init(from decoder: Decoder) throws { var container = try decoder.unkeyedContainer() var valueAggregator = [PrimaryResource]() + var idx = 0 while !container.isAtEnd { - valueAggregator.append(try container.decode(PrimaryResource.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 } @@ -145,3 +154,9 @@ extension ManyResourceBody: CustomStringConvertible { return "PrimaryResourceBody(\(String(describing: values)))" } } + +// MARK: - DecodingError +public struct ManyResourceBodyDecodingError: Swift.Error, Equatable { + public let error: ResourceObjectDecodingError + public let idx: Int +} diff --git a/Sources/JSONAPI/Resource/Id.swift b/Sources/JSONAPI/Resource/Id.swift index d66e3d9..1957b87 100644 --- a/Sources/JSONAPI/Resource/Id.swift +++ b/Sources/JSONAPI/Resource/Id.swift @@ -45,7 +45,10 @@ public protocol OptionalId: Codable { init(rawValue: RawType) } -public protocol IdType: OptionalId, CustomStringConvertible, Hashable where RawType: RawIdType {} +/// marker protocol +public protocol AbstractId {} + +public protocol IdType: AbstractId, OptionalId, CustomStringConvertible, Hashable where RawType: RawIdType {} extension Optional: MaybeRawId where Wrapped: Codable & Equatable {} extension Optional: OptionalId where Wrapped: IdType { @@ -94,7 +97,7 @@ public struct Id: Equa } } -extension Id: Hashable, CustomStringConvertible, IdType where RawType: RawIdType { +extension Id: Hashable, CustomStringConvertible, AbstractId, IdType where RawType: RawIdType { public static func id(from rawValue: RawType) -> Id { return Id(rawValue: rawValue) } diff --git a/Sources/JSONAPI/Resource/Resource Object/ResourceObject.swift b/Sources/JSONAPI/Resource/Resource Object/ResourceObject.swift index e85d8f0..0604b00 100644 --- a/Sources/JSONAPI/Resource/Resource Object/ResourceObject.swift +++ b/Sources/JSONAPI/Resource/Resource Object/ResourceObject.swift @@ -96,10 +96,13 @@ extension ResourceObjectProxy { public static var jsonType: String { return Description.jsonType } } +/// A marker protocol. +public protocol AbstractResourceObject {} + /// ResourceObjectType is the protocol that ResourceObject conforms to. This /// protocol lets other types accept any ResourceObject as a generic /// specialization. -public protocol ResourceObjectType: ResourceObjectProxy, CodablePrimaryResource where Description: ResourceObjectDescription { +public protocol ResourceObjectType: AbstractResourceObject, ResourceObjectProxy, CodablePrimaryResource where Description: ResourceObjectDescription { associatedtype Meta: JSONAPI.Meta associatedtype Links: JSONAPI.Links @@ -414,7 +417,10 @@ public extension ResourceObject { let type = try container.decode(String.self, forKey: .type) guard ResourceObject.jsonType == type else { - throw JSONAPICodingError.typeMismatch(expected: Description.jsonType, found: type, path: decoder.codingPath) + throw ResourceObjectDecodingError( + expectedJSONAPIType: ResourceObject.jsonType, + found: type + ) } let maybeUnidentified = Unidentified() as? EntityRawIdType diff --git a/Sources/JSONAPI/Resource/Resource Object/ResourceObjectDecodingError.swift b/Sources/JSONAPI/Resource/Resource Object/ResourceObjectDecodingError.swift index d1ac49c..b644ce6 100644 --- a/Sources/JSONAPI/Resource/Resource Object/ResourceObjectDecodingError.swift +++ b/Sources/JSONAPI/Resource/Resource Object/ResourceObjectDecodingError.swift @@ -23,11 +23,13 @@ public struct ResourceObjectDecodingError: Swift.Error, Equatable { public enum Location: String, Equatable { case attributes case relationships + case type var singular: String { switch self { case .attributes: return "attribute" case .relationships: return "relationship" + case .type: return "type" } } } @@ -63,6 +65,12 @@ public struct ResourceObjectDecodingError: Swift.Error, Equatable { } } + init(expectedJSONAPIType: String, found: String) { + location = .type + subjectName = "self" + cause = .jsonTypeMismatch(expectedType: expectedJSONAPIType, foundType: found) + } + init(subjectName: String, cause: Cause, location: Location) { self.subjectName = subjectName self.cause = cause @@ -75,8 +83,17 @@ public struct ResourceObjectDecodingError: Swift.Error, Equatable { } static func context(path: [CodingKey]) -> (Location, name: String) { + let location: Location + if path.contains(where: { $0.stringValue == "attributes" }) { + location = .attributes + } else if path.contains(where: { $0.stringValue == "relationships" }) { + location = .relationships + } else { + location = .type + } + return ( - path.contains { $0.stringValue == "attributes" } ? .attributes : .relationships, + location, name: path.last?.stringValue ?? "unnamed" ) } @@ -94,6 +111,8 @@ extension ResourceObjectDecodingError: CustomStringConvertible { return "'\(subjectName)' \(location.singular) is not nullable but null." case .typeMismatch(expectedTypeName: let expected): return "'\(subjectName)' \(location.singular) is not a \(expected) as expected." + case .jsonTypeMismatch(expectedType: let expected, foundType: let found) where location == .type: + return "found JSON:API type \"\(found)\" but expected \"\(expected)\"" case .jsonTypeMismatch(expectedType: let expected, foundType: let found): return "'\(subjectName)' \(location.singular) is of JSON:API type \"\(found)\" but it was expected to be \"\(expected)\"" case .quantityMismatch(expected: let expected): diff --git a/Tests/JSONAPITests/Document/DocumentDecodingErrorTests.swift b/Tests/JSONAPITests/Document/DocumentDecodingErrorTests.swift new file mode 100644 index 0000000..055e007 --- /dev/null +++ b/Tests/JSONAPITests/Document/DocumentDecodingErrorTests.swift @@ -0,0 +1,189 @@ +// +// DocumentDecodingErrorTests.swift +// +// +// Created by Mathew Polzin on 11/10/19. +// + +import XCTest +import JSONAPI +import Poly + +final class DocumentDecodingErrorTests: XCTestCase { + func test_singlePrimaryResource_missing() { + XCTAssertThrowsError( + try testDecoder.decode( + Document, NoMetadata, NoLinks, NoIncludes, NoAPIDescription, UnknownJSONAPIError>.self, + from: single_document_null + ) + ) { error in + guard let docError = error as? DocumentDecodingError, + case .primaryResourceMissing = docError else { + XCTFail("Expected primary resource missing error. Got \(error)") + return + } + + XCTAssertEqual(String(describing: error), "Primary Resource missing.") + } + } + + func test_singlePrimaryResource_failure() { + XCTAssertThrowsError( + try testDecoder.decode( + Document, NoMetadata, NoLinks, NoIncludes, NoAPIDescription, UnknownJSONAPIError>.self, + from: single_document_no_includes_missing_relationship + ) + ) { error in + guard let docError = error as? DocumentDecodingError, + case .primaryResource = docError else { + XCTFail("Expected primary resource document error. Got \(error)") + return + } + + XCTAssertEqual(String(describing: error), "Primary Resource failed to parse because 'author' relationship is required and missing.") + } + } + + func test_manyPrimaryResource_missing() { + XCTAssertThrowsError( + try testDecoder.decode( + Document, NoMetadata, NoLinks, NoIncludes, NoAPIDescription, UnknownJSONAPIError>.self, + from: many_document_no_includes_data_is_null + ) + ) { error in + guard let docError = error as? DocumentDecodingError, + case .primaryResourcesMissing = docError else { + XCTFail("Expected primary resource missing error. Got \(error)") + return + } + + XCTAssertEqual(String(describing: error), "Primary Resources array missing.") + } + } + + func test_manyPrimaryResource_failure() { + XCTAssertThrowsError( + try testDecoder.decode( + Document, NoMetadata, NoLinks, NoIncludes, NoAPIDescription, UnknownJSONAPIError>.self, + from: many_document_no_includes_missing_relationship + ) + ) { error in + guard let docError = error as? DocumentDecodingError, + case .primaryResource = docError else { + XCTFail("Expected primary resource document error. Got \(error)") + return + } + + XCTAssertEqual(String(describing: error), "Primary Resource 2 failed to parse because 'author' relationship is required and missing.") + } + } + + func test_include_failure() { + XCTAssertThrowsError( + try testDecoder.decode( + Document, NoMetadata, NoLinks, Include1, NoAPIDescription, UnknownJSONAPIError>.self, + from: single_document_some_includes_wrong_type + ) + ) { error in + guard let docError = error as? DocumentDecodingError, + case .includes = docError else { + XCTFail("Expected primary resource document error. Got \(error)") + return + } + + XCTAssertEqual(String(describing: error), #"Include 3 failed to parse: found JSON:API type "not_an_author" but expected "authors""#) + } + } +} + +// MARK: - Test Types +extension DocumentDecodingErrorTests { + enum AuthorType: ResourceObjectDescription { + static var jsonType: String { return "authors" } + + typealias Attributes = NoAttributes + typealias Relationships = NoRelationships + } + + typealias Author = BasicEntity + + enum ArticleType: ResourceObjectDescription { + static var jsonType: String { return "articles" } + + typealias Attributes = NoAttributes + + struct Relationships: JSONAPI.Relationships { + let author: ToOneRelationship + } + } + + typealias Article = BasicEntity + + enum BookType: ResourceObjectDescription { + static var jsonType: String { return "books" } + + struct Attributes: JSONAPI.SparsableAttributes { + let pageCount: Attribute + + enum CodingKeys: String, SparsableCodingKey { + case pageCount + } + } + + struct Relationships: JSONAPI.Relationships { + let author: ToOneRelationship + let series: ToManyRelationship + } + } + + typealias Book = BasicEntity + + struct TestPageMetadata: JSONAPI.Meta { + let total: Int + let limit: Int + let offset: Int + } + + struct TestLinks: JSONAPI.Links { + let link: Link + let link2: Link + + struct TestMetadata: JSONAPI.Meta { + let hello: String + } + } + + typealias TestAPIDescription = APIDescription + + enum TestError: JSONAPIError { + case unknownError + case basic(BasicError) + + struct BasicError: Codable, Equatable { + let code: Int + let description: String + } + + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + + self = (try? .basic(container.decode(BasicError.self))) ?? .unknown + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + + switch self { + case .unknownError: + try container.encode("unknown") + case .basic(let error): + try container.encode(error) + } + } + + public static var unknown: Self { + return .unknownError + } + } +} + diff --git a/Tests/JSONAPITests/Document/stubs/DocumentStubs.swift b/Tests/JSONAPITests/Document/stubs/DocumentStubs.swift index 7455a0c..ed83ebf 100644 --- a/Tests/JSONAPITests/Document/stubs/DocumentStubs.swift +++ b/Tests/JSONAPITests/Document/stubs/DocumentStubs.swift @@ -37,6 +37,17 @@ let single_document_no_includes = """ } """.data(using: .utf8)! +let single_document_no_includes_missing_relationship = """ +{ + "data": { + "id": "1", + "type": "articles", + "relationships": { + } + } +} +""".data(using: .utf8)! + let single_document_no_includes_with_api_description = """ { "data": { @@ -247,6 +258,37 @@ let single_document_some_includes = """ } """.data(using: .utf8)! +let single_document_some_includes_wrong_type = """ +{ + "data": { + "id": "1", + "type": "articles", + "relationships": { + "author": { + "data": { + "type": "authors", + "id": "33" + } + } + } + }, + "included": [ + { + "id": "30", + "type": "authors" + }, + { + "id": "31", + "type": "authors" + }, + { + "id": "33", + "type": "not_an_author" + } + ] +} +""".data(using: .utf8)! + let single_document_some_includes_with_api_description = """ { "data": { @@ -452,6 +494,49 @@ let many_document_no_includes = """ } """.data(using: .utf8)! +let many_document_no_includes_data_is_null = """ +{ + "data": null +} +""".data(using: .utf8)! + +let many_document_no_includes_missing_relationship = """ +{ + "data": [ + { + "id": "1", + "type": "articles", + "relationships": { + "author": { + "data": { + "type": "authors", + "id": "33" + } + } + } + }, + { + "id": "2", + "type": "articles", + "relationships": { + } + }, + { + "id": "3", + "type": "articles", + "relationships": { + "author": { + "data": { + "type": "authors", + "id": "11" + } + } + } + } + ] +} +""".data(using: .utf8)! + let many_document_no_includes_with_api_description = """ { "data": [ diff --git a/Tests/JSONAPITests/ResourceObject/ResourceObjectDecodingErrorTests.swift b/Tests/JSONAPITests/ResourceObject/ResourceObjectDecodingErrorTests.swift index 0891d23..d0abb6e 100644 --- a/Tests/JSONAPITests/ResourceObject/ResourceObjectDecodingErrorTests.swift +++ b/Tests/JSONAPITests/ResourceObject/ResourceObjectDecodingErrorTests.swift @@ -287,6 +287,30 @@ extension ResourceObjectDecodingErrorTests { } } +// MARK: - JSON:API Type +extension ResourceObjectDecodingErrorTests { + func test_wrongType() { + XCTAssertThrowsError(try testDecoder.decode( + TestEntity2.self, + from: entity_is_wrong_type + )) { error in + XCTAssertEqual( + error as? ResourceObjectDecodingError, + ResourceObjectDecodingError( + subjectName: "self", + cause: .jsonTypeMismatch(expectedType: "fourteenth_test_entities", foundType: "not_correct_type"), + location: .type + ) + ) + + XCTAssertEqual( + (error as? ResourceObjectDecodingError)?.description, + #"found JSON:API type "not_correct_type" but expected "fourteenth_test_entities""# + ) + } + } +} + // MARK: - Test Types extension ResourceObjectDecodingErrorTests { enum TestEntityType: ResourceObjectDescription { diff --git a/Tests/JSONAPITests/ResourceObject/stubs/ResourceObjectStubs.swift b/Tests/JSONAPITests/ResourceObject/stubs/ResourceObjectStubs.swift index 9fe7355..861ea1c 100644 --- a/Tests/JSONAPITests/ResourceObject/stubs/ResourceObjectStubs.swift +++ b/Tests/JSONAPITests/ResourceObject/stubs/ResourceObjectStubs.swift @@ -533,6 +533,17 @@ let entity_attribute_is_wrong_type3 = """ } """.data(using: .utf8)! +let entity_is_wrong_type = """ +{ + "id": "1", + "type": "not_correct_type", + "attributes": { + "required": "hello", + "yetAnother": 101 + } +} +""".data(using: .utf8)! + let entity_attributes_entirely_missing = """ { "id": "1", diff --git a/Tests/JSONAPITests/XCTestManifests.swift b/Tests/JSONAPITests/XCTestManifests.swift index 0b2c004..0768089 100644 --- a/Tests/JSONAPITests/XCTestManifests.swift +++ b/Tests/JSONAPITests/XCTestManifests.swift @@ -85,6 +85,19 @@ extension CustomAttributesTests { ] } +extension DocumentDecodingErrorTests { + // DO NOT MODIFY: This is autogenerated, use: + // `swift test --generate-linuxmain` + // to regenerate. + static let __allTests__DocumentDecodingErrorTests = [ + ("test_include_failure", test_include_failure), + ("test_manyPrimaryResource_failure", test_manyPrimaryResource_failure), + ("test_manyPrimaryResource_missing", test_manyPrimaryResource_missing), + ("test_singlePrimaryResource_failure", test_singlePrimaryResource_failure), + ("test_singlePrimaryResource_missing", test_singlePrimaryResource_missing), + ] +} + extension DocumentTests { // DO NOT MODIFY: This is autogenerated, use: // `swift test --generate-linuxmain` @@ -379,6 +392,7 @@ extension ResourceObjectDecodingErrorTests { ("test_required_attribute", test_required_attribute), ("test_required_relationship", test_required_relationship), ("test_twoOneVsToMany_relationship", test_twoOneVsToMany_relationship), + ("test_wrongType", test_wrongType), ] } @@ -542,6 +556,7 @@ public func __allTests() -> [XCTestCaseEntry] { testCase(BasicJSONAPIErrorTests.__allTests__BasicJSONAPIErrorTests), testCase(ComputedPropertiesTests.__allTests__ComputedPropertiesTests), testCase(CustomAttributesTests.__allTests__CustomAttributesTests), + testCase(DocumentDecodingErrorTests.__allTests__DocumentDecodingErrorTests), testCase(DocumentTests.__allTests__DocumentTests), testCase(EmptyObjectDecoderTests.__allTests__EmptyObjectDecoderTests), testCase(GenericJSONAPIErrorTests.__allTests__GenericJSONAPIErrorTests), From e9a3b35dc7ae27e1a6390056a276d71064e8b0e6 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Sun, 10 Nov 2019 23:43:35 -0800 Subject: [PATCH 109/235] improve error messages for poly include types --- Package.resolved | 4 +-- Package.swift | 2 +- Sources/JSONAPI/Document/Includes.swift | 33 +++++++++++++++++++ .../Document/DocumentDecodingErrorTests.swift | 26 +++++++++++++++ 4 files changed, 62 insertions(+), 3 deletions(-) diff --git a/Package.resolved b/Package.resolved index cc81a53..add4b9a 100644 --- a/Package.resolved +++ b/Package.resolved @@ -6,8 +6,8 @@ "repositoryURL": "https://github.com/mattpolzin/Poly.git", "state": { "branch": null, - "revision": "18cd995be5c28c4dfdc1464e54ee0efb03e215bf", - "version": "2.3.0" + "revision": "0c9c08204142babc480938d704a23513d11420e5", + "version": "2.3.1" } } ] diff --git a/Package.swift b/Package.swift index 5b780cb..9b3e038 100644 --- a/Package.swift +++ b/Package.swift @@ -18,7 +18,7 @@ let package = Package( targets: ["JSONAPITesting"]) ], dependencies: [ - .package(url: "https://github.com/mattpolzin/Poly.git", .upToNextMajor(from: "2.3.0")), + .package(url: "https://github.com/mattpolzin/Poly.git", .upToNextMajor(from: "2.3.1")), ], targets: [ .target( diff --git a/Sources/JSONAPI/Document/Includes.swift b/Sources/JSONAPI/Document/Includes.swift index 2174c67..32f90ea 100644 --- a/Sources/JSONAPI/Document/Includes.swift +++ b/Sources/JSONAPI/Document/Includes.swift @@ -61,6 +61,27 @@ extension Includes: Decodable where I: Decodable { 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) } @@ -200,3 +221,15 @@ extension IncludesDecodingError: CustomStringConvertible { 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") + } +} diff --git a/Tests/JSONAPITests/Document/DocumentDecodingErrorTests.swift b/Tests/JSONAPITests/Document/DocumentDecodingErrorTests.swift index 055e007..30e2740 100644 --- a/Tests/JSONAPITests/Document/DocumentDecodingErrorTests.swift +++ b/Tests/JSONAPITests/Document/DocumentDecodingErrorTests.swift @@ -94,6 +94,32 @@ final class DocumentDecodingErrorTests: XCTestCase { XCTAssertEqual(String(describing: error), #"Include 3 failed to parse: found JSON:API type "not_an_author" but expected "authors""#) } } + + func test_include_failure2() { + XCTAssertThrowsError( + try testDecoder.decode( + Document, NoMetadata, NoLinks, Include2, NoAPIDescription, UnknownJSONAPIError>.self, + from: single_document_some_includes_wrong_type + ) + ) { error in + guard let docError = error as? DocumentDecodingError, + case .includes = docError else { + XCTFail("Expected primary resource document error. Got \(error)") + return + } + + XCTAssertEqual(String(describing: error), +#""" +Include 3 failed to parse: +Could not have been Include Type 1 because: +found JSON:API type "not_an_author" but expected "articles" + +Could not have been Include Type 2 because: +found JSON:API type "not_an_author" but expected "authors" +"""# + ) + } + } } // MARK: - Test Types From 54551617b4d56e81456feeb5b17e387df9766fbe Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Tue, 12 Nov 2019 18:34:34 -0800 Subject: [PATCH 110/235] Add more errors for the resource object type property --- .../Document/DocumentDecodingError.swift | 10 +- .../Resource Object/ResourceObject.swift | 8 +- .../ResourceObjectDecodingError.swift | 13 +- .../ResourceObjectCompareTests.swift | 1 - .../ResourceObjectDecodingErrorTests.swift | 125 +++++++++++++++++- .../stubs/ResourceObjectStubs.swift | 55 +++++++- .../SparseFieldEncoderTests.swift | 1 - 7 files changed, 193 insertions(+), 20 deletions(-) diff --git a/Sources/JSONAPI/Document/DocumentDecodingError.swift b/Sources/JSONAPI/Document/DocumentDecodingError.swift index 0e58fe3..2bcfa60 100644 --- a/Sources/JSONAPI/Document/DocumentDecodingError.swift +++ b/Sources/JSONAPI/Document/DocumentDecodingError.swift @@ -40,14 +40,12 @@ public enum DocumentDecodingError: Swift.Error, Equatable { private enum Location: Equatable { case data - case other - init(_ context: DecodingError.Context) { - if context.codingPath.contains(where: { $0.stringValue == "data" }) { - self = .data - } else { - self = .other + init?(_ context: DecodingError.Context) { + guard context.codingPath.contains(where: { $0.stringValue == "data" }) else { + return nil } + self = .data } } } diff --git a/Sources/JSONAPI/Resource/Resource Object/ResourceObject.swift b/Sources/JSONAPI/Resource/Resource Object/ResourceObject.swift index 0604b00..83f0d1c 100644 --- a/Sources/JSONAPI/Resource/Resource Object/ResourceObject.swift +++ b/Sources/JSONAPI/Resource/Resource Object/ResourceObject.swift @@ -414,7 +414,13 @@ public extension ResourceObject { init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: ResourceObjectCodingKeys.self) - let type = try container.decode(String.self, forKey: .type) + let type: String + do { + type = try container.decode(String.self, forKey: .type) + } catch let error as DecodingError { + throw ResourceObjectDecodingError(error) + ?? error + } guard ResourceObject.jsonType == type else { throw ResourceObjectDecodingError( diff --git a/Sources/JSONAPI/Resource/Resource Object/ResourceObjectDecodingError.swift b/Sources/JSONAPI/Resource/Resource Object/ResourceObjectDecodingError.swift index b644ce6..c61e9be 100644 --- a/Sources/JSONAPI/Resource/Resource Object/ResourceObjectDecodingError.swift +++ b/Sources/JSONAPI/Resource/Resource Object/ResourceObjectDecodingError.swift @@ -102,13 +102,18 @@ public struct ResourceObjectDecodingError: Swift.Error, Equatable { extension ResourceObjectDecodingError: CustomStringConvertible { public var description: String { switch cause { + case .keyNotFound where subjectName == ResourceObjectDecodingError.entireObject: + return "\(location) object is required and missing." + case .keyNotFound where location == .type: + return "'type' (a.k.a. JSON:API type name) is required and missing." case .keyNotFound: - if subjectName == ResourceObjectDecodingError.entireObject { - return "\(location) object is required and missing." - } return "'\(subjectName)' \(location.singular) is required and missing." + case .valueNotFound where location == .type: + return "'type' (a.k.a. JSON:API type name) is not nullable but null was found." case .valueNotFound: - return "'\(subjectName)' \(location.singular) is not nullable but null." + return "'\(subjectName)' \(location.singular) is not nullable but null was found." + case .typeMismatch(expectedTypeName: let expected) where location == .type: + return "'type' (a.k.a. the JSON:API type name) is not a \(expected) as expected." case .typeMismatch(expectedTypeName: let expected): return "'\(subjectName)' \(location.singular) is not a \(expected) as expected." case .jsonTypeMismatch(expectedType: let expected, foundType: let found) where location == .type: diff --git a/Tests/JSONAPITestingTests/Comparisons/ResourceObjectCompareTests.swift b/Tests/JSONAPITestingTests/Comparisons/ResourceObjectCompareTests.swift index 2c40e87..fd43103 100644 --- a/Tests/JSONAPITestingTests/Comparisons/ResourceObjectCompareTests.swift +++ b/Tests/JSONAPITestingTests/Comparisons/ResourceObjectCompareTests.swift @@ -11,7 +11,6 @@ import JSONAPITesting final class ResourceObjectCompareTests: XCTestCase { func test_same() { - print(test1.compare(to: test1).differences) XCTAssertTrue(test1.compare(to: test1).differences.isEmpty) XCTAssertTrue(test2.compare(to: test2).differences.isEmpty) } diff --git a/Tests/JSONAPITests/ResourceObject/ResourceObjectDecodingErrorTests.swift b/Tests/JSONAPITests/ResourceObject/ResourceObjectDecodingErrorTests.swift index d0abb6e..ce8e334 100644 --- a/Tests/JSONAPITests/ResourceObject/ResourceObjectDecodingErrorTests.swift +++ b/Tests/JSONAPITests/ResourceObject/ResourceObjectDecodingErrorTests.swift @@ -68,7 +68,7 @@ final class ResourceObjectDecodingErrorTests: XCTestCase { XCTAssertEqual( (error as? ResourceObjectDecodingError)?.description, - "'required' relationship is not nullable but null." + "'required' relationship is not nullable but null was found." ) } } @@ -89,7 +89,7 @@ final class ResourceObjectDecodingErrorTests: XCTestCase { XCTAssertEqual( (error as? ResourceObjectDecodingError)?.description, - "'required' relationship is not nullable but null." + "'required' relationship is not nullable but null was found." ) } } @@ -99,7 +99,6 @@ final class ResourceObjectDecodingErrorTests: XCTestCase { TestEntity.self, from: entity_relationship_is_wrong_type )) { error in - print(error) XCTAssertEqual( error as? ResourceObjectDecodingError, ResourceObjectDecodingError( @@ -218,7 +217,7 @@ extension ResourceObjectDecodingErrorTests { XCTAssertEqual( (error as? ResourceObjectDecodingError)?.description, - "'required' attribute is not nullable but null." + "'required' attribute is not nullable but null was found." ) } } @@ -285,11 +284,44 @@ extension ResourceObjectDecodingErrorTests { ) } } + + func test_transformed_attribute() { + XCTAssertThrowsError(try testDecoder.decode( + TestEntity2.self, + from: entity_attribute_is_wrong_type4 + )) { error in + XCTAssertEqual( + error as? ResourceObjectDecodingError, + ResourceObjectDecodingError( + subjectName: "transformed", + cause: .typeMismatch(expectedTypeName: String(describing: Int.self)), + location: .attributes + ) + ) + + XCTAssertEqual( + (error as? ResourceObjectDecodingError)?.description, + "'transformed' attribute is not a Int as expected." + ) + } + } + + func test_transformed_attribute2() { + XCTAssertThrowsError(try testDecoder.decode( + TestEntity2.self, + from: entity_attribute_always_fails + )) { error in + XCTAssertEqual( + String(describing: error), + "Error: Always Fails" + ) + } + } } // MARK: - JSON:API Type extension ResourceObjectDecodingErrorTests { - func test_wrongType() { + func test_wrongJSONAPIType() { XCTAssertThrowsError(try testDecoder.decode( TestEntity2.self, from: entity_is_wrong_type @@ -309,6 +341,69 @@ extension ResourceObjectDecodingErrorTests { ) } } + + func test_wrongDecodedType() { + XCTAssertThrowsError(try testDecoder.decode( + TestEntity2.self, + from: entity_type_is_wrong_type + )) { error in + XCTAssertEqual( + error as? ResourceObjectDecodingError, + ResourceObjectDecodingError( + subjectName: "type", + cause: .typeMismatch(expectedTypeName: String(describing: String.self)), + location: .type + ) + ) + + XCTAssertEqual( + (error as? ResourceObjectDecodingError)?.description, + #"'type' (a.k.a. the JSON:API type name) is not a String as expected."# + ) + } + } + + func test_type_missing() { + XCTAssertThrowsError(try testDecoder.decode( + TestEntity2.self, + from: entity_type_is_missing + )) { error in + XCTAssertEqual( + error as? ResourceObjectDecodingError, + ResourceObjectDecodingError( + subjectName: "type", + cause: .keyNotFound, + location: .type + ) + ) + + XCTAssertEqual( + (error as? ResourceObjectDecodingError)?.description, + #"'type' (a.k.a. JSON:API type name) is required and missing."# + ) + } + } + + func test_type_null() { + XCTAssertThrowsError(try testDecoder.decode( + TestEntity2.self, + from: entity_type_is_null + )) { error in + XCTAssertEqual( + error as? ResourceObjectDecodingError, + ResourceObjectDecodingError( + subjectName: "type", + cause: .valueNotFound, + location: .type + ) + ) + + XCTAssertEqual( + (error as? ResourceObjectDecodingError)?.description, + #"'type' (a.k.a. JSON:API type name) is not nullable but null was found."# + ) + } + } } // MARK: - Test Types @@ -335,10 +430,30 @@ extension ResourceObjectDecodingErrorTests { let required: Attribute let other: Attribute? let yetAnother: Attribute? + let transformed: TransformedAttribute? + let transformed2: TransformedAttribute? } typealias Relationships = NoRelationships } typealias TestEntity2 = BasicEntity + + enum IntToString: Transformer { + static func transform(_ value: Int) throws -> String { + return "\(value)" + } + typealias From = Int + typealias To = String + } + + enum AlwaysFails: Transformer { + static func transform(_ value: String) throws -> String { + throw Error() + } + + struct Error: Swift.Error, CustomStringConvertible { + let description: String = "Error: Always Fails" + } + } } diff --git a/Tests/JSONAPITests/ResourceObject/stubs/ResourceObjectStubs.swift b/Tests/JSONAPITests/ResourceObject/stubs/ResourceObjectStubs.swift index 861ea1c..181826f 100644 --- a/Tests/JSONAPITests/ResourceObject/stubs/ResourceObjectStubs.swift +++ b/Tests/JSONAPITests/ResourceObject/stubs/ResourceObjectStubs.swift @@ -533,6 +533,35 @@ let entity_attribute_is_wrong_type3 = """ } """.data(using: .utf8)! +let entity_attribute_is_wrong_type4 = """ +{ + "id": "1", + "type": "fourteenth_test_entities", + "attributes": { + "required": "hello", + "transformed": "world" + } +} +""".data(using: .utf8)! + +let entity_attribute_always_fails = """ +{ + "id": "1", + "type": "fourteenth_test_entities", + "attributes": { + "required": "hello", + "transformed2": "world" + } +} +""".data(using: .utf8)! + +let entity_attributes_entirely_missing = """ +{ + "id": "1", + "type": "fourteenth_test_entities" +} +""".data(using: .utf8)! + let entity_is_wrong_type = """ { "id": "1", @@ -544,10 +573,32 @@ let entity_is_wrong_type = """ } """.data(using: .utf8)! -let entity_attributes_entirely_missing = """ +let entity_type_is_wrong_type = """ { "id": "1", - "type": "fourteenth_test_entities" + "type": 10, + "attributes": { + "required": "hello" + } +} +""".data(using: .utf8)! + +let entity_type_is_missing = """ +{ + "id": "1", + "attributes": { + "required": "hello" + } +} +""".data(using: .utf8)! + +let entity_type_is_null = """ +{ + "id": "1", + "type": null, + "attributes": { + "required": "hello" + } } """.data(using: .utf8)! diff --git a/Tests/JSONAPITests/SparseFields/SparseFieldEncoderTests.swift b/Tests/JSONAPITests/SparseFields/SparseFieldEncoderTests.swift index c6192f8..2aa9fb2 100644 --- a/Tests/JSONAPITests/SparseFields/SparseFieldEncoderTests.swift +++ b/Tests/JSONAPITests/SparseFields/SparseFieldEncoderTests.swift @@ -17,7 +17,6 @@ class SparseFieldEncoderTests: XCTestCase { do { let _ = try encoder.encode(Wrapper()) } catch let err as Wrapper.OuterFail.FailError { - print(err.path) XCTAssertEqual(err.path.first as? Wrapper.OuterFail.CodingKeys, Wrapper.OuterFail.CodingKeys.inner) } catch { XCTFail("received unexpected error during test") From ae855c85eef0f62894f655ddb34e415636dab46a Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Fri, 15 Nov 2019 08:30:17 -0800 Subject: [PATCH 111/235] going through and fleshing out tests. minor adjustments and bug fixes. --- Sources/JSONAPI/Document/Document.swift | 18 ++- Sources/JSONAPI/Meta/Links.swift | 2 +- .../ResourceObjectDecodingError.swift | 4 +- .../Comparisons/ArrayCompare.swift | 11 -- .../Comparisons/DocumentCompare.swift | 26 +++- .../Comparisons/RelationshipsCompare.swift | 2 +- .../Comparisons/ArrayCompareTests.swift | 86 +++++++++++ .../Comparisons/AttributesCompareTests.swift | 26 +++- .../Comparisons/DocumentCompareTests.swift | 95 +++++++++++- .../RelationshipsCompareTests.swift | 20 ++- .../ResourceObjectCompareTests.swift | 24 ++- .../Test Helpers/EntityTestTypes.swift | 2 + .../Attribute/AttributeTests.swift | 8 + .../Document/DocumentDecodingErrorTests.swift | 22 +++ .../JSONAPITests/Document/DocumentTests.swift | 2 - .../SuccessAndErrorDocumentTests.swift | 139 ++++++++++++++++++ .../Error/GenericJSONAPIErrorTests.swift | 1 + .../Includes/IncludesDecodingErrorTests.swift | 109 ++++++++++++++ .../Includes/stubs/IncludeStubs.swift | 8 +- Tests/JSONAPITests/Poly/PolyProxyTests.swift | 6 + .../Test Helpers/EncodeDecode.swift | 3 +- .../Test Helpers/EncodedAttributeTest.swift | 14 +- .../Test Helpers/EntityTestTypes.swift | 2 + 23 files changed, 573 insertions(+), 57 deletions(-) create mode 100644 Tests/JSONAPITestingTests/Comparisons/ArrayCompareTests.swift create mode 100644 Tests/JSONAPITests/Document/SuccessAndErrorDocumentTests.swift create mode 100644 Tests/JSONAPITests/Includes/IncludesDecodingErrorTests.swift diff --git a/Sources/JSONAPI/Document/Document.swift b/Sources/JSONAPI/Document/Document.swift index 34c32a4..1343559 100644 --- a/Sources/JSONAPI/Document/Document.swift +++ b/Sources/JSONAPI/Document/Document.swift @@ -494,6 +494,10 @@ extension Document { public static func ==(lhs: Document, rhs: ErrorDocument) -> Bool { return lhs == rhs.document } + + public static func ==(lhs: ErrorDocument, rhs: Document) -> Bool { + return lhs.document == rhs + } } /// A Document that only supports success bodies. This is useful if you wish to pass around a @@ -534,7 +538,7 @@ extension Document { /// `nil` if the Document is an error response. Otherwise, /// a structure containing the primary resource, any included /// resources, metadata, and links. - var data: BodyData? { + public var data: BodyData? { return document.body.data } @@ -545,7 +549,7 @@ extension Document { /// resources dependening on the `PrimaryResourceBody` type. /// /// See `SingleResourceBody` and `ManyResourceBody`. - var primaryResource: PrimaryResourceBody? { + public var primaryResource: PrimaryResourceBody? { return document.body.primaryResource } @@ -553,25 +557,29 @@ extension Document { /// /// `nil` if the Document is an error document. Otherwise, /// zero or more includes. - var includes: Includes? { + public var includes: Includes? { return document.body.includes } /// The metadata for the error or data document or `nil` if /// no metadata is found. - var meta: MetaType? { + public var meta: MetaType? { return document.body.meta } /// The links for the error or data document or `nil` if /// no links are found. - var links: LinksType? { + public var links: LinksType? { return document.body.links } public static func ==(lhs: Document, rhs: SuccessDocument) -> Bool { return lhs == rhs.document } + + public static func ==(lhs: SuccessDocument, rhs: Document) -> Bool { + return lhs.document == rhs + } } } diff --git a/Sources/JSONAPI/Meta/Links.swift b/Sources/JSONAPI/Meta/Links.swift index 876995f..ab2b7e1 100644 --- a/Sources/JSONAPI/Meta/Links.swift +++ b/Sources/JSONAPI/Meta/Links.swift @@ -5,7 +5,7 @@ // 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 {} /// Use NoLinks where no links should belong to a JSON API component diff --git a/Sources/JSONAPI/Resource/Resource Object/ResourceObjectDecodingError.swift b/Sources/JSONAPI/Resource/Resource Object/ResourceObjectDecodingError.swift index c61e9be..a79f5a1 100644 --- a/Sources/JSONAPI/Resource/Resource Object/ResourceObjectDecodingError.swift +++ b/Sources/JSONAPI/Resource/Resource Object/ResourceObjectDecodingError.swift @@ -109,11 +109,11 @@ extension ResourceObjectDecodingError: CustomStringConvertible { case .keyNotFound: return "'\(subjectName)' \(location.singular) is required and missing." case .valueNotFound where location == .type: - return "'type' (a.k.a. JSON:API type name) is not nullable but null was found." + return "'\(location.singular)' (a.k.a. JSON:API type name) is not nullable but null was found." case .valueNotFound: return "'\(subjectName)' \(location.singular) is not nullable but null was found." case .typeMismatch(expectedTypeName: let expected) where location == .type: - return "'type' (a.k.a. the JSON:API type name) is not a \(expected) as expected." + return "'\(location.singular)' (a.k.a. the JSON:API type name) is not a \(expected) as expected." case .typeMismatch(expectedTypeName: let expected): return "'\(subjectName)' \(location.singular) is not a \(expected) as expected." case .jsonTypeMismatch(expectedType: let expected, foundType: let found) where location == .type: diff --git a/Sources/JSONAPITesting/Comparisons/ArrayCompare.swift b/Sources/JSONAPITesting/Comparisons/ArrayCompare.swift index a1f7e5c..f93ab62 100644 --- a/Sources/JSONAPITesting/Comparisons/ArrayCompare.swift +++ b/Sources/JSONAPITesting/Comparisons/ArrayCompare.swift @@ -14,17 +14,6 @@ public enum ArrayElementComparison: Equatable, CustomStringConvertible { case differentValues(String, String) case prebuilt(String) - public init(sameTypeComparison: BasicComparison) { - switch sameTypeComparison { - case .same: - self = .same - case .different(let one, let two): - self = .differentValues(one, two) - case .prebuilt(let str): - self = .prebuilt(str) - } - } - public init(resourceObjectComparison: ResourceObjectComparison) { guard !resourceObjectComparison.isSame else { self = .same diff --git a/Sources/JSONAPITesting/Comparisons/DocumentCompare.swift b/Sources/JSONAPITesting/Comparisons/DocumentCompare.swift index d56c84d..4cee720 100644 --- a/Sources/JSONAPITesting/Comparisons/DocumentCompare.swift +++ b/Sources/JSONAPITesting/Comparisons/DocumentCompare.swift @@ -33,10 +33,10 @@ public enum BodyComparison: Equatable, CustomStringConvertible { case differentErrors(ErrorComparison) case differentData(DocumentDataComparison) - public typealias ErrorComparison = [BasicComparison] + public typealias ErrorComparison = [String: BasicComparison] static func compare(errors errors1: [E], _ meta1: M?, _ links1: L?, with errors2: [E], _ meta2: M?, _ links2: L?) -> ErrorComparison { - return errors1.compare( + let errorComparisons = errors1.compare( to: errors2, using: { error1, error2 in guard error1 != error2 else { @@ -48,9 +48,19 @@ public enum BodyComparison: Equatable, CustomStringConvertible { String(describing: error2) ) } - ).map(BasicComparison.init) + [ - BasicComparison(meta1, meta2), - BasicComparison(links1, links2) + ).map(BasicComparison.init) + .filter { !$0.isSame } + .map { $0.rawValue } + .joined(separator: ", ") + + let errorComparisonString = errorComparisons.isEmpty + ? nil + : errorComparisons + + return [ + "Errors": errorComparisonString.map { BasicComparison.prebuilt("(\($0))") } ?? .same, + "Metadata": BasicComparison(meta1, meta2), + "Links": BasicComparison(links1, links2) ] } @@ -67,8 +77,8 @@ public enum BodyComparison: Equatable, CustomStringConvertible { return "\(left) ≠ \(right)" case .differentErrors(let comparisons): return comparisons - .filter { !$0.isSame } - .map { $0.rawValue } + .filter { !$0.value.isSame } + .map { "\($0.key): \($0.value.rawValue)" } .sorted() .joined(separator: ", ") case .differentData(let comparison): @@ -104,7 +114,7 @@ extension DocumentBody where Self: Equatable, PrimaryResourceBody: TestableResou return .differentErrors( BodyComparison.compare( errors: errors1, meta, links, - with: errors2, meta, links + with: errors2, other.meta, other.links ) ) } diff --git a/Sources/JSONAPITesting/Comparisons/RelationshipsCompare.swift b/Sources/JSONAPITesting/Comparisons/RelationshipsCompare.swift index 3bfa7d6..0c24386 100644 --- a/Sources/JSONAPITesting/Comparisons/RelationshipsCompare.swift +++ b/Sources/JSONAPITesting/Comparisons/RelationshipsCompare.swift @@ -1,5 +1,5 @@ // -// File.swift +// RelationshipsCompare.swift // // // Created by Mathew Polzin on 11/3/19. diff --git a/Tests/JSONAPITestingTests/Comparisons/ArrayCompareTests.swift b/Tests/JSONAPITestingTests/Comparisons/ArrayCompareTests.swift new file mode 100644 index 0000000..fccf84c --- /dev/null +++ b/Tests/JSONAPITestingTests/Comparisons/ArrayCompareTests.swift @@ -0,0 +1,86 @@ +// +// ArrayCompareTests.swift +// JSONAPITestingTests +// +// Created by Mathew Polzin on 11/14/19. +// + +import XCTest +@testable import JSONAPITesting + +final class ArrayCompareTests: XCTestCase { + func test_same() { + let arr1 = ["a", "b", "c"] + let arr2 = ["a", "b", "c"] + + let comparison = arr1.compare(to: arr2) { str1, str2 in + str1 == str2 ? .same : .differentValues(str1, str2) + } + + XCTAssertEqual( + comparison, + [.same, .same, .same] + ) + + XCTAssertEqual(comparison.map { $0.description }, ["same", "same", "same"]) + + XCTAssertEqual(comparison.map(BasicComparison.init(reducing:)), [.same, .same, .same]) + + XCTAssertEqual(comparison.map(BasicComparison.init(reducing:)).map { $0.description }, ["same", "same", "same"]) + } + + func test_differentLengths() { + let arr1 = ["a", "b", "c"] + let arr2 = ["a", "b"] + + let comparison1 = arr1.compare(to: arr2) { str1, str2 in + str1 == str2 ? .same : .differentValues(str1, str2) + } + + XCTAssertEqual( + comparison1, + [.same, .same, .missing] + ) + + XCTAssertEqual(comparison1.map { $0.description }, ["same", "same", "missing"]) + + XCTAssertEqual(comparison1.map(BasicComparison.init(reducing:)), [.same, .same, .different("array length 1", "array length 2")]) + + let comparison2 = arr2.compare(to: arr1) { str1, str2 in + str1 == str2 ? .same : .differentValues(str1, str2) + } + + XCTAssertEqual( + comparison2, + [.same, .same, .missing] + ) + + XCTAssertEqual(comparison2.map { $0.description }, ["same", "same", "missing"]) + + XCTAssertEqual(comparison2.map(BasicComparison.init(reducing:)), [.same, .same, .different("array length 1", "array length 2")]) + } + + func test_differentValues() { + let arr1 = ["c", "b", "a"] + let arr2 = ["a", "b", "c"] + + let comparison = arr1.compare(to: arr2) { str1, str2 in + str1 == str2 ? .same : .differentValues(str1, str2) + } + + XCTAssertEqual( + comparison, + [.differentValues("c", "a"), .same, .differentValues("a", "c")] + ) + + XCTAssertEqual(comparison.map { $0.description }, ["c ≠ a", "same", "a ≠ c"]) + } + + func test_reducePrebuilt() { + let prebuilt = ArrayElementComparison.prebuilt("hello world") + + XCTAssertEqual(BasicComparison(reducing: prebuilt), .prebuilt("hello world")) + + XCTAssertEqual(BasicComparison(reducing: prebuilt).description, "hello world") + } +} diff --git a/Tests/JSONAPITestingTests/Comparisons/AttributesCompareTests.swift b/Tests/JSONAPITestingTests/Comparisons/AttributesCompareTests.swift index e7a7dca..e54031c 100644 --- a/Tests/JSONAPITestingTests/Comparisons/AttributesCompareTests.swift +++ b/Tests/JSONAPITestingTests/Comparisons/AttributesCompareTests.swift @@ -10,13 +10,14 @@ import JSONAPI import JSONAPITesting final class AttributesCompareTests: XCTestCase { - func test_sameAttributes() { + func test_sameAttributes() throws { let attr1 = TestAttributes( string: "hello world", int: 10, bool: true, double: 105.4, - struct: .init(value: .init()) + struct: .init(value: .init()), + transformed: try .init(rawValue: 10) ) let attr2 = attr1 @@ -25,24 +26,27 @@ final class AttributesCompareTests: XCTestCase { "int": .same, "bool": .same, "double": .same, - "struct": .same + "struct": .same, + "transformed": .same ]) } - func test_differentAttributes() { + func test_differentAttributes() throws { let attr1 = TestAttributes( string: "hello world", int: 10, bool: true, double: 105.4, - struct: .init(value: .init()) + struct: .init(value: .init()), + transformed: try .init(rawValue: 10) ) let attr2 = TestAttributes( string: "hello", int: 11, bool: false, double: 1.4, - struct: .init(value: .init(val: "there")) + struct: .init(value: .init(val: "there")), + transformed: try .init(rawValue: 11) ) XCTAssertEqual(attr1.compare(to: attr2), [ @@ -50,7 +54,8 @@ final class AttributesCompareTests: XCTestCase { "int": .different("10", "11"), "bool": .different("true", "false"), "double": .different("105.4", "1.4"), - "struct": .different("string: hello", "string: there") + "struct": .different("string: hello", "string: there"), + "transformed": .different("10", "11") ]) } } @@ -61,6 +66,7 @@ private struct TestAttributes: JSONAPI.Attributes { let bool: Attribute let double: Attribute let `struct`: Attribute + let transformed: TransformedAttribute struct Struct: Equatable, Codable, CustomStringConvertible { let string: String @@ -72,3 +78,9 @@ private struct TestAttributes: JSONAPI.Attributes { var description: String { return "string: \(string)" } } } + +private enum TestTransformer: Transformer { + static func transform(_ value: Int) throws -> String { + return "\(value)" + } +} diff --git a/Tests/JSONAPITestingTests/Comparisons/DocumentCompareTests.swift b/Tests/JSONAPITestingTests/Comparisons/DocumentCompareTests.swift index 0d19a5c..9cbf93f 100644 --- a/Tests/JSONAPITestingTests/Comparisons/DocumentCompareTests.swift +++ b/Tests/JSONAPITestingTests/Comparisons/DocumentCompareTests.swift @@ -34,8 +34,33 @@ final class DocumentCompareTests: XCTestCase { } func test_differentErrors() { - XCTAssertEqual(d2.compare(to: d4).differences, [ - "Body": "status: 500, title: Internal Error ≠ status: 404, title: Not Found" + let comparison = d2.compare(to: d4) + XCTAssertEqual(comparison.differences, [ + "Body": "Errors: (status: 500, title: Internal Error ≠ status: 404, title: Not Found)" + ]) + + XCTAssertEqual(String(describing: comparison), "(Body: Errors: (status: 500, title: Internal Error ≠ status: 404, title: Not Found))") + } + + func test_sameErrorsDifferentMetadata() { + let errors = [ + BasicJSONAPIError.error(.init(id: nil, status: "500", title: "Internal Error")) + ] + let doc1 = SingleDocumentWithMetaAndLinks( + apiDescription: TestAPIDescription(version: "1", meta: .none), + errors: errors, + meta: nil, + links: nil + ) + let doc2 = SingleDocumentWithMetaAndLinks( + apiDescription: TestAPIDescription(version: "1", meta: .none), + errors: errors, + meta: .init(total: 11), + links: nil + ) + + XCTAssertEqual(doc1.compare(to: doc2).differences, [ + "Body": "Metadata: nil ≠ Optional(total: 11)" ]) } @@ -60,6 +85,24 @@ final class DocumentCompareTests: XCTestCase { "Body": ##"(Primary Resource: (resource 1: 'age' attribute: 10 ≠ 12, 'bestFriend' relationship: Optional(Id(2)) ≠ nil, 'favoriteColor' attribute: nil ≠ Optional("blue"), 'name' attribute: name ≠ Fig, id: 1 ≠ 5))"## ]) } + + func test_differentMetadata() { + XCTAssertEqual(d11.compare(to: d12).differences, [ + "Body": "(Meta: total: 10 ≠ total: 10000)" + ]) + } + + func test_differentLinks() { + XCTAssertEqual(d11.compare(to: d13).differences, [ + "Body": ##"(Links: TestLinks(link: JSONAPI.Link(url: "http://google.com", meta: No Metadata)) ≠ TestLinks(link: JSONAPI.Link(url: "http://yahoo.com", meta: No Metadata)))"## + ]) + } + + func test_differentAPIDescription() { + XCTAssertEqual(d11.compare(to: d14).differences, [ + "API Description": ##"APIDescription(version: "10", meta: No Metadata) ≠ APIDescription(version: "1", meta: No Metadata)"## + ]) + } } fileprivate enum TestDescription: JSONAPI.ResourceObjectDescription { @@ -98,6 +141,22 @@ fileprivate typealias TestType2 = ResourceObject, NoMetadata, NoLinks, Include2, NoAPIDescription, BasicJSONAPIError> +fileprivate struct TestMetadata: JSONAPI.Meta, CustomStringConvertible { + let total: Int + + var description: String { + "total: \(total)" + } +} + +fileprivate struct TestLinks: JSONAPI.Links { + let link: Link +} + +typealias TestAPIDescription = APIDescription + +fileprivate typealias SingleDocumentWithMetaAndLinks = JSONAPI.Document, TestMetadata, TestLinks, Include2, TestAPIDescription, BasicJSONAPIError> + fileprivate typealias OptionalSingleDocument = JSONAPI.Document, NoMetadata, NoLinks, Include2, NoAPIDescription, BasicJSONAPIError> fileprivate typealias ManyDocument = JSONAPI.Document, NoMetadata, NoLinks, Include2, NoAPIDescription, BasicJSONAPIError> @@ -220,3 +279,35 @@ fileprivate let d10 = SingleDocument( meta: .none, links: .none ) + +fileprivate let d11 = SingleDocumentWithMetaAndLinks( + apiDescription: TestAPIDescription(version: "10", meta: .none), + body: .init(resourceObject: r2), + includes: .none, + meta: TestMetadata(total: 10), + links: TestLinks(link: .init(url: "http://google.com")) +) + +fileprivate let d12 = SingleDocumentWithMetaAndLinks( + apiDescription: TestAPIDescription(version: "10", meta: .none), + body: .init(resourceObject: r2), + includes: .none, + meta: TestMetadata(total: 10000), + links: TestLinks(link: .init(url: "http://google.com")) +) + +fileprivate let d13 = SingleDocumentWithMetaAndLinks( + apiDescription: TestAPIDescription(version: "10", meta: .none), + body: .init(resourceObject: r2), + includes: .none, + meta: TestMetadata(total: 10), + links: TestLinks(link: .init(url: "http://yahoo.com")) +) + +fileprivate let d14 = SingleDocumentWithMetaAndLinks( + apiDescription: TestAPIDescription(version: "1", meta: .none), + body: .init(resourceObject: r2), + includes: .none, + meta: TestMetadata(total: 10), + links: TestLinks(link: .init(url: "http://google.com")) +) diff --git a/Tests/JSONAPITestingTests/Comparisons/RelationshipsCompareTests.swift b/Tests/JSONAPITestingTests/Comparisons/RelationshipsCompareTests.swift index cdb7fda..b2dc83e 100644 --- a/Tests/JSONAPITestingTests/Comparisons/RelationshipsCompareTests.swift +++ b/Tests/JSONAPITestingTests/Comparisons/RelationshipsCompareTests.swift @@ -10,5 +10,23 @@ import JSONAPI import JSONAPITesting final class RelationshipsCompareTests: XCTestCase { - // TODO: write tests + func test_same() { + // TODO: write test + } + + func test_differentIds() { + // TODO: write test + } + + func test_differentTypes() { + // TODO: write test + } + + func test_differentMetadata() { + // TODO: write test + } + + func test_differentLinks() { + // TODO: write tests + } } diff --git a/Tests/JSONAPITestingTests/Comparisons/ResourceObjectCompareTests.swift b/Tests/JSONAPITestingTests/Comparisons/ResourceObjectCompareTests.swift index fd43103..d62f9e2 100644 --- a/Tests/JSONAPITestingTests/Comparisons/ResourceObjectCompareTests.swift +++ b/Tests/JSONAPITestingTests/Comparisons/ResourceObjectCompareTests.swift @@ -1,5 +1,5 @@ // -// File.swift +// ResourceObjectCompareTests.swift // // // Created by Mathew Polzin on 11/3/19. @@ -15,11 +15,31 @@ final class ResourceObjectCompareTests: XCTestCase { XCTAssertTrue(test2.compare(to: test2).differences.isEmpty) } - func test_different() { + func test_differentAttributes() { // TODO: write actual test print(test1.compare(to: test2).differences.map { "\($0): \($1)" }.joined(separator: ", ")) } + func test_differentRelationships() { + // TODO: write test + } + + func test_differentTypes() { + // TODO: write test + } + + func test_differentIds() { + // TODO: write test + } + + func test_differentMetadata() { + // TODO: write test + } + + func test_differentLinks() { + // TODO: write test + } + fileprivate let test1 = TestType( id: "2", attributes: .init( diff --git a/Tests/JSONAPITestingTests/Test Helpers/EntityTestTypes.swift b/Tests/JSONAPITestingTests/Test Helpers/EntityTestTypes.swift index af99a71..1729dbc 100644 --- a/Tests/JSONAPITestingTests/Test Helpers/EntityTestTypes.swift +++ b/Tests/JSONAPITestingTests/Test Helpers/EntityTestTypes.swift @@ -12,3 +12,5 @@ public typealias Entity = Entity public typealias NewEntity = JSONAPI.ResourceObject + +extension String: JSONAPI.JSONAPIURL {} diff --git a/Tests/JSONAPITests/Attribute/AttributeTests.swift b/Tests/JSONAPITests/Attribute/AttributeTests.swift index f68831c..6ffdd22 100644 --- a/Tests/JSONAPITests/Attribute/AttributeTests.swift +++ b/Tests/JSONAPITests/Attribute/AttributeTests.swift @@ -14,6 +14,10 @@ class AttributeTests: XCTestCase { XCTAssertEqual(Attribute(value: "hello").value, "hello") } + func test_AttributeRawType() { + XCTAssert(Attribute(value: "hello").rawValueType == String.self) + } + func test_TransformedAttributeNoThrow() { XCTAssertNoThrow(try TransformedAttribute(rawValue: "10")) } @@ -26,6 +30,10 @@ class AttributeTests: XCTestCase { XCTAssertNoThrow(try TransformedAttribute(transformedValue: 10)) } + func test_TransformedAttributeRawType() throws { + try XCTAssert(TransformedAttribute(rawValue: "10").rawValueType == String.self) + } + func test_EncodedPrimitives() { testEncodedPrimitive(attribute: Attribute(value: 10)) testEncodedPrimitive(attribute: Attribute(value: false)) diff --git a/Tests/JSONAPITests/Document/DocumentDecodingErrorTests.swift b/Tests/JSONAPITests/Document/DocumentDecodingErrorTests.swift index 30e2740..6fa2fb4 100644 --- a/Tests/JSONAPITests/Document/DocumentDecodingErrorTests.swift +++ b/Tests/JSONAPITests/Document/DocumentDecodingErrorTests.swift @@ -120,6 +120,28 @@ found JSON:API type "not_an_author" but expected "authors" ) } } + + func test_wantSuccess_foundError() { + XCTAssertThrowsError( + try testDecoder.decode( + Document, NoMetadata, NoLinks, Include1, NoAPIDescription, UnknownJSONAPIError>.SuccessDocument.self, + from: error_document_no_metadata + ) + ) { error in + XCTAssertEqual(String(describing: error), #"Expected a success document with a 'data' property but found an error document."#) + } + } + + func test_wantError_foundSuccess() { + XCTAssertThrowsError( + try testDecoder.decode( + Document, NoMetadata, NoLinks, Include1, NoAPIDescription, UnknownJSONAPIError>.ErrorDocument.self, + from: single_document_some_includes_with_metadata_with_api_description + ) + ) { error in + XCTAssertEqual(String(describing: error), #"Expected an error document but found a success document with a 'data' property."#) + } + } } // MARK: - Test Types diff --git a/Tests/JSONAPITests/Document/DocumentTests.swift b/Tests/JSONAPITests/Document/DocumentTests.swift index d6f6351..5911a0d 100644 --- a/Tests/JSONAPITests/Document/DocumentTests.swift +++ b/Tests/JSONAPITests/Document/DocumentTests.swift @@ -1554,5 +1554,3 @@ extension DocumentTests { } } } - -extension String: JSONAPI.JSONAPIURL {} diff --git a/Tests/JSONAPITests/Document/SuccessAndErrorDocumentTests.swift b/Tests/JSONAPITests/Document/SuccessAndErrorDocumentTests.swift new file mode 100644 index 0000000..9fa1144 --- /dev/null +++ b/Tests/JSONAPITests/Document/SuccessAndErrorDocumentTests.swift @@ -0,0 +1,139 @@ +// +// SuccessAndErrorDocumentTests.swift +// JSONAPITests +// +// Created by Mathew Polzin on 11/12/19. +// + +import XCTest +import JSONAPI + +final class SuccessAndErrorDocumentTests: XCTestCase { + func test_errorAccessors() { + let apiDescription = TestErrorDocument.APIDescription( + version: "1.0", + meta: .none + ) + let errors = [ + BasicJSONAPIError.error(.init(status: "500")) + ] + let meta = TestMeta(hello: "world") + let links = TestLinks(testLink: .init(url: "http://google.com")) + let errorDoc = TestErrorDocument( + apiDescription: apiDescription, + errors: errors, + meta: meta, + links: links + ) + + guard case let .errors(testErrors, meta: testMeta, links: testLinks) = errorDoc.body else { + XCTFail("Expected an error body") + return + } + + XCTAssertEqual(testErrors, errors) + XCTAssertEqual(testMeta, meta) + XCTAssertEqual(testLinks, links) + + XCTAssertEqual(errorDoc.apiDescription, apiDescription) + XCTAssertEqual(errorDoc.errors, errors) + XCTAssertEqual(errorDoc.meta, meta) + XCTAssertEqual(errorDoc.links, links) + + let equivalentDocument = TestDocument( + apiDescription: apiDescription, + errors: errors, + meta: meta, + links: links + ) + + XCTAssert(equivalentDocument == errorDoc) + XCTAssert(errorDoc == equivalentDocument) + } + + func test_successAccessors() { + let apiDescription = TestErrorDocument.APIDescription( + version: "1.0", + meta: .none + ) + let primaryResource = TestType( + id: "123", + attributes: .none, + relationships: .none, + meta: .none, + links: .none + ) + let resourceBody = SingleResourceBody(resourceObject: primaryResource) + let includedResource = TestType( + id: "456", + attributes: .none, + relationships: .none, + meta: .none, + links: .none + ) + let includes = Includes(values: [.init(includedResource)]) + let meta = TestMeta(hello: "world") + let links = TestLinks(testLink: .init(url: "http://google.com")) + let successDoc = TestSuccessDocument( + apiDescription: apiDescription, + body: resourceBody, + includes: includes, + meta: meta, + links: links + ) + + guard case let .data(data) = successDoc.body else { + XCTFail("Expected an data body") + return + } + + XCTAssertEqual(data.primary, resourceBody) + XCTAssertEqual(data.includes, includes) + XCTAssertEqual(data.meta, meta) + XCTAssertEqual(data.links, links) + + XCTAssertEqual(successDoc.data, data) + XCTAssertEqual(successDoc.apiDescription, apiDescription) + XCTAssertEqual(successDoc.primaryResource, resourceBody) + XCTAssertEqual(successDoc.includes, includes) + XCTAssertEqual(successDoc.meta, meta) + XCTAssertEqual(successDoc.links, links) + + let equivalentDocument = TestDocument( + apiDescription: apiDescription, + body: resourceBody, + includes: includes, + meta: meta, + links: links + ) + + XCTAssert(equivalentDocument == successDoc) + XCTAssert(successDoc == equivalentDocument) + } +} + +// MARK: - Test Type +extension SuccessAndErrorDocumentTests { + enum TestTypeDescription: ResourceObjectDescription { + static let jsonType: String = "tests" + + typealias Attributes = NoAttributes + + typealias Relationships = NoRelationships + } + + struct TestMeta: JSONAPI.Meta { + let hello: String + } + + struct TestLinks: JSONAPI.Links { + let testLink: Link + } + + typealias TestType = ResourceObject + + typealias TestDocument = Document, TestMeta, TestLinks, Include1, APIDescription, BasicJSONAPIError> + + typealias TestSuccessDocument = TestDocument.SuccessDocument + typealias TestErrorDocument = TestDocument.ErrorDocument +} diff --git a/Tests/JSONAPITests/Error/GenericJSONAPIErrorTests.swift b/Tests/JSONAPITests/Error/GenericJSONAPIErrorTests.swift index 21172ed..a3c3552 100644 --- a/Tests/JSONAPITests/Error/GenericJSONAPIErrorTests.swift +++ b/Tests/JSONAPITests/Error/GenericJSONAPIErrorTests.swift @@ -64,6 +64,7 @@ final class GenericJSONAPIErrorTests: XCTestCase { let error = decoded(type: TestGenericJSONAPIError.self, data: data) XCTAssertEqual(error, .unknown) + XCTAssertEqual(String(describing: error), "unknown error") } func test_encode() { diff --git a/Tests/JSONAPITests/Includes/IncludesDecodingErrorTests.swift b/Tests/JSONAPITests/Includes/IncludesDecodingErrorTests.swift new file mode 100644 index 0000000..2db4f75 --- /dev/null +++ b/Tests/JSONAPITests/Includes/IncludesDecodingErrorTests.swift @@ -0,0 +1,109 @@ +// +// IncludesDecodingErrorTests.swift +// JSONAPITests +// +// Created by Mathew Polzin on 11/14/19. +// + +import XCTest +import JSONAPI + +final class IncludesDecodingErrorTests: XCTestCase { + func test_unexpectedIncludeType() { + var error1: Error! + XCTAssertThrowsError(try testDecoder.decode(Includes>.self, from: three_different_type_includes)) { (error: Error) -> Void in + XCTAssertEqual( + (error as? IncludesDecodingError)?.idx, + 2 + ) + + XCTAssertEqual( + (error as? IncludesDecodingError).map(String.init(describing:)), +""" +Include 3 failed to parse: \nCould not have been Include Type 1 because: +found JSON:API type "test_entity4" but expected "test_entity1" + +Could not have been Include Type 2 because: +found JSON:API type "test_entity4" but expected "test_entity2" +""" + ) + + error1 = error + } + + // now test that we get the same error from a different test stub + XCTAssertThrowsError(try testDecoder.decode(Includes>.self, from: four_different_type_includes)) { (error2: Error) -> Void in + XCTAssertEqual( + error1 as? IncludesDecodingError, + error2 as? IncludesDecodingError + ) + } + } +} + +// MARK: - Test Types +extension IncludesDecodingErrorTests { + enum TestEntityType: ResourceObjectDescription { + + typealias Relationships = NoRelationships + + public static var jsonType: String { return "test_entity1" } + + public struct Attributes: JSONAPI.SparsableAttributes { + let foo: Attribute + let bar: Attribute + + public enum CodingKeys: String, Equatable, CodingKey { + case foo + case bar + } + } + } + + typealias TestEntity = BasicEntity + + enum TestEntityType2: ResourceObjectDescription { + + public static var jsonType: String { return "test_entity2" } + + public struct Relationships: JSONAPI.Relationships { + let entity1: ToOneRelationship + } + + public struct Attributes: JSONAPI.SparsableAttributes { + let foo: Attribute + let bar: Attribute + + public enum CodingKeys: String, Equatable, CodingKey { + case foo + case bar + } + } + } + + typealias TestEntity2 = BasicEntity + + enum TestEntityType4: ResourceObjectDescription { + + typealias Attributes = NoAttributes + + typealias Relationships = NoRelationships + + public static var jsonType: String { return "test_entity4" } + } + + typealias TestEntity4 = BasicEntity + + enum TestEntityType6: ResourceObjectDescription { + + typealias Attributes = NoAttributes + + public static var jsonType: String { return "test_entity6" } + + struct Relationships: JSONAPI.Relationships { + let entity4: ToOneRelationship + } + } + + typealias TestEntity6 = BasicEntity +} diff --git a/Tests/JSONAPITests/Includes/stubs/IncludeStubs.swift b/Tests/JSONAPITests/Includes/stubs/IncludeStubs.swift index 5e5f593..e53b144 100644 --- a/Tests/JSONAPITests/Includes/stubs/IncludeStubs.swift +++ b/Tests/JSONAPITests/Includes/stubs/IncludeStubs.swift @@ -129,6 +129,10 @@ let four_different_type_includes = """ } } }, + { + "type": "test_entity4", + "id": "364B3B69-4DF1-467F-B52E-B0C9E44F666E" + }, { "type": "test_entity6", "id": "11113B69-4DF1-467F-B52E-B0C9E44FC444", @@ -140,10 +144,6 @@ let four_different_type_includes = """ } } } - }, - { - "type": "test_entity4", - "id": "364B3B69-4DF1-467F-B52E-B0C9E44F666E" } ] """.data(using: .utf8)! diff --git a/Tests/JSONAPITests/Poly/PolyProxyTests.swift b/Tests/JSONAPITests/Poly/PolyProxyTests.swift index 582a2fc..efae216 100644 --- a/Tests/JSONAPITests/Poly/PolyProxyTests.swift +++ b/Tests/JSONAPITests/Poly/PolyProxyTests.swift @@ -15,6 +15,12 @@ public class PolyProxyTests: XCTestCase { XCTAssertEqual(User.jsonType, "users") } + func test_CannotEncodeOrDecodePoly0() { + XCTAssertThrowsError(try testDecoder.decode(Poly0.self, from: poly_user_stub_1)) + + XCTAssertThrowsError(try testEncoder.encode(Poly0())) + } + func test_UserADecode() { let polyUserA = decoded(type: User.self, data: poly_user_stub_1) let userA = decoded(type: UserA.self, data: poly_user_stub_1) diff --git a/Tests/JSONAPITests/Test Helpers/EncodeDecode.swift b/Tests/JSONAPITests/Test Helpers/EncodeDecode.swift index d0b1c56..a588b71 100644 --- a/Tests/JSONAPITests/Test Helpers/EncodeDecode.swift +++ b/Tests/JSONAPITests/Test Helpers/EncodeDecode.swift @@ -9,13 +9,14 @@ import Foundation import XCTest let testDecoder = JSONDecoder() +let testEncoder = JSONEncoder() func decoded(type: T.Type, data: Data) -> T { return try! testDecoder.decode(T.self, from: data) } func encoded(value: T) -> Data { - return try! JSONEncoder().encode(value) + return try! testEncoder.encode(value) } /// A helper function that tests that decode() == decode().encode().decode(). diff --git a/Tests/JSONAPITests/Test Helpers/EncodedAttributeTest.swift b/Tests/JSONAPITests/Test Helpers/EncodedAttributeTest.swift index 56070a0..2a340d1 100644 --- a/Tests/JSONAPITests/Test Helpers/EncodedAttributeTest.swift +++ b/Tests/JSONAPITests/Test Helpers/EncodedAttributeTest.swift @@ -33,12 +33,9 @@ func testEncodedPrimitive(at let wrapperObject = try! JSONSerialization.jsonObject(with: encodedAttributeData, options: []) as! [String: Any] let jsonObject = wrapperObject["x"] - guard let jsonAttribute = jsonObject as? Transform.From else { - XCTFail("Attribute did not encode to the correct type") - return - } + XCTAssert(jsonObject is Transform.From) - XCTAssertEqual(attribute.rawValue, jsonAttribute) + XCTAssertEqual(attribute.rawValue, jsonObject as? Transform.From) } /// This function attempts to just cast to the type, so it only works @@ -48,10 +45,7 @@ func testEncodedPrimitive(attribute: Attribute = Entity public typealias NewEntity = JSONAPI.ResourceObject + +extension String: JSONAPI.JSONAPIURL {} From 1010489a02425b4e59f0fe5f564987a97f9d6d80 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Fri, 15 Nov 2019 16:59:00 -0800 Subject: [PATCH 112/235] compare(to:) bug fixes and test additions --- .../Comparisons/AttributesCompare.swift | 54 +++++- .../Comparisons/RelationshipsCompare.swift | 54 +++++- .../Comparisons/AttributesCompareTests.swift | 67 +++++-- .../Comparisons/DocumentCompareTests.swift | 2 + .../Optional+AbstractWrapper.swift | 16 ++ .../RelationshipsCompareTests.swift | 169 +++++++++++++++++- 6 files changed, 325 insertions(+), 37 deletions(-) create mode 100644 Tests/JSONAPITestingTests/Comparisons/Optional+AbstractWrapper.swift diff --git a/Sources/JSONAPITesting/Comparisons/AttributesCompare.swift b/Sources/JSONAPITesting/Comparisons/AttributesCompare.swift index 147432c..83aeea2 100644 --- a/Sources/JSONAPITesting/Comparisons/AttributesCompare.swift +++ b/Sources/JSONAPITesting/Comparisons/AttributesCompare.swift @@ -1,5 +1,5 @@ // -// File.swift +// AttributesCompare.swift // // // Created by Mathew Polzin on 11/3/19. @@ -24,12 +24,16 @@ extension Attributes { continue } - if (attributesEqual(child.value, otherChild.value)) { - comparisons[childLabel] = .same - } else { - let otherChildDescription = attributeDescription(of: otherChild.value) + do { + if (try attributesEqual(child.value, otherChild.value)) { + comparisons[childLabel] = .same + } else { + let otherChildDescription = attributeDescription(of: otherChild.value) - comparisons[childLabel] = .different(childDescription, otherChildDescription) + comparisons[childLabel] = .different(childDescription, otherChildDescription) + } + } catch let error { + comparisons[childLabel] = .prebuilt(String(describing: error)) } } @@ -37,9 +41,20 @@ extension Attributes { } } -fileprivate func attributesEqual(_ one: Any, _ two: Any) -> Bool { +enum AttributeCompareError: Swift.Error, CustomStringConvertible { + case nonAttributeTypeProperty(String) + + var description: String { + switch self { + case .nonAttributeTypeProperty(let type): + return "comparison on non-JSON:API Attribute type (\(type)) not supported." + } + } +} + +fileprivate func attributesEqual(_ one: Any, _ two: Any) throws -> Bool { guard let attr = one as? AbstractAttribute else { - return false + throw AttributeCompareError.nonAttributeTypeProperty(String(describing: type(of: one))) } return attr.equals(two) @@ -55,6 +70,29 @@ protocol AbstractAttribute { func equals(_ other: Any) -> Bool } +extension Optional: AbstractAttribute where Wrapped: AbstractAttribute { + var abstractDescription: String { + switch self { + case .none: + return "nil" + case .some(let rel): + return rel.abstractDescription + } + } + + func equals(_ other: Any) -> Bool { + switch self { + case .none: + return (other as? _AbstractWrapper).map { $0.abstractSelf == nil } ?? false + case .some(let rel): + guard case let .some(otherVal) = (other as? _AbstractWrapper)?.abstractSelf else { + return rel.equals(other) + } + return rel.equals(otherVal) + } + } +} + extension Attribute: AbstractAttribute { var abstractDescription: String { String(describing: value) } diff --git a/Sources/JSONAPITesting/Comparisons/RelationshipsCompare.swift b/Sources/JSONAPITesting/Comparisons/RelationshipsCompare.swift index 0c24386..9a8c010 100644 --- a/Sources/JSONAPITesting/Comparisons/RelationshipsCompare.swift +++ b/Sources/JSONAPITesting/Comparisons/RelationshipsCompare.swift @@ -24,12 +24,16 @@ extension Relationships { continue } - if (relationshipsEqual(child.value, otherChild.value)) { - comparisons[childLabel] = .same - } else { - let otherChildDescription = relationshipDescription(of: otherChild.value) - - comparisons[childLabel] = .different(childDescription, otherChildDescription) + do { + if (try relationshipsEqual(child.value, otherChild.value)) { + comparisons[childLabel] = .same + } else { + let otherChildDescription = relationshipDescription(of: otherChild.value) + + comparisons[childLabel] = .different(childDescription, otherChildDescription) + } + } catch let error { + comparisons[childLabel] = .prebuilt(String(describing: error)) } } @@ -37,9 +41,20 @@ extension Relationships { } } -fileprivate func relationshipsEqual(_ one: Any, _ two: Any) -> Bool { +enum RelationshipCompareError: Swift.Error, CustomStringConvertible { + case nonRelationshipTypeProperty(String) + + var description: String { + switch self { + case .nonRelationshipTypeProperty(let type): + return "comparison on non-JSON:API Relationship type (\(type)) not supported." + } + } +} + +fileprivate func relationshipsEqual(_ one: Any, _ two: Any) throws -> Bool { guard let attr = one as? AbstractRelationship else { - return false + throw RelationshipCompareError.nonRelationshipTypeProperty(String(describing: type(of: one))) } return attr.equals(two) @@ -55,6 +70,29 @@ protocol AbstractRelationship { func equals(_ other: Any) -> Bool } +extension Optional: AbstractRelationship where Wrapped: AbstractRelationship { + var abstractDescription: String { + switch self { + case .none: + return "nil" + case .some(let rel): + return rel.abstractDescription + } + } + + func equals(_ other: Any) -> Bool { + switch self { + case .none: + return (other as? _AbstractWrapper).map { $0.abstractSelf == nil } ?? false + case .some(let rel): + guard case let .some(otherVal) = (other as? _AbstractWrapper)?.abstractSelf else { + return rel.equals(other) + } + return rel.equals(otherVal) + } + } +} + extension ToOneRelationship: AbstractRelationship { var abstractDescription: String { if meta is NoMetadata && links is NoLinks { diff --git a/Tests/JSONAPITestingTests/Comparisons/AttributesCompareTests.swift b/Tests/JSONAPITestingTests/Comparisons/AttributesCompareTests.swift index e54031c..6141be6 100644 --- a/Tests/JSONAPITestingTests/Comparisons/AttributesCompareTests.swift +++ b/Tests/JSONAPITestingTests/Comparisons/AttributesCompareTests.swift @@ -1,5 +1,5 @@ // -// File.swift +// AttributesCompareTests.swift // // // Created by Mathew Polzin on 11/3/19. @@ -17,7 +17,9 @@ final class AttributesCompareTests: XCTestCase { bool: true, double: 105.4, struct: .init(value: .init()), - transformed: try .init(rawValue: 10) + transformed: try .init(rawValue: 10), + optional: .init(value: 20), + optionalTransformed: try .init(rawValue: 10) ) let attr2 = attr1 @@ -27,7 +29,9 @@ final class AttributesCompareTests: XCTestCase { "bool": .same, "double": .same, "struct": .same, - "transformed": .same + "transformed": .same, + "optional": .same, + "optionalTransformed": .same ]) } @@ -38,7 +42,9 @@ final class AttributesCompareTests: XCTestCase { bool: true, double: 105.4, struct: .init(value: .init()), - transformed: try .init(rawValue: 10) + transformed: try .init(rawValue: 10), + optional: nil, + optionalTransformed: nil ) let attr2 = TestAttributes( string: "hello", @@ -46,7 +52,9 @@ final class AttributesCompareTests: XCTestCase { bool: false, double: 1.4, struct: .init(value: .init(val: "there")), - transformed: try .init(rawValue: 11) + transformed: try .init(rawValue: 11), + optional: .init(value: 20.5), + optionalTransformed: try .init(rawValue: 10) ) XCTAssertEqual(attr1.compare(to: attr2), [ @@ -55,7 +63,29 @@ final class AttributesCompareTests: XCTestCase { "bool": .different("true", "false"), "double": .different("105.4", "1.4"), "struct": .different("string: hello", "string: there"), - "transformed": .different("10", "11") + "transformed": .different("10", "11"), + "optional": .different("nil", "20.5"), + "optionalTransformed": .different("nil", "10") + ]) + } + + func test_nonAttributeTypes() { + let attr1 = NonAttributeTest( + string: "hello", + int: 10, + double: 11.2, + bool: true, + struct: .init(), + optional: nil + ) + + XCTAssertEqual(attr1.compare(to: attr1), [ + "string": .prebuilt("comparison on non-JSON:API Attribute type (String) not supported."), + "int": .prebuilt("comparison on non-JSON:API Attribute type (Int) not supported."), + "double": .prebuilt("comparison on non-JSON:API Attribute type (Double) not supported."), + "bool": .prebuilt("comparison on non-JSON:API Attribute type (Bool) not supported."), + "struct": .prebuilt("comparison on non-JSON:API Attribute type (Struct) not supported."), + "optional": .prebuilt("comparison on non-JSON:API Attribute type (Optional) not supported.") ]) } } @@ -67,16 +97,18 @@ private struct TestAttributes: JSONAPI.Attributes { let double: Attribute let `struct`: Attribute let transformed: TransformedAttribute + let optional: Attribute? + let optionalTransformed: TransformedAttribute? +} - struct Struct: Equatable, Codable, CustomStringConvertible { - let string: String - - init(val: String = "hello") { - self.string = val - } +private struct Struct: Equatable, Codable, CustomStringConvertible { + let string: String - var description: String { return "string: \(string)" } + init(val: String = "hello") { + self.string = val } + + var description: String { return "string: \(string)" } } private enum TestTransformer: Transformer { @@ -84,3 +116,12 @@ private enum TestTransformer: Transformer { return "\(value)" } } + +private struct NonAttributeTest: JSONAPI.Attributes { + let string: String + let int: Int + let double: Double + let bool: Bool + let `struct`: Struct + let optional: Int? +} diff --git a/Tests/JSONAPITestingTests/Comparisons/DocumentCompareTests.swift b/Tests/JSONAPITestingTests/Comparisons/DocumentCompareTests.swift index 9cbf93f..8b464e6 100644 --- a/Tests/JSONAPITestingTests/Comparisons/DocumentCompareTests.swift +++ b/Tests/JSONAPITestingTests/Comparisons/DocumentCompareTests.swift @@ -21,6 +21,8 @@ final class DocumentCompareTests: XCTestCase { XCTAssertTrue(d8.compare(to: d8).differences.isEmpty) XCTAssertTrue(d9.compare(to: d9).differences.isEmpty) XCTAssertTrue(d10.compare(to: d10).differences.isEmpty) + + XCTAssertEqual(String(describing: d1.compare(to: d1).body), "same") } func test_errorAndData() { diff --git a/Tests/JSONAPITestingTests/Comparisons/Optional+AbstractWrapper.swift b/Tests/JSONAPITestingTests/Comparisons/Optional+AbstractWrapper.swift new file mode 100644 index 0000000..2a068d0 --- /dev/null +++ b/Tests/JSONAPITestingTests/Comparisons/Optional+AbstractWrapper.swift @@ -0,0 +1,16 @@ +// +// Optional+AbstractWrapper.swift +// JSONAPITesting +// +// Created by Mathew Polzin on 11/15/19. +// + +protocol _AbstractWrapper { + var abstractSelf: Any? { get } +} + +extension Optional: _AbstractWrapper { + var abstractSelf: Any? { + return self + } +} diff --git a/Tests/JSONAPITestingTests/Comparisons/RelationshipsCompareTests.swift b/Tests/JSONAPITestingTests/Comparisons/RelationshipsCompareTests.swift index b2dc83e..44b720a 100644 --- a/Tests/JSONAPITestingTests/Comparisons/RelationshipsCompareTests.swift +++ b/Tests/JSONAPITestingTests/Comparisons/RelationshipsCompareTests.swift @@ -1,5 +1,5 @@ // -// File.swift +// RelationshipCompareTests.swift // // // Created by Mathew Polzin on 11/5/19. @@ -11,22 +11,175 @@ import JSONAPITesting final class RelationshipsCompareTests: XCTestCase { func test_same() { - // TODO: write test + let r1 = TestRelationships( + a: t1, + b: t2, + c: t3, + d: t4 + ) + let r2 = r1 + + XCTAssertTrue(r1.compare(to: r2).allSatisfy { $0.value == .same }) + + let r3 = TestRelationships( + a: t1_differentId, + b: t2_differentLinks, + c: t3_differentId, + d: t4_differentLinks + ) + let r4 = r3 + + XCTAssertTrue(r3.compare(to: r4).allSatisfy { $0.value == .same }) + + let r5 = TestRelationships( + a: nil, + b: nil, + c: nil, + d: nil + ) + let r6 = r5 + + XCTAssertTrue(r5.compare(to: r6).allSatisfy { $0.value == .same }) } func test_differentIds() { - // TODO: write test - } + let r1 = TestRelationships( + a: t1, + b: nil, + c: t3, + d: nil + ) + + let r2 = TestRelationships( + a: t1_differentId, + b: nil, + c: t3_differentId, + d: nil + ) - func test_differentTypes() { - // TODO: write test + XCTAssertEqual(r1.compare(to: r2), [ + "a": .different("Id(123)", "Id(999)"), + "b": .same, + "c": .different("123, 456", "999, 1010"), + "d": .same + ]) } func test_differentMetadata() { - // TODO: write test + let r1 = TestRelationships( + a: nil, + b: t2, + c: nil, + d: t4 + ) + + let r2 = TestRelationships( + a: nil, + b: t2_differentMeta, + c: nil, + d: t4_differentMeta + ) + + XCTAssertEqual(r1.compare(to: r2), [ + "a": .same, + "b": .different(#"("Id(456)", "hello: world", "link: http://google.com")"#, #"("Id(456)", "hello: there", "link: http://google.com")"#), + "c": .same, + "d": .different(#"("123, 456", "hello: world", "link: http://google.com")"#, #"("123, 456", "hello: there", "link: http://google.com")"#) + ]) } func test_differentLinks() { - // TODO: write tests + let r1 = TestRelationships( + a: nil, + b: t2, + c: nil, + d: t4 + ) + + let r2 = TestRelationships( + a: nil, + b: t2_differentLinks, + c: nil, + d: t4_differentLinks + ) + + XCTAssertEqual(r1.compare(to: r2), [ + "a": .same, + "b": .different(#"("Id(456)", "hello: world", "link: http://google.com")"#, #"("Id(456)", "hello: world", "link: http://yahoo.com")"#), + "c": .same, + "d": .different(#"("123, 456", "hello: world", "link: http://google.com")"#, #"("123, 456", "hello: world", "link: http://yahoo.com")"#) + ]) + } + + func test_nonRelationshipTypes() { + let r1 = TestNonRelationships( + a: .init(attributes: .none, relationships: .none, meta: .none, links: .none), + b: false, + c: 10, + d: "1234" + ) + + XCTAssertEqual(r1.compare(to: r1), [ + "a": .prebuilt("comparison on non-JSON:API Relationship type (ResourceObject) not supported."), + "b": .prebuilt("comparison on non-JSON:API Relationship type (Bool) not supported."), + "c": .prebuilt("comparison on non-JSON:API Relationship type (Int) not supported."), + "d": .prebuilt("comparison on non-JSON:API Relationship type (Id>) not supported.") + ]) + } + + let t1 = ToOneRelationship(id: "123") + let t2 = ToOneRelationship(id: "456", meta: .init(hello: "world"), links: .init(link: .init(url: "http://google.com"))) + let t3 = ToManyRelationship(ids: ["123", "456"]) + let t4 = ToManyRelationship(ids: ["123", "456"], meta: .init(hello: "world"), links: .init(link: .init(url: "http://google.com"))) + + let t1_differentId = ToOneRelationship(id: "999") + let t3_differentId = ToManyRelationship(ids: ["999", "1010"]) + + let t2_differentLinks = ToOneRelationship(id: "456", meta: .init(hello: "world"), links: .init(link: .init(url: "http://yahoo.com"))) + let t4_differentLinks = ToManyRelationship(ids: ["123", "456"], meta: .init(hello: "world"), links: .init(link: .init(url: "http://yahoo.com"))) + + let t2_differentMeta = ToOneRelationship(id: "456", meta: .init(hello: "there"), links: .init(link: .init(url: "http://google.com"))) + let t4_differentMeta = ToManyRelationship(ids: ["123", "456"], meta: .init(hello: "there"), links: .init(link: .init(url: "http://google.com"))) +} + +// MARK: - Test Types +extension RelationshipsCompareTests { + enum TestTypeDescription: ResourceObjectDescription { + static let jsonType: String = "test" + + typealias Attributes = NoAttributes + typealias Relationships = NoRelationships + } + + typealias TestType = ResourceObject + + struct TestMeta: JSONAPI.Meta, CustomDebugStringConvertible { + let hello: String + + var debugDescription: String { + "hello: \(hello)" + } + } + + struct TestLinks: JSONAPI.Links, CustomDebugStringConvertible { + let link: Link + + var debugDescription: String { + "link: \(link.url)" + } + } + + struct TestRelationships: JSONAPI.Relationships { + let a: ToOneRelationship? + let b: ToOneRelationship? + let c: ToManyRelationship? + let d: ToManyRelationship? + } + + struct TestNonRelationships: JSONAPI.Relationships { + let a: TestType + let b: Bool + let c: Int + let d: JSONAPI.Id } } From a6b7d7a94a4a4ec3c30939b13df14222790f0146 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Fri, 15 Nov 2019 17:05:45 -0800 Subject: [PATCH 113/235] woops, abstract wrapper protocol landed in wrong module by accident --- Sources/JSONAPI/Document/Document.swift | 2 -- Sources/JSONAPI/SparseFields/SparseFieldEncoder.swift | 4 ++-- .../Comparisons/Optional+AbstractWrapper.swift | 0 3 files changed, 2 insertions(+), 4 deletions(-) rename {Tests/JSONAPITestingTests => Sources/JSONAPITesting}/Comparisons/Optional+AbstractWrapper.swift (100%) diff --git a/Sources/JSONAPI/Document/Document.swift b/Sources/JSONAPI/Document/Document.swift index 1343559..a266216 100644 --- a/Sources/JSONAPI/Document/Document.swift +++ b/Sources/JSONAPI/Document/Document.swift @@ -408,8 +408,6 @@ extension Document: Decodable, CodableJSONAPIDocument where PrimaryResourceBody: throw DocumentDecodingError(error) } - // TODO come back to this and make robust - guard let metaVal = meta else { throw JSONAPICodingError.missingOrMalformedMetadata(path: decoder.codingPath) } diff --git a/Sources/JSONAPI/SparseFields/SparseFieldEncoder.swift b/Sources/JSONAPI/SparseFields/SparseFieldEncoder.swift index 36a1a95..2c555bd 100644 --- a/Sources/JSONAPI/SparseFields/SparseFieldEncoder.swift +++ b/Sources/JSONAPI/SparseFields/SparseFieldEncoder.swift @@ -158,7 +158,7 @@ struct SparseFieldKeyedEncodingContainer: KeyedEncodingContainer forKey key: Key) -> KeyedEncodingContainer where NestedKey : CodingKey { guard shouldAllow(key: key) else { return KeyedEncodingContainer( - // TODO: not needed by JSONAPI library, but for completeness could + // NOTE: not needed by JSONAPI library, but for completeness could // add an EmptyObjectEncoder that can be returned here so that // at least nothing gets encoded within the nested container SparseFieldKeyedEncodingContainer(wrapping: wrappedContainer.nestedContainer(keyedBy: keyType, @@ -176,7 +176,7 @@ struct SparseFieldKeyedEncodingContainer: KeyedEncodingContainer public mutating func nestedUnkeyedContainer(forKey key: Key) -> UnkeyedEncodingContainer { guard shouldAllow(key: key) else { - // TODO: not needed by JSONAPI library, but for completeness could + // NOTE: not needed by JSONAPI library, but for completeness could // add an EmptyObjectEncoder that can be returned here so that // at least nothing gets encoded within the nested container return wrappedContainer.nestedUnkeyedContainer(forKey: key) diff --git a/Tests/JSONAPITestingTests/Comparisons/Optional+AbstractWrapper.swift b/Sources/JSONAPITesting/Comparisons/Optional+AbstractWrapper.swift similarity index 100% rename from Tests/JSONAPITestingTests/Comparisons/Optional+AbstractWrapper.swift rename to Sources/JSONAPITesting/Comparisons/Optional+AbstractWrapper.swift From 4a7a14b1b0ee18b9275b9a72758fb693d9200e59 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Fri, 15 Nov 2019 17:36:42 -0800 Subject: [PATCH 114/235] Add test coverage for resource object compare(to:) --- .../ResourceObjectCompareTests.swift | 119 ++++++++++++++++-- 1 file changed, 106 insertions(+), 13 deletions(-) diff --git a/Tests/JSONAPITestingTests/Comparisons/ResourceObjectCompareTests.swift b/Tests/JSONAPITestingTests/Comparisons/ResourceObjectCompareTests.swift index d62f9e2..1f3a245 100644 --- a/Tests/JSONAPITestingTests/Comparisons/ResourceObjectCompareTests.swift +++ b/Tests/JSONAPITestingTests/Comparisons/ResourceObjectCompareTests.swift @@ -12,32 +12,71 @@ import JSONAPITesting final class ResourceObjectCompareTests: XCTestCase { func test_same() { XCTAssertTrue(test1.compare(to: test1).differences.isEmpty) - XCTAssertTrue(test2.compare(to: test2).differences.isEmpty) + XCTAssertTrue(test1_differentId.compare(to: test1_differentId).differences.isEmpty) + XCTAssertTrue(test1_differentAttributes.compare(to: test1_differentAttributes).differences.isEmpty) } func test_differentAttributes() { - // TODO: write actual test - print(test1.compare(to: test2).differences.map { "\($0): \($1)" }.joined(separator: ", ")) + XCTAssertEqual(test1.compare(to: test1_differentAttributes).differences, [ + "'favoriteColor' attribute": #"Optional("red") ≠ nil"#, + "'name' attribute": "James ≠ Fred", + "'age' attribute": "12 ≠ 10" + ]) } func test_differentRelationships() { - // TODO: write test - } - - func test_differentTypes() { - // TODO: write test + XCTAssertEqual(test1.compare(to: test1_differentRelationships).differences, [ + "'parents' relationship": "4, 5 ≠ 3", + "'bestFriend' relationship": "Optional(Id(3)) ≠ nil" + ]) } func test_differentIds() { - // TODO: write test + XCTAssertEqual(test1.compare(to: test1_differentId).differences, [ + "id": "2 ≠ 3" + ]) } func test_differentMetadata() { - // TODO: write test + let test1 = TestType2( + id: "2", + attributes: .none, + relationships: .none, + meta: .init(total: 10), + links: .init(link: .init(url: "http://google.com")) + ) + let test1_differentMeta = TestType2( + id: "2", + attributes: .none, + relationships: .none, + meta: .init(total: 12), + links: .init(link: .init(url: "http://google.com")) + ) + + XCTAssertEqual(test1.compare(to: test1_differentMeta).differences, [ + "meta": "total: 10 ≠ total: 12" + ]) } func test_differentLinks() { - // TODO: write test + let test1 = TestType2( + id: "2", + attributes: .none, + relationships: .none, + meta: .init(total: 10), + links: .init(link: .init(url: "http://google.com")) + ) + let test1_differentLinks = TestType2( + id: "2", + attributes: .none, + relationships: .none, + meta: .init(total: 10), + links: .init(link: .init(url: "http://yahoo.com")) + ) + + XCTAssertEqual(test1.compare(to: test1_differentLinks).differences, [ + "links": "link: http://google.com ≠ link: http://yahoo.com" + ]) } fileprivate let test1 = TestType( @@ -54,15 +93,43 @@ final class ResourceObjectCompareTests: XCTestCase { links: .none ) - fileprivate let test2 = TestType( + fileprivate let test1_differentId = TestType( id: "3", + attributes: .init( + name: "James", + age: 12, + favoriteColor: "red"), + relationships: .init( + bestFriend: "3", + parents: ["4", "5"] + ), + meta: .none, + links: .none + ) + + fileprivate let test1_differentAttributes = TestType( + id: "2", attributes: .init( name: "Fred", age: 10, favoriteColor: .init(value: nil)), + relationships: .init( + bestFriend: "3", + parents: ["4", "5"] + ), + meta: .none, + links: .none + ) + + fileprivate let test1_differentRelationships = TestType( + id: "2", + attributes: .init( + name: "James", + age: 12, + favoriteColor: "red"), relationships: .init( bestFriend: nil, - parents: ["1"] + parents: ["3"] ), meta: .none, links: .none @@ -85,3 +152,29 @@ private enum TestDescription: JSONAPI.ResourceObjectDescription { } private typealias TestType = ResourceObject + +private struct TestMetadata: JSONAPI.Meta, CustomStringConvertible { + let total: Int + + var description: String { + "total: \(total)" + } +} + +private struct TestLinks: JSONAPI.Links, CustomStringConvertible { + let link: Link + + var description: String { + "link: \(link.url)" + } +} + +private enum TestDescription2: JSONAPI.ResourceObjectDescription { + static let jsonType: String = "test_type2" + + typealias Attributes = NoAttributes + + typealias Relationships = NoRelationships +} + +private typealias TestType2 = ResourceObject From 8ee04d89323a23acf80a98aebd5c2cf02a5c68d3 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Fri, 15 Nov 2019 17:38:04 -0800 Subject: [PATCH 115/235] generate linuxmain --- .../JSONAPITestingTests/XCTestManifests.swift | 38 ++++++++++++++++++- Tests/JSONAPITests/XCTestManifests.swift | 34 ++++++++++++++++- 2 files changed, 70 insertions(+), 2 deletions(-) diff --git a/Tests/JSONAPITestingTests/XCTestManifests.swift b/Tests/JSONAPITestingTests/XCTestManifests.swift index b1b07fb..e77e922 100644 --- a/Tests/JSONAPITestingTests/XCTestManifests.swift +++ b/Tests/JSONAPITestingTests/XCTestManifests.swift @@ -1,6 +1,18 @@ #if !canImport(ObjectiveC) import XCTest +extension ArrayCompareTests { + // DO NOT MODIFY: This is autogenerated, use: + // `swift test --generate-linuxmain` + // to regenerate. + static let __allTests__ArrayCompareTests = [ + ("test_differentLengths", test_differentLengths), + ("test_differentValues", test_differentValues), + ("test_reducePrebuilt", test_reducePrebuilt), + ("test_same", test_same), + ] +} + extension Attribute_LiteralTests { // DO NOT MODIFY: This is autogenerated, use: // `swift test --generate-linuxmain` @@ -41,6 +53,7 @@ extension AttributesCompareTests { // to regenerate. static let __allTests__AttributesCompareTests = [ ("test_differentAttributes", test_differentAttributes), + ("test_nonAttributeTypes", test_nonAttributeTypes), ("test_sameAttributes", test_sameAttributes), ] } @@ -50,10 +63,14 @@ extension DocumentCompareTests { // `swift test --generate-linuxmain` // to regenerate. static let __allTests__DocumentCompareTests = [ + ("test_differentAPIDescription", test_differentAPIDescription), ("test_differentData", test_differentData), ("test_differentErrors", test_differentErrors), + ("test_differentLinks", test_differentLinks), + ("test_differentMetadata", test_differentMetadata), ("test_errorAndData", test_errorAndData), ("test_same", test_same), + ("test_sameErrorsDifferentMetadata", test_sameErrorsDifferentMetadata), ] } @@ -103,18 +120,36 @@ extension Relationship_LiteralTests { ] } +extension RelationshipsCompareTests { + // DO NOT MODIFY: This is autogenerated, use: + // `swift test --generate-linuxmain` + // to regenerate. + static let __allTests__RelationshipsCompareTests = [ + ("test_differentIds", test_differentIds), + ("test_differentLinks", test_differentLinks), + ("test_differentMetadata", test_differentMetadata), + ("test_nonRelationshipTypes", test_nonRelationshipTypes), + ("test_same", test_same), + ] +} + extension ResourceObjectCompareTests { // DO NOT MODIFY: This is autogenerated, use: // `swift test --generate-linuxmain` // to regenerate. static let __allTests__ResourceObjectCompareTests = [ - ("test_different", test_different), + ("test_differentAttributes", test_differentAttributes), + ("test_differentIds", test_differentIds), + ("test_differentLinks", test_differentLinks), + ("test_differentMetadata", test_differentMetadata), + ("test_differentRelationships", test_differentRelationships), ("test_same", test_same), ] } public func __allTests() -> [XCTestCaseEntry] { return [ + testCase(ArrayCompareTests.__allTests__ArrayCompareTests), testCase(Attribute_LiteralTests.__allTests__Attribute_LiteralTests), testCase(AttributesCompareTests.__allTests__AttributesCompareTests), testCase(DocumentCompareTests.__allTests__DocumentCompareTests), @@ -122,6 +157,7 @@ public func __allTests() -> [XCTestCaseEntry] { testCase(Id_LiteralTests.__allTests__Id_LiteralTests), testCase(IncludesCompareTests.__allTests__IncludesCompareTests), testCase(Relationship_LiteralTests.__allTests__Relationship_LiteralTests), + testCase(RelationshipsCompareTests.__allTests__RelationshipsCompareTests), testCase(ResourceObjectCompareTests.__allTests__ResourceObjectCompareTests), ] } diff --git a/Tests/JSONAPITests/XCTestManifests.swift b/Tests/JSONAPITests/XCTestManifests.swift index 0768089..ed1efbb 100644 --- a/Tests/JSONAPITests/XCTestManifests.swift +++ b/Tests/JSONAPITests/XCTestManifests.swift @@ -22,10 +22,12 @@ extension AttributeTests { // to regenerate. static let __allTests__AttributeTests = [ ("test_AttributeConstructor", test_AttributeConstructor), + ("test_AttributeRawType", test_AttributeRawType), ("test_EncodedPrimitives", test_EncodedPrimitives), ("test_NullableIsEqualToNonNullableIfNotNil", test_NullableIsEqualToNonNullableIfNotNil), ("test_NullableIsNullIfNil", test_NullableIsNullIfNil), ("test_TransformedAttributeNoThrow", test_TransformedAttributeNoThrow), + ("test_TransformedAttributeRawType", test_TransformedAttributeRawType), ("test_TransformedAttributeReversNoThrow", test_TransformedAttributeReversNoThrow), ("test_TransformedAttributeThrows", test_TransformedAttributeThrows), ] @@ -91,10 +93,13 @@ extension DocumentDecodingErrorTests { // to regenerate. static let __allTests__DocumentDecodingErrorTests = [ ("test_include_failure", test_include_failure), + ("test_include_failure2", test_include_failure2), ("test_manyPrimaryResource_failure", test_manyPrimaryResource_failure), ("test_manyPrimaryResource_missing", test_manyPrimaryResource_missing), ("test_singlePrimaryResource_failure", test_singlePrimaryResource_failure), ("test_singlePrimaryResource_missing", test_singlePrimaryResource_missing), + ("test_wantError_foundSuccess", test_wantError_foundSuccess), + ("test_wantSuccess_foundError", test_wantSuccess_foundError), ] } @@ -283,6 +288,15 @@ extension IncludedTests { ] } +extension IncludesDecodingErrorTests { + // DO NOT MODIFY: This is autogenerated, use: + // `swift test --generate-linuxmain` + // to regenerate. + static let __allTests__IncludesDecodingErrorTests = [ + ("test_unexpectedIncludeType", test_unexpectedIncludeType), + ] +} + extension LinksTests { // DO NOT MODIFY: This is autogenerated, use: // `swift test --generate-linuxmain` @@ -318,6 +332,7 @@ extension PolyProxyTests { static let __allTests__PolyProxyTests = [ ("test_AsymmetricEncodeDecodeUserA", test_AsymmetricEncodeDecodeUserA), ("test_AsymmetricEncodeDecodeUserB", test_AsymmetricEncodeDecodeUserB), + ("test_CannotEncodeOrDecodePoly0", test_CannotEncodeOrDecodePoly0), ("test_generalReasonableness", test_generalReasonableness), ("test_UserAAndBEncodeEquality", test_UserAAndBEncodeEquality), ("test_UserADecode", test_UserADecode), @@ -391,8 +406,13 @@ extension ResourceObjectDecodingErrorTests { ("test_oneTypeVsAnother_relationship", test_oneTypeVsAnother_relationship), ("test_required_attribute", test_required_attribute), ("test_required_relationship", test_required_relationship), + ("test_transformed_attribute", test_transformed_attribute), + ("test_transformed_attribute2", test_transformed_attribute2), ("test_twoOneVsToMany_relationship", test_twoOneVsToMany_relationship), - ("test_wrongType", test_wrongType), + ("test_type_missing", test_type_missing), + ("test_type_null", test_type_null), + ("test_wrongDecodedType", test_wrongDecodedType), + ("test_wrongJSONAPIType", test_wrongJSONAPIType), ] } @@ -538,6 +558,16 @@ extension SparseFieldsetTests { ] } +extension SuccessAndErrorDocumentTests { + // DO NOT MODIFY: This is autogenerated, use: + // `swift test --generate-linuxmain` + // to regenerate. + static let __allTests__SuccessAndErrorDocumentTests = [ + ("test_errorAccessors", test_errorAccessors), + ("test_successAccessors", test_successAccessors), + ] +} + extension TransformerTests { // DO NOT MODIFY: This is autogenerated, use: // `swift test --generate-linuxmain` @@ -561,6 +591,7 @@ public func __allTests() -> [XCTestCaseEntry] { testCase(EmptyObjectDecoderTests.__allTests__EmptyObjectDecoderTests), testCase(GenericJSONAPIErrorTests.__allTests__GenericJSONAPIErrorTests), testCase(IncludedTests.__allTests__IncludedTests), + testCase(IncludesDecodingErrorTests.__allTests__IncludesDecodingErrorTests), testCase(LinksTests.__allTests__LinksTests), testCase(NonJSONAPIRelatableTests.__allTests__NonJSONAPIRelatableTests), testCase(PolyProxyTests.__allTests__PolyProxyTests), @@ -571,6 +602,7 @@ public func __allTests() -> [XCTestCaseEntry] { testCase(ResourceObjectTests.__allTests__ResourceObjectTests), testCase(SparseFieldEncoderTests.__allTests__SparseFieldEncoderTests), testCase(SparseFieldsetTests.__allTests__SparseFieldsetTests), + testCase(SuccessAndErrorDocumentTests.__allTests__SuccessAndErrorDocumentTests), testCase(TransformerTests.__allTests__TransformerTests), ] } From c7696d83fa57b9e28ddbd218d6119d05b92da0c3 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Fri, 15 Nov 2019 17:46:53 -0800 Subject: [PATCH 116/235] update Playground pages to run --- .../Contents.swift | 2 +- .../Pages/Usage.xcplaygroundpage/Contents.swift | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/JSONAPI.playground/Pages/Full Client & Server Example.xcplaygroundpage/Contents.swift b/JSONAPI.playground/Pages/Full Client & Server Example.xcplaygroundpage/Contents.swift index eabd561..96a7168 100644 --- a/JSONAPI.playground/Pages/Full Client & Server Example.xcplaygroundpage/Contents.swift +++ b/JSONAPI.playground/Pages/Full Client & Server Example.xcplaygroundpage/Contents.swift @@ -37,7 +37,7 @@ typealias ToManyRelationship = JSONAPI.ToManyRelationship = JSONAPI.Document> +typealias Document = JSONAPI.Document> // MARK: Entity Definitions diff --git a/JSONAPI.playground/Pages/Usage.xcplaygroundpage/Contents.swift b/JSONAPI.playground/Pages/Usage.xcplaygroundpage/Contents.swift index e5434e5..5917bef 100644 --- a/JSONAPI.playground/Pages/Usage.xcplaygroundpage/Contents.swift +++ b/JSONAPI.playground/Pages/Usage.xcplaygroundpage/Contents.swift @@ -64,11 +64,11 @@ if case let .data(bodyData) = peopleResponse.body { // MARK: - Work in the abstract print("-----") -func process(document: T) { - guard case let .data(body) = document.body else { +func process(document: T) { + guard let body = document.body.data else { return } - let x: T.Body.Data = body + let x: T.BodyData = body } process(document: peopleResponse) From 96da1b4e215a5dcd5a9819814b32fbdcb1722723 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Fri, 15 Nov 2019 23:15:32 -0800 Subject: [PATCH 117/235] update documentation --- .../Contents.swift | 2 +- README.md | 692 ++---------------- documentation/usage.md | 611 ++++++++++++++++ 3 files changed, 668 insertions(+), 637 deletions(-) create mode 100644 documentation/usage.md diff --git a/JSONAPI.playground/Pages/Full Client & Server Example.xcplaygroundpage/Contents.swift b/JSONAPI.playground/Pages/Full Client & Server Example.xcplaygroundpage/Contents.swift index 96a7168..280a614 100644 --- a/JSONAPI.playground/Pages/Full Client & Server Example.xcplaygroundpage/Contents.swift +++ b/JSONAPI.playground/Pages/Full Client & Server Example.xcplaygroundpage/Contents.swift @@ -37,7 +37,7 @@ typealias ToManyRelationship = JSONAPI.ToManyRelationship = JSONAPI.Document> +typealias Document = JSONAPI.Document> // MARK: Entity Definitions diff --git a/README.md b/README.md index c535288..7a1c6cd 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ A Swift package for encoding to- and decoding from **JSON API** compliant reques See the JSON API Spec here: https://jsonapi.org/format/ -:warning: This library provides well-tested type safety when working with JSON:API 1.0. However, the Swift compiler can sometimes have difficulty tracking down small typos when initializing `ResourceObjects`. Once the code is written correctly, it will compile, but tracking down the source of programmer errors can be an annoyance. This is mostly a concern when creating resource objects in-code (servers and test cases must do this). Writing a client that uses this framework to ingest JSON API Compliant API responses is much less painful. +:warning: This library provides well-tested type safety when working with JSON:API 1.0. However, the Swift compiler can sometimes have difficulty tracking down small typos when initializing `ResourceObjects`. Correct code will always compile, but tracking down the source of programmer errors can be an annoyance. This is mostly a concern when creating resource objects in-code (i.e. declaratively) like you might for unit testing. Writing a client that uses this framework to ingest and decode JSON API Compliant API responses is much less painful. ## Quick Start @@ -26,69 +26,21 @@ See the JSON API Spec here: https://jsonapi.org/format/ This library works well when used by both the server responsible for serialization and the client responsible for deserialization. Check out the [example](#example) further down in this README. ## Table of Contents - -- [JSONAPI](#jsonapi) +- JSONAPI - [Primary Goals](#primary-goals) - - [Caveat](#caveat) - [Dev Environment](#dev-environment) - [Prerequisites](#prerequisites) - [Swift Package Manager](#swift-package-manager) - - [CocoaPods](#cocoapods) - [Xcode project](#xcode-project) + - [CocoaPods](#cocoapods) - [Running the Playground](#running-the-playground) - [Project Status](#project-status) - - [JSON:API](#jsonapi) - - [Document](#document) - - [Resource Object](#resource-object) - - [Relationship Object](#relationship-object) - - [Links Object](#links-object) - - [Misc](#misc) - - [Testing](#testing) - - [Resource Object Validator](#resource-object-validator) - - [Potential Improvements](#potential-improvements) - - [Usage](#usage) - - [`JSONAPI.ResourceObjectDescription`](#jsonapiresourceobjectdescription) - - [`JSONAPI.ResourceObject`](#jsonapiresourceobject) - - [`Meta`](#meta) - - [`Links`](#links) - - [`MaybeRawId`](#mayberawid) - - [`RawIdType`](#rawidtype) - - [Convenient `typealiases`](#convenient-typealiases) - - [`JSONAPI.Relationships`](#jsonapirelationships) - - [`JSONAPI.Attributes`](#jsonapiattributes) - - [`Transformer`](#transformer) - - [`Validator`](#validator) - - [Computed `Attribute`](#computed-attribute) - - [Copying/Mutating `ResourceObjects`](#copyingmutating-resourceobjects) - - [`JSONAPI.Document`](#jsonapidocument) - - [`ResourceBody`](#resourcebody) - - [nullable `PrimaryResource`](#nullable-primaryresource) - - [`MetaType`](#metatype) - - [`LinksType`](#linkstype) - - [`IncludeType`](#includetype) - - [`APIDescriptionType`](#apidescriptiontype) - - [`Error`](#error) - - [`UnknownJSONAPIError`](#unknownjsonapierror) - - [`BasicJSONAPIError`](#basicjsonapierror) - - [`GenericJSONAPIError`](#genericjsonapierror) - - [`JSONAPI.Meta`](#jsonapimeta) - - [`JSONAPI.Links`](#jsonapilinks) - - [`JSONAPI.RawIdType`](#jsonapirawidtype) - - [Sparse Fieldsets](#sparse-fieldsets) - - [Supporting Sparse Fieldset Encoding](#supporting-sparse-fieldset-encoding) - - [Sparse Fieldset `typealias` comparisons](#sparse-fieldset-typealias-comparisons) - - [Replacing and Tapping Attributes/Relationships](#replacing-and-tapping-attributesrelationships) - - [Tapping](#tapping) - - [Replacing](#replacing) - - [Custom Attribute or Relationship Key Mapping](#custom-attribute-or-relationship-key-mapping) - - [Custom Attribute Encode/Decode](#custom-attribute-encodedecode) - - [Meta-Attributes](#meta-attributes) - - [Meta-Relationships](#meta-relationships) - [Example](#example) - - [Preamble (Setup shared by server and client)](#preamble-setup-shared-by-server-and-client) - - [Server Pseudo-example](#server-pseudo-example) - - [Client Pseudo-example](#client-pseudo-example) + - [Usage](./documentation/usage.md) - [JSONAPI+Testing](#jsonapitesting) + - [Literal Expressibility](#literal-expressibility) + - [Resource Object `check()`](#resource-object-check) + - [Comparisons](#comparisons) - [JSONAPI+Arbitrary](#jsonapiarbitrary) - [JSONAPI+OpenAPI](#jsonapiopenapi) @@ -99,6 +51,7 @@ The primary goals of this framework are: 2. Leverage `Codable` to avoid additional outside dependencies and get operability with non-JSON encoders/decoders for free. 3. Do not sacrifice type safety. 4. Be platform agnostic so that Swift code can be written once and used by both the client and the server. +5. Provide _human readable_ error output. The errors thrown when decoding an API response and the results of the `JSONAPITesting` framework's `compare(to:)` functions all have digestible human readable descriptions (just use `String(describing:)`). ### Caveat The big caveat is that, although the aim is to support the JSON API spec, this framework ends up being _naturally_ opinionated about certain things that the API Spec does not specify. These caveats are largely a side effect of attempting to write the library in a "Swifty" way. @@ -108,7 +61,7 @@ If you find something wrong with this library and it isn't already mentioned und ## Dev Environment ### Prerequisites 1. Swift 5.1+ -2. Swift Package Manager *OR* Cocoapods +2. Swift Package Manager, Xcode 11+, or Cocoapods ### Swift Package Manager Just include the following in your package's dependencies and add `JSONAPI` to the dependencies for any of your targets. @@ -116,6 +69,12 @@ Just include the following in your package's dependencies and add `JSONAPI` to t .package(url: "https://github.com/mattpolzin/JSONAPI.git", .upToNextMajor(from: "2.2.0")) ``` +### Xcode project +To create an Xcode project for JSONAPI, run +`swift package generate-xcodeproj` + +With Xcode 11+ you can also just open the folder containing your clone of this repository and begin working. + ### CocoaPods To use this framework in your project via Cocoapods, add the following dependencies to your Podfile. ``` @@ -123,12 +82,6 @@ To use this framework in your project via Cocoapods, add the following dependenc pod 'MP-JSONAPI', :git => 'https://github.com/mattpolzin/JSONAPI.git' ``` -### Xcode project -To create an Xcode project for JSONAPI, run -`swift package generate-xcodeproj` - -With Xcode 11+ you can also just open the folder containing your clone of this repository and begin working. - ### Running the Playground To run the included Playground files, create an Xcode project using Swift Package Manager, then create an Xcode Workspace in the root of the repository and add both the generated Xcode project and the playground to the Workspace. @@ -181,575 +134,6 @@ These ideas could be implemented in future versions. - [ ] Support sideposting. JSONAPI spec might become opinionated in the future (https://github.com/json-api/json-api/pull/1197, https://github.com/json-api/json-api/issues/1215, https://github.com/json-api/json-api/issues/1216) but there is also an existing implementation to consider (https://jsonapi-suite.github.io/jsonapi_suite/ruby/writes/nested-writes). At this time, any sidepost implementation would be an awesome tertiary library to be used alongside the primary JSONAPI library. Maybe `JSONAPISideloading`. - [ ] Error or warning if an included resource object is not related to a primary resource object or another included resource object (Turned off or at least not throwing by default). -## Usage - -In this documentation, in order to draw attention to the difference between the `JSONAPI` framework (this Swift library) and the **JSON API Spec** (the specification this library helps you follow), the specification will consistently be referred to below as simply the **SPEC**. - -### `JSONAPI.ResourceObjectDescription` - -A `ResourceObjectDescription` is the `JSONAPI` framework's representation of what the **SPEC** calls a *Resource Object*. You might create the following `ResourceObjectDescription` to represent a person in a network of friends: - -```swift -enum PersonDescription: IdentifiedResourceObjectDescription { - static var jsonType: String { return "people" } - - struct Attributes: JSONAPI.Attributes { - let name: Attribute<[String]> - let favoriteColor: Attribute - } - - struct Relationships: JSONAPI.Relationships { - let friends: ToManyRelationship - } -} -``` - -The requirements of a `ResourceObjectDescription` are: -1. A static `var` "jsonType" that matches the JSON type; The **SPEC** requires every *Resource Object* to have a "type". -2. A `struct` of `Attributes` **- OR -** `typealias Attributes = NoAttributes` -3. A `struct` of `Relationships` **- OR -** `typealias Relationships = NoRelationships` - -Note that an `enum` type is used here for the `ResourceObjectDescription`; it could have been a `struct`, but `ResourceObjectDescription`s do not ever need to be created so an `enum` with no `case`s is a nice fit for the job. - -This readme doesn't go into detail on the **SPEC**, but the following *Resource Object* would be described by the above `PersonDescription`: - -```json -{ - "type": "people", - "id": "9", - "attributes": { - "name": [ - "Jane", - "Doe" - ], - "favoriteColor": "Green" - }, - "relationships": { - "friends": { - "data": [ - { - "id": "7", - "type": "people" - }, - { - "id": "8", - "type": "people" - } - ] - } - } -} -``` - -### `JSONAPI.ResourceObject` - -Once you have a `ResourceObjectDescription`, you _create_, _encode_, and _decode_ `ResourceObjects` that "fit the description". If you have a `CreatableRawIdType` (see the section on `RawIdType`s below) then you can create new `ResourceObjects` that will automatically be given unique Ids, but even without a `CreatableRawIdType` you can encode, decode and work with resource objects. - -The `ResourceObject` and `ResourceObjectDescription` together with a `JSONAPI.Meta` type and a `JSONAPI.Links` type embody the rules and properties of a JSON API *Resource Object*. - -A `ResourceObject` needs to be specialized on four generic types. The first is the `ResourceObjectDescription` described above. The others are a `Meta`, `Links`, and `MaybeRawId`. - -#### `Meta` - -The second generic specialization on `ResourceObject` is `Meta`. This is described in its own section [below](#jsonapimeta). All `Meta` at any level of a JSON API Document follow the same rules. You can use `NoMetadata` if you do not need to package any metadata with the `ResourceObject`. - -#### `Links` - -The third generic specialization on `ResourceObject` is `Links`. This is described in its own section [below](#jsonnapilinks). All `Links` at any level of a JSON API Document follow the same rules, although the **SPEC** makes different suggestions as to what types of links might live on which parts of the Document. You can use `NoLinks` if you do not need to package any links with the `ResourceObject`. - -#### `MaybeRawId` - -The last generic specialization on `ResourceObject` is `MaybeRawId`. This is either a `RawIdType` that can be used to uniquely identify `ResourceObjects` or it is `Unidentified` which is used to indicate a `ResourceObject` does not have an `Id` (which is useful when a client is requesting that the server create a `ResourceObject` and assign it a new `Id`). - -##### `RawIdType` - -The raw type of `Id` to use for the `ResourceObject`. The actual `Id` of the `ResourceObject` will not be a `RawIdType`, though. The `Id` will package a value of `RawIdType` with a specialized reference back to the `ResourceObject` type it identifies. This just looks like `Id>`. - -Having the `ResourceObject` type associated with the `Id` makes it easy to store all of your resource objects in a hash broken out by `ResourceObject` type; You can pass `Ids` around and always know where to look for the `ResourceObject` to which the `Id` refers. This encapsulation provides some type safety because the Ids of two `ResourceObjects` with the "raw ID" of `"1"` but different types will not compare as equal. - -A `RawIdType` is the underlying type that uniquely identifies a `ResourceObject`. This is often a `String` or a `UUID`. - -#### Convenient `typealiases` - -Often you can use one `RawIdType` for many if not all of your `ResourceObjects`. That means you can save yourself some boilerplate by using `typealias`es like the following: -```swift -public typealias ResourceObject = JSONAPI.ResourceObject - -public typealias NewResourceObject = JSONAPI.ResourceObject -``` - -It can also be nice to create a `typealias` for each type of resource object you want to work with: -```swift -typealias Person = ResourceObject - -typealias NewPerson = NewResourceObject -``` - -Note that I am assuming an unidentified person is a "new" person. I suspect that is generally an acceptable conflation because the only time the **SPEC** allows a *Resource Object* to be encoded without an `Id` is when a client is requesting the given *Resource Object* be created by the server and the client wants the server to create the `Id` for that object. - -### `JSONAPI.Relationships` - -There are two types of `Relationships`: `ToOneRelationship` and `ToManyRelationship`. A `ResourceObjectDescription`'s `Relationships` type can contain any number of `Relationship` properties of either of these types. Do not store anything other than `Relationship` properties in the `Relationships` struct of a `ResourceObjectDescription`. - -In addition to identifying resource objects by Id and type, `Relationships` can contain `Meta` or `Links` that follow the same rules as [`Meta`](#jsonapimeta) and [`Links`](#jsonapilinks) elsewhere in the JSON API Document. - -To describe a relationship that may be omitted (i.e. the key is not even present in the JSON object), you make the entire `ToOneRelationship` or `ToManyRelationship` optional. However, this is not recommended because you can also represent optional relationships as nullable which means the key is always present. A `ToManyRelationship` can naturally represent the absence of related values with an empty array, so `ToManyRelationship` does not support nullability at all. A `ToOneRelationship` can be marked as nullable (i.e. the value could be either `null` or a resource identifier) like this: -```swift -let nullableRelative: ToOneRelationship -``` - -A `ResourceObject` that does not have relationships can be described by adding the following to a `ResourceObjectDescription`: -```swift -typealias Relationships = NoRelationships -``` - -`Relationship` values boil down to `Ids` of other resource objects. To access the `Id` of a related `ResourceObject`, you can use the custom `~>` operator with the `KeyPath` of the `Relationship` from which you want the `Id`. The friends of the above `Person` `ResourceObject` can be accessed as follows (type annotations for clarity): -```swift -let friendIds: [Person.Identifier] = person ~> \.friends -``` - -### `JSONAPI.Attributes` - -The `Attributes` of a `ResourceObjectDescription` can contain any JSON encodable/decodable types as long as they are wrapped in an `Attribute`, `ValidatedAttribute`, or `TransformedAttribute` `struct`. - -To describe an attribute that may be omitted (i.e. the key might not even be in the JSON object), you make the entire `Attribute` optional: -```swift -let optionalAttribute: Attribute? -``` - -To describe an attribute that is expected to exist but might have a `null` value, you make the value within the `Attribute` optional: -```swift -let nullableAttribute: Attribute -``` - -A resource object that does not have attributes can be described by adding the following to an `ResourceObjectDescription`: -```swift -typealias Attributes = NoAttributes -``` - -As of Swift 5.1, `Attributes` can be accessed via dynamic member keypath lookup as follows: -```swift -let favoriteColor: String = person.favoriteColor -``` - -:warning: `Attributes` can also be accessed via the older `subscript` operator, but this is a deprecated feature that will be removed in the next major version: -```swift -let favoriteColor: String = person[\.favoriteColor] -``` - -#### `Transformer` - -Sometimes you need to use a type that does not encode or decode itself in the way you need to represent it as a serialized JSON object. For example, the Swift `Foundation` type `Date` can encode/decode itself to `Double` out of the box, but you might want to represent dates as ISO 8601 compliant `String`s instead. The Foundation library `JSONDecoder` has a setting to make this adjustment, but for the sake of an example, you could create a `Transformer`. - -A `Transformer` just provides one static function that transforms one type to another. You might define one for an ISO 8601 compliant `Date` like this: -```swift -enum ISODateTransformer: Transformer { - public static func transform(_ value: String) throws -> Date { - // parse Date out of input and return - } -} -``` - -Then you define the attribute as a `TransformedAttribute` instead of an `Attribute`: -```swift -let date: TransformedAttribute -``` - -Note that the first generic parameter of `TransformAttribute` is the type you expect to decode from JSON, not the type you want to end up with after transformation. - -If you make your `Transformer` a `ReversibleTransformer` then your life will be a bit easier when you construct `TransformedAttributes` because you have access to initializers for both the pre- and post-transformed value types. Continuing with the above example of a `ISODateTransformer`: -```swift -extension ISODateTransformer: ReversibleTransformer { - public static func reverse(_ value: Date) throws -> String { - // serialize Date to a String - } -} - -let exampleAttribute = try? TransformedAttribute(transformedValue: Date()) -let otherAttribute = try? TransformedAttribute(rawValue: "2018-12-01 09:06:41 +0000") -``` - -#### `Validator` - -You can also creator `Validators` and `ValidatedAttribute`s. A `Validator` is just a `Transformer` that by convention does not perform a transformation. It simply `throws` if an attribute value is invalid. - -#### Computed `Attribute` - -You can add computed properties to your `ResourceObjectDescription.Attributes` struct if you would like to expose attributes that are not explicitly represented by the JSON. These computed properties do not have to be wrapped in `Attribute`, `ValidatedAttribute`, or `TransformedAttribute`. This allows computed attributes to be of types that are not `Codable`. Here's an example of how you might take the `person.name` attribute from the example above and create a `fullName` computed property. - -```swift -public var fullName: Attribute { - return name.map { $0.joined(separator: " ") } -} -``` - -If your computed property is wrapped in a `AttributeType` then you can still use the default subscript operator to access it (as would be the case with the `person.fullName` example above). However, if you add a property to the `Attributes` `struct` that is not wrapped in an `AttributeType`, you must either access it from its full path (`person.attributes.newThing`) or with the "direct" subscript accessor (`person[direct: \.newThing]`). This keeps the subscript access unambiguous enough for the compiler to be helpful prior to explicitly casting, comparing, or storing the result. - -### Copying/Mutating `ResourceObjects` -`ResourceObject` is a value type, so copying is its default behavior. There are two common mutations you might want to make when copying a `ResourceObject`: -1. Assigning a new `Identifier` to the copy of an identified `ResourceObject`. -2. Assigning a new `Identifier` to the copy of an unidentified `ResourceObject`. - -The above can be accomplished with code like the following: - -```swift -// use case 1 -let person1 = person.withNewIdentifier() - -// use case 2 -let newlyIdentifiedPerson1 = unidentifiedPerson.identified(byType: String.self) - -let newlyIdentifiedPerson2 = unidentifiedPerson.identified(by: "2232") -``` - -### `JSONAPI.Document` - -The entirety of a JSON API request or response is encoded or decoded from- or to a `Document`. As an example, a JSON API response containing one `Person` and no included resource objects could be decoded as follows: -```swift -let decoder = JSONDecoder() - -let responseStructure = JSONAPI.Document, NoMetadata, NoLinks, NoIncludes, BasicJSONAPIError>.self - -let document = try decoder.decode(responseStructure, from: data) -``` - -A JSON API Document is guaranteed by the **SPEC** to be "data", "metadata", or "errors." If it is "data", it may also contain "metadata" and/or other "included" resources. If it is "errors," it may also contain "metadata." - -#### `ResourceBody` - -The first generic type of a `JSONAPIDocument` is a `ResourceBody`. This can either be a `SingleResourceBody` or a `ManyResourceBody`. You will find zero or one `PrimaryResource` values in a JSON API document that has a `SingleResourceBody` and you will find zero or more `PrimaryResource` values in a JSON API document that has a `ManyResourceBody`. You can use the `Poly` types (`Poly1` through `Poly6`) to specify that a `ResourceBody` will be one of a few different types of `ResourceObject`. These `Poly` types work in the same way as the `Include` types described below. - -If you expect a response to not have a "data" top-level key at all, then use `NoResourceBody` instead. - -##### nullable `PrimaryResource` - -If you expect a `SingleResourceBody` to sometimes come back `null`, you should make your `PrimaryResource` optional. If you do not make your `PrimaryResource` optional then a `null` primary resource will be considered an error when parsing the JSON. - -You cannot, however, use an optional `PrimaryResource` with a `ManyResourceBody` because the **SPEC** requires that an empty document in that case be represented by an empty array rather than `null`. - -#### `MetaType` - -The second generic type of a `JSONAPIDocument` is a `Meta`. This `Meta` follows the same rules as `Meta` at any other part of a JSON API Document. It is described below in its own section, but as an example, the JSON API document could contain the following pagination info in its meta entry: -```json -{ - "meta": { - "total": 100, - "limit": 50, - "offset": 50 - } -} -``` - -You would then create the following `Meta` type: -```swift -struct PageMetadata: JSONAPI.Meta { - let total: Int - let limit: Int - let offset: Int -} -``` - -You can always use `NoMetadata` if this JSON API feature is not needed. - -#### `LinksType` - -The third generic type of a `JSONAPIDocument` is a `Links` struct. `Links` are described in their own section [below](#jsonapilinks). - -#### `IncludeType` - -The fourth generic type of a `JSONAPIDocument` is an `Include`. This type controls which types of `ResourceObject` are looked for when decoding the "included" part of the JSON API document. If you do not expect any included resource objects to be in the document, `NoIncludes` is the way to go. The `JSONAPI` framework provides `Include`s for up to 10 types of included resource objects. These are named `Include1`, `Include2`, `Include3`, and so on. - -**IMPORTANT**: The number trailing "Include" in these type names does not indicate a number of included resource objects, it indicates a number of _types_ of included resource objects. `Include1` can be used to decode any number of included resource objects as long as all the resource objects are of the same _type_. - -To specify that we expect friends of a person to be included in the above example `JSONAPIDocument`, we would use `Include1` instead of `NoIncludes`. - -#### `APIDescriptionType` - -The fifth generic type of a `JSONAPIDocument` is an `APIDescription`. The type represents the "JSON:API Object" described by the **SPEC**. This type describes the highest version of the **SPEC** supported and can carry additional metadata to describe the API. - -You can specify this is not part of the document by using the `NoAPIDescription` type. - -You can describe the API by a version with no metadata by using `APIDescription`. - -You can supply any `JSONAPI.Meta` type as the metadata type of the API description. - -#### `Error` - -The final generic type of a `JSONAPIDocument` is the `Error`. - -You can either create an error type that can handle all the errors you expect your `JSONAPIDocument` to be able to encode/decode or use an out-of-box error type described here. As prescribed by the **SPEC**, these errors will be found under the root document key `errors`. - -##### `UnknownJSONAPIError` -The `UnknownJSONAPIError` type will always succeed in parsing errors but it will not give you any information about what error occurred. You will generally get more bang for your buck out of the next error type described. - -##### `BasicJSONAPIError` -The `BasicJSONAPIError` type will always succeed unless it is faced with an `id` field of an unexpected type, although it still "succeeds" in falling back to its `.unknown` case when that happens. This type extracts _most_ of the fields the **SPEC** describes [here](https://jsonapi.org/format/#error-objects). Because all of these fields are optional in the **SPEC**, they are optional on the `BasicJSONAPIError` type. You will have to create your own error type if you want to define certain fields as non-optional or parse metadata or links out of error objects. - -🗒Metadata and links are supported at the Document level for error responses, the are just not supported hanging off of the individual errors in the `errors` array of the response when using this error type. - -The `BasicJSONAPIError` type is generic on one thing: The type it expects for the `id` field. If you expect integer `ids` back, you use `BasicJSONAPIError`. The same can be done for `String` or any other type that is both `Codable` and `Equatable`. You can even employ something like `AnyCodable` from *Flight-School* as your id field type. If you only need to handle a small subset of possible `id` field types, you can also use the `Poly` library that is already a dependency of `JSONAPI`. For example, you might expect a mix of `String` and `Int` ids for some reason: `BasicJSONAPIError>`. - -The two easiest ways to access the available properties of an error response are under the `payload` property of the error (this property is `nil` if the error was parsed as `.unknown`) or by asking the error for its `definedFields` dictionary. - -As an example, let's say you have the following `Document` type that is destined for errors: -```swift -typealias ErrorDoc = JSONAPI.Document> -``` -And you've parsed an error response -```swift -let errorResponse = try! JSONDecoder().decode(ErrorDoc.self, from: mockErrorData) -``` -You can get at the `Document` body and errors in a couple of different ways, but for one you can switch on the body: -```swift -switch errorResponse.body { -case .data: - print("cool, data!") - -case .errors(let errors, let meta, let links): - let errorDetails = errors.compactMap { $0.payload?.detail } - - print("error details: \(errorDetails)") -} -``` - -##### `GenericJSONAPIError` -This type makes it simple to use your own error payload structures as `JSONAPIError` types. Simply define a `Codable` and `Equatable` struct and then use `GenericJSONAPIError` as the error type for a `Document`. - -### `JSONAPI.Meta` - -A `Meta` struct is totally open-ended. It is described by the **SPEC** as a place to put any information that does not fit into the standard JSON API Document structure anywhere else. - -You can specify `NoMetadata` if the part of the document being described should not contain any `Meta`. - -If you need to support metadata with structure that is not pre-determined, consider an "Any Codable" type such as that found at https://github.com/Flight-School/AnyCodable. - -### `JSONAPI.Links` - -A `Links` struct must contain only `Link` properties. Each `Link` property can either be a `URL` or a `URL` and some `Meta`. Each part of the document has some suggested common `Links` to include but generally any link can be included. - -You can specify `NoLinks` if the part of the document being described should not contain any `Links`. - -### `JSONAPI.RawIdType` - -If you want to create new `JSONAPI.ResourceObject` values and assign them Ids then you will need to conform at least one type to `CreatableRawIdType`. Doing so is easy; here are two example conformances for `UUID` and `String` (via `UUID`): -```swift -extension UUID: CreatableRawIdType { - public static func unique() -> UUID { - return UUID() - } -} - -extension String: CreatableRawIdType { - public static func unique() -> String { - return UUID().uuidString - } -} -``` - -### Sparse Fieldsets -Sparse Fieldsets are currently supported when encoding only. When decoding, Sparse Fieldsets become tricker to support under the current types this library uses and it is assumed that clients will request one or maybe two sparse fieldset combinations for any given model at most so it can simply define the `JSONAPI` models needed to decode those subsets of all possible fields. A server, on the other hand, likely needs to support arbitrary combinations of sparse fieldsets and this library provides a mechanism for encoding those sparse fieldsets without too much extra footwork. - -You can use sparse fieldsets on the primary resources(s) _and_ includes of a `JSONAPI.Document`. - -There is a sparse fieldsets example included with this repository as a Playground page. - -#### Supporting Sparse Fieldset Encoding -1. The `JSONAPI` `ResourceObjectDescription`'s `Attributes` struct must conform to `JSONAPI.SparsableAttributes` rather than `JSONAPI.Attributes`. -2. The `JSONAPI` `ResourceObjectDescription`'s `Attributes` struct must contain a `CodingKeys` enum that conforms to `JSONAPI.SparsableCodingKey` instead of `Swift.CodingKey`. -3. `typealiases` you may have created for `JSONAPI.Document` that allow you to decode Documents will not support the "encode-only" nature of sparse fieldsets. See the next section for `typealias` comparisons. -4. To create a sparse fieldset from a `ResourceObject` just call its `sparse(with: fields)` method and pass an array of `Attributes.CodingKeys` values you would like included in the encoding. -5. Initialize and encode a `Document` containing one or more sparse or full primary resource(s) and any number of sparse or full includes. - -#### Sparse Fieldset `typealias` comparisons -You might have found a `typealias` like the following for encoding/decoding `JSONAPI.Document`s (note the primary resource body is a `JSONAPI.ResourceBody`): -```swift -typealias Document = JSONAPI.Document> -``` - -In order to support sparse fieldsets (which are encode-only), the following companion `typealias` would be useful (note the primary resource body is a `JSONAPI.EncodableResourceBody`): -```swift -typealias SparseDocument = JSONAPI.Document> -``` - -### Replacing and Tapping Attributes/Relationships -When you are working with an immutable Resource Object, it can be useful to replace its attributes or relationships. As a client, you might receive a resource from the server, update something, and then send the server a PATCH request. - -`ResourceObject` is immutable, but you can create a new copy of a `ResourceObject` having updated attributes or relationships. - -#### Tapping -If your `Attributes` or `Relationships` struct is mutable (i.e. its properties are `var`s) then you may find `ResourceObject`'s `tappingAttributes()` and `tappingRelationships()` functions useful. For both, you pass a function that takes an `inout` copy of the respective object or value that you can mutate. The mutated value is then used to create a new `ResourceObject`. - -For example, to take a hypothetical `Dog` resource object and change the name attribute: -```swift -let resourceObject = Dog(...) - -let newResourceObject = resourceObject - .tappingAttributes { $0.name = .init(value: "Charlie") } -``` - -#### Replacing -If your `Attributes` or `Relationships` struct is immutable (i.e. its properties are `let`s) then you may find `ResourceObject`'s `replacingAttributes()` and `replacingRelationships()` functions useful. For both, you pass a function that takes the current attributes or relationships and you return a new value. The new value is then used to create a new `ResourceObject`. - -For example, to take a hypothetical `Dog` resource object and change the name attribute: -```swift -let resourceObject = Dog(...) - -let newResourceObject = resourceObject - .replacingAttributes { _ in - return Dog.Attributes(name: .init(value: "Charlie")) -} -``` - -### Custom Attribute or Relationship Key Mapping -There is not anything special going on at the `JSONAPI.Attributes` and `JSONAPI.Relationships` levels, so you can easily provide custom key mappings by taking advantage of `Codable`'s `CodingKeys` pattern. Here are two models that will encode/decode equivalently but offer different naming in your codebase: -```swift -public enum ResourceObjectDescription1: JSONAPI.ResourceObjectDescription { - public static var jsonType: String { return "entity" } - - public struct Attributes: JSONAPI.Attributes { - public let coolProperty: Attribute - } - - public typealias Relationships = NoRelationships -} - -public enum ResourceObjectDescription2: JSONAPI.ResourceObjectDescription { - public static var jsonType: String { return "entity" } - - public struct Attributes: JSONAPI.Attributes { - public let wholeOtherThing: Attribute - - enum CodingKeys: String, CodingKey { - case wholeOtherThing = "coolProperty" - } - } - - public typealias Relationships = NoRelationships -} -``` - -### Custom Attribute Encode/Decode -You can safely provide your own encoding or decoding functions for your Attributes struct if you need to as long as you are careful that your encode operation correctly reverses your decode operation. Although this is generally not necessary, `AttributeType` provides a convenience method to make your decoding a bit less boilerplate ridden. This is what it looks like: -```swift -public enum ResourceObjectDescription1: JSONAPI.ResourceObjectDescription { - public static var jsonType: String { return "entity" } - - public struct Attributes: JSONAPI.Attributes { - public let property1: Attribute - public let property2: Attribute - public let property3: Attribute - - public let weirdThing: Attribute - - enum CodingKeys: String, CodingKey { - case property1 - case property2 - case property3 - } - } - - public typealias Relationships = NoRelationships -} - -extension ResourceObjectDescription1.Attributes { - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - - property1 = try .defaultDecoding(from: container, forKey: .property1) - property2 = try .defaultDecoding(from: container, forKey: .property2) - property3 = try .defaultDecoding(from: container, forKey: .property3) - - weirdThing = .init(value: "hello world") - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - - try container.encode(property1, forKey: .property1) - try container.encode(property2, forKey: .property2) - try container.encode(property3, forKey: .property3) - } -} -``` - -### Meta-Attributes -This advanced feature may not ever be useful, but if you find yourself in the situation of dealing with an API that does not 100% follow the **SPEC** then you might find meta-attributes are just the thing to make your resource objects more natural to work with. - -Suppose, for example, you are presented with the unfortunate situation where a piece of information you need is only available as part of the `Id` of a resource object. Perhaps a user's `Id` is formatted "{integer}-{createdAt}" where "createdAt" is the unix timestamp when the user account was created. The following `UserDescription` will expose what you need as an attribute. Realistically, the following example code is still terrible for its error handling. Using a `Result` type and/or invariants would clean things up substantially. - -```swift -enum UserDescription: ResourceObjectDescription { - public static var jsonType: String { return "users" } - - struct Attributes: JSONAPI.Attributes { - var createdAt: (User) -> Date { - return { user in - let components = user.id.rawValue.split(separator: "-") - - guard components.count == 2 else { - assertionFailure() - return Date() - } - - let timestamp = TimeInterval(components[1]) - - guard let date = timestamp.map(Date.init(timeIntervalSince1970:)) else { - assertionFailure() - return Date() - } - - return date - } - } - } - - typealias Relationships = NoRelationships -} - -typealias User = JSONAPI.ResourceObject -``` - -Given a value `user` of the above resource object type, you can access the `createdAt` attribute just like you would any other: - -```swift -let createdAt = user.createdAt -``` - -This works because `createdAt` is defined in the form: `var {name}: ({ResourceObject}) -> {Value}` where `{ResourceObject}` is the `JSONAPI.ResourceObject` described by the `ResourceObjectDescription` containing the meta-attribute. - -### Meta-Relationships -This advanced feature may not ever be useful, but if you find yourself in the situation of dealing with an API that does not 100% follow the **SPEC** then you might find meta-relationships are just the thing to make your resource objects more natural to work with. - -Similarly to Meta-Attributes, Meta-Relationships allow you to represent non-compliant relationships as computed relationship properties. In the following example, a relationship is created from some attributes on the JSON model. - -```swift -enum UserDescription: ResourceObjectDescription { - public static var jsonType: String { return "users" } - - struct Attributes: JSONAPI.Attributes { - let friend_id: Attribute - } - - struct Relationships: JSONAPI.Relationships { - public var friend: (User) -> User.Identifier { - return { user in - return User.Identifier(rawValue: user.friend_id) - } - } - } -} - -typealias User = JSONAPI.ResourceObject -``` - -Given a value `user` of the above resource object type, you can access the `friend` relationship just like you would any other: - -```swift -let friendId = user ~> \.friend -``` - -This works because `friend` is defined in the form: `var {name}: ({ResourceObject}) -> {Identifier}` where `{ResourceObject}` is the `JSONAPI.ResourceObject` described by the `ResourceObjectDescription` containing the meta-relationship. - ## Example The following serves as a sort of pseudo-example. It skips server/client implementation details not related to JSON:API but still gives a more complete picture of what an implementation using this framework might look like. You can play with this example code in the Playground provided with this repo. @@ -785,7 +169,7 @@ typealias ToManyRelationship = JSONAPI.ToManyRelationship = JSONAPI.Document> +typealias Document = JSONAPI.Document> // MARK: Entity Definitions @@ -926,14 +310,50 @@ print(response.author) ``` # JSONAPI+Testing -The `JSONAPI` framework is packaged with a test library to help you test your `JSONAPI` integration. +The `JSONAPI` framework is packaged with a test library to help you test your `JSONAPI` integration. The test library is called `JSONAPITesting`. You can see `JSONAPITesting` in action in the Playground included with the `JSONAPI` repository. + +## Literal Expressibility +Literal expressibility for `Attribute`, `ToOneRelationship`, and `Id` are provided so that you can easily write test `ResourceObject` values into your unit tests. + +For example, you could create a mock `Author` (from the above example) as follows +```swift +let author = Author(id: "1234", // You can just use a String directly as an Id + attributes: .init(name: "Janice Bluff"), // The name Attribute does not need to be initialized, you just use a String directly. + relationships: .none, + meta: .none, + links: .none) +``` + +## Resource Object `check()` +The `ResourceObject` gets a `check()` function that can be used to catch problems with your `JSONAPI` structures that are not caught by Swift's type system. + +To catch malformed `JSONAPI.Attributes` and `JSONAPI.Relationships`, just call `check()` in your unit test functions: +```swift +func test_initAuthor() { + let author = Author(...) + Author.check(author) +} +``` + +## Comparisons +You can compare `Documents`, `ResourceObjects`, `Attributes`, etc. and get human-readable output using the `compare(to:)` methods included with `JSONAPITesting`. + +```swift +func test_articleResponse() { + let endToEndAPITestResponse: SingleArticleDocumentWithIncludes = ... -The test library is called `JSONAPITesting`. It provides literal expressibility for `Attribute`, `ToOneRelationship`, and `Id` in many situations so that you can easily write test `ResourceObject` values into your unit tests. It also provides a `check()` function for each `ResourceObject` type that can be used to catch problems with your `JSONAPI` structures that are not caught by Swift's type system. + let expectedResponse: SingleArticleDocumentWithIncludes = ... -You can see the `JSONAPITesting` in action in the Playground included with the `JSONAPI` repository. + let comparison = endToEndAPITestResponse.compare(to: expectedResponse) + + XCTAssert(comparison.isSame, String(describing: comparison)) +} +``` # JSONAPI+Arbitrary -This library has moved into its own Package. See https://github.com/mattpolzin/JSONAPI-Arbitrary for more information. +The `JSONAPI+Arbitrary` library provides `SwiftCheck` `Arbitrary` conformance for many of teh `JSONAPI` types. + +See https://github.com/mattpolzin/JSONAPI-Arbitrary for more information. # JSONAPI+OpenAPI The `JSONAPI+OpenAPI` library generates OpenAPI compliant JSON Schema for models built with the `JSONAPI` library. If your Swift code is your preferred source of truth for API information, this is an easy way to document the response schemas of your API. diff --git a/documentation/usage.md b/documentation/usage.md new file mode 100644 index 0000000..7f6e8ee --- /dev/null +++ b/documentation/usage.md @@ -0,0 +1,611 @@ + +## Usage + +In this documentation, in order to draw attention to the difference between the `JSONAPI` framework (this Swift library) and the **JSON API Spec** (the specification this library helps you follow), the specification will consistently be referred to below as simply the **SPEC**. + + + +- [`JSONAPI.ResourceObjectDescription`](#jsonapiresourceobjectdescription) +- [`JSONAPI.ResourceObject`](#jsonapiresourceobject) + - [`Meta`](#meta) + - [`Links`](#links) + - [`MaybeRawId`](#mayberawid) + - [`RawIdType`](#rawidtype) + - [Convenient `typealiases`](#convenient-typealiases) +- [`JSONAPI.Relationships`](#jsonapirelationships) +- [`JSONAPI.Attributes`](#jsonapiattributes) + - [`Transformer`](#transformer) + - [`Validator`](#validator) + - [Computed `Attribute`](#computed-attribute) +- [Copying/Mutating `ResourceObjects`](#copyingmutating-resourceobjects) +- [`JSONAPI.Document`](#jsonapidocument) + - [`ResourceBody`](#resourcebody) + - [nullable `PrimaryResource`](#nullable-primaryresource) + - [`MetaType`](#metatype) + - [`LinksType`](#linkstype) + - [`IncludeType`](#includetype) + - [`APIDescriptionType`](#apidescriptiontype) + - [`Error`](#error) + - [`UnknownJSONAPIError`](#unknownjsonapierror) + - [`BasicJSONAPIError`](#basicjsonapierror) + - [`GenericJSONAPIError`](#genericjsonapierror) +- [`JSONAPI.Meta`](#jsonapimeta) +- [`JSONAPI.Links`](#jsonapilinks) +- [`JSONAPI.RawIdType`](#jsonapirawidtype) +- [Sparse Fieldsets](#sparse-fieldsets) + - [Supporting Sparse Fieldset Encoding](#supporting-sparse-fieldset-encoding) + - [Sparse Fieldset `typealias` comparisons](#sparse-fieldset-typealias-comparisons) +- [Replacing and Tapping Attributes/Relationships](#replacing-and-tapping-attributesrelationships) + - [Tapping](#tapping) + - [Replacing](#replacing) +- [Custom Attribute or Relationship Key Mapping](#custom-attribute-or-relationship-key-mapping) +- [Custom Attribute Encode/Decode](#custom-attribute-encodedecode) +- [Meta-Attributes](#meta-attributes) +- [Meta-Relationships](#meta-relationships) + + + +### `JSONAPI.ResourceObjectDescription` + +A `ResourceObjectDescription` is the `JSONAPI` framework's representation of what the **SPEC** calls a *Resource Object*. You might create the following `ResourceObjectDescription` to represent a person in a network of friends: + +```swift +enum PersonDescription: IdentifiedResourceObjectDescription { + static var jsonType: String { return "people" } + + struct Attributes: JSONAPI.Attributes { + let name: Attribute<[String]> + let favoriteColor: Attribute + } + + struct Relationships: JSONAPI.Relationships { + let friends: ToManyRelationship + } +} +``` + +The requirements of a `ResourceObjectDescription` are: +1. A static `var` "jsonType" that matches the JSON type; The **SPEC** requires every *Resource Object* to have a "type". +2. A `struct` of `Attributes` **- OR -** `typealias Attributes = NoAttributes` +3. A `struct` of `Relationships` **- OR -** `typealias Relationships = NoRelationships` + +Note that an `enum` type is used here for the `ResourceObjectDescription`; it could have been a `struct`, but `ResourceObjectDescription`s do not ever need to be created so an `enum` with no `case`s is a nice fit for the job. + +This readme doesn't go into detail on the **SPEC**, but the following *Resource Object* would be described by the above `PersonDescription`: + +```json +{ + "type": "people", + "id": "9", + "attributes": { + "name": [ + "Jane", + "Doe" + ], + "favoriteColor": "Green" + }, + "relationships": { + "friends": { + "data": [ + { + "id": "7", + "type": "people" + }, + { + "id": "8", + "type": "people" + } + ] + } + } +} +``` + +### `JSONAPI.ResourceObject` + +Once you have a `ResourceObjectDescription`, you _create_, _encode_, and _decode_ `ResourceObjects` that "fit the description". If you have a `CreatableRawIdType` (see the section on `RawIdType`s below) then you can create new `ResourceObjects` that will automatically be given unique Ids, but even without a `CreatableRawIdType` you can encode, decode and work with resource objects. + +The `ResourceObject` and `ResourceObjectDescription` together with a `JSONAPI.Meta` type and a `JSONAPI.Links` type embody the rules and properties of a JSON API *Resource Object*. + +A `ResourceObject` needs to be specialized on four generic types. The first is the `ResourceObjectDescription` described above. The others are a `Meta`, `Links`, and `MaybeRawId`. + +#### `Meta` + +The second generic specialization on `ResourceObject` is `Meta`. This is described in its own section [below](#jsonapimeta). All `Meta` at any level of a JSON API Document follow the same rules. You can use `NoMetadata` if you do not need to package any metadata with the `ResourceObject`. + +#### `Links` + +The third generic specialization on `ResourceObject` is `Links`. This is described in its own section [below](#jsonnapilinks). All `Links` at any level of a JSON API Document follow the same rules, although the **SPEC** makes different suggestions as to what types of links might live on which parts of the Document. You can use `NoLinks` if you do not need to package any links with the `ResourceObject`. + +#### `MaybeRawId` + +The last generic specialization on `ResourceObject` is `MaybeRawId`. This is either a `RawIdType` that can be used to uniquely identify `ResourceObjects` or it is `Unidentified` which is used to indicate a `ResourceObject` does not have an `Id` (which is useful when a client is requesting that the server create a `ResourceObject` and assign it a new `Id`). + +##### `RawIdType` + +The raw type of `Id` to use for the `ResourceObject`. The actual `Id` of the `ResourceObject` will not be a `RawIdType`, though. The `Id` will package a value of `RawIdType` with a specialized reference back to the `ResourceObject` type it identifies. This just looks like `Id>`. + +Having the `ResourceObject` type associated with the `Id` makes it easy to store all of your resource objects in a hash broken out by `ResourceObject` type; You can pass `Ids` around and always know where to look for the `ResourceObject` to which the `Id` refers. This encapsulation provides some type safety because the Ids of two `ResourceObjects` with the "raw ID" of `"1"` but different types will not compare as equal. + +A `RawIdType` is the underlying type that uniquely identifies a `ResourceObject`. This is often a `String` or a `UUID`. + +#### Convenient `typealiases` + +Often you can use one `RawIdType` for many if not all of your `ResourceObjects`. That means you can save yourself some boilerplate by using `typealias`es like the following: +```swift +public typealias ResourceObject = JSONAPI.ResourceObject + +public typealias NewResourceObject = JSONAPI.ResourceObject +``` + +It can also be nice to create a `typealias` for each type of resource object you want to work with: +```swift +typealias Person = ResourceObject + +typealias NewPerson = NewResourceObject +``` + +Note that I am assuming an unidentified person is a "new" person. I suspect that is generally an acceptable conflation because the only time the **SPEC** allows a *Resource Object* to be encoded without an `Id` is when a client is requesting the given *Resource Object* be created by the server and the client wants the server to create the `Id` for that object. + +### `JSONAPI.Relationships` + +There are two types of `Relationships`: `ToOneRelationship` and `ToManyRelationship`. A `ResourceObjectDescription`'s `Relationships` type can contain any number of `Relationship` properties of either of these types. Do not store anything other than `Relationship` properties in the `Relationships` struct of a `ResourceObjectDescription`. + +In addition to identifying resource objects by Id and type, `Relationships` can contain `Meta` or `Links` that follow the same rules as [`Meta`](#jsonapimeta) and [`Links`](#jsonapilinks) elsewhere in the JSON API Document. + +To describe a relationship that may be omitted (i.e. the key is not even present in the JSON object), you make the entire `ToOneRelationship` or `ToManyRelationship` optional. However, this is not recommended because you can also represent optional relationships as nullable which means the key is always present. A `ToManyRelationship` can naturally represent the absence of related values with an empty array, so `ToManyRelationship` does not support nullability at all. A `ToOneRelationship` can be marked as nullable (i.e. the value could be either `null` or a resource identifier) like this: +```swift +let nullableRelative: ToOneRelationship +``` + +A `ResourceObject` that does not have relationships can be described by adding the following to a `ResourceObjectDescription`: +```swift +typealias Relationships = NoRelationships +``` + +`Relationship` values boil down to `Ids` of other resource objects. To access the `Id` of a related `ResourceObject`, you can use the custom `~>` operator with the `KeyPath` of the `Relationship` from which you want the `Id`. The friends of the above `Person` `ResourceObject` can be accessed as follows (type annotations for clarity): +```swift +let friendIds: [Person.Identifier] = person ~> \.friends +``` + +### `JSONAPI.Attributes` + +The `Attributes` of a `ResourceObjectDescription` can contain any JSON encodable/decodable types as long as they are wrapped in an `Attribute`, `ValidatedAttribute`, or `TransformedAttribute` `struct`. + +To describe an attribute that may be omitted (i.e. the key might not even be in the JSON object), you make the entire `Attribute` optional: +```swift +let optionalAttribute: Attribute? +``` + +To describe an attribute that is expected to exist but might have a `null` value, you make the value within the `Attribute` optional: +```swift +let nullableAttribute: Attribute +``` + +A resource object that does not have attributes can be described by adding the following to an `ResourceObjectDescription`: +```swift +typealias Attributes = NoAttributes +``` + +As of Swift 5.1, `Attributes` can be accessed via dynamic member keypath lookup as follows: +```swift +let favoriteColor: String = person.favoriteColor +``` + +:warning: `Attributes` can also be accessed via the older `subscript` operator, but this is a deprecated feature that will be removed in the next major version: +```swift +let favoriteColor: String = person[\.favoriteColor] +``` + +#### `Transformer` + +Sometimes you need to use a type that does not encode or decode itself in the way you need to represent it as a serialized JSON object. For example, the Swift `Foundation` type `Date` can encode/decode itself to `Double` out of the box, but you might want to represent dates as ISO 8601 compliant `String`s instead. The Foundation library `JSONDecoder` has a setting to make this adjustment, but for the sake of an example, you could create a `Transformer`. + +A `Transformer` just provides one static function that transforms one type to another. You might define one for an ISO 8601 compliant `Date` like this: +```swift +enum ISODateTransformer: Transformer { + public static func transform(_ value: String) throws -> Date { + // parse Date out of input and return + } +} +``` + +Then you define the attribute as a `TransformedAttribute` instead of an `Attribute`: +```swift +let date: TransformedAttribute +``` + +Note that the first generic parameter of `TransformAttribute` is the type you expect to decode from JSON, not the type you want to end up with after transformation. + +If you make your `Transformer` a `ReversibleTransformer` then your life will be a bit easier when you construct `TransformedAttributes` because you have access to initializers for both the pre- and post-transformed value types. Continuing with the above example of a `ISODateTransformer`: +```swift +extension ISODateTransformer: ReversibleTransformer { + public static func reverse(_ value: Date) throws -> String { + // serialize Date to a String + } +} + +let exampleAttribute = try? TransformedAttribute(transformedValue: Date()) +let otherAttribute = try? TransformedAttribute(rawValue: "2018-12-01 09:06:41 +0000") +``` + +#### `Validator` + +You can also creator `Validators` and `ValidatedAttribute`s. A `Validator` is just a `Transformer` that by convention does not perform a transformation. It simply `throws` if an attribute value is invalid. + +#### Computed `Attribute` + +You can add computed properties to your `ResourceObjectDescription.Attributes` struct if you would like to expose attributes that are not explicitly represented by the JSON. These computed properties do not have to be wrapped in `Attribute`, `ValidatedAttribute`, or `TransformedAttribute`. This allows computed attributes to be of types that are not `Codable`. Here's an example of how you might take the `person.name` attribute from the example above and create a `fullName` computed property. + +```swift +public var fullName: Attribute { + return name.map { $0.joined(separator: " ") } +} +``` + +If your computed property is wrapped in a `AttributeType` then you can still use the default subscript operator to access it (as would be the case with the `person.fullName` example above). However, if you add a property to the `Attributes` `struct` that is not wrapped in an `AttributeType`, you must either access it from its full path (`person.attributes.newThing`) or with the "direct" subscript accessor (`person[direct: \.newThing]`). This keeps the subscript access unambiguous enough for the compiler to be helpful prior to explicitly casting, comparing, or storing the result. + +### Copying/Mutating `ResourceObjects` +`ResourceObject` is a value type, so copying is its default behavior. There are two common mutations you might want to make when copying a `ResourceObject`: +1. Assigning a new `Identifier` to the copy of an identified `ResourceObject`. +2. Assigning a new `Identifier` to the copy of an unidentified `ResourceObject`. + +The above can be accomplished with code like the following: + +```swift +// use case 1 +let person1 = person.withNewIdentifier() + +// use case 2 +let newlyIdentifiedPerson1 = unidentifiedPerson.identified(byType: String.self) + +let newlyIdentifiedPerson2 = unidentifiedPerson.identified(by: "2232") +``` + +### `JSONAPI.Document` + +The entirety of a JSON API request or response is encoded or decoded from- or to a `Document`. As an example, a JSON API response containing one `Person` and no included resource objects could be decoded as follows: +```swift +let decoder = JSONDecoder() + +let responseStructure = JSONAPI.Document, NoMetadata, NoLinks, NoIncludes, BasicJSONAPIError>.self + +let document = try decoder.decode(responseStructure, from: data) +``` + +A JSON API Document is guaranteed by the **SPEC** to be "data", "metadata", or "errors." If it is "data", it may also contain "metadata" and/or other "included" resources. If it is "errors," it may also contain "metadata." + +#### `ResourceBody` + +The first generic type of a `JSONAPIDocument` is a `ResourceBody`. This can either be a `SingleResourceBody` or a `ManyResourceBody`. You will find zero or one `PrimaryResource` values in a JSON API document that has a `SingleResourceBody` and you will find zero or more `PrimaryResource` values in a JSON API document that has a `ManyResourceBody`. You can use the `Poly` types (`Poly1` through `Poly6`) to specify that a `ResourceBody` will be one of a few different types of `ResourceObject`. These `Poly` types work in the same way as the `Include` types described below. + +If you expect a response to not have a "data" top-level key at all, then use `NoResourceBody` instead. + +##### nullable `PrimaryResource` + +If you expect a `SingleResourceBody` to sometimes come back `null`, you should make your `PrimaryResource` optional. If you do not make your `PrimaryResource` optional then a `null` primary resource will be considered an error when parsing the JSON. + +You cannot, however, use an optional `PrimaryResource` with a `ManyResourceBody` because the **SPEC** requires that an empty document in that case be represented by an empty array rather than `null`. + +#### `MetaType` + +The second generic type of a `JSONAPIDocument` is a `Meta`. This `Meta` follows the same rules as `Meta` at any other part of a JSON API Document. It is described below in its own section, but as an example, the JSON API document could contain the following pagination info in its meta entry: +```json +{ + "meta": { + "total": 100, + "limit": 50, + "offset": 50 + } +} +``` + +You would then create the following `Meta` type: +```swift +struct PageMetadata: JSONAPI.Meta { + let total: Int + let limit: Int + let offset: Int +} +``` + +You can always use `NoMetadata` if this JSON API feature is not needed. + +#### `LinksType` + +The third generic type of a `JSONAPIDocument` is a `Links` struct. `Links` are described in their own section [below](#jsonapilinks). + +#### `IncludeType` + +The fourth generic type of a `JSONAPIDocument` is an `Include`. This type controls which types of `ResourceObject` are looked for when decoding the "included" part of the JSON API document. If you do not expect any included resource objects to be in the document, `NoIncludes` is the way to go. The `JSONAPI` framework provides `Include`s for up to 10 types of included resource objects. These are named `Include1`, `Include2`, `Include3`, and so on. + +**IMPORTANT**: The number trailing "Include" in these type names does not indicate a number of included resource objects, it indicates a number of _types_ of included resource objects. `Include1` can be used to decode any number of included resource objects as long as all the resource objects are of the same _type_. + +To specify that we expect friends of a person to be included in the above example `JSONAPIDocument`, we would use `Include1` instead of `NoIncludes`. + +#### `APIDescriptionType` + +The fifth generic type of a `JSONAPIDocument` is an `APIDescription`. The type represents the "JSON:API Object" described by the **SPEC**. This type describes the highest version of the **SPEC** supported and can carry additional metadata to describe the API. + +You can specify this is not part of the document by using the `NoAPIDescription` type. + +You can describe the API by a version with no metadata by using `APIDescription`. + +You can supply any `JSONAPI.Meta` type as the metadata type of the API description. + +#### `Error` + +The final generic type of a `JSONAPIDocument` is the `Error`. + +You can either create an error type that can handle all the errors you expect your `JSONAPIDocument` to be able to encode/decode or use an out-of-box error type described here. As prescribed by the **SPEC**, these errors will be found under the root document key `errors`. + +##### `UnknownJSONAPIError` +The `UnknownJSONAPIError` type will always succeed in parsing errors but it will not give you any information about what error occurred. You will generally get more bang for your buck out of the next error type described. + +##### `BasicJSONAPIError` +The `BasicJSONAPIError` type will always succeed unless it is faced with an `id` field of an unexpected type, although it still "succeeds" in falling back to its `.unknown` case when that happens. This type extracts _most_ of the fields the **SPEC** describes [here](https://jsonapi.org/format/#error-objects). Because all of these fields are optional in the **SPEC**, they are optional on the `BasicJSONAPIError` type. You will have to create your own error type if you want to define certain fields as non-optional or parse metadata or links out of error objects. + +🗒Metadata and links are supported at the Document level for error responses, the are just not supported hanging off of the individual errors in the `errors` array of the response when using this error type. + +The `BasicJSONAPIError` type is generic on one thing: The type it expects for the `id` field. If you expect integer `ids` back, you use `BasicJSONAPIError`. The same can be done for `String` or any other type that is both `Codable` and `Equatable`. You can even employ something like `AnyCodable` from *Flight-School* as your id field type. If you only need to handle a small subset of possible `id` field types, you can also use the `Poly` library that is already a dependency of `JSONAPI`. For example, you might expect a mix of `String` and `Int` ids for some reason: `BasicJSONAPIError>`. + +The two easiest ways to access the available properties of an error response are under the `payload` property of the error (this property is `nil` if the error was parsed as `.unknown`) or by asking the error for its `definedFields` dictionary. + +As an example, let's say you have the following `Document` type that is destined for errors: +```swift +typealias ErrorDoc = JSONAPI.Document> +``` +And you've parsed an error response +```swift +let errorResponse = try! JSONDecoder().decode(ErrorDoc.self, from: mockErrorData) +``` +You can get at the `Document` body and errors in a couple of different ways, but for one you can switch on the body: +```swift +switch errorResponse.body { +case .data: + print("cool, data!") + +case .errors(let errors, let meta, let links): + let errorDetails = errors.compactMap { $0.payload?.detail } + + print("error details: \(errorDetails)") +} +``` + +##### `GenericJSONAPIError` +This type makes it simple to use your own error payload structures as `JSONAPIError` types. Simply define a `Codable` and `Equatable` struct and then use `GenericJSONAPIError` as the error type for a `Document`. + +### `JSONAPI.Meta` + +A `Meta` struct is totally open-ended. It is described by the **SPEC** as a place to put any information that does not fit into the standard JSON API Document structure anywhere else. + +You can specify `NoMetadata` if the part of the document being described should not contain any `Meta`. + +If you need to support metadata with structure that is not pre-determined, consider an "Any Codable" type such as that found at https://github.com/Flight-School/AnyCodable. + +### `JSONAPI.Links` + +A `Links` struct must contain only `Link` properties. Each `Link` property can either be a `URL` or a `URL` and some `Meta`. Each part of the document has some suggested common `Links` to include but generally any link can be included. + +You can specify `NoLinks` if the part of the document being described should not contain any `Links`. + +### `JSONAPI.RawIdType` + +If you want to create new `JSONAPI.ResourceObject` values and assign them Ids then you will need to conform at least one type to `CreatableRawIdType`. Doing so is easy; here are two example conformances for `UUID` and `String` (via `UUID`): +```swift +extension UUID: CreatableRawIdType { + public static func unique() -> UUID { + return UUID() + } +} + +extension String: CreatableRawIdType { + public static func unique() -> String { + return UUID().uuidString + } +} +``` + +### Sparse Fieldsets +Sparse Fieldsets are currently supported when encoding only. When decoding, Sparse Fieldsets become tricker to support under the current types this library uses and it is assumed that clients will request one or maybe two sparse fieldset combinations for any given model at most so it can simply define the `JSONAPI` models needed to decode those subsets of all possible fields. A server, on the other hand, likely needs to support arbitrary combinations of sparse fieldsets and this library provides a mechanism for encoding those sparse fieldsets without too much extra footwork. + +You can use sparse fieldsets on the primary resources(s) _and_ includes of a `JSONAPI.Document`. + +There is a sparse fieldsets example included with this repository as a Playground page. + +#### Supporting Sparse Fieldset Encoding +1. The `JSONAPI` `ResourceObjectDescription`'s `Attributes` struct must conform to `JSONAPI.SparsableAttributes` rather than `JSONAPI.Attributes`. +2. The `JSONAPI` `ResourceObjectDescription`'s `Attributes` struct must contain a `CodingKeys` enum that conforms to `JSONAPI.SparsableCodingKey` instead of `Swift.CodingKey`. +3. `typealiases` you may have created for `JSONAPI.Document` that allow you to decode Documents will not support the "encode-only" nature of sparse fieldsets. See the next section for `typealias` comparisons. +4. To create a sparse fieldset from a `ResourceObject` just call its `sparse(with: fields)` method and pass an array of `Attributes.CodingKeys` values you would like included in the encoding. +5. Initialize and encode a `Document` containing one or more sparse or full primary resource(s) and any number of sparse or full includes. + +#### Sparse Fieldset `typealias` comparisons +You might have found a `typealias` like the following for encoding/decoding `JSONAPI.Document`s (note the primary resource body is a `JSONAPI.ResourceBody`): +```swift +typealias Document = JSONAPI.Document> +``` + +In order to support sparse fieldsets (which are encode-only), the following companion `typealias` would be useful (note the primary resource body is a `JSONAPI.EncodableResourceBody`): +```swift +typealias SparseDocument = JSONAPI.Document> +``` + +### Replacing and Tapping Attributes/Relationships +When you are working with an immutable Resource Object, it can be useful to replace its attributes or relationships. As a client, you might receive a resource from the server, update something, and then send the server a PATCH request. + +`ResourceObject` is immutable, but you can create a new copy of a `ResourceObject` having updated attributes or relationships. + +#### Tapping +If your `Attributes` or `Relationships` struct is mutable (i.e. its properties are `var`s) then you may find `ResourceObject`'s `tappingAttributes()` and `tappingRelationships()` functions useful. For both, you pass a function that takes an `inout` copy of the respective object or value that you can mutate. The mutated value is then used to create a new `ResourceObject`. + +For example, to take a hypothetical `Dog` resource object and change the name attribute: +```swift +let resourceObject = Dog(...) + +let newResourceObject = resourceObject + .tappingAttributes { $0.name = .init(value: "Charlie") } +``` + +#### Replacing +If your `Attributes` or `Relationships` struct is immutable (i.e. its properties are `let`s) then you may find `ResourceObject`'s `replacingAttributes()` and `replacingRelationships()` functions useful. For both, you pass a function that takes the current attributes or relationships and you return a new value. The new value is then used to create a new `ResourceObject`. + +For example, to take a hypothetical `Dog` resource object and change the name attribute: +```swift +let resourceObject = Dog(...) + +let newResourceObject = resourceObject + .replacingAttributes { _ in + return Dog.Attributes(name: .init(value: "Charlie")) +} +``` + +### Custom Attribute or Relationship Key Mapping +There is not anything special going on at the `JSONAPI.Attributes` and `JSONAPI.Relationships` levels, so you can easily provide custom key mappings by taking advantage of `Codable`'s `CodingKeys` pattern. Here are two models that will encode/decode equivalently but offer different naming in your codebase: +```swift +public enum ResourceObjectDescription1: JSONAPI.ResourceObjectDescription { + public static var jsonType: String { return "entity" } + + public struct Attributes: JSONAPI.Attributes { + public let coolProperty: Attribute + } + + public typealias Relationships = NoRelationships +} + +public enum ResourceObjectDescription2: JSONAPI.ResourceObjectDescription { + public static var jsonType: String { return "entity" } + + public struct Attributes: JSONAPI.Attributes { + public let wholeOtherThing: Attribute + + enum CodingKeys: String, CodingKey { + case wholeOtherThing = "coolProperty" + } + } + + public typealias Relationships = NoRelationships +} +``` + +### Custom Attribute Encode/Decode +You can safely provide your own encoding or decoding functions for your Attributes struct if you need to as long as you are careful that your encode operation correctly reverses your decode operation. Although this is generally not necessary, `AttributeType` provides a convenience method to make your decoding a bit less boilerplate ridden. This is what it looks like: +```swift +public enum ResourceObjectDescription1: JSONAPI.ResourceObjectDescription { + public static var jsonType: String { return "entity" } + + public struct Attributes: JSONAPI.Attributes { + public let property1: Attribute + public let property2: Attribute + public let property3: Attribute + + public let weirdThing: Attribute + + enum CodingKeys: String, CodingKey { + case property1 + case property2 + case property3 + } + } + + public typealias Relationships = NoRelationships +} + +extension ResourceObjectDescription1.Attributes { + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + property1 = try .defaultDecoding(from: container, forKey: .property1) + property2 = try .defaultDecoding(from: container, forKey: .property2) + property3 = try .defaultDecoding(from: container, forKey: .property3) + + weirdThing = .init(value: "hello world") + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + try container.encode(property1, forKey: .property1) + try container.encode(property2, forKey: .property2) + try container.encode(property3, forKey: .property3) + } +} +``` + +### Meta-Attributes +This advanced feature may not ever be useful, but if you find yourself in the situation of dealing with an API that does not 100% follow the **SPEC** then you might find meta-attributes are just the thing to make your resource objects more natural to work with. + +Suppose, for example, you are presented with the unfortunate situation where a piece of information you need is only available as part of the `Id` of a resource object. Perhaps a user's `Id` is formatted "{integer}-{createdAt}" where "createdAt" is the unix timestamp when the user account was created. The following `UserDescription` will expose what you need as an attribute. Realistically, the following example code is still terrible for its error handling. Using a `Result` type and/or invariants would clean things up substantially. + +```swift +enum UserDescription: ResourceObjectDescription { + public static var jsonType: String { return "users" } + + struct Attributes: JSONAPI.Attributes { + var createdAt: (User) -> Date { + return { user in + let components = user.id.rawValue.split(separator: "-") + + guard components.count == 2 else { + assertionFailure() + return Date() + } + + let timestamp = TimeInterval(components[1]) + + guard let date = timestamp.map(Date.init(timeIntervalSince1970:)) else { + assertionFailure() + return Date() + } + + return date + } + } + } + + typealias Relationships = NoRelationships +} + +typealias User = JSONAPI.ResourceObject +``` + +Given a value `user` of the above resource object type, you can access the `createdAt` attribute just like you would any other: + +```swift +let createdAt = user.createdAt +``` + +This works because `createdAt` is defined in the form: `var {name}: ({ResourceObject}) -> {Value}` where `{ResourceObject}` is the `JSONAPI.ResourceObject` described by the `ResourceObjectDescription` containing the meta-attribute. + +### Meta-Relationships +This advanced feature may not ever be useful, but if you find yourself in the situation of dealing with an API that does not 100% follow the **SPEC** then you might find meta-relationships are just the thing to make your resource objects more natural to work with. + +Similarly to Meta-Attributes, Meta-Relationships allow you to represent non-compliant relationships as computed relationship properties. In the following example, a relationship is created from some attributes on the JSON model. + +```swift +enum UserDescription: ResourceObjectDescription { + public static var jsonType: String { return "users" } + + struct Attributes: JSONAPI.Attributes { + let friend_id: Attribute + } + + struct Relationships: JSONAPI.Relationships { + public var friend: (User) -> User.Identifier { + return { user in + return User.Identifier(rawValue: user.friend_id) + } + } + } +} + +typealias User = JSONAPI.ResourceObject +``` + +Given a value `user` of the above resource object type, you can access the `friend` relationship just like you would any other: + +```swift +let friendId = user ~> \.friend +``` + +This works because `friend` is defined in the form: `var {name}: ({ResourceObject}) -> {Identifier}` where `{ResourceObject}` is the `JSONAPI.ResourceObject` described by the `ResourceObjectDescription` containing the meta-relationship. From 2b4209ccb1feebe58d2df02512218b3fb5ffb0e4 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Fri, 15 Nov 2019 23:41:12 -0800 Subject: [PATCH 118/235] A couple more clarifications/corrections --- documentation/usage.md | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/documentation/usage.md b/documentation/usage.md index 7f6e8ee..bee7a25 100644 --- a/documentation/usage.md +++ b/documentation/usage.md @@ -50,8 +50,8 @@ In this documentation, in order to draw attention to the difference between the A `ResourceObjectDescription` is the `JSONAPI` framework's representation of what the **SPEC** calls a *Resource Object*. You might create the following `ResourceObjectDescription` to represent a person in a network of friends: ```swift -enum PersonDescription: IdentifiedResourceObjectDescription { - static var jsonType: String { return "people" } +enum PersonDescription: ResourceObjectDescription { + static let jsonType: String = "people" struct Attributes: JSONAPI.Attributes { let name: Attribute<[String]> @@ -145,7 +145,7 @@ typealias Person = ResourceObject typealias NewPerson = NewResourceObject ``` -Note that I am assuming an unidentified person is a "new" person. I suspect that is generally an acceptable conflation because the only time the **SPEC** allows a *Resource Object* to be encoded without an `Id` is when a client is requesting the given *Resource Object* be created by the server and the client wants the server to create the `Id` for that object. +Note that I am calling an unidentified person is a "new" person. I suspect that is generally an acceptable conflation because the only time the **SPEC** allows a *Resource Object* to be encoded without an `Id` is when a client is requesting the given *Resource Object* be created by the server and the client wants the server to create the `Id` for that object. ### `JSONAPI.Relationships` @@ -246,11 +246,12 @@ public var fullName: Attribute { If your computed property is wrapped in a `AttributeType` then you can still use the default subscript operator to access it (as would be the case with the `person.fullName` example above). However, if you add a property to the `Attributes` `struct` that is not wrapped in an `AttributeType`, you must either access it from its full path (`person.attributes.newThing`) or with the "direct" subscript accessor (`person[direct: \.newThing]`). This keeps the subscript access unambiguous enough for the compiler to be helpful prior to explicitly casting, comparing, or storing the result. ### Copying/Mutating `ResourceObjects` -`ResourceObject` is a value type, so copying is its default behavior. There are two common mutations you might want to make when copying a `ResourceObject`: +`ResourceObject` is a value type, so copying is its default behavior. There are three common mutations you might want to make when copying a `ResourceObject`: 1. Assigning a new `Identifier` to the copy of an identified `ResourceObject`. 2. Assigning a new `Identifier` to the copy of an unidentified `ResourceObject`. +3. Change attribute or relationship values. -The above can be accomplished with code like the following: +The first two can be accomplished with code like the following: ```swift // use case 1 @@ -262,6 +263,8 @@ let newlyIdentifiedPerson1 = unidentifiedPerson.identified(byType: String.self) let newlyIdentifiedPerson2 = unidentifiedPerson.identified(by: "2232") ``` +The third use-case is described in [Replacing and Tapping Attributes/Relationships](#replacing-and-tapping-attributesrelationships). + ### `JSONAPI.Document` The entirety of a JSON API request or response is encoded or decoded from- or to a `Document`. As an example, a JSON API response containing one `Person` and no included resource objects could be decoded as follows: @@ -321,6 +324,8 @@ The fourth generic type of a `JSONAPIDocument` is an `Include`. This type contro **IMPORTANT**: The number trailing "Include" in these type names does not indicate a number of included resource objects, it indicates a number of _types_ of included resource objects. `Include1` can be used to decode any number of included resource objects as long as all the resource objects are of the same _type_. +Decoding a JSON:API Document will fail if you specify an `IncludeType` that does not cover all of the types of includes you expect a response to contain. + To specify that we expect friends of a person to be included in the above example `JSONAPIDocument`, we would use `Include1` instead of `NoIncludes`. #### `APIDescriptionType` From 75711648a478b21c0a39845201ebffe52a868268 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Fri, 15 Nov 2019 23:48:49 -0800 Subject: [PATCH 119/235] more minor documentation fixes --- documentation/usage.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/documentation/usage.md b/documentation/usage.md index bee7a25..5cfc770 100644 --- a/documentation/usage.md +++ b/documentation/usage.md @@ -280,7 +280,7 @@ A JSON API Document is guaranteed by the **SPEC** to be "data", "metadata", or " #### `ResourceBody` -The first generic type of a `JSONAPIDocument` is a `ResourceBody`. This can either be a `SingleResourceBody` or a `ManyResourceBody`. You will find zero or one `PrimaryResource` values in a JSON API document that has a `SingleResourceBody` and you will find zero or more `PrimaryResource` values in a JSON API document that has a `ManyResourceBody`. You can use the `Poly` types (`Poly1` through `Poly6`) to specify that a `ResourceBody` will be one of a few different types of `ResourceObject`. These `Poly` types work in the same way as the `Include` types described below. +The first generic type of a `JSONAPI.Document` is a `ResourceBody`. This can either be a `SingleResourceBody` or a `ManyResourceBody`. You will find zero or one `PrimaryResource` values in a JSON API document that has a `SingleResourceBody` and you will find zero or more `PrimaryResource` values in a JSON API document that has a `ManyResourceBody`. You can use the `Poly` types (`Poly1` through `Poly6`) to specify that a `ResourceBody` will be one of a few different types of `ResourceObject`. These `Poly` types work in the same way as the `Include` types described below. If you expect a response to not have a "data" top-level key at all, then use `NoResourceBody` instead. @@ -292,7 +292,7 @@ You cannot, however, use an optional `PrimaryResource` with a `ManyResourceBody` #### `MetaType` -The second generic type of a `JSONAPIDocument` is a `Meta`. This `Meta` follows the same rules as `Meta` at any other part of a JSON API Document. It is described below in its own section, but as an example, the JSON API document could contain the following pagination info in its meta entry: +The second generic type of a `JSONAPI.Document` is a `Meta`. This `Meta` follows the same rules as `Meta` at any other part of a JSON API Document. It is described below in its own section, but as an example, the JSON API document could contain the following pagination info in its meta entry: ```json { "meta": { @@ -316,21 +316,21 @@ You can always use `NoMetadata` if this JSON API feature is not needed. #### `LinksType` -The third generic type of a `JSONAPIDocument` is a `Links` struct. `Links` are described in their own section [below](#jsonapilinks). +The third generic type of a `JSONAPI.Document` is a `Links` struct. `Links` are described in their own section [below](#jsonapilinks). #### `IncludeType` -The fourth generic type of a `JSONAPIDocument` is an `Include`. This type controls which types of `ResourceObject` are looked for when decoding the "included" part of the JSON API document. If you do not expect any included resource objects to be in the document, `NoIncludes` is the way to go. The `JSONAPI` framework provides `Include`s for up to 10 types of included resource objects. These are named `Include1`, `Include2`, `Include3`, and so on. +The fourth generic type of a `JSONAPI.Document` is an `Include`. This type controls which types of `ResourceObject` are looked for when decoding the "included" part of the JSON API document. If you do not expect any included resource objects to be in the document, `NoIncludes` is the way to go. The `JSONAPI` framework provides `Include`s for up to 10 types of included resource objects. These are named `Include1`, `Include2`, `Include3`, and so on. **IMPORTANT**: The number trailing "Include" in these type names does not indicate a number of included resource objects, it indicates a number of _types_ of included resource objects. `Include1` can be used to decode any number of included resource objects as long as all the resource objects are of the same _type_. Decoding a JSON:API Document will fail if you specify an `IncludeType` that does not cover all of the types of includes you expect a response to contain. -To specify that we expect friends of a person to be included in the above example `JSONAPIDocument`, we would use `Include1` instead of `NoIncludes`. +To specify that we expect friends of a person to be included in the above example `JSONAPI.Document`, we would use `Include1` instead of `NoIncludes`. #### `APIDescriptionType` -The fifth generic type of a `JSONAPIDocument` is an `APIDescription`. The type represents the "JSON:API Object" described by the **SPEC**. This type describes the highest version of the **SPEC** supported and can carry additional metadata to describe the API. +The fifth generic type of a `JSONAPI.Document` is an `APIDescription`. The type represents the "JSON:API Object" described by the **SPEC**. This type describes the highest version of the **SPEC** supported and can carry additional metadata to describe the API. You can specify this is not part of the document by using the `NoAPIDescription` type. @@ -340,9 +340,9 @@ You can supply any `JSONAPI.Meta` type as the metadata type of the API descripti #### `Error` -The final generic type of a `JSONAPIDocument` is the `Error`. +The final generic type of a `JSONAPI.Document` is the `Error`. -You can either create an error type that can handle all the errors you expect your `JSONAPIDocument` to be able to encode/decode or use an out-of-box error type described here. As prescribed by the **SPEC**, these errors will be found under the root document key `errors`. +You can either create an error type that can handle all the errors you expect your `JSONAPI.Document` to be able to encode/decode or use an out-of-box error type described here. As prescribed by the **SPEC**, these errors will be found under the root document key `errors`. ##### `UnknownJSONAPIError` The `UnknownJSONAPIError` type will always succeed in parsing errors but it will not give you any information about what error occurred. You will generally get more bang for your buck out of the next error type described. @@ -426,9 +426,9 @@ There is a sparse fieldsets example included with this repository as a Playgroun 5. Initialize and encode a `Document` containing one or more sparse or full primary resource(s) and any number of sparse or full includes. #### Sparse Fieldset `typealias` comparisons -You might have found a `typealias` like the following for encoding/decoding `JSONAPI.Document`s (note the primary resource body is a `JSONAPI.ResourceBody`): +You might have found a `typealias` like the following for encoding/decoding `JSONAPI.Document`s (note the primary resource body is a `JSONAPI.CodableResourceBody`): ```swift -typealias Document = JSONAPI.Document> +typealias Document = JSONAPI.Document> ``` In order to support sparse fieldsets (which are encode-only), the following companion `typealias` would be useful (note the primary resource body is a `JSONAPI.EncodableResourceBody`): From 6ab4237c97e27ab0b5e3e1fe34738fa031a8dab9 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Fri, 15 Nov 2019 23:58:01 -0800 Subject: [PATCH 120/235] update podspec --- JSONAPI.podspec | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/JSONAPI.podspec b/JSONAPI.podspec index c6e5573..6224429 100644 --- a/JSONAPI.podspec +++ b/JSONAPI.podspec @@ -16,7 +16,7 @@ Pod::Spec.new do |spec| # spec.name = "MP-JSONAPI" - spec.version = "2.5.0" + spec.version = "3.0.0" spec.summary = "Swift Codable JSON API framework." # 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.xcconfig = { "HEADER_SEARCH_PATHS" => "$(SDKROOT)/usr/include/libxml2" } - spec.dependency "Poly", "~> 2.2" + spec.dependency "Poly", "~> 2.3.1" end From 45ec7ba753056f2f724cdace311ee83dfc092f1d Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Sat, 16 Nov 2019 00:03:46 -0800 Subject: [PATCH 121/235] Update SwiftPM package manifest example for version 3 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7a1c6cd..8319f10 100644 --- a/README.md +++ b/README.md @@ -66,7 +66,7 @@ If you find something wrong with this library and it isn't already mentioned und ### Swift Package Manager Just include the following in your package's dependencies and add `JSONAPI` to the dependencies for any of your targets. ``` - .package(url: "https://github.com/mattpolzin/JSONAPI.git", .upToNextMajor(from: "2.2.0")) + .package(url: "https://github.com/mattpolzin/JSONAPI.git", .upToNextMajor(from: "3.0.0")) ``` ### Xcode project From f41521e33b0fb2d7c3624d52d642a316cb854f1e Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Sat, 16 Nov 2019 00:06:49 -0800 Subject: [PATCH 122/235] Update example indentation --- README.md | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 8319f10..343f074 100644 --- a/README.md +++ b/README.md @@ -317,11 +317,13 @@ Literal expressibility for `Attribute`, `ToOneRelationship`, and `Id` are provid For example, you could create a mock `Author` (from the above example) as follows ```swift -let author = Author(id: "1234", // You can just use a String directly as an Id - attributes: .init(name: "Janice Bluff"), // The name Attribute does not need to be initialized, you just use a String directly. - relationships: .none, - meta: .none, - links: .none) +let author = Author( + id: "1234", // You can just use a String directly as an Id + attributes: .init(name: "Janice Bluff"), // The name Attribute does not need to be initialized, you just use a String directly. + relationships: .none, + meta: .none, + links: .none +) ``` ## Resource Object `check()` From 3345f02f9e2cb36adb0554885a914299637eddbb Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Mon, 2 Dec 2019 18:22:13 -0800 Subject: [PATCH 123/235] Create CONTRIBUTING.md Add contribution guidelines. --- CONTRIBUTING.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..b54a852 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,18 @@ +Thanks for working to make the `JSONAPI` framework better for everyone! + +## Issues +Please create an Issue for any new bug you find or any new feature you would like to see in the future. If you are also able to work on the issue yourself, reference the issue by URL from the description of your PR. + +In the issue, +1. Describe both the expected behavior and the current (buggy) behavior of the library. +2. Specify the version of the framework you are using. +3. Specify the platform you are running the framework on (i.e. Linux, Mac OS, iOS, etc.). +4. Give any information you can about the code you've written that uses the framework or the API requests/responses you are attempting to pass through the framework unsuccessfully. +5. Drop a note if you intend to fix the issue so no one else duplicates your effort in the meantime! + +## Pull Requests +Please try to create Issues prior to creating Pull Requests. In the body of your Pull Request description, mention the issue the PR addresses by URL. + +For all code additions, add tests that cover the new code. If you are fixing a bug, include at least 1 test case that would have failed prior to your fix but succeeds now that you have fixed the bug. This is important to avoid regressions in the future. + +Before creating the pull request, make sure all tests are passing and run `swift test --generate-linuxmain` to generate the test files for running in Linux environments. From 162459d559ecda52a63ab242da912cc722dd85ef Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Mon, 2 Dec 2019 18:29:29 -0800 Subject: [PATCH 124/235] Update issue templates --- .github/ISSUE_TEMPLATE/bug_report.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..3d720c0 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,20 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: '' +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**Platform (only for runtime bugs):** +i.e. Linux, OS X, iOS, etc. + +**Context** +Your code, examples of JSON data, etc. + +**Do you plan to fix yourself?** +If you plan to open a PR to address this bug, let others know here so no one duplicates your effort in the meantime. From a0ff33f290d872f94984eb56513795e9a92ec1f2 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Mon, 2 Dec 2019 18:33:30 -0800 Subject: [PATCH 125/235] Update README.md Add an inline section linking to the usage documentation. --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 343f074..9637e9f 100644 --- a/README.md +++ b/README.md @@ -309,6 +309,9 @@ print(response.article) print(response.author) ``` +## Deeper Dive +See the [usage documentation](./documentation/usage.md). + # JSONAPI+Testing The `JSONAPI` framework is packaged with a test library to help you test your `JSONAPI` integration. The test library is called `JSONAPITesting`. You can see `JSONAPITesting` in action in the Playground included with the `JSONAPI` repository. From 890154653fb35fad9b5bd52ea28ccefc145959c9 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Fri, 20 Dec 2019 13:47:12 -0800 Subject: [PATCH 126/235] Create idea.md --- .github/ISSUE_TEMPLATE/idea.md | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/idea.md diff --git a/.github/ISSUE_TEMPLATE/idea.md b/.github/ISSUE_TEMPLATE/idea.md new file mode 100644 index 0000000..3bc4170 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/idea.md @@ -0,0 +1,8 @@ +--- +name: Idea +about: Create a report to help us improve +title: '' +labels: '' +assignees: '' + +--- From 4f86d85690002bf0d9af8bfa6fa53089f153541e Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Fri, 20 Dec 2019 13:48:20 -0800 Subject: [PATCH 127/235] Create question --- .github/ISSUE_TEMPLATE/question | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/question diff --git a/.github/ISSUE_TEMPLATE/question b/.github/ISSUE_TEMPLATE/question new file mode 100644 index 0000000..5ec1109 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/question @@ -0,0 +1,9 @@ +--- +name: Question +about: Ask a question +title: '' +labels: 'question' +assignees: '' + +--- + From d56fb731ad6d6348c95b3429f0a9b814d13a22f3 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Fri, 20 Dec 2019 13:48:55 -0800 Subject: [PATCH 128/235] Rename question to question.md --- .github/ISSUE_TEMPLATE/{question => question.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .github/ISSUE_TEMPLATE/{question => question.md} (100%) diff --git a/.github/ISSUE_TEMPLATE/question b/.github/ISSUE_TEMPLATE/question.md similarity index 100% rename from .github/ISSUE_TEMPLATE/question rename to .github/ISSUE_TEMPLATE/question.md From 2771eae348e36d95257bc4c732827b32b052cf44 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Mon, 13 Jan 2020 17:41:06 -0800 Subject: [PATCH 129/235] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9637e9f..4605561 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ See the JSON API Spec here: https://jsonapi.org/format/ ## Quick Start -:warning: The following Google Colab examples have correct code, but there appears to be an bug in the branch of the Swift compiler currently being used by the Google Colab Swift notebooks such that the `JSONAPI` package cannot be pulled in and you cannot run the examples in-browser. +:warning: The following Google Colab examples have correct code, but from time to time the Google Colab Swift compiler may be buggy and produce incorrect or erroneous results. Just keep that in mind if you run the code as you read through the Colab examples. ### Clientside - [Basic Example](https://colab.research.google.com/drive/1IS7lRSBGoiW02Vd1nN_rfdDbZvTDj6Te) From 25d7bbd4093887becb49d5a7515346aa2189ef80 Mon Sep 17 00:00:00 2001 From: Seif Kobrosly Date: Mon, 24 Feb 2020 15:43:12 -0500 Subject: [PATCH 130/235] Update README.md Minor typo fix. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4605561..74955ef 100644 --- a/README.md +++ b/README.md @@ -356,7 +356,7 @@ func test_articleResponse() { ``` # JSONAPI+Arbitrary -The `JSONAPI+Arbitrary` library provides `SwiftCheck` `Arbitrary` conformance for many of teh `JSONAPI` types. +The `JSONAPI+Arbitrary` library provides `SwiftCheck` `Arbitrary` conformance for many of the `JSONAPI` types. See https://github.com/mattpolzin/JSONAPI-Arbitrary for more information. From 1e04cc154abaf295491980526732f61138690ca5 Mon Sep 17 00:00:00 2001 From: Zev Eisenberg Date: Sat, 14 Mar 2020 14:02:48 -0400 Subject: [PATCH 131/235] Unindent and add syntax highlighting. --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 74955ef..4e31e3f 100644 --- a/README.md +++ b/README.md @@ -65,8 +65,8 @@ If you find something wrong with this library and it isn't already mentioned und ### Swift Package Manager Just include the following in your package's dependencies and add `JSONAPI` to the dependencies for any of your targets. -``` - .package(url: "https://github.com/mattpolzin/JSONAPI.git", .upToNextMajor(from: "3.0.0")) +```swift +.package(url: "https://github.com/mattpolzin/JSONAPI.git", .upToNextMajor(from: "3.0.0")) ``` ### Xcode project @@ -77,9 +77,9 @@ With Xcode 11+ you can also just open the folder containing your clone of this r ### CocoaPods To use this framework in your project via Cocoapods, add the following dependencies to your Podfile. -``` - pod 'Poly', :git => 'https://github.com/mattpolzin/Poly.git' - pod 'MP-JSONAPI', :git => 'https://github.com/mattpolzin/JSONAPI.git' +```ruby +pod 'Poly', :git => 'https://github.com/mattpolzin/Poly.git' +pod 'MP-JSONAPI', :git => 'https://github.com/mattpolzin/JSONAPI.git' ``` ### Running the Playground From 3ef53a6ad5b430fcd21d0cf5ad1c3d627b3dd581 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Thu, 26 Mar 2020 18:14:24 -0700 Subject: [PATCH 132/235] Add linux tests against 5.1 and 5.2 --- .github/workflows/tests.yml | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 .github/workflows/tests.yml diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..f6da800 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,33 @@ +name: Tests + +on: [push] + +jobs: + xenial_5_1: + container: + image: swift:5.1-xenial + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - run: swift test --enable-test-discovery + bionic_5_1: + container: + image: swift:5.1-bionic + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - run: swift test --enable-test-discovery + xenial_5_2: + container: + image: swift:5.2-xenial + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - run: swift test --enable-test-discovery + bionic_5_2: + container: + image: swift:5.2-bionic + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - run: swift test --enable-test-discovery From 1028f472e218639aa85e47e73beffa349f121712 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Thu, 26 Mar 2020 18:21:06 -0700 Subject: [PATCH 133/235] Remove test manifest files. --- .../JSONAPITestingTests/XCTestManifests.swift | 164 ----- Tests/JSONAPITests/XCTestManifests.swift | 609 ------------------ Tests/LinuxMain.swift | 10 - 3 files changed, 783 deletions(-) delete mode 100644 Tests/JSONAPITestingTests/XCTestManifests.swift delete mode 100644 Tests/JSONAPITests/XCTestManifests.swift delete mode 100644 Tests/LinuxMain.swift diff --git a/Tests/JSONAPITestingTests/XCTestManifests.swift b/Tests/JSONAPITestingTests/XCTestManifests.swift deleted file mode 100644 index e77e922..0000000 --- a/Tests/JSONAPITestingTests/XCTestManifests.swift +++ /dev/null @@ -1,164 +0,0 @@ -#if !canImport(ObjectiveC) -import XCTest - -extension ArrayCompareTests { - // DO NOT MODIFY: This is autogenerated, use: - // `swift test --generate-linuxmain` - // to regenerate. - static let __allTests__ArrayCompareTests = [ - ("test_differentLengths", test_differentLengths), - ("test_differentValues", test_differentValues), - ("test_reducePrebuilt", test_reducePrebuilt), - ("test_same", test_same), - ] -} - -extension Attribute_LiteralTests { - // DO NOT MODIFY: This is autogenerated, use: - // `swift test --generate-linuxmain` - // to regenerate. - static let __allTests__Attribute_LiteralTests = [ - ("test_ArrayLiteral", test_ArrayLiteral), - ("test_BooleanLiteral", test_BooleanLiteral), - ("test_DictionaryLiteral", test_DictionaryLiteral), - ("test_FloatLiteral", test_FloatLiteral), - ("test_IntegerLiteral", test_IntegerLiteral), - ("test_NilLiteral", test_NilLiteral), - ("test_NullableArrayLiteral", test_NullableArrayLiteral), - ("test_NullableBooleanLiteral", test_NullableBooleanLiteral), - ("test_NullableDictionaryLiteral", test_NullableDictionaryLiteral), - ("test_NullableFloatLiteral", test_NullableFloatLiteral), - ("test_NullableIntegerLiteral", test_NullableIntegerLiteral), - ("test_NullableOptionalArrayLiteral", test_NullableOptionalArrayLiteral), - ("test_NullableOptionalBooleanLiteral", test_NullableOptionalBooleanLiteral), - ("test_NullableOptionalDictionaryLiteral", test_NullableOptionalDictionaryLiteral), - ("test_NullableOptionalFloatLiteral", test_NullableOptionalFloatLiteral), - ("test_NullableOptionalIntegerLiteral", test_NullableOptionalIntegerLiteral), - ("test_NullableOptionalStringLiteral", test_NullableOptionalStringLiteral), - ("test_NullableStringLiteral", test_NullableStringLiteral), - ("test_OptionalArrayLiteral", test_OptionalArrayLiteral), - ("test_OptionalBooleanLiteral", test_OptionalBooleanLiteral), - ("test_OptionalDictionaryLiteral", test_OptionalDictionaryLiteral), - ("test_OptionalFloatLiteral", test_OptionalFloatLiteral), - ("test_OptionalIntegerLiteral", test_OptionalIntegerLiteral), - ("test_OptionalNilLiteral", test_OptionalNilLiteral), - ("test_OptionalStringLiteral", test_OptionalStringLiteral), - ("test_StringLiteral", test_StringLiteral), - ] -} - -extension AttributesCompareTests { - // DO NOT MODIFY: This is autogenerated, use: - // `swift test --generate-linuxmain` - // to regenerate. - static let __allTests__AttributesCompareTests = [ - ("test_differentAttributes", test_differentAttributes), - ("test_nonAttributeTypes", test_nonAttributeTypes), - ("test_sameAttributes", test_sameAttributes), - ] -} - -extension DocumentCompareTests { - // DO NOT MODIFY: This is autogenerated, use: - // `swift test --generate-linuxmain` - // to regenerate. - static let __allTests__DocumentCompareTests = [ - ("test_differentAPIDescription", test_differentAPIDescription), - ("test_differentData", test_differentData), - ("test_differentErrors", test_differentErrors), - ("test_differentLinks", test_differentLinks), - ("test_differentMetadata", test_differentMetadata), - ("test_errorAndData", test_errorAndData), - ("test_same", test_same), - ("test_sameErrorsDifferentMetadata", test_sameErrorsDifferentMetadata), - ] -} - -extension EntityCheckTests { - // DO NOT MODIFY: This is autogenerated, use: - // `swift test --generate-linuxmain` - // to regenerate. - static let __allTests__EntityCheckTests = [ - ("test_failsWithBadAttribute", test_failsWithBadAttribute), - ("test_failsWithBadRelationship", test_failsWithBadRelationship), - ("test_failsWithEnumAttributes", test_failsWithEnumAttributes), - ("test_failsWithEnumRelationships", test_failsWithEnumRelationships), - ("test_failsWithOptionalArrayAttribute", test_failsWithOptionalArrayAttribute), - ] -} - -extension Id_LiteralTests { - // DO NOT MODIFY: This is autogenerated, use: - // `swift test --generate-linuxmain` - // to regenerate. - static let __allTests__Id_LiteralTests = [ - ("test_IntegerLiteral", test_IntegerLiteral), - ("test_StringLiteral", test_StringLiteral), - ] -} - -extension IncludesCompareTests { - // DO NOT MODIFY: This is autogenerated, use: - // `swift test --generate-linuxmain` - // to regenerate. - static let __allTests__IncludesCompareTests = [ - ("test_missing", test_missing), - ("test_same", test_same), - ("test_typeMismatch", test_typeMismatch), - ("test_valueMismatch", test_valueMismatch), - ] -} - -extension Relationship_LiteralTests { - // DO NOT MODIFY: This is autogenerated, use: - // `swift test --generate-linuxmain` - // to regenerate. - static let __allTests__Relationship_LiteralTests = [ - ("test_ArrayLiteral", test_ArrayLiteral), - ("test_NilLiteral", test_NilLiteral), - ("test_StringLiteral", test_StringLiteral), - ] -} - -extension RelationshipsCompareTests { - // DO NOT MODIFY: This is autogenerated, use: - // `swift test --generate-linuxmain` - // to regenerate. - static let __allTests__RelationshipsCompareTests = [ - ("test_differentIds", test_differentIds), - ("test_differentLinks", test_differentLinks), - ("test_differentMetadata", test_differentMetadata), - ("test_nonRelationshipTypes", test_nonRelationshipTypes), - ("test_same", test_same), - ] -} - -extension ResourceObjectCompareTests { - // DO NOT MODIFY: This is autogenerated, use: - // `swift test --generate-linuxmain` - // to regenerate. - static let __allTests__ResourceObjectCompareTests = [ - ("test_differentAttributes", test_differentAttributes), - ("test_differentIds", test_differentIds), - ("test_differentLinks", test_differentLinks), - ("test_differentMetadata", test_differentMetadata), - ("test_differentRelationships", test_differentRelationships), - ("test_same", test_same), - ] -} - -public func __allTests() -> [XCTestCaseEntry] { - return [ - testCase(ArrayCompareTests.__allTests__ArrayCompareTests), - testCase(Attribute_LiteralTests.__allTests__Attribute_LiteralTests), - testCase(AttributesCompareTests.__allTests__AttributesCompareTests), - testCase(DocumentCompareTests.__allTests__DocumentCompareTests), - testCase(EntityCheckTests.__allTests__EntityCheckTests), - testCase(Id_LiteralTests.__allTests__Id_LiteralTests), - testCase(IncludesCompareTests.__allTests__IncludesCompareTests), - testCase(Relationship_LiteralTests.__allTests__Relationship_LiteralTests), - testCase(RelationshipsCompareTests.__allTests__RelationshipsCompareTests), - testCase(ResourceObjectCompareTests.__allTests__ResourceObjectCompareTests), - ] -} -#endif diff --git a/Tests/JSONAPITests/XCTestManifests.swift b/Tests/JSONAPITests/XCTestManifests.swift deleted file mode 100644 index ed1efbb..0000000 --- a/Tests/JSONAPITests/XCTestManifests.swift +++ /dev/null @@ -1,609 +0,0 @@ -#if !canImport(ObjectiveC) -import XCTest - -extension APIDescriptionTests { - // DO NOT MODIFY: This is autogenerated, use: - // `swift test --generate-linuxmain` - // to regenerate. - static let __allTests__APIDescriptionTests = [ - ("test_empty", test_empty), - ("test_failsMissingMeta", test_failsMissingMeta), - ("test_init", test_init), - ("test_NoDescriptionString", test_NoDescriptionString), - ("test_WithMeta", test_WithMeta), - ("test_WithVersion", test_WithVersion), - ("test_WithVersionAndMeta", test_WithVersionAndMeta), - ] -} - -extension AttributeTests { - // DO NOT MODIFY: This is autogenerated, use: - // `swift test --generate-linuxmain` - // to regenerate. - static let __allTests__AttributeTests = [ - ("test_AttributeConstructor", test_AttributeConstructor), - ("test_AttributeRawType", test_AttributeRawType), - ("test_EncodedPrimitives", test_EncodedPrimitives), - ("test_NullableIsEqualToNonNullableIfNotNil", test_NullableIsEqualToNonNullableIfNotNil), - ("test_NullableIsNullIfNil", test_NullableIsNullIfNil), - ("test_TransformedAttributeNoThrow", test_TransformedAttributeNoThrow), - ("test_TransformedAttributeRawType", test_TransformedAttributeRawType), - ("test_TransformedAttributeReversNoThrow", test_TransformedAttributeReversNoThrow), - ("test_TransformedAttributeThrows", test_TransformedAttributeThrows), - ] -} - -extension Attribute_FunctorTests { - // DO NOT MODIFY: This is autogenerated, use: - // `swift test --generate-linuxmain` - // to regenerate. - static let __allTests__Attribute_FunctorTests = [ - ("test_mapGuaranteed", test_mapGuaranteed), - ("test_mapGuaranteed_deprecated", test_mapGuaranteed_deprecated), - ("test_mapOptionalFailure", test_mapOptionalFailure), - ("test_mapOptionalFailure_deprecated", test_mapOptionalFailure_deprecated), - ("test_mapOptionalSuccess", test_mapOptionalSuccess), - ("test_mapOptionalSuccess_deprecated", test_mapOptionalSuccess_deprecated), - ] -} - -extension BasicJSONAPIErrorTests { - // DO NOT MODIFY: This is autogenerated, use: - // `swift test --generate-linuxmain` - // to regenerate. - static let __allTests__BasicJSONAPIErrorTests = [ - ("test_decodeAFewExamples", test_decodeAFewExamples), - ("test_definedFields", test_definedFields), - ("test_initAndEquality", test_initAndEquality), - ] -} - -extension ComputedPropertiesTests { - // DO NOT MODIFY: This is autogenerated, use: - // `swift test --generate-linuxmain` - // to regenerate. - static let __allTests__ComputedPropertiesTests = [ - ("test_ComputedAttributeAccess", test_ComputedAttributeAccess), - ("test_ComputedAttributeAccess_deprecated", test_ComputedAttributeAccess_deprecated), - ("test_ComputedNonAttributeAccess", test_ComputedNonAttributeAccess), - ("test_ComputedRelationshipAccess", test_ComputedRelationshipAccess), - ("test_DecodeIgnoresComputed", test_DecodeIgnoresComputed), - ("test_DecodeIgnoresComputed_deprecated", test_DecodeIgnoresComputed_deprecated), - ("test_EncodeIgnoresComputed", test_EncodeIgnoresComputed), - ] -} - -extension CustomAttributesTests { - // DO NOT MODIFY: This is autogenerated, use: - // `swift test --generate-linuxmain` - // to regenerate. - static let __allTests__CustomAttributesTests = [ - ("test_customDecode", test_customDecode), - ("test_customDecode_deprecated", test_customDecode_deprecated), - ("test_customEncode", test_customEncode), - ("test_customKeysDecode", test_customKeysDecode), - ("test_customKeysDecode_deprecated", test_customKeysDecode_deprecated), - ("test_customKeysEncode", test_customKeysEncode), - ] -} - -extension DocumentDecodingErrorTests { - // DO NOT MODIFY: This is autogenerated, use: - // `swift test --generate-linuxmain` - // to regenerate. - static let __allTests__DocumentDecodingErrorTests = [ - ("test_include_failure", test_include_failure), - ("test_include_failure2", test_include_failure2), - ("test_manyPrimaryResource_failure", test_manyPrimaryResource_failure), - ("test_manyPrimaryResource_missing", test_manyPrimaryResource_missing), - ("test_singlePrimaryResource_failure", test_singlePrimaryResource_failure), - ("test_singlePrimaryResource_missing", test_singlePrimaryResource_missing), - ("test_wantError_foundSuccess", test_wantError_foundSuccess), - ("test_wantSuccess_foundError", test_wantSuccess_foundError), - ] -} - -extension DocumentTests { - // DO NOT MODIFY: This is autogenerated, use: - // `swift test --generate-linuxmain` - // to regenerate. - static let __allTests__DocumentTests = [ - ("test_errorDocumentFailsWithNoAPIDescription", test_errorDocumentFailsWithNoAPIDescription), - ("test_errorDocumentNoMeta", test_errorDocumentNoMeta), - ("test_errorDocumentNoMeta_encode", test_errorDocumentNoMeta_encode), - ("test_errorDocumentNoMetaWithAPIDescription", test_errorDocumentNoMetaWithAPIDescription), - ("test_errorDocumentNoMetaWithAPIDescription_encode", test_errorDocumentNoMetaWithAPIDescription_encode), - ("test_genericDocFunc", test_genericDocFunc), - ("test_manyDocumentNoIncludes", test_manyDocumentNoIncludes), - ("test_manyDocumentNoIncludes_encode", test_manyDocumentNoIncludes_encode), - ("test_manyDocumentNoIncludesWithAPIDescription", test_manyDocumentNoIncludesWithAPIDescription), - ("test_manyDocumentNoIncludesWithAPIDescription_encode", test_manyDocumentNoIncludesWithAPIDescription_encode), - ("test_manyDocumentSomeIncludes", test_manyDocumentSomeIncludes), - ("test_manyDocumentSomeIncludes_encode", test_manyDocumentSomeIncludes_encode), - ("test_manyDocumentSomeIncludesWithAPIDescription", test_manyDocumentSomeIncludesWithAPIDescription), - ("test_manyDocumentSomeIncludesWithAPIDescription_encode", test_manyDocumentSomeIncludesWithAPIDescription_encode), - ("test_MergeBodyDataBasic", test_MergeBodyDataBasic), - ("test_MergeBodyDataWithMergeFunctions", test_MergeBodyDataWithMergeFunctions), - ("test_metaDataDocument", test_metaDataDocument), - ("test_metaDataDocument_encode", test_metaDataDocument_encode), - ("test_metaDataDocumentFailsIfMissingAPIDescription", test_metaDataDocumentFailsIfMissingAPIDescription), - ("test_metaDataDocumentWithAPIDescription", test_metaDataDocumentWithAPIDescription), - ("test_metaDataDocumentWithAPIDescription_encode", test_metaDataDocumentWithAPIDescription_encode), - ("test_metaDataDocumentWithLinks", test_metaDataDocumentWithLinks), - ("test_metaDataDocumentWithLinks_encode", test_metaDataDocumentWithLinks_encode), - ("test_metaDataDocumentWithLinksWithAPIDescription", test_metaDataDocumentWithLinksWithAPIDescription), - ("test_metaDataDocumentWithLinksWithAPIDescription_encode", test_metaDataDocumentWithLinksWithAPIDescription_encode), - ("test_metaDocumentMissingMeta", test_metaDocumentMissingMeta), - ("test_singleDocument_PolyPrimaryResource", test_singleDocument_PolyPrimaryResource), - ("test_singleDocument_PolyPrimaryResource_encode", test_singleDocument_PolyPrimaryResource_encode), - ("test_singleDocument_PolyPrimaryResourceWithAPIDescription", test_singleDocument_PolyPrimaryResourceWithAPIDescription), - ("test_singleDocument_PolyPrimaryResourceWithAPIDescription_encode", test_singleDocument_PolyPrimaryResourceWithAPIDescription_encode), - ("test_singleDocumentNoIncludes", test_singleDocumentNoIncludes), - ("test_singleDocumentNoIncludes_encode", test_singleDocumentNoIncludes_encode), - ("test_singleDocumentNoIncludesAddIncludingType", test_singleDocumentNoIncludesAddIncludingType), - ("test_singleDocumentNoIncludesMissingAPIDescription", test_singleDocumentNoIncludesMissingAPIDescription), - ("test_singleDocumentNoIncludesMissingMetadata", test_singleDocumentNoIncludesMissingMetadata), - ("test_singleDocumentNoIncludesOptionalNotNull", test_singleDocumentNoIncludesOptionalNotNull), - ("test_singleDocumentNoIncludesOptionalNotNull_encode", test_singleDocumentNoIncludesOptionalNotNull_encode), - ("test_singleDocumentNoIncludesOptionalNotNullWithAPIDescription", test_singleDocumentNoIncludesOptionalNotNullWithAPIDescription), - ("test_singleDocumentNoIncludesOptionalNotNullWithAPIDescription_encode", test_singleDocumentNoIncludesOptionalNotNullWithAPIDescription_encode), - ("test_singleDocumentNoIncludesWithAPIDescription", test_singleDocumentNoIncludesWithAPIDescription), - ("test_singleDocumentNoIncludesWithAPIDescription_encode", test_singleDocumentNoIncludesWithAPIDescription_encode), - ("test_singleDocumentNoIncludesWithLinks", test_singleDocumentNoIncludesWithLinks), - ("test_singleDocumentNoIncludesWithLinks_encode", test_singleDocumentNoIncludesWithLinks_encode), - ("test_singleDocumentNoIncludesWithLinksWithAPIDescription", test_singleDocumentNoIncludesWithLinksWithAPIDescription), - ("test_singleDocumentNoIncludesWithLinksWithAPIDescription_encode", test_singleDocumentNoIncludesWithLinksWithAPIDescription_encode), - ("test_singleDocumentNoIncludesWithMetadata", test_singleDocumentNoIncludesWithMetadata), - ("test_singleDocumentNoIncludesWithMetadata_encode", test_singleDocumentNoIncludesWithMetadata_encode), - ("test_singleDocumentNoIncludesWithMetadataMissingLinks", test_singleDocumentNoIncludesWithMetadataMissingLinks), - ("test_singleDocumentNoIncludesWithMetadataWithAPIDescription", test_singleDocumentNoIncludesWithMetadataWithAPIDescription), - ("test_singleDocumentNoIncludesWithMetadataWithAPIDescription_encode", test_singleDocumentNoIncludesWithMetadataWithAPIDescription_encode), - ("test_singleDocumentNoIncludesWithMetadataWithLinks", test_singleDocumentNoIncludesWithMetadataWithLinks), - ("test_singleDocumentNoIncludesWithMetadataWithLinks_encode", test_singleDocumentNoIncludesWithMetadataWithLinks_encode), - ("test_singleDocumentNoIncludesWithMetadataWithLinksWithAPIDescription", test_singleDocumentNoIncludesWithMetadataWithLinksWithAPIDescription), - ("test_singleDocumentNoIncludesWithMetadataWithLinksWithAPIDescription_encode", test_singleDocumentNoIncludesWithMetadataWithLinksWithAPIDescription_encode), - ("test_singleDocumentNoIncludesWithSomeIncludesMetadataWithLinks_encode", test_singleDocumentNoIncludesWithSomeIncludesMetadataWithLinks_encode), - ("test_singleDocumentNoIncludesWithSomeIncludesMetadataWithLinksWithAPIDescription_encode", test_singleDocumentNoIncludesWithSomeIncludesMetadataWithLinksWithAPIDescription_encode), - ("test_singleDocumentNoIncludesWithSomeIncludesWithMetadataWithLinks", test_singleDocumentNoIncludesWithSomeIncludesWithMetadataWithLinks), - ("test_singleDocumentNoIncludesWithSomeIncludesWithMetadataWithLinksWithAPIDescription", test_singleDocumentNoIncludesWithSomeIncludesWithMetadataWithLinksWithAPIDescription), - ("test_singleDocumentNonOptionalFailsOnNull", test_singleDocumentNonOptionalFailsOnNull), - ("test_singleDocumentNull", test_singleDocumentNull), - ("test_singleDocumentNull_encode", test_singleDocumentNull_encode), - ("test_singleDocumentNullFailsWithNoAPIDescription", test_singleDocumentNullFailsWithNoAPIDescription), - ("test_singleDocumentNullWithAPIDescription", test_singleDocumentNullWithAPIDescription), - ("test_singleDocumentNullWithAPIDescription_encode", test_singleDocumentNullWithAPIDescription_encode), - ("test_singleDocumentSomeIncludes", test_singleDocumentSomeIncludes), - ("test_singleDocumentSomeIncludes_encode", test_singleDocumentSomeIncludes_encode), - ("test_singleDocumentSomeIncludesAddIncludes", test_singleDocumentSomeIncludesAddIncludes), - ("test_singleDocumentSomeIncludesWithAPIDescription", test_singleDocumentSomeIncludesWithAPIDescription), - ("test_singleDocumentSomeIncludesWithAPIDescription_encode", test_singleDocumentSomeIncludesWithAPIDescription_encode), - ("test_singleDocumentSomeIncludesWithMetadata", test_singleDocumentSomeIncludesWithMetadata), - ("test_singleDocumentSomeIncludesWithMetadata_encode", test_singleDocumentSomeIncludesWithMetadata_encode), - ("test_singleDocumentSomeIncludesWithMetadataWithAPIDescription", test_singleDocumentSomeIncludesWithMetadataWithAPIDescription), - ("test_singleDocumentSomeIncludesWithMetadataWithAPIDescription_encode", test_singleDocumentSomeIncludesWithMetadataWithAPIDescription_encode), - ("test_singleSuccessDocumentNoIncludesAddIncludingType", test_singleSuccessDocumentNoIncludesAddIncludingType), - ("test_singleSuccessDocumentSomeIncludesAddIncludes", test_singleSuccessDocumentSomeIncludesAddIncludes), - ("test_sparseIncludeFullPrimaryResource", test_sparseIncludeFullPrimaryResource), - ("test_sparseIncludeSparsePrimaryResource", test_sparseIncludeSparsePrimaryResource), - ("test_sparsePrimaryResource", test_sparsePrimaryResource), - ("test_sparsePrimaryResourceOptionalAndNil", test_sparsePrimaryResourceOptionalAndNil), - ("test_unknownErrorDocumentAddIncludes", test_unknownErrorDocumentAddIncludes), - ("test_unknownErrorDocumentAddIncludingType", test_unknownErrorDocumentAddIncludingType), - ("test_unknownErrorDocumentMissingLinks", test_unknownErrorDocumentMissingLinks), - ("test_unknownErrorDocumentMissingLinks_encode", test_unknownErrorDocumentMissingLinks_encode), - ("test_unknownErrorDocumentMissingLinksWithAPIDescription", test_unknownErrorDocumentMissingLinksWithAPIDescription), - ("test_unknownErrorDocumentMissingLinksWithAPIDescription_encode", test_unknownErrorDocumentMissingLinksWithAPIDescription_encode), - ("test_unknownErrorDocumentMissingMeta", test_unknownErrorDocumentMissingMeta), - ("test_unknownErrorDocumentMissingMeta_encode", test_unknownErrorDocumentMissingMeta_encode), - ("test_unknownErrorDocumentMissingMetaWithAPIDescription", test_unknownErrorDocumentMissingMetaWithAPIDescription), - ("test_unknownErrorDocumentMissingMetaWithAPIDescription_encode", test_unknownErrorDocumentMissingMetaWithAPIDescription_encode), - ("test_unknownErrorDocumentNoMeta", test_unknownErrorDocumentNoMeta), - ("test_unknownErrorDocumentNoMeta_encode", test_unknownErrorDocumentNoMeta_encode), - ("test_unknownErrorDocumentNoMetaWithAPIDescription", test_unknownErrorDocumentNoMetaWithAPIDescription), - ("test_unknownErrorDocumentNoMetaWithAPIDescription_encode", test_unknownErrorDocumentNoMetaWithAPIDescription_encode), - ("test_unknownErrorDocumentWithLinks", test_unknownErrorDocumentWithLinks), - ("test_unknownErrorDocumentWithLinks_encode", test_unknownErrorDocumentWithLinks_encode), - ("test_unknownErrorDocumentWithLinksWithAPIDescription", test_unknownErrorDocumentWithLinksWithAPIDescription), - ("test_unknownErrorDocumentWithLinksWithAPIDescription_encode", test_unknownErrorDocumentWithLinksWithAPIDescription_encode), - ("test_unknownErrorDocumentWithMeta", test_unknownErrorDocumentWithMeta), - ("test_unknownErrorDocumentWithMeta_encode", test_unknownErrorDocumentWithMeta_encode), - ("test_unknownErrorDocumentWithMetaWithAPIDescription", test_unknownErrorDocumentWithMetaWithAPIDescription), - ("test_unknownErrorDocumentWithMetaWithAPIDescription_encode", test_unknownErrorDocumentWithMetaWithAPIDescription_encode), - ("test_unknownErrorDocumentWithMetaWithLinks", test_unknownErrorDocumentWithMetaWithLinks), - ("test_unknownErrorDocumentWithMetaWithLinks_encode", test_unknownErrorDocumentWithMetaWithLinks_encode), - ("test_unknownErrorDocumentWithMetaWithLinksWithAPIDescription", test_unknownErrorDocumentWithMetaWithLinksWithAPIDescription), - ("test_unknownErrorDocumentWithMetaWithLinksWithAPIDescription_encode", test_unknownErrorDocumentWithMetaWithLinksWithAPIDescription_encode), - ] -} - -extension EmptyObjectDecoderTests { - // DO NOT MODIFY: This is autogenerated, use: - // `swift test --generate-linuxmain` - // to regenerate. - static let __allTests__EmptyObjectDecoderTests = [ - ("testEmptyArray", testEmptyArray), - ("testEmptyStruct", testEmptyStruct), - ("testKeysAndCodingPath", testKeysAndCodingPath), - ("testNonEmptyArray", testNonEmptyArray), - ("testNonEmptyStruct", testNonEmptyStruct), - ("testWantingNestedKeyed", testWantingNestedKeyed), - ("testWantingNestedUnkeyed", testWantingNestedUnkeyed), - ("testWantingNil", testWantingNil), - ("testWantingSingleValue", testWantingSingleValue), - ("testWantsSuper", testWantsSuper), - ] -} - -extension GenericJSONAPIErrorTests { - // DO NOT MODIFY: This is autogenerated, use: - // `swift test --generate-linuxmain` - // to regenerate. - static let __allTests__GenericJSONAPIErrorTests = [ - ("test_decodeKnown", test_decodeKnown), - ("test_decodeUnknown", test_decodeUnknown), - ("test_definedFields", test_definedFields), - ("test_encode", test_encode), - ("test_encodeUnknown", test_encodeUnknown), - ("test_initAndEquality", test_initAndEquality), - ("test_payloadAccess", test_payloadAccess), - ] -} - -extension IncludedTests { - // DO NOT MODIFY: This is autogenerated, use: - // `swift test --generate-linuxmain` - // to regenerate. - static let __allTests__IncludedTests = [ - ("test_appending", test_appending), - ("test_ComboSparseAndFullIncludeTypes", test_ComboSparseAndFullIncludeTypes), - ("test_EightDifferentIncludes", test_EightDifferentIncludes), - ("test_EightDifferentIncludes_encode", test_EightDifferentIncludes_encode), - ("test_ElevenDifferentIncludes", test_ElevenDifferentIncludes), - ("test_ElevenDifferentIncludes_encode", test_ElevenDifferentIncludes_encode), - ("test_FiveDifferentIncludes", test_FiveDifferentIncludes), - ("test_FiveDifferentIncludes_encode", test_FiveDifferentIncludes_encode), - ("test_FourDifferentIncludes", test_FourDifferentIncludes), - ("test_FourDifferentIncludes_encode", test_FourDifferentIncludes_encode), - ("test_NineDifferentIncludes", test_NineDifferentIncludes), - ("test_NineDifferentIncludes_encode", test_NineDifferentIncludes_encode), - ("test_OneInclude", test_OneInclude), - ("test_OneInclude_encode", test_OneInclude_encode), - ("test_OneSparseIncludeType", test_OneSparseIncludeType), - ("test_SevenDifferentIncludes", test_SevenDifferentIncludes), - ("test_SevenDifferentIncludes_encode", test_SevenDifferentIncludes_encode), - ("test_SixDifferentIncludes", test_SixDifferentIncludes), - ("test_SixDifferentIncludes_encode", test_SixDifferentIncludes_encode), - ("test_TenDifferentIncludes", test_TenDifferentIncludes), - ("test_TenDifferentIncludes_encode", test_TenDifferentIncludes_encode), - ("test_ThreeDifferentIncludes", test_ThreeDifferentIncludes), - ("test_ThreeDifferentIncludes_encode", test_ThreeDifferentIncludes_encode), - ("test_TwoDifferentIncludes", test_TwoDifferentIncludes), - ("test_TwoDifferentIncludes_encode", test_TwoDifferentIncludes_encode), - ("test_TwoSameIncludes", test_TwoSameIncludes), - ("test_TwoSameIncludes_encode", test_TwoSameIncludes_encode), - ("test_TwoSparseIncludeTypes", test_TwoSparseIncludeTypes), - ("test_zeroIncludes", test_zeroIncludes), - ("test_zeroIncludes_encode", test_zeroIncludes_encode), - ("test_zeroIncludes_init", test_zeroIncludes_init), - ] -} - -extension IncludesDecodingErrorTests { - // DO NOT MODIFY: This is autogenerated, use: - // `swift test --generate-linuxmain` - // to regenerate. - static let __allTests__IncludesDecodingErrorTests = [ - ("test_unexpectedIncludeType", test_unexpectedIncludeType), - ] -} - -extension LinksTests { - // DO NOT MODIFY: This is autogenerated, use: - // `swift test --generate-linuxmain` - // to regenerate. - static let __allTests__LinksTests = [ - ("test_linkFailsIfMetaNotFound", test_linkFailsIfMetaNotFound), - ("test_linkWithMetadata", test_linkWithMetadata), - ("test_linkWithMetadata_encode", test_linkWithMetadata_encode), - ("test_linkWithNoMeta", test_linkWithNoMeta), - ("test_linkWithNoMeta_encode", test_linkWithNoMeta_encode), - ("test_linkWithNoMetaWithoutOptionalLink", test_linkWithNoMetaWithoutOptionalLink), - ("test_linkWithNoMetaWithoutOptionalLink_encode", test_linkWithNoMetaWithoutOptionalLink_encode), - ("test_linkWithNullMetadata", test_linkWithNullMetadata), - ("test_linkWithNullMetadata_encode", test_linkWithNullMetadata_encode), - ] -} - -extension NonJSONAPIRelatableTests { - // DO NOT MODIFY: This is autogenerated, use: - // `swift test --generate-linuxmain` - // to regenerate. - static let __allTests__NonJSONAPIRelatableTests = [ - ("test_initialization1", test_initialization1), - ("test_initialization2_all_relationships_missing", test_initialization2_all_relationships_missing), - ("test_initialization2_all_relationships_there", test_initialization2_all_relationships_there), - ] -} - -extension PolyProxyTests { - // DO NOT MODIFY: This is autogenerated, use: - // `swift test --generate-linuxmain` - // to regenerate. - static let __allTests__PolyProxyTests = [ - ("test_AsymmetricEncodeDecodeUserA", test_AsymmetricEncodeDecodeUserA), - ("test_AsymmetricEncodeDecodeUserB", test_AsymmetricEncodeDecodeUserB), - ("test_CannotEncodeOrDecodePoly0", test_CannotEncodeOrDecodePoly0), - ("test_generalReasonableness", test_generalReasonableness), - ("test_UserAAndBEncodeEquality", test_UserAAndBEncodeEquality), - ("test_UserADecode", test_UserADecode), - ("test_UserADecode_deprecated", test_UserADecode_deprecated), - ("test_UserBDecode", test_UserBDecode), - ("test_UserBDecode_deprecated", test_UserBDecode_deprecated), - ] -} - -extension RelationshipTests { - // DO NOT MODIFY: This is autogenerated, use: - // `swift test --generate-linuxmain` - // to regenerate. - static let __allTests__RelationshipTests = [ - ("test_initToManyWithEntities", test_initToManyWithEntities), - ("test_initToManyWithRelationships", test_initToManyWithRelationships), - ("test_ToManyRelationship", test_ToManyRelationship), - ("test_ToManyRelationship_encode", test_ToManyRelationship_encode), - ("test_ToManyRelationshipWithLinks", test_ToManyRelationshipWithLinks), - ("test_ToManyRelationshipWithLinks_encode", test_ToManyRelationshipWithLinks_encode), - ("test_ToManyRelationshipWithMeta", test_ToManyRelationshipWithMeta), - ("test_ToManyRelationshipWithMeta_encode", test_ToManyRelationshipWithMeta_encode), - ("test_ToManyRelationshipWithMetaAndLinks", test_ToManyRelationshipWithMetaAndLinks), - ("test_ToManyRelationshipWithMetaAndLinks_encode", test_ToManyRelationshipWithMetaAndLinks_encode), - ("test_ToManyTypeMismatch", test_ToManyTypeMismatch), - ("test_ToOneNullableIsEqualToNonNullableIfNotNil", test_ToOneNullableIsEqualToNonNullableIfNotNil), - ("test_ToOneNullableIsNullIfNil", test_ToOneNullableIsNullIfNil), - ("test_ToOneRelationship", test_ToOneRelationship), - ("test_ToOneRelationship_encode", test_ToOneRelationship_encode), - ("test_ToOneRelationshipWithLinks", test_ToOneRelationshipWithLinks), - ("test_ToOneRelationshipWithLinks_encode", test_ToOneRelationshipWithLinks_encode), - ("test_ToOneRelationshipWithMeta", test_ToOneRelationshipWithMeta), - ("test_ToOneRelationshipWithMeta_encode", test_ToOneRelationshipWithMeta_encode), - ("test_ToOneRelationshipWithMetaAndLinks", test_ToOneRelationshipWithMetaAndLinks), - ("test_ToOneRelationshipWithMetaAndLinks_encode", test_ToOneRelationshipWithMetaAndLinks_encode), - ("test_ToOneTypeMismatch", test_ToOneTypeMismatch), - ] -} - -extension ResourceBodyTests { - // DO NOT MODIFY: This is autogenerated, use: - // `swift test --generate-linuxmain` - // to regenerate. - static let __allTests__ResourceBodyTests = [ - ("test_initializers", test_initializers), - ("test_manyResourceBody", test_manyResourceBody), - ("test_manyResourceBody_encode", test_manyResourceBody_encode), - ("test_manyResourceBodyEmpty", test_manyResourceBodyEmpty), - ("test_manyResourceBodyEmpty_encode", test_manyResourceBodyEmpty_encode), - ("test_manyResourceBodyMerge", test_manyResourceBodyMerge), - ("test_singleResourceBody", test_singleResourceBody), - ("test_singleResourceBody_encode", test_singleResourceBody_encode), - ("test_SparseManyBodyEncode", test_SparseManyBodyEncode), - ("test_SparseSingleBodyEncode", test_SparseSingleBodyEncode), - ] -} - -extension ResourceObjectDecodingErrorTests { - // DO NOT MODIFY: This is autogenerated, use: - // `swift test --generate-linuxmain` - // to regenerate. - static let __allTests__ResourceObjectDecodingErrorTests = [ - ("test_missingAttributesObject", test_missingAttributesObject), - ("test_missingRelationshipsObject", test_missingRelationshipsObject), - ("test_NonNullable_attribute", test_NonNullable_attribute), - ("test_NonNullable_relationship", test_NonNullable_relationship), - ("test_NonNullable_relationship2", test_NonNullable_relationship2), - ("test_oneTypeVsAnother_attribute", test_oneTypeVsAnother_attribute), - ("test_oneTypeVsAnother_attribute2", test_oneTypeVsAnother_attribute2), - ("test_oneTypeVsAnother_attribute3", test_oneTypeVsAnother_attribute3), - ("test_oneTypeVsAnother_relationship", test_oneTypeVsAnother_relationship), - ("test_required_attribute", test_required_attribute), - ("test_required_relationship", test_required_relationship), - ("test_transformed_attribute", test_transformed_attribute), - ("test_transformed_attribute2", test_transformed_attribute2), - ("test_twoOneVsToMany_relationship", test_twoOneVsToMany_relationship), - ("test_type_missing", test_type_missing), - ("test_type_null", test_type_null), - ("test_wrongDecodedType", test_wrongDecodedType), - ("test_wrongJSONAPIType", test_wrongJSONAPIType), - ] -} - -extension ResourceObjectReplacingTests { - // DO NOT MODIFY: This is autogenerated, use: - // `swift test --generate-linuxmain` - // to regenerate. - static let __allTests__ResourceObjectReplacingTests = [ - ("test_replaceImmutableAttributes", test_replaceImmutableAttributes), - ("test_replaceImmutableRelationships", test_replaceImmutableRelationships), - ("test_replaceMutableAttributes", test_replaceMutableAttributes), - ("test_replaceMutableRelationships", test_replaceMutableRelationships), - ("test_tapImmutableAttributes", test_tapImmutableAttributes), - ("test_tapImmutableRelationships", test_tapImmutableRelationships), - ("test_tapMutableAttributes", test_tapMutableAttributes), - ("test_tapMutableRelationships", test_tapMutableRelationships), - ] -} - -extension ResourceObjectTests { - // DO NOT MODIFY: This is autogenerated, use: - // `swift test --generate-linuxmain` - // to regenerate. - static let __allTests__ResourceObjectTests = [ - ("test_copyIdentifiedByType", test_copyIdentifiedByType), - ("test_copyIdentifiedByValue", test_copyIdentifiedByValue), - ("test_copyWithNewId", test_copyWithNewId), - ("test_entityAllAttribute", test_entityAllAttribute), - ("test_entityAllAttribute_deprecated", test_entityAllAttribute_deprecated), - ("test_entityAllAttribute_encode", test_entityAllAttribute_encode), - ("test_entityBrokenNullableOmittedAttribute", test_entityBrokenNullableOmittedAttribute), - ("test_EntityNoRelationshipsNoAttributes", test_EntityNoRelationshipsNoAttributes), - ("test_EntityNoRelationshipsNoAttributes_encode", test_EntityNoRelationshipsNoAttributes_encode), - ("test_EntityNoRelationshipsSomeAttributes", test_EntityNoRelationshipsSomeAttributes), - ("test_EntityNoRelationshipsSomeAttributes_deprecated", test_EntityNoRelationshipsSomeAttributes_deprecated), - ("test_EntityNoRelationshipsSomeAttributes_encode", test_EntityNoRelationshipsSomeAttributes_encode), - ("test_entityOneNullAndOneOmittedAttribute", test_entityOneNullAndOneOmittedAttribute), - ("test_entityOneNullAndOneOmittedAttribute_deprecated", test_entityOneNullAndOneOmittedAttribute_deprecated), - ("test_entityOneNullAndOneOmittedAttribute_encode", test_entityOneNullAndOneOmittedAttribute_encode), - ("test_entityOneNullAttribute", test_entityOneNullAttribute), - ("test_entityOneNullAttribute_deprecated", test_entityOneNullAttribute_deprecated), - ("test_entityOneNullAttribute_encode", test_entityOneNullAttribute_encode), - ("test_entityOneOmittedAttribute", test_entityOneOmittedAttribute), - ("test_entityOneOmittedAttribute_deprecated", test_entityOneOmittedAttribute_deprecated), - ("test_entityOneOmittedAttribute_encode", test_entityOneOmittedAttribute_encode), - ("test_EntitySomeRelationshipsNoAttributes", test_EntitySomeRelationshipsNoAttributes), - ("test_EntitySomeRelationshipsNoAttributes_encode", test_EntitySomeRelationshipsNoAttributes_encode), - ("test_EntitySomeRelationshipsSomeAttributes", test_EntitySomeRelationshipsSomeAttributes), - ("test_EntitySomeRelationshipsSomeAttributes_deprecated", test_EntitySomeRelationshipsSomeAttributes_deprecated), - ("test_EntitySomeRelationshipsSomeAttributes_encode", test_EntitySomeRelationshipsSomeAttributes_encode), - ("test_EntitySomeRelationshipsSomeAttributesWithLinks", test_EntitySomeRelationshipsSomeAttributesWithLinks), - ("test_EntitySomeRelationshipsSomeAttributesWithLinks_deprecated", test_EntitySomeRelationshipsSomeAttributesWithLinks_deprecated), - ("test_EntitySomeRelationshipsSomeAttributesWithLinks_encode", test_EntitySomeRelationshipsSomeAttributesWithLinks_encode), - ("test_EntitySomeRelationshipsSomeAttributesWithMeta", test_EntitySomeRelationshipsSomeAttributesWithMeta), - ("test_EntitySomeRelationshipsSomeAttributesWithMeta_deprecated", test_EntitySomeRelationshipsSomeAttributesWithMeta_deprecated), - ("test_EntitySomeRelationshipsSomeAttributesWithMeta_encode", test_EntitySomeRelationshipsSomeAttributesWithMeta_encode), - ("test_EntitySomeRelationshipsSomeAttributesWithMetaAndLinks", test_EntitySomeRelationshipsSomeAttributesWithMetaAndLinks), - ("test_EntitySomeRelationshipsSomeAttributesWithMetaAndLinks_deprecated", test_EntitySomeRelationshipsSomeAttributesWithMetaAndLinks_deprecated), - ("test_EntitySomeRelationshipsSomeAttributesWithMetaAndLinks_encode", test_EntitySomeRelationshipsSomeAttributesWithMetaAndLinks_encode), - ("test_initialization", test_initialization), - ("test_IntOver10_encode", test_IntOver10_encode), - ("test_IntOver10_failure", test_IntOver10_failure), - ("test_IntOver10_success", test_IntOver10_success), - ("test_IntToString", test_IntToString), - ("test_IntToString_deprecated", test_IntToString_deprecated), - ("test_IntToString_encode", test_IntToString_encode), - ("test_MetaEntityAttributeAccessWorks", test_MetaEntityAttributeAccessWorks), - ("test_MetaEntityAttributeAccessWorks_deprecated", test_MetaEntityAttributeAccessWorks_deprecated), - ("test_MetaEntityRelationshipAccessWorks", test_MetaEntityRelationshipAccessWorks), - ("test_NonNullOptionalNullableAttribute", test_NonNullOptionalNullableAttribute), - ("test_NonNullOptionalNullableAttribute_deprecated", test_NonNullOptionalNullableAttribute_deprecated), - ("test_NonNullOptionalNullableAttribute_encode", test_NonNullOptionalNullableAttribute_encode), - ("test_nullableRelationshipIsNull", test_nullableRelationshipIsNull), - ("test_nullableRelationshipIsNull_encode", test_nullableRelationshipIsNull_encode), - ("test_nullableRelationshipNotNull", test_nullableRelationshipNotNull), - ("test_nullableRelationshipNotNull_encode", test_nullableRelationshipNotNull_encode), - ("test_nullableRelationshipNotNullOrOmitted", test_nullableRelationshipNotNullOrOmitted), - ("test_nullableRelationshipNotNullOrOmitted_encode", test_nullableRelationshipNotNullOrOmitted_encode), - ("test_NullOptionalNullableAttribute", test_NullOptionalNullableAttribute), - ("test_NullOptionalNullableAttribute_deprecated", test_NullOptionalNullableAttribute_deprecated), - ("test_NullOptionalNullableAttribute_encode", test_NullOptionalNullableAttribute_encode), - ("test_optional_relationship_operator_access", test_optional_relationship_operator_access), - ("test_optionalNullableRelationshipNulled", test_optionalNullableRelationshipNulled), - ("test_optionalNullableRelationshipNulled_encode", test_optionalNullableRelationshipNulled_encode), - ("test_optionalNullableRelationshipOmitted", test_optionalNullableRelationshipOmitted), - ("test_optionalToMany_relationship_opeartor_access", test_optionalToMany_relationship_opeartor_access), - ("test_optionalToManyIsNotOmitted", test_optionalToManyIsNotOmitted), - ("test_optionalToManyIsNotOmitted_encode", test_optionalToManyIsNotOmitted_encode), - ("test_pointerWithMetaAndLinks", test_pointerWithMetaAndLinks), - ("test_relationship_access", test_relationship_access), - ("test_relationship_operator_access", test_relationship_operator_access), - ("test_relationshipIds", test_relationshipIds), - ("test_RleationshipsOfSameType", test_RleationshipsOfSameType), - ("test_RleationshipsOfSameType_encode", test_RleationshipsOfSameType_encode), - ("test_toMany_relationship_operator_access", test_toMany_relationship_operator_access), - ("test_toManyMetaRelationshipAccessWorks", test_toManyMetaRelationshipAccessWorks), - ("test_UnidentifiedEntity", test_UnidentifiedEntity), - ("test_UnidentifiedEntity_deprecated", test_UnidentifiedEntity_deprecated), - ("test_UnidentifiedEntity_encode", test_UnidentifiedEntity_encode), - ("test_unidentifiedEntityAttributeAccess", test_unidentifiedEntityAttributeAccess), - ("test_unidentifiedEntityAttributeAccess_deprecated", test_unidentifiedEntityAttributeAccess_deprecated), - ("test_UnidentifiedEntityWithAttributes", test_UnidentifiedEntityWithAttributes), - ("test_UnidentifiedEntityWithAttributes_deprecated", test_UnidentifiedEntityWithAttributes_deprecated), - ("test_UnidentifiedEntityWithAttributes_encode", test_UnidentifiedEntityWithAttributes_encode), - ("test_UnidentifiedEntityWithAttributesAndLinks", test_UnidentifiedEntityWithAttributesAndLinks), - ("test_UnidentifiedEntityWithAttributesAndLinks_deprecated", test_UnidentifiedEntityWithAttributesAndLinks_deprecated), - ("test_UnidentifiedEntityWithAttributesAndLinks_encode", test_UnidentifiedEntityWithAttributesAndLinks_encode), - ("test_UnidentifiedEntityWithAttributesAndMeta", test_UnidentifiedEntityWithAttributesAndMeta), - ("test_UnidentifiedEntityWithAttributesAndMeta_deprecated", test_UnidentifiedEntityWithAttributesAndMeta_deprecated), - ("test_UnidentifiedEntityWithAttributesAndMeta_encode", test_UnidentifiedEntityWithAttributesAndMeta_encode), - ("test_UnidentifiedEntityWithAttributesAndMetaAndLinks", test_UnidentifiedEntityWithAttributesAndMetaAndLinks), - ("test_UnidentifiedEntityWithAttributesAndMetaAndLinks_deprecated", test_UnidentifiedEntityWithAttributesAndMetaAndLinks_deprecated), - ("test_UnidentifiedEntityWithAttributesAndMetaAndLinks_encode", test_UnidentifiedEntityWithAttributesAndMetaAndLinks_encode), - ] -} - -extension SparseFieldEncoderTests { - // DO NOT MODIFY: This is autogenerated, use: - // `swift test --generate-linuxmain` - // to regenerate. - static let __allTests__SparseFieldEncoderTests = [ - ("test_AccurateCodingPath", test_AccurateCodingPath), - ("test_EverythingArsenal_allOff", test_EverythingArsenal_allOff), - ("test_EverythingArsenal_allOn", test_EverythingArsenal_allOn), - ("test_NestedContainers", test_NestedContainers), - ("test_NilEncode", test_NilEncode), - ("test_SkipsOmittedFields", test_SkipsOmittedFields), - ("test_SuperEncoderIsStillSparse", test_SuperEncoderIsStillSparse), - ] -} - -extension SparseFieldsetTests { - // DO NOT MODIFY: This is autogenerated, use: - // `swift test --generate-linuxmain` - // to regenerate. - static let __allTests__SparseFieldsetTests = [ - ("test_FullEncode", test_FullEncode), - ("test_FullEncode_deprecated", test_FullEncode_deprecated), - ("test_PartialEncode", test_PartialEncode), - ("test_PartialEncode_deprecated", test_PartialEncode_deprecated), - ("test_sparseFieldsMethod", test_sparseFieldsMethod), - ("test_sparseFieldsMethod_deprecated", test_sparseFieldsMethod_deprecated), - ] -} - -extension SuccessAndErrorDocumentTests { - // DO NOT MODIFY: This is autogenerated, use: - // `swift test --generate-linuxmain` - // to regenerate. - static let __allTests__SuccessAndErrorDocumentTests = [ - ("test_errorAccessors", test_errorAccessors), - ("test_successAccessors", test_successAccessors), - ] -} - -extension TransformerTests { - // DO NOT MODIFY: This is autogenerated, use: - // `swift test --generate-linuxmain` - // to regenerate. - static let __allTests__TransformerTests = [ - ("testIdentityTransform", testIdentityTransform), - ("testValidator", testValidator), - ] -} - -public func __allTests() -> [XCTestCaseEntry] { - return [ - testCase(APIDescriptionTests.__allTests__APIDescriptionTests), - testCase(AttributeTests.__allTests__AttributeTests), - testCase(Attribute_FunctorTests.__allTests__Attribute_FunctorTests), - testCase(BasicJSONAPIErrorTests.__allTests__BasicJSONAPIErrorTests), - testCase(ComputedPropertiesTests.__allTests__ComputedPropertiesTests), - testCase(CustomAttributesTests.__allTests__CustomAttributesTests), - testCase(DocumentDecodingErrorTests.__allTests__DocumentDecodingErrorTests), - testCase(DocumentTests.__allTests__DocumentTests), - testCase(EmptyObjectDecoderTests.__allTests__EmptyObjectDecoderTests), - testCase(GenericJSONAPIErrorTests.__allTests__GenericJSONAPIErrorTests), - testCase(IncludedTests.__allTests__IncludedTests), - testCase(IncludesDecodingErrorTests.__allTests__IncludesDecodingErrorTests), - testCase(LinksTests.__allTests__LinksTests), - testCase(NonJSONAPIRelatableTests.__allTests__NonJSONAPIRelatableTests), - testCase(PolyProxyTests.__allTests__PolyProxyTests), - testCase(RelationshipTests.__allTests__RelationshipTests), - testCase(ResourceBodyTests.__allTests__ResourceBodyTests), - testCase(ResourceObjectDecodingErrorTests.__allTests__ResourceObjectDecodingErrorTests), - testCase(ResourceObjectReplacingTests.__allTests__ResourceObjectReplacingTests), - testCase(ResourceObjectTests.__allTests__ResourceObjectTests), - testCase(SparseFieldEncoderTests.__allTests__SparseFieldEncoderTests), - testCase(SparseFieldsetTests.__allTests__SparseFieldsetTests), - testCase(SuccessAndErrorDocumentTests.__allTests__SuccessAndErrorDocumentTests), - testCase(TransformerTests.__allTests__TransformerTests), - ] -} -#endif diff --git a/Tests/LinuxMain.swift b/Tests/LinuxMain.swift deleted file mode 100644 index 450cf0f..0000000 --- a/Tests/LinuxMain.swift +++ /dev/null @@ -1,10 +0,0 @@ -import XCTest - -import JSONAPITestingTests -import JSONAPITests - -var tests = [XCTestCaseEntry]() -tests += JSONAPITestingTests.__allTests() -tests += JSONAPITests.__allTests() - -XCTMain(tests) From a1f19b9a94719450264f40f9ab08fc7df611449a Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Thu, 26 Mar 2020 18:32:41 -0700 Subject: [PATCH 134/235] add diagnostic for linux test run --- Tests/JSONAPITests/SparseFields/SparseFieldEncoderTests.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Tests/JSONAPITests/SparseFields/SparseFieldEncoderTests.swift b/Tests/JSONAPITests/SparseFields/SparseFieldEncoderTests.swift index 2aa9fb2..11fe13f 100644 --- a/Tests/JSONAPITests/SparseFields/SparseFieldEncoderTests.swift +++ b/Tests/JSONAPITests/SparseFields/SparseFieldEncoderTests.swift @@ -38,6 +38,8 @@ class SparseFieldEncoderTests: XCTestCase { let allThingsOnDeserialized = try! JSONSerialization.jsonObject(with: allThingsOn, options: []) as! [String: Any] + print(allThingsOnDeserialized) + XCTAssertNil(allThingsOnDeserialized["omittable"]) XCTAssertNotNil(allThingsOnDeserialized["nullable"] as? NSNull) XCTAssertEqual(allThingsOnDeserialized["bool"] as? Bool, true) From 93c989c65291c7e0c185ae3bfa68e787fe9b62ec Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Thu, 26 Mar 2020 18:44:12 -0700 Subject: [PATCH 135/235] work around Linux --- .../SparseFields/SparseFieldEncoderTests.swift | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Tests/JSONAPITests/SparseFields/SparseFieldEncoderTests.swift b/Tests/JSONAPITests/SparseFields/SparseFieldEncoderTests.swift index 11fe13f..9d5bba5 100644 --- a/Tests/JSONAPITests/SparseFields/SparseFieldEncoderTests.swift +++ b/Tests/JSONAPITests/SparseFields/SparseFieldEncoderTests.swift @@ -38,14 +38,18 @@ class SparseFieldEncoderTests: XCTestCase { let allThingsOnDeserialized = try! JSONSerialization.jsonObject(with: allThingsOn, options: []) as! [String: Any] - print(allThingsOnDeserialized) - XCTAssertNil(allThingsOnDeserialized["omittable"]) XCTAssertNotNil(allThingsOnDeserialized["nullable"] as? NSNull) XCTAssertEqual(allThingsOnDeserialized["bool"] as? Bool, true) XCTAssertEqual(allThingsOnDeserialized["double"] as? Double, 10.5) XCTAssertEqual(allThingsOnDeserialized["string"] as? String, "hello") + #if os(Linux) + // There's some bug with Linux where it won't case the value to a float. + // It does exist and it is == 1.2 + XCTAssertEqual(allThingsOnDeserialized["float"] as? Double, 1.2) + #else XCTAssertEqual(allThingsOnDeserialized["float"] as? Float, 1.2) + #endif XCTAssertEqual(allThingsOnDeserialized["int"] as? Int, 3) XCTAssertEqual(allThingsOnDeserialized["int8"] as? Int8, 4) XCTAssertEqual(allThingsOnDeserialized["int16"] as? Int16, 5) From e1df321d5e32eaee3f35e5c3e7c66a95381d8f61 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Thu, 26 Mar 2020 21:10:26 -0700 Subject: [PATCH 136/235] hopefully key ordering was the problem with a linux test error. --- Tests/JSONAPITests/Test Helpers/EncodeDecode.swift | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/Tests/JSONAPITests/Test Helpers/EncodeDecode.swift b/Tests/JSONAPITests/Test Helpers/EncodeDecode.swift index a588b71..75a93e7 100644 --- a/Tests/JSONAPITests/Test Helpers/EncodeDecode.swift +++ b/Tests/JSONAPITests/Test Helpers/EncodeDecode.swift @@ -9,7 +9,16 @@ import Foundation import XCTest let testDecoder = JSONDecoder() -let testEncoder = JSONEncoder() +let testEncoder: JSONEncoder = { + let encoder = JSONEncoder() + if #available(OSX 10.13, iOS 11.0, *) { + encoder.outputFormatting = .sortedKeys + } + #if os(Linux) + encoder.outputFormatting = .sortedKeys + #endif + return encoder +}() func decoded(type: T.Type, data: Data) -> T { return try! testDecoder.decode(T.self, from: data) From 1a5ab612a11dce38c6f0c7b8ae029b7fb1158274 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Mon, 4 May 2020 22:15:03 -0700 Subject: [PATCH 137/235] Add better error reports for missing type or id on a relationship object --- .../ResourceObjectDecodingError.swift | 23 +++++++++- .../ResourceObjectDecodingErrorTests.swift | 42 +++++++++++++++++++ .../stubs/ResourceObjectStubs.swift | 28 +++++++++++++ 3 files changed, 91 insertions(+), 2 deletions(-) diff --git a/Sources/JSONAPI/Resource/Resource Object/ResourceObjectDecodingError.swift b/Sources/JSONAPI/Resource/Resource Object/ResourceObjectDecodingError.swift index a79f5a1..7495cf3 100644 --- a/Sources/JSONAPI/Resource/Resource Object/ResourceObjectDecodingError.swift +++ b/Sources/JSONAPI/Resource/Resource Object/ResourceObjectDecodingError.swift @@ -23,12 +23,16 @@ public struct ResourceObjectDecodingError: Swift.Error, Equatable { public enum Location: String, Equatable { case attributes case relationships + case relationshipType + case relationshipId case type var singular: String { switch self { case .attributes: return "attribute" case .relationships: return "relationship" + case .relationshipType: return "relationship type" + case .relationshipId: return "relationship Id" case .type: return "type" } } @@ -44,8 +48,19 @@ public struct ResourceObjectDecodingError: Swift.Error, Equatable { (location, subjectName) = Self.context(ctx) cause = .valueNotFound case .keyNotFound(let missingKey, let ctx): - (location, _) = Self.context(ctx) - subjectName = missingKey.stringValue + let (location, name) = Self.context(ctx) + let missingKeyString = missingKey.stringValue + + if location == .relationships && missingKeyString == "type" { + self.location = .relationshipType + subjectName = name + } else if location == .relationships && missingKeyString == "id" { + self.location = .relationshipId + subjectName = name + } else { + self.location = location + subjectName = missingKey.stringValue + } cause = .keyNotFound default: return nil @@ -106,6 +121,10 @@ extension ResourceObjectDecodingError: CustomStringConvertible { return "\(location) object is required and missing." case .keyNotFound where location == .type: return "'type' (a.k.a. JSON:API type name) is required and missing." + case .keyNotFound where location == .relationshipType: + return "'\(subjectName)' relationship does not have a 'type'." + case .keyNotFound where location == .relationshipId: + return "'\(subjectName)' relationship does not have an 'id'." case .keyNotFound: return "'\(subjectName)' \(location.singular) is required and missing." case .valueNotFound where location == .type: diff --git a/Tests/JSONAPITests/ResourceObject/ResourceObjectDecodingErrorTests.swift b/Tests/JSONAPITests/ResourceObject/ResourceObjectDecodingErrorTests.swift index ce8e334..bf93ec3 100644 --- a/Tests/JSONAPITests/ResourceObject/ResourceObjectDecodingErrorTests.swift +++ b/Tests/JSONAPITests/ResourceObject/ResourceObjectDecodingErrorTests.swift @@ -52,6 +52,48 @@ final class ResourceObjectDecodingErrorTests: XCTestCase { } } + func test_relationshipWithNoId() { + XCTAssertThrowsError(try testDecoder.decode( + TestEntity.self, + from: entity_required_relationship_no_id + )) { error in + XCTAssertEqual( + error as? ResourceObjectDecodingError, + ResourceObjectDecodingError( + subjectName: "required", + cause: .keyNotFound, + location: .relationshipId + ) + ) + + XCTAssertEqual( + (error as? ResourceObjectDecodingError)?.description, + "'required' relationship does not have an 'id'." + ) + } + } + + func test_relationshipWithNoType() { + XCTAssertThrowsError(try testDecoder.decode( + TestEntity.self, + from: entity_required_relationship_no_type + )) { error in + XCTAssertEqual( + error as? ResourceObjectDecodingError, + ResourceObjectDecodingError( + subjectName: "required", + cause: .keyNotFound, + location: .relationshipType + ) + ) + + XCTAssertEqual( + (error as? ResourceObjectDecodingError)?.description, + "'required' relationship does not have a 'type'." + ) + } + } + func test_NonNullable_relationship() { XCTAssertThrowsError(try testDecoder.decode( TestEntity.self, diff --git a/Tests/JSONAPITests/ResourceObject/stubs/ResourceObjectStubs.swift b/Tests/JSONAPITests/ResourceObject/stubs/ResourceObjectStubs.swift index 181826f..046aefc 100644 --- a/Tests/JSONAPITests/ResourceObject/stubs/ResourceObjectStubs.swift +++ b/Tests/JSONAPITests/ResourceObject/stubs/ResourceObjectStubs.swift @@ -415,6 +415,34 @@ let entity_nonNullable_relationship_is_null2 = """ } """.data(using: .utf8)! +let entity_required_relationship_no_id = """ +{ + "id": "1", + "type": "thirteenth_test_entities", + "relationships": { + "required": { + "data": { + "type": "thirteenth_test_entities" + } + } + } +} +""".data(using: .utf8)! + +let entity_required_relationship_no_type = """ +{ + "id": "1", + "type": "thirteenth_test_entities", + "relationships": { + "required": { + "data": { + "id": "10" + } + } + } +} +""".data(using: .utf8)! + let entity_required_relationship_is_omitted = """ { "id": "1", From 2b578144292ac737dea2358302ad2881d6c0f55f Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Thu, 7 May 2020 22:24:41 -0700 Subject: [PATCH 138/235] Modifying SuccessDocument to guarantee properties of itself that it can guarantee (obviously the document contains a primary resource body because it is not an error body). Fix some warnings new as of the 5.2 compiler. --- Sources/JSONAPI/Document/Document.swift | 101 ++++++------ .../JSONAPITests/Document/DocumentTests.swift | 151 ++++++++++-------- 2 files changed, 142 insertions(+), 110 deletions(-) diff --git a/Sources/JSONAPI/Document/Document.swift b/Sources/JSONAPI/Document/Document.swift index a266216..4e1fce2 100644 --- a/Sources/JSONAPI/Document/Document.swift +++ b/Sources/JSONAPI/Document/Document.swift @@ -502,21 +502,42 @@ extension Document { /// Document type but you wish to constrain it to success values. public struct SuccessDocument: EncodableJSONAPIDocument { public typealias BodyData = Document.BodyData + public typealias APIDescription = Document.APIDescription + public typealias Body = Document.Body + public typealias PrimaryResourceBody = Document.PrimaryResourceBody + public typealias Include = Document.Include + public typealias MetaType = Document.MetaType + public typealias LinksType = Document.LinksType + + public let apiDescription: APIDescription + public let data: BodyData + public let body: Body + + public var document: Document { + Document( + apiDescription: apiDescription, + body: data.primary, + includes: data.includes, + meta: data.meta, + links: data.links + ) + } - public var body: Document.Body { return document.body } - - private let document: Document - - public init(apiDescription: APIDescription, - body: PrimaryResourceBody, - includes: Includes, - meta: MetaType, - links: LinksType) { - document = .init(apiDescription: apiDescription, - body: body, - includes: includes, - meta: meta, - links: links) + public init( + apiDescription: APIDescription, + body: PrimaryResourceBody, + includes: Includes, + meta: MetaType, + links: LinksType + ) { + self.apiDescription = apiDescription + data = .init( + primary: body, + includes: includes, + meta: meta, + links: links + ) + self.body = .data(data) } public func encode(to encoder: Encoder) throws { @@ -525,50 +546,32 @@ extension Document { try container.encode(document) } - /// The JSON API Spec calls this the JSON:API Object. It contains version - /// and metadata information about the API itself. - public var apiDescription: APIDescription { - return document.apiDescription - } - - /// Get the document data - /// - /// `nil` if the Document is an error response. Otherwise, - /// a structure containing the primary resource, any included - /// resources, metadata, and links. - public var data: BodyData? { - return document.body.data - } - /// Quick access to the `data`'s primary resource. /// - /// `nil` if the Document is an error document. Otherwise, - /// the primary resource body, which will contain zero/one, one/many + /// Guaranteed to exist for a `SuccessDocument`. + /// The primary resource body, which will contain zero/one, one/many /// resources dependening on the `PrimaryResourceBody` type. /// /// See `SingleResourceBody` and `ManyResourceBody`. - public var primaryResource: PrimaryResourceBody? { - return document.body.primaryResource + public var primaryResource: PrimaryResourceBody { + return data.primary } /// Quick access to the `data`'s includes. /// - /// `nil` if the Document is an error document. Otherwise, - /// zero or more includes. - public var includes: Includes? { - return document.body.includes + /// Zero or more includes. + public var includes: Includes { + return data.includes } - /// The metadata for the error or data document or `nil` if - /// no metadata is found. - public var meta: MetaType? { - return document.body.meta + /// The metadata for the data document. + public var meta: MetaType { + return data.meta } - /// The links for the error or data document or `nil` if - /// no links are found. - public var links: LinksType? { - return document.body.links + /// The links for the data document. + public var links: LinksType { + return data.links } public static func ==(lhs: Document, rhs: SuccessDocument) -> Bool { @@ -599,11 +602,15 @@ extension Document.SuccessDocument: Decodable, CodableJSONAPIDocument public init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer() - document = try container.decode(Document.self) + let document = try container.decode(Document.self) - guard !document.body.isError else { + guard case .data(let data) = document.body else { throw DocumentDecodingError.foundErrorDocumentWhenExpectingSuccess } + + self.apiDescription = document.apiDescription + self.data = data + self.body = .data(data) } } diff --git a/Tests/JSONAPITests/Document/DocumentTests.swift b/Tests/JSONAPITests/Document/DocumentTests.swift index 5911a0d..d83b0f6 100644 --- a/Tests/JSONAPITests/Document/DocumentTests.swift +++ b/Tests/JSONAPITests/Document/DocumentTests.swift @@ -167,15 +167,16 @@ extension DocumentTests { XCTAssertNil(document.body.primaryResource) XCTAssertNil(document.body.includes) - guard case let .errors(errors) = document.body else { + guard case let .errors(errors, meta, links) = document.body else { XCTFail("Needed body to be in errors case but it was not.") return } - XCTAssertEqual(errors.0.count, 1) - XCTAssertEqual(errors.0, document.body.errors) - XCTAssertEqual(errors.0[0], .unknown) - XCTAssertEqual(errors.meta, NoMetadata()) + XCTAssertEqual(errors.count, 1) + XCTAssertEqual(errors, document.body.errors) + XCTAssertEqual(errors[0], .unknown) + XCTAssertEqual(meta, NoMetadata()) + XCTAssertEqual(links, NoLinks()) // SuccessDocument XCTAssertThrowsError(try JSONDecoder().decode(Document.SuccessDocument.self, @@ -193,15 +194,16 @@ extension DocumentTests { XCTAssertNil(document2.body.primaryResource) XCTAssertNil(document2.body.includes) - guard case let .errors(errors2) = document2.body else { + guard case let .errors(errors2, meta2, links2) = document2.body else { XCTFail("Needed body to be in errors case but it was not.") return } - XCTAssertEqual(errors2.0.count, 1) - XCTAssertEqual(errors2.0, document2.body.errors) - XCTAssertEqual(errors2.0[0], .unknown) - XCTAssertEqual(errors2.meta, NoMetadata()) + XCTAssertEqual(errors2.count, 1) + XCTAssertEqual(errors2, document2.body.errors) + XCTAssertEqual(errors2[0], .unknown) + XCTAssertEqual(meta2, NoMetadata()) + XCTAssertEqual(links2, NoLinks()) } func test_unknownErrorDocumentAddIncludingType() { @@ -255,15 +257,16 @@ extension DocumentTests { XCTAssertNil(document.body.includes) XCTAssertEqual(document.apiDescription.version, "1.0") - guard case let .errors(errors) = document.body else { + guard case let .errors(errors, meta, links) = document.body else { XCTFail("Needed body to be in errors case but it was not.") return } - XCTAssertEqual(errors.0.count, 1) - XCTAssertEqual(errors.0, document.body.errors) - XCTAssertEqual(errors.0[0], .unknown) - XCTAssertEqual(errors.meta, NoMetadata()) + XCTAssertEqual(errors.count, 1) + XCTAssertEqual(errors, document.body.errors) + XCTAssertEqual(errors[0], .unknown) + XCTAssertEqual(meta, NoMetadata()) + XCTAssertEqual(links, NoLinks()) } func test_unknownErrorDocumentNoMetaWithAPIDescription_encode() { @@ -279,15 +282,16 @@ extension DocumentTests { XCTAssertNil(document.body.primaryResource) XCTAssertNil(document.body.includes) - guard case let .errors(errors) = document.body else { + guard case let .errors(errors, meta, links) = document.body else { XCTFail("Needed body to be in errors case but it was not.") return } - XCTAssertEqual(errors.0.count, 1) - XCTAssertEqual(errors.0, document.body.errors) - XCTAssertEqual(errors.0[0], .unknown) - XCTAssertNil(errors.meta) + XCTAssertEqual(errors.count, 1) + XCTAssertEqual(errors, document.body.errors) + XCTAssertEqual(errors[0], .unknown) + XCTAssertNil(meta) + XCTAssertEqual(links, NoLinks()) } func test_unknownErrorDocumentMissingMeta_encode() { @@ -303,15 +307,16 @@ extension DocumentTests { XCTAssertNil(document.body.includes) XCTAssertEqual(document.apiDescription.version, "1.0") - guard case let .errors(errors) = document.body else { + guard case let .errors(errors, meta, links) = document.body else { XCTFail("Needed body to be in errors case but it was not.") return } - XCTAssertEqual(errors.0.count, 1) - XCTAssertEqual(errors.0, document.body.errors) - XCTAssertEqual(errors.0[0], .unknown) - XCTAssertNil(errors.meta) + XCTAssertEqual(errors.count, 1) + XCTAssertEqual(errors, document.body.errors) + XCTAssertEqual(errors[0], .unknown) + XCTAssertNil(meta) + XCTAssertEqual(links, NoLinks()) } func test_unknownErrorDocumentMissingMetaWithAPIDescription_encode() { @@ -327,15 +332,16 @@ extension DocumentTests { XCTAssertNil(document.body.primaryResource) XCTAssertNil(document.body.includes) - guard case let .errors(errors) = document.body else { + guard case let .errors(errors, meta, links) = document.body else { XCTFail("Needed body to be in errors case but it was not.") return } - XCTAssertEqual(errors.0.count, 1) - XCTAssertEqual(errors.0, document.body.errors) - XCTAssertEqual(errors.0[0], TestError.basic(.init(code: 1, description: "Boooo!"))) - XCTAssertEqual(errors.meta, NoMetadata()) + XCTAssertEqual(errors.count, 1) + XCTAssertEqual(errors, document.body.errors) + XCTAssertEqual(errors[0], TestError.basic(.init(code: 1, description: "Boooo!"))) + XCTAssertEqual(meta, NoMetadata()) + XCTAssertEqual(links, NoLinks()) } func test_errorDocumentNoMeta_encode() { @@ -353,15 +359,16 @@ extension DocumentTests { XCTAssertNil(document.body.includes) XCTAssertEqual(document.apiDescription.version, "1.0") - guard case let .errors(errors) = document.body else { + guard case let .errors(errors, meta, links) = document.body else { XCTFail("Needed body to be in errors case but it was not.") return } - XCTAssertEqual(errors.0.count, 1) - XCTAssertEqual(errors.0, document.body.errors) - XCTAssertEqual(errors.0[0], TestError.basic(.init(code: 1, description: "Boooo!"))) - XCTAssertEqual(errors.meta, NoMetadata()) + XCTAssertEqual(errors.count, 1) + XCTAssertEqual(errors, document.body.errors) + XCTAssertEqual(errors[0], TestError.basic(.init(code: 1, description: "Boooo!"))) + XCTAssertEqual(meta, NoMetadata()) + XCTAssertEqual(links, NoLinks()) } func test_errorDocumentNoMetaWithAPIDescription_encode() { @@ -378,14 +385,15 @@ extension DocumentTests { XCTAssertNil(document.body.primaryResource) XCTAssertNil(document.body.includes) - guard case let .errors(errors) = document.body else { + guard case let .errors(errors, meta, links) = document.body else { XCTFail("Needed body to be in errors case but it was not.") return } - XCTAssertEqual(errors.0.count, 1) - XCTAssertEqual(errors.0, document.body.errors) - XCTAssertEqual(errors.meta, TestPageMetadata(total: 70, limit: 40, offset: 10)) + XCTAssertEqual(errors.count, 1) + XCTAssertEqual(errors, document.body.errors) + XCTAssertEqual(meta, TestPageMetadata(total: 70, limit: 40, offset: 10)) + XCTAssertEqual(links, NoLinks()) } func test_unknownErrorDocumentWithMeta_encode() { @@ -403,14 +411,15 @@ extension DocumentTests { XCTAssertNil(document.body.includes) XCTAssertEqual(document.apiDescription.version, "1.0") - guard case let .errors(errors) = document.body else { + guard case let .errors(errors, meta, links) = document.body else { XCTFail("Needed body to be in errors case but it was not.") return } - XCTAssertEqual(errors.0.count, 1) - XCTAssertEqual(errors.0, document.body.errors) - XCTAssertEqual(errors.meta, TestPageMetadata(total: 70, limit: 40, offset: 10)) + XCTAssertEqual(errors.count, 1) + XCTAssertEqual(errors, document.body.errors) + XCTAssertEqual(meta, TestPageMetadata(total: 70, limit: 40, offset: 10)) + XCTAssertEqual(links, NoLinks()) } func test_unknownErrorDocumentWithMetaWithAPIDescription_encode() { @@ -427,14 +436,21 @@ extension DocumentTests { XCTAssertNil(document.body.primaryResource) XCTAssertNil(document.body.includes) - guard case let .errors(errors) = document.body else { + guard case let .errors(errors, meta, links) = document.body else { XCTFail("Needed body to be in errors case but it was not.") return } - XCTAssertEqual(errors.0.count, 1) - XCTAssertEqual(errors.0, document.body.errors) - XCTAssertEqual(errors.meta, TestPageMetadata(total: 70, limit: 40, offset: 10)) + XCTAssertEqual(errors.count, 1) + XCTAssertEqual(errors, document.body.errors) + XCTAssertEqual(meta, TestPageMetadata(total: 70, limit: 40, offset: 10)) + XCTAssertEqual( + links, + TestLinks( + link: Link(url: "https://website.com", meta: NoMetadata()), + link2: Link(url: "https://othersite.com", meta: TestLinks.TestMetadata(hello: "world")) + ) + ) XCTAssertEqual(document.body.links?.link.url, "https://website.com") XCTAssertEqual(document.body.links?.link.meta, NoMetadata()) XCTAssertEqual(document.body.links?.link2.url, "https://othersite.com") @@ -456,14 +472,15 @@ extension DocumentTests { XCTAssertNil(document.body.includes) XCTAssertEqual(document.apiDescription.version, "1.0") - guard case let .errors(errors) = document.body else { + guard case let .errors(errors, meta, links) = document.body else { XCTFail("Needed body to be in errors case but it was not.") return } - XCTAssertEqual(errors.0.count, 1) - XCTAssertEqual(errors.0, document.body.errors) - XCTAssertEqual(errors.meta, TestPageMetadata(total: 70, limit: 40, offset: 10)) + XCTAssertEqual(errors.count, 1) + XCTAssertEqual(errors, document.body.errors) + XCTAssertEqual(meta, TestPageMetadata(total: 70, limit: 40, offset: 10)) + XCTAssertEqual(links, document.body.links) XCTAssertEqual(document.body.links?.link.url, "https://website.com") XCTAssertEqual(document.body.links?.link.meta, NoMetadata()) XCTAssertEqual(document.body.links?.link2.url, "https://othersite.com") @@ -483,13 +500,15 @@ extension DocumentTests { XCTAssertNil(document.body.primaryResource) XCTAssertNil(document.body.includes) - guard case let .errors(errors) = document.body else { + guard case let .errors(errors, meta, links) = document.body else { XCTFail("Needed body to be in errors case but it was not.") return } - XCTAssertEqual(errors.0.count, 1) - XCTAssertEqual(errors.0, document.body.errors) + XCTAssertEqual(errors.count, 1) + XCTAssertEqual(errors, document.body.errors) + XCTAssertNil(meta) + XCTAssertEqual(links, document.body.links) XCTAssertEqual(document.body.links?.link.url, "https://website.com") XCTAssertEqual(document.body.links?.link.meta, NoMetadata()) XCTAssertEqual(document.body.links?.link2.url, "https://othersite.com") @@ -510,13 +529,15 @@ extension DocumentTests { XCTAssertNil(document.body.includes) XCTAssertEqual(document.apiDescription.version, "1.0") - guard case let .errors(errors) = document.body else { + guard case let .errors(errors, meta, links) = document.body else { XCTFail("Needed body to be in errors case but it was not.") return } - XCTAssertEqual(errors.0.count, 1) - XCTAssertEqual(errors.0, document.body.errors) + XCTAssertEqual(errors.count, 1) + XCTAssertEqual(errors, document.body.errors) + XCTAssertNil(meta) + XCTAssertEqual(links, document.body.links) XCTAssertEqual(document.body.links?.link.url, "https://website.com") XCTAssertEqual(document.body.links?.link.meta, NoMetadata()) XCTAssertEqual(document.body.links?.link2.url, "https://othersite.com") @@ -536,13 +557,15 @@ extension DocumentTests { XCTAssertNil(document.body.primaryResource) XCTAssertNil(document.body.includes) - guard case let .errors(errors) = document.body else { + guard case let .errors(errors, meta, links) = document.body else { XCTFail("Needed body to be in errors case but it was not.") return } - XCTAssertEqual(errors.0.count, 1) - XCTAssertEqual(errors.0, document.body.errors) + XCTAssertEqual(errors.count, 1) + XCTAssertEqual(errors, document.body.errors) + XCTAssertNil(meta) + XCTAssertNil(links) XCTAssertNil(document.body.links) } @@ -560,13 +583,15 @@ extension DocumentTests { XCTAssertNil(document.body.includes) XCTAssertEqual(document.apiDescription.version, "1.0") - guard case let .errors(errors) = document.body else { + guard case let .errors(errors, meta, links) = document.body else { XCTFail("Needed body to be in errors case but it was not.") return } - XCTAssertEqual(errors.0.count, 1) - XCTAssertEqual(errors.0, document.body.errors) + XCTAssertEqual(errors.count, 1) + XCTAssertEqual(errors, document.body.errors) + XCTAssertNil(meta) + XCTAssertNil(links) XCTAssertNil(document.body.links) } From 2f1a21d04e159ed44ee73db537e34e35d014075c Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Thu, 7 May 2020 22:44:02 -0700 Subject: [PATCH 139/235] bump required swift version to 5.2, update some map functions to take advantage of key values as functions. --- Package.resolved | 6 ++--- Package.swift | 5 ++--- README.md | 2 +- Sources/JSONAPI/Document/Includes.swift | 22 +++++++++---------- Sources/JSONAPI/Error/BasicJSONAPIError.swift | 2 +- Sources/JSONAPI/Resource/Relationship.swift | 4 ++-- .../Resource Object/ResourceObject.swift | 4 ++-- .../Comparisons/DocumentCompare.swift | 2 +- .../Comparisons/ResourceObjectCompare.swift | 2 +- .../Comparisons/ArrayCompareTests.swift | 10 ++++----- .../Error/GenericJSONAPIErrorTests.swift | 2 +- .../NonJSONAPIRelatableTests.swift | 2 +- .../Relationships/RelationshipTests.swift | 12 +++++----- .../ResourceObject/ResourceObjectTests.swift | 2 +- 14 files changed, 38 insertions(+), 39 deletions(-) diff --git a/Package.resolved b/Package.resolved index add4b9a..7dfff6d 100644 --- a/Package.resolved +++ b/Package.resolved @@ -3,11 +3,11 @@ "pins": [ { "package": "Poly", - "repositoryURL": "https://github.com/mattpolzin/Poly.git", + "repositoryURL": "https://github.com/mattpolzin/Poly", "state": { "branch": null, - "revision": "0c9c08204142babc480938d704a23513d11420e5", - "version": "2.3.1" + "revision": "9031b8497e025cbcfae196e59eecad5ba988e267", + "version": "2.4.0" } } ] diff --git a/Package.swift b/Package.swift index 9b3e038..e83f775 100644 --- a/Package.swift +++ b/Package.swift @@ -1,5 +1,4 @@ -// swift-tools-version:5.1 -// The swift-tools-version declares the minimum version of Swift required to build this package. +// swift-tools-version:5.2 import PackageDescription @@ -18,7 +17,7 @@ let package = Package( targets: ["JSONAPITesting"]) ], dependencies: [ - .package(url: "https://github.com/mattpolzin/Poly.git", .upToNextMajor(from: "2.3.1")), + .package(url: "https://github.com/mattpolzin/Poly", .upToNextMajor(from: "2.4.0")), ], targets: [ .target( diff --git a/README.md b/README.md index 4e31e3f..3e050c0 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,7 @@ If you find something wrong with this library and it isn't already mentioned und ## Dev Environment ### Prerequisites -1. Swift 5.1+ +1. Swift 5.2+ 2. Swift Package Manager, Xcode 11+, or Cocoapods ### Swift Package Manager diff --git a/Sources/JSONAPI/Document/Includes.swift b/Sources/JSONAPI/Document/Includes.swift index 32f90ea..d449a2d 100644 --- a/Sources/JSONAPI/Document/Includes.swift +++ b/Sources/JSONAPI/Document/Includes.swift @@ -121,7 +121,7 @@ public typealias NoIncludes = Include0 public typealias Include1 = Poly1 extension Includes where I: _Poly1 { public subscript(_ lookup: I.A.Type) -> [I.A] { - return values.compactMap { $0.a } + return values.compactMap(\.a) } } @@ -129,7 +129,7 @@ extension Includes where I: _Poly1 { public typealias Include2 = Poly2 extension Includes where I: _Poly2 { public subscript(_ lookup: I.B.Type) -> [I.B] { - return values.compactMap { $0.b } + return values.compactMap(\.b) } } @@ -137,7 +137,7 @@ extension Includes where I: _Poly2 { public typealias Include3 = Poly3 extension Includes where I: _Poly3 { public subscript(_ lookup: I.C.Type) -> [I.C] { - return values.compactMap { $0.c } + return values.compactMap(\.c) } } @@ -145,7 +145,7 @@ extension Includes where I: _Poly3 { public typealias Include4 = Poly4 extension Includes where I: _Poly4 { public subscript(_ lookup: I.D.Type) -> [I.D] { - return values.compactMap { $0.d } + return values.compactMap(\.d) } } @@ -153,7 +153,7 @@ extension Includes where I: _Poly4 { public typealias Include5 = Poly5 extension Includes where I: _Poly5 { public subscript(_ lookup: I.E.Type) -> [I.E] { - return values.compactMap { $0.e } + return values.compactMap(\.e) } } @@ -161,7 +161,7 @@ extension Includes where I: _Poly5 { public typealias Include6 = Poly6 extension Includes where I: _Poly6 { public subscript(_ lookup: I.F.Type) -> [I.F] { - return values.compactMap { $0.f } + return values.compactMap(\.f) } } @@ -169,7 +169,7 @@ extension Includes where I: _Poly6 { public typealias Include7 = Poly7 extension Includes where I: _Poly7 { public subscript(_ lookup: I.G.Type) -> [I.G] { - return values.compactMap { $0.g } + return values.compactMap(\.g) } } @@ -177,7 +177,7 @@ extension Includes where I: _Poly7 { public typealias Include8 = Poly8 extension Includes where I: _Poly8 { public subscript(_ lookup: I.H.Type) -> [I.H] { - return values.compactMap { $0.h } + return values.compactMap(\.h) } } @@ -185,7 +185,7 @@ extension Includes where I: _Poly8 { public typealias Include9 = Poly9 extension Includes where I: _Poly9 { public subscript(_ lookup: I.I.Type) -> [I.I] { - return values.compactMap { $0.i } + return values.compactMap(\.i) } } @@ -193,7 +193,7 @@ extension Includes where I: _Poly9 { public typealias Include10 = Poly10 extension Includes where I: _Poly10 { public subscript(_ lookup: I.J.Type) -> [I.J] { - return values.compactMap { $0.j } + return values.compactMap(\.j) } } @@ -201,7 +201,7 @@ extension Includes where I: _Poly10 { public typealias Include11 = Poly11 extension Includes where I: _Poly11 { public subscript(_ lookup: I.K.Type) -> [I.K] { - return values.compactMap { $0.k } + return values.compactMap(\.k) } } diff --git a/Sources/JSONAPI/Error/BasicJSONAPIError.swift b/Sources/JSONAPI/Error/BasicJSONAPIError.swift index 7d83814..2c18e69 100644 --- a/Sources/JSONAPI/Error/BasicJSONAPIError.swift +++ b/Sources/JSONAPI/Error/BasicJSONAPIError.swift @@ -61,7 +61,7 @@ public struct BasicJSONAPIErrorPayload: Codable, Eq detail.map { ("detail", $0) }, source.flatMap { $0.pointer.map { ("pointer", $0) } }, source.flatMap { $0.parameter.map { ("parameter", $0) } } - ].compactMap { $0 } + ].compactMap { $0 } return Dictionary(uniqueKeysWithValues: keysAndValues) } diff --git a/Sources/JSONAPI/Resource/Relationship.swift b/Sources/JSONAPI/Resource/Relationship.swift index a11031e..b97905a 100644 --- a/Sources/JSONAPI/Resource/Relationship.swift +++ b/Sources/JSONAPI/Resource/Relationship.swift @@ -79,13 +79,13 @@ public struct ToManyRelationship(pointers: [ToOneRelationship], meta: MetaType, links: LinksType) where T.Identifier == Relatable.Identifier { - ids = pointers.map { $0.id } + ids = pointers.map(\.id) self.meta = meta self.links = links } public init(resourceObjects: [T], meta: MetaType, links: LinksType) where T.Id == Relatable.Identifier { - self.init(ids: resourceObjects.map { $0.id }, meta: meta, links: links) + self.init(ids: resourceObjects.map(\.id), meta: meta, links: links) } private init(meta: MetaType, links: LinksType) { diff --git a/Sources/JSONAPI/Resource/Resource Object/ResourceObject.swift b/Sources/JSONAPI/Resource/Resource Object/ResourceObject.swift index 83f0d1c..8fcefbe 100644 --- a/Sources/JSONAPI/Resource/Resource Object/ResourceObject.swift +++ b/Sources/JSONAPI/Resource/Resource Object/ResourceObject.swift @@ -255,7 +255,7 @@ public extension ResourceObjectProxy { subscript(_ path: KeyPath) -> U? where T.ValueType == U? { // Implementation Note: Handles Transform that returns optional // type. - return attributes[keyPath: path].flatMap { $0.value } + return attributes[keyPath: path].flatMap(\.value) } // MARK: Dynaminc Member Keypath Lookup @@ -277,7 +277,7 @@ public extension ResourceObjectProxy { /// allows you to write `resourceObject[\.propertyName]` instead /// of `resourceObject.attributes.propertyName.value`. subscript(dynamicMember path: KeyPath) -> U? where T.ValueType == U? { - return attributes[keyPath: path].flatMap { $0.value } + return attributes[keyPath: path].flatMap(\.value) } // MARK: Direct Keypath Subscript Lookup diff --git a/Sources/JSONAPITesting/Comparisons/DocumentCompare.swift b/Sources/JSONAPITesting/Comparisons/DocumentCompare.swift index 4cee720..966b0ae 100644 --- a/Sources/JSONAPITesting/Comparisons/DocumentCompare.swift +++ b/Sources/JSONAPITesting/Comparisons/DocumentCompare.swift @@ -50,7 +50,7 @@ public enum BodyComparison: Equatable, CustomStringConvertible { } ).map(BasicComparison.init) .filter { !$0.isSame } - .map { $0.rawValue } + .map(\.rawValue) .joined(separator: ", ") let errorComparisonString = errorComparisons.isEmpty diff --git a/Sources/JSONAPITesting/Comparisons/ResourceObjectCompare.swift b/Sources/JSONAPITesting/Comparisons/ResourceObjectCompare.swift index ddfeb49..a7b5ee9 100644 --- a/Sources/JSONAPITesting/Comparisons/ResourceObjectCompare.swift +++ b/Sources/JSONAPITesting/Comparisons/ResourceObjectCompare.swift @@ -43,7 +43,7 @@ public struct ResourceObjectComparison: Equatable, PropertyComparison { uniquingKeysWith: { $1 } ) .filter { $1 != .same } - .mapValues { $0.rawValue } + .mapValues(\.rawValue) } } diff --git a/Tests/JSONAPITestingTests/Comparisons/ArrayCompareTests.swift b/Tests/JSONAPITestingTests/Comparisons/ArrayCompareTests.swift index fccf84c..cbc8ffe 100644 --- a/Tests/JSONAPITestingTests/Comparisons/ArrayCompareTests.swift +++ b/Tests/JSONAPITestingTests/Comparisons/ArrayCompareTests.swift @@ -22,11 +22,11 @@ final class ArrayCompareTests: XCTestCase { [.same, .same, .same] ) - XCTAssertEqual(comparison.map { $0.description }, ["same", "same", "same"]) + XCTAssertEqual(comparison.map(\.description), ["same", "same", "same"]) XCTAssertEqual(comparison.map(BasicComparison.init(reducing:)), [.same, .same, .same]) - XCTAssertEqual(comparison.map(BasicComparison.init(reducing:)).map { $0.description }, ["same", "same", "same"]) + XCTAssertEqual(comparison.map(BasicComparison.init(reducing:)).map(\.description), ["same", "same", "same"]) } func test_differentLengths() { @@ -42,7 +42,7 @@ final class ArrayCompareTests: XCTestCase { [.same, .same, .missing] ) - XCTAssertEqual(comparison1.map { $0.description }, ["same", "same", "missing"]) + XCTAssertEqual(comparison1.map(\.description), ["same", "same", "missing"]) XCTAssertEqual(comparison1.map(BasicComparison.init(reducing:)), [.same, .same, .different("array length 1", "array length 2")]) @@ -55,7 +55,7 @@ final class ArrayCompareTests: XCTestCase { [.same, .same, .missing] ) - XCTAssertEqual(comparison2.map { $0.description }, ["same", "same", "missing"]) + XCTAssertEqual(comparison2.map(\.description), ["same", "same", "missing"]) XCTAssertEqual(comparison2.map(BasicComparison.init(reducing:)), [.same, .same, .different("array length 1", "array length 2")]) } @@ -73,7 +73,7 @@ final class ArrayCompareTests: XCTestCase { [.differentValues("c", "a"), .same, .differentValues("a", "c")] ) - XCTAssertEqual(comparison.map { $0.description }, ["c ≠ a", "same", "a ≠ c"]) + XCTAssertEqual(comparison.map(\.description), ["c ≠ a", "same", "a ≠ c"]) } func test_reducePrebuilt() { diff --git a/Tests/JSONAPITests/Error/GenericJSONAPIErrorTests.swift b/Tests/JSONAPITests/Error/GenericJSONAPIErrorTests.swift index a3c3552..d645c23 100644 --- a/Tests/JSONAPITests/Error/GenericJSONAPIErrorTests.swift +++ b/Tests/JSONAPITests/Error/GenericJSONAPIErrorTests.swift @@ -140,7 +140,7 @@ private struct TestPayload: Codable, Equatable, ErrorDictType { let keysAndValues = [ ("hello", hello), world.map { ("world", String($0)) } - ].compactMap { $0 } + ].compactMap { $0 } return Dictionary(uniqueKeysWithValues: keysAndValues) } } diff --git a/Tests/JSONAPITests/NonJSONAPIRelatable/NonJSONAPIRelatableTests.swift b/Tests/JSONAPITests/NonJSONAPIRelatable/NonJSONAPIRelatableTests.swift index 7546a29..1947c8e 100644 --- a/Tests/JSONAPITests/NonJSONAPIRelatable/NonJSONAPIRelatableTests.swift +++ b/Tests/JSONAPITests/NonJSONAPIRelatable/NonJSONAPIRelatableTests.swift @@ -30,7 +30,7 @@ class NonJSONAPIRelatableTests: XCTestCase { XCTAssertEqual((entity ~> \.nullableOne)?.rawValue, "hello") XCTAssertEqual((entity ~> \.nullableMaybeOne)?.rawValue, "world") XCTAssertEqual((entity ~> \.maybeOne)?.rawValue, "world") - XCTAssertEqual((entity ~> \.maybeMany)?.map { $0.rawValue }, ["world", "hello"]) + XCTAssertEqual((entity ~> \.maybeMany)?.map(\.rawValue), ["world", "hello"]) } func test_initialization2_all_relationships_missing() { diff --git a/Tests/JSONAPITests/Relationships/RelationshipTests.swift b/Tests/JSONAPITests/Relationships/RelationshipTests.swift index 08c1983..c504f51 100644 --- a/Tests/JSONAPITests/Relationships/RelationshipTests.swift +++ b/Tests/JSONAPITests/Relationships/RelationshipTests.swift @@ -18,7 +18,7 @@ class RelationshipTests: XCTestCase { let relationship = ToManyRelationship(resourceObjects: [entity1, entity2, entity3, entity4]) XCTAssertEqual(relationship.ids.count, 4) - XCTAssertEqual(relationship.ids, [entity1, entity2, entity3, entity4].map { $0.id }) + XCTAssertEqual(relationship.ids, [entity1, entity2, entity3, entity4].map(\.id)) } func test_initToManyWithRelationships() { @@ -29,7 +29,7 @@ class RelationshipTests: XCTestCase { let relationship = ToManyRelationship(pointers: [entity1.pointer, entity2.pointer, entity3.pointer, entity4.pointer]) XCTAssertEqual(relationship.ids.count, 4) - XCTAssertEqual(relationship.ids, [entity1, entity2, entity3, entity4].map { $0.id }) + XCTAssertEqual(relationship.ids, [entity1, entity2, entity3, entity4].map(\.id)) } } @@ -91,7 +91,7 @@ extension RelationshipTests { let relationship = decoded(type: ToManyRelationship.self, data: to_many_relationship) - XCTAssertEqual(relationship.ids.map { $0.rawValue }, ["2DF03B69-4B0A-467F-B52E-B0C9E44FCECF", "90F03B69-4DF1-467F-B52E-B0C9E44FC333", "2DF03B69-4B0A-467F-B52E-B0C9E44FCECF"]) + XCTAssertEqual(relationship.ids.map(\.rawValue), ["2DF03B69-4B0A-467F-B52E-B0C9E44FCECF", "90F03B69-4DF1-467F-B52E-B0C9E44FC333", "2DF03B69-4B0A-467F-B52E-B0C9E44FCECF"]) } func test_ToManyRelationship_encode() { @@ -103,7 +103,7 @@ extension RelationshipTests { let relationship = decoded(type: ToManyWithMeta.self, data: to_many_relationship_with_meta) - XCTAssertEqual(relationship.ids.map { $0.rawValue }, ["2DF03B69-4B0A-467F-B52E-B0C9E44FCECF", "90F03B69-4DF1-467F-B52E-B0C9E44FC333", "2DF03B69-4B0A-467F-B52E-B0C9E44FCECF"]) + XCTAssertEqual(relationship.ids.map(\.rawValue), ["2DF03B69-4B0A-467F-B52E-B0C9E44FCECF", "90F03B69-4DF1-467F-B52E-B0C9E44FC333", "2DF03B69-4B0A-467F-B52E-B0C9E44FCECF"]) XCTAssertEqual(relationship.meta.a, "hello") } @@ -116,7 +116,7 @@ extension RelationshipTests { let relationship = decoded(type: ToManyWithLinks.self, data: to_many_relationship_with_links) - XCTAssertEqual(relationship.ids.map { $0.rawValue }, ["2DF03B69-4B0A-467F-B52E-B0C9E44FCECF", "90F03B69-4DF1-467F-B52E-B0C9E44FC333", "2DF03B69-4B0A-467F-B52E-B0C9E44FCECF"]) + XCTAssertEqual(relationship.ids.map(\.rawValue), ["2DF03B69-4B0A-467F-B52E-B0C9E44FCECF", "90F03B69-4DF1-467F-B52E-B0C9E44FC333", "2DF03B69-4B0A-467F-B52E-B0C9E44FCECF"]) XCTAssertEqual(relationship.links.b, .init(url: "world")) } @@ -129,7 +129,7 @@ extension RelationshipTests { let relationship = decoded(type: ToManyWithMetaAndLinks.self, data: to_many_relationship_with_meta_and_links) - XCTAssertEqual(relationship.ids.map { $0.rawValue }, ["2DF03B69-4B0A-467F-B52E-B0C9E44FCECF", "90F03B69-4DF1-467F-B52E-B0C9E44FC333", "2DF03B69-4B0A-467F-B52E-B0C9E44FCECF"]) + XCTAssertEqual(relationship.ids.map(\.rawValue), ["2DF03B69-4B0A-467F-B52E-B0C9E44FCECF", "90F03B69-4DF1-467F-B52E-B0C9E44FC333", "2DF03B69-4B0A-467F-B52E-B0C9E44FCECF"]) XCTAssertEqual(relationship.meta.a, "hello") XCTAssertEqual(relationship.links.b, .init(url: "world")) } diff --git a/Tests/JSONAPITests/ResourceObject/ResourceObjectTests.swift b/Tests/JSONAPITests/ResourceObject/ResourceObjectTests.swift index c061f1e..61e19a2 100644 --- a/Tests/JSONAPITests/ResourceObject/ResourceObjectTests.swift +++ b/Tests/JSONAPITests/ResourceObject/ResourceObjectTests.swift @@ -188,7 +188,7 @@ extension ResourceObjectTests { XCTAssert(type(of: entity.attributes) == NoAttributes.self) - XCTAssertEqual((entity ~> \.others).map { $0.rawValue }, ["364B3B69-4DF1-467F-B52E-B0C9E44F666E"]) + XCTAssertEqual((entity ~> \.others).map(\.rawValue), ["364B3B69-4DF1-467F-B52E-B0C9E44F666E"]) XCTAssertNoThrow(try TestEntity3.check(entity)) testEncoded(entity: entity) From bed30fba200a8270488415271475ce989a99b3ba Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Thu, 7 May 2020 22:57:35 -0700 Subject: [PATCH 140/235] remove deprecated subscript access to JSONAPI.ResourceObject attributes. --- .../Resource Object/ResourceObject.swift | 35 ---- .../Attribute/Attribute+FunctorTests.swift | 21 --- .../ComputedPropertiesTests.swift | 14 -- .../CustomAttributesTests.swift | 16 -- Tests/JSONAPITests/Poly/PolyProxyTests.swift | 14 -- .../ResourceObject/ResourceObjectTests.swift | 178 ------------------ .../SparseFields/SparseFieldsetTests.swift | 85 --------- documentation/usage.md | 5 - 8 files changed, 368 deletions(-) diff --git a/Sources/JSONAPI/Resource/Resource Object/ResourceObject.swift b/Sources/JSONAPI/Resource/Resource Object/ResourceObject.swift index 8fcefbe..f335d8d 100644 --- a/Sources/JSONAPI/Resource/Resource Object/ResourceObject.swift +++ b/Sources/JSONAPI/Resource/Resource Object/ResourceObject.swift @@ -231,33 +231,6 @@ public extension ResourceObject where EntityRawIdType: CreatableRawIdType { // MARK: - Attribute Access public extension ResourceObjectProxy { - // MARK: Keypath Subscript Lookup - /// Access the attribute at the given keypath. This just - /// allows you to write `resourceObject[\.propertyName]` instead - /// of `resourceObject.attributes.propertyName.value`. - @available(*, deprecated, message: "This will be removed in a future version in favor of `resource.` (dynamic member lookup)") - subscript(_ path: KeyPath) -> T.ValueType { - return attributes[keyPath: path].value - } - - /// Access the attribute at the given keypath. This just - /// allows you to write `resourceObject[\.propertyName]` instead - /// of `resourceObject.attributes.propertyName.value`. - @available(*, deprecated, message: "This will be removed in a future version in favor of `resource.` (dynamic member lookup)") - subscript(_ path: KeyPath) -> T.ValueType? { - return attributes[keyPath: path]?.value - } - - /// Access the attribute at the given keypath. This just - /// allows you to write `resourceObject[\.propertyName]` instead - /// of `resourceObject.attributes.propertyName.value`. - @available(*, deprecated, message: "This will be removed in a future version in favor of `resource.` (dynamic member lookup)") - subscript(_ path: KeyPath) -> U? where T.ValueType == U? { - // Implementation Note: Handles Transform that returns optional - // type. - return attributes[keyPath: path].flatMap(\.value) - } - // MARK: Dynaminc Member Keypath Lookup /// Access the attribute at the given keypath. This just /// allows you to write `resourceObject[\.propertyName]` instead @@ -296,14 +269,6 @@ public extension ResourceObjectProxy { // MARK: - Meta-Attribute Access public extension ResourceObjectProxy { - // MARK: Keypath Subscript Lookup - /// Access an attribute requiring a transformation on the RawValue _and_ - /// a secondary transformation on this entity (self). - @available(*, deprecated, message: "This will be removed in a future version in favor of `resource.` (dynamic member lookup)") - subscript(_ path: KeyPath T>) -> T { - return attributes[keyPath: path](self) - } - // MARK: Dynamic Member Keypath Lookup /// Access an attribute requiring a transformation on the RawValue _and_ /// a secondary transformation on this entity (self). diff --git a/Tests/JSONAPITests/Attribute/Attribute+FunctorTests.swift b/Tests/JSONAPITests/Attribute/Attribute+FunctorTests.swift index 78f217f..49d4257 100644 --- a/Tests/JSONAPITests/Attribute/Attribute+FunctorTests.swift +++ b/Tests/JSONAPITests/Attribute/Attribute+FunctorTests.swift @@ -19,13 +19,6 @@ class Attribute_FunctorTests: XCTestCase { XCTAssertEqual(entity?.computedString, "Frankie2") } - @available(*, deprecated, message: "remove next major version") - func test_mapGuaranteed_deprecated() { - let entity = try? TestType(attributes: .init(name: "Frankie", number: .init(rawValue: 22.0)), relationships: .none, meta: .none, links: .none) - - XCTAssertEqual(entity?[\.computedString], "Frankie2") - } - func test_mapOptionalSuccess() { let entity = try? TestType(attributes: .init(name: "Frankie", number: .init(rawValue: 22.0)), relationships: .none, meta: .none, links: .none) @@ -34,13 +27,6 @@ class Attribute_FunctorTests: XCTestCase { XCTAssertEqual(entity?.computedNumber, 22) } - @available(*, deprecated, message: "remove next major version") - func test_mapOptionalSuccess_deprecated() { - let entity = try? TestType(attributes: .init(name: "Frankie", number: .init(rawValue: 22.0)), relationships: .none, meta: .none, links: .none) - - XCTAssertEqual(entity?[\.computedNumber], 22) - } - func test_mapOptionalFailure() { let entity = try? TestType(attributes: .init(name: "Frankie", number: .init(rawValue: 22.5)), relationships: .none, meta: .none, links: .none) @@ -48,13 +34,6 @@ class Attribute_FunctorTests: XCTestCase { XCTAssertNil(entity?.computedNumber) } - - @available(*, deprecated, message: "remove next major version") - func test_mapOptionalFailure_deprecated() { - let entity = try? TestType(attributes: .init(name: "Frankie", number: .init(rawValue: 22.5)), relationships: .none, meta: .none, links: .none) - - XCTAssertNil(entity?[\.computedNumber]) - } } // MARK: Test types diff --git a/Tests/JSONAPITests/Computed Properties/ComputedPropertiesTests.swift b/Tests/JSONAPITests/Computed Properties/ComputedPropertiesTests.swift index 39ea74d..006a560 100644 --- a/Tests/JSONAPITests/Computed Properties/ComputedPropertiesTests.swift +++ b/Tests/JSONAPITests/Computed Properties/ComputedPropertiesTests.swift @@ -19,13 +19,6 @@ class ComputedPropertiesTests: XCTestCase { XCTAssertNoThrow(try TestType.check(entity)) } - @available(*, deprecated, message: "remove next major version") - func test_DecodeIgnoresComputed_deprecated() { - let entity = decoded(type: TestType.self, data: computed_property_attribute) - - XCTAssertEqual(entity[\.name], "Sarah") - } - func test_EncodeIgnoresComputed() { test_DecodeEncodeEquality(type: TestType.self, data: computed_property_attribute) } @@ -37,13 +30,6 @@ class ComputedPropertiesTests: XCTestCase { XCTAssertEqual(entity[direct: \.directSecretsOut], "shhhh") } - @available(*, deprecated, message: "remove next major version") - func test_ComputedAttributeAccess_deprecated() { - let entity = decoded(type: TestType.self, data: computed_property_attribute) - - XCTAssertEqual(entity[\.computed], "Sarah2") - } - func test_ComputedNonAttributeAccess() { let entity = decoded(type: TestType.self, data: computed_property_attribute) diff --git a/Tests/JSONAPITests/Custom Attributes Tests/CustomAttributesTests.swift b/Tests/JSONAPITests/Custom Attributes Tests/CustomAttributesTests.swift index ec0e7f1..8160cbe 100644 --- a/Tests/JSONAPITests/Custom Attributes Tests/CustomAttributesTests.swift +++ b/Tests/JSONAPITests/Custom Attributes Tests/CustomAttributesTests.swift @@ -18,14 +18,6 @@ class CustomAttributesTests: XCTestCase { XCTAssertNoThrow(try CustomAttributeEntity.check(entity)) } - @available(*, deprecated, message: "remove next major version") - func test_customDecode_deprecated() { - let entity = decoded(type: CustomAttributeEntity.self, data: customAttributeEntityData) - - XCTAssertEqual(entity[\.firstName], "Cool") - XCTAssertEqual(entity[\.name], "Cool Name") - } - func test_customEncode() { test_DecodeEncodeEquality(type: CustomAttributeEntity.self, data: customAttributeEntityData) @@ -39,14 +31,6 @@ class CustomAttributesTests: XCTestCase { XCTAssertNoThrow(try CustomKeysEntity.check(entity)) } - @available(*, deprecated, message: "remove next major version") - func test_customKeysDecode_deprecated() { - let entity = decoded(type: CustomKeysEntity.self, data: customAttributeEntityData) - - XCTAssertEqual(entity[\.firstNameSilly], "Cool") - XCTAssertEqual(entity[\.lastNameSilly], "Name") - } - func test_customKeysEncode() { test_DecodeEncodeEquality(type: CustomKeysEntity.self, data: customAttributeEntityData) diff --git a/Tests/JSONAPITests/Poly/PolyProxyTests.swift b/Tests/JSONAPITests/Poly/PolyProxyTests.swift index efae216..e58148d 100644 --- a/Tests/JSONAPITests/Poly/PolyProxyTests.swift +++ b/Tests/JSONAPITests/Poly/PolyProxyTests.swift @@ -33,13 +33,6 @@ public class PolyProxyTests: XCTestCase { XCTAssertEqual(polyUserA[direct: \.x], .init(x: "y")) } - @available(*, deprecated, message: "remove next major version") - func test_UserADecode_deprecated() { - let polyUserA = decoded(type: User.self, data: poly_user_stub_1) - - XCTAssertEqual(polyUserA[\.name], "Ken Moore") - } - func test_UserAAndBEncodeEquality() { test_DecodeEncodeEquality(type: User.self, data: poly_user_stub_1) test_DecodeEncodeEquality(type: User.self, data: poly_user_stub_2) @@ -74,13 +67,6 @@ public class PolyProxyTests: XCTestCase { XCTAssertEqual(polyUserB.relationships, .none) XCTAssertEqual(polyUserB[direct: \.x], .init(x: "y")) } - - @available(*, deprecated, message: "remove next major version") - func test_UserBDecode_deprecated() { - let polyUserB = decoded(type: User.self, data: poly_user_stub_2) - - XCTAssertEqual(polyUserB[\.name], "Ken Less") - } } // MARK: - Test types diff --git a/Tests/JSONAPITests/ResourceObject/ResourceObjectTests.swift b/Tests/JSONAPITests/ResourceObject/ResourceObjectTests.swift index 61e19a2..5d929e5 100644 --- a/Tests/JSONAPITests/ResourceObject/ResourceObjectTests.swift +++ b/Tests/JSONAPITests/ResourceObject/ResourceObjectTests.swift @@ -72,13 +72,6 @@ class ResourceObjectTests: XCTestCase { XCTAssertEqual(entity.me, "hello") } - @available(*, deprecated, message: "remove next major version") - func test_unidentifiedEntityAttributeAccess_deprecated() { - let entity = UnidentifiedTestEntity(attributes: .init(me: "hello"), relationships: .none, meta: .none, links: .none) - - XCTAssertEqual(entity[\.me], "hello") - } - func test_initialization() { let entity1 = TestEntity1(id: .init(rawValue: "wow"), attributes: .none, relationships: .none, meta: .none, links: .none) let entity2 = TestEntity2(id: .init(rawValue: "cool"), attributes: .none, relationships: .init(other: .init(resourceObject: entity1)), meta: .none, links: .none) @@ -170,13 +163,6 @@ extension ResourceObjectTests { testEncoded(entity: entity) } - @available(*, deprecated, message: "remove next major version") - func test_EntityNoRelationshipsSomeAttributes_deprecated() { - let entity = decoded(type: TestEntity5.self, - data: entity_no_relationships_some_attributes) - XCTAssertEqual(entity[\.floater], 123.321) - } - func test_EntityNoRelationshipsSomeAttributes_encode() { test_DecodeEncodeEquality(type: TestEntity5.self, data: entity_no_relationships_some_attributes) @@ -211,15 +197,6 @@ extension ResourceObjectTests { testEncoded(entity: entity) } - @available(*, deprecated, message: "remove next major version") - func test_EntitySomeRelationshipsSomeAttributes_deprecated() { - let entity = decoded(type: TestEntity4.self, - data: entity_some_relationships_some_attributes) - - XCTAssertEqual(entity[\.word], "coolio") - XCTAssertEqual(entity[\.number], 992299) - } - func test_EntitySomeRelationshipsSomeAttributes_encode() { test_DecodeEncodeEquality(type: TestEntity4.self, data: entity_some_relationships_some_attributes) @@ -241,16 +218,6 @@ extension ResourceObjectTests { testEncoded(entity: entity) } - @available(*, deprecated, message: "remove next major version") - func test_entityOneOmittedAttribute_deprecated() { - let entity = decoded(type: TestEntity6.self, - data: entity_one_omitted_attribute) - - XCTAssertEqual(entity[\.here], "Hello") - XCTAssertNil(entity[\.maybeHere]) - XCTAssertEqual(entity[\.maybeNull], "World") - } - func test_entityOneOmittedAttribute_encode() { test_DecodeEncodeEquality(type: TestEntity6.self, data: entity_one_omitted_attribute) @@ -268,16 +235,6 @@ extension ResourceObjectTests { testEncoded(entity: entity) } - @available(*, deprecated, message: "remove next major version") - func test_entityOneNullAttribute_deprecated() { - let entity = decoded(type: TestEntity6.self, - data: entity_one_null_attribute) - - XCTAssertEqual(entity[\.here], "Hello") - XCTAssertEqual(entity[\.maybeHere], "World") - XCTAssertNil(entity[\.maybeNull]) - } - func test_entityOneNullAttribute_encode() { test_DecodeEncodeEquality(type: TestEntity6.self, data: entity_one_null_attribute) @@ -295,16 +252,6 @@ extension ResourceObjectTests { testEncoded(entity: entity) } - @available(*, deprecated, message: "remove next major version") - func test_entityAllAttribute_deprecated() { - let entity = decoded(type: TestEntity6.self, - data: entity_all_attributes) - - XCTAssertEqual(entity[\.here], "Hello") - XCTAssertEqual(entity[\.maybeHere], "World") - XCTAssertEqual(entity[\.maybeNull], "!") - } - func test_entityAllAttribute_encode() { test_DecodeEncodeEquality(type: TestEntity6.self, data: entity_all_attributes) @@ -322,16 +269,6 @@ extension ResourceObjectTests { testEncoded(entity: entity) } - @available(*, deprecated, message: "remove next major version") - func test_entityOneNullAndOneOmittedAttribute_deprecated() { - let entity = decoded(type: TestEntity6.self, - data: entity_one_null_and_one_missing_attribute) - - XCTAssertEqual(entity[\.here], "Hello") - XCTAssertNil(entity[\.maybeHere]) - XCTAssertNil(entity[\.maybeNull]) - } - func test_entityOneNullAndOneOmittedAttribute_encode() { test_DecodeEncodeEquality(type: TestEntity6.self, data: entity_one_null_and_one_missing_attribute) @@ -353,15 +290,6 @@ extension ResourceObjectTests { testEncoded(entity: entity) } - @available(*, deprecated, message: "remove next major version") - func test_NullOptionalNullableAttribute_deprecated() { - let entity = decoded(type: TestEntity7.self, - data: entity_null_optional_nullable_attribute) - - XCTAssertEqual(entity[\.here], "Hello") - XCTAssertNil(entity[\.maybeHereMaybeNull]) - } - func test_NullOptionalNullableAttribute_encode() { test_DecodeEncodeEquality(type: TestEntity7.self, data: entity_null_optional_nullable_attribute) @@ -378,15 +306,6 @@ extension ResourceObjectTests { testEncoded(entity: entity) } - @available(*, deprecated, message: "remove next major version") - func test_NonNullOptionalNullableAttribute_deprecated() { - let entity = decoded(type: TestEntity7.self, - data: entity_non_null_optional_nullable_attribute) - - XCTAssertEqual(entity[\.here], "Hello") - XCTAssertEqual(entity[\.maybeHereMaybeNull], "World") - } - func test_NonNullOptionalNullableAttribute_encode() { test_DecodeEncodeEquality(type: TestEntity7.self, data: entity_non_null_optional_nullable_attribute) @@ -410,19 +329,6 @@ extension ResourceObjectTests { testEncoded(entity: entity) } - @available(*, deprecated, message: "remove next major version") - func test_IntToString_deprecated() { - let entity = decoded(type: TestEntity8.self, - data: entity_int_to_string_attribute) - - XCTAssertEqual(entity[\.string], "22") - XCTAssertEqual(entity[\.int], 22) - XCTAssertEqual(entity[\.stringFromInt], "22") - XCTAssertEqual(entity[\.plus], 122) - XCTAssertEqual(entity[\.doubleFromInt], 22.0) - XCTAssertEqual(entity[\.nullToString], "nil") - } - func test_IntToString_encode() { test_DecodeEncodeEquality(type: TestEntity8.self, data: entity_int_to_string_attribute) @@ -578,14 +484,6 @@ extension ResourceObjectTests { testEncoded(entity: entity) } - @available(*, deprecated, message: "remove next major version") - func test_UnidentifiedEntity_deprecated() { - let entity = decoded(type: UnidentifiedTestEntity.self, - data: entity_unidentified) - - XCTAssertNil(entity[\.me]) - } - func test_UnidentifiedEntity_encode() { test_DecodeEncodeEquality(type: UnidentifiedTestEntity.self, data: entity_unidentified) @@ -602,14 +500,6 @@ extension ResourceObjectTests { testEncoded(entity: entity) } - @available(*, deprecated, message: "remove next major version") - func test_UnidentifiedEntityWithAttributes_deprecated() { - let entity = decoded(type: UnidentifiedTestEntity.self, - data: entity_unidentified_with_attributes) - - XCTAssertEqual(entity[\.me], "unknown") - } - func test_UnidentifiedEntityWithAttributes_encode() { test_DecodeEncodeEquality(type: UnidentifiedTestEntity.self, data: entity_unidentified_with_attributes) @@ -632,14 +522,6 @@ extension ResourceObjectTests { testEncoded(entity: entity) } - @available(*, deprecated, message: "remove next major version") - func test_UnidentifiedEntityWithAttributesAndMeta_deprecated() { - let entity = decoded(type: UnidentifiedTestEntityWithMeta.self, - data: entity_unidentified_with_attributes_and_meta) - - XCTAssertEqual(entity[\.me], "unknown") - } - func test_UnidentifiedEntityWithAttributesAndMeta_encode() { test_DecodeEncodeEquality(type: UnidentifiedTestEntityWithMeta.self, data: entity_unidentified_with_attributes_and_meta) @@ -657,14 +539,6 @@ extension ResourceObjectTests { testEncoded(entity: entity) } - @available(*, deprecated, message: "remove next major version") - func test_UnidentifiedEntityWithAttributesAndLinks_deprecated() { - let entity = decoded(type: UnidentifiedTestEntityWithLinks.self, - data: entity_unidentified_with_attributes_and_links) - - XCTAssertEqual(entity[\.me], "unknown") - } - func test_UnidentifiedEntityWithAttributesAndLinks_encode() { test_DecodeEncodeEquality(type: UnidentifiedTestEntityWithLinks.self, data: entity_unidentified_with_attributes_and_links) @@ -684,14 +558,6 @@ extension ResourceObjectTests { testEncoded(entity: entity) } - @available(*, deprecated, message: "remove next major version") - func test_UnidentifiedEntityWithAttributesAndMetaAndLinks_deprecated() { - let entity = decoded(type: UnidentifiedTestEntityWithMetaAndLinks.self, - data: entity_unidentified_with_attributes_and_meta_and_links) - - XCTAssertEqual(entity[\.me], "unknown") - } - func test_UnidentifiedEntityWithAttributesAndMetaAndLinks_encode() { test_DecodeEncodeEquality(type: UnidentifiedTestEntityWithMetaAndLinks.self, data: entity_unidentified_with_attributes_and_meta_and_links) @@ -711,15 +577,6 @@ extension ResourceObjectTests { testEncoded(entity: entity) } - @available(*, deprecated, message: "remove next major version") - func test_EntitySomeRelationshipsSomeAttributesWithMeta_deprecated() { - let entity = decoded(type: TestEntity4WithMeta.self, - data: entity_some_relationships_some_attributes_with_meta) - - XCTAssertEqual(entity[\.word], "coolio") - XCTAssertEqual(entity[\.number], 992299) - } - func test_EntitySomeRelationshipsSomeAttributesWithMeta_encode() { test_DecodeEncodeEquality(type: TestEntity4WithMeta.self, data: entity_some_relationships_some_attributes_with_meta) @@ -738,15 +595,6 @@ extension ResourceObjectTests { testEncoded(entity: entity) } - @available(*, deprecated, message: "remove next major version") - func test_EntitySomeRelationshipsSomeAttributesWithLinks_deprecated() { - let entity = decoded(type: TestEntity4WithLinks.self, - data: entity_some_relationships_some_attributes_with_links) - - XCTAssertEqual(entity[\.word], "coolio") - XCTAssertEqual(entity[\.number], 992299) - } - func test_EntitySomeRelationshipsSomeAttributesWithLinks_encode() { test_DecodeEncodeEquality(type: TestEntity4WithLinks.self, data: entity_some_relationships_some_attributes_with_links) @@ -767,15 +615,6 @@ extension ResourceObjectTests { testEncoded(entity: entity) } - @available(*, deprecated, message: "remove next major version") - func test_EntitySomeRelationshipsSomeAttributesWithMetaAndLinks_deprecated() { - let entity = decoded(type: TestEntity4WithMetaAndLinks.self, - data: entity_some_relationships_some_attributes_with_meta_and_links) - - XCTAssertEqual(entity[\.word], "coolio") - XCTAssertEqual(entity[\.number], 992299) - } - func test_EntitySomeRelationshipsSomeAttributesWithMetaAndLinks_encode() { test_DecodeEncodeEquality(type: TestEntity4WithMetaAndLinks.self, data: entity_some_relationships_some_attributes_with_meta_and_links) @@ -800,23 +639,6 @@ extension ResourceObjectTests { XCTAssertEqual(entity1.metaAttribute, true) XCTAssertEqual(entity2.metaAttribute, false) } - - @available(*, deprecated, message: "remove next major version") - func test_MetaEntityAttributeAccessWorks_deprecated() { - let entity1 = TestEntityWithMetaAttribute(id: "even", - attributes: .init(), - relationships: .none, - meta: .none, - links: .none) - let entity2 = TestEntityWithMetaAttribute(id: "odd", - attributes: .init(), - relationships: .none, - meta: .none, - links: .none) - - XCTAssertEqual(entity1[\.metaAttribute], true) - XCTAssertEqual(entity2[\.metaAttribute], false) - } } // MARK: With a Meta Relationship diff --git a/Tests/JSONAPITests/SparseFields/SparseFieldsetTests.swift b/Tests/JSONAPITests/SparseFields/SparseFieldsetTests.swift index bffe9dd..6090d19 100644 --- a/Tests/JSONAPITests/SparseFields/SparseFieldsetTests.swift +++ b/Tests/JSONAPITests/SparseFields/SparseFieldsetTests.swift @@ -50,35 +50,6 @@ class SparseFieldsetTests: XCTestCase { XCTAssertNotNil(attributesDict?["optionalNullable"] as? NSNull) } - @available(*, deprecated, message: "remove next major version") - func test_FullEncode_deprecated() { - let jsonEncoder = JSONEncoder() - let sparseWithEverything = SparseFieldset(testEverythingObject, fields: EverythingTest.Attributes.CodingKeys.allCases) - - let encoded = try! jsonEncoder.encode(sparseWithEverything) - - let deserialized = try! JSONSerialization.jsonObject(with: encoded, - options: []) - - let outerDict = deserialized as? [String: Any] - let attributesDict = outerDict?["attributes"] as? [String: Any] - - XCTAssertEqual(attributesDict?["bool"] as? Bool, - testEverythingObject[\.bool]) - XCTAssertEqual(attributesDict?["int"] as? Int, - testEverythingObject[\.int]) - XCTAssertEqual(attributesDict?["double"] as? Double, - testEverythingObject[\.double]) - XCTAssertEqual(attributesDict?["string"] as? String, - testEverythingObject[\.string]) - XCTAssertEqual((attributesDict?["nestedStruct"] as? [String: String])?["hello"], - testEverythingObject[\.nestedStruct].hello) - XCTAssertEqual(attributesDict?["nestedEnum"] as? String, - testEverythingObject[\.nestedEnum].rawValue) - XCTAssertEqual(attributesDict?["array"] as? [Bool], - testEverythingObject[\.array]) - } - func test_PartialEncode() { let jsonEncoder = JSONEncoder() let sparseObject = SparseFieldset(testEverythingObject, fields: [.string, .bool, .array]) @@ -114,34 +85,6 @@ class SparseFieldsetTests: XCTestCase { XCTAssertNil(attributesDict?["optionalNullable"]) } - @available(*, deprecated, message: "remove next major version") - func test_PartialEncode_deprecated() { - let jsonEncoder = JSONEncoder() - let sparseObject = SparseFieldset(testEverythingObject, fields: [.string, .bool, .array]) - - let encoded = try! jsonEncoder.encode(sparseObject) - - let deserialized = try! JSONSerialization.jsonObject(with: encoded, - options: []) - - let outerDict = deserialized as? [String: Any] - let id = outerDict?["id"] as? String - let type = outerDict?["type"] as? String - let attributesDict = outerDict?["attributes"] as? [String: Any] - let relationships = outerDict?["relationships"] - - XCTAssertEqual(id, testEverythingObject.id.rawValue) - XCTAssertEqual(type, EverythingTest.jsonType) - XCTAssertNil(relationships) - - XCTAssertEqual(attributesDict?["bool"] as? Bool, - testEverythingObject[\.bool]) - XCTAssertEqual(attributesDict?["string"] as? String, - testEverythingObject[\.string]) - XCTAssertEqual(attributesDict?["array"] as? [Bool], - testEverythingObject[\.array]) - } - func test_sparseFieldsMethod() { let jsonEncoder = JSONEncoder() let sparseObject = testEverythingObject.sparse(with: [.string, .bool, .array]) @@ -176,34 +119,6 @@ class SparseFieldsetTests: XCTestCase { XCTAssertNil(attributesDict?["nullable"]) XCTAssertNil(attributesDict?["optionalNullable"]) } - - @available(*, deprecated, message: "remove next major version") - func test_sparseFieldsMethod_deprecated() { - let jsonEncoder = JSONEncoder() - let sparseObject = testEverythingObject.sparse(with: [.string, .bool, .array]) - - let encoded = try! jsonEncoder.encode(sparseObject) - - let deserialized = try! JSONSerialization.jsonObject(with: encoded, - options: []) - - let outerDict = deserialized as? [String: Any] - let id = outerDict?["id"] as? String - let type = outerDict?["type"] as? String - let attributesDict = outerDict?["attributes"] as? [String: Any] - let relationships = outerDict?["relationships"] - - XCTAssertEqual(id, testEverythingObject.id.rawValue) - XCTAssertEqual(type, EverythingTest.jsonType) - XCTAssertNil(relationships) - - XCTAssertEqual(attributesDict?["bool"] as? Bool, - testEverythingObject[\.bool]) - XCTAssertEqual(attributesDict?["string"] as? String, - testEverythingObject[\.string]) - XCTAssertEqual(attributesDict?["array"] as? [Bool], - testEverythingObject[\.array]) - } } struct EverythingTestDescription: JSONAPI.ResourceObjectDescription { diff --git a/documentation/usage.md b/documentation/usage.md index 5cfc770..03ef8f7 100644 --- a/documentation/usage.md +++ b/documentation/usage.md @@ -192,11 +192,6 @@ As of Swift 5.1, `Attributes` can be accessed via dynamic member keypath lookup let favoriteColor: String = person.favoriteColor ``` -:warning: `Attributes` can also be accessed via the older `subscript` operator, but this is a deprecated feature that will be removed in the next major version: -```swift -let favoriteColor: String = person[\.favoriteColor] -``` - #### `Transformer` Sometimes you need to use a type that does not encode or decode itself in the way you need to represent it as a serialized JSON object. For example, the Swift `Foundation` type `Date` can encode/decode itself to `Double` out of the box, but you might want to represent dates as ISO 8601 compliant `String`s instead. The Foundation library `JSONDecoder` has a setting to make this adjustment, but for the sake of an example, you could create a `Transformer`. From 89139a46cdc3110cd0d6286ee0033b597d0eaa69 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Thu, 7 May 2020 23:01:35 -0700 Subject: [PATCH 141/235] drop 5.1 tests --- .github/workflows/tests.yml | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index f6da800..6bac9f8 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -3,20 +3,6 @@ name: Tests on: [push] jobs: - xenial_5_1: - container: - image: swift:5.1-xenial - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v1 - - run: swift test --enable-test-discovery - bionic_5_1: - container: - image: swift:5.1-bionic - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v1 - - run: swift test --enable-test-discovery xenial_5_2: container: image: swift:5.2-xenial From cc3a577d78bc8bf3d8ee9e753568ccbf9dde5a8d Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Thu, 7 May 2020 23:10:49 -0700 Subject: [PATCH 142/235] update README to refer to the 4.0 alpha --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3e050c0..0d918b3 100644 --- a/README.md +++ b/README.md @@ -66,7 +66,7 @@ If you find something wrong with this library and it isn't already mentioned und ### Swift Package Manager Just include the following in your package's dependencies and add `JSONAPI` to the dependencies for any of your targets. ```swift -.package(url: "https://github.com/mattpolzin/JSONAPI.git", .upToNextMajor(from: "3.0.0")) +.package(url: "https://github.com/mattpolzin/JSONAPI", from: "4.0.0-alpha.1") ``` ### Xcode project From 7c99ab4542a0fa60987ceb19bfe5c47b915e6243 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Fri, 8 May 2020 00:36:44 -0700 Subject: [PATCH 143/235] Add new MetaRelationship type to address the use-case of a relationship object with no data entry. --- README.md | 2 +- Sources/JSONAPI/Resource/Relationship.swift | 70 ++++++++++++++-- .../Resource Object/ResourceObject.swift | 4 +- .../Comparisons/RelationshipsCompare.swift | 18 ++++ .../JSONAPITesting/ResourceObjectCheck.swift | 1 + .../RelationshipsCompareTests.swift | 70 +++++++++++++--- .../JSONAPITests/Document/DocumentTests.swift | 84 ++++++++++++------- .../Relationships/RelationshipTests.swift | 51 +++++++++++ .../stubs/RelationshipStubs.swift | 27 ++++++ documentation/usage.md | 6 +- 10 files changed, 281 insertions(+), 52 deletions(-) diff --git a/README.md b/README.md index 0d918b3..39bf232 100644 --- a/README.md +++ b/README.md @@ -124,7 +124,7 @@ Note that Playground support for importing non-system Frameworks is still a bit #### Resource Object Validator - [x] Disallow optional array in `Attribute` (should be empty array, not `null`). - [x] Only allow `TransformedAttribute` and its derivatives as stored properties within `Attributes` struct. Computed properties can still be any type because they do not get encoded or decoded. -- [x] Only allow `ToManyRelationship` and `ToOneRelationship` within `Relationships` struct. +- [x] Only allow `MetaRelationship`, `ToManyRelationship` and `ToOneRelationship` within `Relationships` struct. ### Potential Improvements These ideas could be implemented in future versions. diff --git a/Sources/JSONAPI/Resource/Relationship.swift b/Sources/JSONAPI/Resource/Relationship.swift index b97905a..c1003d8 100644 --- a/Sources/JSONAPI/Resource/Relationship.swift +++ b/Sources/JSONAPI/Resource/Relationship.swift @@ -13,7 +13,25 @@ public protocol RelationshipType { var meta: MetaType { get } } -/// An ResourceObject relationship that can be encoded to or decoded from +/// A relationship with no `data` entry (it still must contain at least meta or links). +/// A server might choose to expose certain relationships as just a link that can be +/// used to retrieve the related resource(s) in some cases. +/// +/// If the server is going to deliver one or more resource's `id`/`type` in a `data` +/// entry, you want to use either the `ToOneRelationship` or the +/// `ToManyRelationship` instead. +public struct MetaRelationship: RelationshipType, Equatable { + + public let meta: MetaType + public let links: LinksType + + public init(meta: MetaType, links: LinksType) { + self.meta = meta + self.links = links + } +} + +/// A `ResourceObject` relationship that can be encoded to or decoded from /// a JSON API "Resource Linkage." /// See https://jsonapi.org/format/#document-resource-object-linkage /// A convenient typealias might make your code much more legible: `One` @@ -148,6 +166,36 @@ private enum ResourceIdentifierCodingKeys: String, CodingKey { case entityType = "type" } +extension MetaRelationship: Codable { + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: ResourceLinkageCodingKeys.self) + + if let noMeta = NoMetadata() as? MetaType { + meta = noMeta + } else { + meta = try container.decode(MetaType.self, forKey: .meta) + } + + if let noLinks = NoLinks() as? LinksType { + links = noLinks + } else { + links = try container.decode(LinksType.self, forKey: .links) + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: ResourceLinkageCodingKeys.self) + + if MetaType.self != NoMetadata.self { + try container.encode(meta, forKey: .meta) + } + + if LinksType.self != NoLinks.self { + try container.encode(links, forKey: .links) + } + } +} + extension ToOneRelationship: Codable where Identifiable.Identifier: OptionalId { public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: ResourceLinkageCodingKeys.self) @@ -192,14 +240,20 @@ extension ToOneRelationship: Codable where Identifiable.Identifier: OptionalId { type is _DictionaryType.Type else { throw error } - throw JSONAPICodingError.quantityMismatch(expected: .one, - path: context.codingPath) + throw JSONAPICodingError.quantityMismatch( + expected: .one, + path: context.codingPath + ) } let type = try identifier.decode(String.self, forKey: .entityType) guard type == Identifiable.jsonType else { - throw JSONAPICodingError.typeMismatch(expected: Identifiable.jsonType, found: type, path: decoder.codingPath) + throw JSONAPICodingError.typeMismatch( + expected: Identifiable.jsonType, + found: type, + path: decoder.codingPath + ) } id = Identifiable.Identifier(rawValue: try identifier.decode(Identifiable.Identifier.RawType.self, forKey: .id)) @@ -298,12 +352,16 @@ extension ToManyRelationship: Codable { } // MARK: CustomStringDescribable +extension MetaRelationship: CustomStringConvertible { + public var description: String { "MetaRelationship" } +} + extension ToOneRelationship: CustomStringConvertible { - public var description: String { return "Relationship(\(String(describing: id)))" } + public var description: String { "Relationship(\(String(describing: id)))" } } extension ToManyRelationship: CustomStringConvertible { - public var description: String { return "Relationship([\(ids.map(String.init(describing:)).joined(separator: ", "))])" } + public var description: String { "Relationship([\(ids.map(String.init(describing:)).joined(separator: ", "))])" } } private protocol _DictionaryType {} diff --git a/Sources/JSONAPI/Resource/Resource Object/ResourceObject.swift b/Sources/JSONAPI/Resource/Resource Object/ResourceObject.swift index f335d8d..6c8b6f8 100644 --- a/Sources/JSONAPI/Resource/Resource Object/ResourceObject.swift +++ b/Sources/JSONAPI/Resource/Resource Object/ResourceObject.swift @@ -7,8 +7,8 @@ /// A JSON API structure within an ResourceObject that contains -/// named properties of types `ToOneRelationship` and -/// `ToManyRelationship`. +/// named properties of types `MetaRelationship`, `ToOneRelationship` +/// and `ToManyRelationship`. public protocol Relationships: Codable & Equatable {} /// A JSON API structure within an ResourceObject that contains diff --git a/Sources/JSONAPITesting/Comparisons/RelationshipsCompare.swift b/Sources/JSONAPITesting/Comparisons/RelationshipsCompare.swift index 9a8c010..481016e 100644 --- a/Sources/JSONAPITesting/Comparisons/RelationshipsCompare.swift +++ b/Sources/JSONAPITesting/Comparisons/RelationshipsCompare.swift @@ -93,6 +93,24 @@ extension Optional: AbstractRelationship where Wrapped: AbstractRelationship { } } +extension MetaRelationship: AbstractRelationship { + var abstractDescription: String { + return String(describing: + ( + String(describing: meta), + String(describing: links) + ) + ) + } + + func equals(_ other: Any) -> Bool { + guard let attributeB = other as? Self else { + return false + } + return abstractDescription == attributeB.abstractDescription + } +} + extension ToOneRelationship: AbstractRelationship { var abstractDescription: String { if meta is NoMetadata && links is NoLinks { diff --git a/Sources/JSONAPITesting/ResourceObjectCheck.swift b/Sources/JSONAPITesting/ResourceObjectCheck.swift index 13c1724..0ff0a2c 100644 --- a/Sources/JSONAPITesting/ResourceObjectCheck.swift +++ b/Sources/JSONAPITesting/ResourceObjectCheck.swift @@ -47,6 +47,7 @@ private protocol OptionalRelationshipType {} extension Optional: OptionalRelationshipType where Wrapped: RelationshipType {} private protocol _RelationshipType {} +extension MetaRelationship: _RelationshipType {} extension ToOneRelationship: _RelationshipType {} extension ToManyRelationship: _RelationshipType {} diff --git a/Tests/JSONAPITestingTests/Comparisons/RelationshipsCompareTests.swift b/Tests/JSONAPITestingTests/Comparisons/RelationshipsCompareTests.swift index 44b720a..9c1bb8b 100644 --- a/Tests/JSONAPITestingTests/Comparisons/RelationshipsCompareTests.swift +++ b/Tests/JSONAPITestingTests/Comparisons/RelationshipsCompareTests.swift @@ -15,7 +15,10 @@ final class RelationshipsCompareTests: XCTestCase { a: t1, b: t2, c: t3, - d: t4 + d: t4, + e: t5, + f: t6, + g: t7 ) let r2 = r1 @@ -25,7 +28,10 @@ final class RelationshipsCompareTests: XCTestCase { a: t1_differentId, b: t2_differentLinks, c: t3_differentId, - d: t4_differentLinks + d: t4_differentLinks, + e: t5_differentLinks, + f: t6_differentMeta, + g: t7_differentMetaAndLinks ) let r4 = r3 @@ -35,7 +41,10 @@ final class RelationshipsCompareTests: XCTestCase { a: nil, b: nil, c: nil, - d: nil + d: nil, + e: nil, + f: nil, + g: nil ) let r6 = r5 @@ -47,21 +56,30 @@ final class RelationshipsCompareTests: XCTestCase { a: t1, b: nil, c: t3, - d: nil + d: nil, + e: nil, + f: nil, + g: nil ) let r2 = TestRelationships( a: t1_differentId, b: nil, c: t3_differentId, - d: nil + d: nil, + e: nil, + f: nil, + g: nil ) XCTAssertEqual(r1.compare(to: r2), [ "a": .different("Id(123)", "Id(999)"), "b": .same, "c": .different("123, 456", "999, 1010"), - "d": .same + "d": .same, + "e": .same, + "f": .same, + "g": .same ]) } @@ -70,21 +88,30 @@ final class RelationshipsCompareTests: XCTestCase { a: nil, b: t2, c: nil, - d: t4 + d: t4, + e: nil, + f: t6, + g: t7 ) let r2 = TestRelationships( a: nil, b: t2_differentMeta, c: nil, - d: t4_differentMeta + d: t4_differentMeta, + e: nil, + f: t6_differentMeta, + g: t7_differentMetaAndLinks ) XCTAssertEqual(r1.compare(to: r2), [ "a": .same, "b": .different(#"("Id(456)", "hello: world", "link: http://google.com")"#, #"("Id(456)", "hello: there", "link: http://google.com")"#), "c": .same, - "d": .different(#"("123, 456", "hello: world", "link: http://google.com")"#, #"("123, 456", "hello: there", "link: http://google.com")"#) + "d": .different(#"("123, 456", "hello: world", "link: http://google.com")"#, #"("123, 456", "hello: there", "link: http://google.com")"#), + "e": .same, + "f": .different(#"("hello: hi", "No Links")"#, #"("hello: there", "No Links")"#), + "g": .different(#"("hello: hi", "link: http://google.com")"#, #"("hello: there", "link: http://hi.com")"#) ]) } @@ -93,21 +120,30 @@ final class RelationshipsCompareTests: XCTestCase { a: nil, b: t2, c: nil, - d: t4 + d: t4, + e: t5, + f: nil, + g: nil ) let r2 = TestRelationships( a: nil, b: t2_differentLinks, c: nil, - d: t4_differentLinks + d: t4_differentLinks, + e: t5_differentLinks, + f: nil, + g: nil ) XCTAssertEqual(r1.compare(to: r2), [ "a": .same, "b": .different(#"("Id(456)", "hello: world", "link: http://google.com")"#, #"("Id(456)", "hello: world", "link: http://yahoo.com")"#), "c": .same, - "d": .different(#"("123, 456", "hello: world", "link: http://google.com")"#, #"("123, 456", "hello: world", "link: http://yahoo.com")"#) + "d": .different(#"("123, 456", "hello: world", "link: http://google.com")"#, #"("123, 456", "hello: world", "link: http://yahoo.com")"#), + "e": .different(#"("No Metadata", "link: http://google.com")"#, #"("No Metadata", "link: http://hi.com")"#), + "f": .same, + "g": .same ]) } @@ -131,6 +167,9 @@ final class RelationshipsCompareTests: XCTestCase { let t2 = ToOneRelationship(id: "456", meta: .init(hello: "world"), links: .init(link: .init(url: "http://google.com"))) let t3 = ToManyRelationship(ids: ["123", "456"]) let t4 = ToManyRelationship(ids: ["123", "456"], meta: .init(hello: "world"), links: .init(link: .init(url: "http://google.com"))) + let t5 = MetaRelationship(meta: .none, links: .init(link: .init(url: "http://google.com"))) + let t6 = MetaRelationship(meta: .init(hello: "hi"), links: .none) + let t7 = MetaRelationship(meta: .init(hello: "hi"), links: .init(link: .init(url: "http://google.com"))) let t1_differentId = ToOneRelationship(id: "999") let t3_differentId = ToManyRelationship(ids: ["999", "1010"]) @@ -140,6 +179,10 @@ final class RelationshipsCompareTests: XCTestCase { let t2_differentMeta = ToOneRelationship(id: "456", meta: .init(hello: "there"), links: .init(link: .init(url: "http://google.com"))) let t4_differentMeta = ToManyRelationship(ids: ["123", "456"], meta: .init(hello: "there"), links: .init(link: .init(url: "http://google.com"))) + + let t5_differentLinks = MetaRelationship(meta: .none, links: .init(link: .init(url: "http://hi.com"))) + let t6_differentMeta = MetaRelationship(meta: .init(hello: "there"), links: .none) + let t7_differentMetaAndLinks = MetaRelationship(meta: .init(hello: "there"), links: .init(link: .init(url: "http://hi.com"))) } // MARK: - Test Types @@ -174,6 +217,9 @@ extension RelationshipsCompareTests { let b: ToOneRelationship? let c: ToManyRelationship? let d: ToManyRelationship? + let e: MetaRelationship? + let f: MetaRelationship? + let g: MetaRelationship? } struct TestNonRelationships: JSONAPI.Relationships { diff --git a/Tests/JSONAPITests/Document/DocumentTests.swift b/Tests/JSONAPITests/Document/DocumentTests.swift index d83b0f6..3349949 100644 --- a/Tests/JSONAPITests/Document/DocumentTests.swift +++ b/Tests/JSONAPITests/Document/DocumentTests.swift @@ -1116,11 +1116,16 @@ extension DocumentTests { extension DocumentTests { func test_sparsePrimaryResource() { - let primaryResource = Book(attributes: .init(pageCount: 100), - relationships: .init(author: "1234", - series: []), - meta: .none, - links: .none) + let primaryResource = Book( + attributes: .init(pageCount: 100), + relationships: .init( + author: "1234", + series: [], + collection: .init(meta: .none, links: .init(link: .init(url: "https://more.books.com"), link2: .init(url: "http://extra.com/books", meta: .init(hello: "world")))) + ), + meta: .none, + links: .none + ) .sparse(with: [.pageCount]) let document = Document< @@ -1185,20 +1190,30 @@ extension DocumentTests { } func test_sparseIncludeFullPrimaryResource() { - let bookInclude = Book(id: "444", - attributes: .init(pageCount: 113), - relationships: .init(author: "1234", - series: ["443"]), - meta: .none, - links: .none) + let bookInclude = Book( + id: "444", + attributes: .init(pageCount: 113), + relationships: .init( + author: "1234", + series: ["443"], + collection: nil + ), + meta: .none, + links: .none + ) .sparse(with: []) - let primaryResource = Book(id: "443", - attributes: .init(pageCount: 100), - relationships: .init(author: "1234", - series: ["444"]), - meta: .none, - links: .none) + let primaryResource = Book( + id: "443", + attributes: .init(pageCount: 100), + relationships: .init( + author: "1234", + series: ["444"], + collection: nil + ), + meta: .none, + links: .none + ) let document = Document< SingleResourceBody, @@ -1254,20 +1269,30 @@ extension DocumentTests { } func test_sparseIncludeSparsePrimaryResource() { - let bookInclude = Book(id: "444", - attributes: .init(pageCount: 113), - relationships: .init(author: "1234", - series: ["443"]), - meta: .none, - links: .none) + let bookInclude = Book( + id: "444", + attributes: .init(pageCount: 113), + relationships: .init( + author: "1234", + series: ["443"], + collection: nil + ), + meta: .none, + links: .none + ) .sparse(with: []) - let primaryResource = Book(id: "443", - attributes: .init(pageCount: 100), - relationships: .init(author: "1234", - series: ["444"]), - meta: .none, - links: .none) + let primaryResource = Book( + id: "443", + attributes: .init(pageCount: 100), + relationships: .init( + author: "1234", + series: ["444"], + collection: nil + ), + meta: .none, + links: .none + ) .sparse(with: []) let document = Document< @@ -1526,6 +1551,7 @@ extension DocumentTests { struct Relationships: JSONAPI.Relationships { let author: ToOneRelationship let series: ToManyRelationship + let collection: MetaRelationship? } } diff --git a/Tests/JSONAPITests/Relationships/RelationshipTests.swift b/Tests/JSONAPITests/Relationships/RelationshipTests.swift index c504f51..80f9820 100644 --- a/Tests/JSONAPITests/Relationships/RelationshipTests.swift +++ b/Tests/JSONAPITests/Relationships/RelationshipTests.swift @@ -35,6 +35,57 @@ class RelationshipTests: XCTestCase { // MARK: - Encode/Decode extension RelationshipTests { + func test_MetaRelationshipWithMeta() { + let relationship = decoded( + type: MetaRelationship.self, + data: meta_relationship_with_meta + ) + + XCTAssertEqual(relationship.meta, TestMeta(a: "hello")) + XCTAssertEqual(relationship.links, .none) + } + + func test_MetaRelationshipWithMeta_encode() { + test_DecodeEncodeEquality( + type: MetaRelationship.self, + data: meta_relationship_with_meta + ) + } + + func test_MetaRelationshipWithLinks() { + let relationship = decoded( + type: MetaRelationship.self, + data: meta_relationship_with_links + ) + + XCTAssertEqual(relationship.meta, .none) + XCTAssertEqual(relationship.links, TestLinks(b: .init(url: "world"))) + } + + func test_MetaRelationshipWithLinks_encode() { + test_DecodeEncodeEquality( + type: MetaRelationship.self, + data: meta_relationship_with_links + ) + } + + func test_MetaRelationshipWithMetaAndLinks() { + let relationship = decoded( + type: MetaRelationship.self, + data: meta_relationship_with_meta_and_links + ) + + XCTAssertEqual(relationship.meta, TestMeta(a: "hello")) + XCTAssertEqual(relationship.links, TestLinks(b: .init(url: "world"))) + } + + func test_MetaRelationshipWithMetaAndLinks_encode() { + test_DecodeEncodeEquality( + type: MetaRelationship.self, + data: meta_relationship_with_meta_and_links + ) + } + func test_ToOneRelationship() { let relationship = decoded(type: ToOneRelationship.self, data: to_one_relationship) diff --git a/Tests/JSONAPITests/Relationships/stubs/RelationshipStubs.swift b/Tests/JSONAPITests/Relationships/stubs/RelationshipStubs.swift index 2f5d6d5..fcc552a 100644 --- a/Tests/JSONAPITests/Relationships/stubs/RelationshipStubs.swift +++ b/Tests/JSONAPITests/Relationships/stubs/RelationshipStubs.swift @@ -5,6 +5,33 @@ // Created by Mathew Polzin on 11/12/18. // +let meta_relationship_with_meta = """ +{ + "meta": { + "a": "hello" + } +} +""".data(using: .utf8)! + +let meta_relationship_with_links = """ +{ + "links": { + "b": "world" + } +} +""".data(using: .utf8)! + +let meta_relationship_with_meta_and_links = """ +{ + "meta": { + "a": "hello" + }, + "links": { + "b": "world" + } +} +""".data(using: .utf8)! + let to_one_relationship = """ { "data": { diff --git a/documentation/usage.md b/documentation/usage.md index 03ef8f7..5822b57 100644 --- a/documentation/usage.md +++ b/documentation/usage.md @@ -149,11 +149,13 @@ Note that I am calling an unidentified person is a "new" person. I suspect that ### `JSONAPI.Relationships` -There are two types of `Relationships`: `ToOneRelationship` and `ToManyRelationship`. A `ResourceObjectDescription`'s `Relationships` type can contain any number of `Relationship` properties of either of these types. Do not store anything other than `Relationship` properties in the `Relationships` struct of a `ResourceObjectDescription`. +There are three types of `Relationships`: `MetaRelationship`, `ToOneRelationship` and `ToManyRelationship`. A `ResourceObjectDescription`'s `Relationships` type can contain any number of `Relationship` properties of any of these types. Do not store anything other than `Relationship` properties in the `Relationships` struct of a `ResourceObjectDescription`. In addition to identifying resource objects by Id and type, `Relationships` can contain `Meta` or `Links` that follow the same rules as [`Meta`](#jsonapimeta) and [`Links`](#jsonapilinks) elsewhere in the JSON API Document. -To describe a relationship that may be omitted (i.e. the key is not even present in the JSON object), you make the entire `ToOneRelationship` or `ToManyRelationship` optional. However, this is not recommended because you can also represent optional relationships as nullable which means the key is always present. A `ToManyRelationship` can naturally represent the absence of related values with an empty array, so `ToManyRelationship` does not support nullability at all. A `ToOneRelationship` can be marked as nullable (i.e. the value could be either `null` or a resource identifier) like this: +The `MetaRelationship` is special in that it represents a Relationship Object with no `data` (it must contain at least one of `meta` or `links`). The other two relationship types are Relationship Objects with either singular resource linkages (`ToOneRelationship`) or arrays of resource linkages (`ToManyRelationship`). + +To describe a relationship that may be omitted (i.e. the key is not even present in the JSON object), you make the entire `MetaRelationship`, `ToOneRelationship` or `ToManyRelationship` optional. However, this is not recommended because you can also represent optional relationships as nullable which means the key is always present. A `ToManyRelationship` can naturally represent the absence of related values with an empty array, so `ToManyRelationship` does not support nullability at all. A `ToOneRelationship` can be marked as nullable (i.e. the value could be either `null` or a resource identifier) like this: ```swift let nullableRelative: ToOneRelationship ``` From bbd6db6d7a999c4109b4166a726a59157d16d875 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Fri, 8 May 2020 00:56:09 -0700 Subject: [PATCH 144/235] A bit more test coverage for MetaRelationship and a note at the top of a now-confusingly-named section in the documentation. --- .../ResourceObject/ResourceObjectTests.swift | 29 ++++++++++----- .../stubs/ResourceObjectStubs.swift | 36 +++++++++++++++++++ documentation/usage.md | 2 ++ 3 files changed, 58 insertions(+), 9 deletions(-) diff --git a/Tests/JSONAPITests/ResourceObject/ResourceObjectTests.swift b/Tests/JSONAPITests/ResourceObject/ResourceObjectTests.swift index 5d929e5..bdc5bae 100644 --- a/Tests/JSONAPITests/ResourceObject/ResourceObjectTests.swift +++ b/Tests/JSONAPITests/ResourceObject/ResourceObjectTests.swift @@ -27,7 +27,7 @@ class ResourceObjectTests: XCTestCase { func test_optional_relationship_operator_access() { let entity1 = TestEntity1(attributes: .none, relationships: .none, meta: .none, links: .none) - let entity = TestEntity9(attributes: .none, relationships: .init(one: entity1.pointer, nullableOne: .init(resourceObject: entity1, meta: .none, links: .none), optionalOne: .init(resourceObject: entity1, meta: .none, links: .none), optionalNullableOne: nil, optionalMany: .init(resourceObjects: [entity1, entity1], meta: .none, links: .none)), meta: .none, links: .none) + let entity = TestEntity9(attributes: .none, relationships: .init(meta: .init(meta: .init(x: "hello", y: 5), links: .none), optionalMeta: nil, one: entity1.pointer, nullableOne: .init(resourceObject: entity1, meta: .none, links: .none), optionalOne: .init(resourceObject: entity1, meta: .none, links: .none), optionalNullableOne: nil, optionalMany: .init(resourceObjects: [entity1, entity1], meta: .none, links: .none)), meta: .none, links: .none) XCTAssertEqual(entity ~> \.optionalOne, Optional(entity1.id)) XCTAssertEqual((entity ~> \.optionalOne).rawValue, Optional(entity1.id.rawValue)) @@ -44,7 +44,7 @@ class ResourceObjectTests: XCTestCase { func test_optionalToMany_relationship_opeartor_access() { let entity1 = TestEntity1(attributes: .none, relationships: .none, meta: .none, links: .none) - let entity = TestEntity9(attributes: .none, relationships: .init(one: entity1.pointer, nullableOne: .init(resourceObject: entity1, meta: .none, links: .none), optionalOne: nil, optionalNullableOne: nil, optionalMany: .init(resourceObjects: [entity1, entity1], meta: .none, links: .none)), meta: .none, links: .none) + let entity = TestEntity9(attributes: .none, relationships: .init(meta: .init(meta: .init(x: "hello", y: 5), links: .none), optionalMeta: nil, one: entity1.pointer, nullableOne: .init(resourceObject: entity1, meta: .none, links: .none), optionalOne: nil, optionalNullableOne: nil, optionalMany: .init(resourceObjects: [entity1, entity1], meta: .none, links: .none)), meta: .none, links: .none) XCTAssertEqual(entity ~> \.optionalMany, [entity1.id, entity1.id]) } @@ -84,13 +84,13 @@ class ResourceObjectTests: XCTestCase { let _ = TestEntity6(id: .init(rawValue: "6"), attributes: .init(here: .init(value: "here"), maybeHere: nil, maybeNull: .init(value: nil)), relationships: .none, meta: .none, links: .none) let _ = TestEntity7(id: .init(rawValue: "7"), attributes: .init(here: .init(value: "hello"), maybeHereMaybeNull: .init(value: "world")), relationships: .none, meta: .none, links: .none) XCTAssertNoThrow(try TestEntity8(id: .init(rawValue: "8"), attributes: .init(string: .init(value: "hello"), int: .init(value: 10), stringFromInt: .init(rawValue: 20), plus: .init(rawValue: 30), doubleFromInt: .init(rawValue: 32), omitted: nil, nullToString: .init(rawValue: nil)), relationships: .none, meta: .none, links: .none)) - let _ = TestEntity9(id: .init(rawValue: "9"), attributes: .none, relationships: .init(one: entity1.pointer, nullableOne: nil, optionalOne: nil, optionalNullableOne: nil, optionalMany: nil), meta: .none, links: .none) - let _ = TestEntity9(id: .init(rawValue: "9"), attributes: .none, relationships: .init(one: entity1.pointer, nullableOne: .init(resourceObject: nil), optionalOne: nil, optionalNullableOne: nil, optionalMany: nil), meta: .none, links: .none) - let _ = TestEntity9(id: .init(rawValue: "9"), attributes: .none, relationships: .init(one: entity1.pointer, nullableOne: .init(id: nil), optionalOne: nil, optionalNullableOne: nil, optionalMany: nil), meta: .none, links: .none) - let _ = TestEntity9(id: .init(rawValue: "9"), attributes: .none, relationships: .init(one: entity1.pointer, nullableOne: .init(resourceObject: entity1, meta: .none, links: .none), optionalOne: nil, optionalNullableOne: nil, optionalMany: nil), meta: .none, links: .none) - let _ = TestEntity9(id: .init(rawValue: "9"), attributes: .none, relationships: .init(one: entity1.pointer, nullableOne: nil, optionalOne: entity1.pointer, optionalNullableOne: nil, optionalMany: nil), meta: .none, links: .none) - let _ = TestEntity9(id: .init(rawValue: "9"), attributes: .none, relationships: .init(one: entity1.pointer, nullableOne: nil, optionalOne: nil, optionalNullableOne: .init(resourceObject: entity1, meta: .none, links: .none), optionalMany: nil), meta: .none, links: .none) - let _ = TestEntity9(id: .init(rawValue: "9"), attributes: .none, relationships: .init(one: entity1.pointer, nullableOne: nil, optionalOne: nil, optionalNullableOne: .init(resourceObject: entity1, meta: .none, links: .none), optionalMany: .init(resourceObjects: [], meta: .none, links: .none)), meta: .none, links: .none) + let _ = TestEntity9(id: .init(rawValue: "9"), attributes: .none, relationships: .init(meta: .init(meta: .init(x: "hello", y: 5), links: .none), optionalMeta: nil, one: entity1.pointer, nullableOne: nil, optionalOne: nil, optionalNullableOne: nil, optionalMany: nil), meta: .none, links: .none) + let _ = TestEntity9(id: .init(rawValue: "9"), attributes: .none, relationships: .init(meta: .init(meta: .init(x: "hello", y: 5), links: .none), optionalMeta: .init(meta: .init(x: "hello", y: 5), links: .none), one: entity1.pointer, nullableOne: .init(resourceObject: nil), optionalOne: nil, optionalNullableOne: nil, optionalMany: nil), meta: .none, links: .none) + let _ = TestEntity9(id: .init(rawValue: "9"), attributes: .none, relationships: .init(meta: .init(meta: .init(x: "hello", y: 5), links: .none), optionalMeta: nil, one: entity1.pointer, nullableOne: .init(id: nil), optionalOne: nil, optionalNullableOne: nil, optionalMany: nil), meta: .none, links: .none) + let _ = TestEntity9(id: .init(rawValue: "9"), attributes: .none, relationships: .init(meta: .init(meta: .init(x: "hello", y: 5), links: .none), optionalMeta: nil, one: entity1.pointer, nullableOne: .init(resourceObject: entity1, meta: .none, links: .none), optionalOne: nil, optionalNullableOne: nil, optionalMany: nil), meta: .none, links: .none) + let _ = TestEntity9(id: .init(rawValue: "9"), attributes: .none, relationships: .init(meta: .init(meta: .init(x: "hello", y: 5), links: .none), optionalMeta: nil, one: entity1.pointer, nullableOne: nil, optionalOne: entity1.pointer, optionalNullableOne: nil, optionalMany: nil), meta: .none, links: .none) + let _ = TestEntity9(id: .init(rawValue: "9"), attributes: .none, relationships: .init(meta: .init(meta: .init(x: "hello", y: 5), links: .none), optionalMeta: nil, one: entity1.pointer, nullableOne: nil, optionalOne: nil, optionalNullableOne: .init(resourceObject: entity1, meta: .none, links: .none), optionalMany: nil), meta: .none, links: .none) + let _ = TestEntity9(id: .init(rawValue: "9"), attributes: .none, relationships: .init(meta: .init(meta: .init(x: "hello", y: 5), links: .none), optionalMeta: nil, one: entity1.pointer, nullableOne: nil, optionalOne: nil, optionalNullableOne: .init(resourceObject: entity1, meta: .none, links: .none), optionalMany: .init(resourceObjects: [], meta: .none, links: .none)), meta: .none, links: .none) let e10id1 = TestEntity10.Identifier(rawValue: "hello") let e10id2 = TestEntity10.Id(rawValue: "world") let e10id3: TestEntity10.Id = "!" @@ -356,6 +356,8 @@ extension ResourceObjectTests { let entity = decoded(type: TestEntity9.self, data: entity_optional_not_omitted_relationship) + XCTAssertEqual(entity.relationships.meta.meta, TestEntityMeta(x: "world", y: 5)) + XCTAssertEqual(entity.relationships.optionalMeta?.meta, TestEntityMeta(x: "world", y: 5)) XCTAssertEqual((entity ~> \.nullableOne)?.rawValue, "3323") XCTAssertEqual((entity ~> \.one).rawValue, "4459") XCTAssertNil(entity ~> \.optionalOne) @@ -391,6 +393,8 @@ extension ResourceObjectTests { let entity = decoded(type: TestEntity9.self, data: entity_optional_nullable_nulled_relationship) + XCTAssertEqual(entity.relationships.meta.meta, TestEntityMeta(x: "world", y: 5)) + XCTAssertNil(entity.relationships.optionalMeta) XCTAssertEqual((entity ~> \.nullableOne)?.rawValue, "3323") XCTAssertEqual((entity ~> \.one).rawValue, "4459") XCTAssertNil(entity ~> \.optionalNullableOne) @@ -786,6 +790,10 @@ extension ResourceObjectTests { typealias Attributes = NoAttributes public struct Relationships: JSONAPI.Relationships { + let meta: MetaRelationship + + let optionalMeta: MetaRelationship? + let one: ToOneRelationship let nullableOne: ToOneRelationship @@ -835,11 +843,14 @@ extension ResourceObjectTests { public struct Relationships: JSONAPI.Relationships { public init() { + optionalMeta = nil optionalOne = nil optionalNullableOne = nil optionalMany = nil } + let optionalMeta: MetaRelationship? + let optionalOne: ToOneRelationship? let optionalNullableOne: ToOneRelationship? diff --git a/Tests/JSONAPITests/ResourceObject/stubs/ResourceObjectStubs.swift b/Tests/JSONAPITests/ResourceObject/stubs/ResourceObjectStubs.swift index 046aefc..1973276 100644 --- a/Tests/JSONAPITests/ResourceObject/stubs/ResourceObjectStubs.swift +++ b/Tests/JSONAPITests/ResourceObject/stubs/ResourceObjectStubs.swift @@ -233,6 +233,18 @@ let entity_optional_not_omitted_relationship = """ "id": "1", "type": "ninth_test_entities", "relationships": { + "meta": { + "meta": { + "x": "world", + "y": 5 + } + }, + "optionalMeta": { + "meta": { + "x": "world", + "y": 5 + } + }, "nullableOne": { "data": { "id": "3323", @@ -260,6 +272,12 @@ let entity_optional_nullable_nulled_relationship = """ "id": "1", "type": "ninth_test_entities", "relationships": { + "meta": { + "meta": { + "x": "world", + "y": 5 + } + }, "nullableOne": { "data": { "id": "3323", @@ -284,6 +302,12 @@ let entity_omitted_relationship = """ "id": "1", "type": "ninth_test_entities", "relationships": { + "meta": { + "meta": { + "x": "world", + "y": 5 + } + }, "nullableOne": { "data": { "id": "3323", @@ -305,6 +329,12 @@ let entity_optional_to_many_relationship_not_omitted = """ "id": "1", "type": "ninth_test_entities", "relationships": { + "meta": { + "meta": { + "x": "world", + "y": 5 + } + }, "nullableOne": { "data": { "id": "3323", @@ -334,6 +364,12 @@ let entity_nulled_relationship = """ "id": "1", "type": "ninth_test_entities", "relationships": { + "meta": { + "meta": { + "x": "world", + "y": 5 + } + }, "nullableOne": { "data": null }, diff --git a/documentation/usage.md b/documentation/usage.md index 5822b57..a295347 100644 --- a/documentation/usage.md +++ b/documentation/usage.md @@ -580,6 +580,8 @@ let createdAt = user.createdAt This works because `createdAt` is defined in the form: `var {name}: ({ResourceObject}) -> {Value}` where `{ResourceObject}` is the `JSONAPI.ResourceObject` described by the `ResourceObjectDescription` containing the meta-attribute. ### Meta-Relationships +**NOTE** this section describes an ability to create computed relationships, not to be confused with the similarly named `MetaRelationship` type which is used to create relationships that do not have an `id`/`type` (they only have `links` and/or `meta`). + This advanced feature may not ever be useful, but if you find yourself in the situation of dealing with an API that does not 100% follow the **SPEC** then you might find meta-relationships are just the thing to make your resource objects more natural to work with. Similarly to Meta-Attributes, Meta-Relationships allow you to represent non-compliant relationships as computed relationship properties. In the following example, a relationship is created from some attributes on the JSON model. From 07eed9c2c3edb4634cc2f9cdf1690e865c9d2ec2 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Sat, 16 May 2020 21:12:11 -0700 Subject: [PATCH 145/235] Add some convenience initializers for the most basic success and error documents --- Sources/JSONAPI/Document/Document.swift | 34 ++++++++++++++ .../JSONAPITests/Document/DocumentTests.swift | 44 +++++++++++++++++++ 2 files changed, 78 insertions(+) diff --git a/Sources/JSONAPI/Document/Document.swift b/Sources/JSONAPI/Document/Document.swift index 4e1fce2..e6713ff 100644 --- a/Sources/JSONAPI/Document/Document.swift +++ b/Sources/JSONAPI/Document/Document.swift @@ -655,3 +655,37 @@ extension Document.SuccessDocument where IncludeType: _Poly1 { } } } + +extension Document where MetaType == NoMetadata, LinksType == NoLinks, IncludeType == NoIncludes, APIDescription == NoAPIDescription { + public init(body: PrimaryResourceBody) { + self.init( + apiDescription: .none, + body: body, + includes: .none, + meta: .none, + links: .none + ) + } + + public init(errors: [Error]) { + self.init(apiDescription: .none, errors: errors) + } +} + +extension Document.SuccessDocument where Document.MetaType == NoMetadata, Document.LinksType == NoLinks, Document.IncludeType == NoIncludes, Document.APIDescription == NoAPIDescription { + public init(body: PrimaryResourceBody) { + self.init( + apiDescription: .none, + body: body, + includes: .none, + meta: .none, + links: .none + ) + } +} + +extension Document.ErrorDocument where Document.MetaType == NoMetadata, Document.LinksType == NoLinks, Document.IncludeType == NoIncludes, Document.APIDescription == NoAPIDescription { + public init(errors: [Error]) { + self.init(apiDescription: .none, errors: errors) + } +} diff --git a/Tests/JSONAPITests/Document/DocumentTests.swift b/Tests/JSONAPITests/Document/DocumentTests.swift index 3349949..081edc9 100644 --- a/Tests/JSONAPITests/Document/DocumentTests.swift +++ b/Tests/JSONAPITests/Document/DocumentTests.swift @@ -39,6 +39,28 @@ class DocumentTests: XCTestCase { links: .none )) + test(JSONAPI.Document< + NoResourceBody, + NoMetadata, + NoLinks, + NoIncludes, + NoAPIDescription, + UnknownJSONAPIError + >( + body: .none + )) + + test(JSONAPI.Document< + NoResourceBody, + NoMetadata, + NoLinks, + NoIncludes, + NoAPIDescription, + UnknownJSONAPIError + >( + errors: [] + )) + // Document.SuccessDocument test(JSONAPI.Document< NoResourceBody, @@ -55,6 +77,17 @@ class DocumentTests: XCTestCase { links: .none )) + test(JSONAPI.Document< + NoResourceBody, + NoMetadata, + NoLinks, + NoIncludes, + NoAPIDescription, + UnknownJSONAPIError + >.SuccessDocument( + body: .none + )) + // Document.ErrorDocument test(JSONAPI.Document< NoResourceBody, @@ -67,6 +100,17 @@ class DocumentTests: XCTestCase { apiDescription: .none, errors: [] )) + + test(JSONAPI.Document< + NoResourceBody, + NoMetadata, + NoLinks, + NoIncludes, + NoAPIDescription, + UnknownJSONAPIError + >.ErrorDocument( + errors: [] + )) } func test_singleDocumentNull() { From def323ca9d66fd4a0584563fa39c3c78e65d7d9a Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Mon, 18 May 2020 19:19:45 -0700 Subject: [PATCH 146/235] Add abstraction points around single and many resource bodies that expose values. --- Sources/JSONAPI/Document/ResourceBody.swift | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/Sources/JSONAPI/Document/ResourceBody.swift b/Sources/JSONAPI/Document/ResourceBody.swift index 5a7c96f..01be6d3 100644 --- a/Sources/JSONAPI/Document/ResourceBody.swift +++ b/Sources/JSONAPI/Document/ResourceBody.swift @@ -48,10 +48,14 @@ public func +(_ left: R, right: R) -> R { return left.appending(right) } +public protocol SingleResourceBodyProtocol: EncodableResourceBody { + var value: PrimaryResource { get } +} + /// A type allowing for a document body containing 1 primary resource. /// If the `Entity` specialization is an `Optional` type, the body can contain /// 0 or 1 primary resources. -public struct SingleResourceBody: EncodableResourceBody { +public struct SingleResourceBody: SingleResourceBodyProtocol { public let value: PrimaryResource public init(resourceObject: PrimaryResource) { @@ -59,8 +63,12 @@ public struct SingleResourceBody: EncodableResourceBody, ResourceBodyAppendable { +public struct ManyResourceBody: ManyResourceBodyProtocol, ResourceBodyAppendable { public let values: [PrimaryResource] public init(resourceObjects: [PrimaryResource]) { From accf8ad96900e80c38d2e61df052a8b698579ae2 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Wed, 20 May 2020 20:59:13 -0700 Subject: [PATCH 147/235] Update README.md --- README.md | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 39bf232..099e62b 100644 --- a/README.md +++ b/README.md @@ -355,14 +355,19 @@ func test_articleResponse() { } ``` -# JSONAPI+Arbitrary -The `JSONAPI+Arbitrary` library provides `SwiftCheck` `Arbitrary` conformance for many of the `JSONAPI` types. +# JSONAPI-Arbitrary +The `JSONAPI-Arbitrary` library provides `SwiftCheck` `Arbitrary` conformance for many of the `JSONAPI` types. See https://github.com/mattpolzin/JSONAPI-Arbitrary for more information. -# JSONAPI+OpenAPI -The `JSONAPI+OpenAPI` library generates OpenAPI compliant JSON Schema for models built with the `JSONAPI` library. If your Swift code is your preferred source of truth for API information, this is an easy way to document the response schemas of your API. +# JSONAPI-OpenAPI +The `JSONAPI-OpenAPI` library generates OpenAPI compliant JSON Schema for models built with the `JSONAPI` library. If your Swift code is your preferred source of truth for API information, this is an easy way to document the response schemas of your API. -`JSONAPI+OpenAPI` also has experimental support for generating `JSONAPI` Swift code from Open API documentation (this currently lives on the `feature/gen-swift` branch). +`JSONAPI-OpenAPI` also has experimental support for generating `JSONAPI` Swift code from Open API documentation (this currently lives on the `feature/gen-swift` branch). See https://github.com/mattpolzin/JSONAPI-OpenAPI for more information. + +# JSONAPI-ResourceStorage +The `JSONAPI-ResourceStorage` package has two _very_ early stage modules supporting storage and retrieval of `JSONAPI.ResourceObjects`. Please consider these modules to be more of examples of two directions you could head in than anything else. + +https://github.com/mattpolzin/JSONAPI-ResourceStorage From 68b41bb99f2d8344ed4296af7090454a64bfa0e4 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Wed, 20 May 2020 21:00:09 -0700 Subject: [PATCH 148/235] Update README.md --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 099e62b..7015df3 100644 --- a/README.md +++ b/README.md @@ -41,8 +41,9 @@ This library works well when used by both the server responsible for serializati - [Literal Expressibility](#literal-expressibility) - [Resource Object `check()`](#resource-object-check) - [Comparisons](#comparisons) -- [JSONAPI+Arbitrary](#jsonapiarbitrary) -- [JSONAPI+OpenAPI](#jsonapiopenapi) +- [JSONAPI-Arbitrary](#jsonapiarbitrary) +- [JSONAPI-OpenAPI](#jsonapiopenapi) +- [JSONAPI-ResourceStorage](#jsonapiresourcestorage) ## Primary Goals From 1b5ed1e0a0c03d49f28b0e01bb22a101d196ae12 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Wed, 20 May 2020 21:00:50 -0700 Subject: [PATCH 149/235] Update README.md --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 7015df3..5f1679d 100644 --- a/README.md +++ b/README.md @@ -41,9 +41,9 @@ This library works well when used by both the server responsible for serializati - [Literal Expressibility](#literal-expressibility) - [Resource Object `check()`](#resource-object-check) - [Comparisons](#comparisons) -- [JSONAPI-Arbitrary](#jsonapiarbitrary) -- [JSONAPI-OpenAPI](#jsonapiopenapi) -- [JSONAPI-ResourceStorage](#jsonapiresourcestorage) +- [JSONAPI-Arbitrary](#jsonapi-arbitrary) +- [JSONAPI-OpenAPI](#jsonapi-openapi) +- [JSONAPI-ResourceStorage](#jsonapi-resourcestorage) ## Primary Goals From f67744ec711cef881ba410c647b0e3a9804de1f9 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Tue, 26 May 2020 12:50:33 -0700 Subject: [PATCH 150/235] Add CompoundResource. Add SucceedableJSONAPIDocument and FailableJSONAPIDocument protocols. Make ResourceObject hashable when its Id is hashable. Make Id's Hashable conformance take its type into consideration since the Id type's primary motivation is to retain differentiation based on the type of thing being identified. --- Package.resolved | 4 +- .../JSONAPI/Document/CompoundResource.swift | 76 ++++ Sources/JSONAPI/Document/Document.swift | 69 +++- Sources/JSONAPI/Document/ResourceBody.swift | 4 + Sources/JSONAPI/Resource/Id.swift | 9 +- .../Resource Object/ResourceObject.swift | 12 + .../DocumentCompoundResourceTests.swift | 354 ++++++++++++++++++ .../ResourceObject+HashableTests.swift | 119 ++++++ 8 files changed, 630 insertions(+), 17 deletions(-) create mode 100644 Sources/JSONAPI/Document/CompoundResource.swift create mode 100644 Tests/JSONAPITests/Document/DocumentCompoundResourceTests.swift create mode 100644 Tests/JSONAPITests/ResourceObject/ResourceObject+HashableTests.swift diff --git a/Package.resolved b/Package.resolved index 7dfff6d..2900a5a 100644 --- a/Package.resolved +++ b/Package.resolved @@ -6,8 +6,8 @@ "repositoryURL": "https://github.com/mattpolzin/Poly", "state": { "branch": null, - "revision": "9031b8497e025cbcfae196e59eecad5ba988e267", - "version": "2.4.0" + "revision": "36ba3f624bffa34f5f9b9c7648eab3cfdcab4748", + "version": "2.5.0" } } ] diff --git a/Sources/JSONAPI/Document/CompoundResource.swift b/Sources/JSONAPI/Document/CompoundResource.swift new file mode 100644 index 0000000..d4116be --- /dev/null +++ b/Sources/JSONAPI/Document/CompoundResource.swift @@ -0,0 +1,76 @@ +// +// CompoundResource.swift +// +// +// Created by Mathew Polzin on 5/25/20. +// + +/// A Resource Object and any relevant related resources. This object +/// is helpful in the context of constructing a Document. +/// +/// You can resolve a primary resource and all of the intended includes +/// for that resource and pass them around as a `CompoundResource` +/// prior to constructing a Document. +/// +/// Among other things, using this abstraction means you do not need to +/// specialized for a single or batch document at the same time as you are +/// resolving (i.e. materializing or decoding) one or more resources and its +/// relatives. +public struct CompoundResource: Equatable { + public let primary: JSONAPIModel + public let relatives: [JSONAPIIncludeType] + + public init(primary: JSONAPIModel, relatives: [JSONAPIIncludeType]) { + self.primary = primary + self.relatives = relatives + } +} + +extension EncodableJSONAPIDocument where PrimaryResourceBody: EncodableResourceBody, PrimaryResourceBody.PrimaryResource: ResourceObjectType { + public typealias CompoundResource = JSONAPI.CompoundResource +} + +extension SucceedableJSONAPIDocument where PrimaryResourceBody: SingleResourceBodyProtocol, PrimaryResourceBody.PrimaryResource: ResourceObjectType { + + public init( + apiDescription: APIDescription, + resource: CompoundResource, + meta: MetaType, + links: LinksType + ) { + self.init( + apiDescription: apiDescription, + body: .init(resourceObject: resource.primary), + includes: .init(values: resource.relatives), + meta: meta, + links: links + ) + } +} + +extension SucceedableJSONAPIDocument where PrimaryResourceBody: ManyResourceBodyProtocol, PrimaryResourceBody.PrimaryResource: ResourceObjectType, IncludeType: Hashable { + + public init( + apiDescription: APIDescription, + resources: [CompoundResource], + meta: MetaType, + links: LinksType + ) { + var included = Set() + let includes = resources.reduce(into: [IncludeType]()) { (result, next) in + for include in next.relatives { + if !included.contains(include.hashValue) { + included.insert(include.hashValue) + result.append(include) + } + } + } + self.init( + apiDescription: apiDescription, + body: .init(resourceObjects: resources.map(\.primary)), + includes: .init(values: Array(includes)), + meta: meta, + links: links + ) + } +} diff --git a/Sources/JSONAPI/Document/Document.swift b/Sources/JSONAPI/Document/Document.swift index e6713ff..da0a37f 100644 --- a/Sources/JSONAPI/Document/Document.swift +++ b/Sources/JSONAPI/Document/Document.swift @@ -78,7 +78,7 @@ public protocol DocumentBody: DocumentBodyContext { } /// An `EncodableJSONAPIDocument` supports encoding but not decoding. -/// It is actually more restrictive than `JSONAPIDocument` which supports both +/// It is more restrictive than `CodableJSONAPIDocument` which supports both /// encoding and decoding. public protocol EncodableJSONAPIDocument: Equatable, Encodable, DocumentBodyContext { associatedtype APIDescription: APIDescriptionType @@ -103,6 +103,38 @@ public protocol EncodableJSONAPIDocument: Equatable, Encodable, DocumentBodyCont var apiDescription: APIDescription { get } } +/// A Document that can be constructed as successful (i.e. not an error document). +public protocol SucceedableJSONAPIDocument: EncodableJSONAPIDocument { + /// Create a successful JSONAPI:Document. + /// + /// - Parameters: + /// - apiDescription: The description of the API (a.k.a. the "JSON:API Object"). + /// - body: The primary resource body of the JSON:API Document. Generally a single resource or a batch of resources. + /// - includes: All related resources that are included in this Document. + /// - meta: Any metadata associated with the Document. + /// - links: Any links associated with the Document. + /// + init( + apiDescription: APIDescription, + body: PrimaryResourceBody, + includes: Includes, + meta: MetaType, + links: LinksType + ) +} + +/// A Document that can be constructed as failed (i.e. an error document with no primary +/// resource). +public protocol FailableJSONAPIDocument: EncodableJSONAPIDocument { + /// Create an error JSONAPI:Document. + init( + apiDescription: APIDescription, + errors: [Error], + meta: MetaType?, + links: LinksType? + ) +} + /// A `CodableJSONAPIDocument` supports encoding and decoding of a JSON:API /// compliant Document. public protocol CodableJSONAPIDocument: EncodableJSONAPIDocument, Decodable where PrimaryResourceBody: JSONAPI.CodableResourceBody, IncludeType: Decodable {} @@ -115,7 +147,7 @@ public protocol CodableJSONAPIDocument: EncodableJSONAPIDocument, Decodable wher /// API uses snake case, you will want to use /// a conversion such as the one offerred by the /// Foundation JSONEncoder/Decoder: `KeyDecodingStrategy` -public struct Document: EncodableJSONAPIDocument { +public struct Document: EncodableJSONAPIDocument, SucceedableJSONAPIDocument, FailableJSONAPIDocument { public typealias Include = IncludeType public typealias BodyData = Body.Data @@ -125,19 +157,23 @@ public struct Document, - meta: MetaType, - links: LinksType) { + public init( + apiDescription: APIDescription, + body: PrimaryResourceBody, + includes: Includes, + meta: MetaType, + links: LinksType + ) { self.body = .data( .init( primary: body, @@ -449,14 +485,19 @@ extension Document.Body.Data: CustomStringConvertible { extension Document { /// A Document that only supports error bodies. This is useful if you wish to pass around a /// Document type but you wish to constrain it to error values. - public struct ErrorDocument: EncodableJSONAPIDocument { + public struct ErrorDocument: EncodableJSONAPIDocument, FailableJSONAPIDocument { public typealias BodyData = Document.BodyData public var body: Document.Body { return document.body } private let document: Document - public init(apiDescription: APIDescription, errors: [Error], meta: MetaType? = nil, links: LinksType? = nil) { + public init( + apiDescription: APIDescription, + errors: [Error], + meta: MetaType? = nil, + links: LinksType? = nil + ) { document = .init(apiDescription: apiDescription, errors: errors, meta: meta, links: links) } @@ -500,7 +541,7 @@ extension Document { /// A Document that only supports success bodies. This is useful if you wish to pass around a /// Document type but you wish to constrain it to success values. - public struct SuccessDocument: EncodableJSONAPIDocument { + public struct SuccessDocument: EncodableJSONAPIDocument, SucceedableJSONAPIDocument { public typealias BodyData = Document.BodyData public typealias APIDescription = Document.APIDescription public typealias Body = Document.Body diff --git a/Sources/JSONAPI/Document/ResourceBody.swift b/Sources/JSONAPI/Document/ResourceBody.swift index 01be6d3..0986a4f 100644 --- a/Sources/JSONAPI/Document/ResourceBody.swift +++ b/Sources/JSONAPI/Document/ResourceBody.swift @@ -50,6 +50,8 @@ public func +(_ left: R, right: R) -> R { public protocol SingleResourceBodyProtocol: EncodableResourceBody { var value: PrimaryResource { get } + + init(resourceObject: PrimaryResource) } /// A type allowing for a document body containing 1 primary resource. @@ -65,6 +67,8 @@ public struct SingleResourceBody: Equa } } -extension Id: Hashable, CustomStringConvertible, AbstractId, IdType where RawType: RawIdType { +extension Id: Hashable where RawType: RawIdType { + public func hash(into hasher: inout Hasher) { + hasher.combine(ObjectIdentifier(Self.self)) + hasher.combine(rawValue) + } +} + +extension Id: CustomStringConvertible, AbstractId, IdType where RawType: RawIdType { public static func id(from rawValue: RawType) -> Id { return Id(rawValue: rawValue) } diff --git a/Sources/JSONAPI/Resource/Resource Object/ResourceObject.swift b/Sources/JSONAPI/Resource/Resource Object/ResourceObject.swift index 6c8b6f8..f5f8901 100644 --- a/Sources/JSONAPI/Resource/Resource Object/ResourceObject.swift +++ b/Sources/JSONAPI/Resource/Resource Object/ResourceObject.swift @@ -151,6 +151,18 @@ public struct ResourceObject, NoMetadata, NoLinks, NoIncludes, NoAPIDescription, UnknownJSONAPIError> + + let compoundAuthor = Document.CompoundResource(primary: author, relatives: []) + + let document = Document( + apiDescription: .none, + resource: compoundAuthor, + meta: .none, + links: .none + ) + + XCTAssertEqual(document.body.primaryResource?.value, author) + XCTAssertEqual(document.body.includes!, .none) + } + + func test_singleDocumentEmptyIncludes() { + let author = DocumentTests.Author( + attributes: .none, + relationships: .none, + meta: .none, + links: .none + ) + + let book = DocumentTests.Book( + attributes: .init(pageCount: 10), + relationships: .init( + author: .init(resourceObject: author), + series: .init(ids: []), + collection: nil + ), + meta: .none, + links: .none + ) + + typealias Document = JSONAPI.Document, NoMetadata, NoLinks, Include1, NoAPIDescription, UnknownJSONAPIError> + + let compoundBook = Document.CompoundResource(primary: book, relatives: []) + + let document = Document( + apiDescription: .none, + resource: compoundBook, + meta: .none, + links: .none + ) + + XCTAssertEqual(document.body.primaryResource?.value, book) + XCTAssertEqual(document.body.includes!, .none) + } + + func test_singleDocumentWithIncludes() { + let author = DocumentTests.Author( + attributes: .none, + relationships: .none, + meta: .none, + links: .none + ) + + let book = DocumentTests.Book( + attributes: .init(pageCount: 10), + relationships: .init( + author: .init(resourceObject: author), + series: .init(ids: []), + collection: nil + ), + meta: .none, + links: .none + ) + + typealias Document = JSONAPI.Document, NoMetadata, NoLinks, Include1, NoAPIDescription, UnknownJSONAPIError> + + let compoundBook = Document.CompoundResource( + primary: book, + relatives: [.init(author)] + ) + + let document = Document( + apiDescription: .none, + resource: compoundBook, + meta: .none, + links: .none + ) + + XCTAssertEqual(document.body.primaryResource?.value, book) + XCTAssertEqual(document.body.includes?.values, [.init(author)]) + } + + func test_batchDocumentNoIncludes() { + let author = DocumentTests.Author( + attributes: .none, + relationships: .none, + meta: .none, + links: .none + ) + + let author2 = DocumentTests.Author( + attributes: .none, + relationships: .none, + meta: .none, + links: .none + ) + + typealias Document = JSONAPI.Document, NoMetadata, NoLinks, NoIncludes, NoAPIDescription, UnknownJSONAPIError> + + let compoundAuthor = Document.CompoundResource(primary: author, relatives: []) + let compoundAuthor2 = Document.CompoundResource(primary: author2, relatives: []) + + let document = Document( + apiDescription: .none, + resources: [compoundAuthor, compoundAuthor2], + meta: .none, + links: .none + ) + + XCTAssertEqual(document.body.primaryResource?.values, [author, author2]) + XCTAssertEqual(document.body.includes!, .none) + } + + func test_batchDocumentEmptyIncludes() { + let author = DocumentTests.Author( + attributes: .none, + relationships: .none, + meta: .none, + links: .none + ) + + let book = DocumentTests.Book( + attributes: .init(pageCount: 10), + relationships: .init( + author: .init(resourceObject: author), + series: .init(ids: []), + collection: nil + ), + meta: .none, + links: .none + ) + + let book2 = DocumentTests.Book( + attributes: .init(pageCount: 10), + relationships: .init( + author: .init(resourceObject: author), + series: .init(ids: []), + collection: nil + ), + meta: .none, + links: .none + ) + + typealias Document = JSONAPI.Document, NoMetadata, NoLinks, Include1, NoAPIDescription, UnknownJSONAPIError> + + let compoundBook = Document.CompoundResource(primary: book, relatives: []) + let compoundBook2 = Document.CompoundResource(primary: book2, relatives: []) + + let document = Document( + apiDescription: .none, + resources: [compoundBook, compoundBook2], + meta: .none, + links: .none + ) + + XCTAssertEqual(document.body.primaryResource?.values, [book, book2]) + XCTAssertEqual(document.body.includes!, .none) + } + + func test_batchDocumentWithIncldues() { + let author = DocumentTests.Author( + attributes: .none, + relationships: .none, + meta: .none, + links: .none + ) + + let book = DocumentTests.Book( + attributes: .init(pageCount: 10), + relationships: .init( + author: .init(resourceObject: author), + series: .init(ids: []), + collection: nil + ), + meta: .none, + links: .none + ) + + let book2 = DocumentTests.Book( + attributes: .init(pageCount: 10), + relationships: .init( + author: .init(resourceObject: author), + series: .init(ids: []), + collection: nil + ), + meta: .none, + links: .none + ) + + typealias Document = JSONAPI.Document, NoMetadata, NoLinks, Include1, NoAPIDescription, UnknownJSONAPIError> + + let compoundBook = Document.CompoundResource( + primary: book, + relatives: [.init(author)] + ) + + let compoundBook2 = Document.CompoundResource( + primary: book2, + relatives: [] + ) + + let document = Document( + apiDescription: .none, + resources: [compoundBook, compoundBook2], + meta: .none, + links: .none + ) + + XCTAssertEqual(document.body.primaryResource?.values, [book, book2]) + XCTAssertEqual(document.body.includes?.values, [.init(author)]) + } + + func test_batchDocumentWithSameIncludeTwice() { + // should only add each unique include once + + let author = DocumentTests.Author( + attributes: .none, + relationships: .none, + meta: .none, + links: .none + ) + + let book = DocumentTests.Book( + attributes: .init(pageCount: 10), + relationships: .init( + author: .init(resourceObject: author), + series: .init(ids: []), + collection: nil + ), + meta: .none, + links: .none + ) + + let book2 = DocumentTests.Book( + attributes: .init(pageCount: 10), + relationships: .init( + author: .init(resourceObject: author), + series: .init(ids: []), + collection: nil + ), + meta: .none, + links: .none + ) + + typealias Document = JSONAPI.Document, NoMetadata, NoLinks, Include1, NoAPIDescription, UnknownJSONAPIError> + + let compoundBook = Document.CompoundResource( + primary: book, + relatives: [.init(author)] + ) + + // the key in this test case is that both compound resources + // contain the same included relative. + let compoundBook2 = Document.CompoundResource( + primary: book2, + relatives: [.init(author)] + ) + + let document = Document( + apiDescription: .none, + resources: [compoundBook, compoundBook2], + meta: .none, + links: .none + ) + + XCTAssertEqual(document.body.primaryResource?.values, [book, book2]) + XCTAssertEqual(document.body.includes?.values, [.init(author)]) + } + + func test_batchDocumentWithTwoDifferentIncludes() { + let author = DocumentTests.Author( + attributes: .none, + relationships: .none, + meta: .none, + links: .none + ) + + let author2 = DocumentTests.Author( + attributes: .none, + relationships: .none, + meta: .none, + links: .none + ) + + let book = DocumentTests.Book( + attributes: .init(pageCount: 10), + relationships: .init( + author: .init(resourceObject: author), + series: .init(ids: []), + collection: nil + ), + meta: .none, + links: .none + ) + + let book2 = DocumentTests.Book( + attributes: .init(pageCount: 10), + relationships: .init( + author: .init(resourceObject: author2), + series: .init(ids: []), + collection: nil + ), + meta: .none, + links: .none + ) + + typealias Document = JSONAPI.Document, NoMetadata, NoLinks, Include1, NoAPIDescription, UnknownJSONAPIError> + + let compoundBook = Document.CompoundResource( + primary: book, + relatives: [.init(author)] + ) + + let compoundBook2 = Document.CompoundResource( + primary: book2, + relatives: [.init(author2)] + ) + + let document = Document( + apiDescription: .none, + resources: [compoundBook, compoundBook2], + meta: .none, + links: .none + ) + + XCTAssertEqual(document.body.primaryResource?.values, [book, book2]) + XCTAssertEqual(document.body.includes?.values, [.init(author), .init(author2)]) + } +} diff --git a/Tests/JSONAPITests/ResourceObject/ResourceObject+HashableTests.swift b/Tests/JSONAPITests/ResourceObject/ResourceObject+HashableTests.swift new file mode 100644 index 0000000..7df88d3 --- /dev/null +++ b/Tests/JSONAPITests/ResourceObject/ResourceObject+HashableTests.swift @@ -0,0 +1,119 @@ +// +// ResourceObject+HashableTests.swift +// +// +// Created by Mathew Polzin on 5/26/20. +// + +import XCTest +import JSONAPI +import JSONAPITesting + +class ResourceObjectHashableTests: XCTestCase { + + func test_hashable_sameProeprties() { + let t1 = ResourceObject( + id: "hello", + attributes: .init(floater: 1234), + relationships: .none, + meta: .none, + links: .none + ) + + let t2 = ResourceObject( + id: "hello", + attributes: .init(floater: 1234), + relationships: .none, + meta: .none, + links: .none + ) + + let t3 = ResourceObject( + id: "world", + attributes: .init(floater: 1234), + relationships: .none, + meta: .none, + links: .none + ) + + XCTAssertEqual(t1, t2) + XCTAssertEqual(t1.hashValue, t2.hashValue) + + XCTAssertEqual(t1.attributes, t3.attributes) + XCTAssertEqual(t1.relationships, t3.relationships) + XCTAssertEqual(t1.meta, t3.meta) + XCTAssertEqual(t1.links, t3.links) + XCTAssertNotEqual(t1, t3) + XCTAssertNotEqual(t1.hashValue, t3.hashValue) + } + + func test_hashable_differentProeprties() { + let t1 = ResourceObject( + id: "hello", + attributes: .init(floater: 1234), + relationships: .none, + meta: .none, + links: .none + ) + + let t2 = ResourceObject( + id: "hello", + attributes: .init(floater: 11111), + relationships: .none, + meta: .none, + links: .none + ) + + let t3 = ResourceObject( + id: "world", + attributes: .init(floater: 1111), + relationships: .none, + meta: .none, + links: .none + ) + + XCTAssertNotEqual(t1, t2) + XCTAssertEqual(t1.hashValue, t2.hashValue) + + XCTAssertNotEqual(t1.attributes, t3.attributes) + XCTAssertEqual(t1.relationships, t3.relationships) + XCTAssertEqual(t1.meta, t3.meta) + XCTAssertEqual(t1.links, t3.links) + XCTAssertNotEqual(t1, t3) + XCTAssertNotEqual(t1.hashValue, t3.hashValue) + } + + func test_hashable_differentTypes() { + let t1 = ResourceObject( + id: "hello", + attributes: .none, + relationships: .none, + meta: .none, + links: .none + ) + + let t2 = ResourceObject( + id: "hello", + attributes: .init(floater: 1234), + relationships: .none, + meta: .none, + links: .none + ) + + let t3 = ResourceObject( + id: "world", + attributes: .init(floater: 1234), + relationships: .none, + meta: .none, + links: .none + ) + + XCTAssertNotEqual(t1.hashValue, t2.hashValue) + + XCTAssertNotEqual(t1.hashValue, t3.hashValue) + } +} + +extension ResourceObjectHashableTests { + +} From 38069b3545af57db1c2041b270c20de0adfaf39d Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Tue, 26 May 2020 12:55:36 -0700 Subject: [PATCH 151/235] update GitHub Actions workflow to use a test matrix. --- .github/workflows/tests.yml | 43 ++++++++++++++++++++++++++----------- 1 file changed, 31 insertions(+), 12 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 6bac9f8..ea0bdd2 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -1,19 +1,38 @@ name: Tests -on: [push] +on: + pull_request: + push: + branches: + - master jobs: - xenial_5_2: - container: - image: swift:5.2-xenial + linux: runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + image: + - swift:5.2-xenial + - swift:5.2-bionic + - swiftlang/swift:nightly-master-focal + - swiftlang/swift:nightly-master-centos8 + - swiftlang/swift:nightly-master-amazonlinux2 + container: ${{ matrix.image }} steps: - - uses: actions/checkout@v1 - - run: swift test --enable-test-discovery - bionic_5_2: - container: - image: swift:5.2-bionic - runs-on: ubuntu-latest + - name: Install dependencies if needed + run: ${{ matrix.depscmd }} + - name: Checkout code + uses: actions/checkout@v2 + - name: Run tests + run: swift test --enable-test-discovery + osx: + runs-on: macOS-latest steps: - - uses: actions/checkout@v1 - - run: swift test --enable-test-discovery + - name: Select latest available Xcode + uses: maxim-lobanov/setup-xcode@1.0 + with: { 'xcode-version': 'latest' } + - name: Checkout code + uses: actions/checkout@v2 + - name: Run tests + run: swift test --enable-test-discovery From 531850443a7492a6270fb3c6ff0dc7e771c82d6b Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Tue, 26 May 2020 13:07:11 -0700 Subject: [PATCH 152/235] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5f1679d..d1b28a7 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # JSONAPI -[![MIT license](http://img.shields.io/badge/license-MIT-lightgrey.svg)](http://opensource.org/licenses/MIT) [![Swift 5.1](http://img.shields.io/badge/Swift-5.1-blue.svg)](https://swift.org) [![Build Status](https://app.bitrise.io/app/c8295b9589aa401e/status.svg?token=vzcyqWD5bQ4xqQfZsaVzNw&branch=master)](https://app.bitrise.io/app/c8295b9589aa401e) +[![MIT license](http://img.shields.io/badge/license-MIT-lightgrey.svg)](http://opensource.org/licenses/MIT) [![Swift 5.2](http://img.shields.io/badge/Swift-5.2-blue.svg)](https://swift.org) [![Build Status](https://app.bitrise.io/app/c8295b9589aa401e/status.svg?token=vzcyqWD5bQ4xqQfZsaVzNw&branch=master)](https://app.bitrise.io/app/c8295b9589aa401e) A Swift package for encoding to- and decoding from **JSON API** compliant requests and responses. From a57c6b74ad2f9c3539cb5527d95cbb2fd680d463 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Tue, 26 May 2020 17:18:40 -0700 Subject: [PATCH 153/235] Add filtering of relatives to CompoundResource --- .../JSONAPI/Document/CompoundResource.swift | 47 +++++- .../DocumentCompoundResourceTests.swift | 148 ++++++++++++++++++ 2 files changed, 194 insertions(+), 1 deletion(-) diff --git a/Sources/JSONAPI/Document/CompoundResource.swift b/Sources/JSONAPI/Document/CompoundResource.swift index d4116be..9ad84fe 100644 --- a/Sources/JSONAPI/Document/CompoundResource.swift +++ b/Sources/JSONAPI/Document/CompoundResource.swift @@ -5,6 +5,16 @@ // Created by Mathew Polzin on 5/25/20. // +public protocol CompoundResourceProtocol { + associatedtype JSONAPIModel: JSONAPI.ResourceObjectType + associatedtype JSONAPIIncludeType: JSONAPI.Include + + var primary: JSONAPIModel { get } + var relatives: [JSONAPIIncludeType] { get } + + func filteringRelatives(by filter: (JSONAPIIncludeType) -> Bool) -> Self +} + /// A Resource Object and any relevant related resources. This object /// is helpful in the context of constructing a Document. /// @@ -16,7 +26,11 @@ /// specialized for a single or batch document at the same time as you are /// resolving (i.e. materializing or decoding) one or more resources and its /// relatives. -public struct CompoundResource: Equatable { +/// +/// - Important: This type is not intended to guarantee +/// that all `relationships` of the primary resource are available +/// in the `relatives` array. +public struct CompoundResource: Equatable, CompoundResourceProtocol { public let primary: JSONAPIModel public let relatives: [JSONAPIIncludeType] @@ -24,6 +38,37 @@ public struct CompoundResource Bool) -> CompoundResource { + return .init( + primary: primary, + relatives: relatives.filter(filter) + ) + } +} + +extension Sequence where Element: CompoundResourceProtocol { + /// Create new Compound Resources having + /// filtered the relatives by the given closure (which + /// must return `true` for any relative that should + /// remain part of the `CompoundObject`). + /// + /// This does not remove relatives from the primary + /// resource's `relationships`, it just filters out + /// which relatives have complete resource objects + /// in the newly created `CompoundResource`. + public func filteringRelatives(by filter: (Element.JSONAPIIncludeType) -> Bool) -> [Element] { + return map { $0.filteringRelatives(by: filter) } + } } extension EncodableJSONAPIDocument where PrimaryResourceBody: EncodableResourceBody, PrimaryResourceBody.PrimaryResource: ResourceObjectType { diff --git a/Tests/JSONAPITests/Document/DocumentCompoundResourceTests.swift b/Tests/JSONAPITests/Document/DocumentCompoundResourceTests.swift index 6d5fda1..f42bff3 100644 --- a/Tests/JSONAPITests/Document/DocumentCompoundResourceTests.swift +++ b/Tests/JSONAPITests/Document/DocumentCompoundResourceTests.swift @@ -105,6 +105,94 @@ final class DocumentCompoundResourceTests: XCTestCase { XCTAssertEqual(document.body.includes?.values, [.init(author)]) } + func test_singleDocumentWithFilteredIncludes() { + let author = DocumentTests.Author( + attributes: .none, + relationships: .none, + meta: .none, + links: .none + ) + + let ids = [ + DocumentTests.Book.Id(), + DocumentTests.Book.Id(), + DocumentTests.Book.Id() + ] + + let book = DocumentTests.Book( + id: ids[0], + attributes: .init(pageCount: 10), + relationships: .init( + author: .init(resourceObject: author), + series: .init(ids: [ids[1], ids[2]]), + collection: nil + ), + meta: .none, + links: .none + ) + + let book2 = DocumentTests.Book( + id: ids[1], + attributes: .init(pageCount: 10), + relationships: .init( + author: .init(resourceObject: author), + series: .init(ids: [ids[0], ids[2]]), + collection: nil + ), + meta: .none, + links: .none + ) + + let book3 = DocumentTests.Book( + id: ids[2], + attributes: .init(pageCount: 10), + relationships: .init( + author: .init(resourceObject: author), + series: .init(ids: [ids[0], ids[1]]), + collection: nil + ), + meta: .none, + links: .none + ) + + typealias Document = JSONAPI.Document, NoMetadata, NoLinks, Include2, NoAPIDescription, UnknownJSONAPIError> + + let compoundBook = Document.CompoundResource( + primary: book, + relatives: [.init(author), .init(book2), .init(book3)] + ) + + let document = Document( + apiDescription: .none, + resource: compoundBook.filteringRelatives { $0.value is DocumentTests.Author }, + meta: .none, + links: .none + ) + + XCTAssertEqual(document.body.primaryResource?.value, book) + XCTAssertEqual(document.body.includes?.values, [.init(author)]) + + let document2 = Document( + apiDescription: .none, + resource: compoundBook.filteringRelatives { $0.value is DocumentTests.Book }, + meta: .none, + links: .none + ) + + XCTAssertEqual(document2.body.primaryResource?.value, book) + XCTAssertEqual(document2.body.includes?.values, [.init(book2), .init(book3)]) + + let document3 = Document( + apiDescription: .none, + resource: compoundBook, + meta: .none, + links: .none + ) + + XCTAssertEqual(document3.body.primaryResource?.value, book) + XCTAssertEqual(document3.body.includes?.values, [.init(author), .init(book2), .init(book3)]) + } + func test_batchDocumentNoIncludes() { let author = DocumentTests.Author( attributes: .none, @@ -351,4 +439,64 @@ final class DocumentCompoundResourceTests: XCTestCase { XCTAssertEqual(document.body.primaryResource?.values, [book, book2]) XCTAssertEqual(document.body.includes?.values, [.init(author), .init(author2)]) } + + func test_batchDocumentWithFilteredIncludes() { + let author = DocumentTests.Author( + attributes: .none, + relationships: .none, + meta: .none, + links: .none + ) + + let author2 = DocumentTests.Author( + attributes: .none, + relationships: .none, + meta: .none, + links: .none + ) + + let book = DocumentTests.Book( + attributes: .init(pageCount: 10), + relationships: .init( + author: .init(resourceObject: author), + series: .init(ids: []), + collection: nil + ), + meta: .none, + links: .none + ) + + let book2 = DocumentTests.Book( + attributes: .init(pageCount: 10), + relationships: .init( + author: .init(resourceObject: author2), + series: .init(ids: []), + collection: nil + ), + meta: .none, + links: .none + ) + + typealias Document = JSONAPI.Document, NoMetadata, NoLinks, Include1, NoAPIDescription, UnknownJSONAPIError> + + let compoundBook = Document.CompoundResource( + primary: book, + relatives: [.init(author)] + ) + + let compoundBook2 = Document.CompoundResource( + primary: book2, + relatives: [.init(author2)] + ) + + let document = Document( + apiDescription: .none, + resources: [compoundBook, compoundBook2].filteringRelatives { $0.a?.id == author.id }, + meta: .none, + links: .none + ) + + XCTAssertEqual(document.body.primaryResource?.values, [book, book2]) + XCTAssertEqual(document.body.includes?.values, [.init(author)]) + } } From 3c8bdcaacf5f948760b55fe359a7d18b6d6b63c4 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Fri, 29 May 2020 14:20:44 -0700 Subject: [PATCH 154/235] Rename Identifiable to JSONAPIIdentifiable and add Swift Identifiable conformance to the same types --- Sources/JSONAPI/Resource/Relationship.swift | 54 +++++++++---------- .../Resource Object/ResourceObject.swift | 19 ++++--- .../JSONAPITesting/Relationship+Literal.swift | 24 ++++----- .../NonJSONAPIRelatableTests.swift | 2 +- .../ResourceObject/ResourceObjectTests.swift | 10 ++-- .../JSONAPITests/SwiftIdentifiableTests.swift | 37 +++++++++++++ 6 files changed, 93 insertions(+), 53 deletions(-) create mode 100644 Tests/JSONAPITests/SwiftIdentifiableTests.swift diff --git a/Sources/JSONAPI/Resource/Relationship.swift b/Sources/JSONAPI/Resource/Relationship.swift index c1003d8..8b44a81 100644 --- a/Sources/JSONAPI/Resource/Relationship.swift +++ b/Sources/JSONAPI/Resource/Relationship.swift @@ -35,14 +35,14 @@ public struct MetaRelationship /// a JSON API "Resource Linkage." /// See https://jsonapi.org/format/#document-resource-object-linkage /// A convenient typealias might make your code much more legible: `One` -public struct ToOneRelationship: RelationshipType, Equatable { +public struct ToOneRelationship: RelationshipType, Equatable { - public let id: Identifiable.Identifier + public let id: Identifiable.ID public let meta: MetaType public let links: LinksType - public init(id: Identifiable.Identifier, meta: MetaType, links: LinksType) { + public init(id: Identifiable.ID, meta: MetaType, links: LinksType) { self.id = id self.meta = meta self.links = links @@ -50,31 +50,31 @@ public struct ToOneRelationship(resourceObject: T, meta: MetaType, links: LinksType) where T.Id == Identifiable.Identifier { + public init(resourceObject: T, meta: MetaType, links: LinksType) where T.Id == Identifiable.ID { self.init(id: resourceObject.id, meta: meta, links: links) } } extension ToOneRelationship where MetaType == NoMetadata, LinksType == NoLinks { - public init(resourceObject: T) where T.Id == Identifiable.Identifier { + public init(resourceObject: T) where T.Id == Identifiable.ID { self.init(id: resourceObject.id, meta: .none, links: .none) } } extension ToOneRelationship where Identifiable: OptionalRelatable { - public init(resourceObject: T?, meta: MetaType, links: LinksType) where T.Id == Identifiable.Wrapped.Identifier { + public init(resourceObject: T?, meta: MetaType, links: LinksType) where T.Id == Identifiable.Wrapped.ID { self.init(id: resourceObject?.id, meta: meta, links: links) } } extension ToOneRelationship where Identifiable: OptionalRelatable, MetaType == NoMetadata, LinksType == NoLinks { - public init(resourceObject: T?) where T.Id == Identifiable.Wrapped.Identifier { + public init(resourceObject: T?) where T.Id == Identifiable.Wrapped.ID { self.init(id: resourceObject?.id, meta: .none, links: .none) } } @@ -85,24 +85,24 @@ extension ToOneRelationship where Identifiable: OptionalRelatable, MetaType == N /// A convenient typealias might make your code much more legible: `Many` public struct ToManyRelationship: RelationshipType, Equatable { - public let ids: [Relatable.Identifier] + public let ids: [Relatable.ID] public let meta: MetaType public let links: LinksType - public init(ids: [Relatable.Identifier], meta: MetaType, links: LinksType) { + public init(ids: [Relatable.ID], meta: MetaType, links: LinksType) { self.ids = ids self.meta = meta self.links = links } - public init(pointers: [ToOneRelationship], meta: MetaType, links: LinksType) where T.Identifier == Relatable.Identifier { + public init(pointers: [ToOneRelationship], meta: MetaType, links: LinksType) where T.ID == Relatable.ID { ids = pointers.map(\.id) self.meta = meta self.links = links } - public init(resourceObjects: [T], meta: MetaType, links: LinksType) where T.Id == Relatable.Identifier { + public init(resourceObjects: [T], meta: MetaType, links: LinksType) where T.Id == Relatable.ID { self.init(ids: resourceObjects.map(\.id), meta: meta, links: links) } @@ -117,11 +117,11 @@ public struct ToManyRelationship(pointers: [ToOneRelationship]) where T.Identifier == Relatable.Identifier { + public init(pointers: [ToOneRelationship]) where T.ID == Relatable.ID { self.init(pointers: pointers, meta: .none, links: .none) } @@ -129,28 +129,28 @@ extension ToManyRelationship where MetaType == NoMetadata, LinksType == NoLinks return .none(withMeta: .none, links: .none) } - public init(resourceObjects: [T]) where T.Id == Relatable.Identifier { + public init(resourceObjects: [T]) where T.Id == Relatable.ID { self.init(resourceObjects: resourceObjects, meta: .none, links: .none) } } -public protocol Identifiable: JSONTyped { - associatedtype Identifier: Equatable +public protocol JSONAPIIdentifiable: JSONTyped { + associatedtype ID: Equatable } /// The Relatable protocol describes anything that /// has an IdType Identifier -public protocol Relatable: Identifiable where Identifier: JSONAPI.IdType { +public protocol Relatable: JSONAPIIdentifiable where ID: JSONAPI.IdType { } /// OptionalRelatable just describes an Optional /// with a Reltable Wrapped type. -public protocol OptionalRelatable: Identifiable where Identifier == Wrapped.Identifier? { +public protocol OptionalRelatable: JSONAPIIdentifiable where ID == Wrapped.ID? { associatedtype Wrapped: JSONAPI.Relatable } -extension Optional: Identifiable, OptionalRelatable, JSONTyped where Wrapped: JSONAPI.Relatable { - public typealias Identifier = Wrapped.Identifier? +extension Optional: JSONAPIIdentifiable, OptionalRelatable, JSONTyped where Wrapped: JSONAPI.Relatable { + public typealias ID = Wrapped.ID? public static var jsonType: String { return Wrapped.jsonType } } @@ -196,7 +196,7 @@ extension MetaRelationship: Codable { } } -extension ToOneRelationship: Codable where Identifiable.Identifier: OptionalId { +extension ToOneRelationship: Codable where Identifiable.ID: OptionalId { public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: ResourceLinkageCodingKeys.self) @@ -219,7 +219,7 @@ extension ToOneRelationship: Codable where Identifiable.Identifier: OptionalId { // type at which point we can store nil in `id`. let anyNil: Any? = nil if try container.decodeNil(forKey: .data) { - guard let val = anyNil as? Identifiable.Identifier else { + guard let val = anyNil as? Identifiable.ID else { throw DecodingError.valueNotFound( Self.self, DecodingError.Context( @@ -256,7 +256,7 @@ extension ToOneRelationship: Codable where Identifiable.Identifier: OptionalId { ) } - id = Identifiable.Identifier(rawValue: try identifier.decode(Identifiable.Identifier.RawType.self, forKey: .id)) + id = Identifiable.ID(rawValue: try identifier.decode(Identifiable.ID.RawType.self, forKey: .id)) } public func encode(to encoder: Encoder) throws { @@ -273,7 +273,7 @@ extension ToOneRelationship: Codable where Identifiable.Identifier: OptionalId { // If id is nil, instead of {id: , type: } we will just // encode `null` let anyNil: Any? = nil - let nilId = anyNil as? Identifiable.Identifier + let nilId = anyNil as? Identifiable.ID guard id != nilId else { try container.encodeNil(forKey: .data) return @@ -314,7 +314,7 @@ extension ToManyRelationship: Codable { path: context.codingPath) } - var newIds = [Relatable.Identifier]() + var newIds = [Relatable.ID]() while !identifiers.isAtEnd { let identifier = try identifiers.nestedContainer(keyedBy: ResourceIdentifierCodingKeys.self) @@ -324,7 +324,7 @@ extension ToManyRelationship: Codable { throw JSONAPICodingError.typeMismatch(expected: Relatable.jsonType, found: type, path: decoder.codingPath) } - newIds.append(Relatable.Identifier(rawValue: try identifier.decode(Relatable.Identifier.RawType.self, forKey: .id))) + newIds.append(Relatable.ID(rawValue: try identifier.decode(Relatable.ID.RawType.self, forKey: .id))) } ids = newIds } diff --git a/Sources/JSONAPI/Resource/Resource Object/ResourceObject.swift b/Sources/JSONAPI/Resource/Resource Object/ResourceObject.swift index f5f8901..9ac95d8 100644 --- a/Sources/JSONAPI/Resource/Resource Object/ResourceObject.swift +++ b/Sources/JSONAPI/Resource/Resource Object/ResourceObject.swift @@ -163,10 +163,13 @@ extension ResourceObject: Hashable where EntityRawIdType: RawIdType { } } -extension ResourceObject: Identifiable, IdentifiableResourceObjectType, Relatable where EntityRawIdType: JSONAPI.RawIdType { - public typealias Identifier = ResourceObject.Id +extension ResourceObject: JSONAPIIdentifiable, IdentifiableResourceObjectType, Relatable where EntityRawIdType: JSONAPI.RawIdType { + public typealias ID = ResourceObject.Id } +@available(OSX 10.15, iOS 13, tvOS 13, watchOS 6, *) +extension ResourceObject: Swift.Identifiable where EntityRawIdType: JSONAPI.RawIdType {} + extension ResourceObject: CustomStringConvertible { public var description: String { return "ResourceObject<\(ResourceObject.jsonType)>(id: \(String(describing: id)), attributes: \(String(describing: attributes)), relationships: \(String(describing: relationships)))" @@ -230,7 +233,7 @@ public extension ResourceObject where EntityRawIdType == Unidentified { /// Create a new `ResourceObject` from this one with the given Id. func identified(by id: RawIdType) -> ResourceObject { - return .init(id: ResourceObject.Identifier(rawValue: id), attributes: attributes, relationships: relationships, meta: meta, links: links) + return .init(id: ResourceObject.ID(rawValue: id), attributes: attributes, relationships: relationships, meta: meta, links: links) } } @@ -294,14 +297,14 @@ public extension ResourceObjectProxy { /// Access to an Id of a `ToOneRelationship`. /// This allows you to write `resourceObject ~> \.other` instead /// of `resourceObject.relationships.other.id`. - static func ~>(entity: Self, path: KeyPath>) -> OtherEntity.Identifier { + static func ~>(entity: Self, path: KeyPath>) -> OtherEntity.ID { return entity.relationships[keyPath: path].id } /// Access to an Id of an optional `ToOneRelationship`. /// This allows you to write `resourceObject ~> \.other` instead /// of `resourceObject.relationships.other?.id`. - static func ~>(entity: Self, path: KeyPath?>) -> OtherEntity.Identifier { + static func ~>(entity: Self, path: KeyPath?>) -> OtherEntity.ID { // Implementation Note: This signature applies to `ToOneRelationship?` // whereas the one below applies to `ToOneRelationship?` return entity.relationships[keyPath: path]?.id @@ -310,7 +313,7 @@ public extension ResourceObjectProxy { /// Access to an Id of an optional `ToOneRelationship`. /// This allows you to write `resourceObject ~> \.other` instead /// of `resourceObject.relationships.other?.id`. - static func ~>(entity: Self, path: KeyPath?>) -> OtherEntity.Identifier? { + static func ~>(entity: Self, path: KeyPath?>) -> OtherEntity.ID? { // Implementation Note: This signature applies to `ToOneRelationship?` // whereas the one above applies to `ToOneRelationship?` return entity.relationships[keyPath: path]?.id @@ -319,14 +322,14 @@ public extension ResourceObjectProxy { /// Access to all Ids of a `ToManyRelationship`. /// This allows you to write `resourceObject ~> \.others` instead /// of `resourceObject.relationships.others.ids`. - static func ~>(entity: Self, path: KeyPath>) -> [OtherEntity.Identifier] { + static func ~>(entity: Self, path: KeyPath>) -> [OtherEntity.ID] { return entity.relationships[keyPath: path].ids } /// Access to all Ids of an optional `ToManyRelationship`. /// This allows you to write `resourceObject ~> \.others` instead /// of `resourceObject.relationships.others?.ids`. - static func ~>(entity: Self, path: KeyPath?>) -> [OtherEntity.Identifier]? { + static func ~>(entity: Self, path: KeyPath?>) -> [OtherEntity.ID]? { return entity.relationships[keyPath: path]?.ids } } diff --git a/Sources/JSONAPITesting/Relationship+Literal.swift b/Sources/JSONAPITesting/Relationship+Literal.swift index 551fd88..6b00511 100644 --- a/Sources/JSONAPITesting/Relationship+Literal.swift +++ b/Sources/JSONAPITesting/Relationship+Literal.swift @@ -7,39 +7,39 @@ import JSONAPI -extension ToOneRelationship: ExpressibleByNilLiteral where Identifiable.Identifier: ExpressibleByNilLiteral, MetaType == NoMetadata, LinksType == NoLinks { +extension ToOneRelationship: ExpressibleByNilLiteral where Identifiable.ID: ExpressibleByNilLiteral, MetaType == NoMetadata, LinksType == NoLinks { public init(nilLiteral: ()) { - self.init(id: Identifiable.Identifier(nilLiteral: ())) + self.init(id: Identifiable.ID(nilLiteral: ())) } } -extension ToOneRelationship: ExpressibleByUnicodeScalarLiteral where Identifiable.Identifier: ExpressibleByUnicodeScalarLiteral, MetaType == NoMetadata, LinksType == NoLinks { - public typealias UnicodeScalarLiteralType = Identifiable.Identifier.UnicodeScalarLiteralType +extension ToOneRelationship: ExpressibleByUnicodeScalarLiteral where Identifiable.ID: ExpressibleByUnicodeScalarLiteral, MetaType == NoMetadata, LinksType == NoLinks { + public typealias UnicodeScalarLiteralType = Identifiable.ID.UnicodeScalarLiteralType public init(unicodeScalarLiteral value: UnicodeScalarLiteralType) { - self.init(id: Identifiable.Identifier(unicodeScalarLiteral: value)) + self.init(id: Identifiable.ID(unicodeScalarLiteral: value)) } } -extension ToOneRelationship: ExpressibleByExtendedGraphemeClusterLiteral where Identifiable.Identifier: ExpressibleByExtendedGraphemeClusterLiteral, MetaType == NoMetadata, LinksType == NoLinks { - public typealias ExtendedGraphemeClusterLiteralType = Identifiable.Identifier.ExtendedGraphemeClusterLiteralType +extension ToOneRelationship: ExpressibleByExtendedGraphemeClusterLiteral where Identifiable.ID: ExpressibleByExtendedGraphemeClusterLiteral, MetaType == NoMetadata, LinksType == NoLinks { + public typealias ExtendedGraphemeClusterLiteralType = Identifiable.ID.ExtendedGraphemeClusterLiteralType public init(extendedGraphemeClusterLiteral value: ExtendedGraphemeClusterLiteralType) { - self.init(id: Identifiable.Identifier(extendedGraphemeClusterLiteral: value)) + self.init(id: Identifiable.ID(extendedGraphemeClusterLiteral: value)) } } -extension ToOneRelationship: ExpressibleByStringLiteral where Identifiable.Identifier: ExpressibleByStringLiteral, MetaType == NoMetadata, LinksType == NoLinks { - public typealias StringLiteralType = Identifiable.Identifier.StringLiteralType +extension ToOneRelationship: ExpressibleByStringLiteral where Identifiable.ID: ExpressibleByStringLiteral, MetaType == NoMetadata, LinksType == NoLinks { + public typealias StringLiteralType = Identifiable.ID.StringLiteralType public init(stringLiteral value: StringLiteralType) { - self.init(id: Identifiable.Identifier(stringLiteral: value)) + self.init(id: Identifiable.ID(stringLiteral: value)) } } extension ToManyRelationship: ExpressibleByArrayLiteral where MetaType == NoMetadata, LinksType == NoLinks { - public typealias ArrayLiteralElement = Relatable.Identifier + public typealias ArrayLiteralElement = Relatable.ID public init(arrayLiteral elements: ArrayLiteralElement...) { self.init(ids: elements) diff --git a/Tests/JSONAPITests/NonJSONAPIRelatable/NonJSONAPIRelatableTests.swift b/Tests/JSONAPITests/NonJSONAPIRelatable/NonJSONAPIRelatableTests.swift index 1947c8e..30e14a8 100644 --- a/Tests/JSONAPITests/NonJSONAPIRelatable/NonJSONAPIRelatableTests.swift +++ b/Tests/JSONAPITests/NonJSONAPIRelatable/NonJSONAPIRelatableTests.swift @@ -83,7 +83,7 @@ extension NonJSONAPIRelatableTests { struct NonJSONAPIEntity: Relatable, JSONTyped { static var jsonType: String { return "other" } - typealias Identifier = NonJSONAPIEntity.Id + typealias ID = NonJSONAPIEntity.Id let id: Id diff --git a/Tests/JSONAPITests/ResourceObject/ResourceObjectTests.swift b/Tests/JSONAPITests/ResourceObject/ResourceObjectTests.swift index bdc5bae..b7b6dca 100644 --- a/Tests/JSONAPITests/ResourceObject/ResourceObjectTests.swift +++ b/Tests/JSONAPITests/ResourceObject/ResourceObjectTests.swift @@ -91,7 +91,7 @@ class ResourceObjectTests: XCTestCase { let _ = TestEntity9(id: .init(rawValue: "9"), attributes: .none, relationships: .init(meta: .init(meta: .init(x: "hello", y: 5), links: .none), optionalMeta: nil, one: entity1.pointer, nullableOne: nil, optionalOne: entity1.pointer, optionalNullableOne: nil, optionalMany: nil), meta: .none, links: .none) let _ = TestEntity9(id: .init(rawValue: "9"), attributes: .none, relationships: .init(meta: .init(meta: .init(x: "hello", y: 5), links: .none), optionalMeta: nil, one: entity1.pointer, nullableOne: nil, optionalOne: nil, optionalNullableOne: .init(resourceObject: entity1, meta: .none, links: .none), optionalMany: nil), meta: .none, links: .none) let _ = TestEntity9(id: .init(rawValue: "9"), attributes: .none, relationships: .init(meta: .init(meta: .init(x: "hello", y: 5), links: .none), optionalMeta: nil, one: entity1.pointer, nullableOne: nil, optionalOne: nil, optionalNullableOne: .init(resourceObject: entity1, meta: .none, links: .none), optionalMany: .init(resourceObjects: [], meta: .none, links: .none)), meta: .none, links: .none) - let e10id1 = TestEntity10.Identifier(rawValue: "hello") + let e10id1 = TestEntity10.ID(rawValue: "hello") let e10id2 = TestEntity10.Id(rawValue: "world") let e10id3: TestEntity10.Id = "!" let _ = TestEntity10(id: .init(rawValue: "10"), attributes: .none, relationships: .init(selfRef: .init(id: e10id1), selfRefs: .init(ids: [e10id2, e10id3])), meta: .none, links: .none) @@ -901,15 +901,15 @@ extension ResourceObjectTests { typealias Attributes = NoAttributes struct Relationships: JSONAPI.Relationships { - var metaRelationship: (TestEntityWithMetaRelationship) -> TestEntity1.Identifier { + var metaRelationship: (TestEntityWithMetaRelationship) -> TestEntity1.ID { return { entity in - return TestEntity1.Identifier(rawValue: "hello") + return TestEntity1.ID(rawValue: "hello") } } - var toManyMetaRelationship: (TestEntityWithMetaRelationship) -> [TestEntity1.Identifier] { + var toManyMetaRelationship: (TestEntityWithMetaRelationship) -> [TestEntity1.ID] { return { entity in - return [TestEntity1.Identifier.id(from: "hello")] + return [TestEntity1.ID.id(from: "hello")] } } } diff --git a/Tests/JSONAPITests/SwiftIdentifiableTests.swift b/Tests/JSONAPITests/SwiftIdentifiableTests.swift new file mode 100644 index 0000000..d5de133 --- /dev/null +++ b/Tests/JSONAPITests/SwiftIdentifiableTests.swift @@ -0,0 +1,37 @@ +// +// SwiftIdentifiableTests.swift +// +// +// Created by Mathew Polzin on 5/29/20. +// + +import JSONAPI +import XCTest + +final class SwiftIdentifiableTests: XCTestCase { + @available(OSX 10.15, iOS 13, tvOS 13, watchOS 6, *) + func test_identifiableConformance() { + let t1 = TestType(attributes: .none, relationships: .none, meta: .none, links: .none) + let t2 = TestType(attributes: .none, relationships: .none, meta: .none, links: .none) + + var hash = [AnyHashable: String]() + func storeErased(_ thing: T) { + hash[thing.id] = String(describing: thing.id) + } + + storeErased(t1) + storeErased(t2) + + XCTAssertEqual(hash[t1.id], String(describing: t1.id)) + XCTAssertEqual(hash[t2.id], String(describing: t2.id)) + } +} + +fileprivate enum TestDescription: JSONAPI.ResourceObjectDescription { + static let jsonType: String = "test" + + typealias Attributes = NoAttributes + typealias Relationships = NoRelationships +} + +fileprivate typealias TestType = ResourceObject From 754255b4bdadacc310a5602660aca7b2915ea72b Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Fri, 29 May 2020 14:50:47 -0700 Subject: [PATCH 155/235] rename ResourceObject.Id to ResourceObject.ID so there is only one typealias for a ResourceObject identifying type. --- .../Contents.swift | 4 ++-- .../Contents.swift | 10 +++++----- .../PATCHing.xcplaygroundpage/Contents.swift | 2 +- .../Pages/Usage.xcplaygroundpage/Contents.swift | 6 +++--- JSONAPI.playground/Sources/Entities.swift | 8 ++++---- README.md | 4 ++-- Sources/JSONAPI/Resource/Relationship.swift | 12 ++++++------ .../Resource Object/ResourceObject.swift | 17 ++++++++--------- .../DocumentCompoundResourceTests.swift | 6 +++--- Tests/JSONAPITests/Poly/PolyProxyTests.swift | 4 ++-- .../ResourceObject/ResourceObjectTests.swift | 4 ++-- documentation/usage.md | 12 ++++++------ 12 files changed, 44 insertions(+), 45 deletions(-) diff --git a/JSONAPI.playground/Pages/Full Client & Server Example.xcplaygroundpage/Contents.swift b/JSONAPI.playground/Pages/Full Client & Server Example.xcplaygroundpage/Contents.swift index 280a614..7a83541 100644 --- a/JSONAPI.playground/Pages/Full Client & Server Example.xcplaygroundpage/Contents.swift +++ b/JSONAPI.playground/Pages/Full Client & Server Example.xcplaygroundpage/Contents.swift @@ -30,7 +30,7 @@ typealias UnidentifiedJSONEntity = JSONA // Create relationship typealiases because we do not expect // JSON:API Relationships for this particular API to have // Metadata or Links associated with them. -typealias ToOneRelationship = JSONAPI.ToOneRelationship +typealias ToOneRelationship = JSONAPI.ToOneRelationship typealias ToManyRelationship = JSONAPI.ToManyRelationship // Create a typealias for a Document because we do not expect @@ -86,7 +86,7 @@ typealias SingleArticleDocument = Document, NoInclud func articleDocument(includeAuthor: Bool) -> Either { // Let's pretend all of this is coming from a database: - let authorId = Author.Identifier(rawValue: "1234") + let authorId = Author.ID(rawValue: "1234") let article = Article(id: .init(rawValue: "5678"), attributes: .init(title: .init(value: "JSON:API in Swift"), diff --git a/JSONAPI.playground/Pages/Full Document Verbose Generation.xcplaygroundpage/Contents.swift b/JSONAPI.playground/Pages/Full Document Verbose Generation.xcplaygroundpage/Contents.swift index 81f985b..2628c38 100644 --- a/JSONAPI.playground/Pages/Full Document Verbose Generation.xcplaygroundpage/Contents.swift +++ b/JSONAPI.playground/Pages/Full Document Verbose Generation.xcplaygroundpage/Contents.swift @@ -129,9 +129,9 @@ enum ArticleDocumentError: String, JSONAPIError, Codable { typealias SingleArticleDocument = JSONAPI.Document, DocumentMetadata, SingleArticleDocumentLinks, Include1, APIDescription, ArticleDocumentError> // MARK: - Instantiations -let authorId1 = Author.Identifier() -let authorId2 = Author.Identifier() -let authorId3 = Author.Identifier() +let authorId1 = Author.ID() +let authorId2 = Author.ID() +let authorId3 = Author.ID() let now = Date() let tomorrow = Calendar.current.date(byAdding: .day, value: 1, to: now)! @@ -155,7 +155,7 @@ let author1Links = EntityLinks(selfLink: .init(url: URL(string: "https://article meta: .init(expiry: tomorrow))) let author1 = Author(id: authorId1, attributes: .init(name: .init(value: "James Kinney")), - relationships: .init(articles: .init(ids: [article.id, Article.Identifier(), Article.Identifier()], + relationships: .init(articles: .init(ids: [article.id, Article.ID(), Article.ID()], meta: .init(pagination: .init(total: 3, limit: 50, offset: 0)), @@ -167,7 +167,7 @@ let author2Links = EntityLinks(selfLink: .init(url: URL(string: "https://article meta: .init(expiry: tomorrow))) let author2 = Author(id: authorId2, attributes: .init(name: .init(value: "James Kinney")), - relationships: .init(articles: .init(ids: [article.id, Article.Identifier()], + relationships: .init(articles: .init(ids: [article.id, Article.ID()], meta: .init(pagination: .init(total: 2, limit: 50, offset: 0)), diff --git a/JSONAPI.playground/Pages/PATCHing.xcplaygroundpage/Contents.swift b/JSONAPI.playground/Pages/PATCHing.xcplaygroundpage/Contents.swift index f916054..ded305e 100644 --- a/JSONAPI.playground/Pages/PATCHing.xcplaygroundpage/Contents.swift +++ b/JSONAPI.playground/Pages/PATCHing.xcplaygroundpage/Contents.swift @@ -108,7 +108,7 @@ print("Received dog with owner: \(dog3 ~> \.owner)") // give the dog an owner let changedDog3 = dog3.replacingRelationships { _ in - return .init(owner: .init(id: Id(rawValue: "1"))) + return .init(owner: .init(id: ID(rawValue: "1"))) } // create a document to be used as a request body for a PATCH request diff --git a/JSONAPI.playground/Pages/Usage.xcplaygroundpage/Contents.swift b/JSONAPI.playground/Pages/Usage.xcplaygroundpage/Contents.swift index 5917bef..48296f0 100644 --- a/JSONAPI.playground/Pages/Usage.xcplaygroundpage/Contents.swift +++ b/JSONAPI.playground/Pages/Usage.xcplaygroundpage/Contents.swift @@ -20,18 +20,18 @@ let singleDogData = try! JSONEncoder().encode(singleDogDocument) // MARK: - Parse a request or response body with one Dog in it let dogResponse = try! JSONDecoder().decode(SingleDogDocument.self, from: singleDogData) let dogFromData = dogResponse.body.primaryResource?.value -let dogOwner: Person.Identifier? = dogFromData.flatMap { $0 ~> \.owner } +let dogOwner: Person.ID? = dogFromData.flatMap { $0 ~> \.owner } // MARK: - Parse a request or response body with one Dog in it using an alternative model typealias AltSingleDogDocument = JSONAPI.Document, NoMetadata, NoLinks, NoIncludes, NoAPIDescription, BasicJSONAPIError> let altDogResponse = try! JSONDecoder().decode(AltSingleDogDocument.self, from: singleDogData) let altDogFromData = altDogResponse.body.primaryResource?.value -let altDogHuman: Person.Identifier? = altDogFromData.flatMap { $0 ~> \.human } +let altDogHuman: Person.ID? = altDogFromData.flatMap { $0 ~> \.human } // MARK: - Create a request or response with multiple people and dogs and houses included -let personIds = [Person.Identifier(), Person.Identifier()] +let personIds = [Person.ID(), Person.ID()] let dogs = try! [Dog(name: "Buddy", owner: personIds[0]), Dog(name: "Joy", owner: personIds[0]), Dog(name: "Travis", owner: personIds[1])] let houses = [House(attributes: .none, relationships: .none, meta: .none, links: .none), House(attributes: .none, relationships: .none, meta: .none, links: .none)] let people = try! [Person(id: personIds[0], name: ["Gary", "Doe"], favoriteColor: "Orange-Red", friends: [], dogs: [dogs[0], dogs[1]], home: houses[0]), Person(id: personIds[1], name: ["Elise", "Joy"], favoriteColor: "Red", friends: [], dogs: [dogs[2]], home: houses[1])] diff --git a/JSONAPI.playground/Sources/Entities.swift b/JSONAPI.playground/Sources/Entities.swift index 1bd6109..8b1564a 100644 --- a/JSONAPI.playground/Sources/Entities.swift +++ b/JSONAPI.playground/Sources/Entities.swift @@ -25,7 +25,7 @@ extension String: CreatableRawIdType { // MARK: - typealiases for convenience public typealias ExampleEntity = ResourceObject -public typealias ToOne = ToOneRelationship +public typealias ToOne = ToOneRelationship public typealias ToMany = ToManyRelationship // MARK: - A few resource objects (entities) @@ -63,8 +63,8 @@ public enum PersonDescription: ResourceObjectDescription { public typealias Person = ExampleEntity public extension ResourceObject where Description == PersonDescription, MetaType == NoMetadata, LinksType == NoLinks, EntityRawIdType == String { - init(id: Person.Id? = nil,name: [String], favoriteColor: String, friends: [Person], dogs: [Dog], home: House) throws { - self = Person(id: id ?? Person.Id(), attributes: .init(name: .init(value: name), favoriteColor: .init(value: favoriteColor)), relationships: .init(friends: .init(resourceObjects: friends), dogs: .init(resourceObjects: dogs), home: .init(resourceObject: home)), meta: .none, links: .none) + init(id: Person.ID? = nil,name: [String], favoriteColor: String, friends: [Person], dogs: [Dog], home: House) throws { + self = Person(id: id ?? Person.ID(), attributes: .init(name: .init(value: name), favoriteColor: .init(value: favoriteColor)), relationships: .init(friends: .init(resourceObjects: friends), dogs: .init(resourceObjects: dogs), home: .init(resourceObject: home)), meta: .none, links: .none) } } @@ -147,7 +147,7 @@ public extension ResourceObject where Description == DogDescription, MetaType == self = Dog(attributes: .init(name: .init(value: name)), relationships: DogDescription.Relationships(owner: .init(resourceObject: owner)), meta: .none, links: .none) } - init(name: String, owner: Person.Id) throws { + init(name: String, owner: Person.ID) throws { self = Dog(attributes: .init(name: .init(value: name)), relationships: .init(owner: .init(id: owner)), meta: .none, links: .none) } } diff --git a/README.md b/README.md index d1b28a7..e735354 100644 --- a/README.md +++ b/README.md @@ -163,7 +163,7 @@ typealias UnidentifiedJSONEntity = JSONA // Create relationship typealiases because we do not expect // JSON:API Relationships for this particular API to have // Metadata or Links associated with them. -typealias ToOneRelationship = JSONAPI.ToOneRelationship +typealias ToOneRelationship = JSONAPI.ToOneRelationship typealias ToManyRelationship = JSONAPI.ToManyRelationship // Create a typealias for a Document because we do not expect @@ -220,7 +220,7 @@ typealias SingleArticleDocument = Document, NoInclud func articleDocument(includeAuthor: Bool) -> Either { // Let's pretend all of this is coming from a database: - let authorId = Author.Identifier(rawValue: "1234") + let authorId = Author.ID(rawValue: "1234") let article = Article(id: .init(rawValue: "5678"), attributes: .init(title: .init(value: "JSON:API in Swift"), diff --git a/Sources/JSONAPI/Resource/Relationship.swift b/Sources/JSONAPI/Resource/Relationship.swift index 8b44a81..0ab0dbf 100644 --- a/Sources/JSONAPI/Resource/Relationship.swift +++ b/Sources/JSONAPI/Resource/Relationship.swift @@ -56,25 +56,25 @@ extension ToOneRelationship where MetaType == NoMetadata, LinksType == NoLinks { } extension ToOneRelationship { - public init(resourceObject: T, meta: MetaType, links: LinksType) where T.Id == Identifiable.ID { + public init(resourceObject: T, meta: MetaType, links: LinksType) where T.ID == Identifiable.ID { self.init(id: resourceObject.id, meta: meta, links: links) } } extension ToOneRelationship where MetaType == NoMetadata, LinksType == NoLinks { - public init(resourceObject: T) where T.Id == Identifiable.ID { + public init(resourceObject: T) where T.ID == Identifiable.ID { self.init(id: resourceObject.id, meta: .none, links: .none) } } extension ToOneRelationship where Identifiable: OptionalRelatable { - public init(resourceObject: T?, meta: MetaType, links: LinksType) where T.Id == Identifiable.Wrapped.ID { + public init(resourceObject: T?, meta: MetaType, links: LinksType) where T.ID == Identifiable.Wrapped.ID { self.init(id: resourceObject?.id, meta: meta, links: links) } } extension ToOneRelationship where Identifiable: OptionalRelatable, MetaType == NoMetadata, LinksType == NoLinks { - public init(resourceObject: T?) where T.Id == Identifiable.Wrapped.ID { + public init(resourceObject: T?) where T.ID == Identifiable.Wrapped.ID { self.init(id: resourceObject?.id, meta: .none, links: .none) } } @@ -102,7 +102,7 @@ public struct ToManyRelationship(resourceObjects: [T], meta: MetaType, links: LinksType) where T.Id == Relatable.ID { + public init(resourceObjects: [T], meta: MetaType, links: LinksType) where T.ID == Relatable.ID { self.init(ids: resourceObjects.map(\.id), meta: meta, links: links) } @@ -129,7 +129,7 @@ extension ToManyRelationship where MetaType == NoMetadata, LinksType == NoLinks return .none(withMeta: .none, links: .none) } - public init(resourceObjects: [T]) where T.Id == Relatable.ID { + public init(resourceObjects: [T]) where T.ID == Relatable.ID { self.init(resourceObjects: resourceObjects, meta: .none, links: .none) } } diff --git a/Sources/JSONAPI/Resource/Resource Object/ResourceObject.swift b/Sources/JSONAPI/Resource/Resource Object/ResourceObject.swift index 9ac95d8..b068ebd 100644 --- a/Sources/JSONAPI/Resource/Resource Object/ResourceObject.swift +++ b/Sources/JSONAPI/Resource/Resource Object/ResourceObject.swift @@ -73,7 +73,7 @@ public protocol ResourceObjectProxy: Equatable, JSONTyped { associatedtype Description: ResourceObjectProxyDescription associatedtype EntityRawIdType: JSONAPI.MaybeRawId - typealias Id = JSONAPI.Id + typealias ID = JSONAPI.Id typealias Attributes = Description.Attributes typealias Relationships = Description.Relationships @@ -82,7 +82,7 @@ public protocol ResourceObjectProxy: Equatable, JSONTyped { /// the entity is being created clientside and the /// server is being asked to create a unique Id. Otherwise, /// this should be of a type conforming to `IdType`. - var id: Id { get } + var id: ID { get } /// The JSON API compliant attributes of this `Entity`. var attributes: Attributes { get } @@ -121,6 +121,7 @@ public protocol IdentifiableResourceObjectType: ResourceObjectType, Relatable wh /// See https://jsonapi.org/format/#document-resource-objects public struct ResourceObject: ResourceObjectType { + public typealias ID = JSONAPI.Id public typealias Meta = MetaType public typealias Links = LinksType @@ -128,7 +129,7 @@ public struct ResourceObject { switch self { case .a(let a): - return Id(rawValue: a.id.rawValue) + return ID(rawValue: a.id.rawValue) case .b(let b): - return Id(rawValue: b.id.rawValue) + return ID(rawValue: b.id.rawValue) } } diff --git a/Tests/JSONAPITests/ResourceObject/ResourceObjectTests.swift b/Tests/JSONAPITests/ResourceObject/ResourceObjectTests.swift index b7b6dca..bf4b6be 100644 --- a/Tests/JSONAPITests/ResourceObject/ResourceObjectTests.swift +++ b/Tests/JSONAPITests/ResourceObject/ResourceObjectTests.swift @@ -92,8 +92,8 @@ class ResourceObjectTests: XCTestCase { let _ = TestEntity9(id: .init(rawValue: "9"), attributes: .none, relationships: .init(meta: .init(meta: .init(x: "hello", y: 5), links: .none), optionalMeta: nil, one: entity1.pointer, nullableOne: nil, optionalOne: nil, optionalNullableOne: .init(resourceObject: entity1, meta: .none, links: .none), optionalMany: nil), meta: .none, links: .none) let _ = TestEntity9(id: .init(rawValue: "9"), attributes: .none, relationships: .init(meta: .init(meta: .init(x: "hello", y: 5), links: .none), optionalMeta: nil, one: entity1.pointer, nullableOne: nil, optionalOne: nil, optionalNullableOne: .init(resourceObject: entity1, meta: .none, links: .none), optionalMany: .init(resourceObjects: [], meta: .none, links: .none)), meta: .none, links: .none) let e10id1 = TestEntity10.ID(rawValue: "hello") - let e10id2 = TestEntity10.Id(rawValue: "world") - let e10id3: TestEntity10.Id = "!" + let e10id2 = TestEntity10.ID(rawValue: "world") + let e10id3: TestEntity10.ID = "!" let _ = TestEntity10(id: .init(rawValue: "10"), attributes: .none, relationships: .init(selfRef: .init(id: e10id1), selfRefs: .init(ids: [e10id2, e10id3])), meta: .none, links: .none) XCTAssertNoThrow(try TestEntity11(id: .init(rawValue: "11"), attributes: .init(number: .init(rawValue: 11)), relationships: .none, meta: .none, links: .none)) let _ = UnidentifiedTestEntity(attributes: .init(me: .init(value: "hello")), relationships: .none, meta: .none, links: .none) diff --git a/documentation/usage.md b/documentation/usage.md index a295347..7c11597 100644 --- a/documentation/usage.md +++ b/documentation/usage.md @@ -167,7 +167,7 @@ typealias Relationships = NoRelationships `Relationship` values boil down to `Ids` of other resource objects. To access the `Id` of a related `ResourceObject`, you can use the custom `~>` operator with the `KeyPath` of the `Relationship` from which you want the `Id`. The friends of the above `Person` `ResourceObject` can be accessed as follows (type annotations for clarity): ```swift -let friendIds: [Person.Identifier] = person ~> \.friends +let friendIds: [Person.ID] = person ~> \.friends ``` ### `JSONAPI.Attributes` @@ -244,8 +244,8 @@ If your computed property is wrapped in a `AttributeType` then you can still use ### Copying/Mutating `ResourceObjects` `ResourceObject` is a value type, so copying is its default behavior. There are three common mutations you might want to make when copying a `ResourceObject`: -1. Assigning a new `Identifier` to the copy of an identified `ResourceObject`. -2. Assigning a new `Identifier` to the copy of an unidentified `ResourceObject`. +1. Assigning a new `ID` to the copy of an identified `ResourceObject`. +2. Assigning a new `ID` to the copy of an unidentified `ResourceObject`. 3. Change attribute or relationship values. The first two can be accomplished with code like the following: @@ -595,9 +595,9 @@ enum UserDescription: ResourceObjectDescription { } struct Relationships: JSONAPI.Relationships { - public var friend: (User) -> User.Identifier { + public var friend: (User) -> User.ID { return { user in - return User.Identifier(rawValue: user.friend_id) + return User.ID(rawValue: user.friend_id) } } } @@ -612,4 +612,4 @@ Given a value `user` of the above resource object type, you can access the `frie let friendId = user ~> \.friend ``` -This works because `friend` is defined in the form: `var {name}: ({ResourceObject}) -> {Identifier}` where `{ResourceObject}` is the `JSONAPI.ResourceObject` described by the `ResourceObjectDescription` containing the meta-relationship. +This works because `friend` is defined in the form: `var {name}: ({ResourceObject}) -> {ID}` where `{ResourceObject}` is the `JSONAPI.ResourceObject` described by the `ResourceObjectDescription` containing the meta-relationship. From 9374956b920325d2eae4ed0cbb68801f23eecfbf Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Fri, 29 May 2020 15:03:23 -0700 Subject: [PATCH 156/235] bug fix to playground example. --- .../Pages/PATCHing.xcplaygroundpage/Contents.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/JSONAPI.playground/Pages/PATCHing.xcplaygroundpage/Contents.swift b/JSONAPI.playground/Pages/PATCHing.xcplaygroundpage/Contents.swift index ded305e..f916054 100644 --- a/JSONAPI.playground/Pages/PATCHing.xcplaygroundpage/Contents.swift +++ b/JSONAPI.playground/Pages/PATCHing.xcplaygroundpage/Contents.swift @@ -108,7 +108,7 @@ print("Received dog with owner: \(dog3 ~> \.owner)") // give the dog an owner let changedDog3 = dog3.replacingRelationships { _ in - return .init(owner: .init(id: ID(rawValue: "1"))) + return .init(owner: .init(id: Id(rawValue: "1"))) } // create a document to be used as a request body for a PATCH request From 97b5d1e3f4b26e16eaae08e0acbab37fcbd4b5d4 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Fri, 29 May 2020 15:12:23 -0700 Subject: [PATCH 157/235] minor tweak to usage README. --- documentation/usage.md | 48 +++++++++++++++++++++--------------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/documentation/usage.md b/documentation/usage.md index 7c11597..2b0105a 100644 --- a/documentation/usage.md +++ b/documentation/usage.md @@ -7,37 +7,37 @@ In this documentation, in order to draw attention to the difference between the - [`JSONAPI.ResourceObjectDescription`](#jsonapiresourceobjectdescription) - [`JSONAPI.ResourceObject`](#jsonapiresourceobject) - - [`Meta`](#meta) - - [`Links`](#links) - - [`MaybeRawId`](#mayberawid) - - [`RawIdType`](#rawidtype) - - [Convenient `typealiases`](#convenient-typealiases) + - [`Meta`](#meta) + - [`Links`](#links) + - [`MaybeRawId`](#mayberawid) + - [`RawIdType`](#rawidtype) + - [Convenient `typealiases`](#convenient-typealiases) - [`JSONAPI.Relationships`](#jsonapirelationships) - [`JSONAPI.Attributes`](#jsonapiattributes) - - [`Transformer`](#transformer) - - [`Validator`](#validator) - - [Computed `Attribute`](#computed-attribute) + - [`Transformer`](#transformer) + - [`Validator`](#validator) + - [Computed `Attribute`](#computed-attribute) - [Copying/Mutating `ResourceObjects`](#copyingmutating-resourceobjects) - [`JSONAPI.Document`](#jsonapidocument) - - [`ResourceBody`](#resourcebody) - - [nullable `PrimaryResource`](#nullable-primaryresource) - - [`MetaType`](#metatype) - - [`LinksType`](#linkstype) - - [`IncludeType`](#includetype) - - [`APIDescriptionType`](#apidescriptiontype) - - [`Error`](#error) - - [`UnknownJSONAPIError`](#unknownjsonapierror) - - [`BasicJSONAPIError`](#basicjsonapierror) - - [`GenericJSONAPIError`](#genericjsonapierror) + - [`ResourceBody`](#resourcebody) + - [nullable `PrimaryResource`](#nullable-primaryresource) + - [`MetaType`](#metatype) + - [`LinksType`](#linkstype) + - [`IncludeType`](#includetype) + - [`APIDescriptionType`](#apidescriptiontype) + - [`Error`](#error) + - [`UnknownJSONAPIError`](#unknownjsonapierror) + - [`BasicJSONAPIError`](#basicjsonapierror) + - [`GenericJSONAPIError`](#genericjsonapierror) - [`JSONAPI.Meta`](#jsonapimeta) - [`JSONAPI.Links`](#jsonapilinks) - [`JSONAPI.RawIdType`](#jsonapirawidtype) - [Sparse Fieldsets](#sparse-fieldsets) - - [Supporting Sparse Fieldset Encoding](#supporting-sparse-fieldset-encoding) - - [Sparse Fieldset `typealias` comparisons](#sparse-fieldset-typealias-comparisons) + - [Supporting Sparse Fieldset Encoding](#supporting-sparse-fieldset-encoding) + - [Sparse Fieldset `typealias` comparisons](#sparse-fieldset-typealias-comparisons) - [Replacing and Tapping Attributes/Relationships](#replacing-and-tapping-attributesrelationships) - - [Tapping](#tapping) - - [Replacing](#replacing) + - [Tapping](#tapping) + - [Replacing](#replacing) - [Custom Attribute or Relationship Key Mapping](#custom-attribute-or-relationship-key-mapping) - [Custom Attribute Encode/Decode](#custom-attribute-encodedecode) - [Meta-Attributes](#meta-attributes) @@ -244,8 +244,8 @@ If your computed property is wrapped in a `AttributeType` then you can still use ### Copying/Mutating `ResourceObjects` `ResourceObject` is a value type, so copying is its default behavior. There are three common mutations you might want to make when copying a `ResourceObject`: -1. Assigning a new `ID` to the copy of an identified `ResourceObject`. -2. Assigning a new `ID` to the copy of an unidentified `ResourceObject`. +1. Assigning a new `Id` to the copy of an identified `ResourceObject`. +2. Assigning a new `Id` to the copy of an unidentified `ResourceObject`. 3. Change attribute or relationship values. The first two can be accomplished with code like the following: From 7dd09779a0222d2d57f274d11749d534538b205a Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Fri, 29 May 2020 15:40:05 -0700 Subject: [PATCH 158/235] Attempt to force compiler to acknowledge that ResourceObjectProxy and ResourceObject have the same ID type. --- .../Resource/Resource Object/ResourceObject.swift | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/Sources/JSONAPI/Resource/Resource Object/ResourceObject.swift b/Sources/JSONAPI/Resource/Resource Object/ResourceObject.swift index b068ebd..88ae539 100644 --- a/Sources/JSONAPI/Resource/Resource Object/ResourceObject.swift +++ b/Sources/JSONAPI/Resource/Resource Object/ResourceObject.swift @@ -73,8 +73,6 @@ public protocol ResourceObjectProxy: Equatable, JSONTyped { associatedtype Description: ResourceObjectProxyDescription associatedtype EntityRawIdType: JSONAPI.MaybeRawId - typealias ID = JSONAPI.Id - typealias Attributes = Description.Attributes typealias Relationships = Description.Relationships @@ -82,7 +80,7 @@ public protocol ResourceObjectProxy: Equatable, JSONTyped { /// the entity is being created clientside and the /// server is being asked to create a unique Id. Otherwise, /// this should be of a type conforming to `IdType`. - var id: ID { get } + var id: JSONAPI.Id { get } /// The JSON API compliant attributes of this `Entity`. var attributes: Attributes { get } @@ -91,6 +89,10 @@ public protocol ResourceObjectProxy: Equatable, JSONTyped { var relationships: Relationships { get } } +extension ResourceObjectProxy { + public typealias ID = JSONAPI.Id +} + extension ResourceObjectProxy { /// The JSON API compliant "type" of this `ResourceObject`. public static var jsonType: String { return Description.jsonType } @@ -121,7 +123,6 @@ public protocol IdentifiableResourceObjectType: ResourceObjectType, Relatable wh /// See https://jsonapi.org/format/#document-resource-objects public struct ResourceObject: ResourceObjectType { - public typealias ID = JSONAPI.Id public typealias Meta = MetaType public typealias Links = LinksType From 9027a7f5e6bbfbc52546b5bfe3f78d7a0ea74895 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Fri, 29 May 2020 18:40:48 -0700 Subject: [PATCH 159/235] Had to change course due to type system errors with collision between one protocol's typealias and another's associatedtype --- .../Contents.swift | 4 ++-- .../Contents.swift | 10 +++++----- .../Usage.xcplaygroundpage/Contents.swift | 6 +++--- JSONAPI.playground/Sources/Entities.swift | 8 ++++---- README.md | 4 ++-- Sources/JSONAPI/Resource/Relationship.swift | 12 +++++------ .../Resource Object/ResourceObject.swift | 20 +++++++++---------- .../DocumentCompoundResourceTests.swift | 6 +++--- Tests/JSONAPITests/Poly/PolyProxyTests.swift | 4 ++-- .../ResourceObject/ResourceObjectTests.swift | 4 ++-- documentation/usage.md | 12 +++++------ 11 files changed, 45 insertions(+), 45 deletions(-) diff --git a/JSONAPI.playground/Pages/Full Client & Server Example.xcplaygroundpage/Contents.swift b/JSONAPI.playground/Pages/Full Client & Server Example.xcplaygroundpage/Contents.swift index 7a83541..280a614 100644 --- a/JSONAPI.playground/Pages/Full Client & Server Example.xcplaygroundpage/Contents.swift +++ b/JSONAPI.playground/Pages/Full Client & Server Example.xcplaygroundpage/Contents.swift @@ -30,7 +30,7 @@ typealias UnidentifiedJSONEntity = JSONA // Create relationship typealiases because we do not expect // JSON:API Relationships for this particular API to have // Metadata or Links associated with them. -typealias ToOneRelationship = JSONAPI.ToOneRelationship +typealias ToOneRelationship = JSONAPI.ToOneRelationship typealias ToManyRelationship = JSONAPI.ToManyRelationship // Create a typealias for a Document because we do not expect @@ -86,7 +86,7 @@ typealias SingleArticleDocument = Document, NoInclud func articleDocument(includeAuthor: Bool) -> Either { // Let's pretend all of this is coming from a database: - let authorId = Author.ID(rawValue: "1234") + let authorId = Author.Identifier(rawValue: "1234") let article = Article(id: .init(rawValue: "5678"), attributes: .init(title: .init(value: "JSON:API in Swift"), diff --git a/JSONAPI.playground/Pages/Full Document Verbose Generation.xcplaygroundpage/Contents.swift b/JSONAPI.playground/Pages/Full Document Verbose Generation.xcplaygroundpage/Contents.swift index 2628c38..81f985b 100644 --- a/JSONAPI.playground/Pages/Full Document Verbose Generation.xcplaygroundpage/Contents.swift +++ b/JSONAPI.playground/Pages/Full Document Verbose Generation.xcplaygroundpage/Contents.swift @@ -129,9 +129,9 @@ enum ArticleDocumentError: String, JSONAPIError, Codable { typealias SingleArticleDocument = JSONAPI.Document, DocumentMetadata, SingleArticleDocumentLinks, Include1, APIDescription, ArticleDocumentError> // MARK: - Instantiations -let authorId1 = Author.ID() -let authorId2 = Author.ID() -let authorId3 = Author.ID() +let authorId1 = Author.Identifier() +let authorId2 = Author.Identifier() +let authorId3 = Author.Identifier() let now = Date() let tomorrow = Calendar.current.date(byAdding: .day, value: 1, to: now)! @@ -155,7 +155,7 @@ let author1Links = EntityLinks(selfLink: .init(url: URL(string: "https://article meta: .init(expiry: tomorrow))) let author1 = Author(id: authorId1, attributes: .init(name: .init(value: "James Kinney")), - relationships: .init(articles: .init(ids: [article.id, Article.ID(), Article.ID()], + relationships: .init(articles: .init(ids: [article.id, Article.Identifier(), Article.Identifier()], meta: .init(pagination: .init(total: 3, limit: 50, offset: 0)), @@ -167,7 +167,7 @@ let author2Links = EntityLinks(selfLink: .init(url: URL(string: "https://article meta: .init(expiry: tomorrow))) let author2 = Author(id: authorId2, attributes: .init(name: .init(value: "James Kinney")), - relationships: .init(articles: .init(ids: [article.id, Article.ID()], + relationships: .init(articles: .init(ids: [article.id, Article.Identifier()], meta: .init(pagination: .init(total: 2, limit: 50, offset: 0)), diff --git a/JSONAPI.playground/Pages/Usage.xcplaygroundpage/Contents.swift b/JSONAPI.playground/Pages/Usage.xcplaygroundpage/Contents.swift index 48296f0..5917bef 100644 --- a/JSONAPI.playground/Pages/Usage.xcplaygroundpage/Contents.swift +++ b/JSONAPI.playground/Pages/Usage.xcplaygroundpage/Contents.swift @@ -20,18 +20,18 @@ let singleDogData = try! JSONEncoder().encode(singleDogDocument) // MARK: - Parse a request or response body with one Dog in it let dogResponse = try! JSONDecoder().decode(SingleDogDocument.self, from: singleDogData) let dogFromData = dogResponse.body.primaryResource?.value -let dogOwner: Person.ID? = dogFromData.flatMap { $0 ~> \.owner } +let dogOwner: Person.Identifier? = dogFromData.flatMap { $0 ~> \.owner } // MARK: - Parse a request or response body with one Dog in it using an alternative model typealias AltSingleDogDocument = JSONAPI.Document, NoMetadata, NoLinks, NoIncludes, NoAPIDescription, BasicJSONAPIError> let altDogResponse = try! JSONDecoder().decode(AltSingleDogDocument.self, from: singleDogData) let altDogFromData = altDogResponse.body.primaryResource?.value -let altDogHuman: Person.ID? = altDogFromData.flatMap { $0 ~> \.human } +let altDogHuman: Person.Identifier? = altDogFromData.flatMap { $0 ~> \.human } // MARK: - Create a request or response with multiple people and dogs and houses included -let personIds = [Person.ID(), Person.ID()] +let personIds = [Person.Identifier(), Person.Identifier()] let dogs = try! [Dog(name: "Buddy", owner: personIds[0]), Dog(name: "Joy", owner: personIds[0]), Dog(name: "Travis", owner: personIds[1])] let houses = [House(attributes: .none, relationships: .none, meta: .none, links: .none), House(attributes: .none, relationships: .none, meta: .none, links: .none)] let people = try! [Person(id: personIds[0], name: ["Gary", "Doe"], favoriteColor: "Orange-Red", friends: [], dogs: [dogs[0], dogs[1]], home: houses[0]), Person(id: personIds[1], name: ["Elise", "Joy"], favoriteColor: "Red", friends: [], dogs: [dogs[2]], home: houses[1])] diff --git a/JSONAPI.playground/Sources/Entities.swift b/JSONAPI.playground/Sources/Entities.swift index 8b1564a..1bd6109 100644 --- a/JSONAPI.playground/Sources/Entities.swift +++ b/JSONAPI.playground/Sources/Entities.swift @@ -25,7 +25,7 @@ extension String: CreatableRawIdType { // MARK: - typealiases for convenience public typealias ExampleEntity = ResourceObject -public typealias ToOne = ToOneRelationship +public typealias ToOne = ToOneRelationship public typealias ToMany = ToManyRelationship // MARK: - A few resource objects (entities) @@ -63,8 +63,8 @@ public enum PersonDescription: ResourceObjectDescription { public typealias Person = ExampleEntity public extension ResourceObject where Description == PersonDescription, MetaType == NoMetadata, LinksType == NoLinks, EntityRawIdType == String { - init(id: Person.ID? = nil,name: [String], favoriteColor: String, friends: [Person], dogs: [Dog], home: House) throws { - self = Person(id: id ?? Person.ID(), attributes: .init(name: .init(value: name), favoriteColor: .init(value: favoriteColor)), relationships: .init(friends: .init(resourceObjects: friends), dogs: .init(resourceObjects: dogs), home: .init(resourceObject: home)), meta: .none, links: .none) + init(id: Person.Id? = nil,name: [String], favoriteColor: String, friends: [Person], dogs: [Dog], home: House) throws { + self = Person(id: id ?? Person.Id(), attributes: .init(name: .init(value: name), favoriteColor: .init(value: favoriteColor)), relationships: .init(friends: .init(resourceObjects: friends), dogs: .init(resourceObjects: dogs), home: .init(resourceObject: home)), meta: .none, links: .none) } } @@ -147,7 +147,7 @@ public extension ResourceObject where Description == DogDescription, MetaType == self = Dog(attributes: .init(name: .init(value: name)), relationships: DogDescription.Relationships(owner: .init(resourceObject: owner)), meta: .none, links: .none) } - init(name: String, owner: Person.ID) throws { + init(name: String, owner: Person.Id) throws { self = Dog(attributes: .init(name: .init(value: name)), relationships: .init(owner: .init(id: owner)), meta: .none, links: .none) } } diff --git a/README.md b/README.md index e735354..d1b28a7 100644 --- a/README.md +++ b/README.md @@ -163,7 +163,7 @@ typealias UnidentifiedJSONEntity = JSONA // Create relationship typealiases because we do not expect // JSON:API Relationships for this particular API to have // Metadata or Links associated with them. -typealias ToOneRelationship = JSONAPI.ToOneRelationship +typealias ToOneRelationship = JSONAPI.ToOneRelationship typealias ToManyRelationship = JSONAPI.ToManyRelationship // Create a typealias for a Document because we do not expect @@ -220,7 +220,7 @@ typealias SingleArticleDocument = Document, NoInclud func articleDocument(includeAuthor: Bool) -> Either { // Let's pretend all of this is coming from a database: - let authorId = Author.ID(rawValue: "1234") + let authorId = Author.Identifier(rawValue: "1234") let article = Article(id: .init(rawValue: "5678"), attributes: .init(title: .init(value: "JSON:API in Swift"), diff --git a/Sources/JSONAPI/Resource/Relationship.swift b/Sources/JSONAPI/Resource/Relationship.swift index 0ab0dbf..8b44a81 100644 --- a/Sources/JSONAPI/Resource/Relationship.swift +++ b/Sources/JSONAPI/Resource/Relationship.swift @@ -56,25 +56,25 @@ extension ToOneRelationship where MetaType == NoMetadata, LinksType == NoLinks { } extension ToOneRelationship { - public init(resourceObject: T, meta: MetaType, links: LinksType) where T.ID == Identifiable.ID { + public init(resourceObject: T, meta: MetaType, links: LinksType) where T.Id == Identifiable.ID { self.init(id: resourceObject.id, meta: meta, links: links) } } extension ToOneRelationship where MetaType == NoMetadata, LinksType == NoLinks { - public init(resourceObject: T) where T.ID == Identifiable.ID { + public init(resourceObject: T) where T.Id == Identifiable.ID { self.init(id: resourceObject.id, meta: .none, links: .none) } } extension ToOneRelationship where Identifiable: OptionalRelatable { - public init(resourceObject: T?, meta: MetaType, links: LinksType) where T.ID == Identifiable.Wrapped.ID { + public init(resourceObject: T?, meta: MetaType, links: LinksType) where T.Id == Identifiable.Wrapped.ID { self.init(id: resourceObject?.id, meta: meta, links: links) } } extension ToOneRelationship where Identifiable: OptionalRelatable, MetaType == NoMetadata, LinksType == NoLinks { - public init(resourceObject: T?) where T.ID == Identifiable.Wrapped.ID { + public init(resourceObject: T?) where T.Id == Identifiable.Wrapped.ID { self.init(id: resourceObject?.id, meta: .none, links: .none) } } @@ -102,7 +102,7 @@ public struct ToManyRelationship(resourceObjects: [T], meta: MetaType, links: LinksType) where T.ID == Relatable.ID { + public init(resourceObjects: [T], meta: MetaType, links: LinksType) where T.Id == Relatable.ID { self.init(ids: resourceObjects.map(\.id), meta: meta, links: links) } @@ -129,7 +129,7 @@ extension ToManyRelationship where MetaType == NoMetadata, LinksType == NoLinks return .none(withMeta: .none, links: .none) } - public init(resourceObjects: [T]) where T.ID == Relatable.ID { + public init(resourceObjects: [T]) where T.Id == Relatable.ID { self.init(resourceObjects: resourceObjects, meta: .none, links: .none) } } diff --git a/Sources/JSONAPI/Resource/Resource Object/ResourceObject.swift b/Sources/JSONAPI/Resource/Resource Object/ResourceObject.swift index 88ae539..9ac95d8 100644 --- a/Sources/JSONAPI/Resource/Resource Object/ResourceObject.swift +++ b/Sources/JSONAPI/Resource/Resource Object/ResourceObject.swift @@ -73,6 +73,8 @@ public protocol ResourceObjectProxy: Equatable, JSONTyped { associatedtype Description: ResourceObjectProxyDescription associatedtype EntityRawIdType: JSONAPI.MaybeRawId + typealias Id = JSONAPI.Id + typealias Attributes = Description.Attributes typealias Relationships = Description.Relationships @@ -80,7 +82,7 @@ public protocol ResourceObjectProxy: Equatable, JSONTyped { /// the entity is being created clientside and the /// server is being asked to create a unique Id. Otherwise, /// this should be of a type conforming to `IdType`. - var id: JSONAPI.Id { get } + var id: Id { get } /// The JSON API compliant attributes of this `Entity`. var attributes: Attributes { get } @@ -89,10 +91,6 @@ public protocol ResourceObjectProxy: Equatable, JSONTyped { var relationships: Relationships { get } } -extension ResourceObjectProxy { - public typealias ID = JSONAPI.Id -} - extension ResourceObjectProxy { /// The JSON API compliant "type" of this `ResourceObject`. public static var jsonType: String { return Description.jsonType } @@ -130,7 +128,7 @@ public struct ResourceObject { switch self { case .a(let a): - return ID(rawValue: a.id.rawValue) + return Id(rawValue: a.id.rawValue) case .b(let b): - return ID(rawValue: b.id.rawValue) + return Id(rawValue: b.id.rawValue) } } diff --git a/Tests/JSONAPITests/ResourceObject/ResourceObjectTests.swift b/Tests/JSONAPITests/ResourceObject/ResourceObjectTests.swift index bf4b6be..b7b6dca 100644 --- a/Tests/JSONAPITests/ResourceObject/ResourceObjectTests.swift +++ b/Tests/JSONAPITests/ResourceObject/ResourceObjectTests.swift @@ -92,8 +92,8 @@ class ResourceObjectTests: XCTestCase { let _ = TestEntity9(id: .init(rawValue: "9"), attributes: .none, relationships: .init(meta: .init(meta: .init(x: "hello", y: 5), links: .none), optionalMeta: nil, one: entity1.pointer, nullableOne: nil, optionalOne: nil, optionalNullableOne: .init(resourceObject: entity1, meta: .none, links: .none), optionalMany: nil), meta: .none, links: .none) let _ = TestEntity9(id: .init(rawValue: "9"), attributes: .none, relationships: .init(meta: .init(meta: .init(x: "hello", y: 5), links: .none), optionalMeta: nil, one: entity1.pointer, nullableOne: nil, optionalOne: nil, optionalNullableOne: .init(resourceObject: entity1, meta: .none, links: .none), optionalMany: .init(resourceObjects: [], meta: .none, links: .none)), meta: .none, links: .none) let e10id1 = TestEntity10.ID(rawValue: "hello") - let e10id2 = TestEntity10.ID(rawValue: "world") - let e10id3: TestEntity10.ID = "!" + let e10id2 = TestEntity10.Id(rawValue: "world") + let e10id3: TestEntity10.Id = "!" let _ = TestEntity10(id: .init(rawValue: "10"), attributes: .none, relationships: .init(selfRef: .init(id: e10id1), selfRefs: .init(ids: [e10id2, e10id3])), meta: .none, links: .none) XCTAssertNoThrow(try TestEntity11(id: .init(rawValue: "11"), attributes: .init(number: .init(rawValue: 11)), relationships: .none, meta: .none, links: .none)) let _ = UnidentifiedTestEntity(attributes: .init(me: .init(value: "hello")), relationships: .none, meta: .none, links: .none) diff --git a/documentation/usage.md b/documentation/usage.md index 7c11597..a295347 100644 --- a/documentation/usage.md +++ b/documentation/usage.md @@ -167,7 +167,7 @@ typealias Relationships = NoRelationships `Relationship` values boil down to `Ids` of other resource objects. To access the `Id` of a related `ResourceObject`, you can use the custom `~>` operator with the `KeyPath` of the `Relationship` from which you want the `Id`. The friends of the above `Person` `ResourceObject` can be accessed as follows (type annotations for clarity): ```swift -let friendIds: [Person.ID] = person ~> \.friends +let friendIds: [Person.Identifier] = person ~> \.friends ``` ### `JSONAPI.Attributes` @@ -244,8 +244,8 @@ If your computed property is wrapped in a `AttributeType` then you can still use ### Copying/Mutating `ResourceObjects` `ResourceObject` is a value type, so copying is its default behavior. There are three common mutations you might want to make when copying a `ResourceObject`: -1. Assigning a new `ID` to the copy of an identified `ResourceObject`. -2. Assigning a new `ID` to the copy of an unidentified `ResourceObject`. +1. Assigning a new `Identifier` to the copy of an identified `ResourceObject`. +2. Assigning a new `Identifier` to the copy of an unidentified `ResourceObject`. 3. Change attribute or relationship values. The first two can be accomplished with code like the following: @@ -595,9 +595,9 @@ enum UserDescription: ResourceObjectDescription { } struct Relationships: JSONAPI.Relationships { - public var friend: (User) -> User.ID { + public var friend: (User) -> User.Identifier { return { user in - return User.ID(rawValue: user.friend_id) + return User.Identifier(rawValue: user.friend_id) } } } @@ -612,4 +612,4 @@ Given a value `user` of the above resource object type, you can access the `frie let friendId = user ~> \.friend ``` -This works because `friend` is defined in the form: `var {name}: ({ResourceObject}) -> {ID}` where `{ResourceObject}` is the `JSONAPI.ResourceObject` described by the `ResourceObjectDescription` containing the meta-relationship. +This works because `friend` is defined in the form: `var {name}: ({ResourceObject}) -> {Identifier}` where `{ResourceObject}` is the `JSONAPI.ResourceObject` described by the `ResourceObjectDescription` containing the meta-relationship. From 540199146c01b3a7514b7ea5ba0df87292c79fb9 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Fri, 29 May 2020 18:59:39 -0700 Subject: [PATCH 160/235] re-update documentation --- .../Contents.swift | 2 +- .../Contents.swift | 10 +++++----- .../Pages/Usage.xcplaygroundpage/Contents.swift | 6 +++--- README.md | 4 ++-- documentation/usage.md | 12 ++++++------ 5 files changed, 17 insertions(+), 17 deletions(-) diff --git a/JSONAPI.playground/Pages/Full Client & Server Example.xcplaygroundpage/Contents.swift b/JSONAPI.playground/Pages/Full Client & Server Example.xcplaygroundpage/Contents.swift index 280a614..5cc9cef 100644 --- a/JSONAPI.playground/Pages/Full Client & Server Example.xcplaygroundpage/Contents.swift +++ b/JSONAPI.playground/Pages/Full Client & Server Example.xcplaygroundpage/Contents.swift @@ -86,7 +86,7 @@ typealias SingleArticleDocument = Document, NoInclud func articleDocument(includeAuthor: Bool) -> Either { // Let's pretend all of this is coming from a database: - let authorId = Author.Identifier(rawValue: "1234") + let authorId = Author.Id(rawValue: "1234") let article = Article(id: .init(rawValue: "5678"), attributes: .init(title: .init(value: "JSON:API in Swift"), diff --git a/JSONAPI.playground/Pages/Full Document Verbose Generation.xcplaygroundpage/Contents.swift b/JSONAPI.playground/Pages/Full Document Verbose Generation.xcplaygroundpage/Contents.swift index 81f985b..678296e 100644 --- a/JSONAPI.playground/Pages/Full Document Verbose Generation.xcplaygroundpage/Contents.swift +++ b/JSONAPI.playground/Pages/Full Document Verbose Generation.xcplaygroundpage/Contents.swift @@ -129,9 +129,9 @@ enum ArticleDocumentError: String, JSONAPIError, Codable { typealias SingleArticleDocument = JSONAPI.Document, DocumentMetadata, SingleArticleDocumentLinks, Include1, APIDescription, ArticleDocumentError> // MARK: - Instantiations -let authorId1 = Author.Identifier() -let authorId2 = Author.Identifier() -let authorId3 = Author.Identifier() +let authorId1 = Author.Id() +let authorId2 = Author.Id() +let authorId3 = Author.Id() let now = Date() let tomorrow = Calendar.current.date(byAdding: .day, value: 1, to: now)! @@ -155,7 +155,7 @@ let author1Links = EntityLinks(selfLink: .init(url: URL(string: "https://article meta: .init(expiry: tomorrow))) let author1 = Author(id: authorId1, attributes: .init(name: .init(value: "James Kinney")), - relationships: .init(articles: .init(ids: [article.id, Article.Identifier(), Article.Identifier()], + relationships: .init(articles: .init(ids: [article.id, Article.Id(), Article.Id()], meta: .init(pagination: .init(total: 3, limit: 50, offset: 0)), @@ -167,7 +167,7 @@ let author2Links = EntityLinks(selfLink: .init(url: URL(string: "https://article meta: .init(expiry: tomorrow))) let author2 = Author(id: authorId2, attributes: .init(name: .init(value: "James Kinney")), - relationships: .init(articles: .init(ids: [article.id, Article.Identifier()], + relationships: .init(articles: .init(ids: [article.id, Article.Id()], meta: .init(pagination: .init(total: 2, limit: 50, offset: 0)), diff --git a/JSONAPI.playground/Pages/Usage.xcplaygroundpage/Contents.swift b/JSONAPI.playground/Pages/Usage.xcplaygroundpage/Contents.swift index 5917bef..1d3da07 100644 --- a/JSONAPI.playground/Pages/Usage.xcplaygroundpage/Contents.swift +++ b/JSONAPI.playground/Pages/Usage.xcplaygroundpage/Contents.swift @@ -20,18 +20,18 @@ let singleDogData = try! JSONEncoder().encode(singleDogDocument) // MARK: - Parse a request or response body with one Dog in it let dogResponse = try! JSONDecoder().decode(SingleDogDocument.self, from: singleDogData) let dogFromData = dogResponse.body.primaryResource?.value -let dogOwner: Person.Identifier? = dogFromData.flatMap { $0 ~> \.owner } +let dogOwner: Person.Id? = dogFromData.flatMap { $0 ~> \.owner } // MARK: - Parse a request or response body with one Dog in it using an alternative model typealias AltSingleDogDocument = JSONAPI.Document, NoMetadata, NoLinks, NoIncludes, NoAPIDescription, BasicJSONAPIError> let altDogResponse = try! JSONDecoder().decode(AltSingleDogDocument.self, from: singleDogData) let altDogFromData = altDogResponse.body.primaryResource?.value -let altDogHuman: Person.Identifier? = altDogFromData.flatMap { $0 ~> \.human } +let altDogHuman: Person.Id? = altDogFromData.flatMap { $0 ~> \.human } // MARK: - Create a request or response with multiple people and dogs and houses included -let personIds = [Person.Identifier(), Person.Identifier()] +let personIds = [Person.Id(), Person.Id()] let dogs = try! [Dog(name: "Buddy", owner: personIds[0]), Dog(name: "Joy", owner: personIds[0]), Dog(name: "Travis", owner: personIds[1])] let houses = [House(attributes: .none, relationships: .none, meta: .none, links: .none), House(attributes: .none, relationships: .none, meta: .none, links: .none)] let people = try! [Person(id: personIds[0], name: ["Gary", "Doe"], favoriteColor: "Orange-Red", friends: [], dogs: [dogs[0], dogs[1]], home: houses[0]), Person(id: personIds[1], name: ["Elise", "Joy"], favoriteColor: "Red", friends: [], dogs: [dogs[2]], home: houses[1])] diff --git a/README.md b/README.md index d1b28a7..54a0997 100644 --- a/README.md +++ b/README.md @@ -151,7 +151,7 @@ extension String: CreatableRawIdType { // Create a typealias because we do not expect JSON:API Resource // Objects for this particular API to have Metadata or Links associated -// with them. We also expect them to have String Identifiers. +// with them. We also expect them to have String Ids. typealias JSONEntity = JSONAPI.ResourceObject // Similarly, create a typealias for unidentified entities. JSON:API @@ -220,7 +220,7 @@ typealias SingleArticleDocument = Document, NoInclud func articleDocument(includeAuthor: Bool) -> Either { // Let's pretend all of this is coming from a database: - let authorId = Author.Identifier(rawValue: "1234") + let authorId = Author.Id(rawValue: "1234") let article = Article(id: .init(rawValue: "5678"), attributes: .init(title: .init(value: "JSON:API in Swift"), diff --git a/documentation/usage.md b/documentation/usage.md index a295347..f138cac 100644 --- a/documentation/usage.md +++ b/documentation/usage.md @@ -167,7 +167,7 @@ typealias Relationships = NoRelationships `Relationship` values boil down to `Ids` of other resource objects. To access the `Id` of a related `ResourceObject`, you can use the custom `~>` operator with the `KeyPath` of the `Relationship` from which you want the `Id`. The friends of the above `Person` `ResourceObject` can be accessed as follows (type annotations for clarity): ```swift -let friendIds: [Person.Identifier] = person ~> \.friends +let friendIds: [Person.Id] = person ~> \.friends ``` ### `JSONAPI.Attributes` @@ -244,8 +244,8 @@ If your computed property is wrapped in a `AttributeType` then you can still use ### Copying/Mutating `ResourceObjects` `ResourceObject` is a value type, so copying is its default behavior. There are three common mutations you might want to make when copying a `ResourceObject`: -1. Assigning a new `Identifier` to the copy of an identified `ResourceObject`. -2. Assigning a new `Identifier` to the copy of an unidentified `ResourceObject`. +1. Assigning a new `Id` to the copy of an identified `ResourceObject`. +2. Assigning a new `Id` to the copy of an unidentified `ResourceObject`. 3. Change attribute or relationship values. The first two can be accomplished with code like the following: @@ -595,9 +595,9 @@ enum UserDescription: ResourceObjectDescription { } struct Relationships: JSONAPI.Relationships { - public var friend: (User) -> User.Identifier { + public var friend: (User) -> User.Id { return { user in - return User.Identifier(rawValue: user.friend_id) + return User.Id(rawValue: user.friend_id) } } } @@ -612,4 +612,4 @@ Given a value `user` of the above resource object type, you can access the `frie let friendId = user ~> \.friend ``` -This works because `friend` is defined in the form: `var {name}: ({ResourceObject}) -> {Identifier}` where `{ResourceObject}` is the `JSONAPI.ResourceObject` described by the `ResourceObjectDescription` containing the meta-relationship. +This works because `friend` is defined in the form: `var {name}: ({ResourceObject}) -> {Id}` where `{ResourceObject}` is the `JSONAPI.ResourceObject` described by the `ResourceObjectDescription` containing the meta-relationship. From 43fdde175a2c1ccae4e9f88f53fdb061857b46be Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Fri, 29 May 2020 19:02:20 -0700 Subject: [PATCH 161/235] re-fix example playgrounds --- .../Contents.swift | 2 +- JSONAPI.playground/Sources/Entities.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/JSONAPI.playground/Pages/Full Client & Server Example.xcplaygroundpage/Contents.swift b/JSONAPI.playground/Pages/Full Client & Server Example.xcplaygroundpage/Contents.swift index 5cc9cef..f7b8052 100644 --- a/JSONAPI.playground/Pages/Full Client & Server Example.xcplaygroundpage/Contents.swift +++ b/JSONAPI.playground/Pages/Full Client & Server Example.xcplaygroundpage/Contents.swift @@ -30,7 +30,7 @@ typealias UnidentifiedJSONEntity = JSONA // Create relationship typealiases because we do not expect // JSON:API Relationships for this particular API to have // Metadata or Links associated with them. -typealias ToOneRelationship = JSONAPI.ToOneRelationship +typealias ToOneRelationship = JSONAPI.ToOneRelationship typealias ToManyRelationship = JSONAPI.ToManyRelationship // Create a typealias for a Document because we do not expect diff --git a/JSONAPI.playground/Sources/Entities.swift b/JSONAPI.playground/Sources/Entities.swift index 1bd6109..e64cfbb 100644 --- a/JSONAPI.playground/Sources/Entities.swift +++ b/JSONAPI.playground/Sources/Entities.swift @@ -25,7 +25,7 @@ extension String: CreatableRawIdType { // MARK: - typealiases for convenience public typealias ExampleEntity = ResourceObject -public typealias ToOne = ToOneRelationship +public typealias ToOne = ToOneRelationship public typealias ToMany = ToManyRelationship // MARK: - A few resource objects (entities) From c825eedb0722de2fc6dfd57ac57679bf2303f1ea Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Fri, 29 May 2020 19:10:22 -0700 Subject: [PATCH 162/235] A bit more cleanup --- README.md | 2 +- Sources/JSONAPI/Resource/Relationship.swift | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 54a0997..22ebb8f 100644 --- a/README.md +++ b/README.md @@ -163,7 +163,7 @@ typealias UnidentifiedJSONEntity = JSONA // Create relationship typealiases because we do not expect // JSON:API Relationships for this particular API to have // Metadata or Links associated with them. -typealias ToOneRelationship = JSONAPI.ToOneRelationship +typealias ToOneRelationship = JSONAPI.ToOneRelationship typealias ToManyRelationship = JSONAPI.ToManyRelationship // Create a typealias for a Document because we do not expect diff --git a/Sources/JSONAPI/Resource/Relationship.swift b/Sources/JSONAPI/Resource/Relationship.swift index 8b44a81..88b54d5 100644 --- a/Sources/JSONAPI/Resource/Relationship.swift +++ b/Sources/JSONAPI/Resource/Relationship.swift @@ -96,7 +96,7 @@ public struct ToManyRelationship(pointers: [ToOneRelationship], meta: MetaType, links: LinksType) where T.ID == Relatable.ID { + public init(pointers: [ToOneRelationship], meta: MetaType, links: LinksType) where T.ID == Relatable.ID { ids = pointers.map(\.id) self.meta = meta self.links = links @@ -121,7 +121,7 @@ extension ToManyRelationship where MetaType == NoMetadata, LinksType == NoLinks self.init(ids: ids, meta: .none, links: .none) } - public init(pointers: [ToOneRelationship]) where T.ID == Relatable.ID { + public init(pointers: [ToOneRelationship]) where T.ID == Relatable.ID { self.init(pointers: pointers, meta: .none, links: .none) } From e9b9dbc90961cbd0888c09098516741e83b34d48 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Fri, 29 May 2020 19:15:36 -0700 Subject: [PATCH 163/235] sanity check test and note on the insanity --- Tests/JSONAPITests/SwiftIdentifiableTests.swift | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/Tests/JSONAPITests/SwiftIdentifiableTests.swift b/Tests/JSONAPITests/SwiftIdentifiableTests.swift index d5de133..70f6738 100644 --- a/Tests/JSONAPITests/SwiftIdentifiableTests.swift +++ b/Tests/JSONAPITests/SwiftIdentifiableTests.swift @@ -25,6 +25,19 @@ final class SwiftIdentifiableTests: XCTestCase { XCTAssertEqual(hash[t1.id], String(describing: t1.id)) XCTAssertEqual(hash[t2.id], String(describing: t2.id)) } + + func test_Id_ID_equivalence() { + // it's not at all great to have both of these names for + // the Id type, but I could not do better than this and + // still have a typealias for the Id type on the + // ResourceObjectProxy protocol. One protocol's typealias + // will collide with anotehr protocol's associatedtype in + // very ugly ways. + + XCTAssert(TestType.ID.self == TestType.Id.self) + + XCTAssertEqual(TestType.ID(rawValue: "hello"), TestType.Id(rawValue: "hello")) + } } fileprivate enum TestDescription: JSONAPI.ResourceObjectDescription { From de56460fbcf15c88b073a6dda1b3e83d839ad7b0 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Sat, 30 May 2020 14:57:40 -0700 Subject: [PATCH 164/235] updating and rearranging documentation --- .../Contents.swift | 78 +++--- README.md | 244 +----------------- documentation/client-server-example.md | 186 +++++++++++++ documentation/project-status.md | 46 ++++ documentation/usage.md | 122 ++++++--- 5 files changed, 371 insertions(+), 305 deletions(-) create mode 100644 documentation/client-server-example.md create mode 100644 documentation/project-status.md diff --git a/JSONAPI.playground/Pages/Full Client & Server Example.xcplaygroundpage/Contents.swift b/JSONAPI.playground/Pages/Full Client & Server Example.xcplaygroundpage/Contents.swift index f7b8052..1bfdde5 100644 --- a/JSONAPI.playground/Pages/Full Client & Server Example.xcplaygroundpage/Contents.swift +++ b/JSONAPI.playground/Pages/Full Client & Server Example.xcplaygroundpage/Contents.swift @@ -72,50 +72,62 @@ typealias Article = JSONEntity // We create a typealias to represent a document containing one Article // and including its Author -typealias SingleArticleDocumentWithIncludes = Document, Include1> +typealias SingleArticleDocument = Document, Include1> -// ... and a typealias to represent a document containing one Article and -// not including any related entities. -typealias SingleArticleDocument = Document, NoIncludes> +// ... and a typealias to represent a batch document containing any number of Articles +typealias ManyArticleDocument = Document, Include1> // MARK: - Server Pseudo-example // Skipping over all the API and database stuff, here's a chunk of code // that creates a document. Note that this document is the entirety // of a JSON:API response body. -func articleDocument(includeAuthor: Bool) -> Either { +func article(includeAuthor: Bool) -> CompoundResource { // Let's pretend all of this is coming from a database: let authorId = Author.Id(rawValue: "1234") - let article = Article(id: .init(rawValue: "5678"), - attributes: .init(title: .init(value: "JSON:API in Swift"), - abstract: .init(value: "Not yet written")), - relationships: .init(author: .init(id: authorId)), - meta: .none, - links: .none) - - let document = SingleArticleDocument(apiDescription: .none, - body: .init(resourceObject: article), - includes: .none, - meta: .none, - links: .none) - - switch includeAuthor { - case false: - return .init(document) - - case true: - let author = Author(id: authorId, - attributes: .init(name: .init(value: "Janice Bluff")), - relationships: .none, - meta: .none, - links: .none) - - let includes: Includes = .init(values: [.init(author)]) - - return .init(document.including(includes)) + let article = Article( + id: .init(rawValue: "5678"), + attributes: .init( + title: .init(value: "JSON:API in Swift"), + abstract: .init(value: "Not yet written") + ), + relationships: .init(author: .init(id: authorId)), + meta: .none, + links: .none + ) + + let authorInclude: SingleArticleDocument.Include? + if includeAuthor { + let author = Author( + id: authorId, + attributes: .init(name: .init(value: "Janice Bluff")), + relationships: .none, + meta: .none, + links: .none + ) + authorInclude = .init(author) + } else { + authorInclude = nil } + + return CompoundResource( + primary: article, + relatives: authorInclude.map { [$0] } ?? [] + ) +} + +func articleDocument(includeAuthor: Bool) -> SingleArticleDocument { + + let compoundResource = article(includeAuthor: includeAuthor) + + return SingleArticleDocument( + apiDescription: .none, + resource: compoundResource, + meta: .none, + links: .none + ) } let encoder = JSONEncoder() @@ -151,7 +163,7 @@ func docode(articleResponseData: Data) throws -> (article: Article, author: Auth let decoder = JSONDecoder() decoder.keyDecodingStrategy = .convertFromSnakeCase - let articleDocument = try decoder.decode(SingleArticleDocumentWithIncludes.self, from: articleResponseData) + let articleDocument = try decoder.decode(SingleArticleDocument.self, from: articleResponseData) switch articleDocument.body { case .data(let data): diff --git a/README.md b/README.md index 22ebb8f..6cda148 100644 --- a/README.md +++ b/README.md @@ -5,8 +5,6 @@ A Swift package for encoding to- and decoding from **JSON API** compliant reques See the JSON API Spec here: https://jsonapi.org/format/ -:warning: This library provides well-tested type safety when working with JSON:API 1.0. However, the Swift compiler can sometimes have difficulty tracking down small typos when initializing `ResourceObjects`. Correct code will always compile, but tracking down the source of programmer errors can be an annoyance. This is mostly a concern when creating resource objects in-code (i.e. declaratively) like you might for unit testing. Writing a client that uses this framework to ingest and decode JSON API Compliant API responses is much less painful. - ## Quick Start :warning: The following Google Colab examples have correct code, but from time to time the Google Colab Swift compiler may be buggy and produce incorrect or erroneous results. Just keep that in mind if you run the code as you read through the Colab examples. @@ -17,13 +15,14 @@ See the JSON API Spec here: https://jsonapi.org/format/ - [Metadata Example](https://colab.research.google.com/drive/10dEESwiE9I3YoyfzVeOVwOKUTEgLT3qr) - [Custom Errors Example](https://colab.research.google.com/drive/1TIv6STzlHrkTf_-9Eu8sv8NoaxhZcFZH) - [PATCH Example](https://colab.research.google.com/drive/16KY-0BoLQKiSUh9G7nYmHzB8b2vhXA2U) + ### Serverside - [GET Example](https://colab.research.google.com/drive/1krbhzSfz8mwkBTQQnKUZJLEtYsJKSfYX) - [POST Example](https://colab.research.google.com/drive/1z3n70LwRY7vLIgbsMghvnfHA67QiuqpQ) ### Client+Server -This library works well when used by both the server responsible for serialization and the client responsible for deserialization. Check out the [example](#example) further down in this README. +This library works well when used by both the server responsible for serialization and the client responsible for deserialization. Check out the [example](./documentation/client-server-example.md). ## Table of Contents - JSONAPI @@ -34,8 +33,8 @@ This library works well when used by both the server responsible for serializati - [Xcode project](#xcode-project) - [CocoaPods](#cocoapods) - [Running the Playground](#running-the-playground) - - [Project Status](#project-status) - - [Example](#example) + - [Project Status](./documentation/project-status.md) + - [Server & Client Example](./documentation/client-server-example.md) - [Usage](./documentation/usage.md) - [JSONAPI+Testing](#jsonapitesting) - [Literal Expressibility](#literal-expressibility) @@ -67,14 +66,11 @@ If you find something wrong with this library and it isn't already mentioned und ### Swift Package Manager Just include the following in your package's dependencies and add `JSONAPI` to the dependencies for any of your targets. ```swift -.package(url: "https://github.com/mattpolzin/JSONAPI", from: "4.0.0-alpha.1") +.package(url: "https://github.com/mattpolzin/JSONAPI", from: "4.0.0") ``` ### Xcode project -To create an Xcode project for JSONAPI, run -`swift package generate-xcodeproj` - -With Xcode 11+ you can also just open the folder containing your clone of this repository and begin working. +With Xcode 11+, you can open the folder containing this repository. There is no need for an Xcode project, but you can generate one with `swift package generate-xcodeproj`. ### CocoaPods To use this framework in your project via Cocoapods, add the following dependencies to your Podfile. @@ -86,232 +82,12 @@ pod 'MP-JSONAPI', :git => 'https://github.com/mattpolzin/JSONAPI.git' ### Running the Playground To run the included Playground files, create an Xcode project using Swift Package Manager, then create an Xcode Workspace in the root of the repository and add both the generated Xcode project and the playground to the Workspace. -Note that Playground support for importing non-system Frameworks is still a bit touchy as of Swift 4.2. Sometimes building, cleaning and building, or commenting out and then uncommenting import statements (especially in the Entities.swift Playground Source file) can get things working for me when I am getting an error about `JSONAPI` not being found. - -## Project Status - -### JSON:API -#### Document -- [x] `data` -- [x] `included` -- [x] `errors` -- [x] `meta` -- [x] `jsonapi` (i.e. API Information) -- [x] `links` - -#### Resource Object -- [x] `id` -- [x] `type` -- [x] `attributes` -- [x] `relationships` -- [x] `links` -- [x] `meta` - -#### Relationship Object -- [x] `data` -- [x] `links` -- [x] `meta` - -#### Links Object -- [x] `href` -- [x] `meta` - -### Misc -- [x] Support transforms on `Attributes` values (e.g. to support different representations of `Date`) -- [x] Support validation on `Attributes`. -- [x] Support sparse fieldsets (encoding only). A client can likely just define a new model to represent a sparse population of another model in a very specific use case for decoding purposes. On the server side, sparse fieldsets of Resource Objects can be encoded without creating one model for every possible sparse fieldset. - -### Testing -#### Resource Object Validator -- [x] Disallow optional array in `Attribute` (should be empty array, not `null`). -- [x] Only allow `TransformedAttribute` and its derivatives as stored properties within `Attributes` struct. Computed properties can still be any type because they do not get encoded or decoded. -- [x] Only allow `MetaRelationship`, `ToManyRelationship` and `ToOneRelationship` within `Relationships` struct. - -### Potential Improvements -These ideas could be implemented in future versions. - -- [ ] (Maybe) Use `KeyPath` to specify `Includes` thus creating type safety around the relationship between a primary resource type and the types of included resources. -- [ ] (Maybe) Replace `SingleResourceBody` and `ManyResourceBody` with support at the `Document` level to just interpret `PrimaryResource`, `PrimaryResource?`, or `[PrimaryResource]` as the same decoding/encoding strategies. -- [ ] Support sideposting. JSONAPI spec might become opinionated in the future (https://github.com/json-api/json-api/pull/1197, https://github.com/json-api/json-api/issues/1215, https://github.com/json-api/json-api/issues/1216) but there is also an existing implementation to consider (https://jsonapi-suite.github.io/jsonapi_suite/ruby/writes/nested-writes). At this time, any sidepost implementation would be an awesome tertiary library to be used alongside the primary JSONAPI library. Maybe `JSONAPISideloading`. -- [ ] Error or warning if an included resource object is not related to a primary resource object or another included resource object (Turned off or at least not throwing by default). - -## Example -The following serves as a sort of pseudo-example. It skips server/client implementation details not related to JSON:API but still gives a more complete picture of what an implementation using this framework might look like. You can play with this example code in the Playground provided with this repo. - -### Preamble (Setup shared by server and client) -```swift -// Make String a CreatableRawIdType. -var globalStringId: Int = 0 -extension String: CreatableRawIdType { - public static func unique() -> String { - globalStringId += 1 - return String(globalStringId) - } -} - -// Create a typealias because we do not expect JSON:API Resource -// Objects for this particular API to have Metadata or Links associated -// with them. We also expect them to have String Ids. -typealias JSONEntity = JSONAPI.ResourceObject - -// Similarly, create a typealias for unidentified entities. JSON:API -// only allows unidentified entities (i.e. no "id" field) for client -// requests that create new entities. In these situations, the server -// is expected to assign the new entity a unique ID. -typealias UnidentifiedJSONEntity = JSONAPI.ResourceObject - -// Create relationship typealiases because we do not expect -// JSON:API Relationships for this particular API to have -// Metadata or Links associated with them. -typealias ToOneRelationship = JSONAPI.ToOneRelationship -typealias ToManyRelationship = JSONAPI.ToManyRelationship - -// Create a typealias for a Document because we do not expect -// JSON:API Documents for this particular API to have Metadata, Links, -// useful Errors, or an APIDescription (The *SPEC* calls this -// "API Description" the "JSON:API Object"). -typealias Document = JSONAPI.Document> - -// MARK: Entity Definitions - -enum AuthorDescription: ResourceObjectDescription { - public static var jsonType: String { return "authors" } - - public struct Attributes: JSONAPI.Attributes { - public let name: Attribute - } - - public typealias Relationships = NoRelationships -} - -typealias Author = JSONEntity - -enum ArticleDescription: ResourceObjectDescription { - public static var jsonType: String { return "articles" } - - public struct Attributes: JSONAPI.Attributes { - public let title: Attribute - public let abstract: Attribute - } - - public struct Relationships: JSONAPI.Relationships { - public let author: ToOneRelationship - } -} - -typealias Article = JSONEntity - -// MARK: Document Definitions - -// We create a typealias to represent a document containing one Article -// and including its Author -typealias SingleArticleDocumentWithIncludes = Document, Include1> - -// ... and a typealias to represent a document containing one Article and -// not including any related entities. -typealias SingleArticleDocument = Document, NoIncludes> -``` - -### Server Pseudo-example -```swift -// Skipping over all the API and database stuff, here's a chunk of code -// that creates a document. Note that this document is the entirety -// of a JSON:API response body. -func articleDocument(includeAuthor: Bool) -> Either { - // Let's pretend all of this is coming from a database: - - let authorId = Author.Id(rawValue: "1234") - - let article = Article(id: .init(rawValue: "5678"), - attributes: .init(title: .init(value: "JSON:API in Swift"), - abstract: .init(value: "Not yet written")), - relationships: .init(author: .init(id: authorId)), - meta: .none, - links: .none) - - let document = SingleArticleDocument(apiDescription: .none, - body: .init(resourceObject: article), - includes: .none, - meta: .none, - links: .none) - - switch includeAuthor { - case false: - return .init(document) - - case true: - let author = Author(id: authorId, - attributes: .init(name: .init(value: "Janice Bluff")), - relationships: .none, - meta: .none, - links: .none) - - let includes: Includes = .init(values: [.init(author)]) - - return .init(document.including(includes)) - } -} - -let encoder = JSONEncoder() -encoder.keyEncodingStrategy = .convertToSnakeCase -encoder.outputFormatting = .prettyPrinted - -let responseBody = articleDocument(includeAuthor: true) -let responseData = try! encoder.encode(responseBody) - -// Next step would be setting the HTTP body of a response. -// We will just print it out instead: -print("-----") -print(String(data: responseData, encoding: .utf8)!) - -// ... and if we had received a request for an article without -// including the author: -let otherResponseBody = articleDocument(includeAuthor: false) -let otherResponseData = try! encoder.encode(otherResponseBody) -print("-----") -print(String(data: otherResponseData, encoding: .utf8)!) -``` - -### Client Pseudo-example -```swift -enum NetworkError: Swift.Error { - case serverError - case quantityMismatch -} - -// Skipping over all the API stuff, here's a chunk of code that will -// decode a document. We will assume we have made a request for a -// single article including the author. -func docode(articleResponseData: Data) throws -> (article: Article, author: Author) { - let decoder = JSONDecoder() - decoder.keyDecodingStrategy = .convertFromSnakeCase - - let articleDocument = try decoder.decode(SingleArticleDocumentWithIncludes.self, from: articleResponseData) - - switch articleDocument.body { - case .data(let data): - let authors = data.includes[Author.self] - - guard authors.count == 1 else { - throw NetworkError.quantityMismatch - } - - return (article: data.primary.value, author: authors[0]) - case .errors(let errors, meta: _, links: _): - throw NetworkError.serverError - } -} - -let response = try! docode(articleResponseData: responseData) - -// Next step would be to do something useful with the article and author but we will print them instead. -print("-----") -print(response.article) -print(response.author) -``` +Note that Playground support for importing non-system Frameworks is still a bit touchy as of Swift 4.2. Sometimes building, cleaning and building, or commenting out and then uncommenting import statements (especially in the` Entities.swift` Playground Source file) can get things working for me when I am getting an error about `JSONAPI` not being found. ## Deeper Dive -See the [usage documentation](./documentation/usage.md). +- [Project Status](./documentation/project-status.md) +- [Server & Client Example](./documentation/client-server-example.md) +- [Usage Documentation](./documentation/usage.md) # JSONAPI+Testing The `JSONAPI` framework is packaged with a test library to help you test your `JSONAPI` integration. The test library is called `JSONAPITesting`. You can see `JSONAPITesting` in action in the Playground included with the `JSONAPI` repository. diff --git a/documentation/client-server-example.md b/documentation/client-server-example.md new file mode 100644 index 0000000..87d2205 --- /dev/null +++ b/documentation/client-server-example.md @@ -0,0 +1,186 @@ +## Example +The following serves as a sort of pseudo-example. It skips server/client implementation details not related to JSON:API but still gives a more complete picture of what an implementation using this framework might look like. You can play with this example code in the Playground provided with this repo. + +### Preamble (Setup shared by server and client) +```swift +// Make String a CreatableRawIdType. +var globalStringId: Int = 0 +extension String: CreatableRawIdType { + public static func unique() -> String { + globalStringId += 1 + return String(globalStringId) + } +} + +// Create a typealias because we do not expect JSON:API Resource +// Objects for this particular API to have Metadata or Links associated +// with them. We also expect them to have String Identifiers. +typealias JSONEntity = JSONAPI.ResourceObject + +// Similarly, create a typealias for unidentified entities. JSON:API +// only allows unidentified entities (i.e. no "id" field) for client +// requests that create new entities. In these situations, the server +// is expected to assign the new entity a unique ID. +typealias UnidentifiedJSONEntity = JSONAPI.ResourceObject + +// Create relationship typealiases because we do not expect +// JSON:API Relationships for this particular API to have +// Metadata or Links associated with them. +typealias ToOneRelationship = JSONAPI.ToOneRelationship +typealias ToManyRelationship = JSONAPI.ToManyRelationship + +// Create a typealias for a Document because we do not expect +// JSON:API Documents for this particular API to have Metadata, Links, +// useful Errors, or an APIDescription (The *SPEC* calls this +// "API Description" the "JSON:API Object"). +typealias Document = JSONAPI.Document> + +// MARK: Entity Definitions + +enum AuthorDescription: ResourceObjectDescription { + public static var jsonType: String { return "authors" } + + public struct Attributes: JSONAPI.Attributes { + public let name: Attribute + } + + public typealias Relationships = NoRelationships +} + +typealias Author = JSONEntity + +enum ArticleDescription: ResourceObjectDescription { + public static var jsonType: String { return "articles" } + + public struct Attributes: JSONAPI.Attributes { + public let title: Attribute + public let abstract: Attribute + } + + public struct Relationships: JSONAPI.Relationships { + public let author: ToOneRelationship + } +} + +typealias Article = JSONEntity + +// MARK: Document Definitions + +// We create a typealias to represent a document containing one Article +// and including its Author +typealias SingleArticleDocument = Document, Include1> + +// ... and a typealias to represent a batch document containing any number of Articles +typealias ManyArticleDocument = Document, Include1> +``` + +### Server Pseudo-example +```swift +// Skipping over all the API and database stuff, here's a chunk of code +// that creates a document. Note that this document is the entirety +// of a JSON:API response body. +func article(includeAuthor: Bool) -> CompoundResource { + // Let's pretend all of this is coming from a database: + + let authorId = Author.Id(rawValue: "1234") + + let article = Article( + id: .init(rawValue: "5678"), + attributes: .init( + title: .init(value: "JSON:API in Swift"), + abstract: .init(value: "Not yet written") + ), + relationships: .init(author: .init(id: authorId)), + meta: .none, + links: .none + ) + + let authorInclude: SingleArticleDocument.Include? + if includeAuthor { + let author = Author( + id: authorId, + attributes: .init(name: .init(value: "Janice Bluff")), + relationships: .none, + meta: .none, + links: .none + ) + authorInclude = .init(author) + } else { + authorInclude = nil + } + + return CompoundResource( + primary: article, + relatives: authorInclude.map { [$0] } ?? [] + ) +} + +func articleDocument(includeAuthor: Bool) -> SingleArticleDocument { + + let compoundResource = article(includeAuthor: includeAuthor) + + return SingleArticleDocument( + apiDescription: .none, + resource: compoundResource, + meta: .none, + links: .none + ) +} + +let encoder = JSONEncoder() +encoder.keyEncodingStrategy = .convertToSnakeCase +encoder.outputFormatting = .prettyPrinted + +let responseBody = articleDocument(includeAuthor: true) +let responseData = try! encoder.encode(responseBody) + +// Next step would be setting the HTTP body of a response. +// We will just print it out instead: +print("-----") +print(String(data: responseData, encoding: .utf8)!) + +// ... and if we had received a request for an article without +// including the author: +let otherResponseBody = articleDocument(includeAuthor: false) +let otherResponseData = try! encoder.encode(otherResponseBody) +print("-----") +print(String(data: otherResponseData, encoding: .utf8)!) +``` + +### Client Pseudo-example +```swift +enum NetworkError: Swift.Error { + case serverError + case quantityMismatch +} + +// Skipping over all the API stuff, here's a chunk of code that will +// decode a document. We will assume we have made a request for a +// single article including the author. +func docode(articleResponseData: Data) throws -> (article: Article, author: Author) { + let decoder = JSONDecoder() + decoder.keyDecodingStrategy = .convertFromSnakeCase + + let articleDocument = try decoder.decode(SingleArticleDocument.self, from: articleResponseData) + + switch articleDocument.body { + case .data(let data): + let authors = data.includes[Author.self] + + guard authors.count == 1 else { + throw NetworkError.quantityMismatch + } + + return (article: data.primary.value, author: authors[0]) + case .errors(let errors, meta: _, links: _): + throw NetworkError.serverError + } +} + +let response = try! docode(articleResponseData: responseData) + +// Next step would be to do something useful with the article and author but we will print them instead. +print("-----") +print(response.article) +print(response.author) +``` \ No newline at end of file diff --git a/documentation/project-status.md b/documentation/project-status.md new file mode 100644 index 0000000..6bf7358 --- /dev/null +++ b/documentation/project-status.md @@ -0,0 +1,46 @@ +## Project Status + +### JSON:API +#### Document +- [x] `data` +- [x] `included` +- [x] `errors` +- [x] `meta` +- [x] `jsonapi` (i.e. API Information) +- [x] `links` + +#### Resource Object +- [x] `id` +- [x] `type` +- [x] `attributes` +- [x] `relationships` +- [x] `links` +- [x] `meta` + +#### Relationship Object +- [x] `data` +- [x] `links` +- [x] `meta` + +#### Links Object +- [x] `href` +- [x] `meta` + +### Misc +- [x] Support transforms on `Attributes` values (e.g. to support different representations of `Date`) +- [x] Support validation on `Attributes`. +- [x] Support sparse fieldsets (encoding only). A client can likely just define a new model to represent a sparse population of another model in a very specific use case for decoding purposes. On the server side, sparse fieldsets of Resource Objects can be encoded without creating one model for every possible sparse fieldset. + +### Testing +#### Resource Object Validator +- [x] Disallow optional array in `Attribute` (should be empty array, not `null`). +- [x] Only allow `TransformedAttribute` and its derivatives as stored properties within `Attributes` struct. Computed properties can still be any type because they do not get encoded or decoded. +- [x] Only allow `MetaRelationship`, `ToManyRelationship` and `ToOneRelationship` within `Relationships` struct. + +### Potential Improvements +These ideas could be implemented in future versions. + +- [ ] (Maybe) Use `KeyPath` to specify `Includes` thus creating type safety around the relationship between a primary resource type and the types of included resources. +- [ ] (Maybe) Replace `SingleResourceBody` and `ManyResourceBody` with support at the `Document` level to just interpret `PrimaryResource`, `PrimaryResource?`, or `[PrimaryResource]` as the same decoding/encoding strategies. +- [ ] Support sideposting. JSONAPI spec might become opinionated in the future (https://github.com/json-api/json-api/pull/1197, https://github.com/json-api/json-api/issues/1215, https://github.com/json-api/json-api/issues/1216) but there is also an existing implementation to consider (https://jsonapi-suite.github.io/jsonapi_suite/ruby/writes/nested-writes). At this time, any sidepost implementation would be an awesome tertiary library to be used alongside the primary JSONAPI library. Maybe `JSONAPISideloading`. +- [ ] Error or warning if an included resource object is not related to a primary resource object or another included resource object (Turned off or at least not throwing by default). \ No newline at end of file diff --git a/documentation/usage.md b/documentation/usage.md index 056ce33..8c04481 100644 --- a/documentation/usage.md +++ b/documentation/usage.md @@ -1,53 +1,51 @@ -## Usage +## Usage In this documentation, in order to draw attention to the difference between the `JSONAPI` framework (this Swift library) and the **JSON API Spec** (the specification this library helps you follow), the specification will consistently be referred to below as simply the **SPEC**. - - - [`JSONAPI.ResourceObjectDescription`](#jsonapiresourceobjectdescription) - [`JSONAPI.ResourceObject`](#jsonapiresourceobject) - - [`Meta`](#meta) - - [`Links`](#links) - - [`MaybeRawId`](#mayberawid) - - [`RawIdType`](#rawidtype) - - [Convenient `typealiases`](#convenient-typealiases) + - [`Meta`](#meta) + - [`Links`](#links) + - [`MaybeRawId`](#mayberawid) + - [`RawIdType`](#rawidtype) + - [Convenient `typealiases`](#convenient-typealiases) - [`JSONAPI.Relationships`](#jsonapirelationships) - [`JSONAPI.Attributes`](#jsonapiattributes) - - [`Transformer`](#transformer) - - [`Validator`](#validator) - - [Computed `Attribute`](#computed-attribute) + - [`Transformer`](#transformer) + - [`Validator`](#validator) + - [Computed `Attribute`](#computed-attribute) - [Copying/Mutating `ResourceObjects`](#copyingmutating-resourceobjects) - [`JSONAPI.Document`](#jsonapidocument) - - [`ResourceBody`](#resourcebody) - - [nullable `PrimaryResource`](#nullable-primaryresource) - - [`MetaType`](#metatype) - - [`LinksType`](#linkstype) - - [`IncludeType`](#includetype) - - [`APIDescriptionType`](#apidescriptiontype) - - [`Error`](#error) - - [`UnknownJSONAPIError`](#unknownjsonapierror) - - [`BasicJSONAPIError`](#basicjsonapierror) - - [`GenericJSONAPIError`](#genericjsonapierror) + - [`ResourceBody`](#resourcebody) + - [nullable `PrimaryResource`](#nullable-primaryresource) + - [`MetaType`](#metatype) + - [`LinksType`](#linkstype) + - [`IncludeType`](#includetype) + - [`APIDescriptionType`](#apidescriptiontype) + - [`Error`](#error) + - [`UnknownJSONAPIError`](#unknownjsonapierror) + - [`BasicJSONAPIError`](#basicjsonapierror) + - [`GenericJSONAPIError`](#genericjsonapierror) + - [`SuccessDocument` and `ErrorDocument`](#successdocument-and-errordocument) +- [`CompoundResource`](#compoundresource) - [`JSONAPI.Meta`](#jsonapimeta) - [`JSONAPI.Links`](#jsonapilinks) - [`JSONAPI.RawIdType`](#jsonapirawidtype) - [Sparse Fieldsets](#sparse-fieldsets) - - [Supporting Sparse Fieldset Encoding](#supporting-sparse-fieldset-encoding) - - [Sparse Fieldset `typealias` comparisons](#sparse-fieldset-typealias-comparisons) + - [Supporting Sparse Fieldset Encoding](#supporting-sparse-fieldset-encoding) + - [Sparse Fieldset `typealias` comparisons](#sparse-fieldset-typealias-comparisons) - [Replacing and Tapping Attributes/Relationships](#replacing-and-tapping-attributesrelationships) - - [Tapping](#tapping) - - [Replacing](#replacing) + - [Tapping](#tapping) + - [Replacing](#replacing) - [Custom Attribute or Relationship Key Mapping](#custom-attribute-or-relationship-key-mapping) - [Custom Attribute Encode/Decode](#custom-attribute-encodedecode) - [Meta-Attributes](#meta-attributes) - [Meta-Relationships](#meta-relationships) - - ### `JSONAPI.ResourceObjectDescription` -A `ResourceObjectDescription` is the `JSONAPI` framework's representation of what the **SPEC** calls a *Resource Object*. You might create the following `ResourceObjectDescription` to represent a person in a network of friends: +A `ResourceObjectDescription` is the `JSONAPI` framework's definition of the attributes and relationships of what the **SPEC** calls a *Resource Object*. You might create the following `ResourceObjectDescription` to represent a person in a network of friends: ```swift enum PersonDescription: ResourceObjectDescription { @@ -65,11 +63,11 @@ enum PersonDescription: ResourceObjectDescription { ``` The requirements of a `ResourceObjectDescription` are: -1. A static `var` "jsonType" that matches the JSON type; The **SPEC** requires every *Resource Object* to have a "type". +1. A static `var` (or `let`) "jsonType" that matches the JSON type; The **SPEC** requires every *Resource Object* to have a "type". 2. A `struct` of `Attributes` **- OR -** `typealias Attributes = NoAttributes` 3. A `struct` of `Relationships` **- OR -** `typealias Relationships = NoRelationships` -Note that an `enum` type is used here for the `ResourceObjectDescription`; it could have been a `struct`, but `ResourceObjectDescription`s do not ever need to be created so an `enum` with no `case`s is a nice fit for the job. +Note that an `enum` type was used above for the `PersonDescription`; it could have been a `struct`, but `ResourceObjectDescriptions` do not ever need to be created so an `enum` with no `cases` is a nice fit for the job. This readme doesn't go into detail on the **SPEC**, but the following *Resource Object* would be described by the above `PersonDescription`: @@ -103,7 +101,7 @@ This readme doesn't go into detail on the **SPEC**, but the following *Resource ### `JSONAPI.ResourceObject` -Once you have a `ResourceObjectDescription`, you _create_, _encode_, and _decode_ `ResourceObjects` that "fit the description". If you have a `CreatableRawIdType` (see the section on `RawIdType`s below) then you can create new `ResourceObjects` that will automatically be given unique Ids, but even without a `CreatableRawIdType` you can encode, decode and work with resource objects. +Once you have a `ResourceObjectDescription`, you _create_, _encode_, and _decode_ `ResourceObjects` that "fit the description." If you have a `CreatableRawIdType` (see the section on `RawIdType`s below) then you can create new `ResourceObjects` that will automatically be given unique Ids, but even without a `CreatableRawIdType` you can encode, decode and work with resource objects. The `ResourceObject` and `ResourceObjectDescription` together with a `JSONAPI.Meta` type and a `JSONAPI.Links` type embody the rules and properties of a JSON API *Resource Object*. @@ -111,15 +109,15 @@ A `ResourceObject` needs to be specialized on four generic types. The first is t #### `Meta` -The second generic specialization on `ResourceObject` is `Meta`. This is described in its own section [below](#jsonapimeta). All `Meta` at any level of a JSON API Document follow the same rules. You can use `NoMetadata` if you do not need to package any metadata with the `ResourceObject`. +This is described in its own section [below](#jsonapimeta). All `Meta` at any level of a JSON API Document follow the same rules. You can use `NoMetadata` if you do not need to package any metadata with the `ResourceObject`. #### `Links` -The third generic specialization on `ResourceObject` is `Links`. This is described in its own section [below](#jsonnapilinks). All `Links` at any level of a JSON API Document follow the same rules, although the **SPEC** makes different suggestions as to what types of links might live on which parts of the Document. You can use `NoLinks` if you do not need to package any links with the `ResourceObject`. +This is described in its own section [below](#jsonnapilinks). All `Links` at any level of a JSON API Document follow the same rules, although the **SPEC** makes different suggestions as to what types of links might live on which parts of the Document. You can use `NoLinks` if you do not need to package any links with the `ResourceObject`. #### `MaybeRawId` -The last generic specialization on `ResourceObject` is `MaybeRawId`. This is either a `RawIdType` that can be used to uniquely identify `ResourceObjects` or it is `Unidentified` which is used to indicate a `ResourceObject` does not have an `Id` (which is useful when a client is requesting that the server create a `ResourceObject` and assign it a new `Id`). +The last generic specialization on `ResourceObject` is `MaybeRawId`. This is either a `RawIdType` that can be used to uniquely identify `ResourceObjects` or it is `Unidentified` which is used to indicate a `ResourceObject` does not have an `Id`; it is useful to create unidentified resources when a client is requesting that the server create a `ResourceObject` and assign it a new `Id`. ##### `RawIdType` @@ -131,7 +129,7 @@ A `RawIdType` is the underlying type that uniquely identifies a `ResourceObject` #### Convenient `typealiases` -Often you can use one `RawIdType` for many if not all of your `ResourceObjects`. That means you can save yourself some boilerplate by using `typealias`es like the following: +Often you can use one `RawIdType` for many if not all of your `ResourceObjects`. That means you can save yourself some boilerplate by using `typealiases` like the following: ```swift public typealias ResourceObject = JSONAPI.ResourceObject @@ -145,21 +143,23 @@ typealias Person = ResourceObject typealias NewPerson = NewResourceObject ``` -Note that I am calling an unidentified person is a "new" person. I suspect that is generally an acceptable conflation because the only time the **SPEC** allows a *Resource Object* to be encoded without an `Id` is when a client is requesting the given *Resource Object* be created by the server and the client wants the server to create the `Id` for that object. +Note that I am calling an unidentified person is a "new" person. This is generally an acceptable conflation in naming because the only time the **SPEC** allows a *Resource Object* to be encoded without an `Id` is when a client is requesting the given *Resource Object* be created by the server and the client wants the server to create the `Id` for that object. ### `JSONAPI.Relationships` There are three types of `Relationships`: `MetaRelationship`, `ToOneRelationship` and `ToManyRelationship`. A `ResourceObjectDescription`'s `Relationships` type can contain any number of `Relationship` properties of any of these types. Do not store anything other than `Relationship` properties in the `Relationships` struct of a `ResourceObjectDescription`. -In addition to identifying resource objects by Id and type, `Relationships` can contain `Meta` or `Links` that follow the same rules as [`Meta`](#jsonapimeta) and [`Links`](#jsonapilinks) elsewhere in the JSON API Document. +In addition to identifying resource objects by ID and type, `Relationships` can contain `Meta` or `Links` that follow the same rules as [`Meta`](#jsonapimeta) and [`Links`](#jsonapilinks) elsewhere in the JSON:API Document. The `MetaRelationship` is special in that it represents a Relationship Object with no `data` (it must contain at least one of `meta` or `links`). The other two relationship types are Relationship Objects with either singular resource linkages (`ToOneRelationship`) or arrays of resource linkages (`ToManyRelationship`). -To describe a relationship that may be omitted (i.e. the key is not even present in the JSON object), you make the entire `MetaRelationship`, `ToOneRelationship` or `ToManyRelationship` optional. However, this is not recommended because you can also represent optional relationships as nullable which means the key is always present. A `ToManyRelationship` can naturally represent the absence of related values with an empty array, so `ToManyRelationship` does not support nullability at all. A `ToOneRelationship` can be marked as nullable (i.e. the value could be either `null` or a resource identifier) like this: +To describe a relationship that may be omitted (i.e. the key is not even present in the JSON object), you make the entire `MetaRelationship`, `ToOneRelationship` or `ToManyRelationship` optional. A `ToOneRelationship` can be marked as nullable (i.e. the value could be either `null` or a resource identifier) like this: ```swift let nullableRelative: ToOneRelationship ``` +A `ToManyRelationship` can naturally represent the absence of related values with an empty array, so `ToManyRelationship` do not support nullability. + A `ResourceObject` that does not have relationships can be described by adding the following to a `ResourceObjectDescription`: ```swift typealias Relationships = NoRelationships @@ -377,6 +377,52 @@ case .errors(let errors, let meta, let links): ##### `GenericJSONAPIError` This type makes it simple to use your own error payload structures as `JSONAPIError` types. Simply define a `Codable` and `Equatable` struct and then use `GenericJSONAPIError` as the error type for a `Document`. +#### `SuccessDocument` and `ErrorDocument` +The `Document` type also supplies two nested types that guarantee either a successful data document or error an error document. + +In general, if you want to encode or decode a document you will want the flexibility of representing either success or errors. When you know you will be working with one or the other in a particular context, `Document.SuccessDocument` and `Document.ErrorDocument` will provide additional convenience: they only expose relevant initializers (a success document cannot be initialized with errors), they only succeed to decode given the expected result, and success documents provide non-optional access to the `data` property that is normally optional on the `body`. + +### `CompoundResource` +`CompoundResource` packages a primary resource with relatives (stored using the same `Include` types that `Document` uses). The `CompoundResource` type can be a convenient way to package a resource and its relatives to be later turned into a `Document`; A single resource body for a document is a straight forward representation of a `CompoundResource`, but `Document` will take an array of `CompoundResources` and create a batch ("many") resource body containing all the primary resources and uniquely including each relative as required by the **SPEC**. + +A single resource document: +```swift +typealias SinglePersonDocument = Document, NoMetadata, NoLinks, Include1, NoAPIDescription, BasicJSONAPIError> + +let person: Person = ... +let friends: [Person] = ... +let friendIncludes = friends.map(SinglePersonDocument.Include.init) + +let compoundResource = CompoundResource(primary: person, relatives: friendIncludes) + +let document = SinglePersonDocument( + apiDescription: .none, + resource: compoundResource, + meta: .none, + links: .none +) +``` + +A batch resource document: +```swift +typealias ManyPersonDocument = Document, NoMetadata, NoLinks, Include1, NoAPIDescription, BasicJSONAPIError> + +let people: [Person] = ... +let compoundResources = people.map { person in + let friends: [Person] = ... + let friendIncludes = friends.map(SinglePersonDocument.Include.init) + + return CompoundResource(primary: person, relatives: friendIncludes) +} + +let document = ManyPersonDocument( + apiDescription: .none, + resources: compoundResources, + meta: .none, + links: .none +) +``` + ### `JSONAPI.Meta` A `Meta` struct is totally open-ended. It is described by the **SPEC** as a place to put any information that does not fit into the standard JSON API Document structure anywhere else. From a604d57c8ffeafba9af95e6e56c13fb0096903c0 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Sun, 31 May 2020 15:14:22 -0700 Subject: [PATCH 165/235] link to new Google Colab example on resource storage. --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 6cda148..9a10fc4 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ See the JSON API Spec here: https://jsonapi.org/format/ ## Quick Start -:warning: The following Google Colab examples have correct code, but from time to time the Google Colab Swift compiler may be buggy and produce incorrect or erroneous results. Just keep that in mind if you run the code as you read through the Colab examples. +:warning: The following Google Colab examples have correct code, but from time to time the Google Colab Swift compiler may be buggy and claim it cannot build the JSONAPI library. ### Clientside - [Basic Example](https://colab.research.google.com/drive/1IS7lRSBGoiW02Vd1nN_rfdDbZvTDj6Te) @@ -15,7 +15,7 @@ See the JSON API Spec here: https://jsonapi.org/format/ - [Metadata Example](https://colab.research.google.com/drive/10dEESwiE9I3YoyfzVeOVwOKUTEgLT3qr) - [Custom Errors Example](https://colab.research.google.com/drive/1TIv6STzlHrkTf_-9Eu8sv8NoaxhZcFZH) - [PATCH Example](https://colab.research.google.com/drive/16KY-0BoLQKiSUh9G7nYmHzB8b2vhXA2U) - +- [Resource Storage Example](https://colab.research.google.com/drive/196eCnBlf2xz8pT4lW--ur6eWSVAjpF6b?usp=sharing) ### Serverside - [GET Example](https://colab.research.google.com/drive/1krbhzSfz8mwkBTQQnKUZJLEtYsJKSfYX) From b1780d6345018204f6760fb4f1b2b4ef278d1e7f Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Sun, 31 May 2020 15:21:00 -0700 Subject: [PATCH 166/235] Add example of using SuccessDocument to short circuit errors as decoding failures. --- documentation/usage.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/documentation/usage.md b/documentation/usage.md index 8c04481..65d48fe 100644 --- a/documentation/usage.md +++ b/documentation/usage.md @@ -382,6 +382,20 @@ The `Document` type also supplies two nested types that guarantee either a succe In general, if you want to encode or decode a document you will want the flexibility of representing either success or errors. When you know you will be working with one or the other in a particular context, `Document.SuccessDocument` and `Document.ErrorDocument` will provide additional convenience: they only expose relevant initializers (a success document cannot be initialized with errors), they only succeed to decode given the expected result, and success documents provide non-optional access to the `data` property that is normally optional on the `body`. +For example: +```swift +typealias Response = JSONAPI.Document<...> + +let decoder = JSONDecoder() +let document = try decoder.decode(Response.SuccessDocument.self, from: ...) + +// the following are non-optional because we know that if the document did not +// contain a `data` body (i.e. if it was an error response) then it would have +// failed to decode above. +let primaryResource = document.primaryResource +let includes = document.includes +``` + ### `CompoundResource` `CompoundResource` packages a primary resource with relatives (stored using the same `Include` types that `Document` uses). The `CompoundResource` type can be a convenient way to package a resource and its relatives to be later turned into a `Document`; A single resource body for a document is a straight forward representation of a `CompoundResource`, but `Document` will take an array of `CompoundResources` and create a batch ("many") resource body containing all the primary resources and uniquely including each relative as required by the **SPEC**. From 03136bff945c4ba96eba43d95db0d58ae3710178 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Sun, 31 May 2020 15:37:19 -0700 Subject: [PATCH 167/235] update podspec file for v 4 --- JSONAPI.podspec | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/JSONAPI.podspec b/JSONAPI.podspec index 6224429..12e6f28 100644 --- a/JSONAPI.podspec +++ b/JSONAPI.podspec @@ -16,7 +16,7 @@ Pod::Spec.new do |spec| # spec.name = "MP-JSONAPI" - spec.version = "3.0.0" + spec.version = "4.0.0" spec.summary = "Swift Codable JSON API framework." # This description is used to generate tags and improve search results. @@ -131,11 +131,11 @@ See the JSON API Spec here: https://jsonapi.org/format/ # where they will only apply to your library. If you depend on other Podspecs # you can include multiple dependencies to ensure it works. - spec.swift_version = "5.1" + spec.swift_version = "5.2" spec.module_name = "JSONAPI" # spec.requires_arc = true # spec.xcconfig = { "HEADER_SEARCH_PATHS" => "$(SDKROOT)/usr/include/libxml2" } - spec.dependency "Poly", "~> 2.3.1" + spec.dependency "Poly", "~> 2.4.0" end From 505c20739959f31080b4c0e1ffa971307261dea1 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Sun, 31 May 2020 23:26:26 -0700 Subject: [PATCH 168/235] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9a10fc4..9be080a 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ See the JSON API Spec here: https://jsonapi.org/format/ - [Metadata Example](https://colab.research.google.com/drive/10dEESwiE9I3YoyfzVeOVwOKUTEgLT3qr) - [Custom Errors Example](https://colab.research.google.com/drive/1TIv6STzlHrkTf_-9Eu8sv8NoaxhZcFZH) - [PATCH Example](https://colab.research.google.com/drive/16KY-0BoLQKiSUh9G7nYmHzB8b2vhXA2U) -- [Resource Storage Example](https://colab.research.google.com/drive/196eCnBlf2xz8pT4lW--ur6eWSVAjpF6b?usp=sharing) +- [Resource Storage Example](https://colab.research.google.com/drive/196eCnBlf2xz8pT4lW--ur6eWSVAjpF6b?usp=sharing) (using [JSONAPI-ResourceStorage](#jsonapi-resourcestorage) ### Serverside - [GET Example](https://colab.research.google.com/drive/1krbhzSfz8mwkBTQQnKUZJLEtYsJKSfYX) From 08d5069e4b902124c84745618916a8332a1aad6b Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Sun, 31 May 2020 23:26:39 -0700 Subject: [PATCH 169/235] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9be080a..5afe12a 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ See the JSON API Spec here: https://jsonapi.org/format/ - [Metadata Example](https://colab.research.google.com/drive/10dEESwiE9I3YoyfzVeOVwOKUTEgLT3qr) - [Custom Errors Example](https://colab.research.google.com/drive/1TIv6STzlHrkTf_-9Eu8sv8NoaxhZcFZH) - [PATCH Example](https://colab.research.google.com/drive/16KY-0BoLQKiSUh9G7nYmHzB8b2vhXA2U) -- [Resource Storage Example](https://colab.research.google.com/drive/196eCnBlf2xz8pT4lW--ur6eWSVAjpF6b?usp=sharing) (using [JSONAPI-ResourceStorage](#jsonapi-resourcestorage) +- [Resource Storage Example](https://colab.research.google.com/drive/196eCnBlf2xz8pT4lW--ur6eWSVAjpF6b?usp=sharing) (using [JSONAPI-ResourceStorage](#jsonapi-resourcestorage)) ### Serverside - [GET Example](https://colab.research.google.com/drive/1krbhzSfz8mwkBTQQnKUZJLEtYsJKSfYX) From 295d440b2d0d283c79835d2ba889e71a31e2d411 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Wed, 24 Jun 2020 18:23:04 -0700 Subject: [PATCH 170/235] ditch master nightly tests but add official 5.2 tests for focal, centos8, and amazonlinux and add nightlies for 5.3 --- .github/workflows/tests.yml | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index ea0bdd2..e72ab32 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -15,13 +15,16 @@ jobs: image: - swift:5.2-xenial - swift:5.2-bionic - - swiftlang/swift:nightly-master-focal - - swiftlang/swift:nightly-master-centos8 - - swiftlang/swift:nightly-master-amazonlinux2 + - swift:5.2-focal + - swift:5.2-centos8 + - swift:5.2-amazonlinux2 + - swiftlang/swift:nightly-5.3-xenial + - swiftlang/swift:nightly-5.3-bionic + - swiftlang/swift:nightly-5.3-focal + - swiftlang/swift:nightly-5.3-centos8 + - swiftlang/swift:nightly-5.3-amazonlinux2 container: ${{ matrix.image }} steps: - - name: Install dependencies if needed - run: ${{ matrix.depscmd }} - name: Checkout code uses: actions/checkout@v2 - name: Run tests From 05aba096fc1d0fd1cca1ad9e4f77ec67de969393 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Wed, 24 Jun 2020 18:28:24 -0700 Subject: [PATCH 171/235] remove unavailable images --- .github/workflows/tests.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index e72ab32..dd62f72 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -20,9 +20,6 @@ jobs: - swift:5.2-amazonlinux2 - swiftlang/swift:nightly-5.3-xenial - swiftlang/swift:nightly-5.3-bionic - - swiftlang/swift:nightly-5.3-focal - - swiftlang/swift:nightly-5.3-centos8 - - swiftlang/swift:nightly-5.3-amazonlinux2 container: ${{ matrix.image }} steps: - name: Checkout code From ad951a73737ea5a7610cc11795f3b566d9ca32c5 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Wed, 9 Sep 2020 07:13:02 -0700 Subject: [PATCH 172/235] Update Package.swift Switch to Swift Package Manager dependency URLs that contain the .git extension. This is not required by SwiftPM, but if a project does not consistently use the .git extension on dependency URLs (or not) then SwiftPM can get into trouble. The majority of open source projects use or suggest using the .git extension, so this project will standardize in that direction as well. --- Package.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index e83f775..3762356 100644 --- a/Package.swift +++ b/Package.swift @@ -17,7 +17,7 @@ let package = Package( targets: ["JSONAPITesting"]) ], dependencies: [ - .package(url: "https://github.com/mattpolzin/Poly", .upToNextMajor(from: "2.4.0")), + .package(url: "https://github.com/mattpolzin/Poly.git", .upToNextMajor(from: "2.4.0")), ], targets: [ .target( From f64ef95e23b9dfd1ac243a737f35ebd3fb69c0dd Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Wed, 9 Sep 2020 07:17:57 -0700 Subject: [PATCH 173/235] Update README.md update README to suggest the `.git` extension when pulling JSONAPI into a project via SPM. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5afe12a..c42e248 100644 --- a/README.md +++ b/README.md @@ -66,7 +66,7 @@ If you find something wrong with this library and it isn't already mentioned und ### Swift Package Manager Just include the following in your package's dependencies and add `JSONAPI` to the dependencies for any of your targets. ```swift -.package(url: "https://github.com/mattpolzin/JSONAPI", from: "4.0.0") +.package(url: "https://github.com/mattpolzin/JSONAPI.git", from: "4.0.0") ``` ### Xcode project From 641bf656a4736b4d60e307cc2b2f7c588a7b7161 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Fri, 18 Sep 2020 20:56:21 -0700 Subject: [PATCH 174/235] WIP adding id metadata to relationships. --- Package.resolved | 2 +- Sources/JSONAPI/Meta/Meta.swift | 7 + Sources/JSONAPI/Resource/Relationship.swift | 143 +++++++++++++++--- .../Resource Object/ResourceObject.swift | 16 +- .../JSONAPITesting/Relationship+Literal.swift | 10 +- .../Comparisons/DocumentCompareTests.swift | 8 +- .../Comparisons/IncludesCompareTests.swift | 8 +- .../RelationshipsCompareTests.swift | 28 ++-- .../ResourceObjectCompareTests.swift | 4 +- .../EntityCheckTests.swift | 2 +- .../Relationship+LiteralTests.swift | 8 +- .../ComputedPropertiesTests.swift | 4 +- .../Document/DocumentDecodingErrorTests.swift | 6 +- .../JSONAPITests/Document/DocumentTests.swift | 6 +- .../JSONAPITests/Includes/IncludeTests.swift | 8 +- .../Includes/IncludesDecodingErrorTests.swift | 4 +- .../NonJSONAPIRelatableTests.swift | 12 +- .../Relationships/RelationshipTests.swift | 63 ++++++-- .../stubs/RelationshipStubs.swift | 40 +++++ .../ResourceObject+ReplacingTests.swift | 4 +- .../ResourceObjectDecodingErrorTests.swift | 4 +- .../ResourceObject/ResourceObjectTests.swift | 26 ++-- 22 files changed, 293 insertions(+), 120 deletions(-) diff --git a/Package.resolved b/Package.resolved index 2900a5a..63465c7 100644 --- a/Package.resolved +++ b/Package.resolved @@ -3,7 +3,7 @@ "pins": [ { "package": "Poly", - "repositoryURL": "https://github.com/mattpolzin/Poly", + "repositoryURL": "https://github.com/mattpolzin/Poly.git", "state": { "branch": null, "revision": "36ba3f624bffa34f5f9b9c7648eab3cfdcab4748", diff --git a/Sources/JSONAPI/Meta/Meta.swift b/Sources/JSONAPI/Meta/Meta.swift index 57bac49..7018cd6 100644 --- a/Sources/JSONAPI/Meta/Meta.swift +++ b/Sources/JSONAPI/Meta/Meta.swift @@ -28,3 +28,10 @@ public struct NoMetadata: Meta, CustomStringConvertible { public var description: String { return "No Metadata" } } + +/// The type of metadata found in a Resource Identifier Object. +/// +/// It is sometimes more legible to differentiate between types of metadata +/// even when the underlying type is the same. This typealias is only here +/// to make code more easily understandable. +public typealias NoIdMetadata = NoMetadata diff --git a/Sources/JSONAPI/Resource/Relationship.swift b/Sources/JSONAPI/Resource/Relationship.swift index 88b54d5..e7c79f6 100644 --- a/Sources/JSONAPI/Resource/Relationship.swift +++ b/Sources/JSONAPI/Resource/Relationship.swift @@ -33,47 +33,69 @@ public struct MetaRelationship /// A `ResourceObject` relationship that can be encoded to or decoded from /// a JSON API "Resource Linkage." +/// /// See https://jsonapi.org/format/#document-resource-object-linkage +/// /// A convenient typealias might make your code much more legible: `One` -public struct ToOneRelationship: RelationshipType, Equatable { +/// +/// The `IdMetaType` (if not `NoIdMetadata`) will be parsed out of the Resource Identifier Object. +/// (see https://jsonapi.org/format/#document-resource-identifier-objects) +/// +/// The `MetaType` (if not `NoMetadata`) will be parsed out of the Relationship Object. +/// (see https://jsonapi.org/format/#document-resource-object-relationships) +public struct ToOneRelationship: RelationshipType, Equatable { public let id: Identifiable.ID + public let idMeta: IdMetaType + public let meta: MetaType public let links: LinksType - public init(id: Identifiable.ID, meta: MetaType, links: LinksType) { + public init(id: Identifiable.ID, meta: MetaType, links: LinksType) where IdMetaType == NoIdMetadata { self.id = id + self.idMeta = .none + self.meta = meta + self.links = links + } + + public init(id: (Identifiable.ID, IdMetaType), meta: MetaType, links: LinksType) { + self.id = id.0 + self.idMeta = id.1 self.meta = meta self.links = links } } extension ToOneRelationship where MetaType == NoMetadata, LinksType == NoLinks { - public init(id: Identifiable.ID) { + public init(id: Identifiable.ID) where IdMetaType == NoIdMetadata { + self.init(id: id, meta: .none, links: .none) + } + + public init(id: (Identifiable.ID, IdMetaType)) { self.init(id: id, meta: .none, links: .none) } } extension ToOneRelationship { - public init(resourceObject: T, meta: MetaType, links: LinksType) where T.Id == Identifiable.ID { + public init(resourceObject: T, meta: MetaType, links: LinksType) where T.Id == Identifiable.ID, IdMetaType == NoIdMetadata { self.init(id: resourceObject.id, meta: meta, links: links) } } -extension ToOneRelationship where MetaType == NoMetadata, LinksType == NoLinks { +extension ToOneRelationship where MetaType == NoMetadata, LinksType == NoLinks, IdMetaType == NoIdMetadata { public init(resourceObject: T) where T.Id == Identifiable.ID { self.init(id: resourceObject.id, meta: .none, links: .none) } } -extension ToOneRelationship where Identifiable: OptionalRelatable { +extension ToOneRelationship where Identifiable: OptionalRelatable, IdMetaType == NoIdMetadata { public init(resourceObject: T?, meta: MetaType, links: LinksType) where T.Id == Identifiable.Wrapped.ID { self.init(id: resourceObject?.id, meta: meta, links: links) } } -extension ToOneRelationship where Identifiable: OptionalRelatable, MetaType == NoMetadata, LinksType == NoLinks { +extension ToOneRelationship where Identifiable: OptionalRelatable, MetaType == NoMetadata, LinksType == NoLinks, IdMetaType == NoIdMetadata { public init(resourceObject: T?) where T.Id == Identifiable.Wrapped.ID { self.init(id: resourceObject?.id, meta: .none, links: .none) } @@ -81,33 +103,66 @@ extension ToOneRelationship where Identifiable: OptionalRelatable, MetaType == N /// An ResourceObject relationship that can be encoded to or decoded from /// a JSON API "Resource Linkage." +/// /// See https://jsonapi.org/format/#document-resource-object-linkage +/// /// A convenient typealias might make your code much more legible: `Many` -public struct ToManyRelationship: RelationshipType, Equatable { +/// +/// The `IdMetaType` (if not `NoIdMetadata`) will be parsed out of the Resource Identifier Object. +/// (see https://jsonapi.org/format/#document-resource-identifier-objects) +/// +/// The `MetaType` (if not `NoMetadata`) will be parsed out of the Relationship Object. +/// (see https://jsonapi.org/format/#document-resource-object-relationships) +public struct ToManyRelationship: RelationshipType, Equatable { + + public struct ID: Equatable { + public let id: Relatable.ID + public let meta: IdMetaType - public let ids: [Relatable.ID] + public init(id: Relatable.ID, meta: IdMetaType) { + self.id = id + self.meta = meta + } + + internal init(_ idPair: (Relatable.ID, IdMetaType)) { + self.init(id: idPair.0, meta: idPair.1) + } + } + + public let metaIds: [ID] + + public var ids: [Relatable.ID] { + metaIds.map(\.id) + } public let meta: MetaType public let links: LinksType - public init(ids: [Relatable.ID], meta: MetaType, links: LinksType) { - self.ids = ids + + public init(ids: [Relatable.ID], meta: MetaType, links: LinksType) where IdMetaType == NoIdMetadata { + self.metaIds = ids.map { .init(id: $0, meta: .none) } + self.meta = meta + self.links = links + } + + public init(idsWithMetadata ids: [(Relatable.ID, IdMetaType)], meta: MetaType, links: LinksType) { + self.metaIds = ids.map(ID.init) self.meta = meta self.links = links } - public init(pointers: [ToOneRelationship], meta: MetaType, links: LinksType) where T.ID == Relatable.ID { - ids = pointers.map(\.id) + public init(pointers: [ToOneRelationship], meta: MetaType, links: LinksType) where T.ID == Relatable.ID, IdMetaType == NoIdMetadata { + metaIds = pointers.map { .init(id: $0.id, meta: .none) } self.meta = meta self.links = links } - public init(resourceObjects: [T], meta: MetaType, links: LinksType) where T.Id == Relatable.ID { + public init(resourceObjects: [T], meta: MetaType, links: LinksType) where T.Id == Relatable.ID, IdMetaType == NoIdMetadata { self.init(ids: resourceObjects.map(\.id), meta: meta, links: links) } private init(meta: MetaType, links: LinksType) { - self.init(ids: [], meta: meta, links: links) + self.init(idsWithMetadata: [], meta: meta, links: links) } public static func none(withMeta meta: MetaType, links: LinksType) -> ToManyRelationship { @@ -117,11 +172,15 @@ public struct ToManyRelationship(pointers: [ToOneRelationship]) where T.ID == Relatable.ID { + public init(idsWithMetadata ids: [(Relatable.ID, IdMetaType)]) { + self.init(idsWithMetadata: ids, meta: .none, links: .none) + } + + public init(pointers: [ToOneRelationship]) where T.ID == Relatable.ID, IdMetaType == NoIdMetadata { self.init(pointers: pointers, meta: .none, links: .none) } @@ -129,7 +188,7 @@ extension ToManyRelationship where MetaType == NoMetadata, LinksType == NoLinks return .none(withMeta: .none, links: .none) } - public init(resourceObjects: [T]) where T.Id == Relatable.ID { + public init(resourceObjects: [T]) where T.Id == Relatable.ID, IdMetaType == NoIdMetadata { self.init(resourceObjects: resourceObjects, meta: .none, links: .none) } } @@ -164,6 +223,7 @@ private enum ResourceLinkageCodingKeys: String, CodingKey { private enum ResourceIdentifierCodingKeys: String, CodingKey { case id = "id" case entityType = "type" + case metadata = "meta" } extension MetaRelationship: Codable { @@ -228,6 +288,16 @@ extension ToOneRelationship: Codable where Identifiable.ID: OptionalId { ) ) } + guard let noIdMeta = NoIdMetadata() as? IdMetaType else { + throw DecodingError.valueNotFound( + Self.self, + DecodingError.Context( + codingPath: decoder.codingPath, + debugDescription: "Expected non-null relationship data with metadata inside." + ) + ) + } + idMeta = noIdMeta id = val return } @@ -256,6 +326,15 @@ extension ToOneRelationship: Codable where Identifiable.ID: OptionalId { ) } + let idMeta: IdMetaType + let maybeNoIdMeta: IdMetaType? = NoIdMetadata() as? IdMetaType + if let noIdMeta = maybeNoIdMeta { + idMeta = noIdMeta + } else { + idMeta = try identifier.decode(IdMetaType.self, forKey: .metadata) + } + self.idMeta = idMeta + id = Identifiable.ID(rawValue: try identifier.decode(Identifiable.ID.RawType.self, forKey: .id)) } @@ -282,6 +361,9 @@ extension ToOneRelationship: Codable where Identifiable.ID: OptionalId { var identifier = container.nestedContainer(keyedBy: ResourceIdentifierCodingKeys.self, forKey: .data) try identifier.encode(id.rawValue, forKey: .id) + if IdMetaType.self != NoMetadata.self { + try identifier.encode(idMeta, forKey: .metadata) + } try identifier.encode(Identifiable.jsonType, forKey: .entityType) } } @@ -311,10 +393,10 @@ extension ToManyRelationship: Codable { throw error } throw JSONAPICodingError.quantityMismatch(expected: .many, - path: context.codingPath) + path: context.codingPath) } - var newIds = [Relatable.ID]() + var newIds = [ID]() while !identifiers.isAtEnd { let identifier = try identifiers.nestedContainer(keyedBy: ResourceIdentifierCodingKeys.self) @@ -324,9 +406,19 @@ extension ToManyRelationship: Codable { throw JSONAPICodingError.typeMismatch(expected: Relatable.jsonType, found: type, path: decoder.codingPath) } - newIds.append(Relatable.ID(rawValue: try identifier.decode(Relatable.ID.RawType.self, forKey: .id))) + let id = try identifier.decode(Relatable.ID.RawType.self, forKey: .id) + + let idMeta: IdMetaType + let maybeNoIdMeta: IdMetaType? = NoIdMetadata() as? IdMetaType + if let noIdMeta = maybeNoIdMeta { + idMeta = noIdMeta + } else { + idMeta = try identifier.decode(IdMetaType.self, forKey: .metadata) + } + + newIds.append(.init(id: Relatable.ID(rawValue: id), meta: idMeta) ) } - ids = newIds + metaIds = newIds } public func encode(to encoder: Encoder) throws { @@ -342,10 +434,13 @@ extension ToManyRelationship: Codable { var identifiers = container.nestedUnkeyedContainer(forKey: .data) - for id in ids { + for id in metaIds { var identifier = identifiers.nestedContainer(keyedBy: ResourceIdentifierCodingKeys.self) - try identifier.encode(id.rawValue, forKey: .id) + try identifier.encode(id.id.rawValue, forKey: .id) + if IdMetaType.self != NoMetadata.self { + try identifier.encode(id.meta, forKey: .metadata) + } try identifier.encode(Relatable.jsonType, forKey: .entityType) } } diff --git a/Sources/JSONAPI/Resource/Resource Object/ResourceObject.swift b/Sources/JSONAPI/Resource/Resource Object/ResourceObject.swift index 9ac95d8..106f5ed 100644 --- a/Sources/JSONAPI/Resource/Resource Object/ResourceObject.swift +++ b/Sources/JSONAPI/Resource/Resource Object/ResourceObject.swift @@ -203,12 +203,12 @@ public extension ResourceObject where EntityRawIdType: JSONAPI.RawIdType { /// A `ResourceObject.Pointer` is a `ToOneRelationship` with no metadata or links. /// This is just a convenient way to reference a `ResourceObject` so that /// other ResourceObjects' Relationships can be built up from it. - typealias Pointer = ToOneRelationship + typealias Pointer = ToOneRelationship /// `ResourceObject.Pointers` is a `ToManyRelationship` with no metadata or links. /// This is just a convenient way to reference a bunch of ResourceObjects so /// that other ResourceObjects' Relationships can be built up from them. - typealias Pointers = ToManyRelationship + typealias Pointers = ToManyRelationship /// Get a pointer to this resource object that can be used as a /// relationship to another resource object. @@ -218,7 +218,7 @@ public extension ResourceObject where EntityRawIdType: JSONAPI.RawIdType { /// Get a pointer (i.e. `ToOneRelationship`) to this resource /// object with the given metadata and links attached. - func pointer(withMeta meta: MType, links: LType) -> ToOneRelationship { + func pointer(withMeta meta: MType, links: LType) -> ToOneRelationship { return ToOneRelationship(resourceObject: self, meta: meta, links: links) } } @@ -297,14 +297,14 @@ public extension ResourceObjectProxy { /// Access to an Id of a `ToOneRelationship`. /// This allows you to write `resourceObject ~> \.other` instead /// of `resourceObject.relationships.other.id`. - static func ~>(entity: Self, path: KeyPath>) -> OtherEntity.ID { + static func ~>(entity: Self, path: KeyPath>) -> OtherEntity.ID { return entity.relationships[keyPath: path].id } /// Access to an Id of an optional `ToOneRelationship`. /// This allows you to write `resourceObject ~> \.other` instead /// of `resourceObject.relationships.other?.id`. - static func ~>(entity: Self, path: KeyPath?>) -> OtherEntity.ID { + static func ~>(entity: Self, path: KeyPath?>) -> OtherEntity.ID { // Implementation Note: This signature applies to `ToOneRelationship?` // whereas the one below applies to `ToOneRelationship?` return entity.relationships[keyPath: path]?.id @@ -313,7 +313,7 @@ public extension ResourceObjectProxy { /// Access to an Id of an optional `ToOneRelationship`. /// This allows you to write `resourceObject ~> \.other` instead /// of `resourceObject.relationships.other?.id`. - static func ~>(entity: Self, path: KeyPath?>) -> OtherEntity.ID? { + static func ~>(entity: Self, path: KeyPath?>) -> OtherEntity.ID? { // Implementation Note: This signature applies to `ToOneRelationship?` // whereas the one above applies to `ToOneRelationship?` return entity.relationships[keyPath: path]?.id @@ -322,14 +322,14 @@ public extension ResourceObjectProxy { /// Access to all Ids of a `ToManyRelationship`. /// This allows you to write `resourceObject ~> \.others` instead /// of `resourceObject.relationships.others.ids`. - static func ~>(entity: Self, path: KeyPath>) -> [OtherEntity.ID] { + static func ~>(entity: Self, path: KeyPath>) -> [OtherEntity.ID] { return entity.relationships[keyPath: path].ids } /// Access to all Ids of an optional `ToManyRelationship`. /// This allows you to write `resourceObject ~> \.others` instead /// of `resourceObject.relationships.others?.ids`. - static func ~>(entity: Self, path: KeyPath?>) -> [OtherEntity.ID]? { + static func ~>(entity: Self, path: KeyPath?>) -> [OtherEntity.ID]? { return entity.relationships[keyPath: path]?.ids } } diff --git a/Sources/JSONAPITesting/Relationship+Literal.swift b/Sources/JSONAPITesting/Relationship+Literal.swift index 6b00511..c97ccd1 100644 --- a/Sources/JSONAPITesting/Relationship+Literal.swift +++ b/Sources/JSONAPITesting/Relationship+Literal.swift @@ -7,14 +7,14 @@ import JSONAPI -extension ToOneRelationship: ExpressibleByNilLiteral where Identifiable.ID: ExpressibleByNilLiteral, MetaType == NoMetadata, LinksType == NoLinks { +extension ToOneRelationship: ExpressibleByNilLiteral where Identifiable.ID: ExpressibleByNilLiteral, IdMetaType == NoIdMetadata, MetaType == NoMetadata, LinksType == NoLinks { public init(nilLiteral: ()) { self.init(id: Identifiable.ID(nilLiteral: ())) } } -extension ToOneRelationship: ExpressibleByUnicodeScalarLiteral where Identifiable.ID: ExpressibleByUnicodeScalarLiteral, MetaType == NoMetadata, LinksType == NoLinks { +extension ToOneRelationship: ExpressibleByUnicodeScalarLiteral where Identifiable.ID: ExpressibleByUnicodeScalarLiteral, IdMetaType == NoIdMetadata, MetaType == NoMetadata, LinksType == NoLinks { public typealias UnicodeScalarLiteralType = Identifiable.ID.UnicodeScalarLiteralType public init(unicodeScalarLiteral value: UnicodeScalarLiteralType) { @@ -22,7 +22,7 @@ extension ToOneRelationship: ExpressibleByUnicodeScalarLiteral where Identifiabl } } -extension ToOneRelationship: ExpressibleByExtendedGraphemeClusterLiteral where Identifiable.ID: ExpressibleByExtendedGraphemeClusterLiteral, MetaType == NoMetadata, LinksType == NoLinks { +extension ToOneRelationship: ExpressibleByExtendedGraphemeClusterLiteral where Identifiable.ID: ExpressibleByExtendedGraphemeClusterLiteral, IdMetaType == NoIdMetadata, MetaType == NoMetadata, LinksType == NoLinks { public typealias ExtendedGraphemeClusterLiteralType = Identifiable.ID.ExtendedGraphemeClusterLiteralType public init(extendedGraphemeClusterLiteral value: ExtendedGraphemeClusterLiteralType) { @@ -30,7 +30,7 @@ extension ToOneRelationship: ExpressibleByExtendedGraphemeClusterLiteral where I } } -extension ToOneRelationship: ExpressibleByStringLiteral where Identifiable.ID: ExpressibleByStringLiteral, MetaType == NoMetadata, LinksType == NoLinks { +extension ToOneRelationship: ExpressibleByStringLiteral where Identifiable.ID: ExpressibleByStringLiteral, IdMetaType == NoIdMetadata, MetaType == NoMetadata, LinksType == NoLinks { public typealias StringLiteralType = Identifiable.ID.StringLiteralType public init(stringLiteral value: StringLiteralType) { @@ -38,7 +38,7 @@ extension ToOneRelationship: ExpressibleByStringLiteral where Identifiable.ID: E } } -extension ToManyRelationship: ExpressibleByArrayLiteral where MetaType == NoMetadata, LinksType == NoLinks { +extension ToManyRelationship: ExpressibleByArrayLiteral where IdMetaType == NoIdMetadata, MetaType == NoMetadata, LinksType == NoLinks { public typealias ArrayLiteralElement = Relatable.ID public init(arrayLiteral elements: ArrayLiteralElement...) { diff --git a/Tests/JSONAPITestingTests/Comparisons/DocumentCompareTests.swift b/Tests/JSONAPITestingTests/Comparisons/DocumentCompareTests.swift index 8b464e6..dd05fe3 100644 --- a/Tests/JSONAPITestingTests/Comparisons/DocumentCompareTests.swift +++ b/Tests/JSONAPITestingTests/Comparisons/DocumentCompareTests.swift @@ -117,8 +117,8 @@ fileprivate enum TestDescription: JSONAPI.ResourceObjectDescription { } struct Relationships: JSONAPI.Relationships { - let bestFriend: ToOneRelationship - let parents: ToManyRelationship + let bestFriend: ToOneRelationship + let parents: ToManyRelationship } } @@ -134,8 +134,8 @@ fileprivate enum TestDescription2: JSONAPI.ResourceObjectDescription { } struct Relationships: JSONAPI.Relationships { - let bestFriend: ToOneRelationship - let parents: ToManyRelationship + let bestFriend: ToOneRelationship + let parents: ToManyRelationship } } diff --git a/Tests/JSONAPITestingTests/Comparisons/IncludesCompareTests.swift b/Tests/JSONAPITestingTests/Comparisons/IncludesCompareTests.swift index 7d34521..b25dd11 100644 --- a/Tests/JSONAPITestingTests/Comparisons/IncludesCompareTests.swift +++ b/Tests/JSONAPITestingTests/Comparisons/IncludesCompareTests.swift @@ -214,8 +214,8 @@ private enum TestDescription1: JSONAPI.ResourceObjectDescription { } struct Relationships: JSONAPI.Relationships { - let bestFriend: ToOneRelationship - let parents: ToManyRelationship + let bestFriend: ToOneRelationship + let parents: ToManyRelationship } } @@ -231,8 +231,8 @@ private enum TestDescription2: JSONAPI.ResourceObjectDescription { } struct Relationships: JSONAPI.Relationships { - let bestFriend: ToOneRelationship - let parents: ToManyRelationship + let bestFriend: ToOneRelationship + let parents: ToManyRelationship } } diff --git a/Tests/JSONAPITestingTests/Comparisons/RelationshipsCompareTests.swift b/Tests/JSONAPITestingTests/Comparisons/RelationshipsCompareTests.swift index 9c1bb8b..b747c5a 100644 --- a/Tests/JSONAPITestingTests/Comparisons/RelationshipsCompareTests.swift +++ b/Tests/JSONAPITestingTests/Comparisons/RelationshipsCompareTests.swift @@ -163,22 +163,22 @@ final class RelationshipsCompareTests: XCTestCase { ]) } - let t1 = ToOneRelationship(id: "123") - let t2 = ToOneRelationship(id: "456", meta: .init(hello: "world"), links: .init(link: .init(url: "http://google.com"))) - let t3 = ToManyRelationship(ids: ["123", "456"]) - let t4 = ToManyRelationship(ids: ["123", "456"], meta: .init(hello: "world"), links: .init(link: .init(url: "http://google.com"))) + let t1 = ToOneRelationship(id: "123") + let t2 = ToOneRelationship(id: "456", meta: .init(hello: "world"), links: .init(link: .init(url: "http://google.com"))) + let t3 = ToManyRelationship(ids: ["123", "456"]) + let t4 = ToManyRelationship(ids: ["123", "456"], meta: .init(hello: "world"), links: .init(link: .init(url: "http://google.com"))) let t5 = MetaRelationship(meta: .none, links: .init(link: .init(url: "http://google.com"))) let t6 = MetaRelationship(meta: .init(hello: "hi"), links: .none) let t7 = MetaRelationship(meta: .init(hello: "hi"), links: .init(link: .init(url: "http://google.com"))) - let t1_differentId = ToOneRelationship(id: "999") - let t3_differentId = ToManyRelationship(ids: ["999", "1010"]) + let t1_differentId = ToOneRelationship(id: "999") + let t3_differentId = ToManyRelationship(ids: ["999", "1010"]) - let t2_differentLinks = ToOneRelationship(id: "456", meta: .init(hello: "world"), links: .init(link: .init(url: "http://yahoo.com"))) - let t4_differentLinks = ToManyRelationship(ids: ["123", "456"], meta: .init(hello: "world"), links: .init(link: .init(url: "http://yahoo.com"))) + let t2_differentLinks = ToOneRelationship(id: "456", meta: .init(hello: "world"), links: .init(link: .init(url: "http://yahoo.com"))) + let t4_differentLinks = ToManyRelationship(ids: ["123", "456"], meta: .init(hello: "world"), links: .init(link: .init(url: "http://yahoo.com"))) - let t2_differentMeta = ToOneRelationship(id: "456", meta: .init(hello: "there"), links: .init(link: .init(url: "http://google.com"))) - let t4_differentMeta = ToManyRelationship(ids: ["123", "456"], meta: .init(hello: "there"), links: .init(link: .init(url: "http://google.com"))) + let t2_differentMeta = ToOneRelationship(id: "456", meta: .init(hello: "there"), links: .init(link: .init(url: "http://google.com"))) + let t4_differentMeta = ToManyRelationship(ids: ["123", "456"], meta: .init(hello: "there"), links: .init(link: .init(url: "http://google.com"))) let t5_differentLinks = MetaRelationship(meta: .none, links: .init(link: .init(url: "http://hi.com"))) let t6_differentMeta = MetaRelationship(meta: .init(hello: "there"), links: .none) @@ -213,10 +213,10 @@ extension RelationshipsCompareTests { } struct TestRelationships: JSONAPI.Relationships { - let a: ToOneRelationship? - let b: ToOneRelationship? - let c: ToManyRelationship? - let d: ToManyRelationship? + let a: ToOneRelationship? + let b: ToOneRelationship? + let c: ToManyRelationship? + let d: ToManyRelationship? let e: MetaRelationship? let f: MetaRelationship? let g: MetaRelationship? diff --git a/Tests/JSONAPITestingTests/Comparisons/ResourceObjectCompareTests.swift b/Tests/JSONAPITestingTests/Comparisons/ResourceObjectCompareTests.swift index 1f3a245..abb9cbd 100644 --- a/Tests/JSONAPITestingTests/Comparisons/ResourceObjectCompareTests.swift +++ b/Tests/JSONAPITestingTests/Comparisons/ResourceObjectCompareTests.swift @@ -146,8 +146,8 @@ private enum TestDescription: JSONAPI.ResourceObjectDescription { } struct Relationships: JSONAPI.Relationships { - let bestFriend: ToOneRelationship - let parents: ToManyRelationship + let bestFriend: ToOneRelationship + let parents: ToManyRelationship } } diff --git a/Tests/JSONAPITestingTests/EntityCheckTests.swift b/Tests/JSONAPITestingTests/EntityCheckTests.swift index 2710d83..861d0b7 100644 --- a/Tests/JSONAPITestingTests/EntityCheckTests.swift +++ b/Tests/JSONAPITestingTests/EntityCheckTests.swift @@ -115,7 +115,7 @@ extension EntityCheckTests { public typealias Attributes = NoAttributes public struct Relationships: JSONAPI.Relationships { - let x: ToOneRelationship + let x: ToOneRelationship let y: Id } } diff --git a/Tests/JSONAPITestingTests/Relationship+LiteralTests.swift b/Tests/JSONAPITestingTests/Relationship+LiteralTests.swift index 4dff645..e0753aa 100644 --- a/Tests/JSONAPITestingTests/Relationship+LiteralTests.swift +++ b/Tests/JSONAPITestingTests/Relationship+LiteralTests.swift @@ -12,16 +12,16 @@ import JSONAPITesting class Relationship_LiteralTests: XCTestCase { func test_NilLiteral() { - XCTAssertEqual(ToOneRelationship(id: nil), nil) + XCTAssertEqual(ToOneRelationship(id: nil), nil) } func test_ArrayLiteral() { - XCTAssertEqual(ToManyRelationship(ids: ["1", "2", "3"]), ["1", "2", "3"]) + XCTAssertEqual(ToManyRelationship(ids: ["1", "2", "3"]), ["1", "2", "3"]) } func test_StringLiteral() { - XCTAssertEqual(ToOneRelationship(id: "123"), "123") - XCTAssertEqual(ToOneRelationship(id: "123"), "123") + XCTAssertEqual(ToOneRelationship(id: "123"), "123") + XCTAssertEqual(ToOneRelationship(id: "123"), "123") } } diff --git a/Tests/JSONAPITests/Computed Properties/ComputedPropertiesTests.swift b/Tests/JSONAPITests/Computed Properties/ComputedPropertiesTests.swift index 006a560..5fca21c 100644 --- a/Tests/JSONAPITests/Computed Properties/ComputedPropertiesTests.swift +++ b/Tests/JSONAPITests/Computed Properties/ComputedPropertiesTests.swift @@ -66,9 +66,9 @@ extension ComputedPropertiesTests { } public struct Relationships: JSONAPI.Relationships { - public let other: ToOneRelationship + public let other: ToOneRelationship - public var computed: ToOneRelationship { + public var computed: ToOneRelationship { return other } } diff --git a/Tests/JSONAPITests/Document/DocumentDecodingErrorTests.swift b/Tests/JSONAPITests/Document/DocumentDecodingErrorTests.swift index 6fa2fb4..eb32a45 100644 --- a/Tests/JSONAPITests/Document/DocumentDecodingErrorTests.swift +++ b/Tests/JSONAPITests/Document/DocumentDecodingErrorTests.swift @@ -161,7 +161,7 @@ extension DocumentDecodingErrorTests { typealias Attributes = NoAttributes struct Relationships: JSONAPI.Relationships { - let author: ToOneRelationship + let author: ToOneRelationship } } @@ -179,8 +179,8 @@ extension DocumentDecodingErrorTests { } struct Relationships: JSONAPI.Relationships { - let author: ToOneRelationship - let series: ToManyRelationship + let author: ToOneRelationship + let series: ToManyRelationship } } diff --git a/Tests/JSONAPITests/Document/DocumentTests.swift b/Tests/JSONAPITests/Document/DocumentTests.swift index 081edc9..d6058ec 100644 --- a/Tests/JSONAPITests/Document/DocumentTests.swift +++ b/Tests/JSONAPITests/Document/DocumentTests.swift @@ -1575,7 +1575,7 @@ extension DocumentTests { typealias Attributes = NoAttributes struct Relationships: JSONAPI.Relationships { - let author: ToOneRelationship + let author: ToOneRelationship } } @@ -1593,8 +1593,8 @@ extension DocumentTests { } struct Relationships: JSONAPI.Relationships { - let author: ToOneRelationship - let series: ToManyRelationship + let author: ToOneRelationship + let series: ToManyRelationship let collection: MetaRelationship? } } diff --git a/Tests/JSONAPITests/Includes/IncludeTests.swift b/Tests/JSONAPITests/Includes/IncludeTests.swift index bed7d22..6da21e5 100644 --- a/Tests/JSONAPITests/Includes/IncludeTests.swift +++ b/Tests/JSONAPITests/Includes/IncludeTests.swift @@ -417,7 +417,7 @@ extension IncludedTests { public static var jsonType: String { return "test_entity2" } public struct Relationships: JSONAPI.Relationships { - let entity1: ToOneRelationship + let entity1: ToOneRelationship } public struct Attributes: JSONAPI.SparsableAttributes { @@ -440,8 +440,8 @@ extension IncludedTests { public static var jsonType: String { return "test_entity3" } public struct Relationships: JSONAPI.Relationships { - let entity1: ToOneRelationship - let entity2: ToManyRelationship + let entity1: ToOneRelationship + let entity2: ToManyRelationship } } @@ -476,7 +476,7 @@ extension IncludedTests { public static var jsonType: String { return "test_entity6" } struct Relationships: JSONAPI.Relationships { - let entity4: ToOneRelationship + let entity4: ToOneRelationship } } diff --git a/Tests/JSONAPITests/Includes/IncludesDecodingErrorTests.swift b/Tests/JSONAPITests/Includes/IncludesDecodingErrorTests.swift index 2db4f75..9b223d5 100644 --- a/Tests/JSONAPITests/Includes/IncludesDecodingErrorTests.swift +++ b/Tests/JSONAPITests/Includes/IncludesDecodingErrorTests.swift @@ -67,7 +67,7 @@ extension IncludesDecodingErrorTests { public static var jsonType: String { return "test_entity2" } public struct Relationships: JSONAPI.Relationships { - let entity1: ToOneRelationship + let entity1: ToOneRelationship } public struct Attributes: JSONAPI.SparsableAttributes { @@ -101,7 +101,7 @@ extension IncludesDecodingErrorTests { public static var jsonType: String { return "test_entity6" } struct Relationships: JSONAPI.Relationships { - let entity4: ToOneRelationship + let entity4: ToOneRelationship } } diff --git a/Tests/JSONAPITests/NonJSONAPIRelatable/NonJSONAPIRelatableTests.swift b/Tests/JSONAPITests/NonJSONAPIRelatable/NonJSONAPIRelatableTests.swift index 30e14a8..2a38cba 100644 --- a/Tests/JSONAPITests/NonJSONAPIRelatable/NonJSONAPIRelatableTests.swift +++ b/Tests/JSONAPITests/NonJSONAPIRelatable/NonJSONAPIRelatableTests.swift @@ -58,8 +58,8 @@ extension NonJSONAPIRelatableTests { typealias Attributes = NoAttributes struct Relationships: JSONAPI.Relationships { - let one: ToOneRelationship - let many: ToManyRelationship + let one: ToOneRelationship + let many: ToManyRelationship } } @@ -71,10 +71,10 @@ extension NonJSONAPIRelatableTests { typealias Attributes = NoAttributes struct Relationships: JSONAPI.Relationships { - let nullableOne: ToOneRelationship - let nullableMaybeOne: ToOneRelationship? - let maybeOne: ToOneRelationship? - let maybeMany: ToManyRelationship? + let nullableOne: ToOneRelationship + let nullableMaybeOne: ToOneRelationship? + let maybeOne: ToOneRelationship? + let maybeMany: ToManyRelationship? } } diff --git a/Tests/JSONAPITests/Relationships/RelationshipTests.swift b/Tests/JSONAPITests/Relationships/RelationshipTests.swift index 80f9820..bf804c9 100644 --- a/Tests/JSONAPITests/Relationships/RelationshipTests.swift +++ b/Tests/JSONAPITests/Relationships/RelationshipTests.swift @@ -15,7 +15,7 @@ class RelationshipTests: XCTestCase { let entity2 = TestEntity1(attributes: .none, relationships: .none, meta: .none, links: .none) let entity3 = TestEntity1(attributes: .none, relationships: .none, meta: .none, links: .none) let entity4 = TestEntity1(attributes: .none, relationships: .none, meta: .none, links: .none) - let relationship = ToManyRelationship(resourceObjects: [entity1, entity2, entity3, entity4]) + let relationship = ToManyRelationship(resourceObjects: [entity1, entity2, entity3, entity4]) XCTAssertEqual(relationship.ids.count, 4) XCTAssertEqual(relationship.ids, [entity1, entity2, entity3, entity4].map(\.id)) @@ -26,7 +26,7 @@ class RelationshipTests: XCTestCase { let entity2 = TestEntity1(attributes: .none, relationships: .none, meta: .none, links: .none) let entity3 = TestEntity1(attributes: .none, relationships: .none, meta: .none, links: .none) let entity4 = TestEntity1(attributes: .none, relationships: .none, meta: .none, links: .none) - let relationship = ToManyRelationship(pointers: [entity1.pointer, entity2.pointer, entity3.pointer, entity4.pointer]) + let relationship = ToManyRelationship(pointers: [entity1.pointer, entity2.pointer, entity3.pointer, entity4.pointer]) XCTAssertEqual(relationship.ids.count, 4) XCTAssertEqual(relationship.ids, [entity1, entity2, entity3, entity4].map(\.id)) @@ -87,14 +87,14 @@ extension RelationshipTests { } func test_ToOneRelationship() { - let relationship = decoded(type: ToOneRelationship.self, + let relationship = decoded(type: ToOneRelationship.self, data: to_one_relationship) XCTAssertEqual(relationship.id.rawValue, "2DF03B69-4B0A-467F-B52E-B0C9E44FCECF") } func test_ToOneRelationship_encode() { - test_DecodeEncodeEquality(type: ToOneRelationship.self, + test_DecodeEncodeEquality(type: ToOneRelationship.self, data: to_one_relationship) } @@ -111,6 +111,19 @@ extension RelationshipTests { data: to_one_relationship_with_meta) } + func test_ToOneRelationshipWithMetaInsideIdentifier() { + let relationship = decoded(type: ToOneWithMetaInIds.self, + data: to_one_relationship_with_meta_inside_identifier) + + XCTAssertEqual(relationship.id.rawValue, "2DF03B69-4B0A-467F-B52E-B0C9E44FCECF") + XCTAssertEqual(relationship.idMeta.a, "hello") + } + + func test_ToOneRelationshipWithMetaInsideIdentifier_encode() { + test_DecodeEncodeEquality(type: ToOneWithMetaInIds.self, + data: to_one_relationship_with_meta_inside_identifier) + } + func test_ToOneRelationshipWithLinks() { let relationship = decoded(type: ToOneWithLinks.self, data: to_one_relationship_with_links) @@ -139,14 +152,14 @@ extension RelationshipTests { } func test_ToManyRelationship() { - let relationship = decoded(type: ToManyRelationship.self, + let relationship = decoded(type: ToManyRelationship.self, data: to_many_relationship) XCTAssertEqual(relationship.ids.map(\.rawValue), ["2DF03B69-4B0A-467F-B52E-B0C9E44FCECF", "90F03B69-4DF1-467F-B52E-B0C9E44FC333", "2DF03B69-4B0A-467F-B52E-B0C9E44FCECF"]) } func test_ToManyRelationship_encode() { - test_DecodeEncodeEquality(type: ToManyRelationship.self, + test_DecodeEncodeEquality(type: ToManyRelationship.self, data: to_many_relationship) } @@ -163,6 +176,19 @@ extension RelationshipTests { data: to_many_relationship_with_meta) } + func test_ToManyRelationshipWithMetaInsideIdentifier() { + let relationship = decoded(type: ToManyWithMetaInIds.self, + data: to_many_relationship_with_meta_inside_identifier) + + XCTAssertEqual(relationship.ids.map(\.rawValue), ["2DF03B69-4B0A-467F-B52E-B0C9E44FCECF", "90F03B69-4DF1-467F-B52E-B0C9E44FC333", "2DF03B69-4B0A-467F-B52E-B0C9E44FCECF"]) + XCTAssertEqual(relationship.metaIds[0].meta.a, "hello") + } + + func test_ToManyRelationshipWithMetaInsideIdentifier_encode() { + test_DecodeEncodeEquality(type: ToManyWithMetaInIds.self, + data: to_many_relationship_with_meta_inside_identifier) + } + func test_ToManyRelationshipWithLinks() { let relationship = decoded(type: ToManyWithLinks.self, data: to_many_relationship_with_links) @@ -213,11 +239,11 @@ extension RelationshipTests { // MARK: Failure tests extension RelationshipTests { func test_ToManyTypeMismatch() { - XCTAssertThrowsError(try JSONDecoder().decode(ToManyRelationship.self, from: to_many_relationship_type_mismatch)) + XCTAssertThrowsError(try JSONDecoder().decode(ToManyRelationship.self, from: to_many_relationship_type_mismatch)) } func test_ToOneTypeMismatch() { - XCTAssertThrowsError(try JSONDecoder().decode(ToOneRelationship.self, from: to_one_relationship_type_mismatch)) + XCTAssertThrowsError(try JSONDecoder().decode(ToOneRelationship.self, from: to_one_relationship_type_mismatch)) } } @@ -233,17 +259,22 @@ extension RelationshipTests { typealias TestEntity1 = BasicEntity - typealias ToOneWithMeta = ToOneRelationship + typealias ToOneWithMeta = ToOneRelationship + typealias ToOneWithMetaInIds = ToOneRelationship + + typealias ToOneWithLinks = ToOneRelationship + + typealias ToOneWithMetaAndLinks = ToOneRelationship + + typealias ToManyWithMeta = ToManyRelationship + typealias ToManyWithMetaInIds = ToManyRelationship - typealias ToOneWithLinks = ToOneRelationship - typealias ToOneWithMetaAndLinks = ToOneRelationship + typealias ToManyWithLinks = ToManyRelationship - typealias ToManyWithMeta = ToManyRelationship - typealias ToManyWithLinks = ToManyRelationship - typealias ToManyWithMetaAndLinks = ToManyRelationship + typealias ToManyWithMetaAndLinks = ToManyRelationship - typealias ToOneNullable = ToOneRelationship - typealias ToOneNonNullable = ToOneRelationship + typealias ToOneNullable = ToOneRelationship + typealias ToOneNonNullable = ToOneRelationship struct TestMeta: JSONAPI.Meta { let a: String diff --git a/Tests/JSONAPITests/Relationships/stubs/RelationshipStubs.swift b/Tests/JSONAPITests/Relationships/stubs/RelationshipStubs.swift index fcc552a..e0c4a8c 100644 --- a/Tests/JSONAPITests/Relationships/stubs/RelationshipStubs.swift +++ b/Tests/JSONAPITests/Relationships/stubs/RelationshipStubs.swift @@ -62,6 +62,18 @@ let to_one_relationship_with_meta = """ } """.data(using: .utf8)! +let to_one_relationship_with_meta_inside_identifier = """ +{ + "data": { + "type": "test_entity1", + "id": "2DF03B69-4B0A-467F-B52E-B0C9E44FCECF", + "meta": { + "a": "hello" + } + } +} +""".data(using: .utf8)! + let to_one_relationship_with_links = """ { "data": { @@ -130,6 +142,34 @@ let to_many_relationship_with_meta = """ } """.data(using: .utf8)! +let to_many_relationship_with_meta_inside_identifier = """ +{ + "data": [ + { + "type": "test_entity1", + "id": "2DF03B69-4B0A-467F-B52E-B0C9E44FCECF", + "meta": { + "a": "hello" + } + }, + { + "type": "test_entity1", + "id": "90F03B69-4DF1-467F-B52E-B0C9E44FC333", + "meta": { + "a": "hello" + } + }, + { + "type": "test_entity1", + "id": "2DF03B69-4B0A-467F-B52E-B0C9E44FCECF", + "meta": { + "a": "hello" + } + } + ] +} +""".data(using: .utf8)! + let to_many_relationship_with_links = """ { "data": [ diff --git a/Tests/JSONAPITests/ResourceObject/ResourceObject+ReplacingTests.swift b/Tests/JSONAPITests/ResourceObject/ResourceObject+ReplacingTests.swift index 337a5b6..4a5fffa 100644 --- a/Tests/JSONAPITests/ResourceObject/ResourceObject+ReplacingTests.swift +++ b/Tests/JSONAPITests/ResourceObject/ResourceObject+ReplacingTests.swift @@ -134,7 +134,7 @@ private enum MutableTestDescription: JSONAPI.ResourceObjectDescription { } struct Relationships: JSONAPI.Relationships { - var other: ToOneRelationship + var other: ToOneRelationship } } @@ -148,7 +148,7 @@ private enum ImmutableTestDescription: JSONAPI.ResourceObjectDescription { } struct Relationships: JSONAPI.Relationships { - let other: ToOneRelationship + let other: ToOneRelationship } } diff --git a/Tests/JSONAPITests/ResourceObject/ResourceObjectDecodingErrorTests.swift b/Tests/JSONAPITests/ResourceObject/ResourceObjectDecodingErrorTests.swift index bf93ec3..3e290e3 100644 --- a/Tests/JSONAPITests/ResourceObject/ResourceObjectDecodingErrorTests.swift +++ b/Tests/JSONAPITests/ResourceObject/ResourceObjectDecodingErrorTests.swift @@ -457,8 +457,8 @@ extension ResourceObjectDecodingErrorTests { public struct Relationships: JSONAPI.Relationships { - let required: ToOneRelationship - let omittable: ToManyRelationship? + let required: ToOneRelationship + let omittable: ToManyRelationship? } } diff --git a/Tests/JSONAPITests/ResourceObject/ResourceObjectTests.swift b/Tests/JSONAPITests/ResourceObject/ResourceObjectTests.swift index b7b6dca..6d3fb4f 100644 --- a/Tests/JSONAPITests/ResourceObject/ResourceObjectTests.swift +++ b/Tests/JSONAPITests/ResourceObject/ResourceObjectTests.swift @@ -687,7 +687,7 @@ extension ResourceObjectTests { typealias Attributes = NoAttributes struct Relationships: JSONAPI.Relationships { - let other: ToOneRelationship + let other: ToOneRelationship } } @@ -699,7 +699,7 @@ extension ResourceObjectTests { typealias Attributes = NoAttributes struct Relationships: JSONAPI.Relationships { - let others: ToManyRelationship + let others: ToManyRelationship } } @@ -709,7 +709,7 @@ extension ResourceObjectTests { static var jsonType: String { return "fourth_test_entities"} struct Relationships: JSONAPI.Relationships { - let other: ToOneRelationship + let other: ToOneRelationship } struct Attributes: JSONAPI.Attributes { @@ -794,15 +794,15 @@ extension ResourceObjectTests { let optionalMeta: MetaRelationship? - let one: ToOneRelationship + let one: ToOneRelationship - let nullableOne: ToOneRelationship + let nullableOne: ToOneRelationship - let optionalOne: ToOneRelationship? + let optionalOne: ToOneRelationship? - let optionalNullableOne: ToOneRelationship? + let optionalNullableOne: ToOneRelationship? - let optionalMany: ToManyRelationship? + let optionalMany: ToManyRelationship? // a nullable many is not allowed. it should // just be an empty array. @@ -817,8 +817,8 @@ extension ResourceObjectTests { typealias Attributes = NoAttributes public struct Relationships: JSONAPI.Relationships { - let selfRef: ToOneRelationship - let selfRefs: ToManyRelationship + let selfRef: ToOneRelationship + let selfRefs: ToManyRelationship } } @@ -851,11 +851,11 @@ extension ResourceObjectTests { let optionalMeta: MetaRelationship? - let optionalOne: ToOneRelationship? + let optionalOne: ToOneRelationship? - let optionalNullableOne: ToOneRelationship? + let optionalNullableOne: ToOneRelationship? - let optionalMany: ToManyRelationship? + let optionalMany: ToManyRelationship? } } From cea702bee3ac03b35bf9048c4edd957cb72d09b2 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Fri, 18 Sep 2020 21:17:24 -0700 Subject: [PATCH 175/235] don't use Swift 5.3 language features --- Sources/JSONAPI/Resource/Relationship.swift | 45 ++++++++++++--------- 1 file changed, 26 insertions(+), 19 deletions(-) diff --git a/Sources/JSONAPI/Resource/Relationship.swift b/Sources/JSONAPI/Resource/Relationship.swift index e7c79f6..b21ab7e 100644 --- a/Sources/JSONAPI/Resource/Relationship.swift +++ b/Sources/JSONAPI/Resource/Relationship.swift @@ -52,27 +52,31 @@ public struct ToOneRelationship Date: Fri, 18 Sep 2020 21:19:38 -0700 Subject: [PATCH 176/235] switch to official swift 5.3 docker images. --- .github/workflows/tests.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index dd62f72..0665ea8 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -18,8 +18,11 @@ jobs: - swift:5.2-focal - swift:5.2-centos8 - swift:5.2-amazonlinux2 - - swiftlang/swift:nightly-5.3-xenial - - swiftlang/swift:nightly-5.3-bionic + - swift:5.3-xenial + - swift:5.3-bionic + - swift:5.3-focal + - swift:5.3-centos8 + - swift:5.3-amazonlinux2 container: ${{ matrix.image }} steps: - name: Checkout code From 2d88ef3d093e4830bf53ac8d4405ac1b8d792305 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Fri, 18 Sep 2020 21:23:06 -0700 Subject: [PATCH 177/235] apparently swift 5.2 compiler found this ambiguous somewhere --- Sources/JSONAPI/Resource/Relationship.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/JSONAPI/Resource/Relationship.swift b/Sources/JSONAPI/Resource/Relationship.swift index b21ab7e..264f1f1 100644 --- a/Sources/JSONAPI/Resource/Relationship.swift +++ b/Sources/JSONAPI/Resource/Relationship.swift @@ -143,7 +143,7 @@ public struct ToManyRelationship Date: Sat, 19 Sep 2020 19:11:38 -0700 Subject: [PATCH 178/235] fixing up documentation and code samples --- .../Contents.swift | 4 ++-- .../Contents.swift | 6 +++--- JSONAPI.playground/Sources/Entities.swift | 4 ++-- documentation/client-server-example.md | 6 +++--- documentation/project-status.md | 7 ++++++- documentation/usage.md | 13 ++++++++++--- 6 files changed, 26 insertions(+), 14 deletions(-) diff --git a/JSONAPI.playground/Pages/Full Client & Server Example.xcplaygroundpage/Contents.swift b/JSONAPI.playground/Pages/Full Client & Server Example.xcplaygroundpage/Contents.swift index 1bfdde5..8e633ef 100644 --- a/JSONAPI.playground/Pages/Full Client & Server Example.xcplaygroundpage/Contents.swift +++ b/JSONAPI.playground/Pages/Full Client & Server Example.xcplaygroundpage/Contents.swift @@ -30,8 +30,8 @@ typealias UnidentifiedJSONEntity = JSONA // Create relationship typealiases because we do not expect // JSON:API Relationships for this particular API to have // Metadata or Links associated with them. -typealias ToOneRelationship = JSONAPI.ToOneRelationship -typealias ToManyRelationship = JSONAPI.ToManyRelationship +typealias ToOneRelationship = JSONAPI.ToOneRelationship +typealias ToManyRelationship = JSONAPI.ToManyRelationship // Create a typealias for a Document because we do not expect // JSON:API Documents for this particular API to have Metadata, Links, diff --git a/JSONAPI.playground/Pages/Full Document Verbose Generation.xcplaygroundpage/Contents.swift b/JSONAPI.playground/Pages/Full Document Verbose Generation.xcplaygroundpage/Contents.swift index 678296e..e06dfa2 100644 --- a/JSONAPI.playground/Pages/Full Document Verbose Generation.xcplaygroundpage/Contents.swift +++ b/JSONAPI.playground/Pages/Full Document Verbose Generation.xcplaygroundpage/Contents.swift @@ -72,7 +72,7 @@ enum AuthorDescription: ResourceObjectDescription { } struct Relationships: JSONAPI.Relationships { - let articles: ToManyRelationship + let articles: ToManyRelationship } } @@ -88,11 +88,11 @@ enum ArticleDescription: ResourceObjectDescription { struct Relationships: JSONAPI.Relationships { /// The primary attributed author of the article. - let primaryAuthor: ToOneRelationship + let primaryAuthor: ToOneRelationship /// All authors excluding the primary author. /// It is customary to print the primary author's /// name first, followed by the other authors. - let otherAuthors: ToManyRelationship + let otherAuthors: ToManyRelationship } } diff --git a/JSONAPI.playground/Sources/Entities.swift b/JSONAPI.playground/Sources/Entities.swift index e64cfbb..023c9b1 100644 --- a/JSONAPI.playground/Sources/Entities.swift +++ b/JSONAPI.playground/Sources/Entities.swift @@ -25,8 +25,8 @@ extension String: CreatableRawIdType { // MARK: - typealiases for convenience public typealias ExampleEntity = ResourceObject -public typealias ToOne = ToOneRelationship -public typealias ToMany = ToManyRelationship +public typealias ToOne = ToOneRelationship +public typealias ToMany = ToManyRelationship // MARK: - A few resource objects (entities) public enum PersonDescription: ResourceObjectDescription { diff --git a/documentation/client-server-example.md b/documentation/client-server-example.md index 87d2205..5bf5d83 100644 --- a/documentation/client-server-example.md +++ b/documentation/client-server-example.md @@ -26,8 +26,8 @@ typealias UnidentifiedJSONEntity = JSONA // Create relationship typealiases because we do not expect // JSON:API Relationships for this particular API to have // Metadata or Links associated with them. -typealias ToOneRelationship = JSONAPI.ToOneRelationship -typealias ToManyRelationship = JSONAPI.ToManyRelationship +typealias ToOneRelationship = JSONAPI.ToOneRelationship +typealias ToManyRelationship = JSONAPI.ToManyRelationship // Create a typealias for a Document because we do not expect // JSON:API Documents for this particular API to have Metadata, Links, @@ -183,4 +183,4 @@ let response = try! docode(articleResponseData: responseData) print("-----") print(response.article) print(response.author) -``` \ No newline at end of file +``` diff --git a/documentation/project-status.md b/documentation/project-status.md index 6bf7358..1760289 100644 --- a/documentation/project-status.md +++ b/documentation/project-status.md @@ -22,6 +22,11 @@ - [x] `links` - [x] `meta` +##### Resource Identifier Object +- [x] `id` +- [x] `type` +- [x] `meta` + #### Links Object - [x] `href` - [x] `meta` @@ -43,4 +48,4 @@ These ideas could be implemented in future versions. - [ ] (Maybe) Use `KeyPath` to specify `Includes` thus creating type safety around the relationship between a primary resource type and the types of included resources. - [ ] (Maybe) Replace `SingleResourceBody` and `ManyResourceBody` with support at the `Document` level to just interpret `PrimaryResource`, `PrimaryResource?`, or `[PrimaryResource]` as the same decoding/encoding strategies. - [ ] Support sideposting. JSONAPI spec might become opinionated in the future (https://github.com/json-api/json-api/pull/1197, https://github.com/json-api/json-api/issues/1215, https://github.com/json-api/json-api/issues/1216) but there is also an existing implementation to consider (https://jsonapi-suite.github.io/jsonapi_suite/ruby/writes/nested-writes). At this time, any sidepost implementation would be an awesome tertiary library to be used alongside the primary JSONAPI library. Maybe `JSONAPISideloading`. -- [ ] Error or warning if an included resource object is not related to a primary resource object or another included resource object (Turned off or at least not throwing by default). \ No newline at end of file +- [ ] Error or warning if an included resource object is not related to a primary resource object or another included resource object (Turned off or at least not throwing by default). diff --git a/documentation/usage.md b/documentation/usage.md index 65d48fe..ba63c87 100644 --- a/documentation/usage.md +++ b/documentation/usage.md @@ -57,7 +57,7 @@ enum PersonDescription: ResourceObjectDescription { } struct Relationships: JSONAPI.Relationships { - let friends: ToManyRelationship + let friends: ToManyRelationship } } ``` @@ -153,9 +153,16 @@ In addition to identifying resource objects by ID and type, `Relationships` can The `MetaRelationship` is special in that it represents a Relationship Object with no `data` (it must contain at least one of `meta` or `links`). The other two relationship types are Relationship Objects with either singular resource linkages (`ToOneRelationship`) or arrays of resource linkages (`ToManyRelationship`). -To describe a relationship that may be omitted (i.e. the key is not even present in the JSON object), you make the entire `MetaRelationship`, `ToOneRelationship` or `ToManyRelationship` optional. A `ToOneRelationship` can be marked as nullable (i.e. the value could be either `null` or a resource identifier) like this: +To describe a relationship that may be omitted (i.e. the key is not even present in the JSON object), you make the entire `MetaRelationship`, `ToOneRelationship` or `ToManyRelationship` optional. ```swift -let nullableRelative: ToOneRelationship +// note the question mark at the very end of the line. +let optionalRelative: ToOneRelationship? +``` + +A `ToOneRelationship` can be marked as nullable (i.e. the value could be either `null` or a resource identifier) like this: +```swift +// note the question mark just after `Person`. +let nullableRelative: ToOneRelationship ``` A `ToManyRelationship` can naturally represent the absence of related values with an empty array, so `ToManyRelationship` do not support nullability. From 27604d4ff7ccd972a7744de8a44a26d4551dde1a Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Sat, 19 Sep 2020 19:21:56 -0700 Subject: [PATCH 179/235] a bit more documentation on relationship metadata options --- documentation/usage.md | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/documentation/usage.md b/documentation/usage.md index ba63c87..5dc298f 100644 --- a/documentation/usage.md +++ b/documentation/usage.md @@ -151,6 +151,41 @@ There are three types of `Relationships`: `MetaRelationship`, `ToOneRelationship In addition to identifying resource objects by ID and type, `Relationships` can contain `Meta` or `Links` that follow the same rules as [`Meta`](#jsonapimeta) and [`Links`](#jsonapilinks) elsewhere in the JSON:API Document. +Metadata can be specified both in the Relationship Object and in the Resource Identifier Object. You specify the two types of metadata differently. As always, you can use `NoMetadata` to indicate you do not intend the JSON:API relationship to contain metadata. + +```swift +// No metadata in the Resource Identifer or the Relationship: +// { +// "data" : { +// "id" : "1234", +// "type": "people" +// } +// } +let relationship1: ToOneRelationship + +// No metadata in the Resource Identifier but some metadata in the Relationship: +// { +// "data" : { +// "id" : "1234", +// "type": "people" +// }, +// "meta": { ... } +// } +let relationship2: ToOneRelationship +// ^ assumes `RelMetadata` is a `Codable` struct defined elsewhere + +// Metadata in the Resource Identifier but not the Relationship: +// { +// "data" : { +// "id" : "1234", +// "type": "people", +// "meta": { ... } +// } +// } +let relationship3: ToOneRelationship +// ^ assumes `CoolMetadata` is a `Codable` struct defined elsewhere +``` + The `MetaRelationship` is special in that it represents a Relationship Object with no `data` (it must contain at least one of `meta` or `links`). The other two relationship types are Relationship Objects with either singular resource linkages (`ToOneRelationship`) or arrays of resource linkages (`ToManyRelationship`). To describe a relationship that may be omitted (i.e. the key is not even present in the JSON object), you make the entire `MetaRelationship`, `ToOneRelationship` or `ToManyRelationship` optional. From b04f6a3cf62bb40dd590f2acf571ed1fe758b058 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Sat, 19 Sep 2020 19:49:13 -0700 Subject: [PATCH 180/235] add test coverage and fill a logic gap around nullable relationships paired with non-optional id metadata. --- Sources/JSONAPI/Resource/Relationship.swift | 28 +++++++++++------ .../Relationships/RelationshipTests.swift | 30 ++++++++++++++++++- .../stubs/RelationshipStubs.swift | 6 ++++ 3 files changed, 54 insertions(+), 10 deletions(-) diff --git a/Sources/JSONAPI/Resource/Relationship.swift b/Sources/JSONAPI/Resource/Relationship.swift index 264f1f1..fc03c4e 100644 --- a/Sources/JSONAPI/Resource/Relationship.swift +++ b/Sources/JSONAPI/Resource/Relationship.swift @@ -133,23 +133,23 @@ public struct ToManyRelationship(pointers: [ToOneRelationship], meta: MetaType, links: LinksType) where T.ID == Relatable.ID, IdMetaType == NoIdMetadata { - metaIds = pointers.map { .init(id: $0.id, meta: .none) } + idsWithMeta = pointers.map { .init(id: $0.id, meta: .none) } self.meta = meta self.links = links } @@ -169,7 +169,7 @@ public struct ToManyRelationship.self, data: to_one_relationship_nulled_out) + } } // MARK: Failure tests @@ -245,6 +269,10 @@ extension RelationshipTests { func test_ToOneTypeMismatch() { XCTAssertThrowsError(try JSONDecoder().decode(ToOneRelationship.self, from: to_one_relationship_type_mismatch)) } + + func test_toOneNulledOutWithExpectedIdMetadata() { + XCTAssertThrowsError(try JSONDecoder().decode(ToOneRelationship.self, from: to_one_relationship_nulled_out)) + } } // MARK: - Test types diff --git a/Tests/JSONAPITests/Relationships/stubs/RelationshipStubs.swift b/Tests/JSONAPITests/Relationships/stubs/RelationshipStubs.swift index e0c4a8c..434667f 100644 --- a/Tests/JSONAPITests/Relationships/stubs/RelationshipStubs.swift +++ b/Tests/JSONAPITests/Relationships/stubs/RelationshipStubs.swift @@ -41,6 +41,12 @@ let to_one_relationship = """ } """.data(using: .utf8)! +let to_one_relationship_nulled_out = """ +{ + "data": null +} +""".data(using: .utf8)! + let to_one_relationship_type_mismatch = """ { "data": { From f8d02f819b4a9477b15f3ed1e6dbae7a69ccd2de Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Sat, 19 Sep 2020 19:56:35 -0700 Subject: [PATCH 181/235] prepare podspec file for major version bump --- JSONAPI.podspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/JSONAPI.podspec b/JSONAPI.podspec index 12e6f28..5cf8785 100644 --- a/JSONAPI.podspec +++ b/JSONAPI.podspec @@ -16,7 +16,7 @@ Pod::Spec.new do |spec| # spec.name = "MP-JSONAPI" - spec.version = "4.0.0" + spec.version = "5.0.0" spec.summary = "Swift Codable JSON API framework." # This description is used to generate tags and improve search results. From 1ec7913fb1873e9732911f5778b0dab5a154f69a Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Sat, 19 Sep 2020 20:47:35 -0700 Subject: [PATCH 182/235] instead of a slightly cryptic error about which include failed to parse, use ordinal numbers and add a total include count to the message. --- Package.resolved | 2 +- Sources/JSONAPI/Document/Includes.swift | 33 +- .../Document/DocumentDecodingErrorTests.swift | 49 ++- .../Document/stubs/DocumentStubs.swift | 31 ++ .../Includes/IncludesDecodingErrorTests.swift | 69 +++- .../Includes/stubs/IncludeStubs.swift | 352 ++++++++++++++++++ 6 files changed, 510 insertions(+), 26 deletions(-) diff --git a/Package.resolved b/Package.resolved index 2900a5a..63465c7 100644 --- a/Package.resolved +++ b/Package.resolved @@ -3,7 +3,7 @@ "pins": [ { "package": "Poly", - "repositoryURL": "https://github.com/mattpolzin/Poly", + "repositoryURL": "https://github.com/mattpolzin/Poly.git", "state": { "branch": null, "revision": "36ba3f624bffa34f5f9b9c7648eab3cfdcab4748", diff --git a/Sources/JSONAPI/Document/Includes.swift b/Sources/JSONAPI/Document/Includes.swift index d449a2d..36ae3c6 100644 --- a/Sources/JSONAPI/Document/Includes.swift +++ b/Sources/JSONAPI/Document/Includes.swift @@ -76,14 +76,15 @@ extension Includes: Decodable where I: Decodable { } } guard errors.count == error.individualTypeFailures.count else { - throw IncludesDecodingError(error: error, idx: idx) + throw IncludesDecodingError(error: error, idx: idx, totalIncludesCount: container.count ?? 0) } throw IncludesDecodingError( error: IncludeDecodingError(failures: errors), - idx: idx + idx: idx, + totalIncludesCount: container.count ?? 0 ) } catch let error { - throw IncludesDecodingError(error: error, idx: idx) + throw IncludesDecodingError(error: error, idx: idx, totalIncludesCount: container.count ?? 0) } } @@ -208,7 +209,13 @@ extension Includes where I: _Poly11 { // MARK: - DecodingError public struct IncludesDecodingError: Swift.Error, Equatable { public let error: Swift.Error + /// The zero-based index of the include that failed to decode. public let idx: Int + /// The total count of includes in the document that failed to decode. + /// + /// In other words, "of `totalIncludesCount` includes, the `(idx + 1)`th + /// include failed to decode. + public let totalIncludesCount: Int public static func ==(lhs: Self, rhs: Self) -> Bool { return lhs.idx == rhs.idx @@ -218,7 +225,25 @@ public struct IncludesDecodingError: Swift.Error, Equatable { extension IncludesDecodingError: CustomStringConvertible { public var description: String { - return "Include \(idx + 1) failed to parse: \(error)" + let ordinalSuffix: String + if (idx % 100) + 1 > 9 && (idx % 100) + 1 < 20 { + // the teens + ordinalSuffix = "th" + } else { + switch ((idx % 10) + 1) { + case 1: + ordinalSuffix = "st" + case 2: + ordinalSuffix = "nd" + case 3: + ordinalSuffix = "rd" + default: + ordinalSuffix = "th" + } + } + let ordinalDescription = "\(idx + 1)\(ordinalSuffix)" + + return "Out of \(totalIncludesCount) includes, the \(ordinalDescription) one failed to parse: \(error)" } } diff --git a/Tests/JSONAPITests/Document/DocumentDecodingErrorTests.swift b/Tests/JSONAPITests/Document/DocumentDecodingErrorTests.swift index 6fa2fb4..412b1cf 100644 --- a/Tests/JSONAPITests/Document/DocumentDecodingErrorTests.swift +++ b/Tests/JSONAPITests/Document/DocumentDecodingErrorTests.swift @@ -79,6 +79,7 @@ final class DocumentDecodingErrorTests: XCTestCase { } func test_include_failure() { + // test that if there is only one possible include, we just find out on one line what expecation failed. XCTAssertThrowsError( try testDecoder.decode( Document, NoMetadata, NoLinks, Include1, NoAPIDescription, UnknownJSONAPIError>.self, @@ -91,11 +92,12 @@ final class DocumentDecodingErrorTests: XCTestCase { return } - XCTAssertEqual(String(describing: error), #"Include 3 failed to parse: found JSON:API type "not_an_author" but expected "authors""#) + XCTAssertEqual(String(describing: error), #"Out of 3 includes, the 3rd one failed to parse: found JSON:API type "not_an_author" but expected "authors""#) } } func test_include_failure2() { + // test that if there are two possiblie includes, we find out why each of them was not possible to decode. XCTAssertThrowsError( try testDecoder.decode( Document, NoMetadata, NoLinks, Include2, NoAPIDescription, UnknownJSONAPIError>.self, @@ -108,15 +110,44 @@ final class DocumentDecodingErrorTests: XCTestCase { return } - XCTAssertEqual(String(describing: error), -#""" -Include 3 failed to parse: -Could not have been Include Type 1 because: -found JSON:API type "not_an_author" but expected "articles" + XCTAssertEqual( + String(describing: error), + #""" + Out of 3 includes, the 3rd one failed to parse: + Could not have been Include Type 1 because: + found JSON:API type "not_an_author" but expected "articles" -Could not have been Include Type 2 because: -found JSON:API type "not_an_author" but expected "authors" -"""# + Could not have been Include Type 2 because: + found JSON:API type "not_an_author" but expected "authors" + """# + ) + } + } + + func test_include_failure3() { + // test that if the failed include is at a different index, the other index is reported correctly. + XCTAssertThrowsError( + try testDecoder.decode( + Document, NoMetadata, NoLinks, Include2, NoAPIDescription, UnknownJSONAPIError>.self, + from: single_document_some_includes_wrong_type2 + ) + ) { error in + guard let docError = error as? DocumentDecodingError, + case .includes = docError else { + XCTFail("Expected primary resource document error. Got \(error)") + return + } + + XCTAssertEqual( + String(describing: error), + #""" + Out of 3 includes, the 2nd one failed to parse: + Could not have been Include Type 1 because: + found JSON:API type "not_an_author" but expected "articles" + + Could not have been Include Type 2 because: + found JSON:API type "not_an_author" but expected "authors" + """# ) } } diff --git a/Tests/JSONAPITests/Document/stubs/DocumentStubs.swift b/Tests/JSONAPITests/Document/stubs/DocumentStubs.swift index ed83ebf..09efbdb 100644 --- a/Tests/JSONAPITests/Document/stubs/DocumentStubs.swift +++ b/Tests/JSONAPITests/Document/stubs/DocumentStubs.swift @@ -289,6 +289,37 @@ let single_document_some_includes_wrong_type = """ } """.data(using: .utf8)! +let single_document_some_includes_wrong_type2 = """ +{ + "data": { + "id": "1", + "type": "articles", + "relationships": { + "author": { + "data": { + "type": "authors", + "id": "33" + } + } + } + }, + "included": [ + { + "id": "30", + "type": "authors" + }, + { + "id": "31", + "type": "not_an_author" + }, + { + "id": "33", + "type": "authors" + } + ] +} +""".data(using: .utf8)! + let single_document_some_includes_with_api_description = """ { "data": { diff --git a/Tests/JSONAPITests/Includes/IncludesDecodingErrorTests.swift b/Tests/JSONAPITests/Includes/IncludesDecodingErrorTests.swift index 2db4f75..1ddeccf 100644 --- a/Tests/JSONAPITests/Includes/IncludesDecodingErrorTests.swift +++ b/Tests/JSONAPITests/Includes/IncludesDecodingErrorTests.swift @@ -10,7 +10,6 @@ import JSONAPI final class IncludesDecodingErrorTests: XCTestCase { func test_unexpectedIncludeType() { - var error1: Error! XCTAssertThrowsError(try testDecoder.decode(Includes>.self, from: three_different_type_includes)) { (error: Error) -> Void in XCTAssertEqual( (error as? IncludesDecodingError)?.idx, @@ -19,23 +18,69 @@ final class IncludesDecodingErrorTests: XCTestCase { XCTAssertEqual( (error as? IncludesDecodingError).map(String.init(describing:)), -""" -Include 3 failed to parse: \nCould not have been Include Type 1 because: -found JSON:API type "test_entity4" but expected "test_entity1" + """ + Out of 3 includes, the 3rd one failed to parse: \nCould not have been Include Type 1 because: + found JSON:API type "test_entity4" but expected "test_entity1" -Could not have been Include Type 2 because: -found JSON:API type "test_entity4" but expected "test_entity2" -""" + Could not have been Include Type 2 because: + found JSON:API type "test_entity4" but expected "test_entity2" + """ ) - - error1 = error } - // now test that we get the same error from a different test stub + // now test that we get the same error with a different total include count from a different test stub XCTAssertThrowsError(try testDecoder.decode(Includes>.self, from: four_different_type_includes)) { (error2: Error) -> Void in XCTAssertEqual( - error1 as? IncludesDecodingError, - error2 as? IncludesDecodingError + (error2 as? IncludesDecodingError).map(String.init(describing:)), + """ + Out of 4 includes, the 3rd one failed to parse: \nCould not have been Include Type 1 because: + found JSON:API type "test_entity4" but expected "test_entity1" + + Could not have been Include Type 2 because: + found JSON:API type "test_entity4" but expected "test_entity2" + """ + ) + } + + // and with six total includes + XCTAssertThrowsError(try testDecoder.decode(Includes>.self, from: six_includes_one_bad_type)) { (error2: Error) -> Void in + XCTAssertEqual( + (error2 as? IncludesDecodingError).map(String.init(describing:)), + """ + Out of 6 includes, the 5th one failed to parse: \nCould not have been Include Type 1 because: + found JSON:API type "test_entity4" but expected "test_entity1" + + Could not have been Include Type 2 because: + found JSON:API type "test_entity4" but expected "test_entity2" + """ + ) + } + + // and with a number of total includes between 10 and 19 + XCTAssertThrowsError(try testDecoder.decode(Includes>.self, from: eleven_includes_one_bad_type)) { (error2: Error) -> Void in + XCTAssertEqual( + (error2 as? IncludesDecodingError).map(String.init(describing:)), + """ + Out of 11 includes, the 10th one failed to parse: \nCould not have been Include Type 1 because: + found JSON:API type "test_entity4" but expected "test_entity1" + + Could not have been Include Type 2 because: + found JSON:API type "test_entity4" but expected "test_entity2" + """ + ) + } + + // and finally with a larger number of total includes + XCTAssertThrowsError(try testDecoder.decode(Includes>.self, from: twenty_two_includes_one_bad_type)) { (error2: Error) -> Void in + XCTAssertEqual( + (error2 as? IncludesDecodingError).map(String.init(describing:)), + """ + Out of 22 includes, the 21st one failed to parse: \nCould not have been Include Type 1 because: + found JSON:API type "test_entity4" but expected "test_entity1" + + Could not have been Include Type 2 because: + found JSON:API type "test_entity4" but expected "test_entity2" + """ ) } } diff --git a/Tests/JSONAPITests/Includes/stubs/IncludeStubs.swift b/Tests/JSONAPITests/Includes/stubs/IncludeStubs.swift index e53b144..7d7a3da 100644 --- a/Tests/JSONAPITests/Includes/stubs/IncludeStubs.swift +++ b/Tests/JSONAPITests/Includes/stubs/IncludeStubs.swift @@ -686,3 +686,355 @@ let eleven_different_type_includes = """ } ] """.data(using: .utf8)! + + +let six_includes_one_bad_type = """ +[ +{ + "type": "test_entity1", + "id": "2DF03B69-4B0A-467F-B52E-B0C9E44FCECF", + "attributes": { + "foo": "Hello", + "bar": 123 + } + }, +{ + "type": "test_entity1", + "id": "2DF03B69-4B0A-467F-B52E-B0C9E44FCECF", + "attributes": { + "foo": "Hello", + "bar": 123 + } + }, +{ + "type": "test_entity1", + "id": "2DF03B69-4B0A-467F-B52E-B0C9E44FCECF", + "attributes": { + "foo": "Hello", + "bar": 123 + } + }, + { + "type": "test_entity2", + "id": "90F03B69-4DF1-467F-B52E-B0C9E44FC333", + "attributes": { + "foo": "World", + "bar": 456 + }, + "relationships": { + "entity1": { + "data": { + "type": "test_entity1", + "id": "2DF03B69-4B0A-467F-B52E-B0C9E44FCECF" + } + } + } + }, + { + "type": "test_entity4", + "id": "364B3B69-4DF1-467F-B52E-B0C9E44F666E" + }, + { + "type": "test_entity6", + "id": "11113B69-4DF1-467F-B52E-B0C9E44FC444", + "relationships": { + "entity4": { + "data": { + "type": "test_entity4", + "id": "364B3B69-4DF1-467F-B52E-B0C9E44F666E" + } + } + } + } +] +""".data(using: .utf8)! + +let eleven_includes_one_bad_type = """ +[ +{ + "type": "test_entity1", + "id": "2DF03B69-4B0A-467F-B52E-B0C9E44FCECF", + "attributes": { + "foo": "Hello", + "bar": 123 + } + }, +{ + "type": "test_entity1", + "id": "2DF03B69-4B0A-467F-B52E-B0C9E44FCECF", + "attributes": { + "foo": "Hello", + "bar": 123 + } + }, +{ + "type": "test_entity1", + "id": "2DF03B69-4B0A-467F-B52E-B0C9E44FCECF", + "attributes": { + "foo": "Hello", + "bar": 123 + } + }, +{ + "type": "test_entity1", + "id": "2DF03B69-4B0A-467F-B52E-B0C9E44FCECF", + "attributes": { + "foo": "Hello", + "bar": 123 + } + }, +{ + "type": "test_entity1", + "id": "2DF03B69-4B0A-467F-B52E-B0C9E44FCECF", + "attributes": { + "foo": "Hello", + "bar": 123 + } + }, +{ + "type": "test_entity1", + "id": "2DF03B69-4B0A-467F-B52E-B0C9E44FCECF", + "attributes": { + "foo": "Hello", + "bar": 123 + } + }, +{ + "type": "test_entity1", + "id": "2DF03B69-4B0A-467F-B52E-B0C9E44FCECF", + "attributes": { + "foo": "Hello", + "bar": 123 + } + }, +{ + "type": "test_entity1", + "id": "2DF03B69-4B0A-467F-B52E-B0C9E44FCECF", + "attributes": { + "foo": "Hello", + "bar": 123 + } + }, + { + "type": "test_entity2", + "id": "90F03B69-4DF1-467F-B52E-B0C9E44FC333", + "attributes": { + "foo": "World", + "bar": 456 + }, + "relationships": { + "entity1": { + "data": { + "type": "test_entity1", + "id": "2DF03B69-4B0A-467F-B52E-B0C9E44FCECF" + } + } + } + }, + { + "type": "test_entity4", + "id": "364B3B69-4DF1-467F-B52E-B0C9E44F666E" + }, + { + "type": "test_entity6", + "id": "11113B69-4DF1-467F-B52E-B0C9E44FC444", + "relationships": { + "entity4": { + "data": { + "type": "test_entity4", + "id": "364B3B69-4DF1-467F-B52E-B0C9E44F666E" + } + } + } + } +] +""".data(using: .utf8)! + +let twenty_two_includes_one_bad_type = """ +[ +{ + "type": "test_entity1", + "id": "2DF03B69-4B0A-467F-B52E-B0C9E44FCECF", + "attributes": { + "foo": "Hello", + "bar": 123 + } + }, +{ + "type": "test_entity1", + "id": "2DF03B69-4B0A-467F-B52E-B0C9E44FCECF", + "attributes": { + "foo": "Hello", + "bar": 123 + } + }, +{ + "type": "test_entity1", + "id": "2DF03B69-4B0A-467F-B52E-B0C9E44FCECF", + "attributes": { + "foo": "Hello", + "bar": 123 + } + }, +{ + "type": "test_entity1", + "id": "2DF03B69-4B0A-467F-B52E-B0C9E44FCECF", + "attributes": { + "foo": "Hello", + "bar": 123 + } + }, +{ + "type": "test_entity1", + "id": "2DF03B69-4B0A-467F-B52E-B0C9E44FCECF", + "attributes": { + "foo": "Hello", + "bar": 123 + } + }, +{ + "type": "test_entity1", + "id": "2DF03B69-4B0A-467F-B52E-B0C9E44FCECF", + "attributes": { + "foo": "Hello", + "bar": 123 + } + }, +{ + "type": "test_entity1", + "id": "2DF03B69-4B0A-467F-B52E-B0C9E44FCECF", + "attributes": { + "foo": "Hello", + "bar": 123 + } + }, +{ + "type": "test_entity1", + "id": "2DF03B69-4B0A-467F-B52E-B0C9E44FCECF", + "attributes": { + "foo": "Hello", + "bar": 123 + } + }, +{ + "type": "test_entity1", + "id": "2DF03B69-4B0A-467F-B52E-B0C9E44FCECF", + "attributes": { + "foo": "Hello", + "bar": 123 + } + }, +{ + "type": "test_entity1", + "id": "2DF03B69-4B0A-467F-B52E-B0C9E44FCECF", + "attributes": { + "foo": "Hello", + "bar": 123 + } + }, +{ + "type": "test_entity1", + "id": "2DF03B69-4B0A-467F-B52E-B0C9E44FCECF", + "attributes": { + "foo": "Hello", + "bar": 123 + } + }, +{ + "type": "test_entity1", + "id": "2DF03B69-4B0A-467F-B52E-B0C9E44FCECF", + "attributes": { + "foo": "Hello", + "bar": 123 + } + }, +{ + "type": "test_entity1", + "id": "2DF03B69-4B0A-467F-B52E-B0C9E44FCECF", + "attributes": { + "foo": "Hello", + "bar": 123 + } + }, +{ + "type": "test_entity1", + "id": "2DF03B69-4B0A-467F-B52E-B0C9E44FCECF", + "attributes": { + "foo": "Hello", + "bar": 123 + } + }, +{ + "type": "test_entity1", + "id": "2DF03B69-4B0A-467F-B52E-B0C9E44FCECF", + "attributes": { + "foo": "Hello", + "bar": 123 + } + }, +{ + "type": "test_entity1", + "id": "2DF03B69-4B0A-467F-B52E-B0C9E44FCECF", + "attributes": { + "foo": "Hello", + "bar": 123 + } + }, +{ + "type": "test_entity1", + "id": "2DF03B69-4B0A-467F-B52E-B0C9E44FCECF", + "attributes": { + "foo": "Hello", + "bar": 123 + } + }, +{ + "type": "test_entity1", + "id": "2DF03B69-4B0A-467F-B52E-B0C9E44FCECF", + "attributes": { + "foo": "Hello", + "bar": 123 + } + }, +{ + "type": "test_entity1", + "id": "2DF03B69-4B0A-467F-B52E-B0C9E44FCECF", + "attributes": { + "foo": "Hello", + "bar": 123 + } + }, + { + "type": "test_entity2", + "id": "90F03B69-4DF1-467F-B52E-B0C9E44FC333", + "attributes": { + "foo": "World", + "bar": 456 + }, + "relationships": { + "entity1": { + "data": { + "type": "test_entity1", + "id": "2DF03B69-4B0A-467F-B52E-B0C9E44FCECF" + } + } + } + }, + { + "type": "test_entity4", + "id": "364B3B69-4DF1-467F-B52E-B0C9E44F666E" + }, + { + "type": "test_entity6", + "id": "11113B69-4DF1-467F-B52E-B0C9E44FC444", + "relationships": { + "entity4": { + "data": { + "type": "test_entity4", + "id": "364B3B69-4DF1-467F-B52E-B0C9E44F666E" + } + } + } + } +] +""".data(using: .utf8)! From 914e8c74c18c924640a76cb73833071a424ace24 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Sat, 19 Sep 2020 22:11:40 -0700 Subject: [PATCH 183/235] use jsonapi type names instead of indices in include decode failure messages. --- Sources/JSONAPI/Document/Includes.swift | 2 +- .../Resource Object/ResourceObject.swift | 14 ++-- .../ResourceObjectDecodingError.swift | 25 +++++--- .../Document/DocumentDecodingErrorTests.swift | 8 +-- .../Includes/IncludesDecodingErrorTests.swift | 20 +++--- .../ResourceObjectDecodingErrorTests.swift | 64 ++++++++++++------- 6 files changed, 80 insertions(+), 53 deletions(-) diff --git a/Sources/JSONAPI/Document/Includes.swift b/Sources/JSONAPI/Document/Includes.swift index 36ae3c6..df2d307 100644 --- a/Sources/JSONAPI/Document/Includes.swift +++ b/Sources/JSONAPI/Document/Includes.swift @@ -254,7 +254,7 @@ public struct IncludeDecodingError: Swift.Error, Equatable, CustomStringConverti return failures .enumerated() .map { - "\nCould not have been Include Type \($0.offset + 1) because:\n\($0.element)" + "\nCould not have been Include Type `\($0.element.resourceObjectJsonAPIType)` because:\n\($0.element)" }.joined(separator: "\n") } } diff --git a/Sources/JSONAPI/Resource/Resource Object/ResourceObject.swift b/Sources/JSONAPI/Resource/Resource Object/ResourceObject.swift index 9ac95d8..b4f6534 100644 --- a/Sources/JSONAPI/Resource/Resource Object/ResourceObject.swift +++ b/Sources/JSONAPI/Resource/Resource Object/ResourceObject.swift @@ -398,7 +398,7 @@ public extension ResourceObject { do { type = try container.decode(String.self, forKey: .type) } catch let error as DecodingError { - throw ResourceObjectDecodingError(error) + throw ResourceObjectDecodingError(error, jsonAPIType: Self.jsonType) ?? error } @@ -417,13 +417,14 @@ public extension ResourceObject { ?? container.decodeIfPresent(Description.Attributes.self, forKey: .attributes) ?? Description.Attributes(from: EmptyObjectDecoder()) } catch let decodingError as DecodingError { - throw ResourceObjectDecodingError(decodingError) + throw ResourceObjectDecodingError(decodingError, jsonAPIType: Self.jsonType) ?? decodingError } catch _ as EmptyObjectDecodingError { throw ResourceObjectDecodingError( subjectName: ResourceObjectDecodingError.entireObject, cause: .keyNotFound, - location: .attributes + location: .attributes, + jsonAPIType: Self.jsonType ) } @@ -432,16 +433,17 @@ public extension ResourceObject { ?? container.decodeIfPresent(Description.Relationships.self, forKey: .relationships) ?? Description.Relationships(from: EmptyObjectDecoder()) } catch let decodingError as DecodingError { - throw ResourceObjectDecodingError(decodingError) + throw ResourceObjectDecodingError(decodingError, jsonAPIType: Self.jsonType) ?? decodingError } catch let decodingError as JSONAPICodingError { - throw ResourceObjectDecodingError(decodingError) + throw ResourceObjectDecodingError(decodingError, jsonAPIType: Self.jsonType) ?? decodingError } catch _ as EmptyObjectDecodingError { throw ResourceObjectDecodingError( subjectName: ResourceObjectDecodingError.entireObject, cause: .keyNotFound, - location: .relationships + location: .relationships, + jsonAPIType: Self.jsonType ) } diff --git a/Sources/JSONAPI/Resource/Resource Object/ResourceObjectDecodingError.swift b/Sources/JSONAPI/Resource/Resource Object/ResourceObjectDecodingError.swift index 7495cf3..d790455 100644 --- a/Sources/JSONAPI/Resource/Resource Object/ResourceObjectDecodingError.swift +++ b/Sources/JSONAPI/Resource/Resource Object/ResourceObjectDecodingError.swift @@ -6,6 +6,7 @@ // public struct ResourceObjectDecodingError: Swift.Error, Equatable { + public let resourceObjectJsonAPIType: String public let subjectName: String public let cause: Cause public let location: Location @@ -16,7 +17,7 @@ public struct ResourceObjectDecodingError: Swift.Error, Equatable { case keyNotFound case valueNotFound case typeMismatch(expectedTypeName: String) - case jsonTypeMismatch(expectedType: String, foundType: String) + case jsonTypeMismatch(foundType: String) case quantityMismatch(expected: JSONAPICodingError.Quantity) } @@ -38,7 +39,8 @@ public struct ResourceObjectDecodingError: Swift.Error, Equatable { } } - init?(_ decodingError: DecodingError) { + init?(_ decodingError: DecodingError, jsonAPIType: String) { + self.resourceObjectJsonAPIType = jsonAPIType switch decodingError { case .typeMismatch(let expectedType, let ctx): (location, subjectName) = Self.context(ctx) @@ -67,11 +69,12 @@ public struct ResourceObjectDecodingError: Swift.Error, Equatable { } } - init?(_ jsonAPIError: JSONAPICodingError) { + init?(_ jsonAPIError: JSONAPICodingError, jsonAPIType: String) { + self.resourceObjectJsonAPIType = jsonAPIType switch jsonAPIError { case .typeMismatch(expected: let expected, found: let found, path: let path): (location, subjectName) = Self.context(path: path) - cause = .jsonTypeMismatch(expectedType: expected, foundType: found) + cause = .jsonTypeMismatch(foundType: found) case .quantityMismatch(expected: let expected, path: let path): (location, subjectName) = Self.context(path: path) cause = .quantityMismatch(expected: expected) @@ -81,12 +84,14 @@ public struct ResourceObjectDecodingError: Swift.Error, Equatable { } init(expectedJSONAPIType: String, found: String) { + resourceObjectJsonAPIType = expectedJSONAPIType location = .type subjectName = "self" - cause = .jsonTypeMismatch(expectedType: expectedJSONAPIType, foundType: found) + cause = .jsonTypeMismatch(foundType: found) } - init(subjectName: String, cause: Cause, location: Location) { + init(subjectName: String, cause: Cause, location: Location, jsonAPIType: String) { + self.resourceObjectJsonAPIType = jsonAPIType self.subjectName = subjectName self.cause = cause self.location = location @@ -135,10 +140,10 @@ extension ResourceObjectDecodingError: CustomStringConvertible { return "'\(location.singular)' (a.k.a. the JSON:API type name) is not a \(expected) as expected." case .typeMismatch(expectedTypeName: let expected): return "'\(subjectName)' \(location.singular) is not a \(expected) as expected." - case .jsonTypeMismatch(expectedType: let expected, foundType: let found) where location == .type: - return "found JSON:API type \"\(found)\" but expected \"\(expected)\"" - case .jsonTypeMismatch(expectedType: let expected, foundType: let found): - return "'\(subjectName)' \(location.singular) is of JSON:API type \"\(found)\" but it was expected to be \"\(expected)\"" + case .jsonTypeMismatch(foundType: let found) where location == .type: + return "found JSON:API type \"\(found)\" but expected \"\(resourceObjectJsonAPIType)\"" + case .jsonTypeMismatch(foundType: let found): + return "'\(subjectName)' \(location.singular) is of JSON:API type \"\(found)\" but it was expected to be \"\(resourceObjectJsonAPIType)\"" case .quantityMismatch(expected: let expected): let expecation: String = { switch expected { diff --git a/Tests/JSONAPITests/Document/DocumentDecodingErrorTests.swift b/Tests/JSONAPITests/Document/DocumentDecodingErrorTests.swift index 412b1cf..5fb9919 100644 --- a/Tests/JSONAPITests/Document/DocumentDecodingErrorTests.swift +++ b/Tests/JSONAPITests/Document/DocumentDecodingErrorTests.swift @@ -114,10 +114,10 @@ final class DocumentDecodingErrorTests: XCTestCase { String(describing: error), #""" Out of 3 includes, the 3rd one failed to parse: - Could not have been Include Type 1 because: + Could not have been Include Type `articles` because: found JSON:API type "not_an_author" but expected "articles" - Could not have been Include Type 2 because: + Could not have been Include Type `authors` because: found JSON:API type "not_an_author" but expected "authors" """# ) @@ -142,10 +142,10 @@ final class DocumentDecodingErrorTests: XCTestCase { String(describing: error), #""" Out of 3 includes, the 2nd one failed to parse: - Could not have been Include Type 1 because: + Could not have been Include Type `articles` because: found JSON:API type "not_an_author" but expected "articles" - Could not have been Include Type 2 because: + Could not have been Include Type `authors` because: found JSON:API type "not_an_author" but expected "authors" """# ) diff --git a/Tests/JSONAPITests/Includes/IncludesDecodingErrorTests.swift b/Tests/JSONAPITests/Includes/IncludesDecodingErrorTests.swift index 1ddeccf..8d5c97c 100644 --- a/Tests/JSONAPITests/Includes/IncludesDecodingErrorTests.swift +++ b/Tests/JSONAPITests/Includes/IncludesDecodingErrorTests.swift @@ -19,10 +19,10 @@ final class IncludesDecodingErrorTests: XCTestCase { XCTAssertEqual( (error as? IncludesDecodingError).map(String.init(describing:)), """ - Out of 3 includes, the 3rd one failed to parse: \nCould not have been Include Type 1 because: + Out of 3 includes, the 3rd one failed to parse: \nCould not have been Include Type `test_entity1` because: found JSON:API type "test_entity4" but expected "test_entity1" - Could not have been Include Type 2 because: + Could not have been Include Type `test_entity2` because: found JSON:API type "test_entity4" but expected "test_entity2" """ ) @@ -33,10 +33,10 @@ final class IncludesDecodingErrorTests: XCTestCase { XCTAssertEqual( (error2 as? IncludesDecodingError).map(String.init(describing:)), """ - Out of 4 includes, the 3rd one failed to parse: \nCould not have been Include Type 1 because: + Out of 4 includes, the 3rd one failed to parse: \nCould not have been Include Type `test_entity1` because: found JSON:API type "test_entity4" but expected "test_entity1" - Could not have been Include Type 2 because: + Could not have been Include Type `test_entity2` because: found JSON:API type "test_entity4" but expected "test_entity2" """ ) @@ -47,10 +47,10 @@ final class IncludesDecodingErrorTests: XCTestCase { XCTAssertEqual( (error2 as? IncludesDecodingError).map(String.init(describing:)), """ - Out of 6 includes, the 5th one failed to parse: \nCould not have been Include Type 1 because: + Out of 6 includes, the 5th one failed to parse: \nCould not have been Include Type `test_entity1` because: found JSON:API type "test_entity4" but expected "test_entity1" - Could not have been Include Type 2 because: + Could not have been Include Type `test_entity2` because: found JSON:API type "test_entity4" but expected "test_entity2" """ ) @@ -61,10 +61,10 @@ final class IncludesDecodingErrorTests: XCTestCase { XCTAssertEqual( (error2 as? IncludesDecodingError).map(String.init(describing:)), """ - Out of 11 includes, the 10th one failed to parse: \nCould not have been Include Type 1 because: + Out of 11 includes, the 10th one failed to parse: \nCould not have been Include Type `test_entity1` because: found JSON:API type "test_entity4" but expected "test_entity1" - Could not have been Include Type 2 because: + Could not have been Include Type `test_entity2` because: found JSON:API type "test_entity4" but expected "test_entity2" """ ) @@ -75,10 +75,10 @@ final class IncludesDecodingErrorTests: XCTestCase { XCTAssertEqual( (error2 as? IncludesDecodingError).map(String.init(describing:)), """ - Out of 22 includes, the 21st one failed to parse: \nCould not have been Include Type 1 because: + Out of 22 includes, the 21st one failed to parse: \nCould not have been Include Type `test_entity1` because: found JSON:API type "test_entity4" but expected "test_entity1" - Could not have been Include Type 2 because: + Could not have been Include Type `test_entity2` because: found JSON:API type "test_entity4" but expected "test_entity2" """ ) diff --git a/Tests/JSONAPITests/ResourceObject/ResourceObjectDecodingErrorTests.swift b/Tests/JSONAPITests/ResourceObject/ResourceObjectDecodingErrorTests.swift index bf93ec3..32d6596 100644 --- a/Tests/JSONAPITests/ResourceObject/ResourceObjectDecodingErrorTests.swift +++ b/Tests/JSONAPITests/ResourceObject/ResourceObjectDecodingErrorTests.swift @@ -20,7 +20,8 @@ final class ResourceObjectDecodingErrorTests: XCTestCase { ResourceObjectDecodingError( subjectName: ResourceObjectDecodingError.entireObject, cause: .keyNotFound, - location: .relationships + location: .relationships, + jsonAPIType: TestEntity.jsonType ) ) @@ -41,7 +42,8 @@ final class ResourceObjectDecodingErrorTests: XCTestCase { ResourceObjectDecodingError( subjectName: "required", cause: .keyNotFound, - location: .relationships + location: .relationships, + jsonAPIType: TestEntity.jsonType ) ) @@ -62,7 +64,8 @@ final class ResourceObjectDecodingErrorTests: XCTestCase { ResourceObjectDecodingError( subjectName: "required", cause: .keyNotFound, - location: .relationshipId + location: .relationshipId, + jsonAPIType: TestEntity.jsonType ) ) @@ -83,7 +86,8 @@ final class ResourceObjectDecodingErrorTests: XCTestCase { ResourceObjectDecodingError( subjectName: "required", cause: .keyNotFound, - location: .relationshipType + location: .relationshipType, + jsonAPIType: TestEntity.jsonType ) ) @@ -104,7 +108,8 @@ final class ResourceObjectDecodingErrorTests: XCTestCase { ResourceObjectDecodingError( subjectName: "required", cause: .valueNotFound, - location: .relationships + location: .relationships, + jsonAPIType: TestEntity.jsonType ) ) @@ -125,7 +130,8 @@ final class ResourceObjectDecodingErrorTests: XCTestCase { ResourceObjectDecodingError( subjectName: "required", cause: .valueNotFound, - location: .relationships + location: .relationships, + jsonAPIType: TestEntity.jsonType ) ) @@ -145,8 +151,9 @@ final class ResourceObjectDecodingErrorTests: XCTestCase { error as? ResourceObjectDecodingError, ResourceObjectDecodingError( subjectName: "required", - cause: .jsonTypeMismatch(expectedType: "thirteenth_test_entities", foundType: "not_the_same"), - location: .relationships + cause: .jsonTypeMismatch(foundType: "not_the_same"), + location: .relationships, + jsonAPIType: "thirteenth_test_entities" ) ) @@ -168,7 +175,8 @@ final class ResourceObjectDecodingErrorTests: XCTestCase { ResourceObjectDecodingError( subjectName: "required", cause: .quantityMismatch(expected: .one), - location: .relationships + location: .relationships, + jsonAPIType: TestEntity.jsonType ) ) @@ -187,7 +195,8 @@ final class ResourceObjectDecodingErrorTests: XCTestCase { ResourceObjectDecodingError( subjectName: "omittable", cause: .quantityMismatch(expected: .many), - location: .relationships + location: .relationships, + jsonAPIType: TestEntity.jsonType ) ) @@ -211,7 +220,8 @@ extension ResourceObjectDecodingErrorTests { ResourceObjectDecodingError( subjectName: ResourceObjectDecodingError.entireObject, cause: .keyNotFound, - location: .attributes + location: .attributes, + jsonAPIType: TestEntity2.jsonType ) ) @@ -232,7 +242,8 @@ extension ResourceObjectDecodingErrorTests { ResourceObjectDecodingError( subjectName: "required", cause: .keyNotFound, - location: .attributes + location: .attributes, + jsonAPIType: TestEntity2.jsonType ) ) @@ -253,7 +264,8 @@ extension ResourceObjectDecodingErrorTests { ResourceObjectDecodingError( subjectName: "required", cause: .valueNotFound, - location: .attributes + location: .attributes, + jsonAPIType: TestEntity2.jsonType ) ) @@ -274,7 +286,8 @@ extension ResourceObjectDecodingErrorTests { ResourceObjectDecodingError( subjectName: "required", cause: .typeMismatch(expectedTypeName: String(describing: String.self)), - location: .attributes + location: .attributes, + jsonAPIType: TestEntity2.jsonType ) ) @@ -295,7 +308,8 @@ extension ResourceObjectDecodingErrorTests { ResourceObjectDecodingError( subjectName: "other", cause: .typeMismatch(expectedTypeName: String(describing: Int.self)), - location: .attributes + location: .attributes, + jsonAPIType: TestEntity2.jsonType ) ) @@ -316,7 +330,8 @@ extension ResourceObjectDecodingErrorTests { ResourceObjectDecodingError( subjectName: "yetAnother", cause: .typeMismatch(expectedTypeName: String(describing: Bool.self)), - location: .attributes + location: .attributes, + jsonAPIType: TestEntity2.jsonType ) ) @@ -337,7 +352,8 @@ extension ResourceObjectDecodingErrorTests { ResourceObjectDecodingError( subjectName: "transformed", cause: .typeMismatch(expectedTypeName: String(describing: Int.self)), - location: .attributes + location: .attributes, + jsonAPIType: TestEntity2.jsonType ) ) @@ -372,8 +388,9 @@ extension ResourceObjectDecodingErrorTests { error as? ResourceObjectDecodingError, ResourceObjectDecodingError( subjectName: "self", - cause: .jsonTypeMismatch(expectedType: "fourteenth_test_entities", foundType: "not_correct_type"), - location: .type + cause: .jsonTypeMismatch(foundType: "not_correct_type"), + location: .type, + jsonAPIType: "fourteenth_test_entities" ) ) @@ -394,7 +411,8 @@ extension ResourceObjectDecodingErrorTests { ResourceObjectDecodingError( subjectName: "type", cause: .typeMismatch(expectedTypeName: String(describing: String.self)), - location: .type + location: .type, + jsonAPIType: TestEntity2.jsonType ) ) @@ -415,7 +433,8 @@ extension ResourceObjectDecodingErrorTests { ResourceObjectDecodingError( subjectName: "type", cause: .keyNotFound, - location: .type + location: .type, + jsonAPIType: TestEntity2.jsonType ) ) @@ -436,7 +455,8 @@ extension ResourceObjectDecodingErrorTests { ResourceObjectDecodingError( subjectName: "type", cause: .valueNotFound, - location: .type + location: .type, + jsonAPIType: TestEntity2.jsonType ) ) From 9521e2c98f760144f72229e610d9b6cfe7a6d845 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Sat, 19 Sep 2020 22:23:50 -0700 Subject: [PATCH 184/235] Add test for include failure not related to type mismatch --- .../Includes/IncludesDecodingErrorTests.swift | 23 ++++ .../Includes/stubs/IncludeStubs.swift | 128 +++++++++++------- 2 files changed, 103 insertions(+), 48 deletions(-) diff --git a/Tests/JSONAPITests/Includes/IncludesDecodingErrorTests.swift b/Tests/JSONAPITests/Includes/IncludesDecodingErrorTests.swift index 8d5c97c..fe7dd2c 100644 --- a/Tests/JSONAPITests/Includes/IncludesDecodingErrorTests.swift +++ b/Tests/JSONAPITests/Includes/IncludesDecodingErrorTests.swift @@ -84,6 +84,29 @@ final class IncludesDecodingErrorTests: XCTestCase { ) } } + + func test_missingProperty() { + XCTAssertThrowsError( + try testDecoder.decode( + Includes>.self, + from: three_includes_one_missing_attributes + ) + ) { (error: Error) -> Void in + XCTAssertEqual( + (error as? IncludesDecodingError).map(String.init(describing:)), + """ + Out of 3 includes, the 3rd one failed to parse: \nCould not have been Include Type `test_entity1` because: + found JSON:API type "test_entity2" but expected "test_entity1" + + Could not have been Include Type `test_entity2` because: + 'foo' attribute is required and missing. + + Could not have been Include Type `test_entity4` because: + found JSON:API type "test_entity2" but expected "test_entity4" + """ + ) + } + } } // MARK: - Test Types diff --git a/Tests/JSONAPITests/Includes/stubs/IncludeStubs.swift b/Tests/JSONAPITests/Includes/stubs/IncludeStubs.swift index 7d7a3da..708e154 100644 --- a/Tests/JSONAPITests/Includes/stubs/IncludeStubs.swift +++ b/Tests/JSONAPITests/Includes/stubs/IncludeStubs.swift @@ -7,65 +7,65 @@ let one_include = """ [ -{ -"type": "test_entity1", -"id": "2DF03B69-4B0A-467F-B52E-B0C9E44FCECF", -"attributes": { -"foo": "Hello", -"bar": 123 -} -} + { + "type": "test_entity1", + "id": "2DF03B69-4B0A-467F-B52E-B0C9E44FCECF", + "attributes": { + "foo": "Hello", + "bar": 123 + } + } ] """.data(using: .utf8)! let two_same_type_includes = """ [ -{ -"type": "test_entity1", -"id": "2DF03B69-4B0A-467F-B52E-B0C9E44FCECF", -"attributes": { -"foo": "Hello", -"bar": 123 -} -}, -{ -"type": "test_entity1", -"id": "90F03B69-4DF1-467F-B52E-B0C9E44FC333", -"attributes": { -"foo": "World", -"bar": 456 -} -} + { + "type": "test_entity1", + "id": "2DF03B69-4B0A-467F-B52E-B0C9E44FCECF", + "attributes": { + "foo": "Hello", + "bar": 123 + } + }, + { + "type": "test_entity1", + "id": "90F03B69-4DF1-467F-B52E-B0C9E44FC333", + "attributes": { + "foo": "World", + "bar": 456 + } + } ] """.data(using: .utf8)! let two_different_type_includes = """ [ -{ -"type": "test_entity1", -"id": "2DF03B69-4B0A-467F-B52E-B0C9E44FCECF", -"attributes": { -"foo": "Hello", -"bar": 123 -} -}, -{ -"type": "test_entity2", -"id": "90F03B69-4DF1-467F-B52E-B0C9E44FC333", -"attributes": { -"foo": "World", -"bar": 456 -}, -"relationships": { -"entity1": { -"data": { -"type": "test_entity1", -"id": "2DF03B69-4B0A-467F-B52E-B0C9E44FCECF" -} -} -} -} + { + "type": "test_entity1", + "id": "2DF03B69-4B0A-467F-B52E-B0C9E44FCECF", + "attributes": { + "foo": "Hello", + "bar": 123 + } + }, + { + "type": "test_entity2", + "id": "90F03B69-4DF1-467F-B52E-B0C9E44FC333", + "attributes": { + "foo": "World", + "bar": 456 + }, + "relationships": { + "entity1": { + "data": { + "type": "test_entity1", + "id": "2DF03B69-4B0A-467F-B52E-B0C9E44FCECF" + } + } + } + } ] """.data(using: .utf8)! @@ -688,6 +688,38 @@ let eleven_different_type_includes = """ """.data(using: .utf8)! +let three_includes_one_missing_attributes = """ +[ + { + "type": "test_entity1", + "id": "2DF03B69-4B0A-467F-B52E-B0C9E44FCECF", + "attributes": { + "foo": "Hello", + "bar": 123 + } + }, + { + "type": "test_entity4", + "id": "364B3B69-4DF1-467F-B52E-B0C9E44F666E" + }, + { + "type": "test_entity2", + "id": "90F03B69-4DF1-467F-B52E-B0C9E44FC333", + "attributes": { + "bar": 456 + }, + "relationships": { + "entity1": { + "data": { + "type": "test_entity1", + "id": "2DF03B69-4B0A-467F-B52E-B0C9E44FCECF" + } + } + } + } +] +""".data(using: .utf8)! + let six_includes_one_bad_type = """ [ { From 119a123e173064d601c9084d79df777ffca11302 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Mon, 21 Sep 2020 20:04:39 -0700 Subject: [PATCH 185/235] really get includes errors down to a concise message when we know that all types are wrong or exactly one type is correct. --- Sources/JSONAPI/Document/Includes.swift | 21 +++++++- .../ResourceObjectDecodingError.swift | 5 ++ .../Document/DocumentDecodingErrorTests.swift | 20 ++------ .../Includes/IncludesDecodingErrorTests.swift | 51 +++---------------- 4 files changed, 34 insertions(+), 63 deletions(-) diff --git a/Sources/JSONAPI/Document/Includes.swift b/Sources/JSONAPI/Document/Includes.swift index df2d307..e48a547 100644 --- a/Sources/JSONAPI/Document/Includes.swift +++ b/Sources/JSONAPI/Document/Includes.swift @@ -243,7 +243,7 @@ extension IncludesDecodingError: CustomStringConvertible { } let ordinalDescription = "\(idx + 1)\(ordinalSuffix)" - return "Out of \(totalIncludesCount) includes, the \(ordinalDescription) one failed to parse: \(error)" + return "Out of the \(totalIncludesCount) includes in the document, the \(ordinalDescription) one failed to parse: \(error)" } } @@ -251,6 +251,25 @@ public struct IncludeDecodingError: Swift.Error, Equatable, CustomStringConverti public let failures: [ResourceObjectDecodingError] public var description: String { + // concise error when all failures are mismatched JSON:API types: + if case let .jsonTypeMismatch(foundType: foundType)? = failures.first?.cause, + failures.allSatisfy({ $0.cause.isTypeMismatch }) { + let expectedTypes = failures + .compactMap { "'\($0.resourceObjectJsonAPIType)'" } + .joined(separator: ", ") + + return "Found JSON:API type '\(foundType)' but expected one of \(expectedTypes)" + } + + // concise error when all but failures but one are type mismatches because + // we can assume the correct type was found but there was some other error: + let nonTypeMismatches = failures.filter({ !$0.cause.isTypeMismatch}) + if nonTypeMismatches.count == 1, let nonTypeMismatch = nonTypeMismatches.first { + return String(describing: nonTypeMismatch) + } + + // fall back to just describing all of the reasons it could not have been any of the available + // types: return failures .enumerated() .map { diff --git a/Sources/JSONAPI/Resource/Resource Object/ResourceObjectDecodingError.swift b/Sources/JSONAPI/Resource/Resource Object/ResourceObjectDecodingError.swift index d790455..528f695 100644 --- a/Sources/JSONAPI/Resource/Resource Object/ResourceObjectDecodingError.swift +++ b/Sources/JSONAPI/Resource/Resource Object/ResourceObjectDecodingError.swift @@ -19,6 +19,11 @@ public struct ResourceObjectDecodingError: Swift.Error, Equatable { case typeMismatch(expectedTypeName: String) case jsonTypeMismatch(foundType: String) case quantityMismatch(expected: JSONAPICodingError.Quantity) + + internal var isTypeMismatch: Bool { + guard case .jsonTypeMismatch = self else { return false} + return true + } } public enum Location: String, Equatable { diff --git a/Tests/JSONAPITests/Document/DocumentDecodingErrorTests.swift b/Tests/JSONAPITests/Document/DocumentDecodingErrorTests.swift index 5fb9919..4298773 100644 --- a/Tests/JSONAPITests/Document/DocumentDecodingErrorTests.swift +++ b/Tests/JSONAPITests/Document/DocumentDecodingErrorTests.swift @@ -92,7 +92,7 @@ final class DocumentDecodingErrorTests: XCTestCase { return } - XCTAssertEqual(String(describing: error), #"Out of 3 includes, the 3rd one failed to parse: found JSON:API type "not_an_author" but expected "authors""#) + XCTAssertEqual(String(describing: error), #"Out of the 3 includes in the document, the 3rd one failed to parse: found JSON:API type "not_an_author" but expected "authors""#) } } @@ -112,14 +112,7 @@ final class DocumentDecodingErrorTests: XCTestCase { XCTAssertEqual( String(describing: error), - #""" - Out of 3 includes, the 3rd one failed to parse: - Could not have been Include Type `articles` because: - found JSON:API type "not_an_author" but expected "articles" - - Could not have been Include Type `authors` because: - found JSON:API type "not_an_author" but expected "authors" - """# + "Out of the 3 includes in the document, the 3rd one failed to parse: Found JSON:API type 'not_an_author' but expected one of 'articles', 'authors'" ) } } @@ -140,14 +133,7 @@ final class DocumentDecodingErrorTests: XCTestCase { XCTAssertEqual( String(describing: error), - #""" - Out of 3 includes, the 2nd one failed to parse: - Could not have been Include Type `articles` because: - found JSON:API type "not_an_author" but expected "articles" - - Could not have been Include Type `authors` because: - found JSON:API type "not_an_author" but expected "authors" - """# + "Out of the 3 includes in the document, the 2nd one failed to parse: Found JSON:API type 'not_an_author' but expected one of 'articles', 'authors'" ) } } diff --git a/Tests/JSONAPITests/Includes/IncludesDecodingErrorTests.swift b/Tests/JSONAPITests/Includes/IncludesDecodingErrorTests.swift index fe7dd2c..e562194 100644 --- a/Tests/JSONAPITests/Includes/IncludesDecodingErrorTests.swift +++ b/Tests/JSONAPITests/Includes/IncludesDecodingErrorTests.swift @@ -18,13 +18,7 @@ final class IncludesDecodingErrorTests: XCTestCase { XCTAssertEqual( (error as? IncludesDecodingError).map(String.init(describing:)), - """ - Out of 3 includes, the 3rd one failed to parse: \nCould not have been Include Type `test_entity1` because: - found JSON:API type "test_entity4" but expected "test_entity1" - - Could not have been Include Type `test_entity2` because: - found JSON:API type "test_entity4" but expected "test_entity2" - """ + "Out of the 3 includes in the document, the 3rd one failed to parse: Found JSON:API type 'test_entity4' but expected one of 'test_entity1', 'test_entity2'" ) } @@ -32,13 +26,7 @@ final class IncludesDecodingErrorTests: XCTestCase { XCTAssertThrowsError(try testDecoder.decode(Includes>.self, from: four_different_type_includes)) { (error2: Error) -> Void in XCTAssertEqual( (error2 as? IncludesDecodingError).map(String.init(describing:)), - """ - Out of 4 includes, the 3rd one failed to parse: \nCould not have been Include Type `test_entity1` because: - found JSON:API type "test_entity4" but expected "test_entity1" - - Could not have been Include Type `test_entity2` because: - found JSON:API type "test_entity4" but expected "test_entity2" - """ + "Out of the 4 includes in the document, the 3rd one failed to parse: Found JSON:API type 'test_entity4' but expected one of 'test_entity1', 'test_entity2'" ) } @@ -46,13 +34,7 @@ final class IncludesDecodingErrorTests: XCTestCase { XCTAssertThrowsError(try testDecoder.decode(Includes>.self, from: six_includes_one_bad_type)) { (error2: Error) -> Void in XCTAssertEqual( (error2 as? IncludesDecodingError).map(String.init(describing:)), - """ - Out of 6 includes, the 5th one failed to parse: \nCould not have been Include Type `test_entity1` because: - found JSON:API type "test_entity4" but expected "test_entity1" - - Could not have been Include Type `test_entity2` because: - found JSON:API type "test_entity4" but expected "test_entity2" - """ + "Out of the 6 includes in the document, the 5th one failed to parse: Found JSON:API type 'test_entity4' but expected one of 'test_entity1', 'test_entity2'" ) } @@ -60,13 +42,7 @@ final class IncludesDecodingErrorTests: XCTestCase { XCTAssertThrowsError(try testDecoder.decode(Includes>.self, from: eleven_includes_one_bad_type)) { (error2: Error) -> Void in XCTAssertEqual( (error2 as? IncludesDecodingError).map(String.init(describing:)), - """ - Out of 11 includes, the 10th one failed to parse: \nCould not have been Include Type `test_entity1` because: - found JSON:API type "test_entity4" but expected "test_entity1" - - Could not have been Include Type `test_entity2` because: - found JSON:API type "test_entity4" but expected "test_entity2" - """ + "Out of the 11 includes in the document, the 10th one failed to parse: Found JSON:API type 'test_entity4' but expected one of 'test_entity1', 'test_entity2'" ) } @@ -74,13 +50,7 @@ final class IncludesDecodingErrorTests: XCTestCase { XCTAssertThrowsError(try testDecoder.decode(Includes>.self, from: twenty_two_includes_one_bad_type)) { (error2: Error) -> Void in XCTAssertEqual( (error2 as? IncludesDecodingError).map(String.init(describing:)), - """ - Out of 22 includes, the 21st one failed to parse: \nCould not have been Include Type `test_entity1` because: - found JSON:API type "test_entity4" but expected "test_entity1" - - Could not have been Include Type `test_entity2` because: - found JSON:API type "test_entity4" but expected "test_entity2" - """ + "Out of the 22 includes in the document, the 21st one failed to parse: Found JSON:API type 'test_entity4' but expected one of 'test_entity1', 'test_entity2'" ) } } @@ -94,16 +64,7 @@ final class IncludesDecodingErrorTests: XCTestCase { ) { (error: Error) -> Void in XCTAssertEqual( (error as? IncludesDecodingError).map(String.init(describing:)), - """ - Out of 3 includes, the 3rd one failed to parse: \nCould not have been Include Type `test_entity1` because: - found JSON:API type "test_entity2" but expected "test_entity1" - - Could not have been Include Type `test_entity2` because: - 'foo' attribute is required and missing. - - Could not have been Include Type `test_entity4` because: - found JSON:API type "test_entity2" but expected "test_entity4" - """ + "Out of the 3 includes in the document, the 3rd one failed to parse: 'foo' attribute is required and missing." ) } } From cc57a60ce27ec5eb4a983872fc041fac6bb7e13a Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Mon, 21 Sep 2020 20:51:10 -0700 Subject: [PATCH 186/235] add better documentation on relationship metadata --- documentation/usage.md | 56 ++++++++++++++++++++++++++---------------- 1 file changed, 35 insertions(+), 21 deletions(-) diff --git a/documentation/usage.md b/documentation/usage.md index 5dc298f..30cebdc 100644 --- a/documentation/usage.md +++ b/documentation/usage.md @@ -11,6 +11,7 @@ In this documentation, in order to draw attention to the difference between the - [`RawIdType`](#rawidtype) - [Convenient `typealiases`](#convenient-typealiases) - [`JSONAPI.Relationships`](#jsonapirelationships) + - [Relationship Metadata](#relationship-metadata) - [`JSONAPI.Attributes`](#jsonapiattributes) - [`Transformer`](#transformer) - [`Validator`](#validator) @@ -149,6 +150,33 @@ Note that I am calling an unidentified person is a "new" person. This is general There are three types of `Relationships`: `MetaRelationship`, `ToOneRelationship` and `ToManyRelationship`. A `ResourceObjectDescription`'s `Relationships` type can contain any number of `Relationship` properties of any of these types. Do not store anything other than `Relationship` properties in the `Relationships` struct of a `ResourceObjectDescription`. +The `MetaRelationship` is special in that it represents a Relationship Object with no `data` (it must contain at least one of `meta` or `links`). The other two relationship types are Relationship Objects with either singular resource linkages (`ToOneRelationship`) or arrays of resource linkages (`ToManyRelationship`). + +To describe a relationship that may be omitted (i.e. the key is not even present in the JSON object), you make the entire `MetaRelationship`, `ToOneRelationship` or `ToManyRelationship` optional. +```swift +// note the question mark at the very end of the line. +let optionalRelative: ToOneRelationship? +``` + +A `ToOneRelationship` can be marked as nullable (i.e. the value could be either `null` or a resource identifier) like this: +```swift +// note the question mark just after `Person`. +let nullableRelative: ToOneRelationship +``` + +A `ToManyRelationship` can naturally represent the absence of related values with an empty array, so `ToManyRelationship` do not support nullability. + +A `ResourceObject` that does not have relationships can be described by adding the following to a `ResourceObjectDescription`: +```swift +typealias Relationships = NoRelationships +``` + +`Relationship` values boil down to `Ids` of other resource objects. To access the `Id` of a related `ResourceObject`, you can use the custom `~>` operator with the `KeyPath` of the `Relationship` from which you want the `Id`. The friends of the above `Person` `ResourceObject` can be accessed as follows (type annotations for clarity): +```swift +let friendIds: [Person.Id] = person ~> \.friends +``` + +#### Relationship Metadata In addition to identifying resource objects by ID and type, `Relationships` can contain `Meta` or `Links` that follow the same rules as [`Meta`](#jsonapimeta) and [`Links`](#jsonapilinks) elsewhere in the JSON:API Document. Metadata can be specified both in the Relationship Object and in the Resource Identifier Object. You specify the two types of metadata differently. As always, you can use `NoMetadata` to indicate you do not intend the JSON:API relationship to contain metadata. @@ -186,30 +214,16 @@ let relationship3: ToOneRelationship // ^ assumes `CoolMetadata` is a `Codable` struct defined elsewhere ``` -The `MetaRelationship` is special in that it represents a Relationship Object with no `data` (it must contain at least one of `meta` or `links`). The other two relationship types are Relationship Objects with either singular resource linkages (`ToOneRelationship`) or arrays of resource linkages (`ToManyRelationship`). +When you need metadata out of a to-one relationship, you can access the Relationship Object metadata with the `meta` property and the Resource Identifer metadata with the `idMeta` property. When you need metadata out of a to-many relationship, you can access the Relationship Object metadata with the `meta` property (there is only one such metadata object) and you can access the Resource Identifier metadata (of which there is one per related resource) by asking each element of the `idsWithMeta` property for its `meta` property. -To describe a relationship that may be omitted (i.e. the key is not even present in the JSON object), you make the entire `MetaRelationship`, `ToOneRelationship` or `ToManyRelationship` optional. ```swift -// note the question mark at the very end of the line. -let optionalRelative: ToOneRelationship? -``` +// to-one +let relation = entity.relationships.home +let idMeta = relation.idMeta -A `ToOneRelationship` can be marked as nullable (i.e. the value could be either `null` or a resource identifier) like this: -```swift -// note the question mark just after `Person`. -let nullableRelative: ToOneRelationship -``` - -A `ToManyRelationship` can naturally represent the absence of related values with an empty array, so `ToManyRelationship` do not support nullability. - -A `ResourceObject` that does not have relationships can be described by adding the following to a `ResourceObjectDescription`: -```swift -typealias Relationships = NoRelationships -``` - -`Relationship` values boil down to `Ids` of other resource objects. To access the `Id` of a related `ResourceObject`, you can use the custom `~>` operator with the `KeyPath` of the `Relationship` from which you want the `Id`. The friends of the above `Person` `ResourceObject` can be accessed as follows (type annotations for clarity): -```swift -let friendIds: [Person.Id] = person ~> \.friends +// to-many +let relations = entity.relationships.friends +let idMeta = relations.idsWithMeta.map { $0.meta } ``` ### `JSONAPI.Attributes` From 314891e212f13e1ed385aa975b650d60d4a5449b Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Mon, 21 Sep 2020 20:55:41 -0700 Subject: [PATCH 187/235] update the version of the JSONAPI library indicated in the README. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c42e248..c5a6f52 100644 --- a/README.md +++ b/README.md @@ -66,7 +66,7 @@ If you find something wrong with this library and it isn't already mentioned und ### Swift Package Manager Just include the following in your package's dependencies and add `JSONAPI` to the dependencies for any of your targets. ```swift -.package(url: "https://github.com/mattpolzin/JSONAPI.git", from: "4.0.0") +.package(url: "https://github.com/mattpolzin/JSONAPI.git", from: "5.0.0") ``` ### Xcode project From cc9f523c99b6da2134da4aef1de70fcddc7080ae Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Sat, 26 Sep 2020 12:23:08 -0700 Subject: [PATCH 188/235] Add a note about typealiases to the relationship section of the readme. --- documentation/usage.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/documentation/usage.md b/documentation/usage.md index 30cebdc..af7c05c 100644 --- a/documentation/usage.md +++ b/documentation/usage.md @@ -176,6 +176,8 @@ typealias Relationships = NoRelationships let friendIds: [Person.Id] = person ~> \.friends ``` +🗒You will likely find relationship types more ergonomic and easier to read if you create typealiases. For example, if your project never uses Relationship metadata or links, you can create a typealias like `typealias ToOne = JSONAPI.ToOneRelationship`. + #### Relationship Metadata In addition to identifying resource objects by ID and type, `Relationships` can contain `Meta` or `Links` that follow the same rules as [`Meta`](#jsonapimeta) and [`Links`](#jsonapilinks) elsewhere in the JSON:API Document. From 6a7072e57bc19263d2db8def6fbdec074f376f34 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Sat, 26 Sep 2020 12:52:55 -0700 Subject: [PATCH 189/235] update supported swift versions badge --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c5a6f52..248820d 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # JSONAPI -[![MIT license](http://img.shields.io/badge/license-MIT-lightgrey.svg)](http://opensource.org/licenses/MIT) [![Swift 5.2](http://img.shields.io/badge/Swift-5.2-blue.svg)](https://swift.org) [![Build Status](https://app.bitrise.io/app/c8295b9589aa401e/status.svg?token=vzcyqWD5bQ4xqQfZsaVzNw&branch=master)](https://app.bitrise.io/app/c8295b9589aa401e) +[![MIT license](http://img.shields.io/badge/license-MIT-lightgrey.svg)](http://opensource.org/licenses/MIT) [![Swift 5.2+](http://img.shields.io/badge/Swift-5.2/5.3-blue.svg)](https://swift.org) [![Build Status](https://app.bitrise.io/app/c8295b9589aa401e/status.svg?token=vzcyqWD5bQ4xqQfZsaVzNw&branch=master)](https://app.bitrise.io/app/c8295b9589aa401e) A Swift package for encoding to- and decoding from **JSON API** compliant requests and responses. From 92406d3707371ae77767f341c1d2278dbc3d2178 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Thu, 15 Oct 2020 20:18:36 -0700 Subject: [PATCH 190/235] better links documentation paired with a test to ensure documentation accuracy. --- .../ResourceObject/ResourceObjectTests.swift | 76 +++++++++++++++++++ documentation/usage.md | 65 ++++++++++++++++ 2 files changed, 141 insertions(+) diff --git a/Tests/JSONAPITests/ResourceObject/ResourceObjectTests.swift b/Tests/JSONAPITests/ResourceObject/ResourceObjectTests.swift index 6d3fb4f..d492b18 100644 --- a/Tests/JSONAPITests/ResourceObject/ResourceObjectTests.swift +++ b/Tests/JSONAPITests/ResourceObject/ResourceObjectTests.swift @@ -623,6 +623,24 @@ extension ResourceObjectTests { test_DecodeEncodeEquality(type: TestEntity4WithMetaAndLinks.self, data: entity_some_relationships_some_attributes_with_meta_and_links) } + + func test_fullLinksExample() { + let entity = decoded( + type: ResourceObjectLinksTest.Article.self, + data: ResourceObjectLinksTest.json.data(using: .utf8)! + ) + + XCTAssertEqual(entity.links.`self`.url.absoluteString, "http://example.com/articles/1") + XCTAssertEqual(entity.relationships.author.links.`self`.url.absoluteString, "http://example.com/articles/1/relationships/author") + XCTAssertEqual(entity.relationships.author.links.related.url.absoluteString, "http://example.com/articles/1/author") + } + + func test_fullLinksExample_encode() { + test_DecodeEncodeEquality( + type: ResourceObjectLinksTest.Article.self, + data: ResourceObjectLinksTest.json.data(using: .utf8)! + ) + } } // MARK: With a Meta Attribute @@ -963,3 +981,61 @@ extension ResourceObjectTests { let link1: Link } } + +extension Foundation.URL : JSONAPIURL {} + +enum ResourceObjectLinksTest { + struct PersonStubDescription: JSONAPI.ResourceObjectDescription { + static let jsonType: String = "people" + + typealias Attributes = NoAttributes + typealias Relationships = NoRelationships + } + + typealias Person = JSONAPI.ResourceObject + + struct ArticleAuthorRelationshipLinks: JSONAPI.Links { + let `self`: JSONAPI.Link + let related: JSONAPI.Link + } + + struct ArticleLinks: JSONAPI.Links { + let `self`: JSONAPI.Link + } + + struct ArticleDescription: JSONAPI.ResourceObjectDescription { + static let jsonType: String = "articles" + + struct Attributes: JSONAPI.Attributes { + let title: Attribute + } + + struct Relationships: JSONAPI.Relationships { + let author: ToOneRelationship + } + } + + typealias Article = JSONAPI.ResourceObject + + static let json = """ + { + "type": "articles", + "id": "1", + "attributes": { + "title": "Rails is Omakase" + }, + "relationships": { + "author": { + "links": { + "self": "http://example.com/articles/1/relationships/author", + "related": "http://example.com/articles/1/author" + }, + "data": { "type": "people", "id": "9" } + } + }, + "links": { + "self": "http://example.com/articles/1" + } + } + """ +} diff --git a/documentation/usage.md b/documentation/usage.md index af7c05c..2abafe4 100644 --- a/documentation/usage.md +++ b/documentation/usage.md @@ -509,6 +509,71 @@ A `Links` struct must contain only `Link` properties. Each `Link` property can e You can specify `NoLinks` if the part of the document being described should not contain any `Links`. +**IMPORTANT:** The URL type used in links is a type conforming to `JSONAPIURL`. Any type that is both `Codable` and `Equatable` is eligible, but it must be conformed explicitly. + +For example, +```swift +extension Foundation.URL: JSONAPIURL {} +extension String: JSONAPIURL {} +``` + +Here's an example of an "article" resource with some links object and some JSON it would be capable of parsing: +```swift +struct PersonStubDescription: JSONAPI.ResourceObjectDescription { + // this is just a pretend model to be used in a relationship below. + static let jsonType: String = "people" + + typealias Attributes = NoAttributes + typealias Relationships = NoRelationships +} + +typealias Person = JSONAPI.ResourceObject + +struct ArticleAuthorRelationshipLinks: JSONAPI.Links { + let `self`: JSONAPI.Link + let related: JSONAPI.Link +} + +struct ArticleLinks: JSONAPI.Links { + let `self`: JSONAPI.Link +} + +struct ArticleDescription: JSONAPI.ResourceObjectDescription { + static let jsonType: String = "articles" + + struct Attributes: JSONAPI.Attributes { + let title: Attribute + } + + struct Relationships: JSONAPI.Relationships { + let author: ToOneRelationship + } +} + +typealias Article = JSONAPI.ResourceObject +``` +```json +{ + "type": "articles", + "id": "1", + "attributes": { + "title": "Rails is Omakase" + }, + "relationships": { + "author": { + "links": { + "self": "http://example.com/articles/1/relationships/author", + "related": "http://example.com/articles/1/author" + }, + "data": { "type": "people", "id": "9" } + } + }, + "links": { + "self": "http://example.com/articles/1" + } +} +``` + ### `JSONAPI.RawIdType` If you want to create new `JSONAPI.ResourceObject` values and assign them Ids then you will need to conform at least one type to `CreatableRawIdType`. Doing so is easy; here are two example conformances for `UUID` and `String` (via `UUID`): From 4e6e0b1abb80bc30b909c852b156210f658e2c29 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Thu, 15 Oct 2020 20:29:12 -0700 Subject: [PATCH 191/235] Update usage.md fix typo --- documentation/usage.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/documentation/usage.md b/documentation/usage.md index 2abafe4..229136f 100644 --- a/documentation/usage.md +++ b/documentation/usage.md @@ -517,7 +517,7 @@ extension Foundation.URL: JSONAPIURL {} extension String: JSONAPIURL {} ``` -Here's an example of an "article" resource with some links object and some JSON it would be capable of parsing: +Here's an example of an "article" resource object with some links and the JSON it would be capable of parsing: ```swift struct PersonStubDescription: JSONAPI.ResourceObjectDescription { // this is just a pretend model to be used in a relationship below. From 5a8066184be02b45929dc497c4b6ce13b0da32b6 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Sun, 27 Dec 2020 21:22:32 -0800 Subject: [PATCH 192/235] Update tests.yml --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 0665ea8..4cd1974 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -33,7 +33,7 @@ jobs: runs-on: macOS-latest steps: - name: Select latest available Xcode - uses: maxim-lobanov/setup-xcode@1.0 + uses: maxim-lobanov/setup-xcode@v1.2.1 with: { 'xcode-version': 'latest' } - name: Checkout code uses: actions/checkout@v2 From cfe94883cc2bd26ebf8109054734aa1f9f4c701f Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Wed, 17 Feb 2021 23:00:36 -0800 Subject: [PATCH 193/235] don't bind on unused part of pattern match. --- .../Resource/Resource Object/ResourceObjectDecodingError.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/JSONAPI/Resource/Resource Object/ResourceObjectDecodingError.swift b/Sources/JSONAPI/Resource/Resource Object/ResourceObjectDecodingError.swift index 528f695..7ca4c04 100644 --- a/Sources/JSONAPI/Resource/Resource Object/ResourceObjectDecodingError.swift +++ b/Sources/JSONAPI/Resource/Resource Object/ResourceObjectDecodingError.swift @@ -77,7 +77,7 @@ public struct ResourceObjectDecodingError: Swift.Error, Equatable { init?(_ jsonAPIError: JSONAPICodingError, jsonAPIType: String) { self.resourceObjectJsonAPIType = jsonAPIType switch jsonAPIError { - case .typeMismatch(expected: let expected, found: let found, path: let path): + case .typeMismatch(expected: _, found: let found, path: let path): (location, subjectName) = Self.context(path: path) cause = .jsonTypeMismatch(foundType: found) case .quantityMismatch(expected: let expected, path: let path): From 160f67326c838ab539d764ed9dacf00d47407dc9 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Wed, 17 Feb 2021 23:02:17 -0800 Subject: [PATCH 194/235] add nightly swift 5.4 docker to CI --- .github/workflows/tests.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 4cd1974..b236eb3 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -23,6 +23,7 @@ jobs: - swift:5.3-focal - swift:5.3-centos8 - swift:5.3-amazonlinux2 + - swiftlang/swift:nightly-5.4-focal container: ${{ matrix.image }} steps: - name: Checkout code From 0f792996621fd845bf30f65d3e3db57cb0acc833 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Sat, 6 Mar 2021 15:58:54 -0800 Subject: [PATCH 195/235] see if its just the weirdness around the id name that is tripping the compiler up --- Sources/JSONAPI/Resource/Resource Object/ResourceObject.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/JSONAPI/Resource/Resource Object/ResourceObject.swift b/Sources/JSONAPI/Resource/Resource Object/ResourceObject.swift index a9c10d8..beef565 100644 --- a/Sources/JSONAPI/Resource/Resource Object/ResourceObject.swift +++ b/Sources/JSONAPI/Resource/Resource Object/ResourceObject.swift @@ -167,8 +167,8 @@ extension ResourceObject: JSONAPIIdentifiable, IdentifiableResourceObjectType, R public typealias ID = ResourceObject.Id } -@available(OSX 10.15, iOS 13, tvOS 13, watchOS 6, *) -extension ResourceObject: Swift.Identifiable where EntityRawIdType: JSONAPI.RawIdType {} +//@available(OSX 10.15, iOS 13, tvOS 13, watchOS 6, *) +//extension ResourceObject: Swift.Identifiable where EntityRawIdType: JSONAPI.RawIdType {} extension ResourceObject: CustomStringConvertible { public var description: String { From 17a68c9741a712849ea77a768a2616791857f214 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Sat, 6 Mar 2021 16:01:58 -0800 Subject: [PATCH 196/235] Revert "see if its just the weirdness around the id name that is tripping the compiler up" This reverts commit 0f792996621fd845bf30f65d3e3db57cb0acc833. --- Sources/JSONAPI/Resource/Resource Object/ResourceObject.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/JSONAPI/Resource/Resource Object/ResourceObject.swift b/Sources/JSONAPI/Resource/Resource Object/ResourceObject.swift index beef565..a9c10d8 100644 --- a/Sources/JSONAPI/Resource/Resource Object/ResourceObject.swift +++ b/Sources/JSONAPI/Resource/Resource Object/ResourceObject.swift @@ -167,8 +167,8 @@ extension ResourceObject: JSONAPIIdentifiable, IdentifiableResourceObjectType, R public typealias ID = ResourceObject.Id } -//@available(OSX 10.15, iOS 13, tvOS 13, watchOS 6, *) -//extension ResourceObject: Swift.Identifiable where EntityRawIdType: JSONAPI.RawIdType {} +@available(OSX 10.15, iOS 13, tvOS 13, watchOS 6, *) +extension ResourceObject: Swift.Identifiable where EntityRawIdType: JSONAPI.RawIdType {} extension ResourceObject: CustomStringConvertible { public var description: String { From 87d43cc354d259209ec649930c3e0509e1574a8f Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Sat, 6 Mar 2021 16:02:19 -0800 Subject: [PATCH 197/235] See if using name explicitly helps. --- .../Resource/Resource Object/ResourceObject+Replacing.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/JSONAPI/Resource/Resource Object/ResourceObject+Replacing.swift b/Sources/JSONAPI/Resource/Resource Object/ResourceObject+Replacing.swift index 98ef37c..9960a86 100644 --- a/Sources/JSONAPI/Resource/Resource Object/ResourceObject+Replacing.swift +++ b/Sources/JSONAPI/Resource/Resource Object/ResourceObject+Replacing.swift @@ -15,7 +15,7 @@ public extension JSONAPI.ResourceObject { /// - parameters: /// - replacement: A function that takes the existing `attributes` and returns the replacement. func replacingAttributes(_ replacement: (Description.Attributes) -> Description.Attributes) -> Self { - return Self(id: id, + return ResourceObject(id: id, attributes: replacement(attributes), relationships: relationships, meta: meta, From a8c4bf67d8d84d99c60f9f01adca16d9567fe9ed Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Sat, 6 Mar 2021 16:10:42 -0800 Subject: [PATCH 198/235] revert Self -> ResourceObject change but also try swapping out a typealias reference that seems to be confusing to the compiler for the thing that typealias references. --- .../Resource/Resource Object/ResourceObject+Replacing.swift | 2 +- Sources/JSONAPI/Resource/Resource Object/ResourceObject.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/JSONAPI/Resource/Resource Object/ResourceObject+Replacing.swift b/Sources/JSONAPI/Resource/Resource Object/ResourceObject+Replacing.swift index 9960a86..98ef37c 100644 --- a/Sources/JSONAPI/Resource/Resource Object/ResourceObject+Replacing.swift +++ b/Sources/JSONAPI/Resource/Resource Object/ResourceObject+Replacing.swift @@ -15,7 +15,7 @@ public extension JSONAPI.ResourceObject { /// - parameters: /// - replacement: A function that takes the existing `attributes` and returns the replacement. func replacingAttributes(_ replacement: (Description.Attributes) -> Description.Attributes) -> Self { - return ResourceObject(id: id, + return Self(id: id, attributes: replacement(attributes), relationships: relationships, meta: meta, diff --git a/Sources/JSONAPI/Resource/Resource Object/ResourceObject.swift b/Sources/JSONAPI/Resource/Resource Object/ResourceObject.swift index a9c10d8..77d367e 100644 --- a/Sources/JSONAPI/Resource/Resource Object/ResourceObject.swift +++ b/Sources/JSONAPI/Resource/Resource Object/ResourceObject.swift @@ -128,7 +128,7 @@ public struct ResourceObject /// The JSON API compliant attributes of this `ResourceObject`. public let attributes: Description.Attributes From eb9b1aafc11a7da44344cafbccafb60e0a9f6be7 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Sun, 7 Mar 2021 12:06:54 -0800 Subject: [PATCH 199/235] Update README.md Add notes about (lack of) Carthage support. --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index 248820d..3a81bd5 100644 --- a/README.md +++ b/README.md @@ -79,6 +79,11 @@ pod 'Poly', :git => 'https://github.com/mattpolzin/Poly.git' pod 'MP-JSONAPI', :git => 'https://github.com/mattpolzin/JSONAPI.git' ``` +### Carthage +This library does not support the Carthage package manager. This is intentional to avoid an additional dependency on Xcode and the Xcode's project files as their format changes throughout versions (in addition to the complexity of maintaining different shared schemes for each supported operating system). + +The difference between supporting and not supporting Carthage is the difference between maintaining an Xcode project with at least one shared build scheme; I encourage those that need Carthage support to fork this repository and add support to their fork by committing an Xcode project (you can generate one as described in the [Xcode project](#xcode-project) section above). Once an Xcode project is generated, you need to mark at least one scheme as [shared](https://github.com/Carthage/Carthage#share-your-xcode-schemes). + ### Running the Playground To run the included Playground files, create an Xcode project using Swift Package Manager, then create an Xcode Workspace in the root of the repository and add both the generated Xcode project and the playground to the Workspace. From c00a682a8b11c472c5a5f5478666937eb1a293ab Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Thu, 10 Jun 2021 20:30:00 -0700 Subject: [PATCH 200/235] Add full swift 5.4 CI coverage. --- .github/workflows/tests.yml | 8 ++++++-- README.md | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index b236eb3..2e0ea6f 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -4,7 +4,7 @@ on: pull_request: push: branches: - - master + - main jobs: linux: @@ -23,7 +23,11 @@ jobs: - swift:5.3-focal - swift:5.3-centos8 - swift:5.3-amazonlinux2 - - swiftlang/swift:nightly-5.4-focal + - swift:5.4-xenial + - swift:5.4-bionic + - swift:5.4-focal + - swift:5.4-centos8 + - swift:5.4-amazonlinux2 container: ${{ matrix.image }} steps: - name: Checkout code diff --git a/README.md b/README.md index 3a81bd5..cac438f 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # JSONAPI -[![MIT license](http://img.shields.io/badge/license-MIT-lightgrey.svg)](http://opensource.org/licenses/MIT) [![Swift 5.2+](http://img.shields.io/badge/Swift-5.2/5.3-blue.svg)](https://swift.org) [![Build Status](https://app.bitrise.io/app/c8295b9589aa401e/status.svg?token=vzcyqWD5bQ4xqQfZsaVzNw&branch=master)](https://app.bitrise.io/app/c8295b9589aa401e) +[![MIT license](http://img.shields.io/badge/license-MIT-lightgrey.svg)](http://opensource.org/licenses/MIT) [![Swift 5.2+](http://img.shields.io/badge/Swift-5.2/5.3/5.4-blue.svg)](https://swift.org) [![Build Status](https://app.bitrise.io/app/c8295b9589aa401e/status.svg?token=vzcyqWD5bQ4xqQfZsaVzNw&branch=master)](https://app.bitrise.io/app/c8295b9589aa401e) A Swift package for encoding to- and decoding from **JSON API** compliant requests and responses. From be207b0178042a6e5f7bab8376407c314311f5e5 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Thu, 10 Jun 2021 20:33:13 -0700 Subject: [PATCH 201/235] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index cac438f..878fcdf 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # JSONAPI -[![MIT license](http://img.shields.io/badge/license-MIT-lightgrey.svg)](http://opensource.org/licenses/MIT) [![Swift 5.2+](http://img.shields.io/badge/Swift-5.2/5.3/5.4-blue.svg)](https://swift.org) [![Build Status](https://app.bitrise.io/app/c8295b9589aa401e/status.svg?token=vzcyqWD5bQ4xqQfZsaVzNw&branch=master)](https://app.bitrise.io/app/c8295b9589aa401e) +[![MIT license](http://img.shields.io/badge/license-MIT-lightgrey.svg)](http://opensource.org/licenses/MIT) [![Swift 5.2+](http://img.shields.io/badge/Swift-5.2/5.3/5.4-blue.svg)](https://swift.org) [![Build Status](https://app.bitrise.io/app/c8295b9589aa401e/status.svg?token=vzcyqWD5bQ4xqQfZsaVzNw&branch=main)](https://app.bitrise.io/app/c8295b9589aa401e) A Swift package for encoding to- and decoding from **JSON API** compliant requests and responses. From 33801676ed9c73c79b2f06c0cfb272e49e610d69 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Thu, 17 Jun 2021 08:59:45 -0700 Subject: [PATCH 202/235] Add some examples around heterogeneous primary resource bodies. --- documentation/usage.md | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/documentation/usage.md b/documentation/usage.md index 229136f..e02c797 100644 --- a/documentation/usage.md +++ b/documentation/usage.md @@ -335,10 +335,19 @@ A JSON API Document is guaranteed by the **SPEC** to be "data", "metadata", or " #### `ResourceBody` -The first generic type of a `JSONAPI.Document` is a `ResourceBody`. This can either be a `SingleResourceBody` or a `ManyResourceBody`. You will find zero or one `PrimaryResource` values in a JSON API document that has a `SingleResourceBody` and you will find zero or more `PrimaryResource` values in a JSON API document that has a `ManyResourceBody`. You can use the `Poly` types (`Poly1` through `Poly6`) to specify that a `ResourceBody` will be one of a few different types of `ResourceObject`. These `Poly` types work in the same way as the `Include` types described below. +The first generic type of a `JSONAPI.Document` is a `ResourceBody`. This can either be a `SingleResourceBody` or a `ManyResourceBody`. You will find zero or one `PrimaryResource` values in a JSON API document that has a `SingleResourceBody` and you will find zero or more `PrimaryResource` values in a JSON API document that has a `ManyResourceBody`. You can use the `Poly` types (`Poly1` through `Poly11`) to specify that a `ResourceBody` will be one of a few different types of `ResourceObject`. These `Poly` types work in the same way as the `Include` types described below. If you expect a response to not have a "data" top-level key at all, then use `NoResourceBody` instead. +Examples: +```swift +typealias SingleDog = JSONAPI.Document, NoMetadata, NoLinks, NoIncludes, BasicJSONAPIError> + +typealias ManyCats = JSONAPI.Document, NoMetadata, NoLinks, NoIncludes, BasicJSONAPIError> + +typealias ManyDogsOrCats = JSONAPI.Document>, NoMetadata, NoLinks, NoIncludes, BasicJSONAPIError> +``` + ##### nullable `PrimaryResource` If you expect a `SingleResourceBody` to sometimes come back `null`, you should make your `PrimaryResource` optional. If you do not make your `PrimaryResource` optional then a `null` primary resource will be considered an error when parsing the JSON. From b8b94a4c8641af22a9c8cc58952ffb56e1c3975f Mon Sep 17 00:00:00 2001 From: = <=> Date: Sun, 6 Mar 2022 00:31:39 -0800 Subject: [PATCH 203/235] loosen podspec Poly requirement. Bump patch version. --- JSONAPI.podspec | 4 ++-- README.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/JSONAPI.podspec b/JSONAPI.podspec index 5cf8785..19e5c0e 100644 --- a/JSONAPI.podspec +++ b/JSONAPI.podspec @@ -16,7 +16,7 @@ Pod::Spec.new do |spec| # spec.name = "MP-JSONAPI" - spec.version = "5.0.0" + spec.version = "5.0.2" spec.summary = "Swift Codable JSON API framework." # 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.xcconfig = { "HEADER_SEARCH_PATHS" => "$(SDKROOT)/usr/include/libxml2" } - spec.dependency "Poly", "~> 2.4.0" + spec.dependency "Poly", "~> 2.4" end diff --git a/README.md b/README.md index 878fcdf..c74fb68 100644 --- a/README.md +++ b/README.md @@ -66,7 +66,7 @@ If you find something wrong with this library and it isn't already mentioned und ### Swift Package Manager Just include the following in your package's dependencies and add `JSONAPI` to the dependencies for any of your targets. ```swift -.package(url: "https://github.com/mattpolzin/JSONAPI.git", from: "5.0.0") +.package(url: "https://github.com/mattpolzin/JSONAPI.git", from: "5.0.2") ``` ### Xcode project From 575199e775d6afebcbb497edeccbbe2c7933d7cc Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Tue, 23 Aug 2022 22:07:44 -0700 Subject: [PATCH 204/235] Add Include12 and Include13 types. --- .github/workflows/tests.yml | 5 + Package.resolved | 4 +- Package.swift | 2 +- .../JSONAPI/Document/CompoundResource.swift | 2 +- Sources/JSONAPI/Document/Includes.swift | 16 ++ .../Resource/Poly+PrimaryResource.swift | 64 ++++++ .../Comparisons/DocumentDataCompare.swift | 2 +- .../JSONAPITests/Includes/IncludeTests.swift | 69 +++++++ .../Includes/stubs/IncludeStubs.swift | 189 ++++++++++++++++++ 9 files changed, 348 insertions(+), 5 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 2e0ea6f..3386cdf 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -28,6 +28,11 @@ jobs: - swift:5.4-focal - swift:5.4-centos8 - swift:5.4-amazonlinux2 + - swift:5.5-xenial + - swift:5.5-bionic + - swift:5.5-focal + - swift:5.5-centos8 + - swift:5.5-amazonlinux2 container: ${{ matrix.image }} steps: - name: Checkout code diff --git a/Package.resolved b/Package.resolved index 63465c7..8a582ff 100644 --- a/Package.resolved +++ b/Package.resolved @@ -6,8 +6,8 @@ "repositoryURL": "https://github.com/mattpolzin/Poly.git", "state": { "branch": null, - "revision": "36ba3f624bffa34f5f9b9c7648eab3cfdcab4748", - "version": "2.5.0" + "revision": "c108e9e0a2904134719b082f6c18d64406afc6db", + "version": "2.6.0" } } ] diff --git a/Package.swift b/Package.swift index 3762356..e7a0e51 100644 --- a/Package.swift +++ b/Package.swift @@ -17,7 +17,7 @@ let package = Package( targets: ["JSONAPITesting"]) ], dependencies: [ - .package(url: "https://github.com/mattpolzin/Poly.git", .upToNextMajor(from: "2.4.0")), + .package(url: "https://github.com/mattpolzin/Poly.git", .upToNextMajor(from: "2.6.0")), ], targets: [ .target( diff --git a/Sources/JSONAPI/Document/CompoundResource.swift b/Sources/JSONAPI/Document/CompoundResource.swift index 9ad84fe..9baaf2f 100644 --- a/Sources/JSONAPI/Document/CompoundResource.swift +++ b/Sources/JSONAPI/Document/CompoundResource.swift @@ -71,7 +71,7 @@ extension Sequence where Element: CompoundResourceProtocol { } } -extension EncodableJSONAPIDocument where PrimaryResourceBody: EncodableResourceBody, PrimaryResourceBody.PrimaryResource: ResourceObjectType { +extension EncodableJSONAPIDocument where PrimaryResourceBody.PrimaryResource: ResourceObjectType { public typealias CompoundResource = JSONAPI.CompoundResource } diff --git a/Sources/JSONAPI/Document/Includes.swift b/Sources/JSONAPI/Document/Includes.swift index e48a547..30634ae 100644 --- a/Sources/JSONAPI/Document/Includes.swift +++ b/Sources/JSONAPI/Document/Includes.swift @@ -206,6 +206,22 @@ extension Includes where I: _Poly11 { } } +// MARK: - 12 includes +public typealias Include12 = Poly12 +extension Includes where I: _Poly12 { + public subscript(_ lookup: I.L.Type) -> [I.L] { + return values.compactMap(\.l) + } +} + +// MARK: - 13 includes +public typealias Include13 = Poly13 +extension Includes where I: _Poly13 { + public subscript(_ lookup: I.M.Type) -> [I.M] { + return values.compactMap(\.m) + } +} + // MARK: - DecodingError public struct IncludesDecodingError: Swift.Error, Equatable { public let error: Swift.Error diff --git a/Sources/JSONAPI/Resource/Poly+PrimaryResource.swift b/Sources/JSONAPI/Resource/Poly+PrimaryResource.swift index 73c1246..0dab925 100644 --- a/Sources/JSONAPI/Resource/Poly+PrimaryResource.swift +++ b/Sources/JSONAPI/Resource/Poly+PrimaryResource.swift @@ -162,3 +162,67 @@ extension Poly11: CodablePrimaryResource, OptionalCodablePrimaryResource I: CodablePolyWrapped, J: CodablePolyWrapped, K: CodablePolyWrapped {} + +// MARK: - 12 types +extension Poly12: EncodablePrimaryResource, OptionalEncodablePrimaryResource +where +A: EncodablePolyWrapped, +B: EncodablePolyWrapped, +C: EncodablePolyWrapped, +D: EncodablePolyWrapped, +E: EncodablePolyWrapped, +F: EncodablePolyWrapped, +G: EncodablePolyWrapped, +H: EncodablePolyWrapped, +I: EncodablePolyWrapped, +J: EncodablePolyWrapped, +K: EncodablePolyWrapped, +L: EncodablePolyWrapped {} + +extension Poly12: CodablePrimaryResource, OptionalCodablePrimaryResource +where +A: CodablePolyWrapped, +B: CodablePolyWrapped, +C: CodablePolyWrapped, +D: CodablePolyWrapped, +E: CodablePolyWrapped, +F: CodablePolyWrapped, +G: CodablePolyWrapped, +H: CodablePolyWrapped, +I: CodablePolyWrapped, +J: CodablePolyWrapped, +K: CodablePolyWrapped, +L: CodablePolyWrapped {} + +// MARK: - 13 types +extension Poly13: EncodablePrimaryResource, OptionalEncodablePrimaryResource +where +A: EncodablePolyWrapped, +B: EncodablePolyWrapped, +C: EncodablePolyWrapped, +D: EncodablePolyWrapped, +E: EncodablePolyWrapped, +F: EncodablePolyWrapped, +G: EncodablePolyWrapped, +H: EncodablePolyWrapped, +I: EncodablePolyWrapped, +J: EncodablePolyWrapped, +K: EncodablePolyWrapped, +L: EncodablePolyWrapped, +M: EncodablePolyWrapped {} + +extension Poly13: CodablePrimaryResource, OptionalCodablePrimaryResource +where +A: CodablePolyWrapped, +B: CodablePolyWrapped, +C: CodablePolyWrapped, +D: CodablePolyWrapped, +E: CodablePolyWrapped, +F: CodablePolyWrapped, +G: CodablePolyWrapped, +H: CodablePolyWrapped, +I: CodablePolyWrapped, +J: CodablePolyWrapped, +K: CodablePolyWrapped, +L: CodablePolyWrapped, +M: CodablePolyWrapped {} diff --git a/Sources/JSONAPITesting/Comparisons/DocumentDataCompare.swift b/Sources/JSONAPITesting/Comparisons/DocumentDataCompare.swift index da7f27e..be2cad6 100644 --- a/Sources/JSONAPITesting/Comparisons/DocumentDataCompare.swift +++ b/Sources/JSONAPITesting/Comparisons/DocumentDataCompare.swift @@ -86,7 +86,7 @@ public struct ManyResourceObjectComparison: Equatable, PropertyComparison { } } -extension TestableResourceBody where TestablePrimaryResourceType: ResourceObjectType { +extension TestableResourceBody { public func compare(to other: Self) -> PrimaryResourceBodyComparison { guard let one = testableResourceObject, let two = other.testableResourceObject else { diff --git a/Tests/JSONAPITests/Includes/IncludeTests.swift b/Tests/JSONAPITests/Includes/IncludeTests.swift index 6da21e5..e0a44a4 100644 --- a/Tests/JSONAPITests/Includes/IncludeTests.swift +++ b/Tests/JSONAPITests/Includes/IncludeTests.swift @@ -219,6 +219,53 @@ class IncludedTests: XCTestCase { test_DecodeEncodeEquality(type: Includes>.self, data: eleven_different_type_includes) } + + func test_TwelveDifferentIncludes() { + let includes = decoded(type: Includes>.self, + data: twelve_different_type_includes) + + XCTAssertEqual(includes[TestEntity.self].count, 1) + XCTAssertEqual(includes[TestEntity2.self].count, 1) + XCTAssertEqual(includes[TestEntity3.self].count, 1) + XCTAssertEqual(includes[TestEntity4.self].count, 1) + XCTAssertEqual(includes[TestEntity5.self].count, 1) + XCTAssertEqual(includes[TestEntity6.self].count, 1) + XCTAssertEqual(includes[TestEntity7.self].count, 1) + XCTAssertEqual(includes[TestEntity8.self].count, 1) + XCTAssertEqual(includes[TestEntity9.self].count, 1) + XCTAssertEqual(includes[TestEntity10.self].count, 1) + XCTAssertEqual(includes[TestEntity11.self].count, 1) + XCTAssertEqual(includes[TestEntity12.self].count, 1) + } + + func test_TwelveDifferentIncludes_encode() { + test_DecodeEncodeEquality(type: Includes>.self, + data: twelve_different_type_includes) + } + + func test_ThirteenDifferentIncludes() { + let includes = decoded(type: Includes>.self, + data: thirteen_different_type_includes) + + XCTAssertEqual(includes[TestEntity.self].count, 1) + XCTAssertEqual(includes[TestEntity2.self].count, 1) + XCTAssertEqual(includes[TestEntity3.self].count, 1) + XCTAssertEqual(includes[TestEntity4.self].count, 1) + XCTAssertEqual(includes[TestEntity5.self].count, 1) + XCTAssertEqual(includes[TestEntity6.self].count, 1) + XCTAssertEqual(includes[TestEntity7.self].count, 1) + XCTAssertEqual(includes[TestEntity8.self].count, 1) + XCTAssertEqual(includes[TestEntity9.self].count, 1) + XCTAssertEqual(includes[TestEntity10.self].count, 1) + XCTAssertEqual(includes[TestEntity11.self].count, 1) + XCTAssertEqual(includes[TestEntity12.self].count, 1) + XCTAssertEqual(includes[TestEntity13.self].count, 1) + } + + func test_ThirteenDifferentIncludes_encode() { + test_DecodeEncodeEquality(type: Includes>.self, + data: thirteen_different_type_includes) + } } // MARK: - Appending @@ -536,4 +583,26 @@ extension IncludedTests { } typealias TestEntity11 = BasicEntity + + enum TestEntityType12: ResourceObjectDescription { + + typealias Attributes = NoAttributes + + public static var jsonType: String { return "test_entity12" } + + typealias Relationships = NoRelationships + } + + typealias TestEntity12 = BasicEntity + + enum TestEntityType13: ResourceObjectDescription { + + typealias Attributes = NoAttributes + + public static var jsonType: String { return "test_entity13" } + + typealias Relationships = NoRelationships + } + + typealias TestEntity13 = BasicEntity } diff --git a/Tests/JSONAPITests/Includes/stubs/IncludeStubs.swift b/Tests/JSONAPITests/Includes/stubs/IncludeStubs.swift index 708e154..2cfd3fa 100644 --- a/Tests/JSONAPITests/Includes/stubs/IncludeStubs.swift +++ b/Tests/JSONAPITests/Includes/stubs/IncludeStubs.swift @@ -687,6 +687,195 @@ let eleven_different_type_includes = """ ] """.data(using: .utf8)! +let twelve_different_type_includes = """ +[ + { + "type": "test_entity1", + "id": "2DF03B69-4B0A-467F-B52E-B0C9E44FCECF", + "attributes": { + "foo": "Hello", + "bar": 123 + } + }, + { + "type": "test_entity2", + "id": "90F03B69-4DF1-467F-B52E-B0C9E44FC333", + "attributes": { + "foo": "World", + "bar": 456 + }, + "relationships": { + "entity1": { + "data": { + "type": "test_entity1", + "id": "2DF03B69-4B0A-467F-B52E-B0C9E44FCECF" + } + } + } + }, + { + "type": "test_entity3", + "id": "11223B69-4DF1-467F-B52E-B0C9E44FC443", + "relationships": { + "entity1": { + "data": { + "type": "test_entity1", + "id": "2DF03B69-4B0A-467F-B52E-B0C9E44FCECF" + } + }, + "entity2": { + "data": [ + { + "type": "test_entity2", + "id": "90F03B69-4DF1-467F-B52E-B0C9E44FC333" + } + ] + } + } + }, + { + "type": "test_entity6", + "id": "11113B69-4DF1-467F-B52E-B0C9E44FC444", + "relationships": { + "entity4": { + "data": { + "type": "test_entity4", + "id": "364B3B69-4DF1-467F-B52E-B0C9E44F666E" + } + } + } + }, + { + "type": "test_entity5", + "id": "A24B3B69-4DF1-467F-B52E-B0C9E44F436A" + }, + { + "type": "test_entity4", + "id": "364B3B69-4DF1-467F-B52E-B0C9E44F666E" + }, + { + "type": "test_entity7", + "id": "364B3B69-4DF1-222F-B52E-B0C9E44F666E" + }, + { + "type": "test_entity8", + "id": "364B3B69-4DF1-222F-B52E-B0C9E44F266F" + }, + { + "type": "test_entity9", + "id": "364B3B69-4DF1-218F-B52E-B0C9E44F2661" + }, + { + "type": "test_entity10", + "id": "264B3B69-4DF1-212F-B52E-B0C9E44F2660" + }, + { + "type": "test_entity11", + "id": "264B3B69-4DF3-212F-B32E-A0C9E44F26C0B" + }, + { + "type": "test_entity12", + "id": "264B3B69-4DF3-212F-B32E-A0C9E44F26C00" + } +] +""".data(using: .utf8)! + +let thirteen_different_type_includes = """ +[ + { + "type": "test_entity1", + "id": "2DF03B69-4B0A-467F-B52E-B0C9E44FCECF", + "attributes": { + "foo": "Hello", + "bar": 123 + } + }, + { + "type": "test_entity2", + "id": "90F03B69-4DF1-467F-B52E-B0C9E44FC333", + "attributes": { + "foo": "World", + "bar": 456 + }, + "relationships": { + "entity1": { + "data": { + "type": "test_entity1", + "id": "2DF03B69-4B0A-467F-B52E-B0C9E44FCECF" + } + } + } + }, + { + "type": "test_entity3", + "id": "11223B69-4DF1-467F-B52E-B0C9E44FC443", + "relationships": { + "entity1": { + "data": { + "type": "test_entity1", + "id": "2DF03B69-4B0A-467F-B52E-B0C9E44FCECF" + } + }, + "entity2": { + "data": [ + { + "type": "test_entity2", + "id": "90F03B69-4DF1-467F-B52E-B0C9E44FC333" + } + ] + } + } + }, + { + "type": "test_entity6", + "id": "11113B69-4DF1-467F-B52E-B0C9E44FC444", + "relationships": { + "entity4": { + "data": { + "type": "test_entity4", + "id": "364B3B69-4DF1-467F-B52E-B0C9E44F666E" + } + } + } + }, + { + "type": "test_entity5", + "id": "A24B3B69-4DF1-467F-B52E-B0C9E44F436A" + }, + { + "type": "test_entity4", + "id": "364B3B69-4DF1-467F-B52E-B0C9E44F666E" + }, + { + "type": "test_entity7", + "id": "364B3B69-4DF1-222F-B52E-B0C9E44F666E" + }, + { + "type": "test_entity8", + "id": "364B3B69-4DF1-222F-B52E-B0C9E44F266F" + }, + { + "type": "test_entity9", + "id": "364B3B69-4DF1-218F-B52E-B0C9E44F2661" + }, + { + "type": "test_entity10", + "id": "264B3B69-4DF1-212F-B52E-B0C9E44F2660" + }, + { + "type": "test_entity11", + "id": "264B3B69-4DF3-212F-B32E-A0C9E44F26C0B" + }, + { + "type": "test_entity12", + "id": "264B3B69-4DF3-212F-B32E-A0C9E44F26C00" + }, + { + "type": "test_entity13", + "id": "264B3B69-4DF3-212F-B32E-A0C9E44F26C01" + } +] +""".data(using: .utf8)! let three_includes_one_missing_attributes = """ [ From 91639846031a4a3fae32d1b470b5cf404c2833f9 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Tue, 23 Aug 2022 22:08:56 -0700 Subject: [PATCH 205/235] bump podspec versions --- JSONAPI.podspec | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/JSONAPI.podspec b/JSONAPI.podspec index 19e5c0e..4a22764 100644 --- a/JSONAPI.podspec +++ b/JSONAPI.podspec @@ -16,7 +16,7 @@ Pod::Spec.new do |spec| # spec.name = "MP-JSONAPI" - spec.version = "5.0.2" + spec.version = "5.1.0" spec.summary = "Swift Codable JSON API framework." # 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.xcconfig = { "HEADER_SEARCH_PATHS" => "$(SDKROOT)/usr/include/libxml2" } - spec.dependency "Poly", "~> 2.4" + spec.dependency "Poly", "~> 2.6" end From f7e5df3d5e21b385587c368d80e90a1f736beb76 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Tue, 23 Aug 2022 22:09:51 -0700 Subject: [PATCH 206/235] update readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c74fb68..c42c9d7 100644 --- a/README.md +++ b/README.md @@ -66,7 +66,7 @@ If you find something wrong with this library and it isn't already mentioned und ### Swift Package Manager Just include the following in your package's dependencies and add `JSONAPI` to the dependencies for any of your targets. ```swift -.package(url: "https://github.com/mattpolzin/JSONAPI.git", from: "5.0.2") +.package(url: "https://github.com/mattpolzin/JSONAPI.git", from: "5.1.0") ``` ### Xcode project From 022611713f5a99294ab1c46ea6d63bc481e89610 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Tue, 23 Aug 2022 22:11:21 -0700 Subject: [PATCH 207/235] Add swift 5.6 linux images to tests as well. --- .github/workflows/tests.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 3386cdf..00fc0fe 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -33,6 +33,11 @@ jobs: - swift:5.5-focal - swift:5.5-centos8 - swift:5.5-amazonlinux2 + - swift:5.6-xenial + - swift:5.6-bionic + - swift:5.6-focal + - swift:5.6-centos8 + - swift:5.6-amazonlinux2 container: ${{ matrix.image }} steps: - name: Checkout code From a6ffeab1cb225f87323f218b327b84c83a908e00 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Tue, 23 Aug 2022 22:24:55 -0700 Subject: [PATCH 208/235] See if this fixes latest linux swift versions. --- .../Resource Object/ResourceObjectDecodingError.swift | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/Sources/JSONAPI/Resource/Resource Object/ResourceObjectDecodingError.swift b/Sources/JSONAPI/Resource/Resource Object/ResourceObjectDecodingError.swift index 7ca4c04..72f0a2a 100644 --- a/Sources/JSONAPI/Resource/Resource Object/ResourceObjectDecodingError.swift +++ b/Sources/JSONAPI/Resource/Resource Object/ResourceObjectDecodingError.swift @@ -117,9 +117,16 @@ public struct ResourceObjectDecodingError: Swift.Error, Equatable { location = .type } + let subjectPath: [CodingKey] + if location == .relationships && path.last?.stringValue == "data" { + subjectPath = path.dropLast() + } else { + subjectPath = path + } + return ( location, - name: path.last?.stringValue ?? "unnamed" + name: subjectPath.last?.stringValue ?? "unnamed" ) } } From fd412a4ccd9e3b6fa36ca7e36931a2c00708689a Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Tue, 23 Aug 2022 22:49:31 -0700 Subject: [PATCH 209/235] slowly fixing latest swift version linux tests --- .../ResourceObjectDecodingErrorTests.swift | 96 ++++++++++++++----- 1 file changed, 72 insertions(+), 24 deletions(-) diff --git a/Tests/JSONAPITests/ResourceObject/ResourceObjectDecodingErrorTests.swift b/Tests/JSONAPITests/ResourceObject/ResourceObjectDecodingErrorTests.swift index 6597293..3003dae 100644 --- a/Tests/JSONAPITests/ResourceObject/ResourceObjectDecodingErrorTests.swift +++ b/Tests/JSONAPITests/ResourceObject/ResourceObjectDecodingErrorTests.swift @@ -103,20 +103,44 @@ final class ResourceObjectDecodingErrorTests: XCTestCase { TestEntity.self, from: entity_nonNullable_relationship_is_null )) { error in - XCTAssertEqual( - error as? ResourceObjectDecodingError, - ResourceObjectDecodingError( - subjectName: "required", - cause: .valueNotFound, - location: .relationships, - jsonAPIType: TestEntity.jsonType + let specialError = error as? ResourceObjectDecodingError + + XCTAssertNotNil(specialError) + + // later Linux versions of Swift will catch that the value is not + // a string rather than calling null "not found." The errors are both + // effective, so we check that one of the two is the result: + if specialError?.cause == .valueNotFound { + XCTAssertEqual( + specialError, + ResourceObjectDecodingError( + subjectName: "required", + cause: .valueNotFound, + location: .relationships, + jsonAPIType: TestEntity.jsonType + ) ) - ) - XCTAssertEqual( - (error as? ResourceObjectDecodingError)?.description, - "'required' relationship is not nullable but null was found." - ) + XCTAssertEqual( + specialError?.description, + "'required' relationship is not nullable but null was found." + ) + } else { + XCTAssertEqual( + specialError, + ResourceObjectDecodingError( + subjectName: "required", + cause: .typeMismatch(expectedTypeName: "Dictionary"), + location: .relationships, + jsonAPIType: TestEntity.jsonType + ) + ) + + XCTAssertEqual( + specialError?.description, + "'required' relationship is not a Dictionary as expected." + ) + } } } @@ -259,20 +283,44 @@ extension ResourceObjectDecodingErrorTests { TestEntity2.self, from: entity_nonNullable_attribute_is_null )) { error in - XCTAssertEqual( - error as? ResourceObjectDecodingError, - ResourceObjectDecodingError( - subjectName: "required", - cause: .valueNotFound, - location: .attributes, - jsonAPIType: TestEntity2.jsonType + let specialError = error as? ResourceObjectDecodingError + + XCTAssertNotNil(specialError) + + // later Linux versions of Swift will catch that the value is not + // a string rather than calling null "not found." The errors are both + // effective, so we check that one of the two is the result: + if specialError?.cause == .valueNotFound { + XCTAssertEqual( + specialError, + ResourceObjectDecodingError( + subjectName: "required", + cause: .valueNotFound, + location: .attributes, + jsonAPIType: TestEntity2.jsonType + ) ) - ) - XCTAssertEqual( - (error as? ResourceObjectDecodingError)?.description, - "'required' attribute is not nullable but null was found." - ) + XCTAssertEqual( + specialError?.description, + "'required' attribute is not nullable but null was found." + ) + } else { + XCTAssertEqual( + specialError, + ResourceObjectDecodingError( + subjectName: "required", + cause: .typeMismatch(expectedTypeName: "String"), + location: .attributes, + jsonAPIType: TestEntity2.jsonType + ) + ) + + XCTAssertEqual( + specialError?.description, + "'required' attribute is not a String as expected." + ) + } } } From 5e125abc5943924035dab28dfb5d0d032fd731ac Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Tue, 23 Aug 2022 22:52:21 -0700 Subject: [PATCH 210/235] fix another test --- .../ResourceObjectDecodingErrorTests.swift | 48 ++++++++++++++----- 1 file changed, 36 insertions(+), 12 deletions(-) diff --git a/Tests/JSONAPITests/ResourceObject/ResourceObjectDecodingErrorTests.swift b/Tests/JSONAPITests/ResourceObject/ResourceObjectDecodingErrorTests.swift index 3003dae..d8fcd0c 100644 --- a/Tests/JSONAPITests/ResourceObject/ResourceObjectDecodingErrorTests.swift +++ b/Tests/JSONAPITests/ResourceObject/ResourceObjectDecodingErrorTests.swift @@ -498,20 +498,44 @@ extension ResourceObjectDecodingErrorTests { TestEntity2.self, from: entity_type_is_null )) { error in - XCTAssertEqual( - error as? ResourceObjectDecodingError, - ResourceObjectDecodingError( - subjectName: "type", - cause: .valueNotFound, - location: .type, - jsonAPIType: TestEntity2.jsonType + let specialError = error as? ResourceObjectDecodingError + + XCTAssertNotNil(specialError) + + // later Linux versions of Swift will catch that the value is not + // a string rather than calling null "not found." The errors are both + // effective, so we check that one of the two is the result: + if specialError?.cause == .valueNotFound { + XCTAssertEqual( + specialError, + ResourceObjectDecodingError( + subjectName: "type", + cause: .valueNotFound, + location: .type, + jsonAPIType: TestEntity2.jsonType + ) ) - ) - XCTAssertEqual( - (error as? ResourceObjectDecodingError)?.description, - #"'type' (a.k.a. JSON:API type name) is not nullable but null was found."# - ) + XCTAssertEqual( + specialError?.description, + "'type' (a.k.a. JSON:API type name) is not nullable but null was found." + ) + } else { + XCTAssertEqual( + specialError, + ResourceObjectDecodingError( + subjectName: "type", + cause: .typeMismatch(expectedTypeName: "String"), + location: .type, + jsonAPIType: TestEntity2.jsonType + ) + ) + + XCTAssertEqual( + specialError?.description, + "'type' (a.k.a. JSON:API type name) is not a String as expected." + ) + } } } } From 9d0c320e3b327c3a2831a9623e7765b60aab9cb7 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Tue, 23 Aug 2022 22:56:24 -0700 Subject: [PATCH 211/235] update linux swift images used for later versions of swift. --- .github/workflows/tests.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 00fc0fe..944a0a1 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -33,10 +33,9 @@ jobs: - swift:5.5-focal - swift:5.5-centos8 - swift:5.5-amazonlinux2 - - swift:5.6-xenial - swift:5.6-bionic - swift:5.6-focal - - swift:5.6-centos8 + - swift:5.6-centos7 - swift:5.6-amazonlinux2 container: ${{ matrix.image }} steps: From 6ccd8fd89183cf518e6721614204015ed88b2180 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Tue, 23 Aug 2022 23:14:28 -0700 Subject: [PATCH 212/235] hopefully catch a stupid handling of null in linux decoding of json. --- Sources/JSONAPI/Document/DocumentDecodingError.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Sources/JSONAPI/Document/DocumentDecodingError.swift b/Sources/JSONAPI/Document/DocumentDecodingError.swift index 2bcfa60..e31b816 100644 --- a/Sources/JSONAPI/Document/DocumentDecodingError.swift +++ b/Sources/JSONAPI/Document/DocumentDecodingError.swift @@ -33,6 +33,8 @@ public enum DocumentDecodingError: Swift.Error, Equatable { self = .primaryResourceMissing case .valueNotFound(let type, let context) where Location(context) == .data && type == UnkeyedDecodingContainer.self: self = .primaryResourcesMissing + case .typeMismatch(_, let context) where context.debugDescription.hasSuffix("but found null instead."): + self = .primaryResourcesMissing default: return nil } From d94166074c38b384c3280e54baa44156b9cb615c Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Tue, 23 Aug 2022 23:22:55 -0700 Subject: [PATCH 213/235] tweak test expectation --- .../Resource Object/ResourceObjectDecodingError.swift | 4 ++-- .../ResourceObject/ResourceObjectDecodingErrorTests.swift | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Sources/JSONAPI/Resource/Resource Object/ResourceObjectDecodingError.swift b/Sources/JSONAPI/Resource/Resource Object/ResourceObjectDecodingError.swift index 72f0a2a..004e03c 100644 --- a/Sources/JSONAPI/Resource/Resource Object/ResourceObjectDecodingError.swift +++ b/Sources/JSONAPI/Resource/Resource Object/ResourceObjectDecodingError.swift @@ -137,7 +137,7 @@ extension ResourceObjectDecodingError: CustomStringConvertible { case .keyNotFound where subjectName == ResourceObjectDecodingError.entireObject: return "\(location) object is required and missing." case .keyNotFound where location == .type: - return "'type' (a.k.a. JSON:API type name) is required and missing." + return "'type' (a.k.a. the JSON:API type name) is required and missing." case .keyNotFound where location == .relationshipType: return "'\(subjectName)' relationship does not have a 'type'." case .keyNotFound where location == .relationshipId: @@ -145,7 +145,7 @@ extension ResourceObjectDecodingError: CustomStringConvertible { case .keyNotFound: return "'\(subjectName)' \(location.singular) is required and missing." case .valueNotFound where location == .type: - return "'\(location.singular)' (a.k.a. JSON:API type name) is not nullable but null was found." + return "'\(location.singular)' (a.k.a. the JSON:API type name) is not nullable but null was found." case .valueNotFound: return "'\(subjectName)' \(location.singular) is not nullable but null was found." case .typeMismatch(expectedTypeName: let expected) where location == .type: diff --git a/Tests/JSONAPITests/ResourceObject/ResourceObjectDecodingErrorTests.swift b/Tests/JSONAPITests/ResourceObject/ResourceObjectDecodingErrorTests.swift index d8fcd0c..694acb9 100644 --- a/Tests/JSONAPITests/ResourceObject/ResourceObjectDecodingErrorTests.swift +++ b/Tests/JSONAPITests/ResourceObject/ResourceObjectDecodingErrorTests.swift @@ -488,7 +488,7 @@ extension ResourceObjectDecodingErrorTests { XCTAssertEqual( (error as? ResourceObjectDecodingError)?.description, - #"'type' (a.k.a. JSON:API type name) is required and missing."# + #"'type' (a.k.a. the JSON:API type name) is required and missing."# ) } } @@ -518,7 +518,7 @@ extension ResourceObjectDecodingErrorTests { XCTAssertEqual( specialError?.description, - "'type' (a.k.a. JSON:API type name) is not nullable but null was found." + "'type' (a.k.a. the JSON:API type name) is not nullable but null was found." ) } else { XCTAssertEqual( @@ -533,7 +533,7 @@ extension ResourceObjectDecodingErrorTests { XCTAssertEqual( specialError?.description, - "'type' (a.k.a. JSON:API type name) is not a String as expected." + "'type' (a.k.a. the JSON:API type name) is not a String as expected." ) } } From 86de52ffb87a4d4011d860b46cd3c4341c7a3f8d Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Tue, 23 Aug 2022 23:31:09 -0700 Subject: [PATCH 214/235] ditch centos which had non-test-related error. fix type mismatch heuristic. --- .github/workflows/tests.yml | 1 - Sources/JSONAPI/Document/DocumentDecodingError.swift | 4 +++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 944a0a1..7f5d35b 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -35,7 +35,6 @@ jobs: - swift:5.5-amazonlinux2 - swift:5.6-bionic - swift:5.6-focal - - swift:5.6-centos7 - swift:5.6-amazonlinux2 container: ${{ matrix.image }} steps: diff --git a/Sources/JSONAPI/Document/DocumentDecodingError.swift b/Sources/JSONAPI/Document/DocumentDecodingError.swift index e31b816..f9994cf 100644 --- a/Sources/JSONAPI/Document/DocumentDecodingError.swift +++ b/Sources/JSONAPI/Document/DocumentDecodingError.swift @@ -33,7 +33,9 @@ public enum DocumentDecodingError: Swift.Error, Equatable { self = .primaryResourceMissing case .valueNotFound(let type, let context) where Location(context) == .data && type == UnkeyedDecodingContainer.self: self = .primaryResourcesMissing - case .typeMismatch(_, let context) where context.debugDescription.hasSuffix("but found null instead."): + case .typeMismatch(let type, let context) where Location(context) == .data && type is AbstractResourceObject.Type && context.debugDescription.hasSuffix("but found null instead."): + self = .primaryResourceMissing + case .typeMismatch(let type, let context) where Location(context) == .data && type == UnkeyedDecodingContainer.self && context.debugDescription.hasSuffix("but found null instead."): self = .primaryResourcesMissing default: return nil From 9e8c288a25580da0d3802fa8b2c6cc5c0692fc39 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Tue, 23 Aug 2022 23:35:16 -0700 Subject: [PATCH 215/235] still trying to fix primary resource decoding error --- Sources/JSONAPI/Document/DocumentDecodingError.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/JSONAPI/Document/DocumentDecodingError.swift b/Sources/JSONAPI/Document/DocumentDecodingError.swift index f9994cf..34ff222 100644 --- a/Sources/JSONAPI/Document/DocumentDecodingError.swift +++ b/Sources/JSONAPI/Document/DocumentDecodingError.swift @@ -33,10 +33,10 @@ public enum DocumentDecodingError: Swift.Error, Equatable { self = .primaryResourceMissing case .valueNotFound(let type, let context) where Location(context) == .data && type == UnkeyedDecodingContainer.self: self = .primaryResourcesMissing - case .typeMismatch(let type, let context) where Location(context) == .data && type is AbstractResourceObject.Type && context.debugDescription.hasSuffix("but found null instead."): - self = .primaryResourceMissing case .typeMismatch(let type, let context) where Location(context) == .data && type == UnkeyedDecodingContainer.self && context.debugDescription.hasSuffix("but found null instead."): self = .primaryResourcesMissing + case .typeMismatch(_, let context) where Location(context) == .data && context.debugDescription.hasSuffix("but found null instead."): + self = .primaryResourceMissing default: return nil } From aec39fc010c6408f6c6e52db0bfaccb0787d07ec Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Tue, 23 Aug 2022 23:48:18 -0700 Subject: [PATCH 216/235] trying to determine when a primary resource error should be plural or not. --- Sources/JSONAPI/Document/DocumentDecodingError.swift | 5 ++++- Tests/JSONAPITests/Document/DocumentDecodingErrorTests.swift | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/Sources/JSONAPI/Document/DocumentDecodingError.swift b/Sources/JSONAPI/Document/DocumentDecodingError.swift index 34ff222..92e9ade 100644 --- a/Sources/JSONAPI/Document/DocumentDecodingError.swift +++ b/Sources/JSONAPI/Document/DocumentDecodingError.swift @@ -33,7 +33,7 @@ public enum DocumentDecodingError: Swift.Error, Equatable { self = .primaryResourceMissing case .valueNotFound(let type, let context) where Location(context) == .data && type == UnkeyedDecodingContainer.self: self = .primaryResourcesMissing - case .typeMismatch(let type, let context) where Location(context) == .data && type == UnkeyedDecodingContainer.self && context.debugDescription.hasSuffix("but found null instead."): + case .typeMismatch(let type, let context) where Location(context) == .data && type is _ArrayType.Type && context.debugDescription.hasSuffix("but found null instead."): self = .primaryResourcesMissing case .typeMismatch(_, let context) where Location(context) == .data && context.debugDescription.hasSuffix("but found null instead."): self = .primaryResourceMissing @@ -75,3 +75,6 @@ extension DocumentDecodingError: CustomStringConvertible { } } } + +private protocol _ArrayType {} +extension Array: _ArrayType {} diff --git a/Tests/JSONAPITests/Document/DocumentDecodingErrorTests.swift b/Tests/JSONAPITests/Document/DocumentDecodingErrorTests.swift index a10d45f..0100a07 100644 --- a/Tests/JSONAPITests/Document/DocumentDecodingErrorTests.swift +++ b/Tests/JSONAPITests/Document/DocumentDecodingErrorTests.swift @@ -53,7 +53,7 @@ final class DocumentDecodingErrorTests: XCTestCase { ) { error in guard let docError = error as? DocumentDecodingError, case .primaryResourcesMissing = docError else { - XCTFail("Expected primary resource missing error. Got \(error)") + XCTFail("Expected primary resources missing error. Got \(error)") return } From 4d75ff98fc6476ef8b98c16ba78a6f2a4cc6d085 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Fri, 13 Jan 2023 23:18:35 -0600 Subject: [PATCH 217/235] Update CI action versions --- .github/workflows/tests.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 7f5d35b..1f3cf2e 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -36,19 +36,20 @@ jobs: - swift:5.6-bionic - swift:5.6-focal - swift:5.6-amazonlinux2 + - swift:5.7-jammy container: ${{ matrix.image }} steps: - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Run tests run: swift test --enable-test-discovery osx: runs-on: macOS-latest steps: - name: Select latest available Xcode - uses: maxim-lobanov/setup-xcode@v1.2.1 + uses: maxim-lobanov/setup-xcode@v1 with: { 'xcode-version': 'latest' } - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Run tests run: swift test --enable-test-discovery From df7d9d55f4d0692a60bb5f67f57ba07d893b86d0 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Fri, 13 Jan 2023 23:22:14 -0600 Subject: [PATCH 218/235] Update .github/workflows/tests.yml --- .github/workflows/tests.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 1f3cf2e..e7cd4df 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -36,7 +36,6 @@ jobs: - swift:5.6-bionic - swift:5.6-focal - swift:5.6-amazonlinux2 - - swift:5.7-jammy container: ${{ matrix.image }} steps: - name: Checkout code From b9cb23d6235917a50a1386e06d39e67c0deffbda Mon Sep 17 00:00:00 2001 From: Vitalii Budnik Date: Wed, 20 Sep 2023 18:53:51 +0300 Subject: [PATCH 219/235] chore: allow mixing meta while merging Body.Data MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ignore irrelevant generic types when merging Document.Body.Data. ### Reasoning I have a paginated request and response data has no meta and links, because it’s irrelevant for me. --- Sources/JSONAPI/Document/Document.swift | 11 ++++++----- Tests/JSONAPITests/Document/DocumentTests.swift | 17 +++++++++++++++++ 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/Sources/JSONAPI/Document/Document.swift b/Sources/JSONAPI/Document/Document.swift index da0a37f..b10a97f 100644 --- a/Sources/JSONAPI/Document/Document.swift +++ b/Sources/JSONAPI/Document/Document.swift @@ -261,7 +261,7 @@ extension Document { } extension Document.Body.Data where PrimaryResourceBody: ResourceBodyAppendable { - public func merging(_ other: Document.Body.Data, + public func merging(_ other: Document.Body.Data, combiningMetaWith metaMerge: (MetaType, MetaType) -> MetaType, combiningLinksWith linksMerge: (LinksType, LinksType) -> LinksType) -> Document.Body.Data { return Document.Body.Data(primary: primary.appending(other.primary), @@ -272,10 +272,11 @@ extension Document.Body.Data where PrimaryResourceBody: ResourceBodyAppendable { } extension Document.Body.Data where PrimaryResourceBody: ResourceBodyAppendable, MetaType == NoMetadata, LinksType == NoLinks { - public func merging(_ other: Document.Body.Data) -> Document.Body.Data { - return merging(other, - combiningMetaWith: { _, _ in .none }, - combiningLinksWith: { _, _ in .none }) + public func merging(_ other: Document.Body.Data) -> Document.Body.Data { + return Document.Body.Data(primary: primary.appending(other.primary), + includes: includes.appending(other.includes), + meta: meta, + links: links) } } diff --git a/Tests/JSONAPITests/Document/DocumentTests.swift b/Tests/JSONAPITests/Document/DocumentTests.swift index d6058ec..1acc297 100644 --- a/Tests/JSONAPITests/Document/DocumentTests.swift +++ b/Tests/JSONAPITests/Document/DocumentTests.swift @@ -1528,6 +1528,23 @@ extension DocumentTests { XCTAssertEqual(combined.primary.values, bodyData1.primary.values + bodyData2.primary.values) } + public func test_MergeBodyDataMixedMetaLinksErrorAndAPI(){ + let entity1 = Article(attributes: .none, relationships: .init(author: "2"), meta: .none, links: .none) + let entity2 = Article(attributes: .none, relationships: .init(author: "3"), meta: .none, links: .none) + + let bodyData1 = Document, NoMetadata, NoLinks, NoIncludes, NoAPIDescription, UnknownJSONAPIError>.Body.Data(primary: .init(resourceObjects: [entity1]), + includes: .none, + meta: .none, + links: .none) + let bodyData2 = Document, TestPageMetadata, TestLinks, NoIncludes, TestAPIDescription, GenericJSONAPIError>.Body.Data(primary: .init(resourceObjects: [entity2]), + includes: .none, + meta: .init(total: 5, limit: 2, offset: 2), + links: .init(link: .init(url: "one"), link2: .init(url: .init(), meta: .init(hello: "world")))) + let combined = bodyData1.merging(bodyData2) + + XCTAssertEqual(combined.primary.values, bodyData1.primary.values + bodyData2.primary.values) + } + public func test_MergeBodyDataWithMergeFunctions() { let article1 = Article(attributes: .none, relationships: .init(author: "2"), meta: .none, links: .none) let author1 = Author(id: "2", attributes: .none, relationships: .none, meta: .none, links: .none) From 7f326df678d9b5f6d421488c4bd37da9d2d42c8b Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Fri, 1 Mar 2024 13:36:00 -0600 Subject: [PATCH 220/235] keep the metadata and links for the merged-in document body if the merged-into document body has no metadata or links. --- Sources/JSONAPI/Document/Document.swift | 10 +++++----- Tests/JSONAPITests/Document/DocumentTests.swift | 2 ++ 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/Sources/JSONAPI/Document/Document.swift b/Sources/JSONAPI/Document/Document.swift index b10a97f..3673493 100644 --- a/Sources/JSONAPI/Document/Document.swift +++ b/Sources/JSONAPI/Document/Document.swift @@ -272,11 +272,11 @@ extension Document.Body.Data where PrimaryResourceBody: ResourceBodyAppendable { } extension Document.Body.Data where PrimaryResourceBody: ResourceBodyAppendable, MetaType == NoMetadata, LinksType == NoLinks { - public func merging(_ other: Document.Body.Data) -> Document.Body.Data { - return Document.Body.Data(primary: primary.appending(other.primary), - includes: includes.appending(other.includes), - meta: meta, - links: links) + public func merging(_ other: Document.Body.Data) -> Document.Body.Data { + return .init(primary: primary.appending(other.primary), + includes: includes.appending(other.includes), + meta: other.meta, + links: other.links) } } diff --git a/Tests/JSONAPITests/Document/DocumentTests.swift b/Tests/JSONAPITests/Document/DocumentTests.swift index 1acc297..559a257 100644 --- a/Tests/JSONAPITests/Document/DocumentTests.swift +++ b/Tests/JSONAPITests/Document/DocumentTests.swift @@ -1543,6 +1543,8 @@ extension DocumentTests { let combined = bodyData1.merging(bodyData2) XCTAssertEqual(combined.primary.values, bodyData1.primary.values + bodyData2.primary.values) + XCTAssertEqual(combined.meta, bodyData2.meta) + XCTAssertEqual(combined.links, bodyData2.links) } public func test_MergeBodyDataWithMergeFunctions() { From f1945d29663c828f5916756b857321ddd3c2727b Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Wed, 27 Mar 2024 13:59:18 -0500 Subject: [PATCH 221/235] add support for 14 includes --- Package.resolved | 4 +- Package.swift | 2 +- Sources/JSONAPI/Document/Includes.swift | 8 ++ .../Resource/Poly+PrimaryResource.swift | 35 ++++++ .../JSONAPITests/Includes/IncludeTests.swift | 36 +++++++ .../Includes/stubs/IncludeStubs.swift | 102 ++++++++++++++++++ 6 files changed, 184 insertions(+), 3 deletions(-) diff --git a/Package.resolved b/Package.resolved index 8a582ff..6540547 100644 --- a/Package.resolved +++ b/Package.resolved @@ -6,8 +6,8 @@ "repositoryURL": "https://github.com/mattpolzin/Poly.git", "state": { "branch": null, - "revision": "c108e9e0a2904134719b082f6c18d64406afc6db", - "version": "2.6.0" + "revision": "fb90ab22a09fe32e8cb152f7dea344df82a59d53", + "version": "2.7.0" } } ] diff --git a/Package.swift b/Package.swift index e7a0e51..f6a29f7 100644 --- a/Package.swift +++ b/Package.swift @@ -17,7 +17,7 @@ let package = Package( targets: ["JSONAPITesting"]) ], dependencies: [ - .package(url: "https://github.com/mattpolzin/Poly.git", .upToNextMajor(from: "2.6.0")), + .package(url: "https://github.com/mattpolzin/Poly.git", .upToNextMajor(from: "2.7.0")), ], targets: [ .target( diff --git a/Sources/JSONAPI/Document/Includes.swift b/Sources/JSONAPI/Document/Includes.swift index 30634ae..9f5b1bd 100644 --- a/Sources/JSONAPI/Document/Includes.swift +++ b/Sources/JSONAPI/Document/Includes.swift @@ -222,6 +222,14 @@ extension Includes where I: _Poly13 { } } +// MARK: - 14 includes +public typealias Include14 = Poly14 +extension Includes where I: _Poly14 { + public subscript(_ lookup: I.N.Type) -> [I.N] { + return values.compactMap(\.n) + } +} + // MARK: - DecodingError public struct IncludesDecodingError: Swift.Error, Equatable { public let error: Swift.Error diff --git a/Sources/JSONAPI/Resource/Poly+PrimaryResource.swift b/Sources/JSONAPI/Resource/Poly+PrimaryResource.swift index 0dab925..c341302 100644 --- a/Sources/JSONAPI/Resource/Poly+PrimaryResource.swift +++ b/Sources/JSONAPI/Resource/Poly+PrimaryResource.swift @@ -226,3 +226,38 @@ J: CodablePolyWrapped, K: CodablePolyWrapped, L: CodablePolyWrapped, M: CodablePolyWrapped {} + +// MARK: - 14 types +extension Poly14: EncodablePrimaryResource, OptionalEncodablePrimaryResource +where +A: EncodablePolyWrapped, +B: EncodablePolyWrapped, +C: EncodablePolyWrapped, +D: EncodablePolyWrapped, +E: EncodablePolyWrapped, +F: EncodablePolyWrapped, +G: EncodablePolyWrapped, +H: EncodablePolyWrapped, +I: EncodablePolyWrapped, +J: EncodablePolyWrapped, +K: EncodablePolyWrapped, +L: EncodablePolyWrapped, +M: EncodablePolyWrapped, +N: EncodablePolyWrapped {} + +extension Poly14: CodablePrimaryResource, OptionalCodablePrimaryResource +where +A: CodablePolyWrapped, +B: CodablePolyWrapped, +C: CodablePolyWrapped, +D: CodablePolyWrapped, +E: CodablePolyWrapped, +F: CodablePolyWrapped, +G: CodablePolyWrapped, +H: CodablePolyWrapped, +I: CodablePolyWrapped, +J: CodablePolyWrapped, +K: CodablePolyWrapped, +L: CodablePolyWrapped, +M: CodablePolyWrapped, +N: CodablePolyWrapped {} diff --git a/Tests/JSONAPITests/Includes/IncludeTests.swift b/Tests/JSONAPITests/Includes/IncludeTests.swift index e0a44a4..73b1922 100644 --- a/Tests/JSONAPITests/Includes/IncludeTests.swift +++ b/Tests/JSONAPITests/Includes/IncludeTests.swift @@ -266,6 +266,31 @@ class IncludedTests: XCTestCase { test_DecodeEncodeEquality(type: Includes>.self, data: thirteen_different_type_includes) } + + func test_FourteenDifferentIncludes() { + let includes = decoded(type: Includes>.self, + data: fourteen_different_type_includes) + + XCTAssertEqual(includes[TestEntity.self].count, 1) + XCTAssertEqual(includes[TestEntity2.self].count, 1) + XCTAssertEqual(includes[TestEntity3.self].count, 1) + XCTAssertEqual(includes[TestEntity4.self].count, 1) + XCTAssertEqual(includes[TestEntity5.self].count, 1) + XCTAssertEqual(includes[TestEntity6.self].count, 1) + XCTAssertEqual(includes[TestEntity7.self].count, 1) + XCTAssertEqual(includes[TestEntity8.self].count, 1) + XCTAssertEqual(includes[TestEntity9.self].count, 1) + XCTAssertEqual(includes[TestEntity10.self].count, 1) + XCTAssertEqual(includes[TestEntity11.self].count, 1) + XCTAssertEqual(includes[TestEntity12.self].count, 1) + XCTAssertEqual(includes[TestEntity13.self].count, 1) + XCTAssertEqual(includes[TestEntity14.self].count, 1) + } + + func test_FourteenDifferentIncludes_encode() { + test_DecodeEncodeEquality(type: Includes>.self, + data: fourteen_different_type_includes) + } } // MARK: - Appending @@ -605,4 +630,15 @@ extension IncludedTests { } typealias TestEntity13 = BasicEntity + + enum TestEntityType14: ResourceObjectDescription { + + typealias Attributes = NoAttributes + + public static var jsonType: String { return "test_entity14" } + + typealias Relationships = NoRelationships + } + + typealias TestEntity14 = BasicEntity } diff --git a/Tests/JSONAPITests/Includes/stubs/IncludeStubs.swift b/Tests/JSONAPITests/Includes/stubs/IncludeStubs.swift index 2cfd3fa..7d23d22 100644 --- a/Tests/JSONAPITests/Includes/stubs/IncludeStubs.swift +++ b/Tests/JSONAPITests/Includes/stubs/IncludeStubs.swift @@ -877,6 +877,108 @@ let thirteen_different_type_includes = """ ] """.data(using: .utf8)! +let fourteen_different_type_includes = """ +[ + { + "type": "test_entity1", + "id": "2DF03B69-4B0A-467F-B52E-B0C9E44FCECF", + "attributes": { + "foo": "Hello", + "bar": 123 + } + }, + { + "type": "test_entity2", + "id": "90F03B69-4DF1-467F-B52E-B0C9E44FC333", + "attributes": { + "foo": "World", + "bar": 456 + }, + "relationships": { + "entity1": { + "data": { + "type": "test_entity1", + "id": "2DF03B69-4B0A-467F-B52E-B0C9E44FCECF" + } + } + } + }, + { + "type": "test_entity3", + "id": "11223B69-4DF1-467F-B52E-B0C9E44FC443", + "relationships": { + "entity1": { + "data": { + "type": "test_entity1", + "id": "2DF03B69-4B0A-467F-B52E-B0C9E44FCECF" + } + }, + "entity2": { + "data": [ + { + "type": "test_entity2", + "id": "90F03B69-4DF1-467F-B52E-B0C9E44FC333" + } + ] + } + } + }, + { + "type": "test_entity6", + "id": "11113B69-4DF1-467F-B52E-B0C9E44FC444", + "relationships": { + "entity4": { + "data": { + "type": "test_entity4", + "id": "364B3B69-4DF1-467F-B52E-B0C9E44F666E" + } + } + } + }, + { + "type": "test_entity5", + "id": "A24B3B69-4DF1-467F-B52E-B0C9E44F436A" + }, + { + "type": "test_entity4", + "id": "364B3B69-4DF1-467F-B52E-B0C9E44F666E" + }, + { + "type": "test_entity7", + "id": "364B3B69-4DF1-222F-B52E-B0C9E44F666E" + }, + { + "type": "test_entity8", + "id": "364B3B69-4DF1-222F-B52E-B0C9E44F266F" + }, + { + "type": "test_entity9", + "id": "364B3B69-4DF1-218F-B52E-B0C9E44F2661" + }, + { + "type": "test_entity10", + "id": "264B3B69-4DF1-212F-B52E-B0C9E44F2660" + }, + { + "type": "test_entity11", + "id": "264B3B69-4DF3-212F-B32E-A0C9E44F26C0B" + }, + { + "type": "test_entity12", + "id": "264B3B69-4DF3-212F-B32E-A0C9E44F26C00" + }, + { + "type": "test_entity13", + "id": "264B3B69-4DF3-212F-B32E-A0C9E44F26C01" + }, + { + "type": "test_entity14", + "id": "264B3B69-4DF3-312F-B32E-A0C9E44F26C01" + } +] +""".data(using: .utf8)! + + let three_includes_one_missing_attributes = """ [ { From 9b919b68673bc2b755d6033748de22a8035753ff Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Wed, 27 Mar 2024 17:22:04 -0500 Subject: [PATCH 222/235] support for Include15 type --- Package.resolved | 4 +- Package.swift | 2 +- Sources/JSONAPI/Document/Includes.swift | 8 ++ .../Resource/Poly+PrimaryResource.swift | 37 ++++++ .../JSONAPITests/Includes/IncludeTests.swift | 37 ++++++ .../Includes/stubs/IncludeStubs.swift | 105 ++++++++++++++++++ 6 files changed, 190 insertions(+), 3 deletions(-) diff --git a/Package.resolved b/Package.resolved index 6540547..f7c521e 100644 --- a/Package.resolved +++ b/Package.resolved @@ -6,8 +6,8 @@ "repositoryURL": "https://github.com/mattpolzin/Poly.git", "state": { "branch": null, - "revision": "fb90ab22a09fe32e8cb152f7dea344df82a59d53", - "version": "2.7.0" + "revision": "99e2e8b575620369be52fe348c0dd72028e3674c", + "version": "2.8.0" } } ] diff --git a/Package.swift b/Package.swift index f6a29f7..81b978c 100644 --- a/Package.swift +++ b/Package.swift @@ -17,7 +17,7 @@ let package = Package( targets: ["JSONAPITesting"]) ], dependencies: [ - .package(url: "https://github.com/mattpolzin/Poly.git", .upToNextMajor(from: "2.7.0")), + .package(url: "https://github.com/mattpolzin/Poly.git", .upToNextMajor(from: "2.8.0")), ], targets: [ .target( diff --git a/Sources/JSONAPI/Document/Includes.swift b/Sources/JSONAPI/Document/Includes.swift index 9f5b1bd..b76225e 100644 --- a/Sources/JSONAPI/Document/Includes.swift +++ b/Sources/JSONAPI/Document/Includes.swift @@ -230,6 +230,14 @@ extension Includes where I: _Poly14 { } } +// MARK: - 15 includes +public typealias Include15 = Poly15 +extension Includes where I: _Poly15 { + public subscript(_ lookup: I.O.Type) -> [I.O] { + return values.compactMap(\.o) + } +} + // MARK: - DecodingError public struct IncludesDecodingError: Swift.Error, Equatable { public let error: Swift.Error diff --git a/Sources/JSONAPI/Resource/Poly+PrimaryResource.swift b/Sources/JSONAPI/Resource/Poly+PrimaryResource.swift index c341302..1443610 100644 --- a/Sources/JSONAPI/Resource/Poly+PrimaryResource.swift +++ b/Sources/JSONAPI/Resource/Poly+PrimaryResource.swift @@ -261,3 +261,40 @@ K: CodablePolyWrapped, L: CodablePolyWrapped, M: CodablePolyWrapped, N: CodablePolyWrapped {} + +// MARK: - 15 types +extension Poly15: EncodablePrimaryResource, OptionalEncodablePrimaryResource +where +A: EncodablePolyWrapped, +B: EncodablePolyWrapped, +C: EncodablePolyWrapped, +D: EncodablePolyWrapped, +E: EncodablePolyWrapped, +F: EncodablePolyWrapped, +G: EncodablePolyWrapped, +H: EncodablePolyWrapped, +I: EncodablePolyWrapped, +J: EncodablePolyWrapped, +K: EncodablePolyWrapped, +L: EncodablePolyWrapped, +M: EncodablePolyWrapped, +N: EncodablePolyWrapped, +O: EncodablePolyWrapped {} + +extension Poly15: CodablePrimaryResource, OptionalCodablePrimaryResource +where +A: CodablePolyWrapped, +B: CodablePolyWrapped, +C: CodablePolyWrapped, +D: CodablePolyWrapped, +E: CodablePolyWrapped, +F: CodablePolyWrapped, +G: CodablePolyWrapped, +H: CodablePolyWrapped, +I: CodablePolyWrapped, +J: CodablePolyWrapped, +K: CodablePolyWrapped, +L: CodablePolyWrapped, +M: CodablePolyWrapped, +N: CodablePolyWrapped, +O: CodablePolyWrapped {} diff --git a/Tests/JSONAPITests/Includes/IncludeTests.swift b/Tests/JSONAPITests/Includes/IncludeTests.swift index 73b1922..088fbe2 100644 --- a/Tests/JSONAPITests/Includes/IncludeTests.swift +++ b/Tests/JSONAPITests/Includes/IncludeTests.swift @@ -291,6 +291,32 @@ class IncludedTests: XCTestCase { test_DecodeEncodeEquality(type: Includes>.self, data: fourteen_different_type_includes) } + + func test_FifteenDifferentIncludes() { + let includes = decoded(type: Includes>.self, + data: fifteen_different_type_includes) + + XCTAssertEqual(includes[TestEntity.self].count, 1) + XCTAssertEqual(includes[TestEntity2.self].count, 1) + XCTAssertEqual(includes[TestEntity3.self].count, 1) + XCTAssertEqual(includes[TestEntity4.self].count, 1) + XCTAssertEqual(includes[TestEntity5.self].count, 1) + XCTAssertEqual(includes[TestEntity6.self].count, 1) + XCTAssertEqual(includes[TestEntity7.self].count, 1) + XCTAssertEqual(includes[TestEntity8.self].count, 1) + XCTAssertEqual(includes[TestEntity9.self].count, 1) + XCTAssertEqual(includes[TestEntity10.self].count, 1) + XCTAssertEqual(includes[TestEntity11.self].count, 1) + XCTAssertEqual(includes[TestEntity12.self].count, 1) + XCTAssertEqual(includes[TestEntity13.self].count, 1) + XCTAssertEqual(includes[TestEntity14.self].count, 1) + XCTAssertEqual(includes[TestEntity15.self].count, 1) + } + + func test_FifteenDifferentIncludes_encode() { + test_DecodeEncodeEquality(type: Includes>.self, + data: fifteen_different_type_includes) + } } // MARK: - Appending @@ -641,4 +667,15 @@ extension IncludedTests { } typealias TestEntity14 = BasicEntity + + enum TestEntityType15: ResourceObjectDescription { + + typealias Attributes = NoAttributes + + public static var jsonType: String { return "test_entity15" } + + typealias Relationships = NoRelationships + } + + typealias TestEntity15 = BasicEntity } diff --git a/Tests/JSONAPITests/Includes/stubs/IncludeStubs.swift b/Tests/JSONAPITests/Includes/stubs/IncludeStubs.swift index 7d23d22..df47120 100644 --- a/Tests/JSONAPITests/Includes/stubs/IncludeStubs.swift +++ b/Tests/JSONAPITests/Includes/stubs/IncludeStubs.swift @@ -978,6 +978,111 @@ let fourteen_different_type_includes = """ ] """.data(using: .utf8)! +let fifteen_different_type_includes = """ +[ + { + "type": "test_entity1", + "id": "2DF03B69-4B0A-467F-B52E-B0C9E44FCECF", + "attributes": { + "foo": "Hello", + "bar": 123 + } + }, + { + "type": "test_entity2", + "id": "90F03B69-4DF1-467F-B52E-B0C9E44FC333", + "attributes": { + "foo": "World", + "bar": 456 + }, + "relationships": { + "entity1": { + "data": { + "type": "test_entity1", + "id": "2DF03B69-4B0A-467F-B52E-B0C9E44FCECF" + } + } + } + }, + { + "type": "test_entity3", + "id": "11223B69-4DF1-467F-B52E-B0C9E44FC443", + "relationships": { + "entity1": { + "data": { + "type": "test_entity1", + "id": "2DF03B69-4B0A-467F-B52E-B0C9E44FCECF" + } + }, + "entity2": { + "data": [ + { + "type": "test_entity2", + "id": "90F03B69-4DF1-467F-B52E-B0C9E44FC333" + } + ] + } + } + }, + { + "type": "test_entity6", + "id": "11113B69-4DF1-467F-B52E-B0C9E44FC444", + "relationships": { + "entity4": { + "data": { + "type": "test_entity4", + "id": "364B3B69-4DF1-467F-B52E-B0C9E44F666E" + } + } + } + }, + { + "type": "test_entity5", + "id": "A24B3B69-4DF1-467F-B52E-B0C9E44F436A" + }, + { + "type": "test_entity4", + "id": "364B3B69-4DF1-467F-B52E-B0C9E44F666E" + }, + { + "type": "test_entity7", + "id": "364B3B69-4DF1-222F-B52E-B0C9E44F666E" + }, + { + "type": "test_entity8", + "id": "364B3B69-4DF1-222F-B52E-B0C9E44F266F" + }, + { + "type": "test_entity9", + "id": "364B3B69-4DF1-218F-B52E-B0C9E44F2661" + }, + { + "type": "test_entity10", + "id": "264B3B69-4DF1-212F-B52E-B0C9E44F2660" + }, + { + "type": "test_entity11", + "id": "264B3B69-4DF3-212F-B32E-A0C9E44F26C0B" + }, + { + "type": "test_entity12", + "id": "264B3B69-4DF3-212F-B32E-A0C9E44F26C00" + }, + { + "type": "test_entity13", + "id": "264B3B69-4DF3-212F-B32E-A0C9E44F26C01" + }, + { + "type": "test_entity14", + "id": "264B3B69-4DF3-312F-B32E-A0C9E44F26C01" + }, + { + "type": "test_entity15", + "id": "264B3B69-4DF3-312A-B32E-A0C9E44F26C01" + } +] +""".data(using: .utf8)! + let three_includes_one_missing_attributes = """ [ From 83cf5bba875b44e927b11ae47938f3a4e88666a3 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Tue, 30 Apr 2024 09:31:36 -0500 Subject: [PATCH 223/235] allow optional metadata to be omitted as well as nulled out. --- Sources/JSONAPI/Resource/Relationship.swift | 3 --- .../Resource Object/ResourceObject.swift | 12 +++++++++- .../ResourceObject/ResourceObjectTests.swift | 22 +++++++++++++++++-- 3 files changed, 31 insertions(+), 6 deletions(-) diff --git a/Sources/JSONAPI/Resource/Relationship.swift b/Sources/JSONAPI/Resource/Relationship.swift index fc03c4e..4a2c3fa 100644 --- a/Sources/JSONAPI/Resource/Relationship.swift +++ b/Sources/JSONAPI/Resource/Relationship.swift @@ -263,9 +263,6 @@ extension MetaRelationship: Codable { } } -fileprivate protocol _Optional {} -extension Optional: _Optional {} - extension ToOneRelationship: Codable where Identifiable.ID: OptionalId { public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: ResourceLinkageCodingKeys.self) diff --git a/Sources/JSONAPI/Resource/Resource Object/ResourceObject.swift b/Sources/JSONAPI/Resource/Resource Object/ResourceObject.swift index 77d367e..1b4308b 100644 --- a/Sources/JSONAPI/Resource/Resource Object/ResourceObject.swift +++ b/Sources/JSONAPI/Resource/Resource Object/ResourceObject.swift @@ -447,7 +447,17 @@ public extension ResourceObject { ) } - meta = try (NoMetadata() as? MetaType) ?? container.decode(MetaType.self, forKey: .meta) + do { + meta = try (NoMetadata() as? MetaType) ?? container.decode(MetaType.self, forKey: .meta) + } catch let decodingError as DecodingError { + let anyNil: Any? = nil + + guard case .keyNotFound = decodingError, + let omittedMeta = anyNil as? MetaType else { + throw decodingError + } + meta = omittedMeta + } links = try (NoLinks() as? LinksType) ?? container.decode(LinksType.self, forKey: .links) } diff --git a/Tests/JSONAPITests/ResourceObject/ResourceObjectTests.swift b/Tests/JSONAPITests/ResourceObject/ResourceObjectTests.swift index d492b18..77a75c7 100644 --- a/Tests/JSONAPITests/ResourceObject/ResourceObjectTests.swift +++ b/Tests/JSONAPITests/ResourceObject/ResourceObjectTests.swift @@ -151,6 +151,11 @@ extension ResourceObjectTests { data: entity_no_relationships_no_attributes) } + func test_EntityNoMetaDecodesAsOptionalMeta() { + let _ = decoded(type: TestEntityOptionalMeta.self, + data: entity_no_relationships_no_attributes) + } + func test_EntityNoRelationshipsSomeAttributes() { let entity = decoded(type: TestEntity5.self, data: entity_no_relationships_some_attributes) @@ -861,13 +866,13 @@ extension ResourceObjectTests { public struct Relationships: JSONAPI.Relationships { public init() { - optionalMeta = nil + optionalMeta = nil optionalOne = nil optionalNullableOne = nil optionalMany = nil } - let optionalMeta: MetaRelationship? + let optionalMeta: MetaRelationship? let optionalOne: ToOneRelationship? @@ -879,6 +884,19 @@ extension ResourceObjectTests { typealias TestEntity12 = BasicEntity + enum TestEntityOptionalMetaType: JSONAPI.ResourceObjectDescription { + public static var jsonType: String { return "test_entities" } + + typealias Attributes = NoAttributes + typealias Relationships = NoRelationships + } + + struct UnimportantMeta: JSONAPI.Meta { + let property1: String + } + + typealias TestEntityOptionalMeta = JSONAPI.ResourceObject + enum UnidentifiedTestEntityType: ResourceObjectDescription { public static var jsonType: String { return "unidentified_test_entities" } From 457ac3df7557bdb87d820d0efc3a4c1776363860 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Tue, 30 Apr 2024 11:10:33 -0500 Subject: [PATCH 224/235] fix test for latest MacOS Swift versions --- .../SparseFields/SparseFieldEncoderTests.swift | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/Tests/JSONAPITests/SparseFields/SparseFieldEncoderTests.swift b/Tests/JSONAPITests/SparseFields/SparseFieldEncoderTests.swift index 9d5bba5..2625989 100644 --- a/Tests/JSONAPITests/SparseFields/SparseFieldEncoderTests.swift +++ b/Tests/JSONAPITests/SparseFields/SparseFieldEncoderTests.swift @@ -43,13 +43,9 @@ class SparseFieldEncoderTests: XCTestCase { XCTAssertEqual(allThingsOnDeserialized["bool"] as? Bool, true) XCTAssertEqual(allThingsOnDeserialized["double"] as? Double, 10.5) XCTAssertEqual(allThingsOnDeserialized["string"] as? String, "hello") - #if os(Linux) - // There's some bug with Linux where it won't case the value to a float. - // It does exist and it is == 1.2 + // For latest versions of MacOS and all versions of Linux, floats + // decode as doubles. XCTAssertEqual(allThingsOnDeserialized["float"] as? Double, 1.2) - #else - XCTAssertEqual(allThingsOnDeserialized["float"] as? Float, 1.2) - #endif XCTAssertEqual(allThingsOnDeserialized["int"] as? Int, 3) XCTAssertEqual(allThingsOnDeserialized["int8"] as? Int8, 4) XCTAssertEqual(allThingsOnDeserialized["int16"] as? Int16, 5) From 6f8e133d7d9c43c7a6d5a87b1a978dc2d014599f Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Tue, 30 Apr 2024 11:26:41 -0500 Subject: [PATCH 225/235] Fix decoding error support for newer MacOS Swift versions --- Sources/JSONAPI/Document/DocumentDecodingError.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Sources/JSONAPI/Document/DocumentDecodingError.swift b/Sources/JSONAPI/Document/DocumentDecodingError.swift index 92e9ade..8a96054 100644 --- a/Sources/JSONAPI/Document/DocumentDecodingError.swift +++ b/Sources/JSONAPI/Document/DocumentDecodingError.swift @@ -33,6 +33,10 @@ public enum DocumentDecodingError: Swift.Error, Equatable { self = .primaryResourceMissing case .valueNotFound(let type, let context) where Location(context) == .data && type == UnkeyedDecodingContainer.self: self = .primaryResourcesMissing + case .valueNotFound(let type, let context) where Location(context) == .data && type is _ArrayType.Type && context.debugDescription.hasSuffix("found null value instead"): + self = .primaryResourcesMissing + case .valueNotFound(_, let context) where Location(context) == .data && context.debugDescription.hasSuffix("found null value instead"): + self = .primaryResourceMissing case .typeMismatch(let type, let context) where Location(context) == .data && type is _ArrayType.Type && context.debugDescription.hasSuffix("but found null instead."): self = .primaryResourcesMissing case .typeMismatch(_, let context) where Location(context) == .data && context.debugDescription.hasSuffix("but found null instead."): From ff0495dacb8afdf64418b60af127a7838196afe4 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Mon, 21 Apr 2025 10:02:13 -0500 Subject: [PATCH 226/235] Add test that shows a custom generic included type of resource --- .../JSONAPITests/Includes/IncludeTests.swift | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/Tests/JSONAPITests/Includes/IncludeTests.swift b/Tests/JSONAPITests/Includes/IncludeTests.swift index 088fbe2..e4a24c5 100644 --- a/Tests/JSONAPITests/Includes/IncludeTests.swift +++ b/Tests/JSONAPITests/Includes/IncludeTests.swift @@ -72,6 +72,16 @@ class IncludedTests: XCTestCase { data: three_different_type_includes) } + func test_OneKnownAndTwoGenericIncludes() { + let includes = decoded(type: Includes>.self, + data: three_different_type_includes) + + XCTAssertEqual(includes[TestEntity.self].count, 1) + XCTAssertEqual(includes[TestEntityOther.self].count, 2) + XCTAssert(includes[TestEntityOther.self].contains { $0.type == "test_entity2"}) + XCTAssert(includes[TestEntityOther.self].contains { $0.type == "test_entity4"}) + } + func test_FourDifferentIncludes() { let includes = decoded(type: Includes>.self, data: four_different_type_includes) @@ -678,4 +688,50 @@ extension IncludedTests { } typealias TestEntity15 = BasicEntity + + enum TestEntityTypeOther: ResourceObjectProxyDescription { + public static var jsonType: String { return "_generic" } + + typealias Attributes = NoAttributes + typealias Relationships = NoRelationships + } + + struct TestEntityOther: ResourceObjectProxy, Codable { + typealias Description = TestEntityTypeOther + typealias EntityRawIdType = String + + public let type : String + public let id : JSONAPI.Id + + public let attributes = NoAttributes() + public let relationships = NoRelationships() + + enum CodingKeys: CodingKey { + case id + case type + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + let type: String + do { + type = try container.decode(String.self, forKey: .type) + } catch let error as DecodingError { + throw ResourceObjectDecodingError(error, jsonAPIType: Self.jsonType) + ?? error + } + + id = .init(rawValue: try container.decode(String.self, forKey: .id)) + self.type = type + } + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + try container.encode(type, forKey: .type) + + try container.encode(id, forKey: .id) + } + } } From 9017bc0e00ad8834b48be30b6813703383da5246 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Mon, 21 Apr 2025 10:07:50 -0500 Subject: [PATCH 227/235] remove unmaintained old Swift image variants --- .github/workflows/tests.yml | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index e7cd4df..d6a87ee 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -13,29 +13,19 @@ jobs: fail-fast: false matrix: image: - - swift:5.2-xenial - - swift:5.2-bionic - swift:5.2-focal - swift:5.2-centos8 - - swift:5.2-amazonlinux2 - - swift:5.3-xenial - - swift:5.3-bionic - swift:5.3-focal - swift:5.3-centos8 - - swift:5.3-amazonlinux2 - - swift:5.4-xenial - - swift:5.4-bionic - swift:5.4-focal - swift:5.4-centos8 - - swift:5.4-amazonlinux2 - - swift:5.5-xenial - - swift:5.5-bionic - swift:5.5-focal - swift:5.5-centos8 - - swift:5.5-amazonlinux2 - - swift:5.6-bionic - swift:5.6-focal - - swift:5.6-amazonlinux2 + - swift:5.7-focal + - swift:5.8-focal + - swift:5.9-focal + - swift:5.10-focal container: ${{ matrix.image }} steps: - name: Checkout code From 93981efca07b74749f92f6a865ebca82cbc53161 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Mon, 21 Apr 2025 10:10:16 -0500 Subject: [PATCH 228/235] comment out versions of Linux Swift that have slightly different error messages so tests are not currently passing --- .github/workflows/tests.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index d6a87ee..9644cf8 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -22,10 +22,10 @@ jobs: - swift:5.5-focal - swift:5.5-centos8 - swift:5.6-focal - - swift:5.7-focal - - swift:5.8-focal - - swift:5.9-focal - - swift:5.10-focal +# - swift:5.7-focal +# - swift:5.8-focal +# - swift:5.9-focal +# - swift:5.10-focal container: ${{ matrix.image }} steps: - name: Checkout code From 8cea47ba6cf0d08cf033c357ec0f6128dc0e57f1 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Mon, 28 Apr 2025 09:27:43 -0500 Subject: [PATCH 229/235] switch from google colab examples to markdown --- README.md | 24 +-- documentation/examples/basic-example.md | 174 +++++++++++++++ .../{ => examples}/client-server-example.md | 0 documentation/examples/compound-example.md | 134 ++++++++++++ .../examples/custom-errors-example.md | 153 +++++++++++++ documentation/examples/metadata-example.md | 129 +++++++++++ documentation/examples/patch-example.md | 157 ++++++++++++++ .../examples/resource-storage-example.md | 204 ++++++++++++++++++ .../examples/serverside-get-example.md | 179 +++++++++++++++ .../examples/serverside-post-example.md | 152 +++++++++++++ 10 files changed, 1293 insertions(+), 13 deletions(-) create mode 100644 documentation/examples/basic-example.md rename documentation/{ => examples}/client-server-example.md (100%) create mode 100644 documentation/examples/compound-example.md create mode 100644 documentation/examples/custom-errors-example.md create mode 100644 documentation/examples/metadata-example.md create mode 100644 documentation/examples/patch-example.md create mode 100644 documentation/examples/resource-storage-example.md create mode 100644 documentation/examples/serverside-get-example.md create mode 100644 documentation/examples/serverside-post-example.md diff --git a/README.md b/README.md index c42c9d7..3fe2993 100644 --- a/README.md +++ b/README.md @@ -7,22 +7,20 @@ See the JSON API Spec here: https://jsonapi.org/format/ ## Quick Start -:warning: The following Google Colab examples have correct code, but from time to time the Google Colab Swift compiler may be buggy and claim it cannot build the JSONAPI library. - ### Clientside -- [Basic Example](https://colab.research.google.com/drive/1IS7lRSBGoiW02Vd1nN_rfdDbZvTDj6Te) -- [Compound Example](https://colab.research.google.com/drive/1BdF0Kc7l2ixDfBZEL16FY6palweDszQU) -- [Metadata Example](https://colab.research.google.com/drive/10dEESwiE9I3YoyfzVeOVwOKUTEgLT3qr) -- [Custom Errors Example](https://colab.research.google.com/drive/1TIv6STzlHrkTf_-9Eu8sv8NoaxhZcFZH) -- [PATCH Example](https://colab.research.google.com/drive/16KY-0BoLQKiSUh9G7nYmHzB8b2vhXA2U) -- [Resource Storage Example](https://colab.research.google.com/drive/196eCnBlf2xz8pT4lW--ur6eWSVAjpF6b?usp=sharing) (using [JSONAPI-ResourceStorage](#jsonapi-resourcestorage)) +- [Basic Example](./documentation/examples/basic-example.md) +- [Compound Example](./documentation/examples/compound-example.md) +- [Metadata Example](./documentation/examples/metadata-example.md) +- [Custom Errors Example](./documentation/examples/custom-errors-example.md) +- [PATCH Example](./documentation/examples/patch-example.md) +- [Resource Storage Example](./documentation/examples/resource-storage-example.md) (using [JSONAPI-ResourceStorage](#jsonapi-resourcestorage)) ### Serverside -- [GET Example](https://colab.research.google.com/drive/1krbhzSfz8mwkBTQQnKUZJLEtYsJKSfYX) -- [POST Example](https://colab.research.google.com/drive/1z3n70LwRY7vLIgbsMghvnfHA67QiuqpQ) +- [GET Example](./documentation/examples/serverside-get-example.md) +- [POST Example](./documentation/examples/serverside-post-example.md) ### Client+Server -This library works well when used by both the server responsible for serialization and the client responsible for deserialization. Check out the [example](./documentation/client-server-example.md). +This library works well when used by both the server responsible for serialization and the client responsible for deserialization. Check out the [example](./documentation/examples/client-server-example.md). ## Table of Contents - JSONAPI @@ -34,7 +32,7 @@ This library works well when used by both the server responsible for serializati - [CocoaPods](#cocoapods) - [Running the Playground](#running-the-playground) - [Project Status](./documentation/project-status.md) - - [Server & Client Example](./documentation/client-server-example.md) + - [Server & Client Example](./documentation/examples/client-server-example.md) - [Usage](./documentation/usage.md) - [JSONAPI+Testing](#jsonapitesting) - [Literal Expressibility](#literal-expressibility) @@ -91,7 +89,7 @@ Note that Playground support for importing non-system Frameworks is still a bit ## Deeper Dive - [Project Status](./documentation/project-status.md) -- [Server & Client Example](./documentation/client-server-example.md) +- [Server & Client Example](./documentation/examples/client-server-example.md) - [Usage Documentation](./documentation/usage.md) # JSONAPI+Testing diff --git a/documentation/examples/basic-example.md b/documentation/examples/basic-example.md new file mode 100644 index 0000000..2b1e9fb --- /dev/null +++ b/documentation/examples/basic-example.md @@ -0,0 +1,174 @@ + +# JSONAPI Basic Example + +We are about to walk through a basic example to show how easy it is to set up a +simple model. Information on creating models that take advantage of more of the +features from the JSON:API Specification can be found in the [README](https://github.com/mattpolzin/JSONAPI/blob/main/README.md). + +The `JSONAPI` framework relies heavily on generic types so the first step will +be to alias away some of the JSON:API features we do not need for our simple +example. + +```swift +/// Our Resource objects will not have any metadata or links and they will be identified by Strings. +typealias Resource = JSONAPI.ResourceObject + +/// Our JSON:API Documents will similarly have no metadata or links associated with them. Additionally, there will be no included resources. +typealias SingleDocument = JSONAPI.Document, NoMetadata, NoLinks, NoIncludes, NoAPIDescription, UnknownJSONAPIError> + +typealias BatchDocument = JSONAPI.Document, NoMetadata, NoLinks, NoIncludes, NoAPIDescription, UnknownJSONAPIError> +``` + +The next step is to create `ResourceObjectDescriptions` and `ResourceObjects`. +For our simple example, let's create a `Person` and a `Dog`. + +```swift +struct PersonDescription: ResourceObjectDescription { + // by common convention, we will use the plural form + // of the noun as the JSON:API "type" + static let jsonType: String = "people" + + struct Attributes: JSONAPI.Attributes { + let firstName: Attribute + let lastName: Attribute + + // we mark this attribute as "nullable" because the user can choose + // not to specify an age if they would like to. + let age: Attribute + } + + struct Relationships: JSONAPI.Relationships { + // we will define "Dog" next + let pets: ToManyRelationship + } +} + +// this typealias is optional, but it makes working with resource objects much +// more user friendly. +typealias Person = Resource + +struct DogDescription: ResourceObjectDescription { + static let jsonType: String = "dogs" + + struct Attributes: JSONAPI.Attributes { + let name: Attribute + } + + // we could relate dogs back to their owners, but for the sake of this example + // we will instead show how you would create a resource with no relationships. + typealias Relationships = NoRelationships +} + +typealias Dog = Resource +``` + +At this point we have two objects that can decode JSON:API responses. To +illustrate we can mock up a dog response and parse it. + +```swift +// snag Foundation for JSONDecoder +import Foundation + +let mockBatchDogResponse = +""" +{ + "data": [ + { + "type": "dogs", + "id": "123", + "attributes": { + "name": "Sparky" + } + }, + { + "type": "dogs", + "id": "456", + "attributes": { + "name": "Charlie Dog" + } + } + ] +} +""".data(using: .utf8)! + +let decoder = JSONDecoder() + +let dogsDocument = try! decoder.decode(BatchDocument.self, from: mockBatchDogResponse) + +let dogs = dogsDocument.body.primaryResource!.values + +print("dogs parsed: \(dogs.count ?? 0)") +``` + +To illustrate `ResourceObject` property access, we can loop over the dogs and +print their names. + +```swift +for dog in dogs { + print(dog.name) +} +``` + +Now let's parse a mocked `Person` response. + +```swift +let mockSinglePersonResponse = +""" +{ + "data": { + "type": "people", + "id": "88223", + "attributes": { + "first_name": "Lisa", + "last_name": "Offenbrook", + "age": null + }, + "relationships": { + "pets": { + "data": [ + { + "type": "dogs", + "id": "123" + }, + { + "type": "dogs", + "id": "456" + } + ] + } + } + } +} +""".data(using: .utf8)! + +decoder.keyDecodingStrategy = .convertFromSnakeCase + +let personDocument = try! decoder.decode(SingleDocument.self, from: mockSinglePersonResponse) +``` + +Our `Person` object has both attributes and relationships. Generally what we care +about when accessing relationships is actually the Id(s) of the resource(s); the +loop below shows off how to access those Ids. + +```swift +let person = personDocument.body.primaryResource!.value + +let relatedDogIds = person ~> \.pets + +print("related dog Ids: \(relatedDogIds)") +``` + +To wrap things up, let's throw our dog resources into a local cache and tie +things together a bit. There are many ways to go about storing or caching +resources clientside. For additional examples of more robust solutions, take a +look at [JSONAPI-ResourceStorage](https://github.com/mattpolzin/JSONAPI-ResourceStorage). + +```swift +let dogCache = Dictionary(uniqueKeysWithValues: zip(dogs.map { $0.id }, dogs)) + +func cachedDog(_ id: Dog.Id) -> Dog? { return dogCache[id] } + +print("Our person's name is \(person.firstName) \(person.lastName).") +print("They have \((person ~> \.pets).count) pets named \((person ~> \.pets).compactMap(cachedDog).map { $0.name }.joined(separator: " and ")).") +``` + diff --git a/documentation/client-server-example.md b/documentation/examples/client-server-example.md similarity index 100% rename from documentation/client-server-example.md rename to documentation/examples/client-server-example.md diff --git a/documentation/examples/compound-example.md b/documentation/examples/compound-example.md new file mode 100644 index 0000000..18988dd --- /dev/null +++ b/documentation/examples/compound-example.md @@ -0,0 +1,134 @@ +# JSONAPI Compound Example + +We are about to walk through an example to show how easy it is to parse JSON:API +includes. Information on creating models that take advantage of more of the +features from the JSON:API Specification can be found in the [README](https://github.com/mattpolzin/JSONAPI/blob/main/README.md). + +We will begin by quickly redefining the same types of `ResourceObjects` from the +[Basic Example](https://github.com/mattpolzin/JSONAPI/blob/main/documentation/basic-example.md). + +```swift +typealias Resource = JSONAPI.ResourceObject + +struct PersonDescription: ResourceObjectDescription { + + static let jsonType: String = "people" + + struct Attributes: JSONAPI.Attributes { + let firstName: Attribute + let lastName: Attribute + + let age: Attribute + } + + struct Relationships: JSONAPI.Relationships { + let pets: ToManyRelationship + } +} + +typealias Person = Resource + +struct DogDescription: ResourceObjectDescription { + static let jsonType: String = "dogs" + + struct Attributes: JSONAPI.Attributes { + let name: Attribute + } + + typealias Relationships = NoRelationships +} + +typealias Dog = Resource +``` + +Next we will create similar `typealiases` for single and batch documents as we did +in the **Basic Example**, but we will allow for an include type to be specified. + +```swift +/// Our JSON:API Documents will still have no metadata or links associated with them but they will allow us to specify an include type later. +typealias SingleDocument = JSONAPI.Document, NoMetadata, NoLinks, Include, NoAPIDescription, UnknownJSONAPIError> + +typealias BatchDocument = JSONAPI.Document, NoMetadata, NoLinks, Include, NoAPIDescription, UnknownJSONAPIError> +``` + +Now let's define a mock response containing a single person and including any +dogs that are related to that person. + +```swift +// snag Foundation for Data and JSONDecoder +import Foundation + +let mockSinglePersonResponse = +""" +{ + "data": { + "type": "people", + "id": "88223", + "attributes": { + "first_name": "Lisa", + "last_name": "Offenbrook", + "age": null + }, + "relationships": { + "pets": { + "data": [ + { + "type": "dogs", + "id": "123" + }, + { + "type": "dogs", + "id": "456" + } + ] + } + } + }, + "included": [ + { + "type": "dogs", + "id": "123", + "attributes": { + "name": "Sparky" + } + }, + { + "type": "dogs", + "id": "456", + "attributes": { + "name": "Charlie Dog" + } + } + ] +} +""".data(using: .utf8)! +``` + +Parsing the above response looks almost identical to in the **Basic Example**. The +key thing to note is that instead of specifying `NoIncludes` we specify +`Include1` below. This does not mean "include one dog," it means "include one +type of thing, with that type being `Dog`." The `JSONAPI` framework comes with +built-in support for `Include2<...>`, `Include3<...>` and many more. If you wanted to include +both `Person` and `Dog` resources (perhaps because your primary `Person` resource had +a "friends" relationship), you would use `Include2`. + +```swift +let decoder = JSONDecoder() +decoder.keyDecodingStrategy = .convertFromSnakeCase + +let includeDocument = try! decoder.decode(SingleDocument>.self, from: mockSinglePersonResponse) +``` + +The `Person` is pulled out as before with `Document.body.primaryResource`. The dogs +can be accessed from `Document.body.includes`; note that because multiple types of +things can be included, we must specify that we want things of type `Dog` by using +the `JSONAPI.Includes` subscript operator. + +```swift +let person = includeDocument.body.primaryResource!.value +let dogs = includeDocument.body.includes![Dog.self] + +print("Parsed person named \(person.firstName) \(person.lastName)") +print("Parsed dogs named \(dogs.map { $0.name }.joined(separator: " and "))") +``` + diff --git a/documentation/examples/custom-errors-example.md b/documentation/examples/custom-errors-example.md new file mode 100644 index 0000000..c575ed3 --- /dev/null +++ b/documentation/examples/custom-errors-example.md @@ -0,0 +1,153 @@ + +# JSONAPI Custom Errors Example + +We are about to walk through an example of parsing a JSON:API errors response. +Information on creating models that take advantage of more of the features from +the JSON:API Specification can be found in the [README](https://github.com/mattpolzin/JSONAPI/blob/main/README.md). + +First we will define a structure that can parse each of the errors we might +expect to get back from the server. This is the one type for which the framework +does not offer a generic option but we can pretty easily pick the relevant +properties from the **Error Object** description given by JSON:API +[here](https://www.google.com/url?q=https%3A%2F%2Fjsonapi.org%2Fformat%2F%23error-objects). +We will choose only to distinguish between server and client errors for this +example but that is the tip of the iceberg if you wish to make more robust error +handling for yourself. + +```swift +enum OurExampleError: JSONAPIError { + case unknownError + case server(code: Int, description: String) + case client(code: Int, description: String) + + static var unknown: OurExampleError { return .unknownError } + + // Example decoder just switches on the status code + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + let code = try Int(container.decode(String.self, forKey: .status)) + + guard let statusCode = code else { + throw DecodingError.typeMismatch(Int.self, + .init(codingPath: decoder.codingPath, + debugDescription: "Expected an integer HTTP status code.")) + } + + switch statusCode { + case 400..<500: + self = try .client(code: statusCode, description: container.decode(String.self, forKey: .detail)) + case 500..<600: + self = try .server(code: statusCode, description: container.decode(String.self, forKey: .detail)) + default: + self = .unknown + } + } + + // naturally, opposite of decoding except for needing to put something down + // for the unknown case. We choose 500 here; client won't need to encode errors + // and 500 is fitting for this situation on the server side. + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + let status: String + let detail: String + switch self { + case .server(let code, let description), + .client(let code, let description): + status = String(code) + detail = description + default: + status = "500" + detail = "Unknown problem occurred" + } + + try container.encode(status, forKey: .status) + try container.encode(detail, forKey: .detail) + } + + private enum CodingKeys: String, CodingKey { + case status + case detail + } +} +``` + +Next we will define some utility `typealiases` like we did in the +[Basic Example](https://github.com/mattpolzin/JSONAPI/blob/main/documentation/basic-example.md). +This time, we will specify that our `Document` type expects to parse +`OurExampleError`. + +```swift +typealias Resource = JSONAPI.ResourceObject + +typealias SingleDocument = JSONAPI.Document, NoMetadata, NoLinks, NoIncludes, NoAPIDescription, OurExampleError> +``` + +We will reuse the `Dog` type from the **Basic Example**. We won't actually be +parsing this type because we are showing off error parsing. + +```swift +struct DogDescription: ResourceObjectDescription { + static let jsonType: String = "dogs" + + struct Attributes: JSONAPI.Attributes { + let name: Attribute + } + + typealias Relationships = NoRelationships +} + +typealias Dog = Resource +``` + +Now let's mock up an error response. + +```swift +// snag Foundation for Data and JSONDecoder +import Foundation + +let mockErrorResponse = +""" +{ + "errors": [ + { + "status": "400", + "detail": "You made a bad request" + }, + { + "status": "500", + "detail": "The server fell over because it tried to handle your bad request" + } + ] +} +""".data(using: .utf8)! +``` + +Now we can parse the response data and switch on the response body to see if we +are dealing with an error or successful request (although we know in this case +it will be an error, of course). + +```swift +let decoder = JSONDecoder() + +let dogDocument = try! decoder.decode(SingleDocument.self, from: mockErrorResponse) + +switch dogDocument.body { +case .data(let response): + print("this would be unexpected given our mock data!") + +case .errors(let errors, meta: _, links: _): + print("The server returned the following errors:") + print(errors.map { error -> String in + switch error { + case .client(let code, let description), + .server(let code, let description): + return "\(code): \(description)" + default: + return "unknown" + } + }) +} +``` + diff --git a/documentation/examples/metadata-example.md b/documentation/examples/metadata-example.md new file mode 100644 index 0000000..d1b7ca6 --- /dev/null +++ b/documentation/examples/metadata-example.md @@ -0,0 +1,129 @@ + +# JSONAPI Metadata Example + +We are about to walk through an example of parsing JSON:API metadata. +Information on creating models that take advantage of more of the features from +the JSON:API Specification can be found in the [README](https://github.com/mattpolzin/JSONAPI/blob/main/README.md). + +We will begin by quickly redefining the same types of `ResourceObjects` from the +[Basic Example](https://github.com/mattpolzin/JSONAPI/blob/main/documentation/basic-example.md). + +```swift +typealias Resource = JSONAPI.ResourceObject + +struct PersonDescription: ResourceObjectDescription { + + static let jsonType: String = "people" + + struct Attributes: JSONAPI.Attributes { + let firstName: Attribute + let lastName: Attribute + + let age: Attribute + } + + struct Relationships: JSONAPI.Relationships { + let pets: ToManyRelationship + } +} + +typealias Person = Resource + +struct DogDescription: ResourceObjectDescription { + static let jsonType: String = "dogs" + + struct Attributes: JSONAPI.Attributes { + let name: Attribute + } + + typealias Relationships = NoRelationships +} + +typealias Dog = Resource +``` + +We will additionally define a structure that can parse some pagination metadata. + +```swift +struct PaginationMetadata: JSONAPI.Meta { + + let page: Page + + /// The total count of all resources of the primary type of a given response. + let total: Int + + struct Page: Codable, Equatable { + let index: Int + let size: Int + } +} +``` + +Next we will create similar `typealiases` for single and batch documents as we did +in the **Basic Example**, but we will specify that we expect the `BatchDocument` to +include our `PaginationMetadata`. + +```swift +/// Our JSON:API Documents will still have no metadata or links associated with them but they will allow us to specify an include type later. +typealias SingleDocument = JSONAPI.Document, NoMetadata, NoLinks, NoIncludes, NoAPIDescription, UnknownJSONAPIError> + +typealias BatchDocument = JSONAPI.Document, PaginationMetadata, NoLinks, NoIncludes, NoAPIDescription, UnknownJSONAPIError> +``` + +Now let's define a mock response containing a batch of dogs and pagination +metadata. + +```swift +// snag Foundation for Data and JSONDecoder +import Foundation + +let mockBatchDogResponse = +""" +{ + "data": [ + { + "type": "dogs", + "id": "123", + "attributes": { + "name": "Sparky" + } + }, + { + "type": "dogs", + "id": "456", + "attributes": { + "name": "Charlie Dog" + } + } + ], + "meta": { + "total": 10, + "page": { + "index": 2, + "size": 2 + } + } +} +""".data(using: .utf8)! +``` + +Parsing the above response looks identical to in the **Basic Example**. + +```swift +let decoder = JSONDecoder() +decoder.keyDecodingStrategy = .convertFromSnakeCase + +let metadataDocument = try! decoder.decode(BatchDocument.self, from: mockBatchDogResponse) +``` + +The `Dogs` are pulled out as before with `Document.body.primaryResource`. The +metadata is accessed by the `Document.body.metadata` property. + +```swift +let dogs = metadataDocument.body.primaryResource!.values +let metadata = metadataDocument.body.meta! + +print("Parsed dogs named \(dogs.map { $0.name }.joined(separator: " and "))") +print("Page \(metadata.page.index) out of \(metadata.total / metadata.page.size) at \(metadata.page.size) resources per page.") +``` + diff --git a/documentation/examples/patch-example.md b/documentation/examples/patch-example.md new file mode 100644 index 0000000..d7a8629 --- /dev/null +++ b/documentation/examples/patch-example.md @@ -0,0 +1,157 @@ + +# JSONAPI PATCH Example + +We are about to walk through an example to show how to take an existing resource +object and create a copy with different attributes. Additional information on +the features used here can be found in the [README](https://github.com/mattpolzin/JSONAPI/blob/main/README.md). + +The `JSONAPI` framework relies heavily on generic types so the first step will be +to alias away some of the JSON:API features we do not need for our simple +example. + +```swift +/// Our Resource objects will not have any metadata or links and they will be identified by Strings. +typealias Resource = JSONAPI.ResourceObject + +/// Our JSON:API Documents will similarly have no metadata or links associated with them. Additionally, there will be no included resources. +typealias SingleDocument = JSONAPI.Document, NoMetadata, NoLinks, NoIncludes, NoAPIDescription, UnknownJSONAPIError> +``` + +The next step is to create `ResourceObjectDescriptions` and `ResourceObjects`. For +our simple example, let's create a `Dog`. We will choose to make the properties of +our `Attributes` struct `vars` to facilitate updating them via the `ResourceObject` +`tapping` functions; an alternative approach with an immutable structure is found +later in this tutorial. + +```swift +struct DogDescription: ResourceObjectDescription { + static let jsonType: String = "dogs" + + struct Attributes: JSONAPI.Attributes { + var name: Attribute + } + + typealias Relationships = NoRelationships +} + +typealias Dog = Resource +``` + +At this point we have two objects that can decode JSON:API responses. To +illustrate we can mock up a dog response we might receive from the server and +parse it. + +```swift +// snag Foundation for JSONDecoder +import Foundation + +let mockDogResponse = +""" +{ + "data": { + "type": "dogs", + "id": "123", + "attributes": { + "name": "Sparky" + } + } +} +""".data(using: .utf8)! + +let decoder = JSONDecoder() + +let dogDocument = try! decoder.decode(SingleDocument.self, from: mockDogResponse) + +let dog = dogDocument.body.primaryResource!.value +``` + +We'll demonstrate renaming the dog using `ResourceObject`'s `tappingAttributes()` +function. + +```swift +let updatedDog = dog + .tappingAttributes { $0.name = .init(value: "Charlie") } +``` + +Now we can prepare a document to be used as the request body of a `PATCH` request. + +```swift +let patchRequestDocument = SingleDocument(apiDescription: .none, + body: .init(resourceObject: updatedDog), + includes: .none, + meta: .none, + links: .none) + +let requestBody = JSONEncoder().encode(patchRequestDocument) +``` + +Instead of actually sending off a `PATCH` request, we will just print the request +body out to prove to ourselves that the name was updated. + +```swift +print(String(data: requestBody, encoding: .utf8)!) +``` + +---- + +Alternatively, the `Attributes` struct could have been defined with `let` +properties. Not much changes, but the `name` cannot be mutated so the entire +struct must be recreated. We will take this opportunity to use the +`ResourceObject` `replacingAttributes()` method to contrast it to the +`tappingAttributes()` method. + +The `ImmutableDogDescription` below is almost identical to `DogDescription`, but the `name` +is a `let` constant. + +```swift +struct ImmutableDogDescription: ResourceObjectDescription { + static let jsonType: String = "dogs" + + struct Attributes: JSONAPI.Attributes { + let name: Attribute + } + + typealias Relationships = NoRelationships +} + +typealias Dog2 = Resource +``` + +We can use the same mock data for a single dog document and parse it as a `Dog2`. + +```swift +let dogDocument2 = try! decoder.decode(SingleDocument.self, from: mockDogResponse) + +let dog2 = dogDocument2.body.primaryResource!.value +``` + +We could still use `tappingAttributes()` but we cannot mutate the name property of +the new `Attributes` struct, so we will use `replacingAttributes()` instead. This +method takes as its parameter a function that returns the new attributes. + +```swift +let updatedDog2 = dog2 + .replacingAttributes { _ in + return .init(name: .init(value: "Toby")) +} +``` + +Now create a request document. + +```swift +let patchRequestDocument2 = SingleDocument(apiDescription: .none, + body: .init(resourceObject: updatedDog2), + includes: .none, + meta: .none, + links: .none) + +let requestBody2 = JSONEncoder().encode(patchRequestDocument2) +``` + +Once again, we'll print the request body out instead of sending it with a `PATCH` +request. + +```swift +print(String(data: requestBody2, encoding: .utf8)!) +``` + diff --git a/documentation/examples/resource-storage-example.md b/documentation/examples/resource-storage-example.md new file mode 100644 index 0000000..4e63ae7 --- /dev/null +++ b/documentation/examples/resource-storage-example.md @@ -0,0 +1,204 @@ + +# JSONAPI Resource Storage Example + +We are about to walk through an example to show one possible way to handle +resource caching on the clientside. This example depends on both +[JSONAPI](https://github.com/mattpolzin/JSONAPI) and +[JSONAPI-ResourceStorage](https://github.com/mattpolzin/JSONAPI-ResourceStorage). + +Information on creating models that take advantage of more of the features from +the JSON:API Specification can be found in the [README](https://github.com/mattpolzin/JSONAPI/blob/main/README.md). + +We will begin by quickly redefining the same types of `ResourceObjects` from the +[Basic Example](https://github.com/mattpolzin/JSONAPI/blob/main/documentation/basic-example.md). + +```swift +typealias Resource = JSONAPI.ResourceObject + +struct PersonDescription: ResourceObjectDescription { + + static let jsonType: String = "people" + + struct Attributes: JSONAPI.Attributes { + let firstName: Attribute + let lastName: Attribute + + /// User is not required to specify their age. + let age: Attribute + } + + struct Relationships: JSONAPI.Relationships { + let pets: ToManyRelationship + } +} + +typealias Person = Resource + +struct DogDescription: ResourceObjectDescription { + static let jsonType: String = "dogs" + + struct Attributes: JSONAPI.Attributes { + let name: Attribute + } + + typealias Relationships = NoRelationships +} + +typealias Dog = Resource +``` + +We can borrow the `Document` `typealiases` from the +[Compound Example](https://github.com/mattpolzin/JSONAPI/blob/main/documentation/compound-example.md). + +```swift +/// Our JSON:API Documents will still have no metadata or links associated with them but they will allow us to specify an include type later. +typealias SingleDocument = JSONAPI.Document, NoMetadata, NoLinks, Include, NoAPIDescription, UnknownJSONAPIError> + +typealias BatchDocument = JSONAPI.Document, NoMetadata, NoLinks, Include, NoAPIDescription, UnknownJSONAPIError> +``` + +We define a resource cache capable of storing `Person` and `Dog` types. As a +convenience, we define what it means to merge two `Caches`. The `merge` method is +not a requirement of `ResourceCache` but it will allow us to easily add resources +from our JSON:API document to our cache. + +We are going to use a value type for the cache. A reference type (like the one +in the `JSONAPIResourceStore` module in this package) could also be used, but an +equatable value type works well when you want your app state to be comparable so +your logic can determine when the cache has changed. + +```swift +struct Cache: Equatable, ResourceCache { + var people: ResourceHash = [:] + var dogs: ResourceHash = [:] + + mutating func merge(_ other: Cache) { + // we merge and resolve conflicts with `other`'s versions so we effectively + // "add or update" each resource. + people.merge(other.people, uniquingKeysWith: { $1 }) + dogs.merge(other.dogs, uniquingKeysWith: { $1 }) + } +} +``` + +We need to tell people and dogs where to find themselves in the cache. + +```swift +extension PersonDescription: Materializable { + public static var cachePath: WritableKeyPath> { \.people } +} + +extension DogDescription: Materializable { + public static var cachePath: WritableKeyPath> { \.dogs } +} +``` + +Let's create our app-wide cache of resources. We are going to use a value type; +a reference type could be used just as well, but a value type that is equatable. + +```swift +var cache = Cache() +``` + +Now let's define a mock response containing a single person and including any +dogs that are related to that person. + +```swift +let mockSinglePersonResponse = +""" +{ + "data": { + "type": "people", + "id": "88223", + "attributes": { + "first_name": "Lisa", + "last_name": "Offenbrook", + "age": null + }, + "relationships": { + "pets": { + "data": [ + { + "type": "dogs", + "id": "123" + }, + { + "type": "dogs", + "id": "456" + } + ] + } + } + }, + "included": [ + { + "type": "dogs", + "id": "123", + "attributes": { + "name": "Sparky" + } + }, + { + "type": "dogs", + "id": "456", + "attributes": { + "name": "Charlie Dog" + } + } + ] +} +""".data(using: .utf8)! +``` + +We decode a document like the one mocked above as a `SingleDocument` specialized +on a primary resource type of `Person` and an include type of `Include1` +(a.k.a. all included resources will be of the same type: `Dog`). + +```swift +let decoder = JSONDecoder() +decoder.keyDecodingStrategy = .convertFromSnakeCase + +let document = try decoder.decode(SingleDocument>.self, from: mockSinglePersonResponse) +``` + +We can ask `document` for a cache of resources it contains. Then we can merge that +into our app-wide cache. + +```swift +if let documentResources = document.resourceCache() { + cache.merge(documentResources) +} else { + // probably time to check for an error response. +} +``` + +We can access all people in the cache. + +```swift +for person in cache.people.values { + print("\(person.firstName) \(person.lastName) has \((person ~> \.pets).count) dogs.") +} +``` + +We can access those dogs via the cache using the cache's subscript operator. + +```swift +for person in cache.people.values { + print("\(person.firstName) \(person.lastName) has pets named:") + for dogId in (person ~> \.pets) { + print(cache[dogId]?.name ?? "missing dog info") + } +} +``` + +We can also map the dog ids to materialized dogs. + +```swift +for person in cache.people.values { + let dogs = (person ~> \.pets).compactMap { $0.materialized(from: cache) } + let dogNames = dogs.map(\.name).joined(separator: ", ") + + print("\(person.firstName) \(person.lastName) has pets named: \(dogNames)") +} +``` + diff --git a/documentation/examples/serverside-get-example.md b/documentation/examples/serverside-get-example.md new file mode 100644 index 0000000..a3dc390 --- /dev/null +++ b/documentation/examples/serverside-get-example.md @@ -0,0 +1,179 @@ + +# JSONAPI Serverside GET Example + +We are about to walk through a basic example of serializing a simple model. +Information on creating models that take advantage of more of the features from +the JSON:API Specification can be found in the [README](https://github.com/mattpolzin/JSONAPI/blob/main/README.md). + +Note that the first two steps here are almost identical to the first two steps +in the +[Clientside Basic Example](https://github.com/mattpolzin/JSONAPI/blob/main/documentation/basic-example.md). +The same Swift resource types you create with this framework can be used by the +client and the server. + +The `JSONAPI` framework relies heavily on generic types so the first step will be +to alias away some of the JSON:API features we do not need for our simple +example. + +```swift +/// Our Resource objects will not have any metadata or links and they will be identified by Strings. +typealias Resource = JSONAPI.ResourceObject + +/// Our JSON:API Documents will similarly have no metadata or links associated with them. Additionally, there will be no included resources. +typealias SingleDocument = JSONAPI.Document, NoMetadata, NoLinks, NoIncludes, NoAPIDescription, UnknownJSONAPIError> + +typealias BatchDocument = JSONAPI.Document, NoMetadata, NoLinks, NoIncludes, NoAPIDescription, UnknownJSONAPIError> +``` + +The next step is to create `ResourceObjectDescriptions` and `ResourceObjects`. For +our simple example, let's create a `Person` and a `Dog`. + +```swift +enum API {} + +struct PersonDescription: ResourceObjectDescription { + // by common convention, we will use the plural form + // of the noun as the JSON:API "type" + static let jsonType: String = "people" + + struct Attributes: JSONAPI.Attributes { + let firstName: Attribute + let lastName: Attribute + + // we mark this attribute as "nullable" because the user can choose + // not to specify an age if they would like to. + let age: Attribute + } + + struct Relationships: JSONAPI.Relationships { + // we will define "Dog" next + let pets: ToManyRelationship + } +} + +// this typealias is optional, but it makes working with resource objects much +// more user friendly. +extension API { + typealias Person = Resource +} + +struct DogDescription: ResourceObjectDescription { + static let jsonType: String = "dogs" + + struct Attributes: JSONAPI.Attributes { + let name: Attribute + } + + // we could relate dogs back to their owners, but for the sake of this example + // we will instead show how you would create a resource with no relationships. + typealias Relationships = NoRelationships +} + +extension API { + typealias Dog = Resource +} +``` + +At this point we have two objects that can encode JSON:API responses. To +illustrate we will skip over the details of reading from the database and assume +we have some data ready to be turned into a JSON:API response for a collection +of `Dogs`. + +```swift +// snag Foundation for JSONDecoder +import Foundation + +// This is just a standin for whatever models you've got coming out of the database. +enum DB { + struct Dog { + let id: Int + let name: String + } +} + +// you could handle this any number of ways, but here we will write an extension +// that gets you the `JSONAPI` models from the database models. +extension DB.Dog { + var serializable: API.Dog { + let attributes = API.Dog.Attributes(name: .init(value: name)) + return API.Dog(id: .init(rawValue: String(id)), + attributes: attributes, + relationships: .none, + meta: .none, + links: .none) + } +} + +let dogs = [ + DB.Dog(id: 123, name: "Sparky"), + DB.Dog(id: 456, name: "Charlie Dog") +].map { $0.serializable } + + +let encoder = JSONEncoder() +encoder.outputFormatting = .prettyPrinted + +let primaryResources = ManyResourceBody(resourceObjects: dogs) + +let dogsDocument = BatchDocument(apiDescription: .none, + body: primaryResources, + includes: .none, + meta: .none, + links: .none) + +let dogsResponse = try! encoder.encode(dogsDocument) + +// At this point you can send the response data to the client in whatever way you like. + +print("dogs response: \(String(data: dogsResponse, encoding: .utf8)!)") +``` + +Let's look at a single `Person` response as well. + +```swift +extension DB { + struct Person { + let id: Int + let firstName: String + let lastName: String + let age: Int? + + /// relationship to dogs created as array of String Ids + let dogs: [Int] + } +} + +extension DB.Person { + var serializable: API.Person { + let attributes = API.Person.Attributes(firstName: .init(value: firstName), + lastName: .init(value: lastName), + age: .init(value: age)) + let relationships = API.Person.Relationships(pets: .init(ids: dogs.map { API.Dog.Id(rawValue: String($0)) })) + + return API.Person(id: .init(rawValue: String(id)), + attributes: attributes, + relationships: relationships, + meta: .none, + links: .none) + } +} + +let person = DB.Person(id: 9876, + firstName: "Julie", + lastName: "Stone", + age: nil, + dogs: [123, 456]).serializable + +let personDocument = SingleDocument(apiDescription: .none, + body: .init(resourceObject: person), + includes: .none, + meta: .none, + links: .none) + +let personResponse = try! encoder.encode(personDocument) + +// At this point you can send the response data to the client in whatever way you like. + +print("person response: \(String(data: personResponse, encoding: .utf8)!)") +``` + diff --git a/documentation/examples/serverside-post-example.md b/documentation/examples/serverside-post-example.md new file mode 100644 index 0000000..5be338f --- /dev/null +++ b/documentation/examples/serverside-post-example.md @@ -0,0 +1,152 @@ + +# JSONAPI Serverside POST Example + +We are about to walk through an example handling a POST (resource creation) +request. Information on creating models that take advantage of more of the +features from the JSON:API Specification can be found in the [README](https://github.com/mattpolzin/JSONAPI/blob/main/README.md). + +We will identify our resources using `UUID`s. + +```swift +// If we wanted to, we could just make `UUID` a `RawIdType` +// extension UUID: RawIdType {} + +// We will go a step further and make it a `CreatableRawIdType` and let `JSONAPI` +// create new unique Ids for people in the POST handling code farther down in this +// example. +extension UUID: CreatableRawIdType { + public static func unique() -> UUID { + return UUID() + } +} +``` + +The `JSONAPI` framework relies heavily on generic types so the first step will be +to alias away some of the JSON:API features we do not need for our simple +example. + +```swift +/// Our Resource objects will not have any metadata or links and they will be identified by UUIDs. +typealias Resource = JSONAPI.ResourceObject + +/// The client will send us a POST request with an unidenfitied resource object. We will call this a "new resource object" +typealias New = JSONAPI.ResourceObject + +/// Our JSON:API Documents will similarly have no metadata or links associated with them. Additionally, there will be no included resources. +typealias SingleDocument = JSONAPI.Document, NoMetadata, NoLinks, NoIncludes, NoAPIDescription, UnknownJSONAPIError> +``` + +The next step is to create `ResourceObjectDescriptions` and `ResourceObjects`. For +our simple example, we will handle a `POST` request for a `Person` resource. + +```swift +enum API {} + +struct PersonDescription: ResourceObjectDescription { + // by common convention, we will use the plural form + // of the noun as the JSON:API "type" + static let jsonType: String = "people" + + struct Attributes: JSONAPI.Attributes { + let firstName: Attribute + let lastName: Attribute + + // we mark this attribute as "nullable" because the user can choose + // not to specify an age if they would like to. + let age: Attribute + } + + typealias Relationships = NoRelationships +} + +// this typealias is optional, but it makes working with resource objects much +// more user friendly. +extension API { + typealias Person = Resource +} +``` + +To illustrate using the `JSONAPI` framework, we will skip over the details of +database reading/writing. Let's mock up a database model for a `Person`. + +```swift +// snag Foundation for JSONDecoder +import Foundation + +// This is just a standin for whatever models you've got coming out of the database. +enum DB { + struct Person { + let id: String + let firstName: String + let lastName: String + let age: Int? + } +} + +// you could handle this any number of ways, but here we will write an initializer +// that gets you a database model from the `JSONAPI` model. +extension DB.Person { + + init(_ person: API.Person) { + id = "\(person.id.rawValue)" + firstName = person.firstName + lastName = person.lastName + age = person.age + } +} +``` + +Now we'll handle a `POST` request by creating a new database record (we'll skip +this detail) and responding with a `Person` resource. + +```swift +// NOTE this request has no Id because the client is requesting this new `Person` be created. +let mockPersonRequest = +""" +{ + "data": { + "type": "people", + "attributes": { + "first_name": "Jimmie", + "last_name": "Glows", + "age": 53 + } + } +} +""".data(using: .utf8)! + +let decoder = JSONDecoder() +decoder.keyDecodingStrategy = .convertFromSnakeCase + +// We will decode a "new" resource (see typealiases earlier in this example) +let requestedPersonDocument = try! decoder.decode(SingleDocument>.self, from: mockPersonRequest) +let requestedPerson = requestedPersonDocument.body.primaryResource!.value + +// Our DB.Person initializer expects an identified `Person`, not a `New` +// but we can let the `JSONAPI` framework create a new `UUID` for us: +let identifiedPerson = requestedPerson.identified(byType: UUID.self) + +let dbPerson = DB.Person(identifiedPerson) + +// Here's where we would save our `dbPerson` to te database, if we had an +// actualy database connection in this example. We'd also create our response from +// the result of that database save, ideally. We are going to skip those details +// and pretend the database write was successful. + +// finally, let's create a response +let encoder = JSONEncoder() +encoder.keyEncodingStrategy = .convertToSnakeCase +encoder.outputFormatting = .prettyPrinted + +let responseData = try! encoder.encode(SingleDocument(apiDescription: .none, + body: .init(resourceObject: identifiedPerson), + includes: .none, + meta: .none, + links: .none)) + +// Send it off to the client! + +print("response body:") +print("\(String(data: responseData, encoding: .utf8)!)") +``` + From 28a8a30334b07c7a813063a4496f6d2c25e4fb76 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Mon, 28 Apr 2025 09:40:32 -0500 Subject: [PATCH 230/235] update swift version shield and build/tests shield --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3fe2993..67946cd 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # JSONAPI -[![MIT license](http://img.shields.io/badge/license-MIT-lightgrey.svg)](http://opensource.org/licenses/MIT) [![Swift 5.2+](http://img.shields.io/badge/Swift-5.2/5.3/5.4-blue.svg)](https://swift.org) [![Build Status](https://app.bitrise.io/app/c8295b9589aa401e/status.svg?token=vzcyqWD5bQ4xqQfZsaVzNw&branch=main)](https://app.bitrise.io/app/c8295b9589aa401e) +[![MIT license](http://img.shields.io/badge/license-MIT-lightgrey.svg)](http://opensource.org/licenses/MIT) [![Swift 5.2+](http://img.shields.io/badge/Swift-5.2+-blue.svg)](https://swift.org) [![Tests](https://github.com/mattpolzin/JSONAPI/actions/workflows/tests.yml/badge.svg)](https://github.com/mattpolzin/JSONAPI/actions/workflows/tests.yml) A Swift package for encoding to- and decoding from **JSON API** compliant requests and responses. From 8c15b7838933c565f71ad4ddb04735b662077178 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Thu, 11 Sep 2025 09:24:23 -0500 Subject: [PATCH 231/235] remove support for non-SwiftPM package managers. require Swift 6.0+. --- .github/workflows/tests.yml | 19 +---- JSONAPI.podspec | 141 ------------------------------------ Package.resolved | 25 +++---- Package.swift | 10 +-- README.md | 28 +------ 5 files changed, 25 insertions(+), 198 deletions(-) delete mode 100644 JSONAPI.podspec diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 9644cf8..df8f649 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -13,23 +13,12 @@ jobs: fail-fast: false matrix: image: - - swift:5.2-focal - - swift:5.2-centos8 - - swift:5.3-focal - - swift:5.3-centos8 - - swift:5.4-focal - - swift:5.4-centos8 - - swift:5.5-focal - - swift:5.5-centos8 - - swift:5.6-focal -# - swift:5.7-focal -# - swift:5.8-focal -# - swift:5.9-focal -# - swift:5.10-focal + - swift:6.0 + - swift:6.1 container: ${{ matrix.image }} steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v5 - name: Run tests run: swift test --enable-test-discovery osx: @@ -39,6 +28,6 @@ jobs: uses: maxim-lobanov/setup-xcode@v1 with: { 'xcode-version': 'latest' } - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v5 - name: Run tests run: swift test --enable-test-discovery diff --git a/JSONAPI.podspec b/JSONAPI.podspec deleted file mode 100644 index 4a22764..0000000 --- a/JSONAPI.podspec +++ /dev/null @@ -1,141 +0,0 @@ -# -# Be sure to run `pod spec lint JSONAPI.podspec' to ensure this is a -# valid spec and to remove all comments including this before submitting the spec. -# -# To learn more about Podspec attributes see https://docs.cocoapods.org/specification.html -# To see working Podspecs in the CocoaPods repo see https://github.com/CocoaPods/Specs/ -# - -Pod::Spec.new do |spec| - - # ――― Spec Metadata ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # - # - # These will help people to find your library, and whilst it - # can feel like a chore to fill in it's definitely to your advantage. The - # summary should be tweet-length, and the description more in depth. - # - - spec.name = "MP-JSONAPI" - spec.version = "5.1.0" - spec.summary = "Swift Codable JSON API framework." - - # This description is used to generate tags and improve search results. - # * Think: What does it do? Why did you write it? What is the focus? - # * Try to keep it short, snappy and to the point. - # * Write the description between the DESC delimiters below. - # * Finally, don't worry about the indent, CocoaPods strips it! - spec.description = <<-DESC - A Swift package for encoding to- and decoding from JSON API compliant requests and responses. - -See the JSON API Spec here: https://jsonapi.org/format/ - DESC - - spec.homepage = "https://github.com/mattpolzin/JSONAPI" - # spec.screenshots = "www.example.com/screenshots_1.gif", "www.example.com/screenshots_2.gif" - - - # ――― Spec License ――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # - # - # Licensing your code is important. See https://choosealicense.com for more info. - # CocoaPods will detect a license file if there is a named LICENSE* - # Popular ones are 'MIT', 'BSD' and 'Apache License, Version 2.0'. - # - - # spec.license = "MIT" - spec.license = { :type => "MIT", :file => "LICENSE.txt" } - - - # ――― Author Metadata ――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # - # - # Specify the authors of the library, with email addresses. Email addresses - # of the authors are extracted from the SCM log. E.g. $ git log. CocoaPods also - # accepts just a name if you'd rather not provide an email address. - # - # Specify a social_media_url where others can refer to, for example a twitter - # profile URL. - # - - spec.author = { "Mathew Polzin" => "matt.polzin@gmail.com" } - # Or just: spec.author = "Mathew Polzin" - # spec.authors = { "Mathew Polzin" => "matt.polzin@gmail.com" } - # spec.social_media_url = "https://twitter.com/Mathew Polzin" - - # ――― Platform Specifics ――――――――――――――――――――――――――――――――――――――――――――――――――――――― # - # - # If this Pod runs only on iOS or OS X, then specify the platform and - # the deployment target. You can optionally include the target after the platform. - # - - # spec.platform = :ios - # spec.platform = :ios, "5.0" - - # When using multiple platforms - spec.ios.deployment_target = "8.0" - spec.osx.deployment_target = "10.9" - # spec.watchos.deployment_target = "2.0" - # spec.tvos.deployment_target = "9.0" - - - # ――― Source Location ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # - # - # Specify the location from where the source should be retrieved. - # Supports git, hg, bzr, svn and HTTP. - # - - spec.source = { :git => "https://github.com/mattpolzin/JSONAPI.git", :tag => "#{spec.version}" } - - - # ――― Source Code ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # - # - # CocoaPods is smart about how it includes source code. For source files - # giving a folder will include any swift, h, m, mm, c & cpp files. - # For header files it will include any header in the folder. - # Not including the public_header_files will make all headers public. - # - - spec.source_files = "Sources/JSONAPI/**/*.{swift}" - # spec.exclude_files = "Classes/Exclude" - - # spec.public_header_files = "Classes/**/*.h" - - - # ――― Resources ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # - # - # A list of resources included with the Pod. These are copied into the - # target bundle with a build phase script. Anything else will be cleaned. - # You can preserve files from being cleaned, please don't preserve - # non-essential files like tests, examples and documentation. - # - - # spec.resource = "icon.png" - # spec.resources = "Resources/*.png" - - # spec.preserve_paths = "FilesToSave", "MoreFilesToSave" - - - # ――― Project Linking ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # - # - # Link your library with frameworks, or libraries. Libraries do not include - # the lib prefix of their name. - # - - # spec.frameworks = "SomeFramework", "AnotherFramework" - - # spec.library = "iconv" - # spec.libraries = "iconv", "xml2" - - - # ――― Project Settings ――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # - # - # If your library depends on compiler flags you can set them in the xcconfig hash - # where they will only apply to your library. If you depend on other Podspecs - # you can include multiple dependencies to ensure it works. - - spec.swift_version = "5.2" - spec.module_name = "JSONAPI" - # spec.requires_arc = true - - # spec.xcconfig = { "HEADER_SEARCH_PATHS" => "$(SDKROOT)/usr/include/libxml2" } - spec.dependency "Poly", "~> 2.6" - -end diff --git a/Package.resolved b/Package.resolved index f7c521e..f0e1825 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,16 +1,15 @@ { - "object": { - "pins": [ - { - "package": "Poly", - "repositoryURL": "https://github.com/mattpolzin/Poly.git", - "state": { - "branch": null, - "revision": "99e2e8b575620369be52fe348c0dd72028e3674c", - "version": "2.8.0" - } + "originHash" : "54aae326cbf0090b80ab7860957be502f5396744f5579cf522be7cb3ffa67caf", + "pins" : [ + { + "identity" : "poly", + "kind" : "remoteSourceControl", + "location" : "https://github.com/mattpolzin/Poly.git", + "state" : { + "revision" : "37c942daa23ab373ce05ef87f42103342e59cf3a", + "version" : "3.0.0" } - ] - }, - "version": 1 + } + ], + "version" : 3 } diff --git a/Package.swift b/Package.swift index 81b978c..642091f 100644 --- a/Package.swift +++ b/Package.swift @@ -1,12 +1,12 @@ -// swift-tools-version:5.2 +// swift-tools-version:6.0 import PackageDescription let package = Package( name: "JSONAPI", platforms: [ - .macOS(.v10_10), - .iOS(.v10) + .macOS(.v10_15), + .iOS(.v13) ], products: [ .library( @@ -17,7 +17,7 @@ let package = Package( targets: ["JSONAPITesting"]) ], dependencies: [ - .package(url: "https://github.com/mattpolzin/Poly.git", .upToNextMajor(from: "2.8.0")), + .package(url: "https://github.com/mattpolzin/Poly.git", .upToNextMajor(from: "3.0.0")), ], targets: [ .target( @@ -33,5 +33,5 @@ let package = Package( name: "JSONAPITestingTests", dependencies: ["JSONAPI", "JSONAPITesting"]) ], - swiftLanguageVersions: [.v5] + swiftLanguageModes: [.v5, .v6] ) diff --git a/README.md b/README.md index 67946cd..873a186 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # JSONAPI -[![MIT license](http://img.shields.io/badge/license-MIT-lightgrey.svg)](http://opensource.org/licenses/MIT) [![Swift 5.2+](http://img.shields.io/badge/Swift-5.2+-blue.svg)](https://swift.org) [![Tests](https://github.com/mattpolzin/JSONAPI/actions/workflows/tests.yml/badge.svg)](https://github.com/mattpolzin/JSONAPI/actions/workflows/tests.yml) +[![MIT license](http://img.shields.io/badge/license-MIT-lightgrey.svg)](http://opensource.org/licenses/MIT) [![Swift 6.0+](http://img.shields.io/badge/Swift-6.0+-blue.svg)](https://swift.org) [![Tests](https://github.com/mattpolzin/JSONAPI/actions/workflows/tests.yml/badge.svg)](https://github.com/mattpolzin/JSONAPI/actions/workflows/tests.yml) A Swift package for encoding to- and decoding from **JSON API** compliant requests and responses. @@ -58,35 +58,15 @@ If you find something wrong with this library and it isn't already mentioned und ## Dev Environment ### Prerequisites -1. Swift 5.2+ -2. Swift Package Manager, Xcode 11+, or Cocoapods +1. Swift 6.0+ +2. Swift Package Manager ### Swift Package Manager Just include the following in your package's dependencies and add `JSONAPI` to the dependencies for any of your targets. ```swift -.package(url: "https://github.com/mattpolzin/JSONAPI.git", from: "5.1.0") +.package(url: "https://github.com/mattpolzin/JSONAPI.git", from: "6.0.0") ``` -### Xcode project -With Xcode 11+, you can open the folder containing this repository. There is no need for an Xcode project, but you can generate one with `swift package generate-xcodeproj`. - -### CocoaPods -To use this framework in your project via Cocoapods, add the following dependencies to your Podfile. -```ruby -pod 'Poly', :git => 'https://github.com/mattpolzin/Poly.git' -pod 'MP-JSONAPI', :git => 'https://github.com/mattpolzin/JSONAPI.git' -``` - -### Carthage -This library does not support the Carthage package manager. This is intentional to avoid an additional dependency on Xcode and the Xcode's project files as their format changes throughout versions (in addition to the complexity of maintaining different shared schemes for each supported operating system). - -The difference between supporting and not supporting Carthage is the difference between maintaining an Xcode project with at least one shared build scheme; I encourage those that need Carthage support to fork this repository and add support to their fork by committing an Xcode project (you can generate one as described in the [Xcode project](#xcode-project) section above). Once an Xcode project is generated, you need to mark at least one scheme as [shared](https://github.com/Carthage/Carthage#share-your-xcode-schemes). - -### Running the Playground -To run the included Playground files, create an Xcode project using Swift Package Manager, then create an Xcode Workspace in the root of the repository and add both the generated Xcode project and the playground to the Workspace. - -Note that Playground support for importing non-system Frameworks is still a bit touchy as of Swift 4.2. Sometimes building, cleaning and building, or commenting out and then uncommenting import statements (especially in the` Entities.swift` Playground Source file) can get things working for me when I am getting an error about `JSONAPI` not being found. - ## Deeper Dive - [Project Status](./documentation/project-status.md) - [Server & Client Example](./documentation/examples/client-server-example.md) From 07503f207408a9604f59bf7ce759cbb72c58dc59 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Sat, 13 Sep 2025 17:10:28 -0500 Subject: [PATCH 232/235] a few less controversial changes for starters --- Sources/JSONAPI/Error/BasicJSONAPIError.swift | 6 ++++-- Sources/JSONAPI/Error/GenericJSONAPIError.swift | 2 +- Sources/JSONAPI/JSONAPICodingError.swift | 2 +- Sources/JSONAPI/Resource/Id.swift | 2 +- Sources/JSONAPI/Resource/Poly+PrimaryResource.swift | 6 ++++-- .../Resource Object/ResourceObjectDecodingError.swift | 4 ++-- 6 files changed, 13 insertions(+), 9 deletions(-) diff --git a/Sources/JSONAPI/Error/BasicJSONAPIError.swift b/Sources/JSONAPI/Error/BasicJSONAPIError.swift index 2c18e69..b966229 100644 --- a/Sources/JSONAPI/Error/BasicJSONAPIError.swift +++ b/Sources/JSONAPI/Error/BasicJSONAPIError.swift @@ -39,7 +39,7 @@ public struct BasicJSONAPIErrorPayload: Codable, Eq self.source = source } - public struct Source: Codable, Equatable { + public struct Source: Codable, Equatable, Sendable { /// a JSON Pointer [RFC6901] to the associated entity in the request document [e.g. "/data" for a primary data object, or "/data/attributes/title" for a specific attribute]. public let pointer: String? /// which URI query parameter caused the error @@ -70,6 +70,8 @@ public struct BasicJSONAPIErrorPayload: Codable, Eq } } +extension BasicJSONAPIErrorPayload: Sendable where IdType: Sendable {} + /// `BasicJSONAPIError` optionally decodes many possible fields /// specified by the JSON:API 1.0 Spec. It gives no type-guarantees of what /// will be non-nil, but could provide good diagnostic information when @@ -100,4 +102,4 @@ public struct BasicJSONAPIErrorPayload: Codable, Eq /// with non-nil values in a flattened way. There will be no `source` key /// but there will be `pointer` and `parameter` keys (if those values /// are non-nil). -public typealias BasicJSONAPIError = GenericJSONAPIError> +public typealias BasicJSONAPIError = GenericJSONAPIError> diff --git a/Sources/JSONAPI/Error/GenericJSONAPIError.swift b/Sources/JSONAPI/Error/GenericJSONAPIError.swift index 473caac..457e4f7 100644 --- a/Sources/JSONAPI/Error/GenericJSONAPIError.swift +++ b/Sources/JSONAPI/Error/GenericJSONAPIError.swift @@ -8,7 +8,7 @@ /// `GenericJSONAPIError` can be used to specify whatever error /// payload you expect to need to parse in responses and handle any /// other payload structure as `.unknownError`. -public enum GenericJSONAPIError: JSONAPIError, CustomStringConvertible { +public enum GenericJSONAPIError: JSONAPIError, CustomStringConvertible { case unknownError case error(ErrorPayload) diff --git a/Sources/JSONAPI/JSONAPICodingError.swift b/Sources/JSONAPI/JSONAPICodingError.swift index da0705d..8cf889b 100644 --- a/Sources/JSONAPI/JSONAPICodingError.swift +++ b/Sources/JSONAPI/JSONAPICodingError.swift @@ -13,7 +13,7 @@ public enum JSONAPICodingError: Swift.Error { case missingOrMalformedMetadata(path: [CodingKey]) case missingOrMalformedLinks(path: [CodingKey]) - public enum Quantity: String, Equatable { + public enum Quantity: String, Equatable, Sendable { case one case many diff --git a/Sources/JSONAPI/Resource/Id.swift b/Sources/JSONAPI/Resource/Id.swift index 2fc4bac..fd05123 100644 --- a/Sources/JSONAPI/Resource/Id.swift +++ b/Sources/JSONAPI/Resource/Id.swift @@ -31,7 +31,7 @@ extension String: RawIdType {} /// A type that can be used as the `MaybeRawId` for a `ResourceObject` that does not /// have an Id (most likely because it was created by a client and the server will be responsible /// for assigning it an Id). -public struct Unidentified: MaybeRawId, CustomStringConvertible { +public struct Unidentified: MaybeRawId, CustomStringConvertible, Sendable { public init() {} public var description: String { return "Unidentified" } diff --git a/Sources/JSONAPI/Resource/Poly+PrimaryResource.swift b/Sources/JSONAPI/Resource/Poly+PrimaryResource.swift index 1443610..95f04a7 100644 --- a/Sources/JSONAPI/Resource/Poly+PrimaryResource.swift +++ b/Sources/JSONAPI/Resource/Poly+PrimaryResource.swift @@ -15,12 +15,12 @@ import Poly /// disparate types under one roof for /// the purposes of JSON API compliant /// encoding or decoding. -public typealias EncodableJSONPoly = Poly & EncodablePrimaryResource +public typealias EncodableJSONPoly = Poly & EncodablePrimaryResource & Sendable public typealias EncodablePolyWrapped = Encodable & Equatable public typealias CodablePolyWrapped = EncodablePolyWrapped & Decodable -extension Poly0: CodablePrimaryResource { +extension Poly0: @retroactive Encodable, @retroactive Decodable { public init(from decoder: Decoder) throws { throw JSONAPICodingError.illegalDecoding("Attempted to decode Poly0, which should represent a thing that is not expected to be found in a document.", path: decoder.codingPath) } @@ -30,6 +30,8 @@ extension Poly0: CodablePrimaryResource { } } +extension Poly0: CodablePrimaryResource {} + // MARK: - 1 type extension Poly1: EncodablePrimaryResource, OptionalEncodablePrimaryResource where A: EncodablePolyWrapped {} diff --git a/Sources/JSONAPI/Resource/Resource Object/ResourceObjectDecodingError.swift b/Sources/JSONAPI/Resource/Resource Object/ResourceObjectDecodingError.swift index 004e03c..d458605 100644 --- a/Sources/JSONAPI/Resource/Resource Object/ResourceObjectDecodingError.swift +++ b/Sources/JSONAPI/Resource/Resource Object/ResourceObjectDecodingError.swift @@ -13,7 +13,7 @@ public struct ResourceObjectDecodingError: Swift.Error, Equatable { static let entireObject = "entire object" - public enum Cause: Equatable { + public enum Cause: Equatable, Sendable { case keyNotFound case valueNotFound case typeMismatch(expectedTypeName: String) @@ -26,7 +26,7 @@ public struct ResourceObjectDecodingError: Swift.Error, Equatable { } } - public enum Location: String, Equatable { + public enum Location: String, Equatable, Sendable { case attributes case relationships case relationshipType From 8f66c9247dff207b0bdee1bece897140b304e50a Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Sat, 13 Sep 2025 17:17:07 -0500 Subject: [PATCH 233/235] silence some retroactive conformance warnings where they are acceptable risks --- Sources/JSONAPITesting/Attribute+Literal.swift | 6 +++--- Sources/JSONAPITesting/Optional+Literal.swift | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Sources/JSONAPITesting/Attribute+Literal.swift b/Sources/JSONAPITesting/Attribute+Literal.swift index b631bab..f817a32 100644 --- a/Sources/JSONAPITesting/Attribute+Literal.swift +++ b/Sources/JSONAPITesting/Attribute+Literal.swift @@ -39,7 +39,7 @@ extension Attribute: ExpressibleByFloatLiteral where RawValue: ExpressibleByFloa } } -extension Optional: ExpressibleByFloatLiteral where Wrapped: ExpressibleByFloatLiteral { +extension Optional: @retroactive ExpressibleByFloatLiteral where Wrapped: ExpressibleByFloatLiteral { public typealias FloatLiteralType = Wrapped.FloatLiteralType public init(floatLiteral value: FloatLiteralType) { @@ -55,7 +55,7 @@ extension Attribute: ExpressibleByBooleanLiteral where RawValue: ExpressibleByBo } } -extension Optional: ExpressibleByBooleanLiteral where Wrapped: ExpressibleByBooleanLiteral { +extension Optional: @retroactive ExpressibleByBooleanLiteral where Wrapped: ExpressibleByBooleanLiteral { public typealias BooleanLiteralType = Wrapped.BooleanLiteralType public init(booleanLiteral value: BooleanLiteralType) { @@ -71,7 +71,7 @@ extension Attribute: ExpressibleByIntegerLiteral where RawValue: ExpressibleByIn } } -extension Optional: ExpressibleByIntegerLiteral where Wrapped: ExpressibleByIntegerLiteral { +extension Optional: @retroactive ExpressibleByIntegerLiteral where Wrapped: ExpressibleByIntegerLiteral { public typealias IntegerLiteralType = Wrapped.IntegerLiteralType public init(integerLiteral value: IntegerLiteralType) { diff --git a/Sources/JSONAPITesting/Optional+Literal.swift b/Sources/JSONAPITesting/Optional+Literal.swift index 8c87fab..f0e03d6 100644 --- a/Sources/JSONAPITesting/Optional+Literal.swift +++ b/Sources/JSONAPITesting/Optional+Literal.swift @@ -5,7 +5,7 @@ // Created by Mathew Polzin on 11/29/18. // -extension Optional: ExpressibleByUnicodeScalarLiteral where Wrapped: ExpressibleByUnicodeScalarLiteral { +extension Optional: @retroactive ExpressibleByUnicodeScalarLiteral where Wrapped: ExpressibleByUnicodeScalarLiteral { public typealias UnicodeScalarLiteralType = Wrapped.UnicodeScalarLiteralType public init(unicodeScalarLiteral value: UnicodeScalarLiteralType) { @@ -13,7 +13,7 @@ extension Optional: ExpressibleByUnicodeScalarLiteral where Wrapped: Expressible } } -extension Optional: ExpressibleByExtendedGraphemeClusterLiteral where Wrapped: ExpressibleByExtendedGraphemeClusterLiteral { +extension Optional: @retroactive ExpressibleByExtendedGraphemeClusterLiteral where Wrapped: ExpressibleByExtendedGraphemeClusterLiteral { public typealias ExtendedGraphemeClusterLiteralType = Wrapped.ExtendedGraphemeClusterLiteralType public init(extendedGraphemeClusterLiteral value: ExtendedGraphemeClusterLiteralType) { @@ -21,7 +21,7 @@ extension Optional: ExpressibleByExtendedGraphemeClusterLiteral where Wrapped: E } } -extension Optional: ExpressibleByStringLiteral where Wrapped: ExpressibleByStringLiteral { +extension Optional: @retroactive ExpressibleByStringLiteral where Wrapped: ExpressibleByStringLiteral { public typealias StringLiteralType = Wrapped.StringLiteralType public init(stringLiteral value: StringLiteralType) { From f2e3317242eb53c822f284995f4f8d8b0f03fbe2 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Sat, 13 Sep 2025 17:41:03 -0500 Subject: [PATCH 234/235] and now to conditionally apply sendable more broadly --- Sources/JSONAPI/Document/APIDescription.swift | 4 ++++ Sources/JSONAPI/Document/Document.swift | 15 +++++++++++++++ Sources/JSONAPI/Document/Includes.swift | 2 ++ Sources/JSONAPI/Document/ResourceBody.swift | 6 ++++++ Sources/JSONAPI/Meta/Links.swift | 4 +++- Sources/JSONAPI/Meta/Meta.swift | 2 +- Sources/JSONAPI/Resource/Attribute.swift | 4 ++++ Sources/JSONAPI/Resource/Id.swift | 2 ++ .../JSONAPI/Resource/Poly+PrimaryResource.swift | 2 +- Sources/JSONAPI/Resource/Relationship.swift | 15 +++++++++++++++ .../Resource/Resource Object/ResourceObject.swift | 11 +++++++++-- .../Comparisons/IncludesCompareTests.swift | 4 ++-- 12 files changed, 64 insertions(+), 7 deletions(-) diff --git a/Sources/JSONAPI/Document/APIDescription.swift b/Sources/JSONAPI/Document/APIDescription.swift index cb417a6..123d056 100644 --- a/Sources/JSONAPI/Document/APIDescription.swift +++ b/Sources/JSONAPI/Document/APIDescription.swift @@ -21,6 +21,8 @@ public struct APIDescription: APIDescriptionType { } } +extension APIDescription: Sendable where Meta: Sendable {} + /// Can be used as `APIDescriptionType` for Documents that do not /// have any API Description (a.k.a. "JSON:API Object"). public struct NoAPIDescription: APIDescriptionType, CustomStringConvertible { @@ -33,6 +35,8 @@ public struct NoAPIDescription: APIDescriptionType, CustomStringConvertible { public var description: String { return "No JSON:API Object" } } +extension NoAPIDescription: Sendable {} + extension APIDescription { private enum CodingKeys: String, CodingKey { case version diff --git a/Sources/JSONAPI/Document/Document.swift b/Sources/JSONAPI/Document/Document.swift index 3673493..402cc6e 100644 --- a/Sources/JSONAPI/Document/Document.swift +++ b/Sources/JSONAPI/Document/Document.swift @@ -186,6 +186,10 @@ public struct Document(_ other: Document.Body.Data, combiningMetaWith metaMerge: (MetaType, MetaType) -> MetaType, diff --git a/Sources/JSONAPI/Document/Includes.swift b/Sources/JSONAPI/Document/Includes.swift index b76225e..8eb5000 100644 --- a/Sources/JSONAPI/Document/Includes.swift +++ b/Sources/JSONAPI/Document/Includes.swift @@ -45,6 +45,8 @@ public struct Includes: Encodable, Equatable { } } +extension Includes: Sendable where I: Sendable {} + extension Includes: Decodable where I: Decodable { public init(from decoder: Decoder) throws { var container = try decoder.unkeyedContainer() diff --git a/Sources/JSONAPI/Document/ResourceBody.swift b/Sources/JSONAPI/Document/ResourceBody.swift index 0986a4f..88b8d44 100644 --- a/Sources/JSONAPI/Document/ResourceBody.swift +++ b/Sources/JSONAPI/Document/ResourceBody.swift @@ -65,6 +65,8 @@ public struct SingleResourceBody: Equatable, Coda } } +extension Link: Sendable where Meta: Sendable, URL: Sendable {} + extension Link where Meta == NoMetadata { public init(url: URL) { self.init(url: url, meta: .none) diff --git a/Sources/JSONAPI/Meta/Meta.swift b/Sources/JSONAPI/Meta/Meta.swift index 7018cd6..51aec42 100644 --- a/Sources/JSONAPI/Meta/Meta.swift +++ b/Sources/JSONAPI/Meta/Meta.swift @@ -21,7 +21,7 @@ extension Optional: Meta where Wrapped: Meta {} /// Use this type when you want to specify not to encode or decode any metadata /// for a type. -public struct NoMetadata: Meta, CustomStringConvertible { +public struct NoMetadata: Meta, CustomStringConvertible, Sendable { public static var none: NoMetadata { return NoMetadata() } public init() { } diff --git a/Sources/JSONAPI/Resource/Attribute.swift b/Sources/JSONAPI/Resource/Attribute.swift index 5b0ed41..e39b884 100644 --- a/Sources/JSONAPI/Resource/Attribute.swift +++ b/Sources/JSONAPI/Resource/Attribute.swift @@ -34,6 +34,8 @@ public struct TransformedAttribute { // If we are using the identity transform, we can skip the transform and guarantee no // error is thrown. @@ -82,6 +84,8 @@ public struct Attribute: AttributeType { } } +extension Attribute: Sendable where RawValue: Sendable {} + extension Attribute: CustomStringConvertible { public var description: String { return "Attribute<\(String(describing: RawValue.self))>(\(String(describing: value)))" diff --git a/Sources/JSONAPI/Resource/Id.swift b/Sources/JSONAPI/Resource/Id.swift index fd05123..d8a727c 100644 --- a/Sources/JSONAPI/Resource/Id.swift +++ b/Sources/JSONAPI/Resource/Id.swift @@ -97,6 +97,8 @@ public struct Id: Equa } } +extension Id: Sendable where RawType: Sendable {} + extension Id: Hashable where RawType: RawIdType { public func hash(into hasher: inout Hasher) { hasher.combine(ObjectIdentifier(Self.self)) diff --git a/Sources/JSONAPI/Resource/Poly+PrimaryResource.swift b/Sources/JSONAPI/Resource/Poly+PrimaryResource.swift index 95f04a7..ef2a340 100644 --- a/Sources/JSONAPI/Resource/Poly+PrimaryResource.swift +++ b/Sources/JSONAPI/Resource/Poly+PrimaryResource.swift @@ -15,7 +15,7 @@ import Poly /// disparate types under one roof for /// the purposes of JSON API compliant /// encoding or decoding. -public typealias EncodableJSONPoly = Poly & EncodablePrimaryResource & Sendable +public typealias EncodableJSONPoly = Poly & EncodablePrimaryResource public typealias EncodablePolyWrapped = Encodable & Equatable public typealias CodablePolyWrapped = EncodablePolyWrapped & Decodable diff --git a/Sources/JSONAPI/Resource/Relationship.swift b/Sources/JSONAPI/Resource/Relationship.swift index 4a2c3fa..bccf453 100644 --- a/Sources/JSONAPI/Resource/Relationship.swift +++ b/Sources/JSONAPI/Resource/Relationship.swift @@ -60,6 +60,12 @@ public struct ToOneRelationship let age: Attribute let favoriteColor: Attribute } - struct Relationships: JSONAPI.Relationships { + struct Relationships: JSONAPI.Relationships & Sendable { let bestFriend: ToOneRelationship let parents: ToManyRelationship } From 1e6b5312f323b0247570e7126c9ae7e76194b400 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Sat, 13 Sep 2025 17:51:22 -0500 Subject: [PATCH 235/235] fixing test types --- .../Comparisons/DocumentCompareTests.swift | 4 ++-- .../Test Helpers/String+CreatableRawIdType.swift | 2 +- Tests/JSONAPITests/Document/DocumentDecodingErrorTests.swift | 2 +- Tests/JSONAPITests/Error/GenericJSONAPIErrorTests.swift | 2 +- .../JSONAPITests/Test Helpers/String+CreatableRawIdType.swift | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Tests/JSONAPITestingTests/Comparisons/DocumentCompareTests.swift b/Tests/JSONAPITestingTests/Comparisons/DocumentCompareTests.swift index dd05fe3..a06c7d8 100644 --- a/Tests/JSONAPITestingTests/Comparisons/DocumentCompareTests.swift +++ b/Tests/JSONAPITestingTests/Comparisons/DocumentCompareTests.swift @@ -143,7 +143,7 @@ fileprivate typealias TestType2 = ResourceObject, NoMetadata, NoLinks, Include2, NoAPIDescription, BasicJSONAPIError> -fileprivate struct TestMetadata: JSONAPI.Meta, CustomStringConvertible { +fileprivate struct TestMetadata: JSONAPI.Meta, CustomStringConvertible, Sendable { let total: Int var description: String { @@ -151,7 +151,7 @@ fileprivate struct TestMetadata: JSONAPI.Meta, CustomStringConvertible { } } -fileprivate struct TestLinks: JSONAPI.Links { +fileprivate struct TestLinks: JSONAPI.Links, Sendable { let link: Link } diff --git a/Tests/JSONAPITestingTests/Test Helpers/String+CreatableRawIdType.swift b/Tests/JSONAPITestingTests/Test Helpers/String+CreatableRawIdType.swift index dd3c8f7..282325a 100644 --- a/Tests/JSONAPITestingTests/Test Helpers/String+CreatableRawIdType.swift +++ b/Tests/JSONAPITestingTests/Test Helpers/String+CreatableRawIdType.swift @@ -7,7 +7,7 @@ import JSONAPI -private var uniqueStringCounter = 0 +nonisolated(unsafe) private var uniqueStringCounter = 0 extension String: CreatableRawIdType { public static func unique() -> String { diff --git a/Tests/JSONAPITests/Document/DocumentDecodingErrorTests.swift b/Tests/JSONAPITests/Document/DocumentDecodingErrorTests.swift index 0100a07..8783794 100644 --- a/Tests/JSONAPITests/Document/DocumentDecodingErrorTests.swift +++ b/Tests/JSONAPITests/Document/DocumentDecodingErrorTests.swift @@ -224,7 +224,7 @@ extension DocumentDecodingErrorTests { case unknownError case basic(BasicError) - struct BasicError: Codable, Equatable { + struct BasicError: Codable, Equatable, Sendable { let code: Int let description: String } diff --git a/Tests/JSONAPITests/Error/GenericJSONAPIErrorTests.swift b/Tests/JSONAPITests/Error/GenericJSONAPIErrorTests.swift index d645c23..6f095cc 100644 --- a/Tests/JSONAPITests/Error/GenericJSONAPIErrorTests.swift +++ b/Tests/JSONAPITests/Error/GenericJSONAPIErrorTests.swift @@ -132,7 +132,7 @@ final class GenericJSONAPIErrorTests: XCTestCase { } } -private struct TestPayload: Codable, Equatable, ErrorDictType { +private struct TestPayload: Codable, Equatable, ErrorDictType, Sendable { let hello: String let world: Int? diff --git a/Tests/JSONAPITests/Test Helpers/String+CreatableRawIdType.swift b/Tests/JSONAPITests/Test Helpers/String+CreatableRawIdType.swift index dd3c8f7..282325a 100644 --- a/Tests/JSONAPITests/Test Helpers/String+CreatableRawIdType.swift +++ b/Tests/JSONAPITests/Test Helpers/String+CreatableRawIdType.swift @@ -7,7 +7,7 @@ import JSONAPI -private var uniqueStringCounter = 0 +nonisolated(unsafe) private var uniqueStringCounter = 0 extension String: CreatableRawIdType { public static func unique() -> String {