Compare commits

..

9 Commits

Author SHA1 Message Date
Mathew Polzin 099d17aa58 Update README.md
Removing check boxes for `Arbitrary` conformance and `OpenAPI` integration. Both of these "features" are maintained in separate libraries.
2019-07-03 07:28:31 -07:00
Mathew Polzin 92d1239a98 Merge pull request #19 from gufo/optional-relationships
Allow omitting `relationships` if all are optional
2019-07-03 07:19:14 -07:00
Janko Luin b7ce3d226e Apply EmptyObjectDecoder rather than empty JSON
Co-Authored-By: Mathew Polzin <matt.polzin@gmail.com>
2019-07-03 08:32:44 +02:00
Janko Luin 5b56d596e2 Merge remote-tracking branch 'upstream/master' into optional-relationships 2019-07-03 08:30:17 +02:00
Mathew Polzin e820f34253 change podspec version in anticipation of next release 2019-07-02 18:12:29 -07:00
Mathew Polzin c75912ab79 switch Poly versioning to 'upToNextMajor' and updated the resolved version. 2019-07-02 18:09:06 -07:00
Mathew Polzin e4481c9e4f noticed the README had some typos I missed when I changed Entity to ResourceObject. 2019-07-02 18:05:22 -07:00
Mathew Polzin e9b9a2bd78 Add Empty Object Decoder to be used in upcoming release. 2019-07-02 17:36:54 -07:00
Janko Luin fc78958f76 Allow omitting relationships if all are optional
When all relationships are optional, the `relationships` key is also
optional and not required in the structure.

I'm not super happy with importing Foundation and creating new objects
any time a key is missing, but ultimately none of my attempts at
conditional generics worked out for me.
2019-07-02 17:02:55 +02:00
11 changed files with 284 additions and 261 deletions
+1 -1
View File
@@ -16,7 +16,7 @@ Pod::Spec.new do |spec|
#
spec.name = "JSONAPI"
spec.version = "0.30.0"
spec.version = "0.31.0"
spec.summary = "Swift Codable JSON API framework."
# This description is used to generate tags and improve search results.
+2 -2
View File
@@ -6,8 +6,8 @@
"repositoryURL": "https://github.com/mattpolzin/Poly.git",
"state": {
"branch": null,
"revision": "d24d4c1214dd05f89eb1182a46592856dd0a0645",
"version": "2.0.0"
"revision": "38051821d7ef49e590e26e819a2fe447e50be9ff",
"version": "2.0.1"
}
}
]
+2 -2
View File
@@ -18,7 +18,7 @@ let package = Package(
targets: ["JSONAPITesting"])
],
dependencies: [
.package(url: "https://github.com/mattpolzin/Poly.git", from: "2.0.0"),
.package(url: "https://github.com/mattpolzin/Poly.git", .upToNextMajor(from: "2.0.0")),
],
targets: [
.target(
@@ -34,5 +34,5 @@ let package = Package(
name: "JSONAPITestingTests",
dependencies: ["JSONAPI", "JSONAPITesting"])
],
swiftLanguageVersions: [.version("5.1")]
swiftLanguageVersions: [.v5]
)
+13 -45
View File
@@ -108,75 +108,44 @@ Note that Playground support for importing non-system Frameworks is still a bit
#### Document
- `data`
- [x] Encoding/Decoding
- [x] Arbitrary
- [x] OpenAPI
- `included`
- [x] Encoding/Decoding
- [x] Arbitrary
- [x] OpenAPI
- `errors`
- [x] Encoding/Decoding
- [x] Arbitrary
- [ ] OpenAPI
- `meta`
- [x] Encoding/Decoding
- [x] Arbitrary
- [ ] OpenAPI
- `jsonapi` (i.e. API Information)
- [x] Encoding/Decoding
- [x] Arbitrary
- [ ] OpenAPI
- `links`
- [x] Encoding/Decoding
- [x] Arbitrary
- [ ] OpenAPI
#### Resource Object
- `id`
- [x] Encoding/Decoding
- [x] Arbitrary
- [x] OpenAPI
- `type`
- [x] Encoding/Decoding
- [x] OpenAPI
- `attributes`
- [x] Encoding/Decoding
- [x] OpenAPI
- `relationships`
- [x] Encoding/Decoding
- [x] OpenAPI
- `links`
- [x] Encoding/Decoding
- [x] Arbitrary
- [ ] OpenAPI
- `meta`
- [x] Encoding/Decoding
- [x] Arbitrary
- [ ] OpenAPI
#### Relationship Object
- `data`
- [x] Encoding/Decoding
- [x] Arbitrary
- [x] OpenAPI
- `links`
- [x] Encoding/Decoding
- [ ] Arbitrary
- [ ] OpenAPI
- `meta`
- [x] Encoding/Decoding
- [ ] Arbitrary
- [ ] OpenAPI
#### Links Object
- `href`
- [x] Encoding/Decoding
- [ ] Arbitrary
- [ ] OpenAPI
- `meta`
- [x] Encoding/Decoding
- [ ] Arbitrary
- [ ] OpenAPI
### Misc
- [x] Support transforms on `Attributes` values (e.g. to support different representations of `Date`)
@@ -194,7 +163,6 @@ Note that Playground support for importing non-system Frameworks is still a bit
- [ ] (Maybe) Use `KeyPath` to specify `Includes` thus creating type safety around the relationship between a primary resource type and the types of included resources.
- [ ] (Maybe) Replace `SingleResourceBody` and `ManyResourceBody` with support at the `Document` level to just interpret `PrimaryResource`, `PrimaryResource?`, or `[PrimaryResource]` as the same decoding/encoding strategies.
- [ ] Support sideposting. JSONAPI spec might become opinionated in the future (https://github.com/json-api/json-api/pull/1197, https://github.com/json-api/json-api/issues/1215, https://github.com/json-api/json-api/issues/1216) but there is also an existing implementation to consider (https://jsonapi-suite.github.io/jsonapi_suite/ruby/writes/nested-writes). At this time, any sidepost implementation would be an awesome tertiary library to be used alongside the primary JSONAPI library. Maybe `JSONAPISideloading`.
- [ ] Property-based testing (using `SwiftCheck`).
- [ ] Error or warning if an included resource object is not related to a primary resource object or another included resource object (Turned off or at least not throwing by default).
## Usage
@@ -203,7 +171,7 @@ In this documentation, in order to draw attention to the difference between the
### `JSONAPI.ResourceObjectDescription`
An `ResourceObjectDescription` is the `JSONAPI` framework's representation of what the **SPEC** calls a *Resource Object*. You might create the following `ResourceObjectDescription` to represent a person in a network of friends:
A `ResourceObjectDescription` is the `JSONAPI` framework's representation of what the **SPEC** calls a *Resource Object*. You might create the following `ResourceObjectDescription` to represent a person in a network of friends:
```swift
enum PersonDescription: IdentifiedResourceObjectDescription {
@@ -220,7 +188,7 @@ enum PersonDescription: IdentifiedResourceObjectDescription {
}
```
The requirements of an `ResourceObjectDescription` are:
The requirements of a `ResourceObjectDescription` are:
1. A static `var` "jsonType" that matches the JSON type; The **SPEC** requires every *Resource Object* to have a "type".
2. A `struct` of `Attributes` **- OR -** `typealias Attributes = NoAttributes`
3. A `struct` of `Relationships` **- OR -** `typealias Relationships = NoRelationships`
@@ -259,11 +227,11 @@ This readme doesn't go into detail on the **SPEC**, but the following *Resource
### `JSONAPI.ResourceObject`
Once you have an `ResourceObjectDescription`, you _create_, _encode_, and _decode_ `ResourceObjects` that "fit the description". If you have a `CreatableRawIdType` (see the section on `RawIdType`s below) then you can create new `ResourceObjects` that will automatically be given unique Ids, but even without a `CreatableRawIdType` you can encode, decode and work with resource objects.
Once you have a `ResourceObjectDescription`, you _create_, _encode_, and _decode_ `ResourceObjects` that "fit the description". If you have a `CreatableRawIdType` (see the section on `RawIdType`s below) then you can create new `ResourceObjects` that will automatically be given unique Ids, but even without a `CreatableRawIdType` you can encode, decode and work with resource objects.
The `ResourceObject` and `ResourceObjectDescription` together with a `JSONAPI.Meta` type and a `JSONAPI.Links` type embody the rules and properties of a JSON API *Resource Object*.
An `ResourceObject` needs to be specialized on four generic types. The first is the `ResourceObjectDescription` described above. The others are a `Meta`, `Links`, and `MaybeRawId`.
A `ResourceObject` needs to be specialized on four generic types. The first is the `ResourceObjectDescription` described above. The others are a `Meta`, `Links`, and `MaybeRawId`.
#### `Meta`
@@ -275,7 +243,7 @@ The third generic specialization on `ResourceObject` is `Links`. This is describ
#### `MaybeRawId`
The last generic specialization on `ResourceObject` is `MaybeRawId`. This is either a `RawIdType` that can be used to uniquely identify `ResourceObjects` or it is `Unidentified` which is used to indicate an `ResourceObject` does not have an `Id` (which is useful when a client is requesting that the server create an `ResourceObject` and assign it a new `Id`).
The last generic specialization on `ResourceObject` is `MaybeRawId`. This is either a `RawIdType` that can be used to uniquely identify `ResourceObjects` or it is `Unidentified` which is used to indicate a `ResourceObject` does not have an `Id` (which is useful when a client is requesting that the server create a `ResourceObject` and assign it a new `Id`).
##### `RawIdType`
@@ -283,7 +251,7 @@ The raw type of `Id` to use for the `ResourceObject`. The actual `Id` of the `Re
Having the `ResourceObject` type associated with the `Id` makes it easy to store all of your resource objects in a hash broken out by `ResourceObject` type; You can pass `Ids` around and always know where to look for the `ResourceObject` to which the `Id` refers. This encapsulation provides some type safety because the Ids of two `ResourceObjects` with the "raw ID" of `"1"` but different types will not compare as equal.
A `RawIdType` is the underlying type that uniquely identifies an `ResourceObject`. This is often a `String` or a `UUID`.
A `RawIdType` is the underlying type that uniquely identifies a `ResourceObject`. This is often a `String` or a `UUID`.
#### Convenient `typealiases`
@@ -305,7 +273,7 @@ Note that I am assuming an unidentified person is a "new" person. I suspect that
### `JSONAPI.Relationships`
There are two types of `Relationships`: `ToOneRelationship` and `ToManyRelationship`. An `ResourceObjectDescription`'s `Relationships` type can contain any number of `Relationship` properties of either of these types. Do not store anything other than `Relationship` properties in the `Relationships` struct of an `ResourceObjectDescription`.
There are two types of `Relationships`: `ToOneRelationship` and `ToManyRelationship`. A `ResourceObjectDescription`'s `Relationships` type can contain any number of `Relationship` properties of either of these types. Do not store anything other than `Relationship` properties in the `Relationships` struct of a `ResourceObjectDescription`.
In addition to identifying resource objects by Id and type, `Relationships` can contain `Meta` or `Links` that follow the same rules as [`Meta`](#jsonapimeta) and [`Links`](#jsonapilinks) elsewhere in the JSON API Document.
@@ -314,7 +282,7 @@ To describe a relationship that may be omitted (i.e. the key is not even present
let nullableRelative: ToOneRelationship<Person?, NoMetadata, NoLinks>
```
An resource object that does not have relationships can be described by adding the following to an `ResourceObjectDescription`:
A `ResourceObject` that does not have relationships can be described by adding the following to a `ResourceObjectDescription`:
```swift
typealias Relationships = NoRelationships
```
@@ -326,7 +294,7 @@ let friendIds: [Person.Identifier] = person ~> \.friends
### `JSONAPI.Attributes`
The `Attributes` of an `ResourceObjectDescription` can contain any JSON encodable/decodable types as long as they are wrapped in an `Attribute`, `ValidatedAttribute`, or `TransformedAttribute` `struct`.
The `Attributes` of a `ResourceObjectDescription` can contain any JSON encodable/decodable types as long as they are wrapped in an `Attribute`, `ValidatedAttribute`, or `TransformedAttribute` `struct`.
To describe an attribute that may be omitted (i.e. the key might not even be in the JSON object), you make the entire `Attribute` optional:
```swift
@@ -338,7 +306,7 @@ To describe an attribute that is expected to exist but might have a `null` value
let nullableAttribute: Attribute<String?>
```
An resource object that does not have attributes can be described by adding the following to an `ResourceObjectDescription`:
A resource object that does not have attributes can be described by adding the following to an `ResourceObjectDescription`:
```swift
typealias Attributes = NoAttributes
```
@@ -396,8 +364,8 @@ public var fullName: Attribute<String> {
If your computed property is wrapped in a `AttributeType` then you can still use the default subscript operator to access it (as would be the case with the `person[\.fullName]` example above). However, if you add a property to the `Attributes` `struct` that is not wrapped in an `AttributeType`, you must either access it from its full path (`person.attributes.newThing`) or with the "direct" subscript accessor (`person[direct: \.newThing]`). This keeps the subscript access unambiguous enough for the compiler to be helpful prior to explicitly casting, comparing, or storing the result.
### Copying `ResourceObjects`
`ResourceObject` is a value type, so copying is its default behavior. There are two common mutations you might want to make when copying an `ResourceObject`:
### Copying/Mutating `ResourceObjects`
`ResourceObject` is a value type, so copying is its default behavior. There are two common mutations you might want to make when copying a `ResourceObject`:
1. Assigning a new `Identifier` to the copy of an identified `ResourceObject`.
2. Assigning a new `Identifier` to the copy of an unidentified `ResourceObject`.
@@ -590,7 +558,7 @@ extension ResourceObjectDescription1.Attributes {
### Meta-Attributes
This advanced feature may not ever be useful, but if you find yourself in the situation of dealing with an API that does not 100% follow the **SPEC** then you might find meta-attributes are just the thing to make your resource objects more natural to work with.
Suppose, for example, you are presented with the unfortunate situation where a piece of information you need is only available as part of the `Id` of an resource object. Perhaps a user's `Id` is formatted "{integer}-{createdAt}" where "createdAt" is the unix timestamp when the user account was created. The following `UserDescription` will expose what you need as an attribute. Realistically, the following example code is still terrible for its error handling. Using a `Result` type and/or invariants would clean things up substantially.
Suppose, for example, you are presented with the unfortunate situation where a piece of information you need is only available as part of the `Id` of a resource object. Perhaps a user's `Id` is formatted "{integer}-{createdAt}" where "createdAt" is the unix timestamp when the user account was created. The following `UserDescription` will expose what you need as an attribute. Realistically, the following example code is still terrible for its error handling. Using a `Result` type and/or invariants would clean things up substantially.
```swift
enum UserDescription: ResourceObjectDescription {
+215
View File
@@ -0,0 +1,215 @@
//
// EmptyObjectDecoder.swift
// JSONAPI
//
// Created by Mathew Polzin on 7/2/19.
//
/// `EmptyObjectDecoder` exists internally for the sole purpose of
/// allowing certain fallback logic paths to attempt to create `Decodable`
/// types from empty containers (specifically in a way that is agnostic
/// of any given encoding). In other words, this serves the same purpose
/// as `JSONDecoder().decode(Thing.self, from: "{}".data(using: .utf8)!)`
/// without needing to use a third party or `Foundation` library decoder.
struct EmptyObjectDecoder: Decoder {
var codingPath: [CodingKey] = []
var userInfo: [CodingUserInfoKey : Any] = [:]
func container<Key>(keyedBy type: Key.Type) throws -> KeyedDecodingContainer<Key> where Key : CodingKey {
return KeyedDecodingContainer(EmptyKeyedContainer())
}
func unkeyedContainer() throws -> UnkeyedDecodingContainer {
return EmptyUnkeyedContainer()
}
func singleValueContainer() throws -> SingleValueDecodingContainer {
throw EmptyObjectDecodingError.emptyObjectCannotBeSingleValue
}
}
enum EmptyObjectDecodingError: Swift.Error {
case emptyObjectCannotBeSingleValue
case emptyObjectCannotBeUnkeyedValues
case emptyObjectCannotHaveKeyedValues
case emptyObjectCannotHaveNestedContainers
case emptyObjectCannotHaveSuper
}
struct EmptyUnkeyedContainer: UnkeyedDecodingContainer {
var codingPath: [CodingKey] { return [] }
var count: Int? { return 0 }
var isAtEnd: Bool { return true }
var currentIndex: Int { return 0 }
mutating func decodeNil() throws -> Bool {
throw EmptyObjectDecodingError.emptyObjectCannotBeUnkeyedValues
}
mutating func decode(_ type: Bool.Type) throws -> Bool {
throw EmptyObjectDecodingError.emptyObjectCannotBeUnkeyedValues
}
mutating func decode(_ type: String.Type) throws -> String {
throw EmptyObjectDecodingError.emptyObjectCannotBeUnkeyedValues
}
mutating func decode(_ type: Double.Type) throws -> Double {
throw EmptyObjectDecodingError.emptyObjectCannotBeUnkeyedValues
}
mutating func decode(_ type: Float.Type) throws -> Float {
throw EmptyObjectDecodingError.emptyObjectCannotBeUnkeyedValues
}
mutating func decode(_ type: Int.Type) throws -> Int {
throw EmptyObjectDecodingError.emptyObjectCannotBeUnkeyedValues
}
mutating func decode(_ type: Int8.Type) throws -> Int8 {
throw EmptyObjectDecodingError.emptyObjectCannotBeUnkeyedValues
}
mutating func decode(_ type: Int16.Type) throws -> Int16 {
throw EmptyObjectDecodingError.emptyObjectCannotBeUnkeyedValues
}
mutating func decode(_ type: Int32.Type) throws -> Int32 {
throw EmptyObjectDecodingError.emptyObjectCannotBeUnkeyedValues
}
mutating func decode(_ type: Int64.Type) throws -> Int64 {
throw EmptyObjectDecodingError.emptyObjectCannotBeUnkeyedValues
}
mutating func decode(_ type: UInt.Type) throws -> UInt {
throw EmptyObjectDecodingError.emptyObjectCannotBeUnkeyedValues
}
mutating func decode(_ type: UInt8.Type) throws -> UInt8 {
throw EmptyObjectDecodingError.emptyObjectCannotBeUnkeyedValues
}
mutating func decode(_ type: UInt16.Type) throws -> UInt16 {
throw EmptyObjectDecodingError.emptyObjectCannotBeUnkeyedValues
}
mutating func decode(_ type: UInt32.Type) throws -> UInt32 {
throw EmptyObjectDecodingError.emptyObjectCannotBeUnkeyedValues
}
mutating func decode(_ type: UInt64.Type) throws -> UInt64 {
throw EmptyObjectDecodingError.emptyObjectCannotBeUnkeyedValues
}
mutating func decode<T>(_ type: T.Type) throws -> T where T : Decodable {
throw EmptyObjectDecodingError.emptyObjectCannotBeUnkeyedValues
}
mutating func nestedContainer<NestedKey>(keyedBy type: NestedKey.Type) throws -> KeyedDecodingContainer<NestedKey> where NestedKey : CodingKey {
throw EmptyObjectDecodingError.emptyObjectCannotHaveNestedContainers
}
mutating func nestedUnkeyedContainer() throws -> UnkeyedDecodingContainer {
throw EmptyObjectDecodingError.emptyObjectCannotHaveNestedContainers
}
mutating func superDecoder() throws -> Decoder {
throw EmptyObjectDecodingError.emptyObjectCannotHaveSuper
}
}
struct EmptyKeyedContainer<Key: CodingKey>: KeyedDecodingContainerProtocol {
var codingPath: [CodingKey] { return [] }
var allKeys: [Key] { return [] }
func contains(_ key: Key) -> Bool {
return false
}
func decodeNil(forKey key: Key) throws -> Bool {
throw EmptyObjectDecodingError.emptyObjectCannotHaveKeyedValues
}
func decode(_ type: Bool.Type, forKey key: Key) throws -> Bool {
throw EmptyObjectDecodingError.emptyObjectCannotHaveKeyedValues
}
func decode(_ type: String.Type, forKey key: Key) throws -> String {
throw EmptyObjectDecodingError.emptyObjectCannotHaveKeyedValues
}
func decode(_ type: Double.Type, forKey key: Key) throws -> Double {
throw EmptyObjectDecodingError.emptyObjectCannotHaveKeyedValues
}
func decode(_ type: Float.Type, forKey key: Key) throws -> Float {
throw EmptyObjectDecodingError.emptyObjectCannotHaveKeyedValues
}
func decode(_ type: Int.Type, forKey key: Key) throws -> Int {
throw EmptyObjectDecodingError.emptyObjectCannotHaveKeyedValues
}
func decode(_ type: Int8.Type, forKey key: Key) throws -> Int8 {
throw EmptyObjectDecodingError.emptyObjectCannotHaveKeyedValues
}
func decode(_ type: Int16.Type, forKey key: Key) throws -> Int16 {
throw EmptyObjectDecodingError.emptyObjectCannotHaveKeyedValues
}
func decode(_ type: Int32.Type, forKey key: Key) throws -> Int32 {
throw EmptyObjectDecodingError.emptyObjectCannotHaveKeyedValues
}
func decode(_ type: Int64.Type, forKey key: Key) throws -> Int64 {
throw EmptyObjectDecodingError.emptyObjectCannotHaveKeyedValues
}
func decode(_ type: UInt.Type, forKey key: Key) throws -> UInt {
throw EmptyObjectDecodingError.emptyObjectCannotHaveKeyedValues
}
func decode(_ type: UInt8.Type, forKey key: Key) throws -> UInt8 {
throw EmptyObjectDecodingError.emptyObjectCannotHaveKeyedValues
}
func decode(_ type: UInt16.Type, forKey key: Key) throws -> UInt16 {
throw EmptyObjectDecodingError.emptyObjectCannotHaveKeyedValues
}
func decode(_ type: UInt32.Type, forKey key: Key) throws -> UInt32 {
throw EmptyObjectDecodingError.emptyObjectCannotHaveKeyedValues
}
func decode(_ type: UInt64.Type, forKey key: Key) throws -> UInt64 {
throw EmptyObjectDecodingError.emptyObjectCannotHaveKeyedValues
}
func decode<T>(_ type: T.Type, forKey key: Key) throws -> T where T : Decodable {
throw EmptyObjectDecodingError.emptyObjectCannotHaveKeyedValues
}
func nestedContainer<NestedKey>(keyedBy type: NestedKey.Type, forKey key: Key) throws -> KeyedDecodingContainer<NestedKey> where NestedKey : CodingKey {
throw EmptyObjectDecodingError.emptyObjectCannotHaveNestedContainers
}
func nestedUnkeyedContainer(forKey key: Key) throws -> UnkeyedDecodingContainer {
throw EmptyObjectDecodingError.emptyObjectCannotHaveNestedContainers
}
func superDecoder() throws -> Decoder {
throw EmptyObjectDecodingError.emptyObjectCannotHaveSuper
}
func superDecoder(forKey key: Key) throws -> Decoder {
throw EmptyObjectDecodingError.emptyObjectCannotHaveSuper
}
}
+2 -2
View File
@@ -17,9 +17,9 @@ public protocol AttributeType: Codable {
/// A TransformedAttribute takes a Codable type and attempts to turn it into another type.
public struct TransformedAttribute<RawValue: Codable, Transformer: JSONAPI.Transformer>: AttributeType where Transformer.From == RawValue {
public let rawValue: RawValue
public let value: Transformer.To
public init(rawValue: RawValue) throws {
self.rawValue = rawValue
value = try Transformer.transform(rawValue)
@@ -1,115 +0,0 @@
//
// PropertyWrappers.swift
//
//
// Created by Mathew Polzin on 6/20/19.
//
// MARK: - Transformed
@propertyWrapper
public struct Transformed<Transformer: JSONAPI.Transformer> {
public typealias RawValue = Transformer.From
public typealias Value = Transformer.To
private var _value: Value?
public var wrappedValue: Value {
get {
guard let ret = _value else {
fatalError("Attribute read from before initialization.")
}
return ret
}
set {
_value = newValue
}
}
public init(initialValue: Value, _ transformer: Transformer.Type) {
self._value = initialValue
}
public init(_ transformer: Transformer.Type) {
self._value = nil
}
public init(rawValue: RawValue, _ transformer: Transformer.Type) throws {
self._value = try Transformer.transform(rawValue)
}
}
extension Transformed: Decodable where Transformer.From: Decodable {
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let rawVal = try container.decode(Transformer.From.self)
_value = try Transformer.transform(rawVal)
}
}
extension Transformed: Encodable where Transformer: ReversibleTransformer, Transformer.From: Encodable {
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
guard let value = _value else {
fatalError("Attribute encoded before initialization.")
}
try container.encode(Transformer.reverse(value))
}
}
// MARK: - Nullable
public protocol _Optional {
static var nilValue: Self { get }
var isNilValue: Bool { get }
}
extension Optional: _Optional {
public static var nilValue: Self {
return .none
}
public var isNilValue: Bool { return self == nil }
}
protocol _Nullable {}
@propertyWrapper
public struct Nullable<T: Decodable>: Decodable, _Optional, _Nullable {
public var wrappedValue: T?
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if container.decodeNil() {
wrappedValue = nil
return
}
wrappedValue = try container.decode(T.self)
}
public init(initialValue: T? = nil) {
wrappedValue = initialValue
}
public static var nilValue: Self {
return .init()
}
public var isNilValue: Bool {
return wrappedValue == nil
}
}
extension Nullable: Encodable where T: Encodable {
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(wrappedValue)
}
}
@@ -5,6 +5,7 @@
// Created by Mathew Polzin on 7/24/18.
//
/// A JSON API structure within an ResourceObject that contains
/// named properties of types `ToOneRelationship` and
/// `ToManyRelationship`.
@@ -582,7 +583,6 @@ public extension ResourceObject {
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: ResourceObjectCodingKeys.self)
let type = try container.decode(String.self, forKey: .type)
@@ -597,7 +597,9 @@ public extension ResourceObject {
attributes = try (NoAttributes() as? Description.Attributes) ??
container.decode(Description.Attributes.self, forKey: .attributes)
relationships = try (NoRelationships() as? Description.Relationships) ?? container.decode(Description.Relationships.self, forKey: .relationships)
relationships = try (NoRelationships() as? Description.Relationships)
?? container.decodeIfPresent(Description.Relationships.self, forKey: .relationships)
?? Description.Relationships(from: EmptyObjectDecoder())
meta = try (NoMetadata() as? MetaType) ?? container.decode(MetaType.self, forKey: .meta)
@@ -62,70 +62,6 @@ class AttributeTests: XCTestCase {
}
}
// MARK: Property Wrappers
extension AttributeTests {
func test_Transformed() {
struct Test: Codable {
@Transformed(IntToString.self)
var value: String = ""
}
let test = Test(value: "hello")
XCTAssertEqual(test.value, "hello")
let test2 = try! JSONDecoder().decode(Test.self,
from: #"{"value": 12}"#.data(using: .utf8)!)
XCTAssertEqual(test2.value, "12")
try! print(String(data: JSONEncoder().encode(test2), encoding: .utf8)!)
let test3 = try? JSONDecoder().decode(Test.self,
from: #"{"value": null}"#.data(using: .utf8)!)
XCTAssertNil(test3)
}
func test_Nullable() {
struct Test: Codable {
@Nullable
var value: String?
}
let test = Test(value: nil)
XCTAssertNil(test.value)
let test2 = Test(value: "hello")
XCTAssertEqual(test2.value, "hello")
let test3 = try! JSONDecoder().decode(Test.self,
from: #"{"value": "world"}"#.data(using: .utf8)!)
XCTAssertEqual(test3.value, "world")
try! print(String(data: JSONEncoder().encode(test2), encoding: .utf8)!)
let test4 = try? JSONDecoder().decode(Test.self,
from: #"{"value": null}"#.data(using: .utf8)!)
XCTAssertNotNil(test4)
XCTAssertNil(test4?.value)
}
func test_NullableTransformed() {
struct Test: Codable {
// Nullable<Transformed<IntToString>>
let x: Transformed<IntToString>
// @Nullable @Transformed(IdentityTransformer.self)
@Transformed(IntToString.self) @Nullable
var value: String?
}
let test = Test(x: .init(initialValue: "12", IntToString.self))
print(test.x.wrappedValue)
}
}
// MARK: Test types
extension AttributeTests {
enum TestTransformer: ReversibleTransformer {
@@ -141,37 +77,12 @@ extension AttributeTests {
}
}
enum IntToString: ReversibleTransformer {
enum IntToString: Transformer {
public static func transform(_ from: Int) -> String {
return String(from)
}
public static func reverse(_ value: String) throws -> Int {
guard let intValue = Int(value) else {
fatalError("Reversed IntToString with invalid String value.")
}
return intValue
}
}
enum OptionalIntToOptionalString: ReversibleTransformer {
public static func transform(_ from: Int?) -> String? {
return from.map(String.init)
}
public static func reverse(_ value: String?) throws -> Int? {
guard let stringValue = value else {
return nil
}
guard let intValue = Int(stringValue) else {
fatalError("Reversed IntToString with invalid String value.")
}
return intValue
}
}
enum IntToInt: Transformer {
public static func transform(_ from: Int) -> Int {
return from + 100
+34 -2
View File
@@ -29,9 +29,9 @@ class EntityTests: XCTestCase {
let entity1 = TestEntity1(attributes: .none, relationships: .none, meta: .none, links: .none)
let entity = TestEntity9(attributes: .none, relationships: .init(one: entity1.pointer, nullableOne: .init(resourceObject: entity1, meta: .none, links: .none), optionalOne: .init(resourceObject: entity1, meta: .none, links: .none), optionalNullableOne: nil, optionalMany: .init(resourceObjects: [entity1, entity1], meta: .none, links: .none)), meta: .none, links: .none)
XCTAssertEqual(entity ~> \.optionalOne, Optional(entity1.id))
XCTAssertEqual(entity ~> \.optionalOne, entity1.id)
}
func test_toMany_relationship_operator_access() {
let entity1 = TestEntity1(attributes: .none, relationships: .none, meta: .none, links: .none)
let entity2 = TestEntity1(attributes: .none, relationships: .none, meta: .none, links: .none)
@@ -403,6 +403,16 @@ extension EntityTests {
data: entity_optional_nullable_nulled_relationship)
}
func test_optionalNullableRelationshipOmitted() {
let entity = decoded(type: TestEntity12.self,
data: entity_all_relationships_optional_and_omitted)
XCTAssertNil(entity ~> \.optionalOne)
XCTAssertNil(entity ~> \.optionalNullableOne)
XCTAssertNil(entity ~> \.optionalMany)
XCTAssertNoThrow(try TestEntity12.check(entity))
}
func test_nullableRelationshipIsNull() {
let entity = decoded(type: TestEntity9.self,
data: entity_nulled_relationship)
@@ -806,6 +816,28 @@ extension EntityTests {
typealias TestEntity11 = BasicEntity<TestEntityType11>
enum TestEntityType12: ResourceObjectDescription {
public static var jsonType: String { return "twelfth_test_entities" }
typealias Attributes = NoAttributes
public struct Relationships: JSONAPI.Relationships {
public init() {
optionalOne = nil
optionalNullableOne = nil
optionalMany = nil
}
let optionalOne: ToOneRelationship<TestEntity1, NoMetadata, NoLinks>?
let optionalNullableOne: ToOneRelationship<TestEntity1?, NoMetadata, NoLinks>?
let optionalMany: ToManyRelationship<TestEntity1, NoMetadata, NoLinks>?
}
}
typealias TestEntity12 = BasicEntity<TestEntityType12>
enum UnidentifiedTestEntityType: ResourceObjectDescription {
public static var jsonType: String { return "unidentified_test_entities" }
@@ -383,6 +383,16 @@ let entity_valid_validated_attribute = """
}
""".data(using: .utf8)!
let entity_all_relationships_optional_and_omitted = """
{
"id": "1",
"type": "twelfth_test_entities",
"attributes": {
"number": 10
}
}
""".data(using: .utf8)!
let entity_unidentified = """
{
"type": "unidentified_test_entities",