From 5aef44c3b3eef46c35bc0a2ec9681b8ff451707f Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Sun, 4 Aug 2019 18:44:28 -0700 Subject: [PATCH 01/26] Add Sparse Fieldset support for Attributes --- Sources/JSONAPI/Resource/ResourceObject.swift | 11 +- .../SparseFields/SparseFieldEncodable.swift | 31 +++ .../SparseFields/SparseFieldEncoder.swift | 199 ++++++++++++++++++ .../SparseFieldEncoderTests.swift | 145 +++++++++++++ 4 files changed, 384 insertions(+), 2 deletions(-) create mode 100644 Sources/JSONAPI/SparseFields/SparseFieldEncodable.swift create mode 100644 Sources/JSONAPI/SparseFields/SparseFieldEncoder.swift create mode 100644 Tests/JSONAPITests/SparseFields/SparseFieldEncoderTests.swift diff --git a/Sources/JSONAPI/Resource/ResourceObject.swift b/Sources/JSONAPI/Resource/ResourceObject.swift index 83cef3e..d1f1500 100644 --- a/Sources/JSONAPI/Resource/ResourceObject.swift +++ b/Sources/JSONAPI/Resource/ResourceObject.swift @@ -15,6 +15,12 @@ public protocol Relationships: Codable & Equatable {} /// properties of any types that are JSON encodable. public protocol Attributes: Codable & Equatable {} +/// Attributes containing publicly accessible and `Equatable` +/// CodingKeys are required to support Sparse Fieldsets. +public protocol SparsableAttributes: Attributes { + associatedtype CodingKeys: CodingKey & Equatable +} + /// Can be used as `Relationships` Type for Entities that do not /// have any Relationships. public struct NoRelationships: Relationships { @@ -48,7 +54,7 @@ public protocol ResourceObjectProxyDescription: JSONTyped { associatedtype Relationships: Equatable } -/// An `ResourceObjectDescription` describes a JSON API +/// A `ResourceObjectDescription` describes a JSON API /// Resource Object. The Resource Object /// itself is encoded and decoded as an /// `ResourceObject`, which gets specialized on an @@ -566,7 +572,8 @@ public extension ResourceObject { } if Description.Attributes.self != NoAttributes.self { - try container.encode(attributes, forKey: .attributes) + let nestedEncoder = container.superEncoder(forKey: .attributes) + try attributes.encode(to: nestedEncoder) } if Description.Relationships.self != NoRelationships.self { diff --git a/Sources/JSONAPI/SparseFields/SparseFieldEncodable.swift b/Sources/JSONAPI/SparseFields/SparseFieldEncodable.swift new file mode 100644 index 0000000..4fdec48 --- /dev/null +++ b/Sources/JSONAPI/SparseFields/SparseFieldEncodable.swift @@ -0,0 +1,31 @@ +// +// SparseField.swift +// +// +// Created by Mathew Polzin on 8/4/19. +// + +public struct SparseField< + Description: JSONAPI.ResourceObjectDescription, + MetaType: JSONAPI.Meta, + LinksType: JSONAPI.Links, + EntityRawIdType: JSONAPI.MaybeRawId +>: Encodable where Description.Attributes: SparsableAttributes { + + public typealias Resource = JSONAPI.ResourceObject + + public let resourceObject: Resource + public let fields: [Description.Attributes.CodingKeys] + + public init(_ resourceObject: Resource, fields: [Description.Attributes.CodingKeys]) { + self.resourceObject = resourceObject + self.fields = fields + } + + public func encode(to encoder: Encoder) throws { + let sparseEncoder = SparseFieldEncoder(wrapping: encoder, + encoding: fields) + + try resourceObject.encode(to: sparseEncoder) + } +} diff --git a/Sources/JSONAPI/SparseFields/SparseFieldEncoder.swift b/Sources/JSONAPI/SparseFields/SparseFieldEncoder.swift new file mode 100644 index 0000000..cbdcb33 --- /dev/null +++ b/Sources/JSONAPI/SparseFields/SparseFieldEncoder.swift @@ -0,0 +1,199 @@ +// +// SparseEncoder.swift +// +// +// Created by Mathew Polzin on 8/4/19. +// + +public class SparseFieldEncoder: Encoder { + private let wrappedEncoder: Encoder + private let allowedKeys: [SparseKey] + + public var codingPath: [CodingKey] { + return wrappedEncoder.codingPath + } + + public var userInfo: [CodingUserInfoKey : Any] { + return wrappedEncoder.userInfo + } + + public init(wrapping encoder: Encoder, encoding allowedKeys: [SparseKey]) { + wrappedEncoder = encoder + self.allowedKeys = allowedKeys + } + + public func container(keyedBy type: Key.Type) -> KeyedEncodingContainer where Key : CodingKey { + let container = SparseFieldKeyedEncodingContainer(wrapping: wrappedEncoder.container(keyedBy: type), + encoding: allowedKeys) + return KeyedEncodingContainer(container) + } + + public func container(keyedBy type: SparseKey.Type) -> KeyedEncodingContainer { + let container = SparseFieldKeyedEncodingContainer(wrapping: wrappedEncoder.container(keyedBy: type), + encoding: allowedKeys) + return KeyedEncodingContainer(container) + } + + public func unkeyedContainer() -> UnkeyedEncodingContainer { + return wrappedEncoder.unkeyedContainer() + } + + public func singleValueContainer() -> SingleValueEncodingContainer { + return wrappedEncoder.singleValueContainer() + } +} + +public struct SparseFieldKeyedEncodingContainer: KeyedEncodingContainerProtocol where SparseKey: CodingKey, SparseKey: Equatable, Key: CodingKey { + private var wrappedContainer: KeyedEncodingContainer + private let allowedKeys: [SparseKey] + + public var codingPath: [CodingKey] { + return wrappedContainer.codingPath + } + + public init(wrapping container: KeyedEncodingContainer, encoding allowedKeys: [SparseKey]) { + wrappedContainer = container + self.allowedKeys = allowedKeys + } + + private func shouldAllow(key: Key) -> Bool { + if let key = key as? SparseKey { + return allowedKeys.contains(key) + } + return true + } + + public mutating func encodeNil(forKey key: Key) throws { + guard shouldAllow(key: key) else { return } + + try wrappedContainer.encodeNil(forKey: key) + } + + public mutating func encode(_ value: Bool, forKey key: Key) throws { + guard shouldAllow(key: key) else { return } + + try wrappedContainer.encode(value, forKey: key) + } + + public mutating func encode(_ value: String, forKey key: Key) throws { + guard shouldAllow(key: key) else { return } + + try wrappedContainer.encode(value, forKey: key) + } + + public mutating func encode(_ value: Double, forKey key: Key) throws { + guard shouldAllow(key: key) else { return } + + try wrappedContainer.encode(value, forKey: key) + } + + public mutating func encode(_ value: Float, forKey key: Key) throws { + guard shouldAllow(key: key) else { return } + + try wrappedContainer.encode(value, forKey: key) + } + + public mutating func encode(_ value: Int, forKey key: Key) throws { + guard shouldAllow(key: key) else { return } + + try wrappedContainer.encode(value, forKey: key) + } + + public mutating func encode(_ value: Int8, forKey key: Key) throws { + guard shouldAllow(key: key) else { return } + + try wrappedContainer.encode(value, forKey: key) + } + + public mutating func encode(_ value: Int16, forKey key: Key) throws { + guard shouldAllow(key: key) else { return } + + try wrappedContainer.encode(value, forKey: key) + } + + public mutating func encode(_ value: Int32, forKey key: Key) throws { + guard shouldAllow(key: key) else { return } + + try wrappedContainer.encode(value, forKey: key) + } + + public mutating func encode(_ value: Int64, forKey key: Key) throws { + guard shouldAllow(key: key) else { return } + + try wrappedContainer.encode(value, forKey: key) + } + + public mutating func encode(_ value: UInt, forKey key: Key) throws { + guard shouldAllow(key: key) else { return } + + try wrappedContainer.encode(value, forKey: key) + } + + public mutating func encode(_ value: UInt8, forKey key: Key) throws { + guard shouldAllow(key: key) else { return } + + try wrappedContainer.encode(value, forKey: key) + } + + public mutating func encode(_ value: UInt16, forKey key: Key) throws { + guard shouldAllow(key: key) else { return } + + try wrappedContainer.encode(value, forKey: key) + } + + public mutating func encode(_ value: UInt32, forKey key: Key) throws { + guard shouldAllow(key: key) else { return } + + try wrappedContainer.encode(value, forKey: key) + } + + public mutating func encode(_ value: UInt64, forKey key: Key) throws { + guard shouldAllow(key: key) else { return } + + try wrappedContainer.encode(value, forKey: key) + } + + public mutating func encode(_ value: T, forKey key: Key) throws where T : Encodable { + guard shouldAllow(key: key) else { return } + + try wrappedContainer.encode(value, forKey: key) + } + + public mutating func nestedContainer(keyedBy keyType: NestedKey.Type, + forKey key: Key) -> KeyedEncodingContainer where NestedKey : CodingKey { + guard shouldAllow(key: key) else { + return KeyedEncodingContainer( + SparseFieldKeyedEncodingContainer(wrapping: wrappedContainer.nestedContainer(keyedBy: keyType, + forKey: key), + encoding: []) + ) + } + + return KeyedEncodingContainer( + SparseFieldKeyedEncodingContainer(wrapping: wrappedContainer.nestedContainer(keyedBy: keyType, + forKey: key), + encoding: allowedKeys) + ) + } + + public mutating func nestedUnkeyedContainer(forKey key: Key) -> UnkeyedEncodingContainer { + guard shouldAllow(key: key) else { + // TODO: Seems like this might not work as expected... maybe need an empty unkeyed container + return wrappedContainer.nestedUnkeyedContainer(forKey: key) + } + + return wrappedContainer.nestedUnkeyedContainer(forKey: key) + } + + public mutating func superEncoder() -> Encoder { + return wrappedContainer.superEncoder() + } + + public mutating func superEncoder(forKey key: Key) -> Encoder { + guard shouldAllow(key: key) else { + return SparseFieldEncoder(wrapping: wrappedContainer.superEncoder(forKey: key), encoding: [SparseKey]()) + } + + return SparseFieldEncoder(wrapping: wrappedContainer.superEncoder(forKey: key), encoding: allowedKeys) + } +} diff --git a/Tests/JSONAPITests/SparseFields/SparseFieldEncoderTests.swift b/Tests/JSONAPITests/SparseFields/SparseFieldEncoderTests.swift new file mode 100644 index 0000000..a7f9ccd --- /dev/null +++ b/Tests/JSONAPITests/SparseFields/SparseFieldEncoderTests.swift @@ -0,0 +1,145 @@ +// +// File.swift +// +// +// Created by Mathew Polzin on 8/4/19. +// + +import XCTest +import Foundation +import JSONAPI +import JSONAPITesting + +class SparseFieldEncoderTests: XCTestCase { + func test_FullEncode() { + let jsonEncoder = JSONEncoder() + let sparseWithEverything = SparseField(testEverythingObject, fields: EverythingTest.Attributes.CodingKeys.allCases) + + let encoded = try! jsonEncoder.encode(sparseWithEverything) + + let deserialized = try! JSONSerialization.jsonObject(with: encoded, + options: []) + + let outerDict = deserialized as? [String: Any] + let id = outerDict?["id"] as? String + let type = outerDict?["type"] as? String + let attributesDict = outerDict?["attributes"] as? [String: Any] + let relationships = outerDict?["relationships"] + + XCTAssertEqual(id, testEverythingObject.id.rawValue) + XCTAssertEqual(type, EverythingTest.jsonType) + XCTAssertNil(relationships) + + XCTAssertEqual(attributesDict?.count, 9) // note not 10 because one value is omitted + XCTAssertEqual(attributesDict?["bool"] as? Bool, + testEverythingObject[\.bool]) + XCTAssertEqual(attributesDict?["int"] as? Int, + testEverythingObject[\.int]) + XCTAssertEqual(attributesDict?["double"] as? Double, + testEverythingObject[\.double]) + XCTAssertEqual(attributesDict?["string"] as? String, + testEverythingObject[\.string]) + XCTAssertEqual((attributesDict?["nestedStruct"] as? [String: String])?["hello"], + testEverythingObject[\.nestedStruct].hello) + XCTAssertEqual(attributesDict?["nestedEnum"] as? String, + testEverythingObject[\.nestedEnum].rawValue) + XCTAssertEqual(attributesDict?["array"] as? [Bool], + testEverythingObject[\.array]) + XCTAssertNil(attributesDict?["optional"]) + XCTAssertNotNil(attributesDict?["nullable"] as? NSNull) + XCTAssertNotNil(attributesDict?["optionalNullable"] as? NSNull) + } + + func test_PartialEncode() { + let jsonEncoder = JSONEncoder() + let sparseWithEverything = SparseField(testEverythingObject, fields: [.string, .bool, .array]) + + let encoded = try! jsonEncoder.encode(sparseWithEverything) + + let deserialized = try! JSONSerialization.jsonObject(with: encoded, + options: []) + + let outerDict = deserialized as? [String: Any] + let id = outerDict?["id"] as? String + let type = outerDict?["type"] as? String + let attributesDict = outerDict?["attributes"] as? [String: Any] + let relationships = outerDict?["relationships"] + + XCTAssertEqual(id, testEverythingObject.id.rawValue) + XCTAssertEqual(type, EverythingTest.jsonType) + XCTAssertNil(relationships) + + XCTAssertEqual(attributesDict?.count, 3) + XCTAssertEqual(attributesDict?["bool"] as? Bool, + testEverythingObject[\.bool]) + XCTAssertNil(attributesDict?["int"]) + XCTAssertNil(attributesDict?["double"]) + XCTAssertEqual(attributesDict?["string"] as? String, + testEverythingObject[\.string]) + XCTAssertNil(attributesDict?["nestedStruct"]) + XCTAssertNil(attributesDict?["nestedEnum"]) + XCTAssertEqual(attributesDict?["array"] as? [Bool], + testEverythingObject[\.array]) + XCTAssertNil(attributesDict?["optional"]) + XCTAssertNil(attributesDict?["nullable"]) + XCTAssertNil(attributesDict?["optionalNullable"]) + } +} + +struct EverythingTestDescription: JSONAPI.ResourceObjectDescription { + static let jsonType: String = "everything" + + struct Attributes: JSONAPI.SparsableAttributes { + let bool: Attribute + let int: Attribute + let double: Attribute + let string: Attribute + let nestedStruct: Attribute + let nestedEnum: Attribute + + let array: Attribute<[Bool]> + let optional: Attribute? + let nullable: Attribute + let optionalNullable: Attribute? + + struct NestedStruct: Codable, Equatable { + let hello: String + } + + enum NestedEnum: String, Codable, Equatable { + case hello + case world + } + + enum CodingKeys: String, CodingKey, Equatable, CaseIterable { + case bool + case int + case double + case string + case nestedStruct + case nestedEnum + case array + case optional + case nullable + case optionalNullable + } + } + + typealias Relationships = NoRelationships +} + +typealias EverythingTest = JSONAPI.ResourceObject + +let testEverythingObject = EverythingTest(attributes: .init(bool: true, + int: 10, + double: 10.5, + string: "hello world", + nestedStruct: .init(value: .init(hello: "world")), + nestedEnum: .init(value: .hello), + array: [true, false, false], + optional: nil, + nullable: .init(value: nil), + optionalNullable: .init(value: nil)), + relationships: .none, + meta: .none, + links: .none) From 2f3a61928430ba3f448145622ce43aa9ead49161 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Sun, 4 Aug 2019 18:47:26 -0700 Subject: [PATCH 02/26] Spacing difference --- Sources/JSONAPI/Resource/ResourceObject.swift | 92 +++++++++---------- 1 file changed, 46 insertions(+), 46 deletions(-) diff --git a/Sources/JSONAPI/Resource/ResourceObject.swift b/Sources/JSONAPI/Resource/ResourceObject.swift index d1f1500..af964a7 100644 --- a/Sources/JSONAPI/Resource/ResourceObject.swift +++ b/Sources/JSONAPI/Resource/ResourceObject.swift @@ -553,63 +553,63 @@ infix operator ~> // MARK: - Codable private enum ResourceObjectCodingKeys: String, CodingKey { - case type = "type" - case id = "id" - case attributes = "attributes" - case relationships = "relationships" - case meta = "meta" - case links = "links" + case type = "type" + case id = "id" + case attributes = "attributes" + case relationships = "relationships" + case meta = "meta" + case links = "links" } public extension ResourceObject { - func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: ResourceObjectCodingKeys.self) - - try container.encode(ResourceObject.jsonType, forKey: .type) - - if EntityRawIdType.self != Unidentified.self { - try container.encode(id, forKey: .id) - } - - if Description.Attributes.self != NoAttributes.self { + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: ResourceObjectCodingKeys.self) + + try container.encode(ResourceObject.jsonType, forKey: .type) + + if EntityRawIdType.self != Unidentified.self { + try container.encode(id, forKey: .id) + } + + if Description.Attributes.self != NoAttributes.self { let nestedEncoder = container.superEncoder(forKey: .attributes) try attributes.encode(to: nestedEncoder) - } - - if Description.Relationships.self != NoRelationships.self { - try container.encode(relationships, forKey: .relationships) - } + } - if MetaType.self != NoMetadata.self { - try container.encode(meta, forKey: .meta) - } + if Description.Relationships.self != NoRelationships.self { + try container.encode(relationships, forKey: .relationships) + } - if LinksType.self != NoLinks.self { - try container.encode(links, forKey: .links) - } - } + if MetaType.self != NoMetadata.self { + try container.encode(meta, forKey: .meta) + } - init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: ResourceObjectCodingKeys.self) - - let type = try container.decode(String.self, forKey: .type) - - guard ResourceObject.jsonType == type else { - throw JSONAPIEncodingError.typeMismatch(expected: Description.jsonType, found: type) - } + if LinksType.self != NoLinks.self { + try container.encode(links, forKey: .links) + } + } - let maybeUnidentified = Unidentified() as? EntityRawIdType - id = try maybeUnidentified.map { ResourceObject.Id(rawValue: $0) } ?? container.decode(ResourceObject.Id.self, forKey: .id) + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: ResourceObjectCodingKeys.self) - attributes = try (NoAttributes() as? Description.Attributes) ?? - container.decode(Description.Attributes.self, forKey: .attributes) + let type = try container.decode(String.self, forKey: .type) - relationships = try (NoRelationships() as? Description.Relationships) - ?? container.decodeIfPresent(Description.Relationships.self, forKey: .relationships) - ?? Description.Relationships(from: EmptyObjectDecoder()) + guard ResourceObject.jsonType == type else { + throw JSONAPIEncodingError.typeMismatch(expected: Description.jsonType, found: type) + } - meta = try (NoMetadata() as? MetaType) ?? container.decode(MetaType.self, forKey: .meta) + let maybeUnidentified = Unidentified() as? EntityRawIdType + id = try maybeUnidentified.map { ResourceObject.Id(rawValue: $0) } ?? container.decode(ResourceObject.Id.self, forKey: .id) - links = try (NoLinks() as? LinksType) ?? container.decode(LinksType.self, forKey: .links) - } + attributes = try (NoAttributes() as? Description.Attributes) ?? + container.decode(Description.Attributes.self, forKey: .attributes) + + relationships = try (NoRelationships() as? Description.Relationships) + ?? container.decodeIfPresent(Description.Relationships.self, forKey: .relationships) + ?? Description.Relationships(from: EmptyObjectDecoder()) + + meta = try (NoMetadata() as? MetaType) ?? container.decode(MetaType.self, forKey: .meta) + + links = try (NoLinks() as? LinksType) ?? container.decode(LinksType.self, forKey: .links) + } } From e0b53236bbbeffe57fc8f43d5805d853f6f6df11 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Sun, 4 Aug 2019 23:03:56 -0700 Subject: [PATCH 03/26] Add sparse fields method to ResourceObject and test it. --- .../SparseFields/SparseFieldEncodable.swift | 9 ++++ ....swift => SparseFieldEncodableTests.swift} | 41 +++++++++++++++++-- 2 files changed, 47 insertions(+), 3 deletions(-) rename Tests/JSONAPITests/SparseFields/{SparseFieldEncoderTests.swift => SparseFieldEncodableTests.swift} (77%) diff --git a/Sources/JSONAPI/SparseFields/SparseFieldEncodable.swift b/Sources/JSONAPI/SparseFields/SparseFieldEncodable.swift index 4fdec48..bac54d2 100644 --- a/Sources/JSONAPI/SparseFields/SparseFieldEncodable.swift +++ b/Sources/JSONAPI/SparseFields/SparseFieldEncodable.swift @@ -29,3 +29,12 @@ public struct SparseField< try resourceObject.encode(to: sparseEncoder) } } + +public extension ResourceObject where Description.Attributes: SparsableAttributes { + + /// Get a Sparse Fieldset of this `ResourceObject` that can be encoded + /// as a `PrimaryResource`. + func sparse(with fields: [Description.Attributes.CodingKeys]) -> SparseField { + return SparseField(self, fields: fields) + } +} diff --git a/Tests/JSONAPITests/SparseFields/SparseFieldEncoderTests.swift b/Tests/JSONAPITests/SparseFields/SparseFieldEncodableTests.swift similarity index 77% rename from Tests/JSONAPITests/SparseFields/SparseFieldEncoderTests.swift rename to Tests/JSONAPITests/SparseFields/SparseFieldEncodableTests.swift index a7f9ccd..5a17075 100644 --- a/Tests/JSONAPITests/SparseFields/SparseFieldEncoderTests.swift +++ b/Tests/JSONAPITests/SparseFields/SparseFieldEncodableTests.swift @@ -1,5 +1,5 @@ // -// File.swift +// SparseFieldEncodableTests.swift // // // Created by Mathew Polzin on 8/4/19. @@ -52,9 +52,44 @@ class SparseFieldEncoderTests: XCTestCase { func test_PartialEncode() { let jsonEncoder = JSONEncoder() - let sparseWithEverything = SparseField(testEverythingObject, fields: [.string, .bool, .array]) + let sparseObject = SparseField(testEverythingObject, fields: [.string, .bool, .array]) - let encoded = try! jsonEncoder.encode(sparseWithEverything) + let encoded = try! jsonEncoder.encode(sparseObject) + + let deserialized = try! JSONSerialization.jsonObject(with: encoded, + options: []) + + let outerDict = deserialized as? [String: Any] + let id = outerDict?["id"] as? String + let type = outerDict?["type"] as? String + let attributesDict = outerDict?["attributes"] as? [String: Any] + let relationships = outerDict?["relationships"] + + XCTAssertEqual(id, testEverythingObject.id.rawValue) + XCTAssertEqual(type, EverythingTest.jsonType) + XCTAssertNil(relationships) + + XCTAssertEqual(attributesDict?.count, 3) + XCTAssertEqual(attributesDict?["bool"] as? Bool, + testEverythingObject[\.bool]) + XCTAssertNil(attributesDict?["int"]) + XCTAssertNil(attributesDict?["double"]) + XCTAssertEqual(attributesDict?["string"] as? String, + testEverythingObject[\.string]) + XCTAssertNil(attributesDict?["nestedStruct"]) + XCTAssertNil(attributesDict?["nestedEnum"]) + XCTAssertEqual(attributesDict?["array"] as? [Bool], + testEverythingObject[\.array]) + XCTAssertNil(attributesDict?["optional"]) + XCTAssertNil(attributesDict?["nullable"]) + XCTAssertNil(attributesDict?["optionalNullable"]) + } + + func test_sparseFieldsMethod() { + let jsonEncoder = JSONEncoder() + let sparseObject = testEverythingObject.sparse(with: [.string, .bool, .array]) + + let encoded = try! jsonEncoder.encode(sparseObject) let deserialized = try! JSONSerialization.jsonObject(with: encoded, options: []) From 61e00c2de56d16e6f78701b8894f472d9535d221 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Mon, 5 Aug 2019 08:01:42 -0700 Subject: [PATCH 04/26] small rename --- ...SparseFieldEncodable.swift => SparseFieldset.swift} | 10 +++++----- ...dEncodableTests.swift => SparseFieldsetTests.swift} | 8 ++++---- 2 files changed, 9 insertions(+), 9 deletions(-) rename Sources/JSONAPI/SparseFields/{SparseFieldEncodable.swift => SparseFieldset.swift} (83%) rename Tests/JSONAPITests/SparseFields/{SparseFieldEncodableTests.swift => SparseFieldsetTests.swift} (95%) diff --git a/Sources/JSONAPI/SparseFields/SparseFieldEncodable.swift b/Sources/JSONAPI/SparseFields/SparseFieldset.swift similarity index 83% rename from Sources/JSONAPI/SparseFields/SparseFieldEncodable.swift rename to Sources/JSONAPI/SparseFields/SparseFieldset.swift index bac54d2..8e0979b 100644 --- a/Sources/JSONAPI/SparseFields/SparseFieldEncodable.swift +++ b/Sources/JSONAPI/SparseFields/SparseFieldset.swift @@ -1,11 +1,11 @@ // -// SparseField.swift +// SparseFieldset.swift // // // Created by Mathew Polzin on 8/4/19. // -public struct SparseField< +public struct SparseFieldset< Description: JSONAPI.ResourceObjectDescription, MetaType: JSONAPI.Meta, LinksType: JSONAPI.Links, @@ -33,8 +33,8 @@ public struct SparseField< public extension ResourceObject where Description.Attributes: SparsableAttributes { /// Get a Sparse Fieldset of this `ResourceObject` that can be encoded - /// as a `PrimaryResource`. - func sparse(with fields: [Description.Attributes.CodingKeys]) -> SparseField { - return SparseField(self, fields: fields) + /// as a `SparsePrimaryResource`. + func sparse(with fields: [Description.Attributes.CodingKeys]) -> SparseFieldset { + return SparseFieldset(self, fields: fields) } } diff --git a/Tests/JSONAPITests/SparseFields/SparseFieldEncodableTests.swift b/Tests/JSONAPITests/SparseFields/SparseFieldsetTests.swift similarity index 95% rename from Tests/JSONAPITests/SparseFields/SparseFieldEncodableTests.swift rename to Tests/JSONAPITests/SparseFields/SparseFieldsetTests.swift index 5a17075..7845119 100644 --- a/Tests/JSONAPITests/SparseFields/SparseFieldEncodableTests.swift +++ b/Tests/JSONAPITests/SparseFields/SparseFieldsetTests.swift @@ -1,5 +1,5 @@ // -// SparseFieldEncodableTests.swift +// SparseFieldsetTests.swift // // // Created by Mathew Polzin on 8/4/19. @@ -13,7 +13,7 @@ import JSONAPITesting class SparseFieldEncoderTests: XCTestCase { func test_FullEncode() { let jsonEncoder = JSONEncoder() - let sparseWithEverything = SparseField(testEverythingObject, fields: EverythingTest.Attributes.CodingKeys.allCases) + let sparseWithEverything = SparseFieldset(testEverythingObject, fields: EverythingTest.Attributes.CodingKeys.allCases) let encoded = try! jsonEncoder.encode(sparseWithEverything) @@ -30,7 +30,7 @@ class SparseFieldEncoderTests: XCTestCase { XCTAssertEqual(type, EverythingTest.jsonType) XCTAssertNil(relationships) - XCTAssertEqual(attributesDict?.count, 9) // note not 10 because one value is omitted + XCTAssertEqual(attributesDict?.count, 9) // note not 10 because one value is omitted intentionally at initialization XCTAssertEqual(attributesDict?["bool"] as? Bool, testEverythingObject[\.bool]) XCTAssertEqual(attributesDict?["int"] as? Int, @@ -52,7 +52,7 @@ class SparseFieldEncoderTests: XCTestCase { func test_PartialEncode() { let jsonEncoder = JSONEncoder() - let sparseObject = SparseField(testEverythingObject, fields: [.string, .bool, .array]) + let sparseObject = SparseFieldset(testEverythingObject, fields: [.string, .bool, .array]) let encoded = try! jsonEncoder.encode(sparseObject) From b98fb08353862277f416925392ffee1924b3fd9d Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Mon, 5 Aug 2019 08:13:18 -0700 Subject: [PATCH 05/26] Adding a bit of code doc --- Sources/JSONAPI/Document/ResourceBody.swift | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Sources/JSONAPI/Document/ResourceBody.swift b/Sources/JSONAPI/Document/ResourceBody.swift index b8f170c..bcc4d8b 100644 --- a/Sources/JSONAPI/Document/ResourceBody.swift +++ b/Sources/JSONAPI/Document/ResourceBody.swift @@ -5,6 +5,10 @@ // Created by Mathew Polzin on 11/10/18. // +/// A `MaybePrimaryResource` is just an optional `PrimaryResource`. +/// This protocol allows for `SingleResourceBody` to contain a `null` +/// data object where `ManyResourceBody` cannot (because an empty +/// array should be used for no results). public protocol MaybePrimaryResource: Equatable, Codable {} /// A PrimaryResource is a type that can be used in the body of a JSON API @@ -19,6 +23,8 @@ extension Optional: MaybePrimaryResource where Wrapped: PrimaryResource {} public protocol ResourceBody: Codable, Equatable { } +/// A `ResourceBody` that has the ability to take on more primary +/// resources by appending another similarly typed `ResourceBody`. public protocol AppendableResourceBody: ResourceBody { func appending(_ other: Self) -> Self } From 265cffe8f041fc13e58af6ebf99e88cd4cec9651 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Mon, 5 Aug 2019 08:17:38 -0700 Subject: [PATCH 06/26] Rename MaybePrimaryResource to OptionalPrimaryResource because I use 'Maybe' elsewhere to indicate a type-level distinction whereas this is a value-level distinction that really is just 'Optional' at play. --- Sources/JSONAPI/Document/ResourceBody.swift | 9 ++++----- .../Resource/Poly+PrimaryResource.swift | 18 +++++++++--------- 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/Sources/JSONAPI/Document/ResourceBody.swift b/Sources/JSONAPI/Document/ResourceBody.swift index bcc4d8b..0752331 100644 --- a/Sources/JSONAPI/Document/ResourceBody.swift +++ b/Sources/JSONAPI/Document/ResourceBody.swift @@ -5,17 +5,16 @@ // Created by Mathew Polzin on 11/10/18. // -/// A `MaybePrimaryResource` is just an optional `PrimaryResource`. /// This protocol allows for `SingleResourceBody` to contain a `null` /// data object where `ManyResourceBody` cannot (because an empty /// array should be used for no results). -public protocol MaybePrimaryResource: Equatable, Codable {} +public protocol OptionalPrimaryResource: Equatable, Codable {} /// A PrimaryResource is a type that can be used in the body of a JSON API /// document as the primary resource. -public protocol PrimaryResource: MaybePrimaryResource {} +public protocol PrimaryResource: OptionalPrimaryResource {} -extension Optional: MaybePrimaryResource where Wrapped: PrimaryResource {} +extension Optional: OptionalPrimaryResource where Wrapped: PrimaryResource {} /// A ResourceBody is a representation of the body of the JSON API Document. /// It can either be one resource (which can be specified as optional or not) @@ -33,7 +32,7 @@ public func +(_ left: R, right: R) -> R { return left.appending(right) } -public struct SingleResourceBody: ResourceBody { +public struct SingleResourceBody: ResourceBody { public let value: Entity public init(resourceObject: Entity) { diff --git a/Sources/JSONAPI/Resource/Poly+PrimaryResource.swift b/Sources/JSONAPI/Resource/Poly+PrimaryResource.swift index 66aaf63..e2ed829 100644 --- a/Sources/JSONAPI/Resource/Poly+PrimaryResource.swift +++ b/Sources/JSONAPI/Resource/Poly+PrimaryResource.swift @@ -30,28 +30,28 @@ extension Poly0: PrimaryResource { } // MARK: - 1 type -extension Poly1: PrimaryResource, MaybePrimaryResource where A: PolyWrapped {} +extension Poly1: PrimaryResource, OptionalPrimaryResource where A: PolyWrapped {} // MARK: - 2 types -extension Poly2: PrimaryResource, MaybePrimaryResource where A: PolyWrapped, B: PolyWrapped {} +extension Poly2: PrimaryResource, OptionalPrimaryResource where A: PolyWrapped, B: PolyWrapped {} // MARK: - 3 types -extension Poly3: PrimaryResource, MaybePrimaryResource where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped {} +extension Poly3: PrimaryResource, OptionalPrimaryResource where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped {} // MARK: - 4 types -extension Poly4: PrimaryResource, MaybePrimaryResource where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped {} +extension Poly4: PrimaryResource, OptionalPrimaryResource where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped {} // MARK: - 5 types -extension Poly5: PrimaryResource, MaybePrimaryResource where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped, E: PolyWrapped {} +extension Poly5: PrimaryResource, OptionalPrimaryResource where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped, E: PolyWrapped {} // MARK: - 6 types -extension Poly6: PrimaryResource, MaybePrimaryResource where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped, E: PolyWrapped, F: PolyWrapped {} +extension Poly6: PrimaryResource, OptionalPrimaryResource where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped, E: PolyWrapped, F: PolyWrapped {} // MARK: - 7 types -extension Poly7: PrimaryResource, MaybePrimaryResource where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped, E: PolyWrapped, F: PolyWrapped, G: PolyWrapped {} +extension Poly7: PrimaryResource, OptionalPrimaryResource where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped, E: PolyWrapped, F: PolyWrapped, G: PolyWrapped {} // MARK: - 8 types -extension Poly8: PrimaryResource, MaybePrimaryResource where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped, E: PolyWrapped, F: PolyWrapped, G: PolyWrapped, H: PolyWrapped {} +extension Poly8: PrimaryResource, OptionalPrimaryResource where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped, E: PolyWrapped, F: PolyWrapped, G: PolyWrapped, H: PolyWrapped {} // MARK: - 9 types -extension Poly9: PrimaryResource, MaybePrimaryResource where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped, E: PolyWrapped, F: PolyWrapped, G: PolyWrapped, H: PolyWrapped, I: PolyWrapped {} +extension Poly9: PrimaryResource, OptionalPrimaryResource where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped, E: PolyWrapped, F: PolyWrapped, G: PolyWrapped, H: PolyWrapped, I: PolyWrapped {} From a596ecaecc5be01fd16186b50232f36749e536b8 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Mon, 5 Aug 2019 09:23:44 -0700 Subject: [PATCH 07/26] A stab at separating out decoding enough to make it possible to use encode-only sparse fieldsets with JSONDocument --- Sources/JSONAPI/Document/Document.swift | 144 +++++++++--------- Sources/JSONAPI/Document/Includes.swift | 40 ++--- Sources/JSONAPI/Document/ResourceBody.swift | 72 +++++---- .../Resource/Poly+PrimaryResource.swift | 23 ++- 4 files changed, 159 insertions(+), 120 deletions(-) diff --git a/Sources/JSONAPI/Document/Document.swift b/Sources/JSONAPI/Document/Document.swift index 1a0fd5f..d0a34d9 100644 --- a/Sources/JSONAPI/Document/Document.swift +++ b/Sources/JSONAPI/Document/Document.swift @@ -7,19 +7,21 @@ import Poly -public protocol JSONAPIDocument: Codable, Equatable { - associatedtype PrimaryResourceBody: JSONAPI.ResourceBody - associatedtype MetaType: JSONAPI.Meta - associatedtype LinksType: JSONAPI.Links - associatedtype IncludeType: JSONAPI.Include - associatedtype APIDescription: APIDescriptionType - associatedtype Error: JSONAPIError +public protocol EncodableJSONAPIDocument: Equatable, Encodable { + associatedtype PrimaryResourceBody: JSONAPI.EncodableResourceBody + associatedtype MetaType: JSONAPI.Meta + associatedtype LinksType: JSONAPI.Links + associatedtype IncludeType: JSONAPI.Include + associatedtype APIDescription: APIDescriptionType + associatedtype Error: JSONAPIError - typealias Body = Document.Body + typealias Body = Document.Body - var body: Body { get } + var body: Body { get } } +public protocol JSONAPIDocument: EncodableJSONAPIDocument, Decodable where PrimaryResourceBody: JSONAPI.ResourceBody, IncludeType: Decodable {} + /// A JSON API Document represents the entire body /// of a JSON API request or the entire body of /// a JSON API response. @@ -27,7 +29,7 @@ public protocol JSONAPIDocument: Codable, Equatable { /// API uses snake case, you will want to use /// a conversion such as the one offerred by the /// Foundation JSONEncoder/Decoder: `KeyDecodingStrategy` -public struct Document: JSONAPIDocument { +public struct Document: EncodableJSONAPIDocument { public typealias Include = IncludeType /// The JSON API Spec calls this the JSON:API Object. It contains version @@ -273,66 +275,6 @@ extension Document { case links case jsonapi } - - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: RootCodingKeys.self) - - if let noData = NoAPIDescription() as? APIDescription { - apiDescription = noData - } else { - apiDescription = try container.decode(APIDescription.self, forKey: .jsonapi) - } - - let errors = try container.decodeIfPresent([Error].self, forKey: .errors) - - let meta: MetaType? - if let noMeta = NoMetadata() as? MetaType { - meta = noMeta - } else { - do { - meta = try container.decode(MetaType.self, forKey: .meta) - } catch { - meta = nil - } - } - - let links: LinksType? - if let noLinks = NoLinks() as? LinksType { - links = noLinks - } else { - do { - links = try container.decode(LinksType.self, forKey: .links) - } catch { - links = nil - } - } - - // If there are errors, there cannot be a body. Return errors and any metadata found. - if let errors = errors { - body = .errors(errors, meta: meta, links: links) - return - } - - let data: PrimaryResourceBody - if let noData = NoResourceBody() as? PrimaryResourceBody { - data = noData - } else { - data = try container.decode(PrimaryResourceBody.self, forKey: .data) - } - - let maybeIncludes = try container.decodeIfPresent(Includes.self, forKey: .included) - - // TODO come back to this and make robust - - guard let metaVal = meta else { - throw JSONAPIEncodingError.missingOrMalformedMetadata - } - guard let linksVal = links else { - throw JSONAPIEncodingError.missingOrMalformedLinks - } - - body = .data(.init(primary: data, includes: maybeIncludes ?? Includes.none, meta: metaVal, links: linksVal)) - } public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: RootCodingKeys.self) @@ -377,6 +319,68 @@ extension Document { } } +extension Document: Decodable, JSONAPIDocument where PrimaryResourceBody: ResourceBody, IncludeType: Decodable { + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: RootCodingKeys.self) + + if let noData = NoAPIDescription() as? APIDescription { + apiDescription = noData + } else { + apiDescription = try container.decode(APIDescription.self, forKey: .jsonapi) + } + + let errors = try container.decodeIfPresent([Error].self, forKey: .errors) + + let meta: MetaType? + if let noMeta = NoMetadata() as? MetaType { + meta = noMeta + } else { + do { + meta = try container.decode(MetaType.self, forKey: .meta) + } catch { + meta = nil + } + } + + let links: LinksType? + if let noLinks = NoLinks() as? LinksType { + links = noLinks + } else { + do { + links = try container.decode(LinksType.self, forKey: .links) + } catch { + links = nil + } + } + + // If there are errors, there cannot be a body. Return errors and any metadata found. + if let errors = errors { + body = .errors(errors, meta: meta, links: links) + return + } + + let data: PrimaryResourceBody + if let noData = NoResourceBody() as? PrimaryResourceBody { + data = noData + } else { + data = try container.decode(PrimaryResourceBody.self, forKey: .data) + } + + let maybeIncludes = try container.decodeIfPresent(Includes.self, forKey: .included) + + // TODO come back to this and make robust + + guard let metaVal = meta else { + throw JSONAPIEncodingError.missingOrMalformedMetadata + } + guard let linksVal = links else { + throw JSONAPIEncodingError.missingOrMalformedLinks + } + + body = .data(.init(primary: data, includes: maybeIncludes ?? Includes.none, meta: metaVal, links: linksVal)) + } +} + // MARK: - CustomStringConvertible extension Document: CustomStringConvertible { diff --git a/Sources/JSONAPI/Document/Includes.swift b/Sources/JSONAPI/Document/Includes.swift index 8682ee9..1608b2d 100644 --- a/Sources/JSONAPI/Document/Includes.swift +++ b/Sources/JSONAPI/Document/Includes.swift @@ -7,9 +7,9 @@ import Poly -public typealias Include = JSONPoly +public typealias Include = EncodableJSONPoly -public struct Includes: Codable, Equatable { +public struct Includes: Encodable, Equatable { public static var none: Includes { return .init(values: []) } let values: [I] @@ -17,23 +17,6 @@ public struct Includes: Codable, Equatable { public init(values: [I]) { self.values = values } - - public init(from decoder: Decoder) throws { - var container = try decoder.unkeyedContainer() - - // If not parsing includes, no need to loop over them. - guard I.self != NoIncludes.self else { - values = [] - return - } - - var valueAggregator = [I]() - while !container.isAtEnd { - valueAggregator.append(try container.decode(I.self)) - } - - values = valueAggregator - } public func encode(to encoder: Encoder) throws { var container = encoder.unkeyedContainer() @@ -52,6 +35,25 @@ public struct Includes: Codable, Equatable { } } +extension Includes: Decodable where I: Decodable { + public init(from decoder: Decoder) throws { + var container = try decoder.unkeyedContainer() + + // If not parsing includes, no need to loop over them. + guard I.self != NoIncludes.self else { + values = [] + return + } + + var valueAggregator = [I]() + while !container.isAtEnd { + valueAggregator.append(try container.decode(I.self)) + } + + values = valueAggregator + } +} + extension Includes { public func appending(_ other: Includes) -> Includes { return Includes(values: values + other.values) diff --git a/Sources/JSONAPI/Document/ResourceBody.swift b/Sources/JSONAPI/Document/ResourceBody.swift index 0752331..3e9141d 100644 --- a/Sources/JSONAPI/Document/ResourceBody.swift +++ b/Sources/JSONAPI/Document/ResourceBody.swift @@ -5,26 +5,36 @@ // Created by Mathew Polzin on 11/10/18. // +public protocol OptionalEncodablePrimaryResource: Equatable, Encodable {} + +public protocol EncodablePrimaryResource: OptionalEncodablePrimaryResource {} + /// This protocol allows for `SingleResourceBody` to contain a `null` /// data object where `ManyResourceBody` cannot (because an empty /// array should be used for no results). -public protocol OptionalPrimaryResource: Equatable, Codable {} +public protocol OptionalPrimaryResource: OptionalEncodablePrimaryResource, Decodable {} /// A PrimaryResource is a type that can be used in the body of a JSON API /// document as the primary resource. -public protocol PrimaryResource: OptionalPrimaryResource {} +public protocol PrimaryResource: EncodablePrimaryResource, OptionalPrimaryResource {} + +extension Optional: OptionalEncodablePrimaryResource where Wrapped: EncodablePrimaryResource {} extension Optional: OptionalPrimaryResource where Wrapped: PrimaryResource {} +/// An `EncodableResourceBody` is a `ResourceBody` that only supports being +/// encoded. It is actually weaker than `ResourceBody`, which supports both encoding +/// and decoding. +public protocol EncodableResourceBody: Equatable, Encodable {} + /// A ResourceBody is a representation of the body of the JSON API Document. /// It can either be one resource (which can be specified as optional or not) /// or it can contain many resources (and array with zero or more entries). -public protocol ResourceBody: Codable, Equatable { -} +public protocol ResourceBody: Decodable, EncodableResourceBody {} /// A `ResourceBody` that has the ability to take on more primary /// resources by appending another similarly typed `ResourceBody`. -public protocol AppendableResourceBody: ResourceBody { +public protocol AppendableResourceBody { func appending(_ other: Self) -> Self } @@ -32,7 +42,7 @@ public func +(_ left: R, right: R) -> R { return left.appending(right) } -public struct SingleResourceBody: ResourceBody { +public struct SingleResourceBody: EncodableResourceBody { public let value: Entity public init(resourceObject: Entity) { @@ -40,7 +50,7 @@ public struct SingleResourceBody: Resou } } -public struct ManyResourceBody: AppendableResourceBody { +public struct ManyResourceBody: EncodableResourceBody, AppendableResourceBody { public let values: [Entity] public init(resourceObjects: [Entity]) { @@ -60,19 +70,6 @@ public struct NoResourceBody: ResourceBody { // MARK: Codable extension SingleResourceBody { - public init(from decoder: Decoder) throws { - let container = try decoder.singleValueContainer() - - let anyNil: Any? = nil - if container.decodeNil(), - let val = anyNil as? Entity { - value = val - return - } - - value = try container.decode(Entity.self) - } - public func encode(to encoder: Encoder) throws { var container = encoder.singleValueContainer() @@ -87,16 +84,22 @@ extension SingleResourceBody { } } -extension ManyResourceBody { - public init(from decoder: Decoder) throws { - var container = try decoder.unkeyedContainer() - var valueAggregator = [Entity]() - while !container.isAtEnd { - valueAggregator.append(try container.decode(Entity.self)) - } - values = valueAggregator - } +extension SingleResourceBody: Decodable, ResourceBody where Entity: OptionalPrimaryResource { + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + let anyNil: Any? = nil + if container.decodeNil(), + let val = anyNil as? Entity { + value = val + return + } + + value = try container.decode(Entity.self) + } +} + +extension ManyResourceBody { public func encode(to encoder: Encoder) throws { var container = encoder.unkeyedContainer() @@ -106,6 +109,17 @@ extension ManyResourceBody { } } +extension ManyResourceBody: Decodable, ResourceBody where Entity: PrimaryResource { + public init(from decoder: Decoder) throws { + var container = try decoder.unkeyedContainer() + var valueAggregator = [Entity]() + while !container.isAtEnd { + valueAggregator.append(try container.decode(Entity.self)) + } + values = valueAggregator + } +} + // MARK: CustomStringConvertible extension SingleResourceBody: CustomStringConvertible { diff --git a/Sources/JSONAPI/Resource/Poly+PrimaryResource.swift b/Sources/JSONAPI/Resource/Poly+PrimaryResource.swift index e2ed829..f1cad3a 100644 --- a/Sources/JSONAPI/Resource/Poly+PrimaryResource.swift +++ b/Sources/JSONAPI/Resource/Poly+PrimaryResource.swift @@ -15,9 +15,10 @@ import Poly /// disparate types under one roof for /// the purposes of JSON API compliant /// encoding or decoding. -public typealias JSONPoly = Poly & PrimaryResource +public typealias EncodableJSONPoly = Poly & EncodablePrimaryResource -public typealias PolyWrapped = Codable & Equatable +public typealias EncodablePolyWrapped = Encodable & Equatable +public typealias PolyWrapped = EncodablePolyWrapped & Decodable extension Poly0: PrimaryResource { public init(from decoder: Decoder) throws { @@ -30,28 +31,46 @@ extension Poly0: PrimaryResource { } // MARK: - 1 type +extension Poly1: EncodablePrimaryResource, OptionalEncodablePrimaryResource where A: EncodablePolyWrapped {} + extension Poly1: PrimaryResource, OptionalPrimaryResource where A: PolyWrapped {} // MARK: - 2 types +extension Poly2: EncodablePrimaryResource, OptionalEncodablePrimaryResource where A: EncodablePolyWrapped, B: EncodablePolyWrapped {} + extension Poly2: PrimaryResource, OptionalPrimaryResource where A: PolyWrapped, B: PolyWrapped {} // MARK: - 3 types +extension Poly3: EncodablePrimaryResource, OptionalEncodablePrimaryResource where A: EncodablePolyWrapped, B: EncodablePolyWrapped, C: EncodablePolyWrapped {} + extension Poly3: PrimaryResource, OptionalPrimaryResource where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped {} // MARK: - 4 types +extension Poly4: EncodablePrimaryResource, OptionalEncodablePrimaryResource where A: EncodablePolyWrapped, B: EncodablePolyWrapped, C: EncodablePolyWrapped, D: EncodablePolyWrapped {} + extension Poly4: PrimaryResource, OptionalPrimaryResource where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped {} // MARK: - 5 types +extension Poly5: EncodablePrimaryResource, OptionalEncodablePrimaryResource where A: EncodablePolyWrapped, B: EncodablePolyWrapped, C: EncodablePolyWrapped, D: EncodablePolyWrapped, E: EncodablePolyWrapped {} + extension Poly5: PrimaryResource, OptionalPrimaryResource where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped, E: PolyWrapped {} // MARK: - 6 types +extension Poly6: EncodablePrimaryResource, OptionalEncodablePrimaryResource where A: EncodablePolyWrapped, B: EncodablePolyWrapped, C: EncodablePolyWrapped, D: EncodablePolyWrapped, E: EncodablePolyWrapped, F: EncodablePolyWrapped {} + extension Poly6: PrimaryResource, OptionalPrimaryResource where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped, E: PolyWrapped, F: PolyWrapped {} // MARK: - 7 types +extension Poly7: EncodablePrimaryResource, OptionalEncodablePrimaryResource where A: EncodablePolyWrapped, B: EncodablePolyWrapped, C: EncodablePolyWrapped, D: EncodablePolyWrapped, E: EncodablePolyWrapped, F: EncodablePolyWrapped, G: EncodablePolyWrapped {} + extension Poly7: PrimaryResource, OptionalPrimaryResource where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped, E: PolyWrapped, F: PolyWrapped, G: PolyWrapped {} // MARK: - 8 types +extension Poly8: EncodablePrimaryResource, OptionalEncodablePrimaryResource where A: EncodablePolyWrapped, B: EncodablePolyWrapped, C: EncodablePolyWrapped, D: EncodablePolyWrapped, E: EncodablePolyWrapped, F: EncodablePolyWrapped, G: EncodablePolyWrapped, H: EncodablePolyWrapped {} + extension Poly8: PrimaryResource, OptionalPrimaryResource where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped, E: PolyWrapped, F: PolyWrapped, G: PolyWrapped, H: PolyWrapped {} // MARK: - 9 types +extension Poly9: EncodablePrimaryResource, OptionalEncodablePrimaryResource where A: EncodablePolyWrapped, B: EncodablePolyWrapped, C: EncodablePolyWrapped, D: EncodablePolyWrapped, E: EncodablePolyWrapped, F: EncodablePolyWrapped, G: EncodablePolyWrapped, H: EncodablePolyWrapped, I: EncodablePolyWrapped {} + extension Poly9: PrimaryResource, OptionalPrimaryResource where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped, E: PolyWrapped, F: PolyWrapped, G: PolyWrapped, H: PolyWrapped, I: PolyWrapped {} From 0c7c7edcab854ce25ca8c0cc889d770ba08fb40f Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Mon, 5 Aug 2019 14:48:08 -0700 Subject: [PATCH 08/26] Add sparse fieldset resource body tests --- .../JSONAPI/SparseFields/SparseFieldset.swift | 2 +- .../ResourceBody/ResourceBodyTests.swift | 93 ++++++++++++++++++- 2 files changed, 93 insertions(+), 2 deletions(-) diff --git a/Sources/JSONAPI/SparseFields/SparseFieldset.swift b/Sources/JSONAPI/SparseFields/SparseFieldset.swift index 8e0979b..9f1e579 100644 --- a/Sources/JSONAPI/SparseFields/SparseFieldset.swift +++ b/Sources/JSONAPI/SparseFields/SparseFieldset.swift @@ -10,7 +10,7 @@ public struct SparseFieldset< MetaType: JSONAPI.Meta, LinksType: JSONAPI.Links, EntityRawIdType: JSONAPI.MaybeRawId ->: Encodable where Description.Attributes: SparsableAttributes { +>: EncodablePrimaryResource where Description.Attributes: SparsableAttributes { public typealias Resource = JSONAPI.ResourceObject diff --git a/Tests/JSONAPITests/ResourceBody/ResourceBodyTests.swift b/Tests/JSONAPITests/ResourceBody/ResourceBodyTests.swift index 9bc3f2c..6b12256 100644 --- a/Tests/JSONAPITests/ResourceBody/ResourceBodyTests.swift +++ b/Tests/JSONAPITests/ResourceBody/ResourceBodyTests.swift @@ -102,6 +102,93 @@ class ResourceBodyTests: XCTestCase { } } +// MARK: - Sparse Fieldsets + +extension ResourceBodyTests { + func test_SparseSingleBodyEncode() { + let sparseArticle = Article(attributes: .init(title: "hello world"), + relationships: .none, + meta: .none, + links: .none) + .sparse(with: []) + let body = SingleResourceBody(resourceObject: sparseArticle) + + let encoded = try! JSONEncoder().encode(body) + + let deserialized = try! JSONSerialization.jsonObject(with: encoded, + options: []) + + let deserializedObj = deserialized as? [String: Any] + + XCTAssertNotNil(deserializedObj?["id"]) + XCTAssertEqual(deserializedObj?["id"] as? String, sparseArticle.resourceObject.id.rawValue) + + XCTAssertNotNil(deserializedObj?["type"]) + XCTAssertEqual(deserializedObj?["type"] as? String, Article.jsonType) + + XCTAssertEqual((deserializedObj?["attributes"] as? [String: Any])?.count, 0) + + XCTAssertNil(deserializedObj?["relationships"]) + } + + func test_SparseManyBodyEncode() { + let fields: [Article.Attributes.CodingKeys] = [.title] + let sparseArticle1 = Article(attributes: .init(title: "hello world"), + relationships: .none, + meta: .none, + links: .none) + .sparse(with: fields) + let sparseArticle2 = Article(attributes: .init(title: "hello two"), + relationships: .none, + meta: .none, + links: .none) + .sparse(with: fields) + + let body = ManyResourceBody(resourceObjects: [sparseArticle1, sparseArticle2]) + + let encoded = try! JSONEncoder().encode(body) + + let deserialized = try! JSONSerialization.jsonObject(with: encoded, + options: []) + + let deserializedObj = deserialized as? [Any] + + XCTAssertEqual(deserializedObj?.count, 2) + + guard let deserializedObj1 = deserializedObj?.first as? [String: Any], + let deserializedObj2 = deserializedObj?.last as? [String: Any] else { + XCTFail("Expected to deserialize two objects from array") + return + } + + // first article + XCTAssertNotNil(deserializedObj1["id"]) + XCTAssertEqual(deserializedObj1["id"] as? String, sparseArticle1.resourceObject.id.rawValue) + + XCTAssertNotNil(deserializedObj1["type"]) + XCTAssertEqual(deserializedObj1["type"] as? String, Article.jsonType) + + XCTAssertEqual((deserializedObj1["attributes"] as? [String: Any])?.count, 1) + XCTAssertEqual((deserializedObj1["attributes"] as? [String: Any])?["title"] as? String, "hello world") + + XCTAssertNil(deserializedObj1["relationships"]) + + // second article + XCTAssertNotNil(deserializedObj2["id"]) + XCTAssertEqual(deserializedObj2["id"] as? String, sparseArticle2.resourceObject.id.rawValue) + + XCTAssertNotNil(deserializedObj2["type"]) + XCTAssertEqual(deserializedObj2["type"] as? String, Article.jsonType) + + XCTAssertEqual((deserializedObj2["attributes"] as? [String: Any])?.count, 1) + XCTAssertEqual((deserializedObj2["attributes"] as? [String: Any])?["title"] as? String, "hello two") + + XCTAssertNil(deserializedObj2["relationships"]) + } +} + +// MARK: - Test Types + extension ResourceBodyTests { enum ArticleType: ResourceObjectDescription { @@ -109,8 +196,12 @@ extension ResourceBodyTests { typealias Relationships = NoRelationships - struct Attributes: JSONAPI.Attributes { + struct Attributes: JSONAPI.SparsableAttributes { let title: Attribute + + public enum CodingKeys: String, Equatable, CodingKey { + case title + } } } From 9a07cf7066942c68b7986f06010b5c5f57394d83 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Mon, 5 Aug 2019 14:51:18 -0700 Subject: [PATCH 09/26] Add test class placeholder --- .../SparseFields/SparseFieldEncoderTests.swift | 15 +++++++++++++++ .../SparseFields/SparseFieldsetTests.swift | 2 +- 2 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 Tests/JSONAPITests/SparseFields/SparseFieldEncoderTests.swift diff --git a/Tests/JSONAPITests/SparseFields/SparseFieldEncoderTests.swift b/Tests/JSONAPITests/SparseFields/SparseFieldEncoderTests.swift new file mode 100644 index 0000000..a9790ff --- /dev/null +++ b/Tests/JSONAPITests/SparseFields/SparseFieldEncoderTests.swift @@ -0,0 +1,15 @@ +// +// SparseFieldEncoderTests.swift +// +// +// Created by Mathew Polzin on 8/5/19. +// + +import XCTest +import JSONAPI + +class SparseFieldEncoderTests: XCTestCase { + func test_placeholder() { + // TODO: write tests + } +} diff --git a/Tests/JSONAPITests/SparseFields/SparseFieldsetTests.swift b/Tests/JSONAPITests/SparseFields/SparseFieldsetTests.swift index 7845119..003dcc5 100644 --- a/Tests/JSONAPITests/SparseFields/SparseFieldsetTests.swift +++ b/Tests/JSONAPITests/SparseFields/SparseFieldsetTests.swift @@ -10,7 +10,7 @@ import Foundation import JSONAPI import JSONAPITesting -class SparseFieldEncoderTests: XCTestCase { +class SparseFieldsetTests: XCTestCase { func test_FullEncode() { let jsonEncoder = JSONEncoder() let sparseWithEverything = SparseFieldset(testEverythingObject, fields: EverythingTest.Attributes.CodingKeys.allCases) From 32d584099f0b1640bd564755b9b485e3bc867083 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Mon, 5 Aug 2019 14:51:48 -0700 Subject: [PATCH 10/26] Update linuxmain --- Tests/JSONAPITests/XCTestManifests.swift | 43 ++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/Tests/JSONAPITests/XCTestManifests.swift b/Tests/JSONAPITests/XCTestManifests.swift index 46edbba..3011985 100644 --- a/Tests/JSONAPITests/XCTestManifests.swift +++ b/Tests/JSONAPITests/XCTestManifests.swift @@ -173,6 +173,24 @@ extension DocumentTests { ] } +extension EmptyObjectDecoderTests { + // DO NOT MODIFY: This is autogenerated, use: + // `swift test --generate-linuxmain` + // to regenerate. + static let __allTests__EmptyObjectDecoderTests = [ + ("testEmptyArray", testEmptyArray), + ("testEmptyStruct", testEmptyStruct), + ("testKeysAndCodingPath", testKeysAndCodingPath), + ("testNonEmptyArray", testNonEmptyArray), + ("testNonEmptyStruct", testNonEmptyStruct), + ("testWantingNestedKeyed", testWantingNestedKeyed), + ("testWantingNestedUnkeyed", testWantingNestedUnkeyed), + ("testWantingNil", testWantingNil), + ("testWantingSingleValue", testWantingSingleValue), + ("testWantsSuper", testWantsSuper), + ] +} + extension EntityTests { // DO NOT MODIFY: This is autogenerated, use: // `swift test --generate-linuxmain` @@ -406,6 +424,28 @@ extension ResourceBodyTests { ("test_manyResourceBodyMerge", test_manyResourceBodyMerge), ("test_singleResourceBody", test_singleResourceBody), ("test_singleResourceBody_encode", test_singleResourceBody_encode), + ("test_SparseManyBodyEncode", test_SparseManyBodyEncode), + ("test_SparseSingleBodyEncode", test_SparseSingleBodyEncode), + ] +} + +extension SparseFieldEncoderTests { + // DO NOT MODIFY: This is autogenerated, use: + // `swift test --generate-linuxmain` + // to regenerate. + static let __allTests__SparseFieldEncoderTests = [ + ("test_placeholder", test_placeholder), + ] +} + +extension SparseFieldsetTests { + // DO NOT MODIFY: This is autogenerated, use: + // `swift test --generate-linuxmain` + // to regenerate. + static let __allTests__SparseFieldsetTests = [ + ("test_FullEncode", test_FullEncode), + ("test_PartialEncode", test_PartialEncode), + ("test_sparseFieldsMethod", test_sparseFieldsMethod), ] } @@ -427,6 +467,7 @@ public func __allTests() -> [XCTestCaseEntry] { testCase(ComputedPropertiesTests.__allTests__ComputedPropertiesTests), testCase(CustomAttributesTests.__allTests__CustomAttributesTests), testCase(DocumentTests.__allTests__DocumentTests), + testCase(EmptyObjectDecoderTests.__allTests__EmptyObjectDecoderTests), testCase(EntityTests.__allTests__EntityTests), testCase(IncludedTests.__allTests__IncludedTests), testCase(LinksTests.__allTests__LinksTests), @@ -435,6 +476,8 @@ public func __allTests() -> [XCTestCaseEntry] { testCase(PolyTests.__allTests__PolyTests), testCase(RelationshipTests.__allTests__RelationshipTests), testCase(ResourceBodyTests.__allTests__ResourceBodyTests), + testCase(SparseFieldEncoderTests.__allTests__SparseFieldEncoderTests), + testCase(SparseFieldsetTests.__allTests__SparseFieldsetTests), testCase(TransformerTests.__allTests__TransformerTests), ] } From 61f2edb59a3ba5fa205fc1184e948c09f537cdf5 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Mon, 5 Aug 2019 15:21:59 -0700 Subject: [PATCH 11/26] Add tests for sparse fieldset includes --- .../JSONAPI/SparseFields/SparseFieldset.swift | 6 + .../JSONAPITests/Includes/IncludeTests.swift | 172 +++++++++++++++++- 2 files changed, 176 insertions(+), 2 deletions(-) diff --git a/Sources/JSONAPI/SparseFields/SparseFieldset.swift b/Sources/JSONAPI/SparseFields/SparseFieldset.swift index 9f1e579..3540334 100644 --- a/Sources/JSONAPI/SparseFields/SparseFieldset.swift +++ b/Sources/JSONAPI/SparseFields/SparseFieldset.swift @@ -38,3 +38,9 @@ public extension ResourceObject where Description.Attributes: SparsableAttribute return SparseFieldset(self, fields: fields) } } + +public extension ResourceObject where Description.Attributes: SparsableAttributes { + + /// The Sparse Fieldset type for this `ResourceObject` + typealias SparseType = SparseFieldset +} diff --git a/Tests/JSONAPITests/Includes/IncludeTests.swift b/Tests/JSONAPITests/Includes/IncludeTests.swift index 7f8f0f8..2254efe 100644 --- a/Tests/JSONAPITests/Includes/IncludeTests.swift +++ b/Tests/JSONAPITests/Includes/IncludeTests.swift @@ -178,6 +178,8 @@ class IncludedTests: XCTestCase { } } +// MARK: - Appending + extension IncludedTests { func test_appending() { let include1 = Includes>(values: [.init(TestEntity8(attributes: .none, relationships: .none, meta: .none, links: .none)), .init(TestEntity9(attributes: .none, relationships: .none, meta: .none, links: .none)), .init(TestEntity8(attributes: .none, relationships: .none, meta: .none, links: .none))]) @@ -190,6 +192,162 @@ extension IncludedTests { } } +// MARK: - Sparse Fieldsets + +extension IncludedTests { + func test_OneSparseIncludeType() { + let include1 = TestEntity(attributes: .init(foo: "hello", + bar: 10), + relationships: .none, + meta: .none, + links: .none) + .sparse(with: [.foo]) + + let includes: Includes> = .init(values: [.init(include1)]) + + let encoded = try! JSONEncoder().encode(includes) + + let deserialized = try! JSONSerialization.jsonObject(with: encoded, + options: []) + + let deserializedObj = deserialized as? [Any] + + XCTAssertEqual(deserializedObj?.count, 1) + + guard let deserializedObj1 = deserializedObj?.first as? [String: Any] else { + XCTFail("Expected to deserialize one object from array") + return + } + + XCTAssertNotNil(deserializedObj1["id"]) + XCTAssertEqual(deserializedObj1["id"] as? String, include1.resourceObject.id.rawValue) + + XCTAssertNotNil(deserializedObj1["type"]) + XCTAssertEqual(deserializedObj1["type"] as? String, TestEntity.jsonType) + + XCTAssertEqual((deserializedObj1["attributes"] as? [String: Any])?.count, 1) + XCTAssertEqual((deserializedObj1["attributes"] as? [String: Any])?["foo"] as? String, "hello") + + XCTAssertNil(deserializedObj1["relationships"]) + } + + func test_TwoSparseIncludeTypes() { + let include1 = TestEntity(attributes: .init(foo: "hello", + bar: 10), + relationships: .none, + meta: .none, + links: .none) + .sparse(with: [.foo]) + + let include2 = TestEntity2(attributes: .init(foo: "world", + bar: 2), + relationships: .init(entity1: "1234"), + meta: .none, + links: .none) + .sparse(with: [.bar]) + + let includes: Includes> = .init(values: [.init(include1), .init(include2)]) + + let encoded = try! JSONEncoder().encode(includes) + + let deserialized = try! JSONSerialization.jsonObject(with: encoded, + options: []) + + let deserializedObj = deserialized as? [Any] + + XCTAssertEqual(deserializedObj?.count, 2) + + guard let deserializedObj1 = deserializedObj?.first as? [String: Any], + let deserializedObj2 = deserializedObj?.last as? [String: Any] else { + XCTFail("Expected to deserialize two objects from array") + return + } + + // first include + XCTAssertNotNil(deserializedObj1["id"]) + XCTAssertEqual(deserializedObj1["id"] as? String, include1.resourceObject.id.rawValue) + + XCTAssertNotNil(deserializedObj1["type"]) + XCTAssertEqual(deserializedObj1["type"] as? String, TestEntity.jsonType) + + XCTAssertEqual((deserializedObj1["attributes"] as? [String: Any])?.count, 1) + XCTAssertEqual((deserializedObj1["attributes"] as? [String: Any])?["foo"] as? String, "hello") + + XCTAssertNil(deserializedObj1["relationships"]) + + // second include + XCTAssertNotNil(deserializedObj2["id"]) + XCTAssertEqual(deserializedObj2["id"] as? String, include2.resourceObject.id.rawValue) + + XCTAssertNotNil(deserializedObj2["type"]) + XCTAssertEqual(deserializedObj2["type"] as? String, TestEntity2.jsonType) + + XCTAssertEqual((deserializedObj2["attributes"] as? [String: Any])?.count, 1) + XCTAssertEqual((deserializedObj2["attributes"] as? [String: Any])?["bar"] as? Int, 2) + + XCTAssertNotNil(deserializedObj2["relationships"]) + XCTAssertNotNil((deserializedObj2["relationships"] as? [String: Any])?["entity1"]) + } + + func test_ComboSparseAndFullIncludeTypes() { + let include1 = TestEntity(attributes: .init(foo: "hello", + bar: 10), + relationships: .none, + meta: .none, + links: .none) + .sparse(with: [.foo]) + + let include2 = TestEntity2(attributes: .init(foo: "world", + bar: 2), + relationships: .init(entity1: "1234"), + meta: .none, + links: .none) + + let includes: Includes> = .init(values: [.init(include1), .init(include2)]) + + let encoded = try! JSONEncoder().encode(includes) + + let deserialized = try! JSONSerialization.jsonObject(with: encoded, + options: []) + + let deserializedObj = deserialized as? [Any] + + XCTAssertEqual(deserializedObj?.count, 2) + + guard let deserializedObj1 = deserializedObj?.first as? [String: Any], + let deserializedObj2 = deserializedObj?.last as? [String: Any] else { + XCTFail("Expected to deserialize two objects from array") + return + } + + // first include + XCTAssertNotNil(deserializedObj1["id"]) + XCTAssertEqual(deserializedObj1["id"] as? String, include1.resourceObject.id.rawValue) + + XCTAssertNotNil(deserializedObj1["type"]) + XCTAssertEqual(deserializedObj1["type"] as? String, TestEntity.jsonType) + + XCTAssertEqual((deserializedObj1["attributes"] as? [String: Any])?.count, 1) + XCTAssertEqual((deserializedObj1["attributes"] as? [String: Any])?["foo"] as? String, "hello") + + XCTAssertNil(deserializedObj1["relationships"]) + + // second include + XCTAssertNotNil(deserializedObj2["id"]) + XCTAssertEqual(deserializedObj2["id"] as? String, include2.id.rawValue) + + XCTAssertNotNil(deserializedObj2["type"]) + XCTAssertEqual(deserializedObj2["type"] as? String, TestEntity2.jsonType) + + XCTAssertEqual((deserializedObj2["attributes"] as? [String: Any])?.count, 2) + XCTAssertEqual((deserializedObj2["attributes"] as? [String: Any])?["foo"] as? String, "world") + XCTAssertEqual((deserializedObj2["attributes"] as? [String: Any])?["bar"] as? Int, 2) + + XCTAssertNotNil(deserializedObj2["relationships"]) + XCTAssertNotNil((deserializedObj2["relationships"] as? [String: Any])?["entity1"]) + } +} + // MARK: - Test types extension IncludedTests { enum TestEntityType: ResourceObjectDescription { @@ -198,9 +356,14 @@ extension IncludedTests { public static var jsonType: String { return "test_entity1" } - public struct Attributes: JSONAPI.Attributes { + public struct Attributes: JSONAPI.SparsableAttributes { let foo: Attribute let bar: Attribute + + public enum CodingKeys: String, Equatable, CodingKey { + case foo + case bar + } } } @@ -214,9 +377,14 @@ extension IncludedTests { let entity1: ToOneRelationship } - public struct Attributes: JSONAPI.Attributes { + public struct Attributes: JSONAPI.SparsableAttributes { let foo: Attribute let bar: Attribute + + public enum CodingKeys: String, Equatable, CodingKey { + case foo + case bar + } } } From 83f7a7b60e9b7d19dbcf75f4e05d200ae78c6f85 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Mon, 5 Aug 2019 16:02:40 -0700 Subject: [PATCH 12/26] Add sparse document tests --- Sources/JSONAPI/Resource/ResourceObject.swift | 4 +- .../JSONAPITests/Document/DocumentTests.swift | 230 ++++++++++++++++++ .../JSONAPITests/Includes/IncludeTests.swift | 26 +- 3 files changed, 246 insertions(+), 14 deletions(-) diff --git a/Sources/JSONAPI/Resource/ResourceObject.swift b/Sources/JSONAPI/Resource/ResourceObject.swift index af964a7..7fefbec 100644 --- a/Sources/JSONAPI/Resource/ResourceObject.swift +++ b/Sources/JSONAPI/Resource/ResourceObject.swift @@ -15,10 +15,12 @@ public protocol Relationships: Codable & Equatable {} /// properties of any types that are JSON encodable. public protocol Attributes: Codable & Equatable {} +public typealias SparsableCodingKey = CodingKey & Equatable + /// Attributes containing publicly accessible and `Equatable` /// CodingKeys are required to support Sparse Fieldsets. public protocol SparsableAttributes: Attributes { - associatedtype CodingKeys: CodingKey & Equatable + associatedtype CodingKeys: SparsableCodingKey } /// Can be used as `Relationships` Type for Entities that do not diff --git a/Tests/JSONAPITests/Document/DocumentTests.swift b/Tests/JSONAPITests/Document/DocumentTests.swift index 2aa8229..5f9c18b 100644 --- a/Tests/JSONAPITests/Document/DocumentTests.swift +++ b/Tests/JSONAPITests/Document/DocumentTests.swift @@ -926,6 +926,217 @@ extension DocumentTests { } } +// MARK: Sparse Fieldset Documents + +extension DocumentTests { + func test_sparsePrimaryResource() { + let primaryResource = Book(attributes: .init(pageCount: 100), + relationships: .init(author: "1234", + series: []), + meta: .none, + links: .none) + .sparse(with: [.pageCount]) + + let document = Document< + SingleResourceBody, + NoMetadata, + NoLinks, + NoIncludes, + NoAPIDescription, + UnknownJSONAPIError + >(apiDescription: .none, + body: .init(resourceObject: primaryResource), + includes: .none, + meta: .none, + links: .none) + + let encoded = try! JSONEncoder().encode(document) + + let deserialized = try! JSONSerialization.jsonObject(with: encoded, + options: []) + + let deserializedObj = deserialized as? [String: Any] + + guard let deserializedObj1 = deserializedObj?["data"] as? [String: Any] else { + XCTFail("Expected to deserialize one object from document data") + return + } + + XCTAssertNotNil(deserializedObj1["id"]) + XCTAssertEqual(deserializedObj1["id"] as? String, primaryResource.resourceObject.id.rawValue) + + XCTAssertNotNil(deserializedObj1["type"]) + XCTAssertEqual(deserializedObj1["type"] as? String, Book.jsonType) + + XCTAssertEqual((deserializedObj1["attributes"] as? [String: Any])?.count, 1) + XCTAssertEqual((deserializedObj1["attributes"] as? [String: Any])?["pageCount"] as? Int, 100) + + XCTAssertNotNil(deserializedObj1["relationships"]) + } + + func test_sparsePrimaryResourceOptionalAndNil() { + let document = Document< + SingleResourceBody, + NoMetadata, + NoLinks, + NoIncludes, + NoAPIDescription, + UnknownJSONAPIError + >(apiDescription: .none, + body: .init(resourceObject: nil), + includes: .none, + meta: .none, + links: .none) + + let encoded = try! JSONEncoder().encode(document) + + let deserialized = try! JSONSerialization.jsonObject(with: encoded, + options: []) + + let deserializedObj = deserialized as? [String: Any] + + XCTAssertNotNil(deserializedObj?["data"] as? NSNull) + } + + func test_sparseIncludeFullPrimaryResource() { + let bookInclude = Book(id: "444", + attributes: .init(pageCount: 113), + relationships: .init(author: "1234", + series: ["443"]), + meta: .none, + links: .none) + .sparse(with: []) + + let primaryResource = Book(id: "443", + attributes: .init(pageCount: 100), + relationships: .init(author: "1234", + series: ["444"]), + meta: .none, + links: .none) + + let document = Document< + SingleResourceBody, + NoMetadata, + NoLinks, + Include1, + NoAPIDescription, + UnknownJSONAPIError + >(apiDescription: .none, + body: .init(resourceObject: primaryResource), + includes: .init(values: [.init(bookInclude)]), + meta: .none, + links: .none) + + let encoded = try! JSONEncoder().encode(document) + + let deserialized = try! JSONSerialization.jsonObject(with: encoded, + options: []) + + let deserializedObj = deserialized as? [String: Any] + + guard let deserializedObj1 = deserializedObj?["data"] as? [String: Any] else { + XCTFail("Expected to deserialize one object from document data") + return + } + + XCTAssertNotNil(deserializedObj1["id"]) + XCTAssertEqual(deserializedObj1["id"] as? String, primaryResource.id.rawValue) + + XCTAssertNotNil(deserializedObj1["type"]) + XCTAssertEqual(deserializedObj1["type"] as? String, Book.jsonType) + + XCTAssertEqual((deserializedObj1["attributes"] as? [String: Any])?.count, 1) + XCTAssertEqual((deserializedObj1["attributes"] as? [String: Any])?["pageCount"] as? Int, 100) + + XCTAssertNotNil(deserializedObj1["relationships"]) + + guard let deserializedIncludes = deserializedObj?["included"] as? [Any], + let deserializedObj2 = deserializedIncludes.first as? [String: Any] else { + XCTFail("Expected to deserialize one incude object") + return + } + + XCTAssertNotNil(deserializedObj2["id"]) + XCTAssertEqual(deserializedObj2["id"] as? String, bookInclude.resourceObject.id.rawValue) + + XCTAssertNotNil(deserializedObj2["type"]) + XCTAssertEqual(deserializedObj2["type"] as? String, Book.jsonType) + + XCTAssertEqual((deserializedObj2["attributes"] as? [String: Any])?.count, 0) + + XCTAssertNotNil(deserializedObj2["relationships"]) + } + + func test_sparseIncludeSparsePrimaryResource() { + let bookInclude = Book(id: "444", + attributes: .init(pageCount: 113), + relationships: .init(author: "1234", + series: ["443"]), + meta: .none, + links: .none) + .sparse(with: []) + + let primaryResource = Book(id: "443", + attributes: .init(pageCount: 100), + relationships: .init(author: "1234", + series: ["444"]), + meta: .none, + links: .none) + .sparse(with: []) + + let document = Document< + SingleResourceBody, + NoMetadata, + NoLinks, + Include1, + NoAPIDescription, + UnknownJSONAPIError + >(apiDescription: .none, + body: .init(resourceObject: primaryResource), + includes: .init(values: [.init(bookInclude)]), + meta: .none, + links: .none) + + let encoded = try! JSONEncoder().encode(document) + + let deserialized = try! JSONSerialization.jsonObject(with: encoded, + options: []) + + let deserializedObj = deserialized as? [String: Any] + + guard let deserializedObj1 = deserializedObj?["data"] as? [String: Any] else { + XCTFail("Expected to deserialize one object from document data") + return + } + + XCTAssertNotNil(deserializedObj1["id"]) + XCTAssertEqual(deserializedObj1["id"] as? String, primaryResource.resourceObject.id.rawValue) + + XCTAssertNotNil(deserializedObj1["type"]) + XCTAssertEqual(deserializedObj1["type"] as? String, Book.jsonType) + + XCTAssertEqual((deserializedObj1["attributes"] as? [String: Any])?.count, 0) + + XCTAssertNotNil(deserializedObj1["relationships"]) + + guard let deserializedIncludes = deserializedObj?["included"] as? [Any], + let deserializedObj2 = deserializedIncludes.first as? [String: Any] else { + XCTFail("Expected to deserialize one incude object") + return + } + + XCTAssertNotNil(deserializedObj2["id"]) + XCTAssertEqual(deserializedObj2["id"] as? String, bookInclude.resourceObject.id.rawValue) + + XCTAssertNotNil(deserializedObj2["type"]) + XCTAssertEqual(deserializedObj2["type"] as? String, Book.jsonType) + + XCTAssertEqual((deserializedObj2["attributes"] as? [String: Any])?.count, 0) + + XCTAssertNotNil(deserializedObj2["relationships"]) + } +} + // MARK: Poly PrimaryResource Tests extension DocumentTests { func test_singleDocument_PolyPrimaryResource() { @@ -1115,6 +1326,25 @@ extension DocumentTests { typealias Article = BasicEntity + enum BookType: ResourceObjectDescription { + static var jsonType: String { return "books" } + + struct Attributes: JSONAPI.SparsableAttributes { + let pageCount: Attribute + + enum CodingKeys: String, SparsableCodingKey { + case pageCount + } + } + + struct Relationships: JSONAPI.Relationships { + let author: ToOneRelationship + let series: ToManyRelationship + } + } + + typealias Book = BasicEntity + struct TestPageMetadata: JSONAPI.Meta { let total: Int let limit: Int diff --git a/Tests/JSONAPITests/Includes/IncludeTests.swift b/Tests/JSONAPITests/Includes/IncludeTests.swift index 2254efe..4272083 100644 --- a/Tests/JSONAPITests/Includes/IncludeTests.swift +++ b/Tests/JSONAPITests/Includes/IncludeTests.swift @@ -12,15 +12,15 @@ class IncludedTests: XCTestCase { func test_zeroIncludes() { let includes = decoded(type: Includes.self, data: two_same_type_includes) - + XCTAssertEqual(includes.count, 0) } func test_zeroIncludes_encode() { - XCTAssertThrowsError(try JSONEncoder().encode(decoded(type: Includes.self, - data: two_same_type_includes))) - } - + XCTAssertThrowsError(try JSONEncoder().encode(decoded(type: Includes.self, + data: two_same_type_includes))) + } + func test_OneInclude() { let includes = decoded(type: Includes>.self, data: one_include) @@ -32,7 +32,7 @@ class IncludedTests: XCTestCase { test_DecodeEncodeEquality(type: Includes>.self, data: one_include) } - + func test_TwoSameIncludes() { let includes = decoded(type: Includes>.self, data: two_same_type_includes) @@ -57,7 +57,7 @@ class IncludedTests: XCTestCase { test_DecodeEncodeEquality(type: Includes>.self, data: two_different_type_includes) } - + func test_ThreeDifferentIncludes() { let includes = decoded(type: Includes>.self, data: three_different_type_includes) @@ -71,11 +71,11 @@ class IncludedTests: XCTestCase { test_DecodeEncodeEquality(type: Includes>.self, data: three_different_type_includes) } - + func test_FourDifferentIncludes() { let includes = decoded(type: Includes>.self, data: four_different_type_includes) - + XCTAssertEqual(includes[TestEntity.self].count, 1) XCTAssertEqual(includes[TestEntity2.self].count, 1) XCTAssertEqual(includes[TestEntity4.self].count, 1) @@ -86,11 +86,11 @@ class IncludedTests: XCTestCase { test_DecodeEncodeEquality(type: Includes>.self, data: four_different_type_includes) } - + func test_FiveDifferentIncludes() { let includes = decoded(type: Includes>.self, data: five_different_type_includes) - + XCTAssertEqual(includes[TestEntity.self].count, 1) XCTAssertEqual(includes[TestEntity2.self].count, 1) XCTAssertEqual(includes[TestEntity3.self].count, 1) @@ -102,11 +102,11 @@ class IncludedTests: XCTestCase { test_DecodeEncodeEquality(type: Includes>.self, data: five_different_type_includes) } - + func test_SixDifferentIncludes() { let includes = decoded(type: Includes>.self, data: six_different_type_includes) - + XCTAssertEqual(includes[TestEntity.self].count, 1) XCTAssertEqual(includes[TestEntity2.self].count, 1) XCTAssertEqual(includes[TestEntity3.self].count, 1) From fe1f4c6c198b1bec0580d91e1d89a8d65287889f Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Mon, 5 Aug 2019 16:15:56 -0700 Subject: [PATCH 13/26] A bit of code documentation --- Sources/JSONAPI/Resource/ResourceObject.swift | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/Sources/JSONAPI/Resource/ResourceObject.swift b/Sources/JSONAPI/Resource/ResourceObject.swift index 7fefbec..4d08ba6 100644 --- a/Sources/JSONAPI/Resource/ResourceObject.swift +++ b/Sources/JSONAPI/Resource/ResourceObject.swift @@ -15,6 +15,8 @@ public protocol Relationships: Codable & Equatable {} /// properties of any types that are JSON encodable. public protocol Attributes: Codable & Equatable {} +/// CodingKeys must be `CodingKey` and `Equatable` in order +/// to support Sparse Fieldsets. public typealias SparsableCodingKey = CodingKey & Equatable /// Attributes containing publicly accessible and `Equatable` @@ -403,14 +405,14 @@ extension ResourceObject where MetaType == NoMetadata, LinksType == NoLinks, Ent // MARK: Pointer for Relationships use. public extension ResourceObject where EntityRawIdType: JSONAPI.RawIdType { - /// An ResourceObject.Pointer is a `ToOneRelationship` with no metadata or links. - /// This is just a convenient way to reference an ResourceObject so that - /// other Entities' Relationships can be built up from it. + /// A `ResourceObject.Pointer` is a `ToOneRelationship` with no metadata or links. + /// This is just a convenient way to reference a `ResourceObject` so that + /// other ResourceObjects' Relationships can be built up from it. typealias Pointer = ToOneRelationship - /// ResourceObject.Pointers is a `ToManyRelationship` with no metadata or links. - /// This is just a convenient way to reference a bunch of Entities so - /// that other Entities' Relationships can be built up from them. + /// `ResourceObject.Pointers` is a `ToManyRelationship` with no metadata or links. + /// This is just a convenient way to reference a bunch of ResourceObjects so + /// that other ResourceObjects' Relationships can be built up from them. typealias Pointers = ToManyRelationship /// Get a pointer to this resource object that can be used as a @@ -419,6 +421,8 @@ public extension ResourceObject where EntityRawIdType: JSONAPI.RawIdType { return Pointer(resourceObject: self) } + /// Get a pointer (i.e. `ToOneRelationship`) to this resource + /// object with the given metadata and links attached. func pointer(withMeta meta: MType, links: LType) -> ToOneRelationship { return ToOneRelationship(resourceObject: self, meta: meta, links: links) } @@ -426,20 +430,20 @@ public extension ResourceObject where EntityRawIdType: JSONAPI.RawIdType { // MARK: Identifying Unidentified Entities public extension ResourceObject where EntityRawIdType == Unidentified { - /// Create a new ResourceObject from this one with a newly created + /// Create a new `ResourceObject` from this one with a newly created /// unique Id of the given type. func identified(byType: RawIdType.Type) -> ResourceObject { return .init(attributes: attributes, relationships: relationships, meta: meta, links: links) } - /// Create a new ResourceObject from this one with the given Id. + /// Create a new `ResourceObject` from this one with the given Id. func identified(by id: RawIdType) -> ResourceObject { return .init(id: ResourceObject.Identifier(rawValue: id), attributes: attributes, relationships: relationships, meta: meta, links: links) } } public extension ResourceObject where EntityRawIdType: CreatableRawIdType { - /// Create a copy of this ResourceObject with a new unique Id. + /// Create a copy of this `ResourceObject` with a new unique Id. func withNewIdentifier() -> ResourceObject { return ResourceObject(attributes: attributes, relationships: relationships, meta: meta, links: links) } From 045e88f4d45259a74b64a799e5f2c2c245b452c4 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Mon, 5 Aug 2019 16:28:02 -0700 Subject: [PATCH 14/26] More code documentation --- Sources/JSONAPI/Document/Document.swift | 5 +++++ Sources/JSONAPI/Document/Error.swift | 3 +++ Sources/JSONAPI/Document/ResourceBody.swift | 8 +++++++- Sources/JSONAPI/Meta/Meta.swift | 2 ++ Sources/JSONAPI/Resource/Transformer.swift | 7 ++++++- Sources/JSONAPI/SparseFields/SparseFieldset.swift | 6 +++++- 6 files changed, 28 insertions(+), 3 deletions(-) diff --git a/Sources/JSONAPI/Document/Document.swift b/Sources/JSONAPI/Document/Document.swift index d0a34d9..4034969 100644 --- a/Sources/JSONAPI/Document/Document.swift +++ b/Sources/JSONAPI/Document/Document.swift @@ -7,6 +7,9 @@ import Poly +/// An `EncodableJSONAPIDocument` supports encoding but not decoding. +/// It is actually more restrictive than `JSONAPIDocument` which supports both +/// encoding and decoding. public protocol EncodableJSONAPIDocument: Equatable, Encodable { associatedtype PrimaryResourceBody: JSONAPI.EncodableResourceBody associatedtype MetaType: JSONAPI.Meta @@ -20,6 +23,8 @@ public protocol EncodableJSONAPIDocument: Equatable, Encodable { var body: Body { get } } +/// A `JSONAPIDocument` supports encoding and decoding of a JSON:API +/// compliant Document. public protocol JSONAPIDocument: EncodableJSONAPIDocument, Decodable where PrimaryResourceBody: JSONAPI.ResourceBody, IncludeType: Decodable {} /// A JSON API Document represents the entire body diff --git a/Sources/JSONAPI/Document/Error.swift b/Sources/JSONAPI/Document/Error.swift index 6d6e36e..ab6e3ca 100644 --- a/Sources/JSONAPI/Document/Error.swift +++ b/Sources/JSONAPI/Document/Error.swift @@ -9,6 +9,9 @@ public protocol JSONAPIError: Swift.Error, Equatable, Codable { static var unknown: Self { get } } +/// `UnknownJSONAPIError` can actually be used in any sitaution +/// where you don't know what errors are possible _or_ you just don't +/// care what errors might show up. public enum UnknownJSONAPIError: JSONAPIError { case unknownError diff --git a/Sources/JSONAPI/Document/ResourceBody.swift b/Sources/JSONAPI/Document/ResourceBody.swift index 3e9141d..2eb1bb4 100644 --- a/Sources/JSONAPI/Document/ResourceBody.swift +++ b/Sources/JSONAPI/Document/ResourceBody.swift @@ -5,8 +5,14 @@ // Created by Mathew Polzin on 11/10/18. // +/// This protocol allows for a `SingleResourceBody` to contain a `null` +/// data object where `ManyResourceBody` cannot (because an empty +/// array should be used for no results). public protocol OptionalEncodablePrimaryResource: Equatable, Encodable {} +/// An `EncodablePrimaryResource` is a `PrimaryResource` that only supports encoding. +/// This is actually more restrictave than `PrimaryResource`, which supports both encoding and +/// decoding. public protocol EncodablePrimaryResource: OptionalEncodablePrimaryResource {} /// This protocol allows for `SingleResourceBody` to contain a `null` @@ -14,7 +20,7 @@ public protocol EncodablePrimaryResource: OptionalEncodablePrimaryResource {} /// array should be used for no results). public protocol OptionalPrimaryResource: OptionalEncodablePrimaryResource, Decodable {} -/// A PrimaryResource is a type that can be used in the body of a JSON API +/// A `PrimaryResource` is a type that can be used in the body of a JSON API /// document as the primary resource. public protocol PrimaryResource: EncodablePrimaryResource, OptionalPrimaryResource {} diff --git a/Sources/JSONAPI/Meta/Meta.swift b/Sources/JSONAPI/Meta/Meta.swift index 587dc9c..68b2c94 100644 --- a/Sources/JSONAPI/Meta/Meta.swift +++ b/Sources/JSONAPI/Meta/Meta.swift @@ -19,6 +19,8 @@ public protocol Meta: Codable, Equatable { // nullable. extension Optional: Meta where Wrapped: Meta {} +/// Use this type when you want to specify not to encode or decode any metadata +/// for a type. public struct NoMetadata: Meta, CustomStringConvertible { public static var none: NoMetadata { return NoMetadata() } diff --git a/Sources/JSONAPI/Resource/Transformer.swift b/Sources/JSONAPI/Resource/Transformer.swift index 04d3c04..c7aae56 100644 --- a/Sources/JSONAPI/Resource/Transformer.swift +++ b/Sources/JSONAPI/Resource/Transformer.swift @@ -10,12 +10,16 @@ public protocol Transformer { associatedtype From associatedtype To + /// Turn value of type `From` into a value of type `To` or + /// throw an error on failure. static func transform(_ value: From) throws -> To } /// ReversibleTransformers define a function that reverses the transform /// operation. public protocol ReversibleTransformer: Transformer { + /// Turn a value of type `To` into a value of type `From` or + /// throw an error on failure. static func reverse(_ value: To) throws -> From } @@ -43,7 +47,8 @@ extension Validator { } /// Validate the given value and then return it if valid. - /// throws if invalid. + /// throws an erro if invalid. + /// - returns: The same value passed in, if it was valid. public static func validate(_ value: To) throws -> To { let _ = try transform(value) return value diff --git a/Sources/JSONAPI/SparseFields/SparseFieldset.swift b/Sources/JSONAPI/SparseFields/SparseFieldset.swift index 3540334..b326f13 100644 --- a/Sources/JSONAPI/SparseFields/SparseFieldset.swift +++ b/Sources/JSONAPI/SparseFields/SparseFieldset.swift @@ -5,6 +5,9 @@ // Created by Mathew Polzin on 8/4/19. // +/// A SparseFieldset represents an `Encodable` subset of the fields +/// a `ResourceObject` would normally encode. Currently, you can +/// only apply sparse fieldset's to `ResourceObject.Attributes`. public struct SparseFieldset< Description: JSONAPI.ResourceObjectDescription, MetaType: JSONAPI.Meta, @@ -12,6 +15,7 @@ public struct SparseFieldset< EntityRawIdType: JSONAPI.MaybeRawId >: EncodablePrimaryResource where Description.Attributes: SparsableAttributes { + /// The `ResourceObject` type this `SparseFieldset` is capable of modifying. public typealias Resource = JSONAPI.ResourceObject public let resourceObject: Resource @@ -41,6 +45,6 @@ public extension ResourceObject where Description.Attributes: SparsableAttribute public extension ResourceObject where Description.Attributes: SparsableAttributes { - /// The Sparse Fieldset type for this `ResourceObject` + /// The `SparseFieldset` type for this `ResourceObject` typealias SparseType = SparseFieldset } From 3a60ac5fe22940e3f25ac64fc24c2c7d081496b7 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Mon, 5 Aug 2019 17:59:38 -0700 Subject: [PATCH 15/26] Add playground page showing off sparse fieldset encoding --- .../Contents.swift | 72 +++++++++++++++++++ .../Usage.xcplaygroundpage/Contents.swift | 9 ++- JSONAPI.playground/contents.xcplayground | 1 + Tests/JSONAPITests/XCTestManifests.swift | 7 ++ 4 files changed, 88 insertions(+), 1 deletion(-) create mode 100644 JSONAPI.playground/Pages/Sparse Fieldsets Example.xcplaygroundpage/Contents.swift diff --git a/JSONAPI.playground/Pages/Sparse Fieldsets Example.xcplaygroundpage/Contents.swift b/JSONAPI.playground/Pages/Sparse Fieldsets Example.xcplaygroundpage/Contents.swift new file mode 100644 index 0000000..e34667b --- /dev/null +++ b/JSONAPI.playground/Pages/Sparse Fieldsets Example.xcplaygroundpage/Contents.swift @@ -0,0 +1,72 @@ + +import JSONAPI +import Foundation + +// MARK: - Resource Object + +enum ThingWithPropertiesDescription: JSONAPI.ResourceObjectDescription { + static let jsonType: String = "thing" + + // + // NOTE: `JSONAPI.SparsableAttributes` as opposed to `JSONAPI.Attributes` + // + struct Attributes: JSONAPI.SparsableAttributes { + let stringThing: Attribute + let numberThing: Attribute + let boolThing: Attribute + + // + // NOTE: Special implementation of `CodingKeys` + // + enum CodingKeys: String, JSONAPI.SparsableCodingKey { + case stringThing + case numberThing + case boolThing + } + } + + typealias Relationships = NoRelationships +} + +typealias ThingWithProperties = JSONAPI.ResourceObject + +// MARK: - Document + +// +// NOTE: Using `JSONAPI.EncodableResourceBody` which means the document type will be `Encodable` but not `Decodable`. +// +typealias Document = JSONAPI.Document + +// +// NOTE: Using `JSONAPI.EncodablePrimaryResource` which means the `ResourceBody` will be `Encodable` but not `Decodable. +// +typealias SingleDocument = Document, NoIncludes> + +// MARK: - Resource Initialization + +let resource = ThingWithProperties(id: .init(rawValue: "1234"), + attributes: .init(stringThing: .init(value: "hello world"), + numberThing: .init(value: 10), + boolThing: .init(value: nil)), + relationships: .none, + meta: .none, + links: .none) +// +// NOTE: Creating a sparse resource that will only encode +// the attribute named "stringThing" +// +let sparseResource = resource.sparse(with: [.stringThing]) + +// MARK: - Encoding + +let encoder = JSONEncoder() + +let sparseResourceDoc = SingleDocument(apiDescription: .none, + body: .init(resourceObject: sparseResource), + includes: .none, + meta: .none, + links: .none) + +let data = try! encoder.encode(sparseResourceDoc) + +print(String(data: data, encoding: .utf8)!) diff --git a/JSONAPI.playground/Pages/Usage.xcplaygroundpage/Contents.swift b/JSONAPI.playground/Pages/Usage.xcplaygroundpage/Contents.swift index 22bb3c0..00b5172 100644 --- a/JSONAPI.playground/Pages/Usage.xcplaygroundpage/Contents.swift +++ b/JSONAPI.playground/Pages/Usage.xcplaygroundpage/Contents.swift @@ -8,6 +8,7 @@ Please enjoy these examples, but allow me the forced casting and the lack of err ********/ + // MARK: - Create a request or response body with one Dog in it let dogFromCode = try! Dog(name: "Buddy", owner: nil) @@ -15,17 +16,20 @@ let singleDogDocument = SingleDogDocument(apiDescription: .none, body: .init(res let singleDogData = try! JSONEncoder().encode(singleDogDocument) + // MARK: - Parse a request or response body with one Dog in it let dogResponse = try! JSONDecoder().decode(SingleDogDocument.self, from: singleDogData) let dogFromData = dogResponse.body.primaryResource?.value let dogOwner: Person.Identifier? = dogFromData.flatMap { $0 ~> \.owner } -// MARKL - Parse a request or response body with one Dog in it using an alternative model + +// MARK: - Parse a request or response body with one Dog in it using an alternative model typealias AltSingleDogDocument = JSONAPI.Document, NoMetadata, NoLinks, NoIncludes, NoAPIDescription, UnknownJSONAPIError> let altDogResponse = try! JSONDecoder().decode(AltSingleDogDocument.self, from: singleDogData) let altDogFromData = altDogResponse.body.primaryResource?.value let altDogHuman: Person.Identifier? = altDogFromData.flatMap { $0 ~> \.human } + // MARK: - Create a request or response with multiple people and dogs and houses included let personIds = [Person.Identifier(), Person.Identifier()] let dogs = try! [Dog(name: "Buddy", owner: personIds[0]), Dog(name: "Joy", owner: personIds[0]), Dog(name: "Travis", owner: personIds[1])] @@ -36,6 +40,7 @@ let includes = dogs.map { BatchPeopleDocument.Include($0) } + houses.map { Batch let batchPeopleDocument = BatchPeopleDocument(apiDescription: .none, body: .init(resourceObjects: people), includes: .init(values: includes), meta: .none, links: .none) let batchPeopleData = try! JSONEncoder().encode(batchPeopleDocument) + // MARK: - Parse a request or response body with multiple people in it and dogs and houses included let peopleResponse = try! JSONDecoder().decode(BatchPeopleDocument.self, from: batchPeopleData) @@ -47,6 +52,7 @@ print("-----") print(peopleResponse) print("-----") + // MARK: - Pass successfully parsed body to other parts of the code /* @@ -59,6 +65,7 @@ if case let .data(bodyData) = peopleResponse.body { } */ + // MARK: - Work in the abstract func process(document: T) { diff --git a/JSONAPI.playground/contents.xcplayground b/JSONAPI.playground/contents.xcplayground index 3da156e..21e919c 100644 --- a/JSONAPI.playground/contents.xcplayground +++ b/JSONAPI.playground/contents.xcplayground @@ -5,5 +5,6 @@ + \ No newline at end of file diff --git a/Tests/JSONAPITests/XCTestManifests.swift b/Tests/JSONAPITests/XCTestManifests.swift index 3011985..dc100bd 100644 --- a/Tests/JSONAPITests/XCTestManifests.swift +++ b/Tests/JSONAPITests/XCTestManifests.swift @@ -144,6 +144,10 @@ extension DocumentTests { ("test_singleDocumentSomeIncludesWithMetadata_encode", test_singleDocumentSomeIncludesWithMetadata_encode), ("test_singleDocumentSomeIncludesWithMetadataWithAPIDescription", test_singleDocumentSomeIncludesWithMetadataWithAPIDescription), ("test_singleDocumentSomeIncludesWithMetadataWithAPIDescription_encode", test_singleDocumentSomeIncludesWithMetadataWithAPIDescription_encode), + ("test_sparseIncludeFullPrimaryResource", test_sparseIncludeFullPrimaryResource), + ("test_sparseIncludeSparsePrimaryResource", test_sparseIncludeSparsePrimaryResource), + ("test_sparsePrimaryResource", test_sparsePrimaryResource), + ("test_sparsePrimaryResourceOptionalAndNil", test_sparsePrimaryResourceOptionalAndNil), ("test_unknownErrorDocumentAddIncludes", test_unknownErrorDocumentAddIncludes), ("test_unknownErrorDocumentAddIncludingType", test_unknownErrorDocumentAddIncludingType), ("test_unknownErrorDocumentMissingLinks", test_unknownErrorDocumentMissingLinks), @@ -275,6 +279,7 @@ extension IncludedTests { // to regenerate. static let __allTests__IncludedTests = [ ("test_appending", test_appending), + ("test_ComboSparseAndFullIncludeTypes", test_ComboSparseAndFullIncludeTypes), ("test_EightDifferentIncludes", test_EightDifferentIncludes), ("test_EightDifferentIncludes_encode", test_EightDifferentIncludes_encode), ("test_FiveDifferentIncludes", test_FiveDifferentIncludes), @@ -285,6 +290,7 @@ extension IncludedTests { ("test_NineDifferentIncludes_encode", test_NineDifferentIncludes_encode), ("test_OneInclude", test_OneInclude), ("test_OneInclude_encode", test_OneInclude_encode), + ("test_OneSparseIncludeType", test_OneSparseIncludeType), ("test_SevenDifferentIncludes", test_SevenDifferentIncludes), ("test_SevenDifferentIncludes_encode", test_SevenDifferentIncludes_encode), ("test_SixDifferentIncludes", test_SixDifferentIncludes), @@ -295,6 +301,7 @@ extension IncludedTests { ("test_TwoDifferentIncludes_encode", test_TwoDifferentIncludes_encode), ("test_TwoSameIncludes", test_TwoSameIncludes), ("test_TwoSameIncludes_encode", test_TwoSameIncludes_encode), + ("test_TwoSparseIncludeTypes", test_TwoSparseIncludeTypes), ("test_zeroIncludes", test_zeroIncludes), ("test_zeroIncludes_encode", test_zeroIncludes_encode), ("test_zeroIncludes_init", test_zeroIncludes_init), From 6c8646a1b4a093caf470d97721cc2ef51c657a38 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Mon, 5 Aug 2019 18:09:29 -0700 Subject: [PATCH 16/26] whitespace --- Sources/JSONAPI/Resource/Transformer.swift | 28 +++++++++++----------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/Sources/JSONAPI/Resource/Transformer.swift b/Sources/JSONAPI/Resource/Transformer.swift index c7aae56..203b9b5 100644 --- a/Sources/JSONAPI/Resource/Transformer.swift +++ b/Sources/JSONAPI/Resource/Transformer.swift @@ -7,12 +7,12 @@ /// A Transformer simply defines a static function that transforms a value. public protocol Transformer { - associatedtype From - associatedtype To + associatedtype From + associatedtype To /// Turn value of type `From` into a value of type `To` or /// throw an error on failure. - static func transform(_ value: From) throws -> To + static func transform(_ value: From) throws -> To } /// ReversibleTransformers define a function that reverses the transform @@ -20,13 +20,13 @@ public protocol Transformer { public protocol ReversibleTransformer: Transformer { /// Turn a value of type `To` into a value of type `From` or /// throw an error on failure. - static func reverse(_ value: To) throws -> From + static func reverse(_ value: To) throws -> From } /// The IdentityTransformer does not perform any transformation on a value. public enum IdentityTransformer: ReversibleTransformer { - public static func transform(_ value: T) throws -> T { return value } - public static func reverse(_ value: T) throws -> T { return value } + public static func transform(_ value: T) throws -> T { return value } + public static func reverse(_ value: T) throws -> T { return value } } // MARK: - Validator @@ -41,16 +41,16 @@ public protocol Validator: ReversibleTransformer where From == To { } extension Validator { - public static func reverse(_ value: To) throws -> To { - let _ = try transform(value) - return value - } + public static func reverse(_ value: To) throws -> To { + let _ = try transform(value) + return value + } /// Validate the given value and then return it if valid. /// throws an erro if invalid. /// - returns: The same value passed in, if it was valid. - public static func validate(_ value: To) throws -> To { - let _ = try transform(value) - return value - } + public static func validate(_ value: To) throws -> To { + let _ = try transform(value) + return value + } } From ed2334935123aa60b0ac6c9d625cb27bd9e1a653 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Mon, 5 Aug 2019 18:45:13 -0700 Subject: [PATCH 17/26] Update README --- README.md | 32 ++++++++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index f00fafd..7a128bc 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ See the JSON API Spec here: https://jsonapi.org/format/ - [`Transformer`](#transformer) - [`Validator`](#validator) - [Computed `Attribute`](#computed-attribute) - - [Copying `ResourceObjects`](#copying-resourceobjects) + - [Copying/Mutating `ResourceObjects`](#copyingmutating-resourceobjects) - [`JSONAPI.Document`](#jsonapidocument) - [`ResourceBody`](#resourcebody) - [nullable `PrimaryResource`](#nullable-primaryresource) @@ -54,6 +54,9 @@ See the JSON API Spec here: https://jsonapi.org/format/ - [`JSONAPI.Meta`](#jsonapimeta) - [`JSONAPI.Links`](#jsonapilinks) - [`JSONAPI.RawIdType`](#jsonapirawidtype) + - [Sparse Fieldsets](#sparse-fieldsets) + - [Supporting Sparse Fieldset Encoding](#supporting-sparse-fieldset-encoding) + - [Sparse Fieldset `typealias` comparisons](#sparse-fieldset-typealias-comparisons) - [Custom Attribute or Relationship Key Mapping](#custom-attribute-or-relationship-key-mapping) - [Custom Attribute Encode/Decode](#custom-attribute-encodedecode) - [Meta-Attributes](#meta-attributes) @@ -150,7 +153,7 @@ Note that Playground support for importing non-system Frameworks is still a bit ### Misc - [x] Support transforms on `Attributes` values (e.g. to support different representations of `Date`) - [x] Support validation on `Attributes`. -- [ ] Support sparse fieldsets. At the moment, not sure what this support will look like. A client can likely just define a new model to represent a sparse population of another model in a very specific use case. On the server side, it becomes much more appealing to be able to support arbitrary combinations of omitted fields. +- [x] Support sparse fieldsets (encoding only). A client can likely just define a new model to represent a sparse population of another model in a very specific use case. On the server side, sparse fieldsets of Resource Objects can be encoded without creating one model for every possible sparse fieldset. - [ ] Create more descriptive errors that are easier to use for troubleshooting. ### Testing @@ -487,6 +490,31 @@ extension String: CreatableRawIdType { } ``` +### Sparse Fieldsets +Sparse Fieldsets are currently supported when encoding only. When decoding, Sparse Fieldsets become tricker to support under the current types this library uses and it is assumed that clients will request one or maybe two sparse fieldset combinations for any given model at most so it can simply define the `JSONAPI` models needed to decode those subsets of all possible fields. A server, on the other hand, likely needs to support arbitrary combinations of sparse fieldsets and this library provides a mechanism for encoding those sparse fieldsets without too much extra footwork. + +You can use sparse fieldsets on the primary resources(s) _and_ includes of a `JSONAPI.Document`. + +There is a sparse fieldsets example included with this repository as a Playground page. + +#### Supporting Sparse Fieldset Encoding +1. The `JSONAPI` `ResourceObjectDescription`'s `Attributes` struct must conform to `JSONAPI.SparsableAttributes` rather than `JSONAPI.Attributes`. +2. The `JSONAPI` `ResourceObjectDescription`'s `Attributes` struct must contain a `CodingKeys` enum that conforms to `JSONAPI.SparsableCodingKey` instead of `Swift.CodingKey`. +3. `typealiases` you may have created for `JSONAPI.Document` that allow you to decode Documents will not support the "encode-only" nature of sparse fieldsets. See the next section for `typealias` comparisons. +4. To create a sparse fieldset from a `ResourceObject` just call its `sparse(with: fields)` method and pass an array of `Attributes.CodingKeys` values you would like included in the encoding. +5. Initialize and encode a `Document` containing one or more sparse or full primary resource(s) and any number of sparse or full includes. + +#### Sparse Fieldset `typealias` comparisons +You might have found a `typealias` like the following for encoding/decoding `JSONAPI.Document`s (note the primary resource body is a `JSONAPI.ResourceBody`): +```swift +typealias Document = JSONAPI.Document +``` + +In order to support sparse fieldsets (which are encode-only), the following companion `typealias` would be useful (note the primary resource body is a `JSONAPI.EncodableResourceBody`): +```swift +typealias SparseDocument = JSONAPI.Document +``` + ### Custom Attribute or Relationship Key Mapping There is not anything special going on at the `JSONAPI.Attributes` and `JSONAPI.Relationships` levels, so you can easily provide custom key mappings by taking advantage of `Codable`'s `CodingKeys` pattern. Here are two models that will encode/decode equivalently but offer different naming in your codebase: ```swift From e23ec090ed090ce66828a63c75d6e71d044ea19a Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Mon, 5 Aug 2019 19:44:09 -0700 Subject: [PATCH 18/26] Starting to add SparseFieldEncoder tests --- .../SparseFields/SparseFieldEncoder.swift | 6 - .../SparseFieldEncoderTests.swift | 181 +++++++++++++++++- 2 files changed, 179 insertions(+), 8 deletions(-) diff --git a/Sources/JSONAPI/SparseFields/SparseFieldEncoder.swift b/Sources/JSONAPI/SparseFields/SparseFieldEncoder.swift index cbdcb33..e5985c8 100644 --- a/Sources/JSONAPI/SparseFields/SparseFieldEncoder.swift +++ b/Sources/JSONAPI/SparseFields/SparseFieldEncoder.swift @@ -28,12 +28,6 @@ public class SparseFieldEncoder: Encoder { return KeyedEncodingContainer(container) } - public func container(keyedBy type: SparseKey.Type) -> KeyedEncodingContainer { - let container = SparseFieldKeyedEncodingContainer(wrapping: wrappedEncoder.container(keyedBy: type), - encoding: allowedKeys) - return KeyedEncodingContainer(container) - } - public func unkeyedContainer() -> UnkeyedEncodingContainer { return wrappedEncoder.unkeyedContainer() } diff --git a/Tests/JSONAPITests/SparseFields/SparseFieldEncoderTests.swift b/Tests/JSONAPITests/SparseFields/SparseFieldEncoderTests.swift index a9790ff..e6b3a9d 100644 --- a/Tests/JSONAPITests/SparseFields/SparseFieldEncoderTests.swift +++ b/Tests/JSONAPITests/SparseFields/SparseFieldEncoderTests.swift @@ -7,9 +7,186 @@ import XCTest import JSONAPI +import Foundation class SparseFieldEncoderTests: XCTestCase { - func test_placeholder() { - // TODO: write tests + func test_AccurateCodingPath() { + let encoder = JSONEncoder() + XCTAssertThrowsError(try encoder.encode(Wrapper())) + + do { + let _ = try encoder.encode(Wrapper()) + } catch let err as Wrapper.OuterFail.FailError { + print(err.path) + XCTAssertEqual(err.path.first as? Wrapper.OuterFail.CodingKeys, Wrapper.OuterFail.CodingKeys.inner) + } catch { + XCTFail("received unexpected error during test") + } + } + + func test_SkipsOmittedFields() { + let encoder = JSONEncoder() + + // does not throw because we omit the field that would have failed + XCTAssertNoThrow(try encoder.encode(Wrapper(fields: []))) + } + + func test_EverythingArsenal_allOn() { + let encoder = JSONEncoder() + + let allThingsOn = try! encoder.encode(EverythingWrapper(fields: EverythingWrapper.EverythingWrapped.CodingKeys.allCases)) + + let allThingsOnDeserialized = try! JSONSerialization.jsonObject(with: allThingsOn, + options: []) as! [String: Any] + + XCTAssertNil(allThingsOnDeserialized["omittable"]) + XCTAssertNotNil(allThingsOnDeserialized["nullable"] as? NSNull) + XCTAssertEqual(allThingsOnDeserialized["bool"] as? Bool, true) + XCTAssertEqual(allThingsOnDeserialized["double"] as? Double, 10.5) + XCTAssertEqual(allThingsOnDeserialized["string"] as? String, "hello") + XCTAssertEqual(allThingsOnDeserialized["float"] as? Float, 1.2) + XCTAssertEqual(allThingsOnDeserialized["int"] as? Int, 3) + XCTAssertEqual(allThingsOnDeserialized["int8"] as? Int8, 4) + XCTAssertEqual(allThingsOnDeserialized["int16"] as? Int16, 5) + XCTAssertEqual(allThingsOnDeserialized["int32"] as? Int32, 6) + XCTAssertEqual(allThingsOnDeserialized["int64"] as? Int64, 7) + XCTAssertEqual(allThingsOnDeserialized["uint"] as? UInt, 8) + XCTAssertEqual(allThingsOnDeserialized["uint8"] as? UInt8, 9) + XCTAssertEqual(allThingsOnDeserialized["uint16"] as? UInt16, 10) + XCTAssertEqual(allThingsOnDeserialized["uint32"] as? UInt32, 11) + XCTAssertEqual(allThingsOnDeserialized["uint64"] as? UInt64, 12) + XCTAssertEqual(allThingsOnDeserialized["nested"] as? String, "world") + } + + func test_EverythingArsenal_allOff() { + let encoder = JSONEncoder() + + let allThingsOn = try! encoder.encode(EverythingWrapper(fields: [])) + + let allThingsOnDeserialized = try! JSONSerialization.jsonObject(with: allThingsOn, + options: []) as! [String: Any] + + XCTAssertNil(allThingsOnDeserialized["omittable"]) + XCTAssertNil(allThingsOnDeserialized["nullable"]) + XCTAssertNil(allThingsOnDeserialized["bool"]) + XCTAssertNil(allThingsOnDeserialized["double"]) + XCTAssertNil(allThingsOnDeserialized["string"]) + XCTAssertNil(allThingsOnDeserialized["float"]) + XCTAssertNil(allThingsOnDeserialized["int"]) + XCTAssertNil(allThingsOnDeserialized["int8"]) + XCTAssertNil(allThingsOnDeserialized["int16"]) + XCTAssertNil(allThingsOnDeserialized["int32"]) + XCTAssertNil(allThingsOnDeserialized["int64"]) + XCTAssertNil(allThingsOnDeserialized["uint"]) + XCTAssertNil(allThingsOnDeserialized["uint8"]) + XCTAssertNil(allThingsOnDeserialized["uint16"]) + XCTAssertNil(allThingsOnDeserialized["uint32"]) + XCTAssertNil(allThingsOnDeserialized["uint64"]) + XCTAssertNil(allThingsOnDeserialized["nested"]) + XCTAssertEqual(allThingsOnDeserialized.count, 0) + } +} + +extension SparseFieldEncoderTests { + struct Wrapper: Encodable { + + let fields: [OuterFail.CodingKeys] + + init(fields: [OuterFail.CodingKeys] = OuterFail.CodingKeys.allCases) { + self.fields = fields + } + + func encode(to encoder: Encoder) throws { + let sparseEncoder = SparseFieldEncoder(wrapping: encoder, + encoding: fields) + try OuterFail(inner: .init()).encode(to: sparseEncoder) + } + + struct OuterFail: Encodable { + let inner: InnerFail + + public enum CodingKeys: String, Equatable, CaseIterable, CodingKey { + case inner + } + + struct InnerFail: Encodable { + func encode(to encoder: Encoder) throws { + + throw FailError(path: encoder.codingPath) + } + } + + struct FailError: Swift.Error { + let path: [CodingKey] + } + } + } + + struct EverythingWrapper: Encodable { + let fields: [EverythingWrapped.CodingKeys] + + func encode(to encoder: Encoder) throws { + let sparseEncoder = SparseFieldEncoder(wrapping: encoder, + encoding: fields) + + try EverythingWrapped(omittable: nil, + nullable: .init(value: nil), + bool: true, + double: 10.5, + string: "hello", + float: 1.2, + int: 3, + int8: 4, + int16: 5, + int32: 6, + int64: 7, + uint: 8, + uint8: 9, + uint16: 10, + uint32: 11, + uint64: 12, + nested: .init(value: "world")) + .encode(to: sparseEncoder) + } + + struct EverythingWrapped: Encodable { + let omittable: Int? + let nullable: Attribute + let bool: Bool + let double: Double + let string: String + let float: Float + let int: Int + let int8: Int8 + let int16: Int16 + let int32: Int32 + let int64: Int64 + let uint: UInt + let uint8: UInt8 + let uint16: UInt16 + let uint32: UInt32 + let uint64: UInt64 + let nested: Attribute + + enum CodingKeys: String, Equatable, CaseIterable, CodingKey { + case omittable + case nullable + case bool + case double + case string + case float + case int + case int8 + case int16 + case int32 + case int64 + case uint + case uint8 + case uint16 + case uint32 + case uint64 + case nested + } + } } } From 6ba217f5531374cec2c9daec50553ff6fd2c6c29 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Tue, 6 Aug 2019 08:27:22 -0700 Subject: [PATCH 19/26] More sparse field encoder tests --- .../SparseFields/SparseFieldEncoder.swift | 21 ++- .../SparseFieldEncoderTests.swift | 158 ++++++++++++++++++ 2 files changed, 174 insertions(+), 5 deletions(-) diff --git a/Sources/JSONAPI/SparseFields/SparseFieldEncoder.swift b/Sources/JSONAPI/SparseFields/SparseFieldEncoder.swift index e5985c8..a377fc9 100644 --- a/Sources/JSONAPI/SparseFields/SparseFieldEncoder.swift +++ b/Sources/JSONAPI/SparseFields/SparseFieldEncoder.swift @@ -50,7 +50,8 @@ public struct SparseFieldKeyedEncodingContainer: KeyedEncodingCo self.allowedKeys = allowedKeys } - private func shouldAllow(key: Key) -> Bool { + /// Ask the container whether the given key should be encoded. + public func shouldAllow(key: Key) -> Bool { if let key = key as? SparseKey { return allowedKeys.contains(key) } @@ -157,6 +158,9 @@ public struct SparseFieldKeyedEncodingContainer: KeyedEncodingCo forKey key: Key) -> KeyedEncodingContainer where NestedKey : CodingKey { guard shouldAllow(key: key) else { return KeyedEncodingContainer( + // TODO: not needed by JSONAPI library, but for completeness could + // add an EmptyObjectEncoder that can be returned here so that + // at least nothing gets encoded within the nested container SparseFieldKeyedEncodingContainer(wrapping: wrappedContainer.nestedContainer(keyedBy: keyType, forKey: key), encoding: []) @@ -172,7 +176,9 @@ public struct SparseFieldKeyedEncodingContainer: KeyedEncodingCo public mutating func nestedUnkeyedContainer(forKey key: Key) -> UnkeyedEncodingContainer { guard shouldAllow(key: key) else { - // TODO: Seems like this might not work as expected... maybe need an empty unkeyed container + // TODO: not needed by JSONAPI library, but for completeness could + // add an EmptyObjectEncoder that can be returned here so that + // at least nothing gets encoded within the nested container return wrappedContainer.nestedUnkeyedContainer(forKey: key) } @@ -180,14 +186,19 @@ public struct SparseFieldKeyedEncodingContainer: KeyedEncodingCo } public mutating func superEncoder() -> Encoder { - return wrappedContainer.superEncoder() + return SparseFieldEncoder(wrapping: wrappedContainer.superEncoder(), + encoding: allowedKeys) } public mutating func superEncoder(forKey key: Key) -> Encoder { guard shouldAllow(key: key) else { - return SparseFieldEncoder(wrapping: wrappedContainer.superEncoder(forKey: key), encoding: [SparseKey]()) + // NOTE: We are creating a sparse field encoder with no allowed keys + // here because the given key should not be allowed. + return SparseFieldEncoder(wrapping: wrappedContainer.superEncoder(forKey: key), + encoding: [SparseKey]()) } - return SparseFieldEncoder(wrapping: wrappedContainer.superEncoder(forKey: key), encoding: allowedKeys) + return SparseFieldEncoder(wrapping: wrappedContainer.superEncoder(forKey: key), + encoding: allowedKeys) } } diff --git a/Tests/JSONAPITests/SparseFields/SparseFieldEncoderTests.swift b/Tests/JSONAPITests/SparseFields/SparseFieldEncoderTests.swift index e6b3a9d..f0abce2 100644 --- a/Tests/JSONAPITests/SparseFields/SparseFieldEncoderTests.swift +++ b/Tests/JSONAPITests/SparseFields/SparseFieldEncoderTests.swift @@ -85,6 +85,73 @@ class SparseFieldEncoderTests: XCTestCase { XCTAssertNil(allThingsOnDeserialized["nested"]) XCTAssertEqual(allThingsOnDeserialized.count, 0) } + + func test_NilEncode() { + let encoder = JSONEncoder() + + let nilOn = try! encoder.encode(NilWrapper(fields: [.hello])) + let nilOff = try! encoder.encode(NilWrapper(fields: [])) + + let nilOnDeserialized = try! JSONSerialization.jsonObject(with: nilOn, + options: []) as! [String: Any] + + let nilOffDeserialized = try! JSONSerialization.jsonObject(with: nilOff, + options: []) as! [String: Any] + + XCTAssertEqual(nilOnDeserialized.count, 1) + XCTAssertNotNil(nilOnDeserialized["hello"] as? NSNull) + XCTAssertEqual(nilOffDeserialized.count, 0) + } + + func test_NestedContainers() { + let encoder = JSONEncoder() + + let nestedOn = try! encoder.encode(NestedWrapper(fields: [.hello, .world])) + let nestedOff = try! encoder.encode(NestedWrapper(fields: [])) + + let nestedOnDeserialized = try! JSONSerialization.jsonObject(with: nestedOn, + options: []) as! [String: Any] + let nestedOffDeserialized = try! JSONSerialization.jsonObject(with: nestedOff, + options: []) as! [String: Any] + + XCTAssertEqual(nestedOnDeserialized.count, 2) + XCTAssertEqual((nestedOnDeserialized["hello"] as? [String: Bool])?["nestedKey"], true) + XCTAssertEqual((nestedOnDeserialized["world"] as? [Bool])?.first, false) + + // NOTE: When a nested container is explicitly requested, + // the best we can do to omit the field is to encode + // nothing _within_ the nested container. + XCTAssertEqual(nestedOffDeserialized.count, 2) + // TODO: The container currently does not encode empty object + // for the keyed nested container but I think it should. + XCTAssertEqual((nestedOffDeserialized["hello"] as? [String: Bool])?.count, 1) + // TODO: The container currently does not encode empty array + // for the unkeyed nested container but I think it should. + XCTAssertEqual((nestedOffDeserialized["world"] as? [Bool])?.count, 1) + } + + func test_SuperEncoderIsStillSparse() { + let encoder = JSONEncoder() + + let superOn = try! encoder.encode(SuperWrapper(fields: [.hello, .world])) + let superOff = try! encoder.encode(SuperWrapper(fields: [])) + + let superOnDeserialized = try! JSONSerialization.jsonObject(with: superOn, + options: []) as! [String: Any] + let superOffDeserialized = try! JSONSerialization.jsonObject(with: superOff, + options: []) as! [String: Any] + + XCTAssertEqual(superOnDeserialized.count, 2) + XCTAssertEqual((superOnDeserialized["hello"] as? [String: Bool])?["hello"], true) + XCTAssertEqual((superOnDeserialized["super"] as? [String: Bool])?["world"], false) + + // NOTE: When explicitly requesting a super encoder + // the best we can do is tell the super encoder only + // to encode the same keys + XCTAssertEqual(superOffDeserialized.count, 2) + XCTAssertEqual((superOffDeserialized["hello"] as? [String: Bool])?.count, 0) + XCTAssertEqual((superOffDeserialized["super"] as? [String: Bool])?.count, 0) + } } extension SparseFieldEncoderTests { @@ -189,4 +256,95 @@ extension SparseFieldEncoderTests { } } } + + struct NilWrapper: Encodable { + let fields: [NilWrapped.CodingKeys] + + func encode(to encoder: Encoder) throws { + let sparseEncoder = SparseFieldEncoder(wrapping: encoder, + encoding: fields) + + try NilWrapped() + .encode(to: sparseEncoder) + } + + struct NilWrapped: Encodable { + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + try container.encodeNil(forKey: .hello) + } + + enum CodingKeys: String, Equatable, CodingKey { + case hello + } + } + } + + struct NestedWrapper: Encodable { + let fields: [NestedWrapped.CodingKeys] + + func encode(to encoder: Encoder) throws { + let sparseEncoder = SparseFieldEncoder(wrapping: encoder, + encoding: fields) + + try NestedWrapped() + .encode(to: sparseEncoder) + } + + struct NestedWrapped: Encodable { + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + var nestedContainer1 = container.nestedContainer(keyedBy: NestedKeys.self, forKey: .hello) + + var nestedContainer2 = container.nestedUnkeyedContainer(forKey: .world) + + try nestedContainer1.encode(true, forKey: .nestedKey) + try nestedContainer2.encode(false) + } + + enum CodingKeys: String, Equatable, CodingKey { + case hello + case world + } + + enum NestedKeys: String, CodingKey { + case nestedKey + } + } + } + + struct SuperWrapper: Encodable { + let fields: [SuperWrapped.CodingKeys] + + func encode(to encoder: Encoder) throws { + let sparseEncoder = SparseFieldEncoder(wrapping: encoder, + encoding: fields) + + try SuperWrapped() + .encode(to: sparseEncoder) + } + + struct SuperWrapped: Encodable { + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + let superEncoder1 = container.superEncoder(forKey: .hello) + + let superEncoder2 = container.superEncoder() + + var container1 = superEncoder1.container(keyedBy: CodingKeys.self) + var container2 = superEncoder2.container(keyedBy: CodingKeys.self) + + try container1.encode(true, forKey: .hello) + try container2.encode(false, forKey: .world) + } + + enum CodingKeys: String, Equatable, CodingKey { + case hello + case world + } + } + } } From 1df891ce06467c3ec78f05cd089bebf619939bd9 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Tue, 6 Aug 2019 08:55:49 -0700 Subject: [PATCH 20/26] Add test that uses JSONAPIDocument protocol in generic context --- .../JSONAPITests/Document/DocumentTests.swift | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/Tests/JSONAPITests/Document/DocumentTests.swift b/Tests/JSONAPITests/Document/DocumentTests.swift index 5f9c18b..be10f7c 100644 --- a/Tests/JSONAPITests/Document/DocumentTests.swift +++ b/Tests/JSONAPITests/Document/DocumentTests.swift @@ -11,6 +11,34 @@ import Poly class DocumentTests: XCTestCase { + func test_genericDocFunc() { + func test(_ doc: Doc) { + let _ = encoded(value: doc) + + XCTAssert(Doc.PrimaryResourceBody.self == NoResourceBody.self) + XCTAssert(Doc.MetaType.self == NoMetadata.self) + XCTAssert(Doc.LinksType.self == NoLinks.self) + XCTAssert(Doc.IncludeType.self == NoIncludes.self) + XCTAssert(Doc.APIDescription.self == NoAPIDescription.self) + XCTAssert(Doc.Error.self == UnknownJSONAPIError.self) + } + + test(JSONAPI.Document< + NoResourceBody, + NoMetadata, + NoLinks, + NoIncludes, + NoAPIDescription, + UnknownJSONAPIError + >( + apiDescription: .none, + body: .none, + includes: .none, + meta: .none, + links: .none + )) + } + func test_singleDocumentNull() { let document = decoded(type: Document, NoMetadata, NoLinks, NoIncludes, NoAPIDescription, UnknownJSONAPIError>.self, data: single_document_null) From d5b4aa70c7c0aa716e1241ffce944eb8b59008ef Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Tue, 6 Aug 2019 09:04:15 -0700 Subject: [PATCH 21/26] Update linuxmain --- Tests/JSONAPITests/XCTestManifests.swift | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/Tests/JSONAPITests/XCTestManifests.swift b/Tests/JSONAPITests/XCTestManifests.swift index dc100bd..97b52b0 100644 --- a/Tests/JSONAPITests/XCTestManifests.swift +++ b/Tests/JSONAPITests/XCTestManifests.swift @@ -77,6 +77,7 @@ extension DocumentTests { ("test_errorDocumentNoMeta_encode", test_errorDocumentNoMeta_encode), ("test_errorDocumentNoMetaWithAPIDescription", test_errorDocumentNoMetaWithAPIDescription), ("test_errorDocumentNoMetaWithAPIDescription_encode", test_errorDocumentNoMetaWithAPIDescription_encode), + ("test_genericDocFunc", test_genericDocFunc), ("test_manyDocumentNoIncludes", test_manyDocumentNoIncludes), ("test_manyDocumentNoIncludes_encode", test_manyDocumentNoIncludes_encode), ("test_manyDocumentNoIncludesWithAPIDescription", test_manyDocumentNoIncludesWithAPIDescription), @@ -441,7 +442,13 @@ extension SparseFieldEncoderTests { // `swift test --generate-linuxmain` // to regenerate. static let __allTests__SparseFieldEncoderTests = [ - ("test_placeholder", test_placeholder), + ("test_AccurateCodingPath", test_AccurateCodingPath), + ("test_EverythingArsenal_allOff", test_EverythingArsenal_allOff), + ("test_EverythingArsenal_allOn", test_EverythingArsenal_allOn), + ("test_NestedContainers", test_NestedContainers), + ("test_NilEncode", test_NilEncode), + ("test_SkipsOmittedFields", test_SkipsOmittedFields), + ("test_SuperEncoderIsStillSparse", test_SuperEncoderIsStillSparse), ] } From 9a0bba8d070aa7fee680fa477ab16070d6ff1cd9 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Tue, 6 Aug 2019 09:12:02 -0700 Subject: [PATCH 22/26] Rename AppendableResourceBody to Appendable. It is used in conjunction with ResourceBody but does not conform to it anymore. --- Sources/JSONAPI/Document/Document.swift | 4 ++-- Sources/JSONAPI/Document/ResourceBody.swift | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Sources/JSONAPI/Document/Document.swift b/Sources/JSONAPI/Document/Document.swift index 4034969..caad138 100644 --- a/Sources/JSONAPI/Document/Document.swift +++ b/Sources/JSONAPI/Document/Document.swift @@ -203,7 +203,7 @@ extension Document where IncludeType == NoIncludes, MetaType == NoMetadata, Link } */ -extension Document.Body.Data where PrimaryResourceBody: AppendableResourceBody { +extension Document.Body.Data where PrimaryResourceBody: Appendable { public func merging(_ other: Document.Body.Data, combiningMetaWith metaMerge: (MetaType, MetaType) -> MetaType, combiningLinksWith linksMerge: (LinksType, LinksType) -> LinksType) -> Document.Body.Data { @@ -214,7 +214,7 @@ extension Document.Body.Data where PrimaryResourceBody: AppendableResourceBody { } } -extension Document.Body.Data where PrimaryResourceBody: AppendableResourceBody, MetaType == NoMetadata, LinksType == NoLinks { +extension Document.Body.Data where PrimaryResourceBody: Appendable, MetaType == NoMetadata, LinksType == NoLinks { public func merging(_ other: Document.Body.Data) -> Document.Body.Data { return merging(other, combiningMetaWith: { _, _ in .none }, diff --git a/Sources/JSONAPI/Document/ResourceBody.swift b/Sources/JSONAPI/Document/ResourceBody.swift index 2eb1bb4..4dbac87 100644 --- a/Sources/JSONAPI/Document/ResourceBody.swift +++ b/Sources/JSONAPI/Document/ResourceBody.swift @@ -40,11 +40,11 @@ public protocol ResourceBody: Decodable, EncodableResourceBody {} /// A `ResourceBody` that has the ability to take on more primary /// resources by appending another similarly typed `ResourceBody`. -public protocol AppendableResourceBody { +public protocol Appendable { func appending(_ other: Self) -> Self } -public func +(_ left: R, right: R) -> R { +public func +(_ left: R, right: R) -> R { return left.appending(right) } @@ -56,7 +56,7 @@ public struct SingleResourceBody: EncodableResourceBody, AppendableResourceBody { +public struct ManyResourceBody: EncodableResourceBody, Appendable { public let values: [Entity] public init(resourceObjects: [Entity]) { From 453ce4b3a8a2b50fb541f8627096cb4d2739d0ba Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Tue, 6 Aug 2019 09:19:06 -0700 Subject: [PATCH 23/26] whitespace changes -- trying to switch from one whitespace standard to another in cohesive chunks to fit Xcode 11 default. --- .../JSONAPITests/Includes/IncludeTests.swift | 40 +++++++++---------- .../ResourceBody/ResourceBodyTests.swift | 16 ++++---- 2 files changed, 28 insertions(+), 28 deletions(-) diff --git a/Tests/JSONAPITests/Includes/IncludeTests.swift b/Tests/JSONAPITests/Includes/IncludeTests.swift index 4272083..1bca6f5 100644 --- a/Tests/JSONAPITests/Includes/IncludeTests.swift +++ b/Tests/JSONAPITests/Includes/IncludeTests.swift @@ -350,45 +350,45 @@ extension IncludedTests { // MARK: - Test types extension IncludedTests { - enum TestEntityType: ResourceObjectDescription { + enum TestEntityType: ResourceObjectDescription { - typealias Relationships = NoRelationships + typealias Relationships = NoRelationships - public static var jsonType: String { return "test_entity1" } + public static var jsonType: String { return "test_entity1" } - public struct Attributes: JSONAPI.SparsableAttributes { - let foo: Attribute - let bar: Attribute + public struct Attributes: JSONAPI.SparsableAttributes { + let foo: Attribute + let bar: Attribute public enum CodingKeys: String, Equatable, CodingKey { case foo case bar } - } - } + } + } - typealias TestEntity = BasicEntity + typealias TestEntity = BasicEntity - enum TestEntityType2: ResourceObjectDescription { + enum TestEntityType2: ResourceObjectDescription { - public static var jsonType: String { return "test_entity2" } + public static var jsonType: String { return "test_entity2" } - public struct Relationships: JSONAPI.Relationships { - let entity1: ToOneRelationship - } + public struct Relationships: JSONAPI.Relationships { + let entity1: ToOneRelationship + } - public struct Attributes: JSONAPI.SparsableAttributes { - let foo: Attribute - let bar: Attribute + public struct Attributes: JSONAPI.SparsableAttributes { + let foo: Attribute + let bar: Attribute public enum CodingKeys: String, Equatable, CodingKey { case foo case bar } - } - } + } + } - typealias TestEntity2 = BasicEntity + typealias TestEntity2 = BasicEntity enum TestEntityType3: ResourceObjectDescription { diff --git a/Tests/JSONAPITests/ResourceBody/ResourceBodyTests.swift b/Tests/JSONAPITests/ResourceBody/ResourceBodyTests.swift index 6b12256..ad4c3fb 100644 --- a/Tests/JSONAPITests/ResourceBody/ResourceBodyTests.swift +++ b/Tests/JSONAPITests/ResourceBody/ResourceBodyTests.swift @@ -191,19 +191,19 @@ extension ResourceBodyTests { extension ResourceBodyTests { - enum ArticleType: ResourceObjectDescription { - public static var jsonType: String { return "articles" } + enum ArticleType: ResourceObjectDescription { + public static var jsonType: String { return "articles" } - typealias Relationships = NoRelationships + typealias Relationships = NoRelationships - struct Attributes: JSONAPI.SparsableAttributes { - let title: Attribute + struct Attributes: JSONAPI.SparsableAttributes { + let title: Attribute public enum CodingKeys: String, Equatable, CodingKey { case title } - } - } + } + } - typealias Article = BasicEntity + typealias Article = BasicEntity } From 8f9ec11f270734309b3441c0eaf8d1206763fe9a Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Wed, 14 Aug 2019 08:57:57 -0700 Subject: [PATCH 24/26] Housekeeping in the README --- README.md | 58 ++++++++++++++++++++----------------------------------- 1 file changed, 21 insertions(+), 37 deletions(-) diff --git a/README.md b/README.md index 7a128bc..ff5baeb 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ A Swift package for encoding to- and decoding from **JSON API** compliant reques See the JSON API Spec here: https://jsonapi.org/format/ -:warning: Although I find the type-safety of this framework appealing, the Swift compiler currently has enough trouble with it that it can become difficult to reason about errors produced by small typos. Similarly, auto-complete fails to provide reasonable suggestions much of the time. If you get the code right, everything compiles, otherwise it can suck to figure out what is wrong. This is mostly a concern when creating resource objects in-code (servers and test suites must do this). Writing a client that uses this framework to ingest JSON API Compliant API responses is much less painful. :warning: +:warning: This library provides well-tested type safety when working with JSON:API 1.0, however the Swift compiler can sometimes have difficulty tracking down small typos when initializing `ResourceObjects`. Once the code is written correctly, it will compile, but tracking down the source of programmer errors can be an annoyance. This is mostly a concern when creating resource objects in-code (servers and test cases must do this). Writing a client that uses this framework to ingest JSON API Compliant API responses is much less painful. :warning: ## Table of Contents @@ -109,52 +109,34 @@ Note that Playground support for importing non-system Frameworks is still a bit ### JSON:API #### Document -- `data` - - [x] Encoding/Decoding -- `included` - - [x] Encoding/Decoding -- `errors` - - [x] Encoding/Decoding -- `meta` - - [x] Encoding/Decoding -- `jsonapi` (i.e. API Information) - - [x] Encoding/Decoding -- `links` - - [x] Encoding/Decoding +- [x] `data` +- [x] `included` +- [x] `errors` +- [x] `meta` +- [x] `jsonapi` (i.e. API Information) +- [x] `links` #### Resource Object -- `id` - - [x] Encoding/Decoding -- `type` - - [x] Encoding/Decoding -- `attributes` - - [x] Encoding/Decoding -- `relationships` - - [x] Encoding/Decoding -- `links` - - [x] Encoding/Decoding -- `meta` - - [x] Encoding/Decoding +- [x] `id` +- [x] `type` +- [x] `attributes` +- [x] `relationships` +- [x] `links` +- [x] `meta` #### Relationship Object -- `data` - - [x] Encoding/Decoding -- `links` - - [x] Encoding/Decoding -- `meta` - - [x] Encoding/Decoding +- [x] `data` +- [x] `links` +- [x] `meta` #### Links Object -- `href` - - [x] Encoding/Decoding -- `meta` - - [x] Encoding/Decoding +- [x] `href` +- [x] `meta` ### Misc - [x] Support transforms on `Attributes` values (e.g. to support different representations of `Date`) - [x] Support validation on `Attributes`. -- [x] Support sparse fieldsets (encoding only). A client can likely just define a new model to represent a sparse population of another model in a very specific use case. On the server side, sparse fieldsets of Resource Objects can be encoded without creating one model for every possible sparse fieldset. -- [ ] Create more descriptive errors that are easier to use for troubleshooting. +- [x] Support sparse fieldsets (encoding only). A client can likely just define a new model to represent a sparse population of another model in a very specific use case for decoding purposes. On the server side, sparse fieldsets of Resource Objects can be encoded without creating one model for every possible sparse fieldset. ### Testing #### Resource Object Validator @@ -163,6 +145,8 @@ Note that Playground support for importing non-system Frameworks is still a bit - [x] Only allow `ToManyRelationship` and `ToOneRelationship` within `Relationships` struct. ### Potential Improvements +These ideas could be implemented in future versions. + - [ ] (Maybe) Use `KeyPath` to specify `Includes` thus creating type safety around the relationship between a primary resource type and the types of included resources. - [ ] (Maybe) Replace `SingleResourceBody` and `ManyResourceBody` with support at the `Document` level to just interpret `PrimaryResource`, `PrimaryResource?`, or `[PrimaryResource]` as the same decoding/encoding strategies. - [ ] Support sideposting. JSONAPI spec might become opinionated in the future (https://github.com/json-api/json-api/pull/1197, https://github.com/json-api/json-api/issues/1215, https://github.com/json-api/json-api/issues/1216) but there is also an existing implementation to consider (https://jsonapi-suite.github.io/jsonapi_suite/ruby/writes/nested-writes). At this time, any sidepost implementation would be an awesome tertiary library to be used alongside the primary JSONAPI library. Maybe `JSONAPISideloading`. From 86a9345fdd2f85d67d6338045628664663d18346 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Wed, 14 Aug 2019 09:22:06 -0700 Subject: [PATCH 25/26] Adding some documentation and making SparseFieldEncoder internal because it does not need to be public. --- Sources/JSONAPI/Document/Document.swift | 4 ++++ Sources/JSONAPI/Document/Includes.swift | 10 ++++++++++ Sources/JSONAPI/Document/ResourceBody.swift | 4 ++++ Sources/JSONAPI/Resource/Id.swift | 3 +++ Sources/JSONAPI/SparseFields/SparseFieldEncoder.swift | 4 ++-- .../SparseFields/SparseFieldEncoderTests.swift | 2 +- 6 files changed, 24 insertions(+), 3 deletions(-) diff --git a/Sources/JSONAPI/Document/Document.swift b/Sources/JSONAPI/Document/Document.swift index caad138..aedb87a 100644 --- a/Sources/JSONAPI/Document/Document.swift +++ b/Sources/JSONAPI/Document/Document.swift @@ -53,7 +53,9 @@ public struct Document public let meta: MetaType public let links: LinksType @@ -66,6 +68,8 @@ public struct Document> = ...` +/// +/// then you can access all `Thing1` included resources with +/// +/// `let includedThings = includes[Thing1.self]` public struct Includes: Encodable, Equatable { public static var none: Includes { return .init(values: []) } diff --git a/Sources/JSONAPI/Document/ResourceBody.swift b/Sources/JSONAPI/Document/ResourceBody.swift index 4dbac87..3fdef10 100644 --- a/Sources/JSONAPI/Document/ResourceBody.swift +++ b/Sources/JSONAPI/Document/ResourceBody.swift @@ -48,6 +48,9 @@ public func +(_ left: R, right: R) -> R { return left.appending(right) } +/// A type allowing for a document body containing 1 primary resource. +/// If the `Entity` specialization is an `Optional` type, the body can contain +/// 0 or 1 primary resources. public struct SingleResourceBody: EncodableResourceBody { public let value: Entity @@ -56,6 +59,7 @@ public struct SingleResourceBody: EncodableResourceBody, Appendable { public let values: [Entity] diff --git a/Sources/JSONAPI/Resource/Id.swift b/Sources/JSONAPI/Resource/Id.swift index 7265a18..a375c08 100644 --- a/Sources/JSONAPI/Resource/Id.swift +++ b/Sources/JSONAPI/Resource/Id.swift @@ -28,6 +28,9 @@ public protocol CreatableRawIdType: RawIdType { extension String: RawIdType {} +/// A type that can be used as the `MaybeRawId` for a `ResourceObject` that does not +/// have an Id (most likely because it was created by a client and the server will be responsible +/// for assigning it an Id). public struct Unidentified: MaybeRawId, CustomStringConvertible { public init() {} diff --git a/Sources/JSONAPI/SparseFields/SparseFieldEncoder.swift b/Sources/JSONAPI/SparseFields/SparseFieldEncoder.swift index a377fc9..36a1a95 100644 --- a/Sources/JSONAPI/SparseFields/SparseFieldEncoder.swift +++ b/Sources/JSONAPI/SparseFields/SparseFieldEncoder.swift @@ -5,7 +5,7 @@ // Created by Mathew Polzin on 8/4/19. // -public class SparseFieldEncoder: Encoder { +class SparseFieldEncoder: Encoder { private let wrappedEncoder: Encoder private let allowedKeys: [SparseKey] @@ -37,7 +37,7 @@ public class SparseFieldEncoder: Encoder { } } -public struct SparseFieldKeyedEncodingContainer: KeyedEncodingContainerProtocol where SparseKey: CodingKey, SparseKey: Equatable, Key: CodingKey { +struct SparseFieldKeyedEncodingContainer: KeyedEncodingContainerProtocol where SparseKey: CodingKey, SparseKey: Equatable, Key: CodingKey { private var wrappedContainer: KeyedEncodingContainer private let allowedKeys: [SparseKey] diff --git a/Tests/JSONAPITests/SparseFields/SparseFieldEncoderTests.swift b/Tests/JSONAPITests/SparseFields/SparseFieldEncoderTests.swift index f0abce2..c6192f8 100644 --- a/Tests/JSONAPITests/SparseFields/SparseFieldEncoderTests.swift +++ b/Tests/JSONAPITests/SparseFields/SparseFieldEncoderTests.swift @@ -6,7 +6,7 @@ // import XCTest -import JSONAPI +@testable import JSONAPI import Foundation class SparseFieldEncoderTests: XCTestCase { From 89217f7187e9118d988f921325015addcfb870bc Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Wed, 14 Aug 2019 17:31:43 -0700 Subject: [PATCH 26/26] bump podspec version --- JSONAPI.podspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/JSONAPI.podspec b/JSONAPI.podspec index d884b6c..615b1c1 100644 --- a/JSONAPI.podspec +++ b/JSONAPI.podspec @@ -16,7 +16,7 @@ Pod::Spec.new do |spec| # spec.name = "JSONAPI" - spec.version = "0.31.1" + spec.version = "1.0.0" spec.summary = "Swift Codable JSON API framework." # This description is used to generate tags and improve search results.