Skip to content

Commit 689517c

Browse files
committed
Add ability to differentiate between things that _act_ like Entities and things that _are_ entities in the sense that the latter are persisted between client and server but the former is just a clientside convenience.
1 parent 65b80ee commit 689517c

3 files changed

Lines changed: 170 additions & 18 deletions

File tree

Sources/JSONAPI/Resource/Entity.swift

Lines changed: 27 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,15 @@ public typealias Attributes = Codable & Equatable
1616

1717
/// Can be used as `Relationships` Type for Entities that do not
1818
/// have any Relationships.
19-
public struct NoRelationships: Relationships {}
19+
public struct NoRelationships: Relationships {
20+
public static var none: NoRelationships { return .init() }
21+
}
2022

2123
/// Can be used as `Attributes` Type for Entities that do not
2224
/// have any Attributes.
23-
public struct NoAttributes: Attributes {}
25+
public struct NoAttributes: Attributes {
26+
public static var none: NoAttributes { return .init() }
27+
}
2428

2529
/// An `EntityDescription` describes a JSON API
2630
/// Resource Object. The Resource Object
@@ -34,10 +38,10 @@ public protocol EntityDescription {
3438
static var type: String { get }
3539
}
3640

37-
/// EntityType is the protocol that Entity conforms to. This
38-
/// protocol lets other types accept any Entity as a generic
39-
/// specialization.
40-
public protocol EntityType: PrimaryResource {
41+
/// EntityProxy is a protocol that can be used to create
42+
/// types that _act_ like Entities but cannot be encoded
43+
/// or decoded as Entities.
44+
public protocol EntityProxy: Equatable {
4145
associatedtype Description: EntityDescription
4246
associatedtype EntityRawIdType: JSONAPI.MaybeRawId
4347

@@ -59,17 +63,24 @@ public protocol EntityType: PrimaryResource {
5963
var relationships: Relationships { get }
6064
}
6165

66+
extension EntityProxy {
67+
/// The JSON API compliant "type" of this `Entity`.
68+
public static var type: String { return Description.type }
69+
}
70+
71+
/// EntityType is the protocol that Entity conforms to. This
72+
/// protocol lets other types accept any Entity as a generic
73+
/// specialization.
74+
public protocol EntityType: EntityProxy, PrimaryResource {
75+
}
76+
6277
public protocol IdentifiableEntityType: EntityType, Relatable where EntityRawIdType: JSONAPI.RawIdType {}
6378

6479
/// An `Entity` is a single model type that can be
6580
/// encoded to or decoded from a JSON API
6681
/// "Resource Object."
6782
/// See https://jsonapi.org/format/#document-resource-objects
6883
public struct Entity<Description: JSONAPI.EntityDescription, EntityRawIdType: JSONAPI.MaybeRawId>: EntityType {
69-
70-
/// The JSON API compliant "type" of this `Entity`.
71-
public static var type: String { return Description.type }
72-
7384
/// The `Entity`'s Id. This can be of type `Unidentified` if
7485
/// the entity is being created clientside and the
7586
/// server is being asked to create a unique Id. Otherwise,
@@ -155,21 +166,21 @@ public extension Entity where EntityRawIdType: JSONAPI.RawIdType {
155166
}
156167

157168
// MARK: Attribute Access
158-
public extension Entity {
169+
public extension EntityProxy {
159170
/// Access the attribute at the given keypath. This just
160171
/// allows you to write `entity[\.propertyName]` instead
161172
/// of `entity.relationships.propertyName`.
162173
subscript<T, TFRM: Transformer>(_ path: KeyPath<Description.Attributes, TransformedAttribute<T, TFRM>>) -> TFRM.To {
163174
return attributes[keyPath: path].value
164175
}
165-
176+
166177
/// Access the attribute at the given keypath. This just
167178
/// allows you to write `entity[\.propertyName]` instead
168179
/// of `entity.relationships.propertyName`.
169180
subscript<T, TFRM: Transformer>(_ path: KeyPath<Description.Attributes, TransformedAttribute<T, TFRM>?>) -> TFRM.To? {
170181
return attributes[keyPath: path]?.value
171182
}
172-
183+
173184
/// Access the attribute at the given keypath. This just
174185
/// allows you to write `entity[\.propertyName]` instead
175186
/// of `entity.relationships.propertyName`.
@@ -179,18 +190,18 @@ public extension Entity {
179190
}
180191

181192
// MARK: Relationship Access
182-
public extension Entity {
193+
public extension EntityProxy {
183194
/// Access to an Id of a `ToOneRelationship`.
184195
/// This allows you to write `entity ~> \.other` instead
185196
/// of `entity.relationships.other.id`.
186-
public static func ~><OtherEntity: OptionalRelatable>(entity: Entity, path: KeyPath<Description.Relationships, ToOneRelationship<OtherEntity>>) -> OtherEntity.WrappedIdentifier {
197+
public static func ~><OtherEntity: OptionalRelatable>(entity: Self, path: KeyPath<Description.Relationships, ToOneRelationship<OtherEntity>>) -> OtherEntity.WrappedIdentifier {
187198
return entity.relationships[keyPath: path].id
188199
}
189200

190201
/// Access to all Ids of a `ToManyRelationship`.
191202
/// This allows you to write `entity ~> \.others` instead
192203
/// of `entity.relationships.others.ids`.
193-
public static func ~><OtherEntity: Relatable>(entity: Entity, path: KeyPath<Description.Relationships, ToManyRelationship<OtherEntity>>) -> [OtherEntity.Identifier] {
204+
public static func ~><OtherEntity: Relatable>(entity: Self, path: KeyPath<Description.Relationships, ToManyRelationship<OtherEntity>>) -> [OtherEntity.Identifier] {
194205
return entity.relationships[keyPath: path].ids
195206
}
196207
}

Sources/JSONAPI/Resource/Id.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ public struct Unidentified: MaybeRawId, CustomStringConvertible {
3535
}
3636

3737
public protocol MaybeId: Codable {
38-
associatedtype EntityType: JSONAPI.EntityType
38+
associatedtype EntityType: JSONAPI.EntityProxy
3939
associatedtype RawType: MaybeRawId
4040
}
4141

@@ -53,7 +53,7 @@ public protocol CreatableIdType: IdType {
5353

5454
/// An Entity ID. These IDs can be encoded to or decoded from
5555
/// JSON API IDs.
56-
public struct Id<RawType: MaybeRawId, EntityType: JSONAPI.EntityType>: Codable, Equatable, MaybeId {
56+
public struct Id<RawType: MaybeRawId, EntityType: JSONAPI.EntityProxy>: Codable, Equatable, MaybeId {
5757

5858
public let rawValue: RawType
5959

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
//
2+
// PolyProxyTests.swift
3+
// JSONAPITests
4+
//
5+
// Created by Mathew Polzin on 11/29/18.
6+
//
7+
8+
import XCTest
9+
import JSONAPI
10+
11+
public class PolyProxyTests: XCTestCase {
12+
func test_generalReasonableness() {
13+
XCTAssertNotEqual(decoded(type: User.self, data: poly_user_stub_1), decoded(type: User.self, data: poly_user_stub_2))
14+
XCTAssertEqual(User.type, "users")
15+
}
16+
17+
func test_UserADecode() {
18+
let polyUserA = decoded(type: User.self, data: poly_user_stub_1)
19+
let userA = decoded(type: UserA.self, data: poly_user_stub_1)
20+
21+
XCTAssertEqual(polyUserA.userA, userA)
22+
XCTAssertNil(polyUserA.userB)
23+
XCTAssertEqual(polyUserA[\.name], "Ken Moore")
24+
XCTAssertEqual(polyUserA.id, "1")
25+
XCTAssertEqual(polyUserA.relationships, .none)
26+
}
27+
28+
func test_UserAEncode() {
29+
test_DecodeEncodeEquality(type: User.self, data: poly_user_stub_1)
30+
test_DecodeEncodeEquality(type: User.self, data: poly_user_stub_2)
31+
}
32+
33+
func test_UserBDecode() {
34+
let polyUserB = decoded(type: User.self, data: poly_user_stub_2)
35+
let userB = decoded(type: UserB.self, data: poly_user_stub_2)
36+
37+
XCTAssertEqual(polyUserB.userB, userB)
38+
XCTAssertNil(polyUserB.userA)
39+
XCTAssertEqual(polyUserB[\.name], "Ken Less")
40+
XCTAssertEqual(polyUserB.id, "2")
41+
XCTAssertEqual(polyUserB.relationships, .none)
42+
}
43+
}
44+
45+
// MARK: - Test types
46+
public extension PolyProxyTests {
47+
public enum UserDescription1: EntityDescription {
48+
public static var type: String { return "users" }
49+
50+
public struct Attributes: JSONAPI.Attributes {
51+
let firstName: Attribute<String>
52+
let lastName: Attribute<String>
53+
}
54+
55+
public typealias Relationships = NoRelationships
56+
}
57+
58+
public enum UserDescription2: EntityDescription {
59+
public static var type: String { return "users" }
60+
61+
public struct Attributes: JSONAPI.Attributes {
62+
let name: Attribute<[String]>
63+
}
64+
65+
public typealias Relationships = NoRelationships
66+
}
67+
68+
public typealias UserA = Entity<UserDescription1>
69+
public typealias UserB = Entity<UserDescription2>
70+
71+
public typealias User = Poly2<UserA, UserB>
72+
}
73+
74+
extension Poly2: EntityProxy where A == PolyProxyTests.UserA, B == PolyProxyTests.UserB {
75+
76+
public var userA: PolyProxyTests.UserA? {
77+
return a
78+
}
79+
80+
public var userB: PolyProxyTests.UserB? {
81+
return b
82+
}
83+
84+
public var id: Id<EntityRawIdType, PolyProxyTests.User> {
85+
switch self {
86+
case .a(let a):
87+
return Id(rawValue: a.id.rawValue)
88+
case .b(let b):
89+
return Id(rawValue: b.id.rawValue)
90+
}
91+
}
92+
93+
public var attributes: SharedUserDescription.Attributes {
94+
switch self {
95+
case .a(let a):
96+
return .init(name: .init(value: "\(a[\.firstName]) \(a[\.lastName])"))
97+
case .b(let b):
98+
return .init(name: .init(value: b[\.name].joined(separator: " ")))
99+
}
100+
}
101+
102+
public var relationships: NoRelationships {
103+
return .none
104+
}
105+
106+
public enum SharedUserDescription: EntityDescription {
107+
public static var type: String { return A.type }
108+
109+
public struct Attributes: JSONAPI.Attributes {
110+
let name: Attribute<String>
111+
}
112+
113+
public typealias Relationships = NoRelationships
114+
}
115+
116+
public typealias Description = SharedUserDescription
117+
118+
public typealias EntityRawIdType = A.EntityRawIdType
119+
}
120+
121+
// MARK: - Test stubs
122+
private let poly_user_stub_1 = """
123+
{
124+
"id": "1",
125+
"type": "users",
126+
"attributes": {
127+
"firstName": "Ken",
128+
"lastName": "Moore"
129+
}
130+
}
131+
""".data(using: .utf8)!
132+
133+
private let poly_user_stub_2 = """
134+
{
135+
"id": "2",
136+
"type": "users",
137+
"attributes": {
138+
"name": ["Ken", "Less"]
139+
}
140+
}
141+
""".data(using: .utf8)!

0 commit comments

Comments
 (0)