mirror of
https://github.com/encounter/JSONAPI.git
synced 2026-03-30 11:18:38 -07:00
Prepping for ToOneRelationships that have optional related types for use in situations where null is an acceptable relationship value.
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user