mirror of
https://github.com/encounter/JSONAPI.git
synced 2026-03-30 11:18:38 -07:00
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:
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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)!)
|
||||
}
|
||||
Reference in New Issue
Block a user