Compare commits

...

29 Commits

Author SHA1 Message Date
Mathew Polzin 372e2de721 update linuxmain 2018-11-25 20:07:47 -08:00
Mathew Polzin 1a2ba17f02 more documentation updates 2018-11-25 20:06:27 -08:00
Mathew Polzin 9f2c7aa2e4 Update README 2018-11-25 19:48:50 -08:00
Mathew Polzin 650937dbb9 improve test coverage of some JSONAPIDocument accessors. 2018-11-25 19:41:10 -08:00
Mathew Polzin b08cbdb0ae Add some tests around documents with links. 2018-11-25 19:12:30 -08:00
Mathew Polzin c30a2615f2 Added links to document but currently untested 2018-11-25 00:02:37 -08:00
Mathew Polzin 86e87ebf5d Add Links and tests 2018-11-24 22:46:53 -08:00
Mathew Polzin 987bf2b750 Update linuxmain 2018-11-23 20:54:41 -08:00
Mathew Polzin af1aca9cf4 More failure test cases. pretty ok test coverage at this point. 2018-11-23 20:44:12 -08:00
Mathew Polzin b97649fab6 Add some failure testing 2018-11-23 20:34:43 -08:00
Mathew Polzin 8114132189 Add attribute validation tests 2018-11-23 20:21:00 -08:00
Mathew Polzin b61a41c99b Allow EntityType and Poly to be PrimaryResource in JSONAPIDocument 2018-11-23 19:33:06 -08:00
Mathew Polzin 16b83ddbef lots of tests of Poly 2018-11-23 19:09:53 -08:00
Mathew Polzin 6dff02bd51 Update to version of Result that does not even import Foundation for tests 2018-11-23 08:58:13 -08:00
Mathew Polzin 2563edf419 Add subscript access to Poly types. Add empty test class for Poly type. Remove unneeded Foundation import. Small README change to read more clearly. 2018-11-23 08:49:55 -08:00
Mathew Polzin 98bb64268f Split Include1 through Include6 off to a new file and a type I am calling Poly. This new type will allow me to use the same structures to represent polymorphic relationships. 2018-11-22 22:52:21 -08:00
Mathew Polzin 399644b62a update readme 2018-11-22 22:23:44 -08:00
Mathew Polzin 3b1e5f7414 Switched to a fork of the Result repo that does not depend on Foundation (outside of tests) 2018-11-22 22:08:18 -08:00
Mathew Polzin bf842bd781 Remove a couple unneeded Foundation imports that snuck in 2018-11-22 21:35:58 -08:00
Mathew Polzin 7edb631f09 Update documentation 2018-11-22 21:32:44 -08:00
Mathew Polzin 3c5a356b44 Add a couple more tests around metadata. 2018-11-22 21:14:19 -08:00
Mathew Polzin f526963707 Update README and update linuxmain 2018-11-22 21:06:04 -08:00
Mathew Polzin c4032eb351 Added meta data to JSONAPIDocument. Added tests around JSONAPIDocument error finally. Needs a few more tests around metadata. 2018-11-22 21:04:58 -08:00
Mathew Polzin 7a3a1b8b65 Round off CustomStringConvertible implementations to make print output a lot easier to read. 2018-11-20 07:27:28 -08:00
Mathew Polzin b3a4591a5d comment out the rest of the once again relevant playground example 2018-11-18 21:51:22 -08:00
Mathew Polzin 23d057c47b Adding more custom string convertible implementations to reduce the verbosity of printing to terminal. expose JSONAPIDocument.Include so that its constructor can be used. 2018-11-18 21:47:00 -08:00
Mathew Polzin 3327a93df5 Moved Examples.swift into a Playground, added some initialization code required to create entities and documents in code. 2018-11-18 17:47:26 -08:00
Mathew Polzin 713fd2ba3a Add xcode workspace files to git ignore. 2018-11-18 15:53:00 -08:00
Mathew Polzin 072479c160 Move Transformer into its own class and add a Validator protocol based on Transformer 2018-11-18 15:49:06 -08:00
31 changed files with 2631 additions and 582 deletions
+1
View File
@@ -2,3 +2,4 @@
/.build
/Packages
/*.xcodeproj
/*.xcworkspace
-78
View File
@@ -1,78 +0,0 @@
//
// Examples.swift
// JSONAPI
//
// Created by Mathew Polzin on 11/12/18.
//
import Foundation
import JSONAPI
/*******
Please enjoy these examples, but allow me the forced casting and the lack of error checking for the sake of brevity.
********/
typealias ExampleEntity<Description: EntityDescription> = Entity<Description, Id<String, Description>>
// MARK: - A few resource objects (entities)
enum PersonDescription: EntityDescription {
static var type: String { return "people" }
struct Attributes: JSONAPI.Attributes {
let name: [String]
let favoriteColor: String
}
struct Relationships: JSONAPI.Relationships {
let friends: ToManyRelationship<Person>
let dogs: ToManyRelationship<Dog>
let home: ToOneRelationship<House>
}
}
typealias Person = ExampleEntity<PersonDescription>
enum DogDescription: EntityDescription {
static var type: String { return "dogs" }
struct Attributes: JSONAPI.Attributes {
let name: String
}
struct Relationships: JSONAPI.Relationships {
let owner: ToOneRelationship<Person?>
}
}
typealias Dog = ExampleEntity<DogDescription>
enum HouseDescription: EntityDescription {
static var type: String { return "houses" }
typealias Attributes = NoAttributes
typealias Relationships = NoRelatives
}
typealias House = ExampleEntity<HouseDescription>
// MARK: - Parse a response body with one Dog in it
typealias SingleDogResponse = JSONAPIDocument<SingleResourceBody<Dog>, NoIncludes, BasicJSONAPIError>
let dummyData = "".data(using: .utf8)!
let dogResponse = try! JSONDecoder().decode(SingleDogResponse.self, from: dummyData)
let dog = dogResponse.body.data?.primary.value
// MARK: Parse a response body with multiple people in it and dogs and houses included
typealias BatchPeopleResponse = JSONAPIDocument<ManyResourceBody<Person>, Include2<Dog, House>, BasicJSONAPIError>
let peopleResponse = try! JSONDecoder().decode(BatchPeopleResponse.self, from: dummyData)
let people = peopleResponse.body.data?.primary.values
let dogs = peopleResponse.body.data?.included[Dog.self]
let houses = peopleResponse.body.data?.included[House.self]
+41
View File
@@ -0,0 +1,41 @@
import Foundation
import JSONAPI
/*******
Please enjoy these examples, but allow me the forced casting and the lack of error checking for the sake of brevity.
********/
// MARK: - Create a request or response body with one Dog in it
let dogFromCode = try! Dog(name: "Buddy", owner: nil)
typealias SingleDogDocument = JSONAPIDocument<SingleResourceBody<Dog>, NoIncludes, BasicJSONAPIError>
let singleDogDocument = SingleDogDocument(body: SingleResourceBody(entity: dogFromCode))
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.data?.primary.value
// 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])]
let houses = [House(), House()]
let people = try! [Person(id: personIds[0], name: ["Gary", "Doe"], favoriteColor: "Orange-Red", friends: [], dogs: [dogs[0], dogs[1]], home: houses[0]), Person(id: personIds[1], name: ["Elise", "Joy"], favoriteColor: "Red", friends: [], dogs: [dogs[2]], home: houses[1])]
typealias BatchPeopleDocument = JSONAPIDocument<ManyResourceBody<Person>, Include2<Dog, House>, BasicJSONAPIError>
let includes = dogs.map { BatchPeopleDocument.Include($0) } + houses.map { BatchPeopleDocument.Include($0) }
let batchPeopleDocument = BatchPeopleDocument(body: .init(entities: people), includes: .init(values: includes))
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)
let peopleFromData = peopleResponse.body.data?.primary.values
let dogsFromData = peopleResponse.body.data?.included[Dog.self]
let housesFromData = peopleResponse.body.data?.included[House.self]
+89
View File
@@ -0,0 +1,89 @@
//
// Examples.swift
// JSONAPI
//
// Created by Mathew Polzin on 11/12/18.
//
import Foundation
import JSONAPI
/*******
Please enjoy these examples, but allow me the forced casting and the lack of error checking for the sake of brevity.
********/
// MARK: - String as CreatableRawIdType
var GlobalStringId: Int = 0
extension String: CreatableRawIdType {
public static func unique() -> String {
GlobalStringId += 1
return String(GlobalStringId)
}
}
// MARK: - Entity typealias for convenience
public typealias ExampleEntity<Description: EntityDescription> = Entity<Description, Id<String, Description>>
// MARK: - A few resource objects (entities)
public enum PersonDescription: EntityDescription {
public static var type: String { return "people" }
public struct Attributes: JSONAPI.Attributes {
public let name: Attribute<[String]>
public let favoriteColor: Attribute<String>
}
public struct Relationships: JSONAPI.Relationships {
public let friends: ToManyRelationship<Person>
public let dogs: ToManyRelationship<Dog>
public let home: ToOneRelationship<House>
}
}
public typealias Person = ExampleEntity<PersonDescription>
public extension Entity where Description == PersonDescription, Identifier == Id<String, PersonDescription> {
public init(id: Person.Identifier? = nil,name: [String], favoriteColor: String, friends: [Person], dogs: [Dog], home: House) throws {
self = try Person(id: id ?? Person.Identifier(), attributes: .init(name: .init(rawValue: name), favoriteColor: .init(rawValue: favoriteColor)), relationships: .init(friends: .init(entities: friends), dogs: .init(entities: dogs), home: .init(entity: home)))
}
}
public enum DogDescription: EntityDescription {
public static var type: String { return "dogs" }
public struct Attributes: JSONAPI.Attributes {
public let name: Attribute<String>
}
public struct Relationships: JSONAPI.Relationships {
public let owner: ToOneRelationship<Person?>
}
}
public typealias Dog = ExampleEntity<DogDescription>
public extension Entity where Description == DogDescription, Identifier == Id<String, DogDescription> {
public init(name: String, owner: Person?) throws {
self = try Dog(attributes: .init(name: .init(rawValue: name)), relationships: DogDescription.Relationships(owner: .init(entity: owner)))
}
public init(name: String, owner: Person.Identifier) throws {
self = try Dog(attributes: .init(name: .init(rawValue: name)), relationships: .init(owner: .init(id: owner)))
}
}
public enum HouseDescription: EntityDescription {
public static var type: String { return "houses" }
public typealias Attributes = NoAttributes
public typealias Relationships = NoRelatives
}
public typealias House = ExampleEntity<HouseDescription>
+4
View File
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<playground version='5.0' target-platform='macos' executeOnSourceChanges='false'>
<timeline fileName='timeline.xctimeline'/>
</playground>
+4 -4
View File
@@ -3,11 +3,11 @@
"pins": [
{
"package": "Result",
"repositoryURL": "https://github.com/antitypical/Result.git",
"repositoryURL": "https://github.com/mattpolzin/Result",
"state": {
"branch": null,
"revision": "8fc088dcf72802801efeecba76ea8fb041fb773d",
"version": "4.0.0"
"branch": "master",
"revision": "b98e238da6ea030fa7862ae6fd6500552370019c",
"version": null
}
}
]
+1 -1
View File
@@ -12,7 +12,7 @@ let package = Package(
targets: ["JSONAPI"]),
],
dependencies: [
.package(url: "https://github.com/antitypical/Result.git", from: "4.0.0")
.package(url: "https://github.com/mattpolzin/Result", .branch("master"))
],
targets: [
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
+63 -17
View File
@@ -1,7 +1,7 @@
# JSONAPI
[![MIT license](http://img.shields.io/badge/license-MIT-lightgrey.svg)](http://opensource.org/licenses/MIT) [![Swift 4.2](http://img.shields.io/badge/Swift-4.2-blue.svg)](https://swift.org) [![Build Status](https://app.bitrise.io/app/c8295b9589aa401e/status.svg?token=vzcyqWD5bQ4xqQfZsaVzNw&branch=master)](https://app.bitrise.io/app/c8295b9589aa401e)
A Swift package for encoding and decoding to *JSON API* compliant requests and responses.
A Swift package for encoding to- and decoding from *JSON API* compliant requests and responses.
See the JSON API Spec here: https://jsonapi.org/format/
@@ -19,10 +19,10 @@ The primary goals of this framework are:
#### Document
- [x] `data`
- [x] `included`
- [x] `errors` (untested)
- [ ] `meta`
- [x] `errors`
- [x] `meta`
- [ ] `jsonapi`
- [ ] `links`
- [x] `links`
#### Resource Object
- [x] `id`
@@ -37,14 +37,18 @@ The primary goals of this framework are:
- [ ] `links`
- [ ] `meta`
#### Links Object
- [x] `href`
- [x] `meta`
### Encoding
#### Document
- [x] `data`
- [x] `included`
- [x] `errors` (untested)
- [ ] `meta`
- [x] `errors`
- [x] `meta`
- [ ] `jsonapi`
- [ ] `links`
- [x] `links`
#### Resource Object
- [x] `id`
@@ -59,6 +63,10 @@ The primary goals of this framework are:
- [ ] `links`
- [ ] `meta`
#### Links Object
- [x] `href`
- [x] `meta`
### EntityDescription Validator (using reflection)
- [ ] Disallow optional array in `Attribute` and `Relationship` (should be empty array, not `null`).
- [ ] Only allow `Attribute` and `TransformAttribute` within `Attributes` struct.
@@ -71,12 +79,12 @@ The primary goals of this framework are:
- [x] Support transforms on `Attributes` values (e.g. to support different representations of `Date`)
- [x] Support ability to distinguish between `Attributes` fields that are optional (i.e. the key might not be there) and `Attributes` values that are optional (i.e. the key is guaranteed to be there but it might be `null`).
- [x] Fix `ToOneRelationship` so that it is possible to specify an optional relationship where the value is `null` rather than the key being omitted.
- [ ] Conform to `CustomStringConvertible`
- [ ] More tests around failing to decode improperly structured JSON (not bad JSON, but JSON that is not to spec)
- [x] Conform to `CustomStringConvertible`
- [x] More tests around failing to decode improperly structured JSON (not bad JSON, but JSON that is not to spec)
- [ ] Use `KeyPath` to specify `Includes` thus creating type safety around the relationship between a primary resource type and the types of included resources????
- [x] For `NoIncludes`, do not even loop over the "included" JSON API section if it exists.
- [ ] Property-based testing (using `SwiftCheck`)
- [ ] Roll my own `Result` or find an alternative that doesn't use `Foundation`.
- [x] Roll my own `Result` or find an alternative that doesn't use `Foundation`.
- [ ] Create more descriptive errors that are easier to use for troubleshooting.
## Usage
@@ -102,7 +110,7 @@ enum PersonDescription: IdentifiedEntityDescription {
}
```
To enumerate them, the requirements of an `EntityDescription` are
The requirements of an `EntityDescription` are:
1. A static `var` "type" that matches the JSON type; The JSON spec requires every *Resource Object* to have a "type".
2. A `struct` of `Attributes` **- OR -** `typealias Attributes = NoAttributes`
3. A `struct` of `Relationships` **- OR -** `typealias Relationships = NoRelatives`
@@ -225,31 +233,69 @@ enum ISODateTransformer: Transformer {
}
```
Then you define the attribute as a `TransformAttribute` instead of an `Attribute`:
Then you define the attribute as a `TransformedAttribute` instead of an `Attribute`:
```
let date: TransformAttribute<String, ISODateTransformer>
let date: TransformedAttribute<String, ISODateTransformer>
```
Note that the first generic parameter of `TransformAttribute` is the type you expect to decode from JSON, not the type you want to end up with after transformation.
#### `Validator`
You can also creator `Validator`s and `ValidatedAttribute`s. A `Validator` is just a `Transformer` that by convention does not perform a transformation. It simply `throws` if an attribute value is invalid.
### `JSONAPIDocument`
The entirety of a JSON API request or response is encoded or decoded from- or to a `JSONAPIDocument`. As an example, a JSON API response containing one `Person` and no included entities could be decoded as follows:
```
let decoder = JSONDecoder()
let responseStructure = JSONAPIDocument<SingleResourceBody<Person>, NoIncludes, BasicJSONAPIError>.self
let responseStructure = JSONAPIDocument<SingleResourceBody<Person>, NoMetadata, NoLinks, NoIncludes, BasicJSONAPIError>.self
let document = try decoder.decode(responseStructure, from: data)
```
This document is guaranteed by the JSON API spec to be "data", "metadata", or "errors." If it is "data", it may also contain "metadata" and/or other "included" resources. If it is "errors," it may also contain "metadata."
#### `ResourceBody`
The first generic type of a `JSONAPIDocument` is a `ResourceBody`. This can either be a `SingleResourceBody` or a `ManyResourceBody`. You will find zero or one `Entity` values in a JSON API document that has a `SingleResourceBody` and you will find zero or more `Entity` values in a JSON API document that has a `ManyResourceBody`.
The first generic type of a `JSONAPIDocument` is a `ResourceBody`. This can either be a `SingleResourceBody` or a `ManyResourceBody`. You will find zero or one `Entity` values in a JSON API document that has a `SingleResourceBody` and you will find zero or more `Entity` values in a JSON API document that has a `ManyResourceBody`. You can use the `Poly` types (`Poly1` through `Poly6`) to specify that a `ResourceBody` will be one of a few different types of `Entity`. These `Poly` types work in the same way as the `Include` types described below.
#### `IncludeDecoder`
If you expect a response to not have a "data" top-level key at all, then use `NoResourceBody` instead.
The second generic type of a `JSONAPIDocument` is an `IncludeDecoder`. This type controls which types of `Entity` are looked for when decoding the "included" part of the JSON API document. If you do not expect any included entities to be in the document, `NoIncludes` is the way to go. The `JSONAPI` framework provides `IncludeDecoder`s for up to six types of included entities. These are named `Include1`, `Include2`, `Include3`, and so on.
#### `MetaType`
The second generic type of a `JSONAPIDocument` is a `Meta`. This structure is entirely open-ended. As an example, the JSON API document may contain the following pagination info in its meta entry:
```
{
"meta": {
"total": 100,
"limit": 50,
"offset": 50
}
}
```
You would then create the following `Meta` type:
```
struct PageMetadata: JSONAPI.Meta {
let total: Int
let limit: Int
let offset: Int
}
```
You can always use `NoMetadata` if this JSON API feature is not needed.
#### `LinksType`
The third generic type of a `JSONAPIDocument` is a `Links` struct. A `Links` struct must contain only `Link` properties. Each `Link` property can either be a `URL` or a `URL` and some `Meta`.
You can specify `NoLinks` if the document should not contain any links.
#### `IncludeType`
The fourth generic type of a `JSONAPIDocument` is an `Include`. This type controls which types of `Entity` are looked for when decoding the "included" part of the JSON API document. If you do not expect any included entities to be in the document, `NoIncludes` is the way to go. The `JSONAPI` framework provides `Include`s for up to six types of included entities. These are named `Include1`, `Include2`, `Include3`, and so on.
**IMPORTANT**: The number trailing "Include" in these type names does not indicate a number of included entities, it indicates a number of _types_ of included entities. `Include1` can be used to decode any number of included entities as long as all the entities are of the same _type_.
+157 -15
View File
@@ -12,25 +12,105 @@
/// API uses snake case, you will want to use
/// a conversion such as the one offerred by the
/// Foundation JSONEncoder/Decoder: `KeyDecodingStrategy`
public struct JSONAPIDocument<ResourceBody: JSONAPI.ResourceBody, Include: IncludeDecoder, Error: JSONAPIError>: Equatable {
public struct JSONAPIDocument<ResourceBody: JSONAPI.ResourceBody, MetaType: JSONAPI.Meta, LinksType: JSONAPI.Links, IncludeType: JSONAPI.Include, Error: JSONAPIError>: Equatable {
public typealias Include = IncludeType
public let body: Body
// public let meta: Meta?
// public let jsonApi: APIDescription?
// public let links: Links?
public enum Body: Equatable {
case errors([Error])
case data(primary: ResourceBody, included: Includes<Include>)
case errors([Error], meta: MetaType?, links: LinksType?)
case data(primary: ResourceBody, included: Includes<Include>, meta: MetaType, links: LinksType)
public var isError: Bool {
guard case .errors = self else { return false }
return true
}
public var data: (primary: ResourceBody, included: Includes<Include>)? {
guard case let .data(primary: body, included: includes) = self else { return nil }
return (primary: body, included: includes)
public var errors: [Error]? {
guard case let .errors(errors, meta: _, links: _) = self else { return nil }
return errors
}
public var primaryData: ResourceBody? {
guard case let .data(primary: body, included: _, meta: _, links: _) = self else { return nil }
return body
}
public var includes: Includes<Include>? {
guard case let .data(primary: _, included: includes, meta: _, links: _) = self else { return nil }
return includes
}
public var meta: MetaType? {
switch self {
case .data(primary: _, included: _, meta: let metadata, links: _),
.errors(_, meta: let metadata?, links: _):
return metadata
default:
return nil
}
}
public var links: LinksType? {
switch self {
case .data(primary: _, included: _, meta: _, links: let links),
.errors(_, meta: _, links: let links?):
return links
default:
return nil
}
}
}
public init(errors: [Error], meta: MetaType? = nil, links: LinksType? = nil) {
body = .errors(errors, meta: meta, links: links)
}
public init(body: ResourceBody, includes: Includes<Include>, meta: MetaType, links: LinksType) {
self.body = .data(primary: body, included: includes, meta: meta, links: links)
}
}
extension JSONAPIDocument where IncludeType == NoIncludes {
public init(body: ResourceBody, meta: MetaType, links: LinksType) {
self.body = .data(primary: body, included: .none, meta: meta, links: links)
}
}
extension JSONAPIDocument where MetaType == NoMetadata {
public init(body: ResourceBody, includes: Includes<Include>, links: LinksType) {
self.body = .data(primary: body, included: includes, meta: .none, links: links)
}
}
extension JSONAPIDocument where LinksType == NoLinks {
public init(body: ResourceBody, includes: Includes<Include>, meta: MetaType) {
self.body = .data(primary: body, included: includes, meta: meta, links: .none)
}
}
extension JSONAPIDocument where IncludeType == NoIncludes, LinksType == NoLinks {
public init(body: ResourceBody, meta: MetaType) {
self.body = .data(primary: body, included: .none, meta: meta, links: .none)
}
}
extension JSONAPIDocument where IncludeType == NoIncludes, MetaType == NoMetadata {
public init(body: ResourceBody, links: LinksType) {
self.body = .data(primary: body, included: .none, meta: .none, links: links)
}
}
extension JSONAPIDocument where MetaType == NoMetadata, LinksType == NoLinks {
public init(body: ResourceBody, includes: Includes<Include>) {
self.body = .data(primary: body, included: includes, meta: .none, links: .none)
}
}
extension JSONAPIDocument where IncludeType == NoIncludes, MetaType == NoMetadata, LinksType == NoLinks {
public init(body: ResourceBody) {
self.body = .data(primary: body, included: .none, meta: .none, links: .none)
}
}
@@ -49,36 +129,98 @@ extension JSONAPIDocument: Codable {
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)
body = .errors(errors, meta: meta, links: links)
return
}
let data = try container.decode(ResourceBody.self, forKey: .data)
let data: ResourceBody
if let noData = NoResourceBody() as? ResourceBody {
data = noData
} else {
data = try container.decode(ResourceBody.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(primary: data, included: maybeIncludes ?? Includes<Include>.none)
body = .data(primary: data, included: maybeIncludes ?? Includes<Include>.none, meta: metaVal, links: linksVal)
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: RootCodingKeys.self)
switch body {
case .errors(let errors):
case .errors(let errors, meta: let meta, links: let links):
var errContainer = container.nestedUnkeyedContainer(forKey: .errors)
for error in errors {
try errContainer.encode(error)
}
case .data(primary: let resourceBody, included: let includes):
if MetaType.self != NoMetadata.self,
let metaVal = meta {
try container.encode(metaVal, forKey: .meta)
}
if LinksType.self != NoLinks.self,
let linksVal = links {
try container.encode(linksVal, forKey: .links)
}
case .data(primary: let resourceBody, included: let includes, let meta, links: let links):
try container.encode(resourceBody, forKey: .data)
if Include.self != NoIncludes.self {
try container.encode(includes, forKey: .included)
}
if MetaType.self != NoMetadata.self {
try container.encode(meta, forKey: .meta)
}
if LinksType.self != NoLinks.self {
try container.encode(links, forKey: .links)
}
}
}
}
// MARK: - CustomStringConvertible
extension JSONAPIDocument: CustomStringConvertible {
public var description: String {
return "JSONAPIDocument(body: \(String(describing: body))"
}
}
+27 -393
View File
@@ -5,16 +5,14 @@
// Created by Mathew Polzin on 11/10/18.
//
import Result
public typealias Include = Poly
public protocol IncludeDecoder: Codable, Equatable {}
public struct Includes<I: IncludeDecoder>: Codable, Equatable {
public struct Includes<I: Include>: Codable, Equatable {
public static var none: Includes { return .init(values: []) }
let values: [I]
private init(values: [I]) {
public init(values: [I]) {
self.values = values
}
@@ -52,433 +50,69 @@ public struct Includes<I: IncludeDecoder>: Codable, Equatable {
}
}
// MARK: - Decoding
extension Includes: CustomStringConvertible {
public var description: String {
return "Includes(\(String(describing: values))"
}
}
func decode<Entity: JSONAPI.EntityType>(_ type: Entity.Type, from container: SingleValueDecodingContainer) throws -> Result<Entity, EncodingError> {
let ret: Result<Entity, EncodingError>
do {
ret = try .success(container.decode(Entity.self))
} catch (let err as EncodingError) {
ret = .failure(err)
} catch (let err) {
ret = .failure(EncodingError.invalidValue(Entity.Description.self,
.init(codingPath: container.codingPath,
debugDescription: err.localizedDescription,
underlyingError: err)))
extension Includes where I == NoIncludes {
public init() {
values = []
}
return ret
}
// MARK: - 0 includes
public protocol _Include0: IncludeDecoder { }
public struct Include0: _Include0 {
public init(from decoder: Decoder) throws {
}
public func encode(to encoder: Encoder) throws {
throw JSONAPIEncodingError.illegalEncoding("Attempted to encode Include0, which should be represented by the absence of an 'included' entry altogether.")
}
}
public typealias Include0 = Poly0
public typealias NoIncludes = Include0
// MARK: - 1 include
public protocol _Include1: _Include0 {
associatedtype A: EntityType
var a: A? { get }
}
public enum Include1<A: EntityType>: _Include1 {
case a(A)
public var a: A? {
guard case let .a(ret) = self else { return nil }
return ret
}
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
self = .a(try container.decode(A.self))
}
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch self {
case .a(let a):
try container.encode(a)
}
}
}
extension Includes where I: _Include1 {
public typealias Include1 = Poly1
extension Includes where I: _Poly1 {
public subscript(_ lookup: I.A.Type) -> [I.A] {
return values.compactMap { $0.a }
}
}
// MARK: - 2 includes
public protocol _Include2: _Include1 {
associatedtype B: EntityType
var b: B? { get }
}
public enum Include2<A: EntityType, B: EntityType>: _Include2 {
case a(A)
case b(B)
public var a: A? {
guard case let .a(ret) = self else { return nil }
return ret
}
public var b: B? {
guard case let .b(ret) = self else { return nil }
return ret
}
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let attempts = [
try decode(A.self, from: container).map { Include2.a($0) },
try decode(B.self, from: container).map { Include2.b($0) } ]
let maybeVal: Include2<A, B>? = attempts
.compactMap { $0.value }
.first
guard let val = maybeVal else {
throw EncodingError.invalidValue(Include2<A, B>.self, .init(codingPath: decoder.codingPath, debugDescription: "Failed to find an include of the expected type. Attempts: \(attempts.map { $0.error }.compactMap { $0 })"))
}
self = val
}
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch self {
case .a(let a):
try container.encode(a)
case .b(let b):
try container.encode(b)
}
}
}
extension Includes where I: _Include2 {
public typealias Include2 = Poly2
extension Includes where I: _Poly2 {
public subscript(_ lookup: I.B.Type) -> [I.B] {
return values.compactMap { $0.b }
}
}
// MARK: - 3 includes
public protocol _Include3: _Include2 {
associatedtype C: EntityType
var c: C? { get }
}
public enum Include3<A: EntityType, B: EntityType, C: EntityType>: _Include3 {
case a(A)
case b(B)
case c(C)
public var a: A? {
guard case let .a(ret) = self else { return nil }
return ret
}
public var b: B? {
guard case let .b(ret) = self else { return nil }
return ret
}
public var c: C? {
guard case let .c(ret) = self else { return nil }
return ret
}
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let attempts = [
try decode(A.self, from: container).map { Include3.a($0) },
try decode(B.self, from: container).map { Include3.b($0) },
try decode(C.self, from: container).map { Include3.c($0) }]
let maybeVal: Include3<A, B, C>? = attempts
.compactMap { $0.value }
.first
guard let val = maybeVal else {
throw EncodingError.invalidValue(Include3<A, B, C>.self, .init(codingPath: decoder.codingPath, debugDescription: "Failed to find an include of the expected type. Attempts: \(attempts.map { $0.error }.compactMap { $0 })"))
}
self = val
}
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch self {
case .a(let a):
try container.encode(a)
case .b(let b):
try container.encode(b)
case .c(let c):
try container.encode(c)
}
}
}
extension Includes where I: _Include3 {
public typealias Include3 = Poly3
extension Includes where I: _Poly3 {
public subscript(_ lookup: I.C.Type) -> [I.C] {
return values.compactMap { $0.c }
}
}
// MARK: - 4 includes
public protocol _Include4: _Include3 {
associatedtype D: EntityType
var d: D? { get }
}
public enum Include4<A: EntityType, B: EntityType, C: EntityType, D: EntityType>: _Include4 {
case a(A)
case b(B)
case c(C)
case d(D)
public var a: A? {
guard case let .a(ret) = self else { return nil }
return ret
}
public var b: B? {
guard case let .b(ret) = self else { return nil }
return ret
}
public var c: C? {
guard case let .c(ret) = self else { return nil }
return ret
}
public var d: D? {
guard case let .d(ret) = self else { return nil }
return ret
}
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let attempts = [
try decode(A.self, from: container).map { Include4.a($0) },
try decode(B.self, from: container).map { Include4.b($0) },
try decode(C.self, from: container).map { Include4.c($0) },
try decode(D.self, from: container).map { Include4.d($0) }]
let maybeVal: Include4<A, B, C, D>? = attempts
.compactMap { $0.value }
.first
guard let val = maybeVal else {
throw EncodingError.invalidValue(Include4<A, B, C, D>.self, .init(codingPath: decoder.codingPath, debugDescription: "Failed to find an include of the expected type. Attempts: \(attempts.map { $0.error }.compactMap { $0 })"))
}
self = val
}
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch self {
case .a(let a):
try container.encode(a)
case .b(let b):
try container.encode(b)
case .c(let c):
try container.encode(c)
case .d(let d):
try container.encode(d)
}
}
}
extension Includes where I: _Include4 {
public typealias Include4 = Poly4
extension Includes where I: _Poly4 {
public subscript(_ lookup: I.D.Type) -> [I.D] {
return values.compactMap { $0.d }
}
}
// MARK: - 5 includes
public protocol _Include5: _Include4 {
associatedtype E: EntityType
var e: E? { get }
}
public enum Include5<A: EntityType, B: EntityType, C: EntityType, D: EntityType, E: EntityType>: _Include5 {
case a(A)
case b(B)
case c(C)
case d(D)
case e(E)
public var a: A? {
guard case let .a(ret) = self else { return nil }
return ret
}
public var b: B? {
guard case let .b(ret) = self else { return nil }
return ret
}
public var c: C? {
guard case let .c(ret) = self else { return nil }
return ret
}
public var d: D? {
guard case let .d(ret) = self else { return nil }
return ret
}
public var e: E? {
guard case let .e(ret) = self else { return nil }
return ret
}
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let attempts = [
try decode(A.self, from: container).map { Include5.a($0) },
try decode(B.self, from: container).map { Include5.b($0) },
try decode(C.self, from: container).map { Include5.c($0) },
try decode(D.self, from: container).map { Include5.d($0) },
try decode(E.self, from: container).map { Include5.e($0) }]
let maybeVal: Include5<A, B, C, D, E>? = attempts
.compactMap { $0.value }
.first
guard let val = maybeVal else {
throw EncodingError.invalidValue(Include5<A, B, C, D, E>.self, .init(codingPath: decoder.codingPath, debugDescription: "Failed to find an include of the expected type. Attempts: \(attempts.map { $0.error }.compactMap { $0 })"))
}
self = val
}
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch self {
case .a(let a):
try container.encode(a)
case .b(let b):
try container.encode(b)
case .c(let c):
try container.encode(c)
case .d(let d):
try container.encode(d)
case .e(let e):
try container.encode(e)
}
}
}
extension Includes where I: _Include5 {
public typealias Include5 = Poly5
extension Includes where I: _Poly5 {
public subscript(_ lookup: I.E.Type) -> [I.E] {
return values.compactMap { $0.e }
}
}
// MARK: - 6 includes
public protocol _Include6: _Include5 {
associatedtype F: EntityType
var f: F? { get }
}
public enum Include6<A: EntityType, B: EntityType, C: EntityType, D: EntityType, E: EntityType, F: EntityType>: _Include6 {
case a(A)
case b(B)
case c(C)
case d(D)
case e(E)
case f(F)
public var a: A? {
guard case let .a(ret) = self else { return nil }
return ret
}
public var b: B? {
guard case let .b(ret) = self else { return nil }
return ret
}
public var c: C? {
guard case let .c(ret) = self else { return nil }
return ret
}
public var d: D? {
guard case let .d(ret) = self else { return nil }
return ret
}
public var e: E? {
guard case let .e(ret) = self else { return nil }
return ret
}
public var f: F? {
guard case let .f(ret) = self else { return nil }
return ret
}
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let attempts = [
try decode(A.self, from: container).map { Include6.a($0) },
try decode(B.self, from: container).map { Include6.b($0) },
try decode(C.self, from: container).map { Include6.c($0) },
try decode(D.self, from: container).map { Include6.d($0) },
try decode(E.self, from: container).map { Include6.e($0) },
try decode(F.self, from: container).map { Include6.f($0) }]
let maybeVal: Include6<A, B, C, D, E, F>? = attempts
.compactMap { $0.value }
.first
guard let val = maybeVal else {
throw EncodingError.invalidValue(Include6<A, B, C, D, E, F>.self, .init(codingPath: decoder.codingPath, debugDescription: "Failed to find an include of the expected type. Attempts: \(attempts.map { $0.error }.compactMap { $0 })"))
}
self = val
}
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch self {
case .a(let a):
try container.encode(a)
case .b(let b):
try container.encode(b)
case .c(let c):
try container.encode(c)
case .d(let d):
try container.encode(d)
case .e(let e):
try container.encode(e)
case .f(let f):
try container.encode(f)
}
}
}
extension Includes where I: _Include6 {
public typealias Include6 = Poly6
extension Includes where I: _Poly6 {
public subscript(_ lookup: I.F.Type) -> [I.F] {
return values.compactMap { $0.f }
}
}
// MARK: - 7 includes
// MARK: - 8 includes
+31 -1
View File
@@ -5,15 +5,31 @@
// Created by Mathew Polzin on 11/10/18.
//
public protocol PrimaryResource: Equatable, Codable {}
public protocol ResourceBody: Codable, Equatable {
}
public struct SingleResourceBody<Entity: JSONAPI.EntityType>: ResourceBody {
public struct SingleResourceBody<Entity: JSONAPI.PrimaryResource>: ResourceBody {
public let value: Entity?
public init(entity: Entity?) {
self.value = entity
}
}
public struct ManyResourceBody<Entity: JSONAPI.EntityType>: ResourceBody {
public let values: [Entity]
public init(entities: [Entity]) {
values = entities
}
}
/// Use NoResourceBody to indicate you expect a JSON API document to not
/// contain a "data" top-level key.
public struct NoResourceBody: ResourceBody {
public static var none: NoResourceBody { return NoResourceBody() }
}
// MARK: Decodable
@@ -59,3 +75,17 @@ extension ManyResourceBody {
}
}
}
// MARK: CustomStringConvertible
extension SingleResourceBody: CustomStringConvertible {
public var description: String {
return "ResourceBody(\(String(describing: value)))"
}
}
extension ManyResourceBody: CustomStringConvertible {
public var description: String {
return "ResourceBody(\(String(describing: values)))"
}
}
+54
View File
@@ -0,0 +1,54 @@
//
// Links.swift
// JSONAPI
//
// Created by Mathew Polzin on 11/24/18.
//
/// A Links structure should contain nothing but JSONAPI.Link properties.
public protocol Links: Codable, Equatable {}
/// Use NoLinks where no links should belong to a JSON API component
public struct NoLinks: Links {
public static var none: NoLinks { return NoLinks() }
public init() {}
}
public protocol URL: Codable, Equatable {}
public struct Link<URL: JSONAPI.URL, Meta: JSONAPI.Meta>: Equatable, Codable {
public let url: URL
public let meta: Meta
}
public extension Link {
private enum CodingKeys: String, CodingKey {
case href
case meta
}
init(from decoder: Decoder) throws {
guard Meta.self == NoMetadata.self,
let noMeta = NoMetadata() as? Meta else {
let container = try decoder.container(keyedBy: CodingKeys.self)
meta = try container.decode(Meta.self, forKey: .meta)
url = try container.decode(URL.self, forKey: .href)
return
}
let container = try decoder.singleValueContainer()
url = try container.decode(URL.self)
meta = noMeta
}
func encode(to encoder: Encoder) throws {
guard Meta.self == NoMetadata.self else {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(url, forKey: .href)
try container.encode(meta, forKey: .meta)
return
}
var container = encoder.singleValueContainer()
try container.encode(url)
}
}
+26
View File
@@ -0,0 +1,26 @@
//
// Meta.swift
// JSONAPI
//
// Created by Mathew Polzin on 11/21/18.
//
/// Conform a type to this protocol to indicate it can be encoded to or decoded from
/// the meta data attached to a component of a JSON API document. Different meta data
/// can be stored all over the place: On the root document, on a resource object, on
/// link objects, etc.
///
/// JSON API Metadata is totally open ended. It can take whatever JSON-compliant structure
/// the server and client agree upon.
public protocol Meta: Codable, Equatable {
}
// We make Optional a Meta if it wraps a Meta so that Metadata can be specified as
// nullable.
extension Optional: Meta where Wrapped: Meta {}
public struct NoMetadata: Meta {
public static var none: NoMetadata { return NoMetadata() }
public init() { }
}
+12 -17
View File
@@ -5,7 +5,7 @@
// Created by Mathew Polzin on 11/13/18.
//
public struct TransformAttribute<RawValue: Codable, Transformer: JSONAPI.Transformer>: Codable where Transformer.From == RawValue {
public struct TransformedAttribute<RawValue: Codable, Transformer: JSONAPI.Transformer>: Codable where Transformer.From == RawValue {
private let rawValue: RawValue
public let value: Transformer.To
@@ -16,12 +16,20 @@ public struct TransformAttribute<RawValue: Codable, Transformer: JSONAPI.Transfo
}
}
public typealias Attribute<T: Codable> = TransformAttribute<T, IdentityTransformer<T>>
extension TransformedAttribute: CustomStringConvertible {
public var description: String {
return "Attribute<\(String(describing: Transformer.From.self)) -> \(String(describing: Transformer.To.self))>(\(String(describing: value)))"
}
}
extension TransformAttribute: Equatable where Transformer.From: Equatable, Transformer.To: Equatable {}
extension TransformedAttribute: Equatable where Transformer.From: Equatable, Transformer.To: Equatable {}
public typealias ValidatedAttribute<RawValue: Codable, Validator: JSONAPI.Validator> = TransformedAttribute<RawValue, Validator> where RawValue == Validator.From
public typealias Attribute<T: Codable> = TransformedAttribute<T, IdentityTransformer<T>>
// MARK: - Codable
extension TransformAttribute {
extension TransformedAttribute {
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
@@ -58,16 +66,3 @@ extension TransformAttribute {
try container.encode(rawValue)
}
}
// MARK: - Transformers
public protocol Transformer {
associatedtype From
associatedtype To
static func transform(_ from: From) throws -> To
}
public enum IdentityTransformer<T>: Transformer {
public static func transform(_ from: T) throws -> T { return from }
}
+10 -4
View File
@@ -37,7 +37,7 @@ public protocol EntityDescription {
/// EntityType is the protocol that Entity conforms to. This
/// protocol lets other types accept any Entity as a generic
/// specialization.
public protocol EntityType: Codable, Equatable {
public protocol EntityType: PrimaryResource {
associatedtype Description: EntityDescription
associatedtype Identifier: Equatable & Codable
}
@@ -70,6 +70,12 @@ public struct Entity<Description: JSONAPI.EntityDescription, Identifier: JSONAPI
}
}
extension Entity: CustomStringConvertible {
public var description: String {
return "Entity<\(Entity.type)>(id: \(String(describing: id)), attributes: \(String(describing: attributes)), relationships: \(String(describing: relationships)))"
}
}
// MARK: Convenience initializers
extension Entity where Identifier: CreatableIdType {
public init(attributes: Description.Attributes, relationships: Description.Relationships) {
@@ -129,21 +135,21 @@ public extension Entity {
/// Access the attribute at the given keypath. This just
/// allows you to write `entity[\.propertyName]` instead
/// of `entity.relationships.propertyName`.
subscript<T, TFRM: Transformer>(_ path: KeyPath<Description.Attributes, TransformAttribute<T, TFRM>>) -> TFRM.To {
subscript<T, TFRM: Transformer>(_ path: KeyPath<Description.Attributes, TransformedAttribute<T, TFRM>>) -> TFRM.To {
return attributes[keyPath: path].value
}
/// Access the attribute at the given keypath. This just
/// allows you to write `entity[\.propertyName]` instead
/// of `entity.relationships.propertyName`.
subscript<T, TFRM: Transformer>(_ path: KeyPath<Description.Attributes, TransformAttribute<T, TFRM>?>) -> TFRM.To? {
subscript<T, TFRM: Transformer>(_ path: KeyPath<Description.Attributes, TransformedAttribute<T, TFRM>?>) -> TFRM.To? {
return attributes[keyPath: path]?.value
}
/// Access the attribute at the given keypath. This just
/// allows you to write `entity[\.propertyName]` instead
/// of `entity.relationships.propertyName`.
subscript<T, TFRM: Transformer, U>(_ path: KeyPath<Description.Attributes, TransformAttribute<T, TFRM>?>) -> U? where TFRM.To == U? {
subscript<T, TFRM: Transformer, U>(_ path: KeyPath<Description.Attributes, TransformedAttribute<T, TFRM>?>) -> U? where TFRM.To == U? {
return attributes[keyPath: path].flatMap { $0.value }
}
}
File diff suppressed because it is too large Load Diff
+13 -6
View File
@@ -16,6 +16,10 @@ public struct ToOneRelationship<Relatable: JSONAPI.OptionalRelatable>: Equatable
public var ids: [Relatable.WrappedIdentifier] {
return [id]
}
public init(id: Relatable.WrappedIdentifier) {
self.id = id
}
}
extension ToOneRelationship where Relatable.WrappedIdentifier == Relatable.Identifier {
@@ -25,8 +29,8 @@ extension ToOneRelationship where Relatable.WrappedIdentifier == Relatable.Ident
}
extension ToOneRelationship where Relatable.WrappedIdentifier == Optional<Relatable.Identifier> {
public init(entity: Entity<Relatable.Description, Relatable.Identifier>) {
id = entity.id
public init(entity: Entity<Relatable.Description, Relatable.Identifier>?) {
id = entity?.id
}
}
@@ -92,6 +96,9 @@ private enum ResourceIdentifierCodingKeys: String, CodingKey {
public enum JSONAPIEncodingError: Swift.Error {
case typeMismatch(expected: String, found: String)
case illegalEncoding(String)
case illegalDecoding(String)
case missingOrMalformedMetadata
case missingOrMalformedLinks
}
extension ToOneRelationship {
@@ -170,10 +177,10 @@ extension ToManyRelationship {
}
// MARK: CustomStringDescribable
public extension ToOneRelationship {
var description: String { return "Relationship(\(String(describing: id)))" }
extension ToOneRelationship: CustomStringConvertible {
public var description: String { return "Relationship(\(String(describing: id)))" }
}
public extension ToManyRelationship {
var description: String { return "Relationship(\(String(describing: ids)))" }
extension ToManyRelationship: CustomStringConvertible {
public var description: String { return "Relationship(\(String(describing: ids)))" }
}
@@ -0,0 +1,31 @@
//
// Transformer.swift
// JSONAPI
//
// Created by Mathew Polzin on 11/17/18.
//
public protocol Transformer {
associatedtype From
associatedtype To
static func transform(_ from: From) throws -> To
}
public enum IdentityTransformer<T>: Transformer {
public static func transform(_ from: T) throws -> T { return from }
}
// MARK: - Validator
/// A Validator is a Transformer that throws an error if an invalid value
/// is passed to it but it does not change the type of the value. Any
/// Transformer will perform validation in one sense so a Validator is
/// really just semantic sugar (it can provide clarity in its use).
public protocol Validator: Transformer where From == To {}
extension Validator {
public static func validate(_ value: To) throws -> To {
return try transform(value)
}
}
File diff suppressed because it is too large Load Diff
@@ -28,6 +28,85 @@ let single_document_no_includes = """
}
""".data(using: .utf8)!
let single_document_no_includes_with_metadata = """
{
"data": {
"id": "1",
"type": "articles",
"relationships": {
"author": {
"data": {
"type": "authors",
"id": "33"
}
}
}
},
"meta": {
"total": 70,
"limit": 40,
"offset": 10
}
}
""".data(using: .utf8)!
let single_document_no_includes_with_links = """
{
"data": {
"id": "1",
"type": "articles",
"relationships": {
"author": {
"data": {
"type": "authors",
"id": "33"
}
}
}
},
"links": {
"link": "https://website.com",
"link2": {
"href": "https://othersite.com",
"meta": {
"hello": "world"
}
}
}
}
""".data(using: .utf8)!
let single_document_no_includes_with_metadata_with_links = """
{
"data": {
"id": "1",
"type": "articles",
"relationships": {
"author": {
"data": {
"type": "authors",
"id": "33"
}
}
}
},
"links": {
"link": "https://website.com",
"link2": {
"href": "https://othersite.com",
"meta": {
"hello": "world"
}
}
},
"meta": {
"total": 70,
"limit": 40,
"offset": 10
}
}
""".data(using: .utf8)!
let single_document_some_includes = """
{
"data": {
@@ -51,6 +130,71 @@ let single_document_some_includes = """
}
""".data(using: .utf8)!
let single_document_some_includes_with_metadata_with_links = """
{
"data": {
"id": "1",
"type": "articles",
"relationships": {
"author": {
"data": {
"type": "authors",
"id": "33"
}
}
}
},
"included": [
{
"id": "33",
"type": "authors"
}
],
"links": {
"link": "https://website.com",
"link2": {
"href": "https://othersite.com",
"meta": {
"hello": "world"
}
}
},
"meta": {
"total": 70,
"limit": 40,
"offset": 10
}
}
""".data(using: .utf8)!
let single_document_some_includes_with_metadata = """
{
"data": {
"id": "1",
"type": "articles",
"relationships": {
"author": {
"data": {
"type": "authors",
"id": "33"
}
}
}
},
"included": [
{
"id": "33",
"type": "authors"
}
],
"meta": {
"total": 70,
"limit": 40,
"offset": 10
}
}
""".data(using: .utf8)!
let many_document_no_includes = """
{
"data": [
@@ -150,3 +294,115 @@ let many_document_some_includes = """
]
}
""".data(using: .utf8)!
let error_document_no_metadata = """
{
"errors": [
{
"description": "Boooo!",
"code": 1
}
]
}
""".data(using: .utf8)!
let error_document_with_metadata = """
{
"errors": [
{
"description": "Boooo!",
"code": 1
}
],
"meta": {
"total": 70,
"limit": 40,
"offset": 10
}
}
""".data(using: .utf8)!
let error_document_with_links = """
{
"errors": [
{
"description": "Boooo!",
"code": 1
}
],
"links": {
"link": "https://website.com",
"link2": {
"href": "https://othersite.com",
"meta": {
"hello": "world"
}
}
}
}
""".data(using: .utf8)!
let error_document_with_metadata_with_links = """
{
"errors": [
{
"description": "Boooo!",
"code": 1
}
],
"meta": {
"total": 70,
"limit": 40,
"offset": 10
},
"links": {
"link": "https://website.com",
"link2": {
"href": "https://othersite.com",
"meta": {
"hello": "world"
}
}
}
}
""".data(using: .utf8)!
let metadata_document = """
{
"meta": {
"total": 100,
"limit": 50,
"offset": 0
}
}
""".data(using: .utf8)!
let metadata_document_with_links = """
{
"meta": {
"total": 100,
"limit": 50,
"offset": 0
},
"links": {
"link": "https://website.com",
"link2": {
"href": "https://othersite.com",
"meta": {
"hello": "world"
}
}
}
}
""".data(using: .utf8)!
let metadata_document_missing_metadata = """
{
}
""".data(using: .utf8)!
let metadata_document_missing_metadata2 = """
{
"meta": null
}
""".data(using: .utf8)!

Some files were not shown because too many files have changed in this diff Show More