Prepping for ToOneRelationships that have optional related types for use in situations where null is an acceptable relationship value.

This commit is contained in:
Mathew Polzin
2018-11-14 23:39:12 -08:00
parent fd82d5d7de
commit bf44c2fcdd
9 changed files with 137 additions and 47 deletions
+12 -1
View File
@@ -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.
+6
View File
@@ -21,6 +21,12 @@ public struct Includes<I: IncludeDecoder>: 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))
+4 -4
View File
@@ -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<EntityType> {
public var pointer: ToOneRelationship<Entity> {
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 ~><OtherEntityType: JSONAPI.EntityDescription>(entity: Entity<EntityType>, path: KeyPath<EntityType.Relationships, ToOneRelationship<OtherEntityType>>) -> OtherEntityType.Identifier {
public static func ~><OtherEntity: Relatable>(entity: Entity<EntityType>, path: KeyPath<EntityType.Relationships, ToOneRelationship<OtherEntity>>) -> 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 ~><OtherEntityType: JSONAPI.EntityDescription>(entity: Entity<EntityType>, path: KeyPath<EntityType.Relationships, ToManyRelationship<OtherEntityType>>) -> [OtherEntityType.Identifier] {
public static func ~><OtherEntity: Relatable>(entity: Entity<EntityType>, path: KeyPath<EntityType.Relationships, ToManyRelationship<OtherEntity>>) -> [OtherEntity.Description.Identifier] {
return entity.relationships[keyPath: path].ids
}
}
+8 -2
View File
@@ -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()
}
+34 -3
View File
@@ -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<EntityDescription>`
public struct ToOneRelationship<EntityType: JSONAPI.EntityDescription>: Equatable, Relationship, Decodable where EntityType.Identifier: IdType {
public struct ToOneRelationship<Relatable: JSONAPI.OptionalRelatable>: Equatable, Relationship, Decodable {
public typealias EntityType = Relatable.Description
public let id: EntityType.Identifier
public init(entity: Entity<EntityType>) {
@@ -35,7 +37,9 @@ public struct ToOneRelationship<EntityType: JSONAPI.EntityDescription>: 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<EntityDescription>`
public struct ToManyRelationship<EntityType: JSONAPI.EntityDescription>: Equatable, Relationship, Decodable where EntityType.Identifier: IdType {
public struct ToManyRelationship<Relatable: JSONAPI.Relatable>: Equatable, Relationship, Decodable {
public typealias EntityType = Relatable.Description
public let ids: [EntityType.Identifier]
public init(entities: [Entity<EntityType>]) {
@@ -51,6 +55,24 @@ public struct ToManyRelationship<EntityType: JSONAPI.EntityDescription>: Equatab
}
}
/// The OptionalRelatable protocol ONLY describes
/// Optional<T: Relatable> 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)))" }
}
@@ -91,7 +91,7 @@ class DocumentTests: XCTestCase {
typealias Attributes = NoAttributes
struct Relationships: JSONAPI.Relationships {
let author: ToOneRelationship<AuthorType>
let author: ToOneRelationship<Author>
}
}
+27 -28
View File
@@ -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<TestEntityType1>
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<TestEntityType1>
let other: ToOneRelationship<TestEntity1>
}
}
typealias TestEntity2 = Entity<TestEntityType2>
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<TestEntityType1>
let others: ToManyRelationship<TestEntity1>
}
}
typealias TestEntity3 = Entity<TestEntityType3>
enum TestEntityType4: EntityDescription {
static var type: String { return "fourth_test_entities"}
typealias Identifier = Id<String, TestEntityType4>
struct Relationships: JSONAPI.Relationships {
let other: ToOneRelationship<TestEntityType2>
let other: ToOneRelationship<TestEntity2>
}
struct Attributes: JSONAPI.Attributes {
let word: Attribute<String>
let number: Attribute<Int>
let array: Attribute<[Double]>
}
}
typealias TestEntity4 = Entity<TestEntityType4>
enum TestEntityType5: EntityDescription {
static var type: String { return "fifth_test_entities"}
typealias Identifier = Id<String, TestEntityType5>
typealias Relationships = NoRelatives
struct Attributes: JSONAPI.Attributes {
let floater: Attribute<Double>
}
}
typealias TestEntity5 = Entity<TestEntityType5>
enum TestEntityType6: EntityDescription {
static var type: String { return "sixth_test_entities" }
typealias Identifier = Id<String, TestEntityType6>
typealias Relationships = NoRelatives
struct Attributes: JSONAPI.Attributes {
let here: Attribute<String>
let maybeHere: Attribute<String>?
let maybeNull: Attribute<String?>
}
}
typealias TestEntity6 = Entity<TestEntityType6>
enum TestEntityType7: EntityDescription {
static var type: String { return "seventh_test_entities" }
typealias Identifier = Id<String, TestEntityType7>
typealias Relationships = NoRelatives
struct Attributes: JSONAPI.Attributes {
let here: Attribute<String>
let maybeHereMaybeNull: Attribute<String?>?
}
}
typealias TestEntity7 = Entity<TestEntityType7>
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<EntityTests.TestEntityType1>) {
init(other: ToOneRelationship<EntityTests.TestEntity1>) {
self.init(relationships: .init(other: other))
}
}
extension Entity where EntityType == EntityTests.TestEntityType3 {
init(others: ToManyRelationship<EntityTests.TestEntityType1>) {
init(others: ToManyRelationship<EntityTests.TestEntity1>) {
self.init(relationships: .init(others: others))
}
}
+16 -4
View File
@@ -6,6 +6,18 @@ class IncludedTests: XCTestCase {
let decoder = JSONDecoder()
func test_zeroIncludes() {
let maybeIncludes = try? decoder.decode(Includes<NoIncludes>.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<Include1<TestEntityType>>.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<TestEntityType>
let entity1: ToOneRelationship<TestEntity>
}
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<TestEntityType>
let entity2: ToManyRelationship<TestEntityType2>
let entity1: ToOneRelationship<TestEntity>
let entity2: ToManyRelationship<TestEntity2>
}
}
@@ -186,7 +198,7 @@ extension IncludedTests {
public static var type: String { return "test_entity6" }
struct Relationships: JSONAPI.Relationships {
let entity4: ToOneRelationship<TestEntityType4>
let entity4: ToOneRelationship<TestEntity4>
}
}
@@ -15,7 +15,7 @@ class RelationshipTests: XCTestCase {
let entity2 = TestEntity1()
let entity3 = TestEntity1()
let entity4 = TestEntity1()
let relationship = ToManyRelationship<TestEntityType1>(entities: [entity1, entity2, entity3, entity4])
let relationship = ToManyRelationship<TestEntity1>(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<TestEntityType1>(relationships: [entity1.pointer, entity2.pointer, entity3.pointer, entity4.pointer])
let relationship = ToManyRelationship<TestEntity1>(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<TestEntityType1>.self, from: to_one_relationship)
let relationship = try? JSONDecoder().decode(ToOneRelationship<TestEntity1>.self, from: to_one_relationship)
XCTAssertNotNil(relationship)
@@ -42,7 +42,7 @@ class RelationshipTests: XCTestCase {
}
func test_ToManyRelationship() {
let relationship = try? JSONDecoder().decode(ToManyRelationship<TestEntityType1>.self, from: to_many_relationship)
let relationship = try? JSONDecoder().decode(ToManyRelationship<TestEntity1>.self, from: to_many_relationship)
XCTAssertNotNil(relationship)
@@ -61,3 +61,28 @@ class RelationshipTests: XCTestCase {
typealias TestEntity1 = Entity<TestEntityType1>
}
// 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<String, TestEntityType2>
typealias Attributes = NoAttributes
public struct Relationships: JSONAPI.Relationships {
let maybeOne: ToOneRelationship<TestEntity1>?
let maybeMore: ToManyRelationship<TestEntity1>?
let nullableOne: ToOneRelationship<TestEntity1?>
// a nullable many is not allowed. it should
// just be an empty array.
}
}
}