From c9d388579fd147abaa87ca80971b7b6b867d7951 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Sat, 8 Dec 2018 19:48:10 -0800 Subject: [PATCH] Made it much more convenient to work with Non-EntityType relationships. Discovered and fixed a bug where nullable relationships were encoded incorrectly. --- Sources/JSONAPI/Resource/Attribute.swift | 13 +- Sources/JSONAPI/Resource/Id.swift | 35 +++++- Sources/JSONAPI/Resource/Relationship.swift | 25 ++-- Tests/JSONAPITests/Entity/EntityTests.swift | 2 + .../NonJSONAPIRelatableTests.swift | 116 +++++++++++++----- .../Relationships/RelationshipTests.swift | 22 ++++ .../Test Helpers/EncodeDecode.swift | 4 + .../Test Helpers/PrintEncoded.swift | 12 ++ 8 files changed, 175 insertions(+), 54 deletions(-) create mode 100644 Tests/JSONAPITests/Test Helpers/PrintEncoded.swift diff --git a/Sources/JSONAPI/Resource/Attribute.swift b/Sources/JSONAPI/Resource/Attribute.swift index 40d1eec..ebf1e83 100644 --- a/Sources/JSONAPI/Resource/Attribute.swift +++ b/Sources/JSONAPI/Resource/Attribute.swift @@ -83,12 +83,13 @@ extension TransformedAttribute { // See note in decode above about the weirdness // going on here. - let anyNil: Any? = nil - if let _ = anyNil as? Transformer.From, - (rawValue as Any?) == nil { - try container.encodeNil() - } - +// let anyNil: Any? = nil +// let nilRawValue = anyNil as? Transformer.From +// guard rawValue != nilRawValue else { +// try container.encodeNil() +// return +// } + try container.encode(rawValue) } } diff --git a/Sources/JSONAPI/Resource/Id.swift b/Sources/JSONAPI/Resource/Id.swift index 4c3d1dd..fc611fe 100644 --- a/Sources/JSONAPI/Resource/Id.swift +++ b/Sources/JSONAPI/Resource/Id.swift @@ -34,13 +34,31 @@ public struct Unidentified: MaybeRawId, CustomStringConvertible { public var description: String { return "Unidentified" } } -public protocol MaybeId: Codable { +public protocol OptionalId: Codable { associatedtype IdentifiableType: JSONAPI.JSONTyped associatedtype RawType: MaybeRawId + + var rawValue: RawType { get } + init(rawValue: RawType) } -public protocol IdType: MaybeId, CustomStringConvertible, Hashable where RawType: RawIdType { - var rawValue: RawType { get } +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 extension IdType { @@ -53,7 +71,7 @@ public protocol CreatableIdType: IdType { /// An Entity ID. These IDs can be encoded to or decoded from /// JSON API IDs. -public struct Id: Codable, Equatable, MaybeId { +public struct Id: Equatable, OptionalId { public let rawValue: RawType @@ -63,7 +81,8 @@ public struct Id: Coda public init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer() - rawValue = try container.decode(RawType.self) + let rawValue = try container.decode(RawType.self) + self.init(rawValue: rawValue) } public func encode(to encoder: Encoder) throws { @@ -72,7 +91,11 @@ public struct Id: Coda } } -extension Id: Hashable, CustomStringConvertible, IdType where RawType: RawIdType {} +extension Id: Hashable, CustomStringConvertible, IdType where RawType: RawIdType { + public static func id(from rawValue: RawType) -> Id { + return Id(rawValue: rawValue) + } +} extension Id: CreatableIdType where RawType: CreatableRawIdType { public init() { diff --git a/Sources/JSONAPI/Resource/Relationship.swift b/Sources/JSONAPI/Resource/Relationship.swift index 58a6297..700627a 100644 --- a/Sources/JSONAPI/Resource/Relationship.swift +++ b/Sources/JSONAPI/Resource/Relationship.swift @@ -5,7 +5,7 @@ // Created by Mathew Polzin on 8/31/18. // -public protocol RelationshipType: Codable { +public protocol RelationshipType { associatedtype LinksType associatedtype MetaType @@ -117,7 +117,7 @@ extension ToManyRelationship where MetaType == NoMetadata, LinksType == NoLinks } public protocol Identifiable: JSONTyped { - associatedtype Identifier: Equatable, Codable + associatedtype Identifier: Equatable } /// The Relatable protocol describes anything that @@ -148,7 +148,7 @@ private enum ResourceIdentifierCodingKeys: String, CodingKey { case entityType = "type" } -extension ToOneRelationship { +extension ToOneRelationship: Codable where Identifiable.Identifier: OptionalId { public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: ResourceLinkageCodingKeys.self) @@ -184,7 +184,7 @@ extension ToOneRelationship { throw JSONAPIEncodingError.typeMismatch(expected: Identifiable.type, found: type) } - id = try identifier.decode(Identifiable.Identifier.self, forKey: .id) + id = Identifiable.Identifier(rawValue: try identifier.decode(Identifiable.Identifier.RawType.self, forKey: .id)) } public func encode(to encoder: Encoder) throws { @@ -202,14 +202,23 @@ extension ToOneRelationship { 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, forKey: .id) + try identifier.encode(id.rawValue, forKey: .id) try identifier.encode(Identifiable.type, forKey: .entityType) } } -extension ToManyRelationship { +extension ToManyRelationship: Codable { public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: ResourceLinkageCodingKeys.self) @@ -237,7 +246,7 @@ extension ToManyRelationship { throw JSONAPIEncodingError.typeMismatch(expected: Relatable.type, found: type) } - newIds.append(try identifier.decode(Relatable.Identifier.self, forKey: .id)) + newIds.append(Relatable.Identifier(rawValue: try identifier.decode(Relatable.Identifier.RawType.self, forKey: .id))) } ids = newIds } @@ -258,7 +267,7 @@ extension ToManyRelationship { for id in ids { var identifier = identifiers.nestedContainer(keyedBy: ResourceIdentifierCodingKeys.self) - try identifier.encode(id, forKey: .id) + try identifier.encode(id.rawValue, forKey: .id) try identifier.encode(Relatable.type, forKey: .entityType) } } diff --git a/Tests/JSONAPITests/Entity/EntityTests.swift b/Tests/JSONAPITests/Entity/EntityTests.swift index dd9dc5d..b290cc6 100644 --- a/Tests/JSONAPITests/Entity/EntityTests.swift +++ b/Tests/JSONAPITests/Entity/EntityTests.swift @@ -226,6 +226,8 @@ extension EntityTests { XCTAssertEqual(entity[\.here], "Hello") XCTAssertNil(entity[\.maybeHereMaybeNull]) XCTAssertNoThrow(try TestEntity7.check(entity)) + + print(encodable: entity) } func test_NullOptionalNullableAttribute_encode() { diff --git a/Tests/JSONAPITests/NonJSONAPIRelatable/NonJSONAPIRelatableTests.swift b/Tests/JSONAPITests/NonJSONAPIRelatable/NonJSONAPIRelatableTests.swift index 8d1f9aa..79d957f 100644 --- a/Tests/JSONAPITests/NonJSONAPIRelatable/NonJSONAPIRelatableTests.swift +++ b/Tests/JSONAPITests/NonJSONAPIRelatable/NonJSONAPIRelatableTests.swift @@ -9,45 +9,93 @@ import XCTest import JSONAPI class NonJSONAPIRelatableTests: XCTestCase { + func test_initialization1() { + let e1 = NonJSONAPIEntity(id: .init(rawValue: "hello")) + let e2 = NonJSONAPIEntity(id: .init(rawValue: "world")) + let entity = TestEntity(relationships: .init(one: .init(id: e1.id), many: .init(ids: [e1.id, e2.id]))) + + XCTAssertEqual(entity ~> \.one, e1.id) + XCTAssertEqual(entity ~> \.many, [e1.id, e2.id]) + + XCTAssertNoThrow(try TestEntity.check(entity)) + } + + func test_initialization2_all_relationships_there() { + let e1 = NonJSONAPIEntity(id: .init(rawValue: "hello")) + let e2 = NonJSONAPIEntity(id: .init(rawValue: "world")) + + let entity = TestEntity2(relationships: .init(nullableOne: .init(id: e1.id), nullableMaybeOne: .init(id: e2.id), maybeOne: .init(id: e2.id), maybeMany: .init(ids: [e2.id, e1.id]))) + + XCTAssertEqual((entity ~> \.nullableOne)?.rawValue, "hello") + XCTAssertEqual((entity ~> \.nullableMaybeOne)?.rawValue, "world") + XCTAssertEqual((entity ~> \.maybeOne)?.rawValue, "world") + XCTAssertEqual((entity ~> \.maybeMany)?.map { $0.rawValue }, ["world", "hello"]) + } + + func test_initialization2_all_relationships_missing() { + + let entity = TestEntity2(relationships: .init(nullableOne: .init(id: nil), nullableMaybeOne: .init(id: nil), maybeOne: nil, maybeMany: nil)) + let entity2 = TestEntity2(relationships: .init(nullableOne: .init(id: nil), nullableMaybeOne: nil, maybeOne: nil, maybeMany: nil)) + + XCTAssertNil((entity ~> \.nullableOne)) + XCTAssertNil((entity ~> \.nullableMaybeOne)) + XCTAssertNil((entity ~> \.maybeOne)) + XCTAssertNil((entity ~> \.maybeMany)) + + XCTAssertNil((entity2 ~> \.nullableOne)) + XCTAssertNil((entity2 ~> \.nullableMaybeOne)) + XCTAssertNil((entity2 ~> \.maybeOne)) + XCTAssertNil((entity2 ~> \.maybeMany)) + } } // MARK: - Test Types extension NonJSONAPIRelatableTests { -// enum TestEntityDescription: EntityDescription { -// static var type: String { return "test" } -// -// typealias Attributes = NoAttributes -// -// struct Relationships: JSONAPI.Relationships { -// let one: ToOneRelationship -// let many: ToManyRelationship -// } -// } + enum TestEntityDescription: EntityDescription { + static var type: String { return "test" } -// enum NonJSONAPIEntityDescription: EntityDescription { -// static var type: String { return "other" } -// -// typealias Attributes = NoAttributes -// typealias Relationships = NoRelationships -// } + typealias Attributes = NoAttributes -// struct NonJSONAPIEntity: Relatable, OptionalRelatable, JSONTyped { -// static var type: String { return "other" } -// -// typealias Identifier = NonJSONAPIEntity.Id -// typealias WrappedIdentifier = NonJSONAPIEntity.Id -// -// let id: Id -// -// let attributes: NoAttributes -// let relationships: NoRelationships -// -// struct Id: IdType { -// var rawValue: String -// -// typealias IdentifiableType = NonJSONAPIEntity -// typealias RawType = String -// } -// } + struct Relationships: JSONAPI.Relationships { + let one: ToOneRelationship + let many: ToManyRelationship + } + } + + typealias TestEntity = JSONAPI.Entity + + enum TestEntity2Description: EntityDescription { + static var type: String { return "test" } + + typealias Attributes = NoAttributes + + struct Relationships: JSONAPI.Relationships { + let nullableOne: ToOneRelationship + let nullableMaybeOne: ToOneRelationship? + let maybeOne: ToOneRelationship? + let maybeMany: ToManyRelationship? + } + } + + typealias TestEntity2 = JSONAPI.Entity + + struct NonJSONAPIEntity: Relatable, JSONTyped { + static var type: String { return "other" } + + typealias Identifier = NonJSONAPIEntity.Id + + let id: Id + + struct Id: IdType { + var rawValue: String + + typealias IdentifiableType = NonJSONAPIEntity + typealias RawType = String + + static func id(from rawValue: String) -> Id { + return Id(rawValue: rawValue) + } + } + } } diff --git a/Tests/JSONAPITests/Relationships/RelationshipTests.swift b/Tests/JSONAPITests/Relationships/RelationshipTests.swift index 09a7d00..a295595 100644 --- a/Tests/JSONAPITests/Relationships/RelationshipTests.swift +++ b/Tests/JSONAPITests/Relationships/RelationshipTests.swift @@ -140,6 +140,25 @@ extension RelationshipTests { } } +// MARK: Nullable +extension RelationshipTests { + func test_ToOneNullableIsNullIfNil() { + let relationship = ToOneNullable(entity: nil) + let relationshipData = try! JSONEncoder().encode(relationship) + let relationshipString = String(data: relationshipData, encoding: .utf8)! + + XCTAssertEqual(relationshipString, "{\"data\":null}") + } + + func test_ToOneNullableIsEqualToNonNullableIfNotNil() { + let entity = TestEntity1() + let relationship1 = ToOneNonNullable(entity: entity) + let relationship2 = ToOneNullable(entity: entity) + + XCTAssertEqual(encoded(value: relationship1), encoded(value: relationship2)) + } +} + // MARK: Failure tests extension RelationshipTests { func test_ToManyTypeMismatch() { @@ -172,6 +191,9 @@ extension RelationshipTests { typealias ToManyWithLinks = ToManyRelationship typealias ToManyWithMetaAndLinks = ToManyRelationship + typealias ToOneNullable = ToOneRelationship + typealias ToOneNonNullable = ToOneRelationship + struct TestMeta: JSONAPI.Meta { let a: String } diff --git a/Tests/JSONAPITests/Test Helpers/EncodeDecode.swift b/Tests/JSONAPITests/Test Helpers/EncodeDecode.swift index 3a9bbc9..a2eeb56 100644 --- a/Tests/JSONAPITests/Test Helpers/EncodeDecode.swift +++ b/Tests/JSONAPITests/Test Helpers/EncodeDecode.swift @@ -12,6 +12,10 @@ func decoded(type: T.Type, data: Data) -> T { return try! JSONDecoder().decode(T.self, from: data) } +func encoded(value: T) -> Data { + return try! JSONEncoder().encode(value) +} + /// A helper function that tests that decode() == decode().encode().decode(). /// If decoding is well tested and the above is true then encoding is well /// tested. diff --git a/Tests/JSONAPITests/Test Helpers/PrintEncoded.swift b/Tests/JSONAPITests/Test Helpers/PrintEncoded.swift new file mode 100644 index 0000000..3da4bd8 --- /dev/null +++ b/Tests/JSONAPITests/Test Helpers/PrintEncoded.swift @@ -0,0 +1,12 @@ +// +// PrintEncoded.swift +// JSONAPITests +// +// Created by Mathew Polzin on 12/8/18. +// + +import Foundation + +func print(encodable: T) { + print(String(data: try! JSONEncoder().encode(encodable), encoding: .utf8)!) +}