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 2613c5a..1b0647a 100644 --- a/README.md +++ b/README.md @@ -757,7 +757,7 @@ func articleDocument(includeAuthor: Bool) -> Either = .init(values: [.init(author)]) - return .init(document.including(.init(values: [.init(author)]))) + return .init(document.including(includes)) } } @@ -820,10 +820,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. 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/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() { 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" } diff --git a/Tests/JSONAPITests/ResourceObject/ResourceObjectTests.swift b/Tests/JSONAPITests/ResourceObject/ResourceObjectTests.swift index ba3cd47..2e023a0 100644 --- a/Tests/JSONAPITests/ResourceObject/ResourceObjectTests.swift +++ b/Tests/JSONAPITests/ResourceObject/ResourceObjectTests.swift @@ -10,14 +10,14 @@ import JSONAPI import JSONAPITesting class ResourceObjectTests: XCTestCase { - + func test_relationship_access() { let entity1 = TestEntity1(attributes: .none, relationships: .none, meta: .none, links: .none) let entity2 = TestEntity2(attributes: .none, relationships: .init(other: entity1.pointer), meta: .none, links: .none) - + XCTAssertEqual(entity2.relationships.other, entity1.pointer) } - + func test_relationship_operator_access() { let entity1 = TestEntity1(attributes: .none, relationships: .none, meta: .none, links: .none) let entity2 = TestEntity2(attributes: .none, relationships: .init(other: entity1.pointer), meta: .none, links: .none) @@ -30,6 +30,7 @@ class ResourceObjectTests: 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, Optional(entity1.id)) + XCTAssertEqual((entity ~> \.optionalOne).rawValue, Optional(entity1.id.rawValue)) } func test_toMany_relationship_operator_access() { @@ -47,11 +48,11 @@ class ResourceObjectTests: XCTestCase { XCTAssertEqual(entity ~> \.optionalMany, [entity1.id, entity1.id]) } - + func test_relationshipIds() { let entity1 = TestEntity1(attributes: .none, relationships: .none, meta: .none, links: .none) let entity2 = TestEntity2(attributes: .none, relationships: .init(other: entity1.pointer), meta: .none, links: .none) - + XCTAssertEqual(entity2.relationships.other.id, entity1.id) } @@ -174,7 +175,7 @@ extension ResourceObjectTests { data: entity_some_relationships_no_attributes) XCTAssert(type(of: entity.attributes) == NoAttributes.self) - + XCTAssertEqual((entity ~> \.others).map { $0.rawValue }, ["364B3B69-4DF1-467F-B52E-B0C9E44F666E"]) XCTAssertNoThrow(try TestEntity3.check(entity)) @@ -185,11 +186,11 @@ extension ResourceObjectTests { test_DecodeEncodeEquality(type: TestEntity3.self, data: entity_some_relationships_no_attributes) } - + func test_EntitySomeRelationshipsSomeAttributes() { let entity = decoded(type: TestEntity4.self, data: entity_some_relationships_some_attributes) - + XCTAssertEqual(entity[\.word], "coolio") XCTAssertEqual(entity.word, "coolio") XCTAssertEqual(entity[\.number], 992299) @@ -208,11 +209,11 @@ extension ResourceObjectTests { // MARK: Attribute omission and nullification 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]) @@ -228,11 +229,11 @@ extension ResourceObjectTests { test_DecodeEncodeEquality(type: TestEntity6.self, data: entity_one_omitted_attribute) } - + func test_entityOneNullAttribute() { let entity = decoded(type: TestEntity6.self, data: entity_one_null_attribute) - + XCTAssertEqual(entity[\.here], "Hello") XCTAssertEqual(entity.here, "Hello") XCTAssertEqual(entity[\.maybeHere], "World") @@ -248,11 +249,11 @@ extension ResourceObjectTests { test_DecodeEncodeEquality(type: TestEntity6.self, data: entity_one_null_attribute) } - + func test_entityAllAttribute() { let entity = decoded(type: TestEntity6.self, data: entity_all_attributes) - + XCTAssertEqual(entity[\.here], "Hello") XCTAssertEqual(entity.here, "Hello") XCTAssertEqual(entity[\.maybeHere], "World") @@ -268,11 +269,11 @@ extension ResourceObjectTests { test_DecodeEncodeEquality(type: TestEntity6.self, data: entity_all_attributes) } - + func test_entityOneNullAndOneOmittedAttribute() { 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]) @@ -288,16 +289,16 @@ extension ResourceObjectTests { test_DecodeEncodeEquality(type: TestEntity6.self, data: entity_one_null_and_one_missing_attribute) } - + func test_entityBrokenNullableOmittedAttribute() { XCTAssertThrowsError(try JSONDecoder().decode(TestEntity6.self, from: entity_broken_missing_nullable_attribute)) } - + func test_NullOptionalNullableAttribute() { let entity = decoded(type: TestEntity7.self, data: entity_null_optional_nullable_attribute) - + XCTAssertEqual(entity[\.here], "Hello") XCTAssertEqual(entity.here, "Hello") XCTAssertNil(entity[\.maybeHereMaybeNull]) @@ -311,7 +312,7 @@ extension ResourceObjectTests { test_DecodeEncodeEquality(type: TestEntity7.self, data: entity_null_optional_nullable_attribute) } - + func test_NonNullOptionalNullableAttribute() { let entity = decoded(type: TestEntity7.self, data: entity_non_null_optional_nullable_attribute) @@ -336,7 +337,7 @@ 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) @@ -419,6 +420,7 @@ extension ResourceObjectTests { 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) @@ -690,6 +692,16 @@ extension ResourceObjectTests { 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 @@ -708,7 +720,7 @@ extension ResourceObjectTests { static var jsonType: String { return "second_test_entities"} typealias Attributes = NoAttributes - + struct Relationships: JSONAPI.Relationships { let other: ToOneRelationship } @@ -720,7 +732,7 @@ extension ResourceObjectTests { static var jsonType: String { return "third_test_entities"} typealias Attributes = NoAttributes - + struct Relationships: JSONAPI.Relationships { let others: ToManyRelationship } @@ -793,7 +805,7 @@ extension ResourceObjectTests { static var jsonType: String { return "eighth_test_entities" } typealias Relationships = NoRelationships - + struct Attributes: JSONAPI.Attributes { let string: Attribute let int: Attribute @@ -804,7 +816,7 @@ extension ResourceObjectTests { let nullToString: TransformedAttribute> } } - + typealias TestEntity8 = BasicEntity enum TestEntityType9: ResourceObjectDescription { @@ -922,6 +934,12 @@ extension ResourceObjectTests { return TestEntity1.Identifier(rawValue: "hello") } } + + var toManyMetaRelationship: (TestEntityWithMetaRelationship) -> [TestEntity1.Identifier] { + return { entity in + return [TestEntity1.Identifier.id(from: "hello")] + } + } } } @@ -932,19 +950,19 @@ extension ResourceObjectTests { return String(from) } } - + enum IntPlusOneHundred: Transformer { public static func transform(_ from: Int) -> Int { return from + 100 } } - + enum IntToDouble: Transformer { public static func transform(_ from: Int) -> Double { return Double(from) } } - + enum OptionalToString: Transformer { public static func transform(_ from: T?) -> String { return String(describing: from)