mirror of
https://github.com/encounter/JSONAPI.git
synced 2026-03-30 11:18:38 -07:00
Support both attributes whose keys can be omitted and attributes whose values can be null.
This commit is contained in:
@@ -60,7 +60,7 @@ The primary goals of this framework are:
|
||||
|
||||
### Misc
|
||||
- [ ] Support transforms on `Attributes` values (e.g. to support different representations of `Date`)
|
||||
- [ ] 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`).
|
||||
- [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)
|
||||
- [ ] Property-based testing (using `SwiftCheck`)
|
||||
- [ ] Roll my own `Result` or find an alternative that doesn't use `Foundation`.
|
||||
@@ -82,8 +82,8 @@ enum PersonDescription: IdentifiedEntityDescription {
|
||||
typealias Identifier = Id<String, PersonDescription>
|
||||
|
||||
struct Attributes: JSONAPI.Attributes {
|
||||
let name: [String]
|
||||
let favoriteColor: String
|
||||
let name: Attribute<[String]>
|
||||
let favoriteColor: Attribute<String>
|
||||
}
|
||||
|
||||
struct Relationships: JSONAPI.Relationships {
|
||||
@@ -152,7 +152,17 @@ let friendIds: [Person.Identifier] = person ~> \.friends
|
||||
|
||||
### `Attributes`
|
||||
|
||||
The `Attributes` of an `EntityDescription` can contain any JSON encodable/decodable types. This is the place to store all attributes of an entity.
|
||||
The `Attributes` of an `EntityDescription` can contain any JSON encodable/decodable types as long as they are wrapped in an `Attribute` `struct`. This is the place to store all attributes of an entity.
|
||||
|
||||
To describe an attribute that may be omitted (i.e. the key might not even be in the JSON object), you make the entire `Attribute` optional:
|
||||
```
|
||||
let optionalAttribute: Attribute<String>?
|
||||
```
|
||||
|
||||
To describe an attribute that is expected to exist but might have a `null` value, you make the value within the `Attribute` optional:
|
||||
```
|
||||
let nullableAttribute: Attribute<String?>
|
||||
```
|
||||
|
||||
An entity that does not have attributes can be described by adding the following to an `EntityDescription`:
|
||||
```
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
//
|
||||
// Attribute.swift
|
||||
// JSONAPI
|
||||
//
|
||||
// Created by Mathew Polzin on 11/13/18.
|
||||
//
|
||||
|
||||
public struct Attribute<Value: Codable>: Codable {
|
||||
public let value: Value
|
||||
}
|
||||
|
||||
extension Attribute: Equatable where Value: Equatable {}
|
||||
|
||||
// MARK: - Codable
|
||||
extension Attribute {
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.singleValueContainer()
|
||||
|
||||
// A little trickery follows. If the value is nil, the
|
||||
// container.decode(Value.self) will fail even if Value
|
||||
// is Optional. However, we can check if decoding nil
|
||||
// succeeds and then attempt to coerce nil to a Value
|
||||
// type at which point we can store nil in `value`.
|
||||
let anyNil: Any? = nil
|
||||
if container.decodeNil(),
|
||||
let val = anyNil as? Value {
|
||||
value = val
|
||||
return
|
||||
}
|
||||
|
||||
value = try container.decode(Value.self)
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.singleValueContainer()
|
||||
|
||||
// See note in decode above about the weirdness
|
||||
// going on here.
|
||||
let anyNil: Any? = nil
|
||||
if let _ = anyNil as? Value,
|
||||
(value as Any?) == nil {
|
||||
try container.encodeNil()
|
||||
}
|
||||
|
||||
try container.encode(value)
|
||||
}
|
||||
}
|
||||
@@ -139,8 +139,15 @@ public extension Entity {
|
||||
/// Access the attribute at the given keypath. This just
|
||||
/// allows you to write `entity[\.propertyName]` instead
|
||||
/// of `entity.relationships.propertyName`.
|
||||
subscript<T>(_ path: KeyPath<EntityType.Attributes, T>) -> T {
|
||||
return attributes[keyPath: path]
|
||||
subscript<T>(_ path: KeyPath<EntityType.Attributes, Attribute<T>>) -> T {
|
||||
return attributes[keyPath: path].value
|
||||
}
|
||||
|
||||
/// Access the attribute at the given keypath. This just
|
||||
/// allows you to write `entity[\.propertyName]` instead
|
||||
/// of `entity.relationships.propertyName`.
|
||||
subscript<T>(_ path: KeyPath<EntityType.Attributes, Attribute<T>?>) -> T? {
|
||||
return attributes[keyPath: path]?.value
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -81,6 +81,69 @@ class EntityTests: XCTestCase {
|
||||
XCTAssertEqual(e[\.number], 992299)
|
||||
XCTAssertEqual((e ~> \.other).rawValue, "2DF03B69-4B0A-467F-B52E-B0C9E44FCECF")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: Attribute omission and nullification
|
||||
extension EntityTests {
|
||||
|
||||
func test_entityOneOmittedAttribute() {
|
||||
let entity = try? JSONDecoder().decode(TestEntity6.self, from: entity_one_omitted_attribute)
|
||||
|
||||
XCTAssertNotNil(entity)
|
||||
|
||||
guard let e = entity else { return }
|
||||
|
||||
XCTAssertEqual(e[\.here], "Hello")
|
||||
XCTAssertNil(e[\.maybeHere])
|
||||
XCTAssertEqual(e[\.maybeNull], "World")
|
||||
}
|
||||
|
||||
func test_entityOneNullAttribute() {
|
||||
let entity = try? JSONDecoder().decode(TestEntity6.self, from: entity_one_null_attribute)
|
||||
|
||||
XCTAssertNotNil(entity)
|
||||
|
||||
guard let e = entity else { return }
|
||||
|
||||
XCTAssertEqual(e[\.here], "Hello")
|
||||
XCTAssertEqual(e[\.maybeHere], "World")
|
||||
XCTAssertNil(e[\.maybeNull])
|
||||
}
|
||||
|
||||
func test_entityAllAttribute() {
|
||||
let entity = try? JSONDecoder().decode(TestEntity6.self, from: entity_all_attributes)
|
||||
|
||||
XCTAssertNotNil(entity)
|
||||
|
||||
guard let e = entity else { return }
|
||||
|
||||
XCTAssertEqual(e[\.here], "Hello")
|
||||
XCTAssertEqual(e[\.maybeHere], "World")
|
||||
XCTAssertEqual(e[\.maybeNull], "!")
|
||||
}
|
||||
|
||||
func test_entityOneNullAndOneOmittedAttribute() {
|
||||
let entity = try? JSONDecoder().decode(TestEntity6.self, from: entity_one_null_and_one_missing_attribute)
|
||||
|
||||
XCTAssertNotNil(entity)
|
||||
|
||||
guard let e = entity else { return }
|
||||
|
||||
XCTAssertEqual(e[\.here], "Hello")
|
||||
XCTAssertNil(e[\.maybeHere])
|
||||
XCTAssertNil(e[\.maybeNull])
|
||||
}
|
||||
|
||||
func test_entityBrokenNullableOmittedAttribute() {
|
||||
let entity = try? JSONDecoder().decode(TestEntity6.self, from: entity_broken_missing_nullable_attribute)
|
||||
|
||||
XCTAssertNil(entity)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Test Types
|
||||
extension EntityTests {
|
||||
|
||||
enum TestEntityType1: EntityDescription {
|
||||
static var type: String { return "test_entities"}
|
||||
@@ -128,8 +191,9 @@ class EntityTests: XCTestCase {
|
||||
}
|
||||
|
||||
struct Attributes: JSONAPI.Attributes {
|
||||
let word: String
|
||||
let number: Int
|
||||
let word: Attribute<String>
|
||||
let number: Attribute<Int>
|
||||
let array: Attribute<[Double]>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,11 +206,26 @@ class EntityTests: XCTestCase {
|
||||
typealias Relationships = NoRelatives
|
||||
|
||||
struct Attributes: JSONAPI.Attributes {
|
||||
let floater: Double
|
||||
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>
|
||||
}
|
||||
|
||||
extension Entity where EntityType == EntityTests.TestEntityType2 {
|
||||
|
||||
@@ -43,7 +43,8 @@ let entity_some_relationships_some_attributes = """
|
||||
"type": "fourth_test_entities",
|
||||
"attributes": {
|
||||
"word": "coolio",
|
||||
"number": 992299
|
||||
"number": 992299,
|
||||
"array": [12.3, 4, 0.1]
|
||||
},
|
||||
"relationships": {
|
||||
"other": {
|
||||
@@ -55,3 +56,60 @@ let entity_some_relationships_some_attributes = """
|
||||
}
|
||||
}
|
||||
""".data(using: .utf8)!
|
||||
|
||||
let entity_one_omitted_attribute = """
|
||||
{
|
||||
"id": "1",
|
||||
"type": "sixth_test_entities",
|
||||
"attributes": {
|
||||
"here": "Hello",
|
||||
"maybeNull": "World"
|
||||
}
|
||||
}
|
||||
""".data(using: .utf8)!
|
||||
|
||||
let entity_one_null_attribute = """
|
||||
{
|
||||
"id": "1",
|
||||
"type": "sixth_test_entities",
|
||||
"attributes": {
|
||||
"here": "Hello",
|
||||
"maybeHere": "World",
|
||||
"maybeNull": null
|
||||
}
|
||||
}
|
||||
""".data(using: .utf8)!
|
||||
|
||||
let entity_all_attributes = """
|
||||
{
|
||||
"id": "1",
|
||||
"type": "sixth_test_entities",
|
||||
"attributes": {
|
||||
"here": "Hello",
|
||||
"maybeHere": "World",
|
||||
"maybeNull": "!"
|
||||
}
|
||||
}
|
||||
""".data(using: .utf8)!
|
||||
|
||||
let entity_one_null_and_one_missing_attribute = """
|
||||
{
|
||||
"id": "1",
|
||||
"type": "sixth_test_entities",
|
||||
"attributes": {
|
||||
"here": "Hello",
|
||||
"maybeNull": null
|
||||
}
|
||||
}
|
||||
""".data(using: .utf8)!
|
||||
|
||||
let entity_broken_missing_nullable_attribute = """
|
||||
{
|
||||
"id": "1",
|
||||
"type": "sixth_test_entities",
|
||||
"attributes": {
|
||||
"here": "Hello",
|
||||
"maybeHere": "World"
|
||||
}
|
||||
}
|
||||
""".data(using: .utf8)!
|
||||
|
||||
@@ -11,8 +11,13 @@ extension DocumentTests {
|
||||
|
||||
extension EntityTests {
|
||||
static let __allTests = [
|
||||
("test_entityAllAttribute", test_entityAllAttribute),
|
||||
("test_entityBrokenNullableOmittedAttribute", test_entityBrokenNullableOmittedAttribute),
|
||||
("test_EntityNoRelationshipsNoAttributes", test_EntityNoRelationshipsNoAttributes),
|
||||
("test_EntityNoRelationshipsSomeAttributes", test_EntityNoRelationshipsSomeAttributes),
|
||||
("test_entityOneNullAndOneOmittedAttribute", test_entityOneNullAndOneOmittedAttribute),
|
||||
("test_entityOneNullAttribute", test_entityOneNullAttribute),
|
||||
("test_entityOneOmittedAttribute", test_entityOneOmittedAttribute),
|
||||
("test_EntitySomeRelationshipsNoAttributes", test_EntitySomeRelationshipsNoAttributes),
|
||||
("test_EntitySomeRelationshipsSomeAttributes", test_EntitySomeRelationshipsSomeAttributes),
|
||||
("test_relationship_access", test_relationship_access),
|
||||
|
||||
Reference in New Issue
Block a user