mirror of
https://github.com/encounter/JSONAPI.git
synced 2026-03-30 11:18:38 -07:00
Add Sparse Fieldset support for Attributes
This commit is contained in:
@@ -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)
|
||||
Reference in New Issue
Block a user