Add Sparse Fieldset support for Attributes

This commit is contained in:
Mathew Polzin
2019-08-04 18:44:28 -07:00
parent 17e2ce3138
commit 5aef44c3b3
4 changed files with 384 additions and 2 deletions
@@ -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 {
@@ -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<Description, MetaType, LinksType, EntityRawIdType>
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)
}
}
@@ -0,0 +1,199 @@
//
// SparseEncoder.swift
//
//
// Created by Mathew Polzin on 8/4/19.
//
public class SparseFieldEncoder<SparseKey: CodingKey & Equatable>: 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<Key>(keyedBy type: Key.Type) -> KeyedEncodingContainer<Key> where Key : CodingKey {
let container = SparseFieldKeyedEncodingContainer(wrapping: wrappedEncoder.container(keyedBy: type),
encoding: allowedKeys)
return KeyedEncodingContainer(container)
}
public func container(keyedBy type: SparseKey.Type) -> KeyedEncodingContainer<SparseKey> {
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<Key, SparseKey>: KeyedEncodingContainerProtocol where SparseKey: CodingKey, SparseKey: Equatable, Key: CodingKey {
private var wrappedContainer: KeyedEncodingContainer<Key>
private let allowedKeys: [SparseKey]
public var codingPath: [CodingKey] {
return wrappedContainer.codingPath
}
public init(wrapping container: KeyedEncodingContainer<Key>, 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<T>(_ 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<NestedKey>(keyedBy keyType: NestedKey.Type,
forKey key: Key) -> KeyedEncodingContainer<NestedKey> where NestedKey : CodingKey {
guard shouldAllow(key: key) else {
return KeyedEncodingContainer(
SparseFieldKeyedEncodingContainer<NestedKey, SparseKey>(wrapping: wrappedContainer.nestedContainer(keyedBy: keyType,
forKey: key),
encoding: [])
)
}
return KeyedEncodingContainer(
SparseFieldKeyedEncodingContainer<NestedKey, SparseKey>(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)
}
}
@@ -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<Bool>
let int: Attribute<Int>
let double: Attribute<Double>
let string: Attribute<String>
let nestedStruct: Attribute<NestedStruct>
let nestedEnum: Attribute<NestedEnum>
let array: Attribute<[Bool]>
let optional: Attribute<Bool>?
let nullable: Attribute<Bool?>
let optionalNullable: Attribute<Bool?>?
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<EverythingTestDescription, NoMetadata, NoLinks, String>
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)