mirror of
https://github.com/encounter/JSONAPI.git
synced 2026-03-30 11:18:38 -07:00
merge w/ master
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
|
||||
import JSONAPI
|
||||
import Foundation
|
||||
|
||||
// MARK: - Resource Object
|
||||
|
||||
enum ThingWithPropertiesDescription: JSONAPI.ResourceObjectDescription {
|
||||
static let jsonType: String = "thing"
|
||||
|
||||
//
|
||||
// NOTE: `JSONAPI.SparsableAttributes` as opposed to `JSONAPI.Attributes`
|
||||
//
|
||||
struct Attributes: JSONAPI.SparsableAttributes {
|
||||
let stringThing: Attribute<String>
|
||||
let numberThing: Attribute<Double>
|
||||
let boolThing: Attribute<Bool?>
|
||||
|
||||
//
|
||||
// NOTE: Special implementation of `CodingKeys`
|
||||
//
|
||||
enum CodingKeys: String, JSONAPI.SparsableCodingKey {
|
||||
case stringThing
|
||||
case numberThing
|
||||
case boolThing
|
||||
}
|
||||
}
|
||||
|
||||
typealias Relationships = NoRelationships
|
||||
}
|
||||
|
||||
typealias ThingWithProperties = JSONAPI.ResourceObject<ThingWithPropertiesDescription, NoMetadata, NoLinks, String>
|
||||
|
||||
// MARK: - Document
|
||||
|
||||
//
|
||||
// NOTE: Using `JSONAPI.EncodableResourceBody` which means the document type will be `Encodable` but not `Decodable`.
|
||||
//
|
||||
typealias Document<PrimaryResourceBody: JSONAPI.EncodableResourceBody, IncludeType: JSONAPI.Include> = JSONAPI.Document<PrimaryResourceBody, NoMetadata, NoLinks, IncludeType, NoAPIDescription, UnknownJSONAPIError>
|
||||
|
||||
//
|
||||
// NOTE: Using `JSONAPI.EncodablePrimaryResource` which means the `ResourceBody` will be `Encodable` but not `Decodable.
|
||||
//
|
||||
typealias SingleDocument<T: JSONAPI.EncodablePrimaryResource> = Document<SingleResourceBody<T>, NoIncludes>
|
||||
|
||||
// MARK: - Resource Initialization
|
||||
|
||||
let resource = ThingWithProperties(id: .init(rawValue: "1234"),
|
||||
attributes: .init(stringThing: .init(value: "hello world"),
|
||||
numberThing: .init(value: 10),
|
||||
boolThing: .init(value: nil)),
|
||||
relationships: .none,
|
||||
meta: .none,
|
||||
links: .none)
|
||||
//
|
||||
// NOTE: Creating a sparse resource that will only encode
|
||||
// the attribute named "stringThing"
|
||||
//
|
||||
let sparseResource = resource.sparse(with: [.stringThing])
|
||||
|
||||
// MARK: - Encoding
|
||||
|
||||
let encoder = JSONEncoder()
|
||||
|
||||
let sparseResourceDoc = SingleDocument(apiDescription: .none,
|
||||
body: .init(resourceObject: sparseResource),
|
||||
includes: .none,
|
||||
meta: .none,
|
||||
links: .none)
|
||||
|
||||
let data = try! encoder.encode(sparseResourceDoc)
|
||||
|
||||
print(String(data: data, encoding: .utf8)!)
|
||||
@@ -8,6 +8,7 @@ Please enjoy these examples, but allow me the forced casting and the lack of err
|
||||
|
||||
********/
|
||||
|
||||
|
||||
// MARK: - Create a request or response body with one Dog in it
|
||||
let dogFromCode = try! Dog(name: "Buddy", owner: nil)
|
||||
|
||||
@@ -15,17 +16,20 @@ let singleDogDocument = SingleDogDocument(apiDescription: .none, body: .init(res
|
||||
|
||||
let singleDogData = try! JSONEncoder().encode(singleDogDocument)
|
||||
|
||||
|
||||
// MARK: - Parse a request or response body with one Dog in it
|
||||
let dogResponse = try! JSONDecoder().decode(SingleDogDocument.self, from: singleDogData)
|
||||
let dogFromData = dogResponse.body.primaryResource?.value
|
||||
let dogOwner: Person.Identifier? = dogFromData.flatMap { $0 ~> \.owner }
|
||||
|
||||
// MARKL - Parse a request or response body with one Dog in it using an alternative model
|
||||
|
||||
// MARK: - Parse a request or response body with one Dog in it using an alternative model
|
||||
typealias AltSingleDogDocument = JSONAPI.Document<SingleResourceBody<AlternativeDog>, NoMetadata, NoLinks, NoIncludes, NoAPIDescription, UnknownJSONAPIError>
|
||||
let altDogResponse = try! JSONDecoder().decode(AltSingleDogDocument.self, from: singleDogData)
|
||||
let altDogFromData = altDogResponse.body.primaryResource?.value
|
||||
let altDogHuman: Person.Identifier? = altDogFromData.flatMap { $0 ~> \.human }
|
||||
|
||||
|
||||
// MARK: - Create a request or response with multiple people and dogs and houses included
|
||||
let personIds = [Person.Identifier(), Person.Identifier()]
|
||||
let dogs = try! [Dog(name: "Buddy", owner: personIds[0]), Dog(name: "Joy", owner: personIds[0]), Dog(name: "Travis", owner: personIds[1])]
|
||||
@@ -36,6 +40,7 @@ let includes = dogs.map { BatchPeopleDocument.Include($0) } + houses.map { Batch
|
||||
let batchPeopleDocument = BatchPeopleDocument(apiDescription: .none, body: .init(resourceObjects: people), includes: .init(values: includes), meta: .none, links: .none)
|
||||
let batchPeopleData = try! JSONEncoder().encode(batchPeopleDocument)
|
||||
|
||||
|
||||
// MARK: - Parse a request or response body with multiple people in it and dogs and houses included
|
||||
|
||||
let peopleResponse = try! JSONDecoder().decode(BatchPeopleDocument.self, from: batchPeopleData)
|
||||
@@ -47,6 +52,7 @@ print("-----")
|
||||
print(peopleResponse)
|
||||
print("-----")
|
||||
|
||||
|
||||
// MARK: - Pass successfully parsed body to other parts of the code
|
||||
|
||||
if case let .data(bodyData) = peopleResponse.body {
|
||||
@@ -55,6 +61,7 @@ if case let .data(bodyData) = peopleResponse.body {
|
||||
print("no body data")
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Work in the abstract
|
||||
|
||||
func process<T: JSONAPIDocument>(document: T) {
|
||||
|
||||
@@ -5,5 +5,6 @@
|
||||
<page name='Usage'/>
|
||||
<page name='Full Client & Server Example'/>
|
||||
<page name='Full Document Verbose Generation'/>
|
||||
<page name='Sparse Fieldsets Example'/>
|
||||
</pages>
|
||||
</playground>
|
||||
+1
-1
@@ -16,7 +16,7 @@ Pod::Spec.new do |spec|
|
||||
#
|
||||
|
||||
spec.name = "JSONAPI"
|
||||
spec.version = "0.31.1"
|
||||
spec.version = "1.0.0"
|
||||
spec.summary = "Swift Codable JSON API framework."
|
||||
|
||||
# This description is used to generate tags and improve search results.
|
||||
|
||||
@@ -5,7 +5,7 @@ A Swift package for encoding to- and decoding from **JSON API** compliant reques
|
||||
|
||||
See the JSON API Spec here: https://jsonapi.org/format/
|
||||
|
||||
:warning: Although I find the type-safety of this framework appealing, the Swift compiler currently has enough trouble with it that it can become difficult to reason about errors produced by small typos. Similarly, auto-complete fails to provide reasonable suggestions much of the time. If you get the code right, everything compiles, otherwise it can suck to figure out what is wrong. This is mostly a concern when creating resource objects in-code (servers and test suites must do this). Writing a client that uses this framework to ingest JSON API Compliant API responses is much less painful. Note that this is a compile-time concern -- test coverage of this library's behavior is very good. :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. :warning:
|
||||
|
||||
## Table of Contents
|
||||
<!-- TOC depthFrom:1 depthTo:6 withLinks:1 updateOnSave:1 orderedList:0 -->
|
||||
@@ -42,7 +42,7 @@ See the JSON API Spec here: https://jsonapi.org/format/
|
||||
- [`Transformer`](#transformer)
|
||||
- [`Validator`](#validator)
|
||||
- [Computed `Attribute`](#computed-attribute)
|
||||
- [Copying `ResourceObjects`](#copying-resourceobjects)
|
||||
- [Copying/Mutating `ResourceObjects`](#copyingmutating-resourceobjects)
|
||||
- [`JSONAPI.Document`](#jsonapidocument)
|
||||
- [`ResourceBody`](#resourcebody)
|
||||
- [nullable `PrimaryResource`](#nullable-primaryresource)
|
||||
@@ -54,6 +54,9 @@ See the JSON API Spec here: https://jsonapi.org/format/
|
||||
- [`JSONAPI.Meta`](#jsonapimeta)
|
||||
- [`JSONAPI.Links`](#jsonapilinks)
|
||||
- [`JSONAPI.RawIdType`](#jsonapirawidtype)
|
||||
- [Sparse Fieldsets](#sparse-fieldsets)
|
||||
- [Supporting Sparse Fieldset Encoding](#supporting-sparse-fieldset-encoding)
|
||||
- [Sparse Fieldset `typealias` comparisons](#sparse-fieldset-typealias-comparisons)
|
||||
- [Custom Attribute or Relationship Key Mapping](#custom-attribute-or-relationship-key-mapping)
|
||||
- [Custom Attribute Encode/Decode](#custom-attribute-encodedecode)
|
||||
- [Meta-Attributes](#meta-attributes)
|
||||
@@ -106,52 +109,34 @@ Note that Playground support for importing non-system Frameworks is still a bit
|
||||
|
||||
### JSON:API
|
||||
#### Document
|
||||
- `data`
|
||||
- [x] Encoding/Decoding
|
||||
- `included`
|
||||
- [x] Encoding/Decoding
|
||||
- `errors`
|
||||
- [x] Encoding/Decoding
|
||||
- `meta`
|
||||
- [x] Encoding/Decoding
|
||||
- `jsonapi` (i.e. API Information)
|
||||
- [x] Encoding/Decoding
|
||||
- `links`
|
||||
- [x] Encoding/Decoding
|
||||
- [x] `data`
|
||||
- [x] `included`
|
||||
- [x] `errors`
|
||||
- [x] `meta`
|
||||
- [x] `jsonapi` (i.e. API Information)
|
||||
- [x] `links`
|
||||
|
||||
#### Resource Object
|
||||
- `id`
|
||||
- [x] Encoding/Decoding
|
||||
- `type`
|
||||
- [x] Encoding/Decoding
|
||||
- `attributes`
|
||||
- [x] Encoding/Decoding
|
||||
- `relationships`
|
||||
- [x] Encoding/Decoding
|
||||
- `links`
|
||||
- [x] Encoding/Decoding
|
||||
- `meta`
|
||||
- [x] Encoding/Decoding
|
||||
- [x] `id`
|
||||
- [x] `type`
|
||||
- [x] `attributes`
|
||||
- [x] `relationships`
|
||||
- [x] `links`
|
||||
- [x] `meta`
|
||||
|
||||
#### Relationship Object
|
||||
- `data`
|
||||
- [x] Encoding/Decoding
|
||||
- `links`
|
||||
- [x] Encoding/Decoding
|
||||
- `meta`
|
||||
- [x] Encoding/Decoding
|
||||
- [x] `data`
|
||||
- [x] `links`
|
||||
- [x] `meta`
|
||||
|
||||
#### Links Object
|
||||
- `href`
|
||||
- [x] Encoding/Decoding
|
||||
- `meta`
|
||||
- [x] Encoding/Decoding
|
||||
- [x] `href`
|
||||
- [x] `meta`
|
||||
|
||||
### Misc
|
||||
- [x] Support transforms on `Attributes` values (e.g. to support different representations of `Date`)
|
||||
- [x] Support validation on `Attributes`.
|
||||
- [ ] Support sparse fieldsets. At the moment, not sure what this support will look like. A client can likely just define a new model to represent a sparse population of another model in a very specific use case. On the server side, it becomes much more appealing to be able to support arbitrary combinations of omitted fields.
|
||||
- [ ] Create more descriptive errors that are easier to use for troubleshooting.
|
||||
- [x] Support sparse fieldsets (encoding only). A client can likely just define a new model to represent a sparse population of another model in a very specific use case for decoding purposes. On the server side, sparse fieldsets of Resource Objects can be encoded without creating one model for every possible sparse fieldset.
|
||||
|
||||
### Testing
|
||||
#### Resource Object Validator
|
||||
@@ -160,6 +145,8 @@ Note that Playground support for importing non-system Frameworks is still a bit
|
||||
- [x] Only allow `ToManyRelationship` and `ToOneRelationship` within `Relationships` struct.
|
||||
|
||||
### Potential Improvements
|
||||
These ideas could be implemented in future versions.
|
||||
|
||||
- [ ] (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`.
|
||||
@@ -494,6 +481,31 @@ extension String: CreatableRawIdType {
|
||||
}
|
||||
```
|
||||
|
||||
### Sparse Fieldsets
|
||||
Sparse Fieldsets are currently supported when encoding only. When decoding, Sparse Fieldsets become tricker to support under the current types this library uses and it is assumed that clients will request one or maybe two sparse fieldset combinations for any given model at most so it can simply define the `JSONAPI` models needed to decode those subsets of all possible fields. A server, on the other hand, likely needs to support arbitrary combinations of sparse fieldsets and this library provides a mechanism for encoding those sparse fieldsets without too much extra footwork.
|
||||
|
||||
You can use sparse fieldsets on the primary resources(s) _and_ includes of a `JSONAPI.Document`.
|
||||
|
||||
There is a sparse fieldsets example included with this repository as a Playground page.
|
||||
|
||||
#### Supporting Sparse Fieldset Encoding
|
||||
1. The `JSONAPI` `ResourceObjectDescription`'s `Attributes` struct must conform to `JSONAPI.SparsableAttributes` rather than `JSONAPI.Attributes`.
|
||||
2. The `JSONAPI` `ResourceObjectDescription`'s `Attributes` struct must contain a `CodingKeys` enum that conforms to `JSONAPI.SparsableCodingKey` instead of `Swift.CodingKey`.
|
||||
3. `typealiases` you may have created for `JSONAPI.Document` that allow you to decode Documents will not support the "encode-only" nature of sparse fieldsets. See the next section for `typealias` comparisons.
|
||||
4. To create a sparse fieldset from a `ResourceObject` just call its `sparse(with: fields)` method and pass an array of `Attributes.CodingKeys` values you would like included in the encoding.
|
||||
5. Initialize and encode a `Document` containing one or more sparse or full primary resource(s) and any number of sparse or full includes.
|
||||
|
||||
#### Sparse Fieldset `typealias` comparisons
|
||||
You might have found a `typealias` like the following for encoding/decoding `JSONAPI.Document`s (note the primary resource body is a `JSONAPI.ResourceBody`):
|
||||
```swift
|
||||
typealias Document<PrimaryResourceBody: JSONAPI.ResourceBody, IncludeType: JSONAPI.Include> = JSONAPI.Document<PrimaryResourceBody, NoMetadata, NoLinks, IncludeType, NoAPIDescription, UnknownJSONAPIError>
|
||||
```
|
||||
|
||||
In order to support sparse fieldsets (which are encode-only), the following companion `typealias` would be useful (note the primary resource body is a `JSONAPI.EncodableResourceBody`):
|
||||
```swift
|
||||
typealias SparseDocument<PrimaryResourceBody: JSONAPI.EncodableResourceBody, IncludeType: JSONAPI.Include> = JSONAPI.Document<PrimaryResourceBody, NoMetadata, NoLinks, IncludeType, NoAPIDescription, UnknownJSONAPIError>
|
||||
```
|
||||
|
||||
### Custom Attribute or Relationship Key Mapping
|
||||
There is not anything special going on at the `JSONAPI.Attributes` and `JSONAPI.Relationships` levels, so you can easily provide custom key mappings by taking advantage of `Codable`'s `CodingKeys` pattern. Here are two models that will encode/decode equivalently but offer different naming in your codebase:
|
||||
```swift
|
||||
|
||||
@@ -7,19 +7,26 @@
|
||||
|
||||
import Poly
|
||||
|
||||
public protocol JSONAPIDocument: Codable, Equatable {
|
||||
associatedtype PrimaryResourceBody: JSONAPI.ResourceBody
|
||||
associatedtype MetaType: JSONAPI.Meta
|
||||
associatedtype LinksType: JSONAPI.Links
|
||||
associatedtype IncludeType: JSONAPI.Include
|
||||
associatedtype APIDescription: APIDescriptionType
|
||||
associatedtype Error: JSONAPIError
|
||||
/// An `EncodableJSONAPIDocument` supports encoding but not decoding.
|
||||
/// It is actually more restrictive than `JSONAPIDocument` which supports both
|
||||
/// encoding and decoding.
|
||||
public protocol EncodableJSONAPIDocument: Equatable, Encodable {
|
||||
associatedtype PrimaryResourceBody: JSONAPI.EncodableResourceBody
|
||||
associatedtype MetaType: JSONAPI.Meta
|
||||
associatedtype LinksType: JSONAPI.Links
|
||||
associatedtype IncludeType: JSONAPI.Include
|
||||
associatedtype APIDescription: APIDescriptionType
|
||||
associatedtype Error: JSONAPIError
|
||||
|
||||
typealias Body = Document<PrimaryResourceBody, MetaType, LinksType, IncludeType, APIDescription, Error>.Body
|
||||
typealias Body = Document<PrimaryResourceBody, MetaType, LinksType, IncludeType, APIDescription, Error>.Body
|
||||
|
||||
var body: Body { get }
|
||||
var body: Body { get }
|
||||
}
|
||||
|
||||
/// A `JSONAPIDocument` supports encoding and decoding of a JSON:API
|
||||
/// compliant Document.
|
||||
public protocol JSONAPIDocument: EncodableJSONAPIDocument, Decodable where PrimaryResourceBody: JSONAPI.ResourceBody, IncludeType: Decodable {}
|
||||
|
||||
/// A JSON API Document represents the entire body
|
||||
/// of a JSON API request or the entire body of
|
||||
/// a JSON API response.
|
||||
@@ -27,7 +34,7 @@ public protocol JSONAPIDocument: Codable, Equatable {
|
||||
/// API uses snake case, you will want to use
|
||||
/// a conversion such as the one offerred by the
|
||||
/// Foundation JSONEncoder/Decoder: `KeyDecodingStrategy`
|
||||
public struct Document<PrimaryResourceBody: JSONAPI.ResourceBody, MetaType: JSONAPI.Meta, LinksType: JSONAPI.Links, IncludeType: JSONAPI.Include, APIDescription: APIDescriptionType, Error: JSONAPIError>: JSONAPIDocument {
|
||||
public struct Document<PrimaryResourceBody: JSONAPI.EncodableResourceBody, MetaType: JSONAPI.Meta, LinksType: JSONAPI.Links, IncludeType: JSONAPI.Include, APIDescription: APIDescriptionType, Error: JSONAPIError>: EncodableJSONAPIDocument {
|
||||
public typealias Include = IncludeType
|
||||
|
||||
/// The JSON API Spec calls this the JSON:API Object. It contains version
|
||||
@@ -46,7 +53,9 @@ public struct Document<PrimaryResourceBody: JSONAPI.ResourceBody, MetaType: JSON
|
||||
case data(Data)
|
||||
|
||||
public struct Data: Equatable {
|
||||
/// The document's Primary Resource object(s)
|
||||
public let primary: PrimaryResourceBody
|
||||
/// The document's included objects
|
||||
public let includes: Includes<Include>
|
||||
public let meta: MetaType
|
||||
public let links: LinksType
|
||||
@@ -59,6 +68,8 @@ public struct Document<PrimaryResourceBody: JSONAPI.ResourceBody, MetaType: JSON
|
||||
}
|
||||
}
|
||||
|
||||
/// `true` if the document represents one or more errors. `false` if the
|
||||
/// document represents JSON:API data and/or metadata.
|
||||
public var isError: Bool {
|
||||
guard case .errors = self else { return false }
|
||||
return true
|
||||
@@ -196,7 +207,7 @@ extension Document where IncludeType == NoIncludes, MetaType == NoMetadata, Link
|
||||
}
|
||||
*/
|
||||
|
||||
extension Document.Body.Data where PrimaryResourceBody: AppendableResourceBody {
|
||||
extension Document.Body.Data where PrimaryResourceBody: Appendable {
|
||||
public func merging(_ other: Document.Body.Data,
|
||||
combiningMetaWith metaMerge: (MetaType, MetaType) -> MetaType,
|
||||
combiningLinksWith linksMerge: (LinksType, LinksType) -> LinksType) -> Document.Body.Data {
|
||||
@@ -207,7 +218,7 @@ extension Document.Body.Data where PrimaryResourceBody: AppendableResourceBody {
|
||||
}
|
||||
}
|
||||
|
||||
extension Document.Body.Data where PrimaryResourceBody: AppendableResourceBody, MetaType == NoMetadata, LinksType == NoLinks {
|
||||
extension Document.Body.Data where PrimaryResourceBody: Appendable, MetaType == NoMetadata, LinksType == NoLinks {
|
||||
public func merging(_ other: Document.Body.Data) -> Document.Body.Data {
|
||||
return merging(other,
|
||||
combiningMetaWith: { _, _ in .none },
|
||||
@@ -273,66 +284,6 @@ extension Document {
|
||||
case links
|
||||
case jsonapi
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: RootCodingKeys.self)
|
||||
|
||||
if let noData = NoAPIDescription() as? APIDescription {
|
||||
apiDescription = noData
|
||||
} else {
|
||||
apiDescription = try container.decode(APIDescription.self, forKey: .jsonapi)
|
||||
}
|
||||
|
||||
let errors = try container.decodeIfPresent([Error].self, forKey: .errors)
|
||||
|
||||
let meta: MetaType?
|
||||
if let noMeta = NoMetadata() as? MetaType {
|
||||
meta = noMeta
|
||||
} else {
|
||||
do {
|
||||
meta = try container.decode(MetaType.self, forKey: .meta)
|
||||
} catch {
|
||||
meta = nil
|
||||
}
|
||||
}
|
||||
|
||||
let links: LinksType?
|
||||
if let noLinks = NoLinks() as? LinksType {
|
||||
links = noLinks
|
||||
} else {
|
||||
do {
|
||||
links = try container.decode(LinksType.self, forKey: .links)
|
||||
} catch {
|
||||
links = nil
|
||||
}
|
||||
}
|
||||
|
||||
// If there are errors, there cannot be a body. Return errors and any metadata found.
|
||||
if let errors = errors {
|
||||
body = .errors(errors, meta: meta, links: links)
|
||||
return
|
||||
}
|
||||
|
||||
let data: PrimaryResourceBody
|
||||
if let noData = NoResourceBody() as? PrimaryResourceBody {
|
||||
data = noData
|
||||
} else {
|
||||
data = try container.decode(PrimaryResourceBody.self, forKey: .data)
|
||||
}
|
||||
|
||||
let maybeIncludes = try container.decodeIfPresent(Includes<Include>.self, forKey: .included)
|
||||
|
||||
// TODO come back to this and make robust
|
||||
|
||||
guard let metaVal = meta else {
|
||||
throw JSONAPIEncodingError.missingOrMalformedMetadata
|
||||
}
|
||||
guard let linksVal = links else {
|
||||
throw JSONAPIEncodingError.missingOrMalformedLinks
|
||||
}
|
||||
|
||||
body = .data(.init(primary: data, includes: maybeIncludes ?? Includes<Include>.none, meta: metaVal, links: linksVal))
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: RootCodingKeys.self)
|
||||
@@ -377,6 +328,68 @@ extension Document {
|
||||
}
|
||||
}
|
||||
|
||||
extension Document: Decodable, JSONAPIDocument where PrimaryResourceBody: ResourceBody, IncludeType: Decodable {
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: RootCodingKeys.self)
|
||||
|
||||
if let noData = NoAPIDescription() as? APIDescription {
|
||||
apiDescription = noData
|
||||
} else {
|
||||
apiDescription = try container.decode(APIDescription.self, forKey: .jsonapi)
|
||||
}
|
||||
|
||||
let errors = try container.decodeIfPresent([Error].self, forKey: .errors)
|
||||
|
||||
let meta: MetaType?
|
||||
if let noMeta = NoMetadata() as? MetaType {
|
||||
meta = noMeta
|
||||
} else {
|
||||
do {
|
||||
meta = try container.decode(MetaType.self, forKey: .meta)
|
||||
} catch {
|
||||
meta = nil
|
||||
}
|
||||
}
|
||||
|
||||
let links: LinksType?
|
||||
if let noLinks = NoLinks() as? LinksType {
|
||||
links = noLinks
|
||||
} else {
|
||||
do {
|
||||
links = try container.decode(LinksType.self, forKey: .links)
|
||||
} catch {
|
||||
links = nil
|
||||
}
|
||||
}
|
||||
|
||||
// If there are errors, there cannot be a body. Return errors and any metadata found.
|
||||
if let errors = errors {
|
||||
body = .errors(errors, meta: meta, links: links)
|
||||
return
|
||||
}
|
||||
|
||||
let data: PrimaryResourceBody
|
||||
if let noData = NoResourceBody() as? PrimaryResourceBody {
|
||||
data = noData
|
||||
} else {
|
||||
data = try container.decode(PrimaryResourceBody.self, forKey: .data)
|
||||
}
|
||||
|
||||
let maybeIncludes = try container.decodeIfPresent(Includes<Include>.self, forKey: .included)
|
||||
|
||||
// TODO come back to this and make robust
|
||||
|
||||
guard let metaVal = meta else {
|
||||
throw JSONAPIEncodingError.missingOrMalformedMetadata
|
||||
}
|
||||
guard let linksVal = links else {
|
||||
throw JSONAPIEncodingError.missingOrMalformedLinks
|
||||
}
|
||||
|
||||
body = .data(.init(primary: data, includes: maybeIncludes ?? Includes<Include>.none, meta: metaVal, links: linksVal))
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - CustomStringConvertible
|
||||
|
||||
extension Document: CustomStringConvertible {
|
||||
|
||||
@@ -9,6 +9,9 @@ public protocol JSONAPIError: Swift.Error, Equatable, Codable {
|
||||
static var unknown: Self { get }
|
||||
}
|
||||
|
||||
/// `UnknownJSONAPIError` can actually be used in any sitaution
|
||||
/// where you don't know what errors are possible _or_ you just don't
|
||||
/// care what errors might show up.
|
||||
public enum UnknownJSONAPIError: JSONAPIError {
|
||||
case unknownError
|
||||
|
||||
|
||||
@@ -7,9 +7,19 @@
|
||||
|
||||
import Poly
|
||||
|
||||
public typealias Include = JSONPoly
|
||||
public typealias Include = EncodableJSONPoly
|
||||
|
||||
public struct Includes<I: Include>: Codable, Equatable {
|
||||
/// A structure holding zero or more included Resource Objects.
|
||||
/// The resources are accessed by their type using a subscript.
|
||||
///
|
||||
/// If you have
|
||||
///
|
||||
/// `let includes: Includes<Include2<Thing1, Thing2>> = ...`
|
||||
///
|
||||
/// then you can access all `Thing1` included resources with
|
||||
///
|
||||
/// `let includedThings = includes[Thing1.self]`
|
||||
public struct Includes<I: Include>: Encodable, Equatable {
|
||||
public static var none: Includes { return .init(values: []) }
|
||||
|
||||
let values: [I]
|
||||
@@ -17,23 +27,6 @@ public struct Includes<I: Include>: Codable, Equatable {
|
||||
public init(values: [I]) {
|
||||
self.values = values
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
var container = try decoder.unkeyedContainer()
|
||||
|
||||
// If not parsing includes, no need to loop over them.
|
||||
guard I.self != NoIncludes.self else {
|
||||
values = []
|
||||
return
|
||||
}
|
||||
|
||||
var valueAggregator = [I]()
|
||||
while !container.isAtEnd {
|
||||
valueAggregator.append(try container.decode(I.self))
|
||||
}
|
||||
|
||||
values = valueAggregator
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.unkeyedContainer()
|
||||
@@ -52,6 +45,25 @@ public struct Includes<I: Include>: Codable, Equatable {
|
||||
}
|
||||
}
|
||||
|
||||
extension Includes: Decodable where I: Decodable {
|
||||
public init(from decoder: Decoder) throws {
|
||||
var container = try decoder.unkeyedContainer()
|
||||
|
||||
// If not parsing includes, no need to loop over them.
|
||||
guard I.self != NoIncludes.self else {
|
||||
values = []
|
||||
return
|
||||
}
|
||||
|
||||
var valueAggregator = [I]()
|
||||
while !container.isAtEnd {
|
||||
valueAggregator.append(try container.decode(I.self))
|
||||
}
|
||||
|
||||
values = valueAggregator
|
||||
}
|
||||
}
|
||||
|
||||
extension Includes {
|
||||
public func appending(_ other: Includes<I>) -> Includes {
|
||||
return Includes(values: values + other.values)
|
||||
|
||||
@@ -5,29 +5,53 @@
|
||||
// Created by Mathew Polzin on 11/10/18.
|
||||
//
|
||||
|
||||
public protocol MaybePrimaryResource: Equatable, Codable {}
|
||||
/// This protocol allows for a `SingleResourceBody` to contain a `null`
|
||||
/// data object where `ManyResourceBody` cannot (because an empty
|
||||
/// array should be used for no results).
|
||||
public protocol OptionalEncodablePrimaryResource: Equatable, Encodable {}
|
||||
|
||||
/// A PrimaryResource is a type that can be used in the body of a JSON API
|
||||
/// An `EncodablePrimaryResource` is a `PrimaryResource` that only supports encoding.
|
||||
/// This is actually more restrictave than `PrimaryResource`, which supports both encoding and
|
||||
/// decoding.
|
||||
public protocol EncodablePrimaryResource: OptionalEncodablePrimaryResource {}
|
||||
|
||||
/// This protocol allows for `SingleResourceBody` to contain a `null`
|
||||
/// data object where `ManyResourceBody` cannot (because an empty
|
||||
/// array should be used for no results).
|
||||
public protocol OptionalPrimaryResource: OptionalEncodablePrimaryResource, Decodable {}
|
||||
|
||||
/// A `PrimaryResource` is a type that can be used in the body of a JSON API
|
||||
/// document as the primary resource.
|
||||
public protocol PrimaryResource: MaybePrimaryResource {}
|
||||
public protocol PrimaryResource: EncodablePrimaryResource, OptionalPrimaryResource {}
|
||||
|
||||
extension Optional: MaybePrimaryResource where Wrapped: PrimaryResource {}
|
||||
extension Optional: OptionalEncodablePrimaryResource where Wrapped: EncodablePrimaryResource {}
|
||||
|
||||
extension Optional: OptionalPrimaryResource where Wrapped: PrimaryResource {}
|
||||
|
||||
/// An `EncodableResourceBody` is a `ResourceBody` that only supports being
|
||||
/// encoded. It is actually weaker than `ResourceBody`, which supports both encoding
|
||||
/// and decoding.
|
||||
public protocol EncodableResourceBody: Equatable, Encodable {}
|
||||
|
||||
/// A ResourceBody is a representation of the body of the JSON API Document.
|
||||
/// It can either be one resource (which can be specified as optional or not)
|
||||
/// or it can contain many resources (and array with zero or more entries).
|
||||
public protocol ResourceBody: Codable, Equatable {
|
||||
}
|
||||
public protocol ResourceBody: Decodable, EncodableResourceBody {}
|
||||
|
||||
public protocol AppendableResourceBody: ResourceBody {
|
||||
/// A `ResourceBody` that has the ability to take on more primary
|
||||
/// resources by appending another similarly typed `ResourceBody`.
|
||||
public protocol Appendable {
|
||||
func appending(_ other: Self) -> Self
|
||||
}
|
||||
|
||||
public func +<R: AppendableResourceBody>(_ left: R, right: R) -> R {
|
||||
public func +<R: Appendable>(_ left: R, right: R) -> R {
|
||||
return left.appending(right)
|
||||
}
|
||||
|
||||
public struct SingleResourceBody<Entity: JSONAPI.MaybePrimaryResource>: ResourceBody {
|
||||
/// A type allowing for a document body containing 1 primary resource.
|
||||
/// If the `Entity` specialization is an `Optional` type, the body can contain
|
||||
/// 0 or 1 primary resources.
|
||||
public struct SingleResourceBody<Entity: JSONAPI.OptionalEncodablePrimaryResource>: EncodableResourceBody {
|
||||
public let value: Entity
|
||||
|
||||
public init(resourceObject: Entity) {
|
||||
@@ -35,7 +59,8 @@ public struct SingleResourceBody<Entity: JSONAPI.MaybePrimaryResource>: Resource
|
||||
}
|
||||
}
|
||||
|
||||
public struct ManyResourceBody<Entity: JSONAPI.PrimaryResource>: AppendableResourceBody {
|
||||
/// A type allowing for a document body containing 0 or more primary resources.
|
||||
public struct ManyResourceBody<Entity: JSONAPI.EncodablePrimaryResource>: EncodableResourceBody, Appendable {
|
||||
public let values: [Entity]
|
||||
|
||||
public init(resourceObjects: [Entity]) {
|
||||
@@ -55,19 +80,6 @@ public struct NoResourceBody: ResourceBody {
|
||||
|
||||
// MARK: Codable
|
||||
extension SingleResourceBody {
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.singleValueContainer()
|
||||
|
||||
let anyNil: Any? = nil
|
||||
if container.decodeNil(),
|
||||
let val = anyNil as? Entity {
|
||||
value = val
|
||||
return
|
||||
}
|
||||
|
||||
value = try container.decode(Entity.self)
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.singleValueContainer()
|
||||
|
||||
@@ -82,16 +94,22 @@ extension SingleResourceBody {
|
||||
}
|
||||
}
|
||||
|
||||
extension ManyResourceBody {
|
||||
public init(from decoder: Decoder) throws {
|
||||
var container = try decoder.unkeyedContainer()
|
||||
var valueAggregator = [Entity]()
|
||||
while !container.isAtEnd {
|
||||
valueAggregator.append(try container.decode(Entity.self))
|
||||
}
|
||||
values = valueAggregator
|
||||
}
|
||||
extension SingleResourceBody: Decodable, ResourceBody where Entity: OptionalPrimaryResource {
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.singleValueContainer()
|
||||
|
||||
let anyNil: Any? = nil
|
||||
if container.decodeNil(),
|
||||
let val = anyNil as? Entity {
|
||||
value = val
|
||||
return
|
||||
}
|
||||
|
||||
value = try container.decode(Entity.self)
|
||||
}
|
||||
}
|
||||
|
||||
extension ManyResourceBody {
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.unkeyedContainer()
|
||||
|
||||
@@ -101,6 +119,17 @@ extension ManyResourceBody {
|
||||
}
|
||||
}
|
||||
|
||||
extension ManyResourceBody: Decodable, ResourceBody where Entity: PrimaryResource {
|
||||
public init(from decoder: Decoder) throws {
|
||||
var container = try decoder.unkeyedContainer()
|
||||
var valueAggregator = [Entity]()
|
||||
while !container.isAtEnd {
|
||||
valueAggregator.append(try container.decode(Entity.self))
|
||||
}
|
||||
values = valueAggregator
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: CustomStringConvertible
|
||||
|
||||
extension SingleResourceBody: CustomStringConvertible {
|
||||
|
||||
@@ -19,6 +19,8 @@ public protocol Meta: Codable, Equatable {
|
||||
// nullable.
|
||||
extension Optional: Meta where Wrapped: Meta {}
|
||||
|
||||
/// Use this type when you want to specify not to encode or decode any metadata
|
||||
/// for a type.
|
||||
public struct NoMetadata: Meta, CustomStringConvertible {
|
||||
public static var none: NoMetadata { return NoMetadata() }
|
||||
|
||||
|
||||
@@ -28,6 +28,9 @@ public protocol CreatableRawIdType: RawIdType {
|
||||
|
||||
extension String: RawIdType {}
|
||||
|
||||
/// A type that can be used as the `MaybeRawId` for a `ResourceObject` that does not
|
||||
/// have an Id (most likely because it was created by a client and the server will be responsible
|
||||
/// for assigning it an Id).
|
||||
public struct Unidentified: MaybeRawId, CustomStringConvertible {
|
||||
public init() {}
|
||||
|
||||
|
||||
@@ -15,9 +15,10 @@ import Poly
|
||||
/// disparate types under one roof for
|
||||
/// the purposes of JSON API compliant
|
||||
/// encoding or decoding.
|
||||
public typealias JSONPoly = Poly & PrimaryResource
|
||||
public typealias EncodableJSONPoly = Poly & EncodablePrimaryResource
|
||||
|
||||
public typealias PolyWrapped = Codable & Equatable
|
||||
public typealias EncodablePolyWrapped = Encodable & Equatable
|
||||
public typealias PolyWrapped = EncodablePolyWrapped & Decodable
|
||||
|
||||
extension Poly0: PrimaryResource {
|
||||
public init(from decoder: Decoder) throws {
|
||||
@@ -30,28 +31,46 @@ extension Poly0: PrimaryResource {
|
||||
}
|
||||
|
||||
// MARK: - 1 type
|
||||
extension Poly1: PrimaryResource, MaybePrimaryResource where A: PolyWrapped {}
|
||||
extension Poly1: EncodablePrimaryResource, OptionalEncodablePrimaryResource where A: EncodablePolyWrapped {}
|
||||
|
||||
extension Poly1: PrimaryResource, OptionalPrimaryResource where A: PolyWrapped {}
|
||||
|
||||
// MARK: - 2 types
|
||||
extension Poly2: PrimaryResource, MaybePrimaryResource where A: PolyWrapped, B: PolyWrapped {}
|
||||
extension Poly2: EncodablePrimaryResource, OptionalEncodablePrimaryResource where A: EncodablePolyWrapped, B: EncodablePolyWrapped {}
|
||||
|
||||
extension Poly2: PrimaryResource, OptionalPrimaryResource where A: PolyWrapped, B: PolyWrapped {}
|
||||
|
||||
// MARK: - 3 types
|
||||
extension Poly3: PrimaryResource, MaybePrimaryResource where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped {}
|
||||
extension Poly3: EncodablePrimaryResource, OptionalEncodablePrimaryResource where A: EncodablePolyWrapped, B: EncodablePolyWrapped, C: EncodablePolyWrapped {}
|
||||
|
||||
extension Poly3: PrimaryResource, OptionalPrimaryResource where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped {}
|
||||
|
||||
// MARK: - 4 types
|
||||
extension Poly4: PrimaryResource, MaybePrimaryResource where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped {}
|
||||
extension Poly4: EncodablePrimaryResource, OptionalEncodablePrimaryResource where A: EncodablePolyWrapped, B: EncodablePolyWrapped, C: EncodablePolyWrapped, D: EncodablePolyWrapped {}
|
||||
|
||||
extension Poly4: PrimaryResource, OptionalPrimaryResource where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped {}
|
||||
|
||||
// MARK: - 5 types
|
||||
extension Poly5: PrimaryResource, MaybePrimaryResource where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped, E: PolyWrapped {}
|
||||
extension Poly5: EncodablePrimaryResource, OptionalEncodablePrimaryResource where A: EncodablePolyWrapped, B: EncodablePolyWrapped, C: EncodablePolyWrapped, D: EncodablePolyWrapped, E: EncodablePolyWrapped {}
|
||||
|
||||
extension Poly5: PrimaryResource, OptionalPrimaryResource where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped, E: PolyWrapped {}
|
||||
|
||||
// MARK: - 6 types
|
||||
extension Poly6: PrimaryResource, MaybePrimaryResource where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped, E: PolyWrapped, F: PolyWrapped {}
|
||||
extension Poly6: EncodablePrimaryResource, OptionalEncodablePrimaryResource where A: EncodablePolyWrapped, B: EncodablePolyWrapped, C: EncodablePolyWrapped, D: EncodablePolyWrapped, E: EncodablePolyWrapped, F: EncodablePolyWrapped {}
|
||||
|
||||
extension Poly6: PrimaryResource, OptionalPrimaryResource where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped, E: PolyWrapped, F: PolyWrapped {}
|
||||
|
||||
// MARK: - 7 types
|
||||
extension Poly7: PrimaryResource, MaybePrimaryResource where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped, E: PolyWrapped, F: PolyWrapped, G: PolyWrapped {}
|
||||
extension Poly7: EncodablePrimaryResource, OptionalEncodablePrimaryResource where A: EncodablePolyWrapped, B: EncodablePolyWrapped, C: EncodablePolyWrapped, D: EncodablePolyWrapped, E: EncodablePolyWrapped, F: EncodablePolyWrapped, G: EncodablePolyWrapped {}
|
||||
|
||||
extension Poly7: PrimaryResource, OptionalPrimaryResource where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped, E: PolyWrapped, F: PolyWrapped, G: PolyWrapped {}
|
||||
|
||||
// MARK: - 8 types
|
||||
extension Poly8: PrimaryResource, MaybePrimaryResource where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped, E: PolyWrapped, F: PolyWrapped, G: PolyWrapped, H: PolyWrapped {}
|
||||
extension Poly8: EncodablePrimaryResource, OptionalEncodablePrimaryResource where A: EncodablePolyWrapped, B: EncodablePolyWrapped, C: EncodablePolyWrapped, D: EncodablePolyWrapped, E: EncodablePolyWrapped, F: EncodablePolyWrapped, G: EncodablePolyWrapped, H: EncodablePolyWrapped {}
|
||||
|
||||
extension Poly8: PrimaryResource, OptionalPrimaryResource where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped, E: PolyWrapped, F: PolyWrapped, G: PolyWrapped, H: PolyWrapped {}
|
||||
|
||||
// MARK: - 9 types
|
||||
extension Poly9: PrimaryResource, MaybePrimaryResource where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped, E: PolyWrapped, F: PolyWrapped, G: PolyWrapped, H: PolyWrapped, I: PolyWrapped {}
|
||||
extension Poly9: EncodablePrimaryResource, OptionalEncodablePrimaryResource where A: EncodablePolyWrapped, B: EncodablePolyWrapped, C: EncodablePolyWrapped, D: EncodablePolyWrapped, E: EncodablePolyWrapped, F: EncodablePolyWrapped, G: EncodablePolyWrapped, H: EncodablePolyWrapped, I: EncodablePolyWrapped {}
|
||||
|
||||
extension Poly9: PrimaryResource, OptionalPrimaryResource where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped, E: PolyWrapped, F: PolyWrapped, G: PolyWrapped, H: PolyWrapped, I: PolyWrapped {}
|
||||
|
||||
@@ -15,6 +15,16 @@ public protocol Relationships: Codable & Equatable {}
|
||||
/// properties of any types that are JSON encodable.
|
||||
public protocol Attributes: Codable & Equatable {}
|
||||
|
||||
/// CodingKeys must be `CodingKey` and `Equatable` in order
|
||||
/// to support Sparse Fieldsets.
|
||||
public typealias SparsableCodingKey = CodingKey & Equatable
|
||||
|
||||
/// Attributes containing publicly accessible and `Equatable`
|
||||
/// CodingKeys are required to support Sparse Fieldsets.
|
||||
public protocol SparsableAttributes: Attributes {
|
||||
associatedtype CodingKeys: SparsableCodingKey
|
||||
}
|
||||
|
||||
/// Can be used as `Relationships` Type for Entities that do not
|
||||
/// have any Relationships.
|
||||
public struct NoRelationships: Relationships {
|
||||
@@ -48,7 +58,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
|
||||
@@ -396,14 +406,14 @@ extension ResourceObject where MetaType == NoMetadata, LinksType == NoLinks, Ent
|
||||
// MARK: - Pointer for Relationships use
|
||||
public extension ResourceObject where EntityRawIdType: JSONAPI.RawIdType {
|
||||
|
||||
/// An ResourceObject.Pointer is a `ToOneRelationship` with no metadata or links.
|
||||
/// This is just a convenient way to reference an ResourceObject so that
|
||||
/// other Entities' Relationships can be built up from it.
|
||||
/// A `ResourceObject.Pointer` is a `ToOneRelationship` with no metadata or links.
|
||||
/// This is just a convenient way to reference a `ResourceObject` so that
|
||||
/// other ResourceObjects' Relationships can be built up from it.
|
||||
typealias Pointer = ToOneRelationship<ResourceObject, NoMetadata, NoLinks>
|
||||
|
||||
/// ResourceObject.Pointers is a `ToManyRelationship` with no metadata or links.
|
||||
/// This is just a convenient way to reference a bunch of Entities so
|
||||
/// that other Entities' Relationships can be built up from them.
|
||||
/// `ResourceObject.Pointers` is a `ToManyRelationship` with no metadata or links.
|
||||
/// This is just a convenient way to reference a bunch of ResourceObjects so
|
||||
/// that other ResourceObjects' Relationships can be built up from them.
|
||||
typealias Pointers = ToManyRelationship<ResourceObject, NoMetadata, NoLinks>
|
||||
|
||||
/// Get a pointer to this resource object that can be used as a
|
||||
@@ -412,6 +422,8 @@ public extension ResourceObject where EntityRawIdType: JSONAPI.RawIdType {
|
||||
return Pointer(resourceObject: self)
|
||||
}
|
||||
|
||||
/// Get a pointer (i.e. `ToOneRelationship`) to this resource
|
||||
/// object with the given metadata and links attached.
|
||||
func pointer<MType: JSONAPI.Meta, LType: JSONAPI.Links>(withMeta meta: MType, links: LType) -> ToOneRelationship<ResourceObject, MType, LType> {
|
||||
return ToOneRelationship(resourceObject: self, meta: meta, links: links)
|
||||
}
|
||||
@@ -419,20 +431,20 @@ public extension ResourceObject where EntityRawIdType: JSONAPI.RawIdType {
|
||||
|
||||
// MARK: - Identifying Unidentified Entities
|
||||
public extension ResourceObject where EntityRawIdType == Unidentified {
|
||||
/// Create a new ResourceObject from this one with a newly created
|
||||
/// Create a new `ResourceObject` from this one with a newly created
|
||||
/// unique Id of the given type.
|
||||
func identified<RawIdType: CreatableRawIdType>(byType: RawIdType.Type) -> ResourceObject<Description, MetaType, LinksType, RawIdType> {
|
||||
return .init(attributes: attributes, relationships: relationships, meta: meta, links: links)
|
||||
}
|
||||
|
||||
/// Create a new ResourceObject from this one with the given Id.
|
||||
/// Create a new `ResourceObject` from this one with the given Id.
|
||||
func identified<RawIdType: JSONAPI.RawIdType>(by id: RawIdType) -> ResourceObject<Description, MetaType, LinksType, RawIdType> {
|
||||
return .init(id: ResourceObject<Description, MetaType, LinksType, RawIdType>.Identifier(rawValue: id), attributes: attributes, relationships: relationships, meta: meta, links: links)
|
||||
}
|
||||
}
|
||||
|
||||
public extension ResourceObject where EntityRawIdType: CreatableRawIdType {
|
||||
/// Create a copy of this ResourceObject with a new unique Id.
|
||||
/// Create a copy of this `ResourceObject` with a new unique Id.
|
||||
func withNewIdentifier() -> ResourceObject {
|
||||
return ResourceObject(attributes: attributes, relationships: relationships, meta: meta, links: links)
|
||||
}
|
||||
@@ -580,62 +592,63 @@ infix operator ~>
|
||||
|
||||
// MARK: - Codable
|
||||
private enum ResourceObjectCodingKeys: String, CodingKey {
|
||||
case type = "type"
|
||||
case id = "id"
|
||||
case attributes = "attributes"
|
||||
case relationships = "relationships"
|
||||
case meta = "meta"
|
||||
case links = "links"
|
||||
case type = "type"
|
||||
case id = "id"
|
||||
case attributes = "attributes"
|
||||
case relationships = "relationships"
|
||||
case meta = "meta"
|
||||
case links = "links"
|
||||
}
|
||||
|
||||
public extension ResourceObject {
|
||||
func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: ResourceObjectCodingKeys.self)
|
||||
|
||||
try container.encode(ResourceObject.jsonType, forKey: .type)
|
||||
|
||||
if EntityRawIdType.self != Unidentified.self {
|
||||
try container.encode(id, forKey: .id)
|
||||
}
|
||||
|
||||
if Description.Attributes.self != NoAttributes.self {
|
||||
try container.encode(attributes, forKey: .attributes)
|
||||
}
|
||||
|
||||
if Description.Relationships.self != NoRelationships.self {
|
||||
try container.encode(relationships, forKey: .relationships)
|
||||
}
|
||||
func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: ResourceObjectCodingKeys.self)
|
||||
|
||||
if MetaType.self != NoMetadata.self {
|
||||
try container.encode(meta, forKey: .meta)
|
||||
}
|
||||
try container.encode(ResourceObject.jsonType, forKey: .type)
|
||||
|
||||
if LinksType.self != NoLinks.self {
|
||||
try container.encode(links, forKey: .links)
|
||||
}
|
||||
}
|
||||
if EntityRawIdType.self != Unidentified.self {
|
||||
try container.encode(id, forKey: .id)
|
||||
}
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: ResourceObjectCodingKeys.self)
|
||||
|
||||
let type = try container.decode(String.self, forKey: .type)
|
||||
|
||||
guard ResourceObject.jsonType == type else {
|
||||
throw JSONAPIEncodingError.typeMismatch(expected: Description.jsonType, found: type)
|
||||
}
|
||||
if Description.Attributes.self != NoAttributes.self {
|
||||
let nestedEncoder = container.superEncoder(forKey: .attributes)
|
||||
try attributes.encode(to: nestedEncoder)
|
||||
}
|
||||
|
||||
let maybeUnidentified = Unidentified() as? EntityRawIdType
|
||||
id = try maybeUnidentified.map { ResourceObject.Id(rawValue: $0) } ?? container.decode(ResourceObject.Id.self, forKey: .id)
|
||||
if Description.Relationships.self != NoRelationships.self {
|
||||
try container.encode(relationships, forKey: .relationships)
|
||||
}
|
||||
|
||||
attributes = try (NoAttributes() as? Description.Attributes) ??
|
||||
container.decode(Description.Attributes.self, forKey: .attributes)
|
||||
if MetaType.self != NoMetadata.self {
|
||||
try container.encode(meta, forKey: .meta)
|
||||
}
|
||||
|
||||
relationships = try (NoRelationships() as? Description.Relationships)
|
||||
?? container.decodeIfPresent(Description.Relationships.self, forKey: .relationships)
|
||||
?? Description.Relationships(from: EmptyObjectDecoder())
|
||||
if LinksType.self != NoLinks.self {
|
||||
try container.encode(links, forKey: .links)
|
||||
}
|
||||
}
|
||||
|
||||
meta = try (NoMetadata() as? MetaType) ?? container.decode(MetaType.self, forKey: .meta)
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: ResourceObjectCodingKeys.self)
|
||||
|
||||
links = try (NoLinks() as? LinksType) ?? container.decode(LinksType.self, forKey: .links)
|
||||
}
|
||||
let type = try container.decode(String.self, forKey: .type)
|
||||
|
||||
guard ResourceObject.jsonType == type else {
|
||||
throw JSONAPIEncodingError.typeMismatch(expected: Description.jsonType, found: type)
|
||||
}
|
||||
|
||||
let maybeUnidentified = Unidentified() as? EntityRawIdType
|
||||
id = try maybeUnidentified.map { ResourceObject.Id(rawValue: $0) } ?? container.decode(ResourceObject.Id.self, forKey: .id)
|
||||
|
||||
attributes = try (NoAttributes() as? Description.Attributes) ??
|
||||
container.decode(Description.Attributes.self, forKey: .attributes)
|
||||
|
||||
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)
|
||||
|
||||
links = try (NoLinks() as? LinksType) ?? container.decode(LinksType.self, forKey: .links)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,22 +7,26 @@
|
||||
|
||||
/// A Transformer simply defines a static function that transforms a value.
|
||||
public protocol Transformer {
|
||||
associatedtype From
|
||||
associatedtype To
|
||||
associatedtype From
|
||||
associatedtype To
|
||||
|
||||
static func transform(_ value: From) throws -> To
|
||||
/// Turn value of type `From` into a value of type `To` or
|
||||
/// throw an error on failure.
|
||||
static func transform(_ value: From) throws -> To
|
||||
}
|
||||
|
||||
/// ReversibleTransformers define a function that reverses the transform
|
||||
/// operation.
|
||||
public protocol ReversibleTransformer: Transformer {
|
||||
static func reverse(_ value: To) throws -> From
|
||||
/// Turn a value of type `To` into a value of type `From` or
|
||||
/// throw an error on failure.
|
||||
static func reverse(_ value: To) throws -> From
|
||||
}
|
||||
|
||||
/// The IdentityTransformer does not perform any transformation on a value.
|
||||
public enum IdentityTransformer<T>: ReversibleTransformer {
|
||||
public static func transform(_ value: T) throws -> T { return value }
|
||||
public static func reverse(_ value: T) throws -> T { return value }
|
||||
public static func transform(_ value: T) throws -> T { return value }
|
||||
public static func reverse(_ value: T) throws -> T { return value }
|
||||
}
|
||||
|
||||
// MARK: - Validator
|
||||
@@ -37,15 +41,16 @@ public protocol Validator: ReversibleTransformer where From == To {
|
||||
}
|
||||
|
||||
extension Validator {
|
||||
public static func reverse(_ value: To) throws -> To {
|
||||
let _ = try transform(value)
|
||||
return value
|
||||
}
|
||||
public static func reverse(_ value: To) throws -> To {
|
||||
let _ = try transform(value)
|
||||
return value
|
||||
}
|
||||
|
||||
/// Validate the given value and then return it if valid.
|
||||
/// throws if invalid.
|
||||
public static func validate(_ value: To) throws -> To {
|
||||
let _ = try transform(value)
|
||||
return value
|
||||
}
|
||||
/// throws an erro if invalid.
|
||||
/// - returns: The same value passed in, if it was valid.
|
||||
public static func validate(_ value: To) throws -> To {
|
||||
let _ = try transform(value)
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,204 @@
|
||||
//
|
||||
// SparseEncoder.swift
|
||||
//
|
||||
//
|
||||
// Created by Mathew Polzin on 8/4/19.
|
||||
//
|
||||
|
||||
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 unkeyedContainer() -> UnkeyedEncodingContainer {
|
||||
return wrappedEncoder.unkeyedContainer()
|
||||
}
|
||||
|
||||
public func singleValueContainer() -> SingleValueEncodingContainer {
|
||||
return wrappedEncoder.singleValueContainer()
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
/// Ask the container whether the given key should be encoded.
|
||||
public 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(
|
||||
// TODO: not needed by JSONAPI library, but for completeness could
|
||||
// add an EmptyObjectEncoder that can be returned here so that
|
||||
// at least nothing gets encoded within the nested container
|
||||
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: not needed by JSONAPI library, but for completeness could
|
||||
// add an EmptyObjectEncoder that can be returned here so that
|
||||
// at least nothing gets encoded within the nested container
|
||||
return wrappedContainer.nestedUnkeyedContainer(forKey: key)
|
||||
}
|
||||
|
||||
return wrappedContainer.nestedUnkeyedContainer(forKey: key)
|
||||
}
|
||||
|
||||
public mutating func superEncoder() -> Encoder {
|
||||
return SparseFieldEncoder(wrapping: wrappedContainer.superEncoder(),
|
||||
encoding: allowedKeys)
|
||||
}
|
||||
|
||||
public mutating func superEncoder(forKey key: Key) -> Encoder {
|
||||
guard shouldAllow(key: key) else {
|
||||
// NOTE: We are creating a sparse field encoder with no allowed keys
|
||||
// here because the given key should not be allowed.
|
||||
return SparseFieldEncoder(wrapping: wrappedContainer.superEncoder(forKey: key),
|
||||
encoding: [SparseKey]())
|
||||
}
|
||||
|
||||
return SparseFieldEncoder(wrapping: wrappedContainer.superEncoder(forKey: key),
|
||||
encoding: allowedKeys)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
//
|
||||
// SparseFieldset.swift
|
||||
//
|
||||
//
|
||||
// Created by Mathew Polzin on 8/4/19.
|
||||
//
|
||||
|
||||
/// A SparseFieldset represents an `Encodable` subset of the fields
|
||||
/// a `ResourceObject` would normally encode. Currently, you can
|
||||
/// only apply sparse fieldset's to `ResourceObject.Attributes`.
|
||||
public struct SparseFieldset<
|
||||
Description: JSONAPI.ResourceObjectDescription,
|
||||
MetaType: JSONAPI.Meta,
|
||||
LinksType: JSONAPI.Links,
|
||||
EntityRawIdType: JSONAPI.MaybeRawId
|
||||
>: EncodablePrimaryResource where Description.Attributes: SparsableAttributes {
|
||||
|
||||
/// The `ResourceObject` type this `SparseFieldset` is capable of modifying.
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
public extension ResourceObject where Description.Attributes: SparsableAttributes {
|
||||
|
||||
/// Get a Sparse Fieldset of this `ResourceObject` that can be encoded
|
||||
/// as a `SparsePrimaryResource`.
|
||||
func sparse(with fields: [Description.Attributes.CodingKeys]) -> SparseFieldset<Description, MetaType, LinksType, EntityRawIdType> {
|
||||
return SparseFieldset(self, fields: fields)
|
||||
}
|
||||
}
|
||||
|
||||
public extension ResourceObject where Description.Attributes: SparsableAttributes {
|
||||
|
||||
/// The `SparseFieldset` type for this `ResourceObject`
|
||||
typealias SparseType = SparseFieldset<Description, MetaType, LinksType, EntityRawIdType>
|
||||
}
|
||||
@@ -11,6 +11,34 @@ import Poly
|
||||
|
||||
class DocumentTests: XCTestCase {
|
||||
|
||||
func test_genericDocFunc() {
|
||||
func test<Doc: JSONAPIDocument>(_ doc: Doc) {
|
||||
let _ = encoded(value: doc)
|
||||
|
||||
XCTAssert(Doc.PrimaryResourceBody.self == NoResourceBody.self)
|
||||
XCTAssert(Doc.MetaType.self == NoMetadata.self)
|
||||
XCTAssert(Doc.LinksType.self == NoLinks.self)
|
||||
XCTAssert(Doc.IncludeType.self == NoIncludes.self)
|
||||
XCTAssert(Doc.APIDescription.self == NoAPIDescription.self)
|
||||
XCTAssert(Doc.Error.self == UnknownJSONAPIError.self)
|
||||
}
|
||||
|
||||
test(JSONAPI.Document<
|
||||
NoResourceBody,
|
||||
NoMetadata,
|
||||
NoLinks,
|
||||
NoIncludes,
|
||||
NoAPIDescription,
|
||||
UnknownJSONAPIError
|
||||
>(
|
||||
apiDescription: .none,
|
||||
body: .none,
|
||||
includes: .none,
|
||||
meta: .none,
|
||||
links: .none
|
||||
))
|
||||
}
|
||||
|
||||
func test_singleDocumentNull() {
|
||||
let document = decoded(type: Document<SingleResourceBody<Article?>, NoMetadata, NoLinks, NoIncludes, NoAPIDescription, UnknownJSONAPIError>.self,
|
||||
data: single_document_null)
|
||||
@@ -926,6 +954,217 @@ extension DocumentTests {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Sparse Fieldset Documents
|
||||
|
||||
extension DocumentTests {
|
||||
func test_sparsePrimaryResource() {
|
||||
let primaryResource = Book(attributes: .init(pageCount: 100),
|
||||
relationships: .init(author: "1234",
|
||||
series: []),
|
||||
meta: .none,
|
||||
links: .none)
|
||||
.sparse(with: [.pageCount])
|
||||
|
||||
let document = Document<
|
||||
SingleResourceBody<Book.SparseType>,
|
||||
NoMetadata,
|
||||
NoLinks,
|
||||
NoIncludes,
|
||||
NoAPIDescription,
|
||||
UnknownJSONAPIError
|
||||
>(apiDescription: .none,
|
||||
body: .init(resourceObject: primaryResource),
|
||||
includes: .none,
|
||||
meta: .none,
|
||||
links: .none)
|
||||
|
||||
let encoded = try! JSONEncoder().encode(document)
|
||||
|
||||
let deserialized = try! JSONSerialization.jsonObject(with: encoded,
|
||||
options: [])
|
||||
|
||||
let deserializedObj = deserialized as? [String: Any]
|
||||
|
||||
guard let deserializedObj1 = deserializedObj?["data"] as? [String: Any] else {
|
||||
XCTFail("Expected to deserialize one object from document data")
|
||||
return
|
||||
}
|
||||
|
||||
XCTAssertNotNil(deserializedObj1["id"])
|
||||
XCTAssertEqual(deserializedObj1["id"] as? String, primaryResource.resourceObject.id.rawValue)
|
||||
|
||||
XCTAssertNotNil(deserializedObj1["type"])
|
||||
XCTAssertEqual(deserializedObj1["type"] as? String, Book.jsonType)
|
||||
|
||||
XCTAssertEqual((deserializedObj1["attributes"] as? [String: Any])?.count, 1)
|
||||
XCTAssertEqual((deserializedObj1["attributes"] as? [String: Any])?["pageCount"] as? Int, 100)
|
||||
|
||||
XCTAssertNotNil(deserializedObj1["relationships"])
|
||||
}
|
||||
|
||||
func test_sparsePrimaryResourceOptionalAndNil() {
|
||||
let document = Document<
|
||||
SingleResourceBody<Book.SparseType?>,
|
||||
NoMetadata,
|
||||
NoLinks,
|
||||
NoIncludes,
|
||||
NoAPIDescription,
|
||||
UnknownJSONAPIError
|
||||
>(apiDescription: .none,
|
||||
body: .init(resourceObject: nil),
|
||||
includes: .none,
|
||||
meta: .none,
|
||||
links: .none)
|
||||
|
||||
let encoded = try! JSONEncoder().encode(document)
|
||||
|
||||
let deserialized = try! JSONSerialization.jsonObject(with: encoded,
|
||||
options: [])
|
||||
|
||||
let deserializedObj = deserialized as? [String: Any]
|
||||
|
||||
XCTAssertNotNil(deserializedObj?["data"] as? NSNull)
|
||||
}
|
||||
|
||||
func test_sparseIncludeFullPrimaryResource() {
|
||||
let bookInclude = Book(id: "444",
|
||||
attributes: .init(pageCount: 113),
|
||||
relationships: .init(author: "1234",
|
||||
series: ["443"]),
|
||||
meta: .none,
|
||||
links: .none)
|
||||
.sparse(with: [])
|
||||
|
||||
let primaryResource = Book(id: "443",
|
||||
attributes: .init(pageCount: 100),
|
||||
relationships: .init(author: "1234",
|
||||
series: ["444"]),
|
||||
meta: .none,
|
||||
links: .none)
|
||||
|
||||
let document = Document<
|
||||
SingleResourceBody<Book>,
|
||||
NoMetadata,
|
||||
NoLinks,
|
||||
Include1<Book.SparseType>,
|
||||
NoAPIDescription,
|
||||
UnknownJSONAPIError
|
||||
>(apiDescription: .none,
|
||||
body: .init(resourceObject: primaryResource),
|
||||
includes: .init(values: [.init(bookInclude)]),
|
||||
meta: .none,
|
||||
links: .none)
|
||||
|
||||
let encoded = try! JSONEncoder().encode(document)
|
||||
|
||||
let deserialized = try! JSONSerialization.jsonObject(with: encoded,
|
||||
options: [])
|
||||
|
||||
let deserializedObj = deserialized as? [String: Any]
|
||||
|
||||
guard let deserializedObj1 = deserializedObj?["data"] as? [String: Any] else {
|
||||
XCTFail("Expected to deserialize one object from document data")
|
||||
return
|
||||
}
|
||||
|
||||
XCTAssertNotNil(deserializedObj1["id"])
|
||||
XCTAssertEqual(deserializedObj1["id"] as? String, primaryResource.id.rawValue)
|
||||
|
||||
XCTAssertNotNil(deserializedObj1["type"])
|
||||
XCTAssertEqual(deserializedObj1["type"] as? String, Book.jsonType)
|
||||
|
||||
XCTAssertEqual((deserializedObj1["attributes"] as? [String: Any])?.count, 1)
|
||||
XCTAssertEqual((deserializedObj1["attributes"] as? [String: Any])?["pageCount"] as? Int, 100)
|
||||
|
||||
XCTAssertNotNil(deserializedObj1["relationships"])
|
||||
|
||||
guard let deserializedIncludes = deserializedObj?["included"] as? [Any],
|
||||
let deserializedObj2 = deserializedIncludes.first as? [String: Any] else {
|
||||
XCTFail("Expected to deserialize one incude object")
|
||||
return
|
||||
}
|
||||
|
||||
XCTAssertNotNil(deserializedObj2["id"])
|
||||
XCTAssertEqual(deserializedObj2["id"] as? String, bookInclude.resourceObject.id.rawValue)
|
||||
|
||||
XCTAssertNotNil(deserializedObj2["type"])
|
||||
XCTAssertEqual(deserializedObj2["type"] as? String, Book.jsonType)
|
||||
|
||||
XCTAssertEqual((deserializedObj2["attributes"] as? [String: Any])?.count, 0)
|
||||
|
||||
XCTAssertNotNil(deserializedObj2["relationships"])
|
||||
}
|
||||
|
||||
func test_sparseIncludeSparsePrimaryResource() {
|
||||
let bookInclude = Book(id: "444",
|
||||
attributes: .init(pageCount: 113),
|
||||
relationships: .init(author: "1234",
|
||||
series: ["443"]),
|
||||
meta: .none,
|
||||
links: .none)
|
||||
.sparse(with: [])
|
||||
|
||||
let primaryResource = Book(id: "443",
|
||||
attributes: .init(pageCount: 100),
|
||||
relationships: .init(author: "1234",
|
||||
series: ["444"]),
|
||||
meta: .none,
|
||||
links: .none)
|
||||
.sparse(with: [])
|
||||
|
||||
let document = Document<
|
||||
SingleResourceBody<Book.SparseType>,
|
||||
NoMetadata,
|
||||
NoLinks,
|
||||
Include1<Book.SparseType>,
|
||||
NoAPIDescription,
|
||||
UnknownJSONAPIError
|
||||
>(apiDescription: .none,
|
||||
body: .init(resourceObject: primaryResource),
|
||||
includes: .init(values: [.init(bookInclude)]),
|
||||
meta: .none,
|
||||
links: .none)
|
||||
|
||||
let encoded = try! JSONEncoder().encode(document)
|
||||
|
||||
let deserialized = try! JSONSerialization.jsonObject(with: encoded,
|
||||
options: [])
|
||||
|
||||
let deserializedObj = deserialized as? [String: Any]
|
||||
|
||||
guard let deserializedObj1 = deserializedObj?["data"] as? [String: Any] else {
|
||||
XCTFail("Expected to deserialize one object from document data")
|
||||
return
|
||||
}
|
||||
|
||||
XCTAssertNotNil(deserializedObj1["id"])
|
||||
XCTAssertEqual(deserializedObj1["id"] as? String, primaryResource.resourceObject.id.rawValue)
|
||||
|
||||
XCTAssertNotNil(deserializedObj1["type"])
|
||||
XCTAssertEqual(deserializedObj1["type"] as? String, Book.jsonType)
|
||||
|
||||
XCTAssertEqual((deserializedObj1["attributes"] as? [String: Any])?.count, 0)
|
||||
|
||||
XCTAssertNotNil(deserializedObj1["relationships"])
|
||||
|
||||
guard let deserializedIncludes = deserializedObj?["included"] as? [Any],
|
||||
let deserializedObj2 = deserializedIncludes.first as? [String: Any] else {
|
||||
XCTFail("Expected to deserialize one incude object")
|
||||
return
|
||||
}
|
||||
|
||||
XCTAssertNotNil(deserializedObj2["id"])
|
||||
XCTAssertEqual(deserializedObj2["id"] as? String, bookInclude.resourceObject.id.rawValue)
|
||||
|
||||
XCTAssertNotNil(deserializedObj2["type"])
|
||||
XCTAssertEqual(deserializedObj2["type"] as? String, Book.jsonType)
|
||||
|
||||
XCTAssertEqual((deserializedObj2["attributes"] as? [String: Any])?.count, 0)
|
||||
|
||||
XCTAssertNotNil(deserializedObj2["relationships"])
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Poly PrimaryResource Tests
|
||||
extension DocumentTests {
|
||||
func test_singleDocument_PolyPrimaryResource() {
|
||||
@@ -1115,6 +1354,25 @@ extension DocumentTests {
|
||||
|
||||
typealias Article = BasicEntity<ArticleType>
|
||||
|
||||
enum BookType: ResourceObjectDescription {
|
||||
static var jsonType: String { return "books" }
|
||||
|
||||
struct Attributes: JSONAPI.SparsableAttributes {
|
||||
let pageCount: Attribute<Int>
|
||||
|
||||
enum CodingKeys: String, SparsableCodingKey {
|
||||
case pageCount
|
||||
}
|
||||
}
|
||||
|
||||
struct Relationships: JSONAPI.Relationships {
|
||||
let author: ToOneRelationship<Author, NoMetadata, NoLinks>
|
||||
let series: ToManyRelationship<Book, NoMetadata, NoLinks>
|
||||
}
|
||||
}
|
||||
|
||||
typealias Book = BasicEntity<BookType>
|
||||
|
||||
struct TestPageMetadata: JSONAPI.Meta {
|
||||
let total: Int
|
||||
let limit: Int
|
||||
|
||||
@@ -12,15 +12,15 @@ class IncludedTests: XCTestCase {
|
||||
func test_zeroIncludes() {
|
||||
let includes = decoded(type: Includes<NoIncludes>.self,
|
||||
data: two_same_type_includes)
|
||||
|
||||
|
||||
XCTAssertEqual(includes.count, 0)
|
||||
}
|
||||
|
||||
func test_zeroIncludes_encode() {
|
||||
XCTAssertThrowsError(try JSONEncoder().encode(decoded(type: Includes<NoIncludes>.self,
|
||||
data: two_same_type_includes)))
|
||||
}
|
||||
|
||||
XCTAssertThrowsError(try JSONEncoder().encode(decoded(type: Includes<NoIncludes>.self,
|
||||
data: two_same_type_includes)))
|
||||
}
|
||||
|
||||
func test_OneInclude() {
|
||||
let includes = decoded(type: Includes<Include1<TestEntity>>.self,
|
||||
data: one_include)
|
||||
@@ -32,7 +32,7 @@ class IncludedTests: XCTestCase {
|
||||
test_DecodeEncodeEquality(type: Includes<Include1<TestEntity>>.self,
|
||||
data: one_include)
|
||||
}
|
||||
|
||||
|
||||
func test_TwoSameIncludes() {
|
||||
let includes = decoded(type: Includes<Include1<TestEntity>>.self,
|
||||
data: two_same_type_includes)
|
||||
@@ -57,7 +57,7 @@ class IncludedTests: XCTestCase {
|
||||
test_DecodeEncodeEquality(type: Includes<Include2<TestEntity, TestEntity2>>.self,
|
||||
data: two_different_type_includes)
|
||||
}
|
||||
|
||||
|
||||
func test_ThreeDifferentIncludes() {
|
||||
let includes = decoded(type: Includes<Include3<TestEntity, TestEntity2, TestEntity4>>.self,
|
||||
data: three_different_type_includes)
|
||||
@@ -71,11 +71,11 @@ class IncludedTests: XCTestCase {
|
||||
test_DecodeEncodeEquality(type: Includes<Include3<TestEntity, TestEntity2, TestEntity4>>.self,
|
||||
data: three_different_type_includes)
|
||||
}
|
||||
|
||||
|
||||
func test_FourDifferentIncludes() {
|
||||
let includes = decoded(type: Includes<Include4<TestEntity, TestEntity2, TestEntity4, TestEntity6>>.self,
|
||||
data: four_different_type_includes)
|
||||
|
||||
|
||||
XCTAssertEqual(includes[TestEntity.self].count, 1)
|
||||
XCTAssertEqual(includes[TestEntity2.self].count, 1)
|
||||
XCTAssertEqual(includes[TestEntity4.self].count, 1)
|
||||
@@ -86,11 +86,11 @@ class IncludedTests: XCTestCase {
|
||||
test_DecodeEncodeEquality(type: Includes<Include4<TestEntity, TestEntity2, TestEntity4, TestEntity6>>.self,
|
||||
data: four_different_type_includes)
|
||||
}
|
||||
|
||||
|
||||
func test_FiveDifferentIncludes() {
|
||||
let includes = decoded(type: Includes<Include5<TestEntity, TestEntity2, TestEntity3, TestEntity4, TestEntity6>>.self,
|
||||
data: five_different_type_includes)
|
||||
|
||||
|
||||
XCTAssertEqual(includes[TestEntity.self].count, 1)
|
||||
XCTAssertEqual(includes[TestEntity2.self].count, 1)
|
||||
XCTAssertEqual(includes[TestEntity3.self].count, 1)
|
||||
@@ -102,11 +102,11 @@ class IncludedTests: XCTestCase {
|
||||
test_DecodeEncodeEquality(type: Includes<Include5<TestEntity, TestEntity2, TestEntity3, TestEntity4, TestEntity6>>.self,
|
||||
data: five_different_type_includes)
|
||||
}
|
||||
|
||||
|
||||
func test_SixDifferentIncludes() {
|
||||
let includes = decoded(type: Includes<Include6<TestEntity, TestEntity2, TestEntity3, TestEntity4, TestEntity5, TestEntity6>>.self,
|
||||
data: six_different_type_includes)
|
||||
|
||||
|
||||
XCTAssertEqual(includes[TestEntity.self].count, 1)
|
||||
XCTAssertEqual(includes[TestEntity2.self].count, 1)
|
||||
XCTAssertEqual(includes[TestEntity3.self].count, 1)
|
||||
@@ -178,6 +178,8 @@ class IncludedTests: XCTestCase {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Appending
|
||||
|
||||
extension IncludedTests {
|
||||
func test_appending() {
|
||||
let include1 = Includes<Include2<TestEntity8, TestEntity9>>(values: [.init(TestEntity8(attributes: .none, relationships: .none, meta: .none, links: .none)), .init(TestEntity9(attributes: .none, relationships: .none, meta: .none, links: .none)), .init(TestEntity8(attributes: .none, relationships: .none, meta: .none, links: .none))])
|
||||
@@ -190,37 +192,203 @@ extension IncludedTests {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Sparse Fieldsets
|
||||
|
||||
extension IncludedTests {
|
||||
func test_OneSparseIncludeType() {
|
||||
let include1 = TestEntity(attributes: .init(foo: "hello",
|
||||
bar: 10),
|
||||
relationships: .none,
|
||||
meta: .none,
|
||||
links: .none)
|
||||
.sparse(with: [.foo])
|
||||
|
||||
let includes: Includes<Include1<TestEntity.SparseType>> = .init(values: [.init(include1)])
|
||||
|
||||
let encoded = try! JSONEncoder().encode(includes)
|
||||
|
||||
let deserialized = try! JSONSerialization.jsonObject(with: encoded,
|
||||
options: [])
|
||||
|
||||
let deserializedObj = deserialized as? [Any]
|
||||
|
||||
XCTAssertEqual(deserializedObj?.count, 1)
|
||||
|
||||
guard let deserializedObj1 = deserializedObj?.first as? [String: Any] else {
|
||||
XCTFail("Expected to deserialize one object from array")
|
||||
return
|
||||
}
|
||||
|
||||
XCTAssertNotNil(deserializedObj1["id"])
|
||||
XCTAssertEqual(deserializedObj1["id"] as? String, include1.resourceObject.id.rawValue)
|
||||
|
||||
XCTAssertNotNil(deserializedObj1["type"])
|
||||
XCTAssertEqual(deserializedObj1["type"] as? String, TestEntity.jsonType)
|
||||
|
||||
XCTAssertEqual((deserializedObj1["attributes"] as? [String: Any])?.count, 1)
|
||||
XCTAssertEqual((deserializedObj1["attributes"] as? [String: Any])?["foo"] as? String, "hello")
|
||||
|
||||
XCTAssertNil(deserializedObj1["relationships"])
|
||||
}
|
||||
|
||||
func test_TwoSparseIncludeTypes() {
|
||||
let include1 = TestEntity(attributes: .init(foo: "hello",
|
||||
bar: 10),
|
||||
relationships: .none,
|
||||
meta: .none,
|
||||
links: .none)
|
||||
.sparse(with: [.foo])
|
||||
|
||||
let include2 = TestEntity2(attributes: .init(foo: "world",
|
||||
bar: 2),
|
||||
relationships: .init(entity1: "1234"),
|
||||
meta: .none,
|
||||
links: .none)
|
||||
.sparse(with: [.bar])
|
||||
|
||||
let includes: Includes<Include2<TestEntity.SparseType, TestEntity2.SparseType>> = .init(values: [.init(include1), .init(include2)])
|
||||
|
||||
let encoded = try! JSONEncoder().encode(includes)
|
||||
|
||||
let deserialized = try! JSONSerialization.jsonObject(with: encoded,
|
||||
options: [])
|
||||
|
||||
let deserializedObj = deserialized as? [Any]
|
||||
|
||||
XCTAssertEqual(deserializedObj?.count, 2)
|
||||
|
||||
guard let deserializedObj1 = deserializedObj?.first as? [String: Any],
|
||||
let deserializedObj2 = deserializedObj?.last as? [String: Any] else {
|
||||
XCTFail("Expected to deserialize two objects from array")
|
||||
return
|
||||
}
|
||||
|
||||
// first include
|
||||
XCTAssertNotNil(deserializedObj1["id"])
|
||||
XCTAssertEqual(deserializedObj1["id"] as? String, include1.resourceObject.id.rawValue)
|
||||
|
||||
XCTAssertNotNil(deserializedObj1["type"])
|
||||
XCTAssertEqual(deserializedObj1["type"] as? String, TestEntity.jsonType)
|
||||
|
||||
XCTAssertEqual((deserializedObj1["attributes"] as? [String: Any])?.count, 1)
|
||||
XCTAssertEqual((deserializedObj1["attributes"] as? [String: Any])?["foo"] as? String, "hello")
|
||||
|
||||
XCTAssertNil(deserializedObj1["relationships"])
|
||||
|
||||
// second include
|
||||
XCTAssertNotNil(deserializedObj2["id"])
|
||||
XCTAssertEqual(deserializedObj2["id"] as? String, include2.resourceObject.id.rawValue)
|
||||
|
||||
XCTAssertNotNil(deserializedObj2["type"])
|
||||
XCTAssertEqual(deserializedObj2["type"] as? String, TestEntity2.jsonType)
|
||||
|
||||
XCTAssertEqual((deserializedObj2["attributes"] as? [String: Any])?.count, 1)
|
||||
XCTAssertEqual((deserializedObj2["attributes"] as? [String: Any])?["bar"] as? Int, 2)
|
||||
|
||||
XCTAssertNotNil(deserializedObj2["relationships"])
|
||||
XCTAssertNotNil((deserializedObj2["relationships"] as? [String: Any])?["entity1"])
|
||||
}
|
||||
|
||||
func test_ComboSparseAndFullIncludeTypes() {
|
||||
let include1 = TestEntity(attributes: .init(foo: "hello",
|
||||
bar: 10),
|
||||
relationships: .none,
|
||||
meta: .none,
|
||||
links: .none)
|
||||
.sparse(with: [.foo])
|
||||
|
||||
let include2 = TestEntity2(attributes: .init(foo: "world",
|
||||
bar: 2),
|
||||
relationships: .init(entity1: "1234"),
|
||||
meta: .none,
|
||||
links: .none)
|
||||
|
||||
let includes: Includes<Include2<TestEntity.SparseType, TestEntity2>> = .init(values: [.init(include1), .init(include2)])
|
||||
|
||||
let encoded = try! JSONEncoder().encode(includes)
|
||||
|
||||
let deserialized = try! JSONSerialization.jsonObject(with: encoded,
|
||||
options: [])
|
||||
|
||||
let deserializedObj = deserialized as? [Any]
|
||||
|
||||
XCTAssertEqual(deserializedObj?.count, 2)
|
||||
|
||||
guard let deserializedObj1 = deserializedObj?.first as? [String: Any],
|
||||
let deserializedObj2 = deserializedObj?.last as? [String: Any] else {
|
||||
XCTFail("Expected to deserialize two objects from array")
|
||||
return
|
||||
}
|
||||
|
||||
// first include
|
||||
XCTAssertNotNil(deserializedObj1["id"])
|
||||
XCTAssertEqual(deserializedObj1["id"] as? String, include1.resourceObject.id.rawValue)
|
||||
|
||||
XCTAssertNotNil(deserializedObj1["type"])
|
||||
XCTAssertEqual(deserializedObj1["type"] as? String, TestEntity.jsonType)
|
||||
|
||||
XCTAssertEqual((deserializedObj1["attributes"] as? [String: Any])?.count, 1)
|
||||
XCTAssertEqual((deserializedObj1["attributes"] as? [String: Any])?["foo"] as? String, "hello")
|
||||
|
||||
XCTAssertNil(deserializedObj1["relationships"])
|
||||
|
||||
// second include
|
||||
XCTAssertNotNil(deserializedObj2["id"])
|
||||
XCTAssertEqual(deserializedObj2["id"] as? String, include2.id.rawValue)
|
||||
|
||||
XCTAssertNotNil(deserializedObj2["type"])
|
||||
XCTAssertEqual(deserializedObj2["type"] as? String, TestEntity2.jsonType)
|
||||
|
||||
XCTAssertEqual((deserializedObj2["attributes"] as? [String: Any])?.count, 2)
|
||||
XCTAssertEqual((deserializedObj2["attributes"] as? [String: Any])?["foo"] as? String, "world")
|
||||
XCTAssertEqual((deserializedObj2["attributes"] as? [String: Any])?["bar"] as? Int, 2)
|
||||
|
||||
XCTAssertNotNil(deserializedObj2["relationships"])
|
||||
XCTAssertNotNil((deserializedObj2["relationships"] as? [String: Any])?["entity1"])
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Test types
|
||||
extension IncludedTests {
|
||||
enum TestEntityType: ResourceObjectDescription {
|
||||
enum TestEntityType: ResourceObjectDescription {
|
||||
|
||||
typealias Relationships = NoRelationships
|
||||
typealias Relationships = NoRelationships
|
||||
|
||||
public static var jsonType: String { return "test_entity1" }
|
||||
public static var jsonType: String { return "test_entity1" }
|
||||
|
||||
public struct Attributes: JSONAPI.Attributes {
|
||||
let foo: Attribute<String>
|
||||
let bar: Attribute<Int>
|
||||
}
|
||||
}
|
||||
public struct Attributes: JSONAPI.SparsableAttributes {
|
||||
let foo: Attribute<String>
|
||||
let bar: Attribute<Int>
|
||||
|
||||
typealias TestEntity = BasicEntity<TestEntityType>
|
||||
public enum CodingKeys: String, Equatable, CodingKey {
|
||||
case foo
|
||||
case bar
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum TestEntityType2: ResourceObjectDescription {
|
||||
typealias TestEntity = BasicEntity<TestEntityType>
|
||||
|
||||
public static var jsonType: String { return "test_entity2" }
|
||||
enum TestEntityType2: ResourceObjectDescription {
|
||||
|
||||
public struct Relationships: JSONAPI.Relationships {
|
||||
let entity1: ToOneRelationship<TestEntity, NoMetadata, NoLinks>
|
||||
}
|
||||
public static var jsonType: String { return "test_entity2" }
|
||||
|
||||
public struct Attributes: JSONAPI.Attributes {
|
||||
let foo: Attribute<String>
|
||||
let bar: Attribute<Int>
|
||||
}
|
||||
}
|
||||
public struct Relationships: JSONAPI.Relationships {
|
||||
let entity1: ToOneRelationship<TestEntity, NoMetadata, NoLinks>
|
||||
}
|
||||
|
||||
typealias TestEntity2 = BasicEntity<TestEntityType2>
|
||||
public struct Attributes: JSONAPI.SparsableAttributes {
|
||||
let foo: Attribute<String>
|
||||
let bar: Attribute<Int>
|
||||
|
||||
public enum CodingKeys: String, Equatable, CodingKey {
|
||||
case foo
|
||||
case bar
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
typealias TestEntity2 = BasicEntity<TestEntityType2>
|
||||
|
||||
enum TestEntityType3: ResourceObjectDescription {
|
||||
|
||||
|
||||
@@ -102,17 +102,108 @@ class ResourceBodyTests: XCTestCase {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Sparse Fieldsets
|
||||
|
||||
extension ResourceBodyTests {
|
||||
func test_SparseSingleBodyEncode() {
|
||||
let sparseArticle = Article(attributes: .init(title: "hello world"),
|
||||
relationships: .none,
|
||||
meta: .none,
|
||||
links: .none)
|
||||
.sparse(with: [])
|
||||
let body = SingleResourceBody(resourceObject: sparseArticle)
|
||||
|
||||
let encoded = try! JSONEncoder().encode(body)
|
||||
|
||||
let deserialized = try! JSONSerialization.jsonObject(with: encoded,
|
||||
options: [])
|
||||
|
||||
let deserializedObj = deserialized as? [String: Any]
|
||||
|
||||
XCTAssertNotNil(deserializedObj?["id"])
|
||||
XCTAssertEqual(deserializedObj?["id"] as? String, sparseArticle.resourceObject.id.rawValue)
|
||||
|
||||
XCTAssertNotNil(deserializedObj?["type"])
|
||||
XCTAssertEqual(deserializedObj?["type"] as? String, Article.jsonType)
|
||||
|
||||
XCTAssertEqual((deserializedObj?["attributes"] as? [String: Any])?.count, 0)
|
||||
|
||||
XCTAssertNil(deserializedObj?["relationships"])
|
||||
}
|
||||
|
||||
func test_SparseManyBodyEncode() {
|
||||
let fields: [Article.Attributes.CodingKeys] = [.title]
|
||||
let sparseArticle1 = Article(attributes: .init(title: "hello world"),
|
||||
relationships: .none,
|
||||
meta: .none,
|
||||
links: .none)
|
||||
.sparse(with: fields)
|
||||
let sparseArticle2 = Article(attributes: .init(title: "hello two"),
|
||||
relationships: .none,
|
||||
meta: .none,
|
||||
links: .none)
|
||||
.sparse(with: fields)
|
||||
|
||||
let body = ManyResourceBody(resourceObjects: [sparseArticle1, sparseArticle2])
|
||||
|
||||
let encoded = try! JSONEncoder().encode(body)
|
||||
|
||||
let deserialized = try! JSONSerialization.jsonObject(with: encoded,
|
||||
options: [])
|
||||
|
||||
let deserializedObj = deserialized as? [Any]
|
||||
|
||||
XCTAssertEqual(deserializedObj?.count, 2)
|
||||
|
||||
guard let deserializedObj1 = deserializedObj?.first as? [String: Any],
|
||||
let deserializedObj2 = deserializedObj?.last as? [String: Any] else {
|
||||
XCTFail("Expected to deserialize two objects from array")
|
||||
return
|
||||
}
|
||||
|
||||
// first article
|
||||
XCTAssertNotNil(deserializedObj1["id"])
|
||||
XCTAssertEqual(deserializedObj1["id"] as? String, sparseArticle1.resourceObject.id.rawValue)
|
||||
|
||||
XCTAssertNotNil(deserializedObj1["type"])
|
||||
XCTAssertEqual(deserializedObj1["type"] as? String, Article.jsonType)
|
||||
|
||||
XCTAssertEqual((deserializedObj1["attributes"] as? [String: Any])?.count, 1)
|
||||
XCTAssertEqual((deserializedObj1["attributes"] as? [String: Any])?["title"] as? String, "hello world")
|
||||
|
||||
XCTAssertNil(deserializedObj1["relationships"])
|
||||
|
||||
// second article
|
||||
XCTAssertNotNil(deserializedObj2["id"])
|
||||
XCTAssertEqual(deserializedObj2["id"] as? String, sparseArticle2.resourceObject.id.rawValue)
|
||||
|
||||
XCTAssertNotNil(deserializedObj2["type"])
|
||||
XCTAssertEqual(deserializedObj2["type"] as? String, Article.jsonType)
|
||||
|
||||
XCTAssertEqual((deserializedObj2["attributes"] as? [String: Any])?.count, 1)
|
||||
XCTAssertEqual((deserializedObj2["attributes"] as? [String: Any])?["title"] as? String, "hello two")
|
||||
|
||||
XCTAssertNil(deserializedObj2["relationships"])
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Test Types
|
||||
|
||||
extension ResourceBodyTests {
|
||||
|
||||
enum ArticleType: ResourceObjectDescription {
|
||||
public static var jsonType: String { return "articles" }
|
||||
enum ArticleType: ResourceObjectDescription {
|
||||
public static var jsonType: String { return "articles" }
|
||||
|
||||
typealias Relationships = NoRelationships
|
||||
typealias Relationships = NoRelationships
|
||||
|
||||
struct Attributes: JSONAPI.Attributes {
|
||||
let title: Attribute<String>
|
||||
}
|
||||
}
|
||||
struct Attributes: JSONAPI.SparsableAttributes {
|
||||
let title: Attribute<String>
|
||||
|
||||
typealias Article = BasicEntity<ArticleType>
|
||||
public enum CodingKeys: String, Equatable, CodingKey {
|
||||
case title
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
typealias Article = BasicEntity<ArticleType>
|
||||
}
|
||||
|
||||
@@ -0,0 +1,350 @@
|
||||
//
|
||||
// SparseFieldEncoderTests.swift
|
||||
//
|
||||
//
|
||||
// Created by Mathew Polzin on 8/5/19.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import JSONAPI
|
||||
import Foundation
|
||||
|
||||
class SparseFieldEncoderTests: XCTestCase {
|
||||
func test_AccurateCodingPath() {
|
||||
let encoder = JSONEncoder()
|
||||
XCTAssertThrowsError(try encoder.encode(Wrapper()))
|
||||
|
||||
do {
|
||||
let _ = try encoder.encode(Wrapper())
|
||||
} catch let err as Wrapper.OuterFail.FailError {
|
||||
print(err.path)
|
||||
XCTAssertEqual(err.path.first as? Wrapper.OuterFail.CodingKeys, Wrapper.OuterFail.CodingKeys.inner)
|
||||
} catch {
|
||||
XCTFail("received unexpected error during test")
|
||||
}
|
||||
}
|
||||
|
||||
func test_SkipsOmittedFields() {
|
||||
let encoder = JSONEncoder()
|
||||
|
||||
// does not throw because we omit the field that would have failed
|
||||
XCTAssertNoThrow(try encoder.encode(Wrapper(fields: [])))
|
||||
}
|
||||
|
||||
func test_EverythingArsenal_allOn() {
|
||||
let encoder = JSONEncoder()
|
||||
|
||||
let allThingsOn = try! encoder.encode(EverythingWrapper(fields: EverythingWrapper.EverythingWrapped.CodingKeys.allCases))
|
||||
|
||||
let allThingsOnDeserialized = try! JSONSerialization.jsonObject(with: allThingsOn,
|
||||
options: []) as! [String: Any]
|
||||
|
||||
XCTAssertNil(allThingsOnDeserialized["omittable"])
|
||||
XCTAssertNotNil(allThingsOnDeserialized["nullable"] as? NSNull)
|
||||
XCTAssertEqual(allThingsOnDeserialized["bool"] as? Bool, true)
|
||||
XCTAssertEqual(allThingsOnDeserialized["double"] as? Double, 10.5)
|
||||
XCTAssertEqual(allThingsOnDeserialized["string"] as? String, "hello")
|
||||
XCTAssertEqual(allThingsOnDeserialized["float"] as? Float, 1.2)
|
||||
XCTAssertEqual(allThingsOnDeserialized["int"] as? Int, 3)
|
||||
XCTAssertEqual(allThingsOnDeserialized["int8"] as? Int8, 4)
|
||||
XCTAssertEqual(allThingsOnDeserialized["int16"] as? Int16, 5)
|
||||
XCTAssertEqual(allThingsOnDeserialized["int32"] as? Int32, 6)
|
||||
XCTAssertEqual(allThingsOnDeserialized["int64"] as? Int64, 7)
|
||||
XCTAssertEqual(allThingsOnDeserialized["uint"] as? UInt, 8)
|
||||
XCTAssertEqual(allThingsOnDeserialized["uint8"] as? UInt8, 9)
|
||||
XCTAssertEqual(allThingsOnDeserialized["uint16"] as? UInt16, 10)
|
||||
XCTAssertEqual(allThingsOnDeserialized["uint32"] as? UInt32, 11)
|
||||
XCTAssertEqual(allThingsOnDeserialized["uint64"] as? UInt64, 12)
|
||||
XCTAssertEqual(allThingsOnDeserialized["nested"] as? String, "world")
|
||||
}
|
||||
|
||||
func test_EverythingArsenal_allOff() {
|
||||
let encoder = JSONEncoder()
|
||||
|
||||
let allThingsOn = try! encoder.encode(EverythingWrapper(fields: []))
|
||||
|
||||
let allThingsOnDeserialized = try! JSONSerialization.jsonObject(with: allThingsOn,
|
||||
options: []) as! [String: Any]
|
||||
|
||||
XCTAssertNil(allThingsOnDeserialized["omittable"])
|
||||
XCTAssertNil(allThingsOnDeserialized["nullable"])
|
||||
XCTAssertNil(allThingsOnDeserialized["bool"])
|
||||
XCTAssertNil(allThingsOnDeserialized["double"])
|
||||
XCTAssertNil(allThingsOnDeserialized["string"])
|
||||
XCTAssertNil(allThingsOnDeserialized["float"])
|
||||
XCTAssertNil(allThingsOnDeserialized["int"])
|
||||
XCTAssertNil(allThingsOnDeserialized["int8"])
|
||||
XCTAssertNil(allThingsOnDeserialized["int16"])
|
||||
XCTAssertNil(allThingsOnDeserialized["int32"])
|
||||
XCTAssertNil(allThingsOnDeserialized["int64"])
|
||||
XCTAssertNil(allThingsOnDeserialized["uint"])
|
||||
XCTAssertNil(allThingsOnDeserialized["uint8"])
|
||||
XCTAssertNil(allThingsOnDeserialized["uint16"])
|
||||
XCTAssertNil(allThingsOnDeserialized["uint32"])
|
||||
XCTAssertNil(allThingsOnDeserialized["uint64"])
|
||||
XCTAssertNil(allThingsOnDeserialized["nested"])
|
||||
XCTAssertEqual(allThingsOnDeserialized.count, 0)
|
||||
}
|
||||
|
||||
func test_NilEncode() {
|
||||
let encoder = JSONEncoder()
|
||||
|
||||
let nilOn = try! encoder.encode(NilWrapper(fields: [.hello]))
|
||||
let nilOff = try! encoder.encode(NilWrapper(fields: []))
|
||||
|
||||
let nilOnDeserialized = try! JSONSerialization.jsonObject(with: nilOn,
|
||||
options: []) as! [String: Any]
|
||||
|
||||
let nilOffDeserialized = try! JSONSerialization.jsonObject(with: nilOff,
|
||||
options: []) as! [String: Any]
|
||||
|
||||
XCTAssertEqual(nilOnDeserialized.count, 1)
|
||||
XCTAssertNotNil(nilOnDeserialized["hello"] as? NSNull)
|
||||
XCTAssertEqual(nilOffDeserialized.count, 0)
|
||||
}
|
||||
|
||||
func test_NestedContainers() {
|
||||
let encoder = JSONEncoder()
|
||||
|
||||
let nestedOn = try! encoder.encode(NestedWrapper(fields: [.hello, .world]))
|
||||
let nestedOff = try! encoder.encode(NestedWrapper(fields: []))
|
||||
|
||||
let nestedOnDeserialized = try! JSONSerialization.jsonObject(with: nestedOn,
|
||||
options: []) as! [String: Any]
|
||||
let nestedOffDeserialized = try! JSONSerialization.jsonObject(with: nestedOff,
|
||||
options: []) as! [String: Any]
|
||||
|
||||
XCTAssertEqual(nestedOnDeserialized.count, 2)
|
||||
XCTAssertEqual((nestedOnDeserialized["hello"] as? [String: Bool])?["nestedKey"], true)
|
||||
XCTAssertEqual((nestedOnDeserialized["world"] as? [Bool])?.first, false)
|
||||
|
||||
// NOTE: When a nested container is explicitly requested,
|
||||
// the best we can do to omit the field is to encode
|
||||
// nothing _within_ the nested container.
|
||||
XCTAssertEqual(nestedOffDeserialized.count, 2)
|
||||
// TODO: The container currently does not encode empty object
|
||||
// for the keyed nested container but I think it should.
|
||||
XCTAssertEqual((nestedOffDeserialized["hello"] as? [String: Bool])?.count, 1)
|
||||
// TODO: The container currently does not encode empty array
|
||||
// for the unkeyed nested container but I think it should.
|
||||
XCTAssertEqual((nestedOffDeserialized["world"] as? [Bool])?.count, 1)
|
||||
}
|
||||
|
||||
func test_SuperEncoderIsStillSparse() {
|
||||
let encoder = JSONEncoder()
|
||||
|
||||
let superOn = try! encoder.encode(SuperWrapper(fields: [.hello, .world]))
|
||||
let superOff = try! encoder.encode(SuperWrapper(fields: []))
|
||||
|
||||
let superOnDeserialized = try! JSONSerialization.jsonObject(with: superOn,
|
||||
options: []) as! [String: Any]
|
||||
let superOffDeserialized = try! JSONSerialization.jsonObject(with: superOff,
|
||||
options: []) as! [String: Any]
|
||||
|
||||
XCTAssertEqual(superOnDeserialized.count, 2)
|
||||
XCTAssertEqual((superOnDeserialized["hello"] as? [String: Bool])?["hello"], true)
|
||||
XCTAssertEqual((superOnDeserialized["super"] as? [String: Bool])?["world"], false)
|
||||
|
||||
// NOTE: When explicitly requesting a super encoder
|
||||
// the best we can do is tell the super encoder only
|
||||
// to encode the same keys
|
||||
XCTAssertEqual(superOffDeserialized.count, 2)
|
||||
XCTAssertEqual((superOffDeserialized["hello"] as? [String: Bool])?.count, 0)
|
||||
XCTAssertEqual((superOffDeserialized["super"] as? [String: Bool])?.count, 0)
|
||||
}
|
||||
}
|
||||
|
||||
extension SparseFieldEncoderTests {
|
||||
struct Wrapper: Encodable {
|
||||
|
||||
let fields: [OuterFail.CodingKeys]
|
||||
|
||||
init(fields: [OuterFail.CodingKeys] = OuterFail.CodingKeys.allCases) {
|
||||
self.fields = fields
|
||||
}
|
||||
|
||||
func encode(to encoder: Encoder) throws {
|
||||
let sparseEncoder = SparseFieldEncoder(wrapping: encoder,
|
||||
encoding: fields)
|
||||
try OuterFail(inner: .init()).encode(to: sparseEncoder)
|
||||
}
|
||||
|
||||
struct OuterFail: Encodable {
|
||||
let inner: InnerFail
|
||||
|
||||
public enum CodingKeys: String, Equatable, CaseIterable, CodingKey {
|
||||
case inner
|
||||
}
|
||||
|
||||
struct InnerFail: Encodable {
|
||||
func encode(to encoder: Encoder) throws {
|
||||
|
||||
throw FailError(path: encoder.codingPath)
|
||||
}
|
||||
}
|
||||
|
||||
struct FailError: Swift.Error {
|
||||
let path: [CodingKey]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct EverythingWrapper: Encodable {
|
||||
let fields: [EverythingWrapped.CodingKeys]
|
||||
|
||||
func encode(to encoder: Encoder) throws {
|
||||
let sparseEncoder = SparseFieldEncoder(wrapping: encoder,
|
||||
encoding: fields)
|
||||
|
||||
try EverythingWrapped(omittable: nil,
|
||||
nullable: .init(value: nil),
|
||||
bool: true,
|
||||
double: 10.5,
|
||||
string: "hello",
|
||||
float: 1.2,
|
||||
int: 3,
|
||||
int8: 4,
|
||||
int16: 5,
|
||||
int32: 6,
|
||||
int64: 7,
|
||||
uint: 8,
|
||||
uint8: 9,
|
||||
uint16: 10,
|
||||
uint32: 11,
|
||||
uint64: 12,
|
||||
nested: .init(value: "world"))
|
||||
.encode(to: sparseEncoder)
|
||||
}
|
||||
|
||||
struct EverythingWrapped: Encodable {
|
||||
let omittable: Int?
|
||||
let nullable: Attribute<Int?>
|
||||
let bool: Bool
|
||||
let double: Double
|
||||
let string: String
|
||||
let float: Float
|
||||
let int: Int
|
||||
let int8: Int8
|
||||
let int16: Int16
|
||||
let int32: Int32
|
||||
let int64: Int64
|
||||
let uint: UInt
|
||||
let uint8: UInt8
|
||||
let uint16: UInt16
|
||||
let uint32: UInt32
|
||||
let uint64: UInt64
|
||||
let nested: Attribute<String>
|
||||
|
||||
enum CodingKeys: String, Equatable, CaseIterable, CodingKey {
|
||||
case omittable
|
||||
case nullable
|
||||
case bool
|
||||
case double
|
||||
case string
|
||||
case float
|
||||
case int
|
||||
case int8
|
||||
case int16
|
||||
case int32
|
||||
case int64
|
||||
case uint
|
||||
case uint8
|
||||
case uint16
|
||||
case uint32
|
||||
case uint64
|
||||
case nested
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct NilWrapper: Encodable {
|
||||
let fields: [NilWrapped.CodingKeys]
|
||||
|
||||
func encode(to encoder: Encoder) throws {
|
||||
let sparseEncoder = SparseFieldEncoder(wrapping: encoder,
|
||||
encoding: fields)
|
||||
|
||||
try NilWrapped()
|
||||
.encode(to: sparseEncoder)
|
||||
}
|
||||
|
||||
struct NilWrapped: Encodable {
|
||||
func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
try container.encodeNil(forKey: .hello)
|
||||
}
|
||||
|
||||
enum CodingKeys: String, Equatable, CodingKey {
|
||||
case hello
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct NestedWrapper: Encodable {
|
||||
let fields: [NestedWrapped.CodingKeys]
|
||||
|
||||
func encode(to encoder: Encoder) throws {
|
||||
let sparseEncoder = SparseFieldEncoder(wrapping: encoder,
|
||||
encoding: fields)
|
||||
|
||||
try NestedWrapped()
|
||||
.encode(to: sparseEncoder)
|
||||
}
|
||||
|
||||
struct NestedWrapped: Encodable {
|
||||
func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
var nestedContainer1 = container.nestedContainer(keyedBy: NestedKeys.self, forKey: .hello)
|
||||
|
||||
var nestedContainer2 = container.nestedUnkeyedContainer(forKey: .world)
|
||||
|
||||
try nestedContainer1.encode(true, forKey: .nestedKey)
|
||||
try nestedContainer2.encode(false)
|
||||
}
|
||||
|
||||
enum CodingKeys: String, Equatable, CodingKey {
|
||||
case hello
|
||||
case world
|
||||
}
|
||||
|
||||
enum NestedKeys: String, CodingKey {
|
||||
case nestedKey
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct SuperWrapper: Encodable {
|
||||
let fields: [SuperWrapped.CodingKeys]
|
||||
|
||||
func encode(to encoder: Encoder) throws {
|
||||
let sparseEncoder = SparseFieldEncoder(wrapping: encoder,
|
||||
encoding: fields)
|
||||
|
||||
try SuperWrapped()
|
||||
.encode(to: sparseEncoder)
|
||||
}
|
||||
|
||||
struct SuperWrapped: Encodable {
|
||||
func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
let superEncoder1 = container.superEncoder(forKey: .hello)
|
||||
|
||||
let superEncoder2 = container.superEncoder()
|
||||
|
||||
var container1 = superEncoder1.container(keyedBy: CodingKeys.self)
|
||||
var container2 = superEncoder2.container(keyedBy: CodingKeys.self)
|
||||
|
||||
try container1.encode(true, forKey: .hello)
|
||||
try container2.encode(false, forKey: .world)
|
||||
}
|
||||
|
||||
enum CodingKeys: String, Equatable, CodingKey {
|
||||
case hello
|
||||
case world
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user