merge w/ master

This commit is contained in:
Mathew Polzin
2019-08-23 17:53:33 -07:00
22 changed files with 1818 additions and 287 deletions
@@ -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) {
+1
View File
@@ -5,5 +5,6 @@
<page name='Usage'/>
<page name='Full Client &amp; Server Example'/>
<page name='Full Document Verbose Generation'/>
<page name='Sparse Fieldsets Example'/>
</pages>
</playground>
+1 -1
View File
@@ -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.
+50 -38
View File
@@ -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
+85 -72
View File
@@ -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 {
+3
View File
@@ -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
+31 -19
View File
@@ -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)
+61 -32
View File
@@ -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 {
+2
View File
@@ -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() }
+3
View File
@@ -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 {}
+70 -57
View File
@@ -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)
}
}
+20 -15
View File
@@ -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
+201 -33
View File
@@ -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