Compare commits

...

5 Commits

Author SHA1 Message Date
Mathew Polzin f8545128cf beginning again to add property wrappers but only to realize property wrapper composition did not land in Swift 5.1 I'll put this on hold again for now. 2019-10-01 00:32:30 -07:00
Mathew Polzin c26d6d99c0 Update README.md 2019-09-29 17:21:43 -07:00
Mathew Polzin 43dcc4fb12 Update README.md 2019-09-29 17:21:08 -07:00
Mathew Polzin abae975f59 Update README.md 2019-09-29 17:20:42 -07:00
Mathew Polzin 0e6b2a7771 Update README.md 2019-09-29 17:14:25 -07:00
7 changed files with 239 additions and 2 deletions
+4 -2
View File
@@ -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
}
}
@@ -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)!)
}