diff --git a/README.md b/README.md index 6e64e60..697d863 100644 --- a/README.md +++ b/README.md @@ -58,10 +58,21 @@ The primary goals of this framework are: - [ ] `links` - [ ] `meta` +### EntityDescription Validator +- [ ] Disallow optional array in `Attribute` and `Relationship` (should be empty array, not `null`). +- [ ] Only allow `Attribute` and `TransformAttribute` within `Attributes` struct. +- [ ] Only allow `ToManyRelationship` and `ToOneRelationship` within `Relationships` struct. + +### Strict Decoding/Encoding Settings +- [ ] Error (potentially while still encoding/decoding successfully) if an included entity is not related to a primary entity (Turned off by default). + ### Misc - [x] Support transforms on `Attributes` values (e.g. to support different representations of `Date`) - [x] Support ability to distinguish between `Attributes` fields that are optional (i.e. the key might not be there) and `Attributes` values that are optional (i.e. the key is guaranteed to be there but it might be `null`). -- [ ] `EntityType` validator (using reflection) +- [ ] Fix `ToOneRelationship` so that it is possible to specify an optional relationship where the value is `null` rather than the key being omitted. +- [ ] Conform to `CustomStringConvertible` +- [x] For `NoIncludes`, do not even loop over the "included" JSON API section if it exists. +- [ ] `EntityDescription` validator (using reflection) - [ ] Property-based testing (using `SwiftCheck`) - [ ] Roll my own `Result` or find an alternative that doesn't use `Foundation`. - [ ] Create more descriptive errors that are easier to use for troubleshooting. diff --git a/Sources/JSONAPI/Document/Includes.swift b/Sources/JSONAPI/Document/Includes.swift index 3227ae2..dddfa7e 100644 --- a/Sources/JSONAPI/Document/Includes.swift +++ b/Sources/JSONAPI/Document/Includes.swift @@ -21,6 +21,12 @@ public struct Includes: 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)) diff --git a/Sources/JSONAPI/Resource/Entity.swift b/Sources/JSONAPI/Resource/Entity.swift index 1d35fd2..fc27bde 100644 --- a/Sources/JSONAPI/Resource/Entity.swift +++ b/Sources/JSONAPI/Resource/Entity.swift @@ -129,7 +129,7 @@ extension Entity where EntityType.Attributes == NoAttributes, EntityType.Relatio public extension Entity where EntityType.Identifier: IdType { /// Get a pointer to this entity that can be used as a /// relationship to another entity. - public var pointer: ToOneRelationship { + public var pointer: ToOneRelationship { return ToOneRelationship(entity: self) } } @@ -163,14 +163,14 @@ public extension Entity { /// Access to an Id of a `ToOneRelationship`. /// This allows you to write `entity ~> \.other` instead /// of `entity.relationships.other.id`. - public static func ~>(entity: Entity, path: KeyPath>) -> OtherEntityType.Identifier { + public static func ~>(entity: Entity, path: KeyPath>) -> OtherEntity.Description.Identifier { return entity.relationships[keyPath: path].id } - + /// Access to all Ids of a `ToManyRelationship`. /// This allows you to write `entity ~> \.others` instead /// of `entity.relationships.others.ids`. - public static func ~>(entity: Entity, path: KeyPath>) -> [OtherEntityType.Identifier] { + public static func ~>(entity: Entity, path: KeyPath>) -> [OtherEntity.Description.Identifier] { return entity.relationships[keyPath: path].ids } } diff --git a/Sources/JSONAPI/Resource/Id.swift b/Sources/JSONAPI/Resource/Id.swift index 1c9f616..4813a0b 100644 --- a/Sources/JSONAPI/Resource/Id.swift +++ b/Sources/JSONAPI/Resource/Id.swift @@ -24,17 +24,23 @@ extension String: RawIdType {} public protocol Identifier: Codable, Equatable {} -public struct Unidentified: Identifier { +public struct Unidentified: Identifier, CustomStringConvertible { public init() {} + + public var description: String { return "Id(Unidentified)" } } -public protocol IdType: Identifier { +public protocol IdType: Identifier, CustomStringConvertible { associatedtype EntityType: JSONAPI.EntityDescription associatedtype RawType: RawIdType var rawValue: RawType { get } } +public extension IdType { + var description: String { return "Id(\(String(describing: rawValue)))" } +} + public protocol CreatableIdType: IdType { init() } diff --git a/Sources/JSONAPI/Resource/Relationship.swift b/Sources/JSONAPI/Resource/Relationship.swift index 5845d11..6b74de3 100644 --- a/Sources/JSONAPI/Resource/Relationship.swift +++ b/Sources/JSONAPI/Resource/Relationship.swift @@ -10,7 +10,7 @@ /// You should use the `ToOneRelationship` and `ToManyRelationship` /// concrete types. /// See https://jsonapi.org/format/#document-resource-object-linkage -public protocol Relationship: Equatable, Encodable { +public protocol Relationship: Equatable, Encodable, CustomStringConvertible { associatedtype EntityType: JSONAPI.EntityDescription where EntityType.Identifier: IdType var ids: [EntityType.Identifier] { get } } @@ -19,7 +19,9 @@ public protocol Relationship: Equatable, Encodable { /// 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: Equatable, Relationship, Decodable where EntityType.Identifier: IdType { +public struct ToOneRelationship: Equatable, Relationship, Decodable { + public typealias EntityType = Relatable.Description + public let id: EntityType.Identifier public init(entity: Entity) { @@ -35,7 +37,9 @@ public struct ToOneRelationship: Equatabl /// 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: Equatable, Relationship, Decodable where EntityType.Identifier: IdType { +public struct ToManyRelationship: Equatable, Relationship, Decodable { + public typealias EntityType = Relatable.Description + public let ids: [EntityType.Identifier] public init(entities: [Entity]) { @@ -51,6 +55,24 @@ public struct ToManyRelationship: Equatab } } +/// The OptionalRelatable protocol ONLY describes +/// Optional types. +public protocol OptionalRelatable { + associatedtype Description: EntityDescription where Description.Identifier: IdType +} + +/// The Relatable protocol describes anything that +/// has an EntityDescription +public protocol Relatable: OptionalRelatable {} + +extension Entity: Relatable, OptionalRelatable where EntityType.Identifier: IdType { + public typealias Description = EntityType +} + +extension Optional: OptionalRelatable where Wrapped: Relatable { + public typealias Description = Wrapped.Description +} + // MARK: Codable private enum ResourceLinkageCodingKeys: String, CodingKey { case data = "data" @@ -120,3 +142,12 @@ extension ToManyRelationship { } } } + +// MARK: CustomStringDescribable +public extension ToOneRelationship { + var description: String { return "Relationship(\(String(describing: id)))" } +} + +public extension ToManyRelationship { + var description: String { return "Relationship(\(String(describing: ids)))" } +} diff --git a/Tests/JSONAPITests/Document/DocumentTests.swift b/Tests/JSONAPITests/Document/DocumentTests.swift index b1f6fca..74fbf02 100644 --- a/Tests/JSONAPITests/Document/DocumentTests.swift +++ b/Tests/JSONAPITests/Document/DocumentTests.swift @@ -91,7 +91,7 @@ class DocumentTests: XCTestCase { typealias Attributes = NoAttributes struct Relationships: JSONAPI.Relationships { - let author: ToOneRelationship + let author: ToOneRelationship } } diff --git a/Tests/JSONAPITests/Entity/EntityTests.swift b/Tests/JSONAPITests/Entity/EntityTests.swift index 8423333..9f765dd 100644 --- a/Tests/JSONAPITests/Entity/EntityTests.swift +++ b/Tests/JSONAPITests/Entity/EntityTests.swift @@ -165,7 +165,6 @@ extension EntityTests { } // MARK: Attribute Transformation - extension EntityTests { func test_IntToString() { let entity = try? JSONDecoder().decode(TestEntity8.self, from: entity_int_to_string_attribute) @@ -193,9 +192,9 @@ extension EntityTests { typealias Attributes = NoAttributes typealias Relationships = NoRelatives } - + typealias TestEntity1 = Entity - + enum TestEntityType2: EntityDescription { static var type: String { return "second_test_entities"} @@ -203,12 +202,12 @@ extension EntityTests { typealias Attributes = NoAttributes struct Relationships: JSONAPI.Relationships { - let other: ToOneRelationship + let other: ToOneRelationship } } - + typealias TestEntity2 = Entity - + enum TestEntityType3: EntityDescription { static var type: String { return "third_test_entities"} @@ -216,72 +215,72 @@ extension EntityTests { typealias Attributes = NoAttributes struct Relationships: JSONAPI.Relationships { - let others: ToManyRelationship + let others: ToManyRelationship } } typealias TestEntity3 = Entity - + enum TestEntityType4: EntityDescription { static var type: String { return "fourth_test_entities"} - + typealias Identifier = Id - + struct Relationships: JSONAPI.Relationships { - let other: ToOneRelationship + let other: ToOneRelationship } - + struct Attributes: JSONAPI.Attributes { let word: Attribute let number: Attribute let array: Attribute<[Double]> } } - + typealias TestEntity4 = Entity - + enum TestEntityType5: EntityDescription { static var type: String { return "fifth_test_entities"} - + typealias Identifier = Id typealias Relationships = NoRelatives - + struct Attributes: JSONAPI.Attributes { let floater: Attribute } } - + typealias TestEntity5 = Entity - + enum TestEntityType6: EntityDescription { static var type: String { return "sixth_test_entities" } - + typealias Identifier = Id typealias Relationships = NoRelatives - + struct Attributes: JSONAPI.Attributes { let here: Attribute let maybeHere: Attribute? let maybeNull: Attribute } } - + typealias TestEntity6 = Entity - + enum TestEntityType7: EntityDescription { static var type: String { return "seventh_test_entities" } - + typealias Identifier = Id typealias Relationships = NoRelatives - + struct Attributes: JSONAPI.Attributes { let here: Attribute let maybeHereMaybeNull: Attribute? } } - + typealias TestEntity7 = Entity - + enum TestEntityType8: EntityDescription { static var type: String { return "eighth_test_entities" } @@ -327,13 +326,13 @@ extension EntityTests { } extension Entity where EntityType == EntityTests.TestEntityType2 { - init(other: ToOneRelationship) { + init(other: ToOneRelationship) { self.init(relationships: .init(other: other)) } } extension Entity where EntityType == EntityTests.TestEntityType3 { - init(others: ToManyRelationship) { + init(others: ToManyRelationship) { self.init(relationships: .init(others: others)) } } diff --git a/Tests/JSONAPITests/Includes/IncludeTests.swift b/Tests/JSONAPITests/Includes/IncludeTests.swift index 31fa469..0027b81 100644 --- a/Tests/JSONAPITests/Includes/IncludeTests.swift +++ b/Tests/JSONAPITests/Includes/IncludeTests.swift @@ -6,6 +6,18 @@ class IncludedTests: XCTestCase { let decoder = JSONDecoder() + func test_zeroIncludes() { + let maybeIncludes = try? decoder.decode(Includes.self, from: two_same_type_includes) + + XCTAssertNotNil(maybeIncludes) + + guard let includes = maybeIncludes else { + return + } + + XCTAssertEqual(includes.count, 0) + } + func test_OneInclude() { let maybeIncludes = try? decoder.decode(Includes>.self, from: one_include) @@ -128,7 +140,7 @@ extension IncludedTests { public static var type: String { return "test_entity2" } public struct Relationships: JSONAPI.Relationships { - let entity1: ToOneRelationship + let entity1: ToOneRelationship } public struct Attributes: JSONAPI.Attributes { @@ -147,8 +159,8 @@ extension IncludedTests { public static var type: String { return "test_entity3" } public struct Relationships: JSONAPI.Relationships { - let entity1: ToOneRelationship - let entity2: ToManyRelationship + let entity1: ToOneRelationship + let entity2: ToManyRelationship } } @@ -186,7 +198,7 @@ extension IncludedTests { public static var type: String { return "test_entity6" } struct Relationships: JSONAPI.Relationships { - let entity4: ToOneRelationship + let entity4: ToOneRelationship } } diff --git a/Tests/JSONAPITests/Relationships/RelationshipTests.swift b/Tests/JSONAPITests/Relationships/RelationshipTests.swift index 7ef9204..642d39d 100644 --- a/Tests/JSONAPITests/Relationships/RelationshipTests.swift +++ b/Tests/JSONAPITests/Relationships/RelationshipTests.swift @@ -15,7 +15,7 @@ class RelationshipTests: XCTestCase { let entity2 = TestEntity1() let entity3 = TestEntity1() let entity4 = TestEntity1() - let relationship = ToManyRelationship(entities: [entity1, entity2, entity3, entity4]) + let relationship = ToManyRelationship(entities: [entity1, entity2, entity3, entity4]) XCTAssertEqual(relationship.ids.count, 4) XCTAssertEqual(relationship.ids, [entity1, entity2, entity3, entity4].map { $0.id }) @@ -26,14 +26,14 @@ class RelationshipTests: XCTestCase { let entity2 = TestEntity1() let entity3 = TestEntity1() let entity4 = TestEntity1() - let relationship = ToManyRelationship(relationships: [entity1.pointer, entity2.pointer, entity3.pointer, entity4.pointer]) + let relationship = ToManyRelationship(relationships: [entity1.pointer, entity2.pointer, entity3.pointer, entity4.pointer]) XCTAssertEqual(relationship.ids.count, 4) XCTAssertEqual(relationship.ids, [entity1, entity2, entity3, entity4].map { $0.id }) } func test_ToOneRelationship() { - let relationship = try? JSONDecoder().decode(ToOneRelationship.self, from: to_one_relationship) + let relationship = try? JSONDecoder().decode(ToOneRelationship.self, from: to_one_relationship) XCTAssertNotNil(relationship) @@ -42,7 +42,7 @@ class RelationshipTests: XCTestCase { } func test_ToManyRelationship() { - let relationship = try? JSONDecoder().decode(ToManyRelationship.self, from: to_many_relationship) + let relationship = try? JSONDecoder().decode(ToManyRelationship.self, from: to_many_relationship) XCTAssertNotNil(relationship) @@ -61,3 +61,28 @@ class RelationshipTests: XCTestCase { typealias TestEntity1 = Entity } + +// MARK: omission and nullification +extension RelationshipTests { + func test_omittedRelationship() { + // TODO: fill out test + } + + enum TestEntityType2: EntityDescription { + public static var type: String { return "test_entity2" } + + typealias Identifier = Id + + typealias Attributes = NoAttributes + + public struct Relationships: JSONAPI.Relationships { + let maybeOne: ToOneRelationship? + let maybeMore: ToManyRelationship? + + let nullableOne: ToOneRelationship + + // a nullable many is not allowed. it should + // just be an empty array. + } + } +}