mirror of
https://github.com/encounter/JSONAPI.git
synced 2026-07-10 12:18:40 -07:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f8545128cf | |||
| c26d6d99c0 | |||
| 43dcc4fb12 | |||
| abae975f59 | |||
| 0e6b2a7771 |
@@ -5,15 +5,17 @@ A Swift package for encoding to- and decoding from **JSON API** compliant reques
|
||||
|
||||
See the JSON API Spec here: https://jsonapi.org/format/
|
||||
|
||||
: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:
|
||||
: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.
|
||||
|
||||
## Quick Start
|
||||
|
||||
:warning: The following Google Colab examples have correct code, but there appears to be an bug in the branch of the Swift compiler currently being used by the Google Colab Swift notebooks such that the `JSONAPI` package cannot be pulled in and you cannot run the examples in-browser.
|
||||
|
||||
### Clientside
|
||||
- [Basic Example](https://colab.research.google.com/drive/1IS7lRSBGoiW02Vd1nN_rfdDbZvTDj6Te)
|
||||
- [Compound Example](https://colab.research.google.com/drive/1BdF0Kc7l2ixDfBZEL16FY6palweDszQU)
|
||||
- [Metadata Example](https://colab.research.google.com/drive/10dEESwiE9I3YoyfzVeOVwOKUTEgLT3qr)
|
||||
- [Errors Example](https://colab.research.google.com/drive/1TIv6STzlHrkTf_-9Eu8sv8NoaxhZcFZH)
|
||||
- [Custom Errors Example](https://colab.research.google.com/drive/1TIv6STzlHrkTf_-9Eu8sv8NoaxhZcFZH)
|
||||
|
||||
### Serverside
|
||||
- [GET Example](https://colab.research.google.com/drive/1krbhzSfz8mwkBTQQnKUZJLEtYsJKSfYX)
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
//
|
||||
// TransformedAttribute.swift
|
||||
//
|
||||
// Created by Mathew Polzin on 9/30/19.
|
||||
//
|
||||
|
||||
public protocol AttrType {
|
||||
associatedtype SerializedType
|
||||
}
|
||||
|
||||
@propertyWrapper
|
||||
public struct Attr<Serializer, Deserializer, Value>: AttrType where Serializer: Transformer, Deserializer: Transformer, Serializer.From == Value, Deserializer.To == Value {
|
||||
|
||||
public typealias SerializedType = Value
|
||||
|
||||
private var _wrappedValue: Value?
|
||||
public var wrappedValue: Value {
|
||||
guard let ret = _wrappedValue else {
|
||||
fatalError("Accessed Transformed Value prior to initializing or decoding it.")
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
public init(wrappedValue: Value) {
|
||||
_wrappedValue = wrappedValue
|
||||
}
|
||||
|
||||
public init(wrappedValue: Value, serializer: Serializer.Type, deserializer: Deserializer.Type) {
|
||||
_wrappedValue = wrappedValue
|
||||
}
|
||||
|
||||
public init(serializer: Serializer.Type, deserializer: Deserializer.Type) {
|
||||
_wrappedValue = nil
|
||||
}
|
||||
}
|
||||
|
||||
extension Attr where
|
||||
Serializer == IdentityTransformer<Value>,
|
||||
Deserializer == IdentityTransformer<Value> {
|
||||
public init(wrappedValue: Value) {
|
||||
_wrappedValue = wrappedValue
|
||||
}
|
||||
|
||||
public init() {
|
||||
_wrappedValue = nil
|
||||
}
|
||||
}
|
||||
|
||||
extension Attr where
|
||||
Deserializer == IdentityTransformer<Value>,
|
||||
Serializer: Transformer, Serializer.From == Value {
|
||||
|
||||
public init(wrappedValue: Value, serializer: Serializer.Type) {
|
||||
_wrappedValue = wrappedValue
|
||||
}
|
||||
|
||||
public init(serializer: Serializer.Type) {
|
||||
_wrappedValue = nil
|
||||
}
|
||||
}
|
||||
|
||||
extension Attr where
|
||||
Serializer == IdentityTransformer<Value>,
|
||||
Deserializer: Transformer, Deserializer.To == Value {
|
||||
|
||||
public init(wrappedValue: Value, deserializer: Deserializer.Type) {
|
||||
_wrappedValue = wrappedValue
|
||||
}
|
||||
|
||||
public init(deserializer: Deserializer.Type) {
|
||||
_wrappedValue = nil
|
||||
}
|
||||
}
|
||||
|
||||
extension Attr: Encodable where
|
||||
Serializer: Transformer, Serializer.To: Encodable {
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.singleValueContainer()
|
||||
|
||||
try container.encode(Serializer.transform(wrappedValue))
|
||||
}
|
||||
}
|
||||
|
||||
extension Attr: Decodable where
|
||||
Deserializer: Transformer, Deserializer.From: Decodable {
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.singleValueContainer()
|
||||
|
||||
let anyNil: Any? = nil
|
||||
if container.decodeNil(),
|
||||
let val = anyNil as? Value {
|
||||
_wrappedValue = val
|
||||
} else {
|
||||
_wrappedValue = try Deserializer.transform(container.decode(Deserializer.From.self))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public protocol _Omittable {
|
||||
static var nilValue: Self { get }
|
||||
}
|
||||
|
||||
@propertyWrapper
|
||||
public struct Omittable<T>: AttrType, _Omittable {
|
||||
|
||||
public typealias SerializedType = T?
|
||||
|
||||
public let wrappedValue: T?
|
||||
|
||||
public init(wrappedValue: T?) {
|
||||
self.wrappedValue = wrappedValue
|
||||
}
|
||||
|
||||
public static var nilValue: Omittable<T> { return .init(wrappedValue: nil) }
|
||||
}
|
||||
|
||||
extension Omittable: Encodable where T: Encodable {
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.singleValueContainer()
|
||||
|
||||
if Optional(wrappedValue) != nil {
|
||||
try container.encode(wrappedValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Omittable: Decodable where T: Decodable {
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.singleValueContainer()
|
||||
|
||||
self = .init(wrappedValue: try container.decode(T.self))
|
||||
}
|
||||
}
|
||||
|
||||
extension KeyedDecodingContainer {
|
||||
public func decode<T>(_ type: T.Type, forKey key: KeyedDecodingContainer<K>.Key) throws -> T where T : Decodable, T: _Omittable {
|
||||
return try decodeIfPresent(T.self, forKey: key) ?? T.nilValue
|
||||
}
|
||||
}
|
||||
+6
@@ -29,6 +29,12 @@ public enum IdentityTransformer<T>: ReversibleTransformer {
|
||||
public static func reverse(_ value: T) throws -> T { return value }
|
||||
}
|
||||
|
||||
extension Optional: Transformer where Wrapped: Transformer {
|
||||
public static func transform(_ value: Wrapped.From?) throws -> Wrapped.To? {
|
||||
return try value.map { try Wrapped.transform($0) }
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Validator
|
||||
|
||||
/// A Validator is a Transformer that throws an error if an invalid value
|
||||
@@ -9,7 +9,74 @@ import XCTest
|
||||
import JSONAPI
|
||||
import JSONAPITesting
|
||||
|
||||
struct T1: Encodable {
|
||||
@Attr(serializer: IntToString.self)
|
||||
var x: Int
|
||||
|
||||
init(x: Int) {
|
||||
self._x = .init(wrappedValue: x)
|
||||
}
|
||||
}
|
||||
|
||||
struct T2: Decodable {
|
||||
@Attr(deserializer: IntToString.self)
|
||||
var y: String
|
||||
}
|
||||
|
||||
struct Tmp {
|
||||
@Attr(serializer: IntToString.self)
|
||||
var x: Int = 5
|
||||
|
||||
@Attr(deserializer: IntToString.self)
|
||||
var y: String = "2"
|
||||
}
|
||||
|
||||
struct T3: Codable {
|
||||
@Attr(serializer: StringToInt.self, deserializer: IntToString.self)
|
||||
var y: String = "1"
|
||||
}
|
||||
|
||||
struct T4: Encodable {
|
||||
@Attr(serializer: StringToInt?.self)
|
||||
var w: String?
|
||||
}
|
||||
|
||||
struct T5: Codable {
|
||||
@Attr(serializer: StringToInt?.self, deserializer: IntToString?.self)
|
||||
var x: String?
|
||||
}
|
||||
|
||||
struct T6: Decodable {
|
||||
@Omittable
|
||||
@Attr(deserializer: IntToString?.self)
|
||||
var y: String?
|
||||
}
|
||||
|
||||
class TransformerTests: XCTestCase {
|
||||
func test_tmp() {
|
||||
let t1 = T1(x: 2)
|
||||
print(encodable: t1)
|
||||
|
||||
let t3 = T3(y: "3")
|
||||
print(encodable: t3)
|
||||
|
||||
let t2 = decoded(type: T2.self, data: #"{"y":63}"#.data(using: .utf8)!)
|
||||
print(t2.y)
|
||||
|
||||
let t4 = T4(w: .init(wrappedValue: nil))
|
||||
let t4_2 = T4(w: .init(wrappedValue: "12"))
|
||||
print(encodable: t4)
|
||||
print(encodable: t4_2)
|
||||
|
||||
let t5 = decoded(type: T5.self, data: #"{"x":null}"#.data(using: .utf8)!)
|
||||
let t5_2 = decoded(type: T5.self, data: #"{"x":10}"#.data(using: .utf8)!)
|
||||
XCTAssertThrowsError(try JSONDecoder().decode(T5.self, from: #"{}"#.data(using: .utf8)!))
|
||||
print(t5.x)
|
||||
print(t5_2.x)
|
||||
|
||||
let t6 = decoded(type: T6.self, data: #"{}"#.data(using: .utf8)!)
|
||||
}
|
||||
|
||||
func testIdentityTransform() {
|
||||
let inString = "hello world"
|
||||
|
||||
@@ -49,3 +116,23 @@ enum MoreThanFiveCharValidator: Validator {
|
||||
case fewerThanFiveChars
|
||||
}
|
||||
}
|
||||
|
||||
enum StringToInt: Transformer {
|
||||
public static func transform(_ value: String) throws -> Int {
|
||||
let res = Int(value)
|
||||
guard let ret = res else {
|
||||
throw Error.nonIntegerString
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
enum Error: Swift.Error {
|
||||
case nonIntegerString
|
||||
}
|
||||
}
|
||||
|
||||
enum IntToString: Transformer {
|
||||
public static func transform(_ value: Int) throws -> String {
|
||||
return String(value)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
/// Print the UTF8 encoded String value of data produced by encoding the given object.
|
||||
func print<T: Encodable>(encodable: T) {
|
||||
print(String(data: try! JSONEncoder().encode(encodable), encoding: .utf8)!)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user