Made it much more convenient to work with Non-EntityType relationships. Discovered and fixed a bug where nullable relationships were encoded incorrectly.

This commit is contained in:
Mathew Polzin
2018-12-08 19:48:10 -08:00
parent 1061283905
commit c9d388579f
8 changed files with 175 additions and 54 deletions
+7 -6
View File
@@ -83,12 +83,13 @@ extension TransformedAttribute {
// See note in decode above about the weirdness
// going on here.
let anyNil: Any? = nil
if let _ = anyNil as? Transformer.From,
(rawValue as Any?) == nil {
try container.encodeNil()
}
// let anyNil: Any? = nil
// let nilRawValue = anyNil as? Transformer.From
// guard rawValue != nilRawValue else {
// try container.encodeNil()
// return
// }
try container.encode(rawValue)
}
}
+29 -6
View File
@@ -34,13 +34,31 @@ public struct Unidentified: MaybeRawId, CustomStringConvertible {
public var description: String { return "Unidentified" }
}
public protocol MaybeId: Codable {
public protocol OptionalId: Codable {
associatedtype IdentifiableType: JSONAPI.JSONTyped
associatedtype RawType: MaybeRawId
var rawValue: RawType { get }
init(rawValue: RawType)
}
public protocol IdType: MaybeId, CustomStringConvertible, Hashable where RawType: RawIdType {
var rawValue: RawType { get }
public protocol IdType: OptionalId, CustomStringConvertible, Hashable where RawType: RawIdType {}
extension Optional: MaybeRawId where Wrapped: Codable & Equatable {}
extension Optional: OptionalId where Wrapped: IdType {
public typealias IdentifiableType = Wrapped.IdentifiableType
public typealias RawType = Wrapped.RawType?
public var rawValue: Wrapped.RawType? {
guard case .some(let value) = self else {
return nil
}
return value.rawValue
}
public init(rawValue: Wrapped.RawType?) {
self = rawValue.map { Wrapped(rawValue: $0) }
}
}
public extension IdType {
@@ -53,7 +71,7 @@ public protocol CreatableIdType: IdType {
/// An Entity ID. These IDs can be encoded to or decoded from
/// JSON API IDs.
public struct Id<RawType: MaybeRawId, IdentifiableType: JSONAPI.JSONTyped>: Codable, Equatable, MaybeId {
public struct Id<RawType: MaybeRawId, IdentifiableType: JSONAPI.JSONTyped>: Equatable, OptionalId {
public let rawValue: RawType
@@ -63,7 +81,8 @@ public struct Id<RawType: MaybeRawId, IdentifiableType: JSONAPI.JSONTyped>: Coda
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
rawValue = try container.decode(RawType.self)
let rawValue = try container.decode(RawType.self)
self.init(rawValue: rawValue)
}
public func encode(to encoder: Encoder) throws {
@@ -72,7 +91,11 @@ public struct Id<RawType: MaybeRawId, IdentifiableType: JSONAPI.JSONTyped>: Coda
}
}
extension Id: Hashable, CustomStringConvertible, IdType where RawType: RawIdType {}
extension Id: Hashable, CustomStringConvertible, IdType where RawType: RawIdType {
public static func id(from rawValue: RawType) -> Id<RawType, IdentifiableType> {
return Id(rawValue: rawValue)
}
}
extension Id: CreatableIdType where RawType: CreatableRawIdType {
public init() {
+17 -8
View File
@@ -5,7 +5,7 @@
// Created by Mathew Polzin on 8/31/18.
//
public protocol RelationshipType: Codable {
public protocol RelationshipType {
associatedtype LinksType
associatedtype MetaType
@@ -117,7 +117,7 @@ extension ToManyRelationship where MetaType == NoMetadata, LinksType == NoLinks
}
public protocol Identifiable: JSONTyped {
associatedtype Identifier: Equatable, Codable
associatedtype Identifier: Equatable
}
/// The Relatable protocol describes anything that
@@ -148,7 +148,7 @@ private enum ResourceIdentifierCodingKeys: String, CodingKey {
case entityType = "type"
}
extension ToOneRelationship {
extension ToOneRelationship: Codable where Identifiable.Identifier: OptionalId {
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: ResourceLinkageCodingKeys.self)
@@ -184,7 +184,7 @@ extension ToOneRelationship {
throw JSONAPIEncodingError.typeMismatch(expected: Identifiable.type, found: type)
}
id = try identifier.decode(Identifiable.Identifier.self, forKey: .id)
id = Identifiable.Identifier(rawValue: try identifier.decode(Identifiable.Identifier.RawType.self, forKey: .id))
}
public func encode(to encoder: Encoder) throws {
@@ -202,14 +202,23 @@ extension ToOneRelationship {
try container.encode(links, forKey: .links)
}
// If id is nil, instead of {id: , type: } we will just
// encode `null`
let anyNil: Any? = nil
let nilId = anyNil as? Identifiable.Identifier
guard id != nilId else {
try container.encodeNil(forKey: .data)
return
}
var identifier = container.nestedContainer(keyedBy: ResourceIdentifierCodingKeys.self, forKey: .data)
try identifier.encode(id, forKey: .id)
try identifier.encode(id.rawValue, forKey: .id)
try identifier.encode(Identifiable.type, forKey: .entityType)
}
}
extension ToManyRelationship {
extension ToManyRelationship: Codable {
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: ResourceLinkageCodingKeys.self)
@@ -237,7 +246,7 @@ extension ToManyRelationship {
throw JSONAPIEncodingError.typeMismatch(expected: Relatable.type, found: type)
}
newIds.append(try identifier.decode(Relatable.Identifier.self, forKey: .id))
newIds.append(Relatable.Identifier(rawValue: try identifier.decode(Relatable.Identifier.RawType.self, forKey: .id)))
}
ids = newIds
}
@@ -258,7 +267,7 @@ extension ToManyRelationship {
for id in ids {
var identifier = identifiers.nestedContainer(keyedBy: ResourceIdentifierCodingKeys.self)
try identifier.encode(id, forKey: .id)
try identifier.encode(id.rawValue, forKey: .id)
try identifier.encode(Relatable.type, forKey: .entityType)
}
}
@@ -226,6 +226,8 @@ extension EntityTests {
XCTAssertEqual(entity[\.here], "Hello")
XCTAssertNil(entity[\.maybeHereMaybeNull])
XCTAssertNoThrow(try TestEntity7.check(entity))
print(encodable: entity)
}
func test_NullOptionalNullableAttribute_encode() {
@@ -9,45 +9,93 @@ import XCTest
import JSONAPI
class NonJSONAPIRelatableTests: XCTestCase {
func test_initialization1() {
let e1 = NonJSONAPIEntity(id: .init(rawValue: "hello"))
let e2 = NonJSONAPIEntity(id: .init(rawValue: "world"))
let entity = TestEntity(relationships: .init(one: .init(id: e1.id), many: .init(ids: [e1.id, e2.id])))
XCTAssertEqual(entity ~> \.one, e1.id)
XCTAssertEqual(entity ~> \.many, [e1.id, e2.id])
XCTAssertNoThrow(try TestEntity.check(entity))
}
func test_initialization2_all_relationships_there() {
let e1 = NonJSONAPIEntity(id: .init(rawValue: "hello"))
let e2 = NonJSONAPIEntity(id: .init(rawValue: "world"))
let entity = TestEntity2(relationships: .init(nullableOne: .init(id: e1.id), nullableMaybeOne: .init(id: e2.id), maybeOne: .init(id: e2.id), maybeMany: .init(ids: [e2.id, e1.id])))
XCTAssertEqual((entity ~> \.nullableOne)?.rawValue, "hello")
XCTAssertEqual((entity ~> \.nullableMaybeOne)?.rawValue, "world")
XCTAssertEqual((entity ~> \.maybeOne)?.rawValue, "world")
XCTAssertEqual((entity ~> \.maybeMany)?.map { $0.rawValue }, ["world", "hello"])
}
func test_initialization2_all_relationships_missing() {
let entity = TestEntity2(relationships: .init(nullableOne: .init(id: nil), nullableMaybeOne: .init(id: nil), maybeOne: nil, maybeMany: nil))
let entity2 = TestEntity2(relationships: .init(nullableOne: .init(id: nil), nullableMaybeOne: nil, maybeOne: nil, maybeMany: nil))
XCTAssertNil((entity ~> \.nullableOne))
XCTAssertNil((entity ~> \.nullableMaybeOne))
XCTAssertNil((entity ~> \.maybeOne))
XCTAssertNil((entity ~> \.maybeMany))
XCTAssertNil((entity2 ~> \.nullableOne))
XCTAssertNil((entity2 ~> \.nullableMaybeOne))
XCTAssertNil((entity2 ~> \.maybeOne))
XCTAssertNil((entity2 ~> \.maybeMany))
}
}
// MARK: - Test Types
extension NonJSONAPIRelatableTests {
// enum TestEntityDescription: EntityDescription {
// static var type: String { return "test" }
//
// typealias Attributes = NoAttributes
//
// struct Relationships: JSONAPI.Relationships {
// let one: ToOneRelationship<NonJSONAPIEntity, NoMetadata, NoLinks>
// let many: ToManyRelationship<NonJSONAPIEntity, NoMetadata, NoLinks>
// }
// }
enum TestEntityDescription: EntityDescription {
static var type: String { return "test" }
// enum NonJSONAPIEntityDescription: EntityDescription {
// static var type: String { return "other" }
//
// typealias Attributes = NoAttributes
// typealias Relationships = NoRelationships
// }
typealias Attributes = NoAttributes
// struct NonJSONAPIEntity: Relatable, OptionalRelatable, JSONTyped {
// static var type: String { return "other" }
//
// typealias Identifier = NonJSONAPIEntity.Id
// typealias WrappedIdentifier = NonJSONAPIEntity.Id
//
// let id: Id
//
// let attributes: NoAttributes
// let relationships: NoRelationships
//
// struct Id: IdType {
// var rawValue: String
//
// typealias IdentifiableType = NonJSONAPIEntity
// typealias RawType = String
// }
// }
struct Relationships: JSONAPI.Relationships {
let one: ToOneRelationship<NonJSONAPIEntity, NoMetadata, NoLinks>
let many: ToManyRelationship<NonJSONAPIEntity, NoMetadata, NoLinks>
}
}
typealias TestEntity = JSONAPI.Entity<TestEntityDescription, NoMetadata, NoLinks, String>
enum TestEntity2Description: EntityDescription {
static var type: String { return "test" }
typealias Attributes = NoAttributes
struct Relationships: JSONAPI.Relationships {
let nullableOne: ToOneRelationship<NonJSONAPIEntity?, NoMetadata, NoLinks>
let nullableMaybeOne: ToOneRelationship<NonJSONAPIEntity?, NoMetadata, NoLinks>?
let maybeOne: ToOneRelationship<NonJSONAPIEntity, NoMetadata, NoLinks>?
let maybeMany: ToManyRelationship<NonJSONAPIEntity, NoMetadata, NoLinks>?
}
}
typealias TestEntity2 = JSONAPI.Entity<TestEntity2Description, NoMetadata, NoLinks, String>
struct NonJSONAPIEntity: Relatable, JSONTyped {
static var type: String { return "other" }
typealias Identifier = NonJSONAPIEntity.Id
let id: Id
struct Id: IdType {
var rawValue: String
typealias IdentifiableType = NonJSONAPIEntity
typealias RawType = String
static func id(from rawValue: String) -> Id {
return Id(rawValue: rawValue)
}
}
}
}
@@ -140,6 +140,25 @@ extension RelationshipTests {
}
}
// MARK: Nullable
extension RelationshipTests {
func test_ToOneNullableIsNullIfNil() {
let relationship = ToOneNullable(entity: nil)
let relationshipData = try! JSONEncoder().encode(relationship)
let relationshipString = String(data: relationshipData, encoding: .utf8)!
XCTAssertEqual(relationshipString, "{\"data\":null}")
}
func test_ToOneNullableIsEqualToNonNullableIfNotNil() {
let entity = TestEntity1()
let relationship1 = ToOneNonNullable(entity: entity)
let relationship2 = ToOneNullable(entity: entity)
XCTAssertEqual(encoded(value: relationship1), encoded(value: relationship2))
}
}
// MARK: Failure tests
extension RelationshipTests {
func test_ToManyTypeMismatch() {
@@ -172,6 +191,9 @@ extension RelationshipTests {
typealias ToManyWithLinks = ToManyRelationship<TestEntity1, NoMetadata, TestLinks>
typealias ToManyWithMetaAndLinks = ToManyRelationship<TestEntity1, TestMeta, TestLinks>
typealias ToOneNullable = ToOneRelationship<TestEntity1?, NoMetadata, NoLinks>
typealias ToOneNonNullable = ToOneRelationship<TestEntity1, NoMetadata, NoLinks>
struct TestMeta: JSONAPI.Meta {
let a: String
}
@@ -12,6 +12,10 @@ func decoded<T: Decodable>(type: T.Type, data: Data) -> T {
return try! JSONDecoder().decode(T.self, from: data)
}
func encoded<T: Encodable>(value: T) -> Data {
return try! JSONEncoder().encode(value)
}
/// A helper function that tests that decode() == decode().encode().decode().
/// If decoding is well tested and the above is true then encoding is well
/// tested.
@@ -0,0 +1,12 @@
//
// PrintEncoded.swift
// JSONAPITests
//
// Created by Mathew Polzin on 12/8/18.
//
import Foundation
func print<T: Encodable>(encodable: T) {
print(String(data: try! JSONEncoder().encode(encodable), encoding: .utf8)!)
}