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)