Skip to content

Commit aedd5dc

Browse files
committed
Add Entity validation via a function in JSONAPITestLib.
1 parent 3964202 commit aedd5dc

10 files changed

Lines changed: 232 additions & 16 deletions

File tree

JSONAPI.playground/Pages/Test Library.xcplaygroundpage/Contents.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,11 @@ Please enjoy these examples, but allow me the forced casting and the lack of err
1414
// The JSONAPITestLib provides literal expressibility for key types to
1515
// make creating tests easier
1616
let dog = Dog(id: "1234", attributes: Dog.Attributes(name: "Buddy"), relationships: Dog.Relationships(owner: nil))
17+
18+
// MARK: - JSON API structure checking
19+
// The JSONAPITestLib provides a `check` function for each Entity type
20+
// that uses reflection to catch mistakes that are not forbidden by
21+
// Swift's type system but will result in unexpected results when
22+
// encoding/decoding. It is a good idea to add a `check` to each of
23+
// your unit tests that create Entities.
24+
try Dog.check(dog)

README.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -75,10 +75,10 @@ To create an Xcode project for JSONAPI, run
7575
- [x] `href`
7676
- [x] `meta`
7777

78-
### EntityDescription Validator (using reflection)
78+
### Entity Validator (using reflection)
7979
- [ ] Disallow optional array in `Attribute` and `Relationship` (should be empty array, not `null`).
80-
- [ ] Only allow `Attribute` and `TransformAttribute` within `Attributes` struct.
81-
- [ ] Only allow `ToManyRelationship` and `ToOneRelationship` within `Relationships` struct.
80+
- [x] Only allow `TransformedAttribute` and its derivatives within `Attributes` struct.
81+
- [x] Only allow `ToManyRelationship` and `ToOneRelationship` within `Relationships` struct.
8282

8383
### Strict Decoding/Encoding Settings
8484
- [ ] Error (potentially while still encoding/decoding successfully) if an included entity is not related to a primary entity (Turned off by default).
@@ -94,7 +94,7 @@ To create an Xcode project for JSONAPI, run
9494
- [ ] Property-based testing (using `SwiftCheck`)
9595
- [x] Roll my own `Result` or find an alternative that doesn't use `Foundation`.
9696
- [ ] Create more descriptive errors that are easier to use for troubleshooting.
97-
- [ ] Make it easier to construct `Attributes` and `Relationships` values.
97+
- [x] Make it easier to construct `Attributes` and `Relationships` values in test cases (literal expressibility).
9898

9999
## Usage
100100

@@ -330,4 +330,4 @@ extension String: CreatableRawIdType {
330330
```
331331

332332
## Testing
333-
JSONAPI comes with a test library to help you test your JSON API integration. The test library is called `JSONAPITestLib`. It provides literal expressibility for `Attribute`, `ToOneRelationship`, and `Id` in many situations so that you can easily write test `Entity` values into your unit tests. You can see the JSONAPITestLib in action in the Playground included with the JSONAPI repository.
333+
JSONAPI comes with a test library to help you test your JSON API integration. The test library is called `JSONAPITestLib`. It provides literal expressibility for `Attribute`, `ToOneRelationship`, and `Id` in many situations so that you can easily write test `Entity` values into your unit tests. It also provides a `check()` function for each `Entity` type that can be used to catch problems with your JSONAPI structures that are not caught by Swift's type system. You can see the JSONAPITestLib in action in the Playground included with the JSONAPI repository.

Sources/JSONAPI/Resource/Attribute.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@
55
// Created by Mathew Polzin on 11/13/18.
66
//
77

8-
public struct TransformedAttribute<RawValue: Codable, Transformer: JSONAPI.Transformer>: Codable where Transformer.From == RawValue {
8+
public protocol AttributeType: Codable {
9+
}
10+
11+
public struct TransformedAttribute<RawValue: Codable, Transformer: JSONAPI.Transformer>: AttributeType where Transformer.From == RawValue {
912
private let rawValue: RawValue
1013

1114
public let value: Transformer.To

Sources/JSONAPI/Resource/Entity.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,7 @@ public extension Entity {
190190

191191
try container.encode(Entity.type, forKey: .type)
192192

193-
if Identifier.self != Unidentified.self {
193+
if Identifier.self != Unidentified<Description>.self {
194194
try container.encode(id, forKey: .id)
195195
}
196196

@@ -213,7 +213,7 @@ public extension Entity {
213213
throw JSONAPIEncodingError.typeMismatch(expected: Description.type, found: type)
214214
}
215215

216-
id = try (Unidentified() as? Identifier) ?? container.decode(Identifier.self, forKey: .id)
216+
id = try (Unidentified<Description>() as? Identifier) ?? container.decode(Identifier.self, forKey: .id)
217217

218218
attributes = try (NoAttributes() as? Description.Attributes) ?? container.decode(Description.Attributes.self, forKey: .attributes)
219219

Sources/JSONAPI/Resource/Id.swift

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,16 +22,17 @@ public protocol CreatableRawIdType: RawIdType {
2222

2323
extension String: RawIdType {}
2424

25-
public protocol Identifier: Codable, Equatable {}
25+
public protocol Identifier: Codable, Equatable {
26+
associatedtype EntityDescription: JSONAPI.EntityDescription
27+
}
2628

27-
public struct Unidentified: Identifier, CustomStringConvertible {
29+
public struct Unidentified<EntityDescription: JSONAPI.EntityDescription>: Identifier, CustomStringConvertible {
2830
public init() {}
2931

3032
public var description: String { return "Id(Unidentified)" }
3133
}
3234

3335
public protocol IdType: Identifier, CustomStringConvertible {
34-
associatedtype EntityDescription: JSONAPI.EntityDescription
3536
associatedtype RawType: RawIdType
3637

3738
var rawValue: RawType { get }

Sources/JSONAPI/Resource/Relationship.swift

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,13 @@
55
// Created by Mathew Polzin on 8/31/18.
66
//
77

8+
public protocol RelationshipType: Codable {}
9+
810
/// An Entity relationship that can be encoded to or decoded from
911
/// a JSON API "Resource Linkage."
1012
/// See https://jsonapi.org/format/#document-resource-object-linkage
1113
/// A convenient typealias might make your code much more legible: `One<EntityDescription>`
12-
public struct ToOneRelationship<Relatable: JSONAPI.OptionalRelatable>: Equatable, Codable {
14+
public struct ToOneRelationship<Relatable: JSONAPI.OptionalRelatable>: RelationshipType, Equatable {
1315

1416
public let id: Relatable.WrappedIdentifier
1517

@@ -34,7 +36,7 @@ extension ToOneRelationship where Relatable.WrappedIdentifier == Optional<Relata
3436
/// a JSON API "Resource Linkage."
3537
/// See https://jsonapi.org/format/#document-resource-object-linkage
3638
/// A convenient typealias might make your code much more legible: `Many<EntityDescription>`
37-
public struct ToManyRelationship<Relatable: JSONAPI.Relatable>: Equatable, Codable {
39+
public struct ToManyRelationship<Relatable: JSONAPI.Relatable>: RelationshipType, Equatable {
3840

3941
public let ids: [Relatable.Identifier]
4042

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
//
2+
// EntityCheck.swift
3+
// JSONAPITestLib
4+
//
5+
// Created by Mathew Polzin on 11/27/18.
6+
//
7+
8+
import JSONAPI
9+
10+
public enum EntityCheckError: Swift.Error {
11+
case attributesNotStruct
12+
case relationshipsNotStruct
13+
case badAttribute(named: String)
14+
case badRelationship(named: String)
15+
case badId
16+
}
17+
18+
public struct EntityCheckErrors: Swift.Error {
19+
let problems: [EntityCheckError]
20+
}
21+
22+
public protocol OptionalAttributeType {}
23+
24+
extension Optional: OptionalAttributeType where Wrapped: AttributeType {}
25+
26+
public extension Entity {
27+
public static func check(_ entity: Entity) throws {
28+
var problems = [EntityCheckError]()
29+
30+
if Swift.type(of: entity.id).EntityDescription.self != Description.self {
31+
problems.append(.badId)
32+
}
33+
34+
let attributesMirror = Mirror(reflecting: entity.attributes)
35+
36+
if attributesMirror.displayStyle != .`struct` {
37+
problems.append(.attributesNotStruct)
38+
}
39+
40+
for attribute in attributesMirror.children {
41+
if attribute.value as? AttributeType == nil,
42+
attribute.value as? OptionalAttributeType == nil {
43+
problems.append(.badAttribute(named: attribute.label ?? "unnamed"))
44+
}
45+
}
46+
47+
let relationshipsMirror = Mirror(reflecting: entity.relationships)
48+
49+
if relationshipsMirror.displayStyle != .`struct` {
50+
problems.append(.relationshipsNotStruct)
51+
}
52+
53+
for relationship in relationshipsMirror.children {
54+
if relationship.value as? RelationshipType == nil {
55+
problems.append(.badRelationship(named: relationship.label ?? "unnamed"))
56+
}
57+
}
58+
59+
guard problems.count == 0 else {
60+
throw EntityCheckErrors(problems: problems)
61+
}
62+
}
63+
}

Tests/JSONAPITests/Entity/EntityTests.swift

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
import XCTest
99
import JSONAPI
10+
import JSONAPITestLib
1011

1112
class EntityTests: XCTestCase {
1213

@@ -50,6 +51,7 @@ extension EntityTests {
5051

5152
XCTAssert(type(of: entity.relationships) == NoRelationships.self)
5253
XCTAssert(type(of: entity.attributes) == NoAttributes.self)
54+
XCTAssertNoThrow(try TestEntity1.check(entity))
5355
}
5456

5557
func test_EntityNoRelationshipsNoAttributes_encode() {
@@ -64,6 +66,7 @@ extension EntityTests {
6466
XCTAssert(type(of: entity.relationships) == NoRelationships.self)
6567

6668
XCTAssertEqual(entity[\.floater], 123.321)
69+
XCTAssertNoThrow(try TestEntity5.check(entity))
6770
}
6871

6972
func test_EntityNoRelationshipsSomeAttributes_encode() {
@@ -78,6 +81,7 @@ extension EntityTests {
7881
XCTAssert(type(of: entity.attributes) == NoAttributes.self)
7982

8083
XCTAssertEqual((entity ~> \.others).map { $0.rawValue }, ["364B3B69-4DF1-467F-B52E-B0C9E44F666E"])
84+
XCTAssertNoThrow(try TestEntity3.check(entity))
8185
}
8286

8387
func test_EntitySomeRelationshipsNoAttributes_encode() {
@@ -92,6 +96,7 @@ extension EntityTests {
9296
XCTAssertEqual(entity[\.word], "coolio")
9397
XCTAssertEqual(entity[\.number], 992299)
9498
XCTAssertEqual((entity ~> \.other).rawValue, "2DF03B69-4B0A-467F-B52E-B0C9E44FCECF")
99+
XCTAssertNoThrow(try TestEntity4.check(entity))
95100
}
96101

97102
func test_EntitySomeRelationshipsSomeAttributes_encode() {
@@ -110,6 +115,7 @@ extension EntityTests {
110115
XCTAssertEqual(entity[\.here], "Hello")
111116
XCTAssertNil(entity[\.maybeHere])
112117
XCTAssertEqual(entity[\.maybeNull], "World")
118+
XCTAssertNoThrow(try TestEntity6.check(entity))
113119
}
114120

115121
func test_entityOneOmittedAttribute_encode() {
@@ -124,6 +130,7 @@ extension EntityTests {
124130
XCTAssertEqual(entity[\.here], "Hello")
125131
XCTAssertEqual(entity[\.maybeHere], "World")
126132
XCTAssertNil(entity[\.maybeNull])
133+
XCTAssertNoThrow(try TestEntity6.check(entity))
127134
}
128135

129136
func test_entityOneNullAttribute_encode() {
@@ -138,6 +145,7 @@ extension EntityTests {
138145
XCTAssertEqual(entity[\.here], "Hello")
139146
XCTAssertEqual(entity[\.maybeHere], "World")
140147
XCTAssertEqual(entity[\.maybeNull], "!")
148+
XCTAssertNoThrow(try TestEntity6.check(entity))
141149
}
142150

143151
func test_entityAllAttribute_encode() {
@@ -152,6 +160,7 @@ extension EntityTests {
152160
XCTAssertEqual(entity[\.here], "Hello")
153161
XCTAssertNil(entity[\.maybeHere])
154162
XCTAssertNil(entity[\.maybeNull])
163+
XCTAssertNoThrow(try TestEntity6.check(entity))
155164
}
156165

157166
func test_entityOneNullAndOneOmittedAttribute_encode() {
@@ -170,6 +179,7 @@ extension EntityTests {
170179

171180
XCTAssertEqual(entity[\.here], "Hello")
172181
XCTAssertNil(entity[\.maybeHereMaybeNull])
182+
XCTAssertNoThrow(try TestEntity7.check(entity))
173183
}
174184

175185
func test_NullOptionalNullableAttribute_encode() {
@@ -183,6 +193,7 @@ extension EntityTests {
183193

184194
XCTAssertEqual(entity[\.here], "Hello")
185195
XCTAssertEqual(entity[\.maybeHereMaybeNull], "World")
196+
XCTAssertNoThrow(try TestEntity7.check(entity))
186197
}
187198

188199
func test_NonNullOptionalNullableAttribute_encode() {
@@ -203,6 +214,7 @@ extension EntityTests {
203214
XCTAssertEqual(entity[\.plus], 122)
204215
XCTAssertEqual(entity[\.doubleFromInt], 22.0)
205216
XCTAssertEqual(entity[\.nullToString], "nil")
217+
XCTAssertNoThrow(try TestEntity8.check(entity))
206218
}
207219

208220
func test_IntToString_encode() {
@@ -215,8 +227,6 @@ extension EntityTests {
215227
extension EntityTests {
216228
func test_IntOver10_success() {
217229
XCTAssertNoThrow(decoded(type: TestEntity11.self, data: entity_valid_validated_attribute))
218-
219-
220230
}
221231

222232
func test_IntOver10_encode() {
@@ -236,6 +246,7 @@ extension EntityTests {
236246

237247
XCTAssertEqual((entity ~> \.nullableOne)?.rawValue, "3323")
238248
XCTAssertEqual((entity ~> \.one).rawValue, "4459")
249+
XCTAssertNoThrow(try TestEntity9.check(entity))
239250
}
240251

241252
func test_nullableRelationshipNotNull_encode() {
@@ -249,6 +260,7 @@ extension EntityTests {
249260

250261
XCTAssertNil(entity ~> \.nullableOne)
251262
XCTAssertEqual((entity ~> \.one).rawValue, "4452")
263+
XCTAssertNoThrow(try TestEntity9.check(entity))
252264
}
253265

254266
func test_nullableRelationshipIsNull_encode() {
@@ -265,6 +277,7 @@ extension EntityTests {
265277
data: entity_self_ref_relationship)
266278

267279
XCTAssertEqual((entity ~> \.selfRef).rawValue, "1")
280+
XCTAssertNoThrow(try TestEntity10.check(entity))
268281
}
269282

270283
func test_RleationshipsOfSameType_encode() {
@@ -282,6 +295,7 @@ extension EntityTests {
282295

283296
XCTAssertNil(entity[\.me])
284297
XCTAssertEqual(entity.id, Unidentified())
298+
XCTAssertNoThrow(try UnidentifiedTestEntity.check(entity))
285299
}
286300

287301
func test_UnidentifiedEntity_encode() {
@@ -295,6 +309,7 @@ extension EntityTests {
295309

296310
XCTAssertEqual(entity[\.me], "unknown")
297311
XCTAssertEqual(entity.id, Unidentified())
312+
XCTAssertNoThrow(try UnidentifiedTestEntity.check(entity))
298313
}
299314

300315
func test_UnidentifiedEntityWithAttributes_encode() {

Tests/JSONAPITests/Test Helpers/Entity+Id.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,4 @@ import JSONAPI
99

1010
public typealias Entity<Description: JSONAPI.EntityDescription> = JSONAPI.Entity<Description, Id<String, Description>>
1111

12-
public typealias NewEntity<Description: JSONAPI.EntityDescription> = JSONAPI.Entity<Description, Unidentified>
12+
public typealias NewEntity<Description: JSONAPI.EntityDescription> = JSONAPI.Entity<Description, Unidentified<Description>>

0 commit comments

Comments
 (0)