Compare commits

..

45 Commits

Author SHA1 Message Date
Mathew Polzin 232554ec51 Wrap JSONAPIDocument body up a bit nicer for use when the JSONAPIDocument does not represent errors. 2018-11-27 16:12:36 -08:00
Mathew Polzin 8f36bb40e3 Add note about the library being a bit 'opinionated' 2018-11-27 14:04:53 -08:00
Mathew Polzin 55f2f52676 minor README change 2018-11-27 13:59:01 -08:00
Mathew Polzin dd5f3b1737 Added check for nullable array attributes 2018-11-27 13:57:55 -08:00
Mathew Polzin 7d7b3d7f19 Update linuxmain 2018-11-27 13:34:22 -08:00
Mathew Polzin aedd5dc29b Add Entity validation via a function in JSONAPITestLib. 2018-11-27 13:33:55 -08:00
Mathew Polzin 3964202ea2 Mention JSONAPITestLib in the README 2018-11-27 11:53:21 -08:00
Mathew Polzin dcabafd583 Add a playground page to show off JSONAPITestLib and add nil literal expressibility to ToOneRelationship 2018-11-27 11:45:15 -08:00
Mathew Polzin 9df9efc2dc rename NoRelatives to NoRelationships to better pair with NoAttributes and NoLinks 2018-11-27 11:08:53 -08:00
Mathew Polzin a55a4cbed0 Added test library, attribute literal expressibility, and tests 2018-11-27 10:54:51 -08:00
Mathew Polzin 6f6ed87b4c Update included playground 2018-11-27 09:14:47 -08:00
Mathew Polzin 59ff145aa8 Rename the included Error type. 2018-11-27 09:11:13 -08:00
Mathew Polzin 20a685e67b Add goal of making it easier to construct Attributes values 2018-11-26 22:29:31 -08:00
Mathew Polzin 3d8d10584b Add typealiases to make accessing Entity Relationships and Attributes types more convenient 2018-11-26 22:27:10 -08:00
Mathew Polzin b93580c900 Allow ManyResourceBody to use any PrimaryResource type 2018-11-26 18:57:34 -08:00
Mathew Polzin 8927938d56 Rename URL to JSONAPIURL to avoid conflict with Foundation.URL 2018-11-26 18:29:24 -08:00
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
44 changed files with 3338 additions and 637 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]
@@ -0,0 +1,24 @@
//: [Previous](@previous)
import Foundation
import JSONAPI
import JSONAPITestLib
/*******
Please enjoy these examples, but allow me the forced casting and the lack of error checking for the sake of brevity.
********/
// MARK: - Literal Expressibility
// The JSONAPITestLib provides literal expressibility for key types to
// make creating tests easier
let dog = Dog(id: "1234", attributes: Dog.Attributes(name: "Buddy"), relationships: Dog.Relationships(owner: nil))
// MARK: - JSON API structure checking
// The JSONAPITestLib provides a `check` function for each Entity type
// that uses reflection to catch mistakes that are not forbidden by
// Swift's type system but will result in unexpected results when
// encoding/decoding. It is a good idea to add a `check` to each of
// your unit tests that create Entities.
try Dog.check(dog)
@@ -0,0 +1,49 @@
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>, NoMetadata, NoLinks, NoIncludes, UnknownJSONAPIError>
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.primaryData?.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>, NoMetadata, NoLinks, Include2<Dog, House>, UnknownJSONAPIError>
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.primaryData?.values
let dogsFromData = peopleResponse.body.includes?[Dog.self]
let housesFromData = peopleResponse.body.includes?[House.self]
// MARK: - Pass successfully parsed body to other parts of the code
if case let .data(bodyData) = peopleResponse.body {
print(bodyData)
} else {
print("no body data")
}
+108
View File
@@ -0,0 +1,108 @@
//
// 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 init(name: Attribute<[String]>, favoriteColor: Attribute<String>) {
self.name = name
self.favoriteColor = favoriteColor
}
}
public struct Relationships: JSONAPI.Relationships {
public let friends: ToManyRelationship<Person>
public let dogs: ToManyRelationship<Dog>
public let home: ToOneRelationship<House>
public init(friends: ToManyRelationship<Person>, dogs: ToManyRelationship<Dog>, home: ToOneRelationship<House>) {
self.friends = friends
self.dogs = dogs
self.home = home
}
}
}
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 init(name: Attribute<String>) {
self.name = name
}
}
public struct Relationships: JSONAPI.Relationships {
public let owner: ToOneRelationship<Person?>
public init(owner: ToOneRelationship<Person?>) {
self.owner = owner
}
}
}
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 = NoRelationships
}
public typealias House = ExampleEntity<HouseDescription>
+7
View File
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<playground version='6.0' target-platform='macos' executeOnSourceChanges='false'>
<pages>
<page name='Test Library'/>
<page name='Usage'/>
</pages>
</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
}
}
]
+9 -5
View File
@@ -6,23 +6,27 @@ import PackageDescription
let package = Package(
name: "JSONAPI",
products: [
// Products define the executables and libraries produced by a package, and make them visible to other packages.
.library(
name: "JSONAPI",
targets: ["JSONAPI"]),
.library(
name: "JSONAPITestLib",
targets: ["JSONAPITestLib"])
],
dependencies: [
.package(url: "https://github.com/antitypical/Result.git", from: "4.0.0")
// antitypical/Result without the Foundation requirement:
.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.
// Targets can depend on other targets in this package, and on products in packages which this package depends on.
.target(
name: "JSONAPI",
dependencies: ["Result"]),
.target(
name: "JSONAPITestLib",
dependencies: ["JSONAPI"]),
.testTarget(
name: "JSONAPITests",
dependencies: ["JSONAPI"]),
dependencies: ["JSONAPITestLib"])
],
swiftLanguageVersions: [.v4_2]
)
+86 -25
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/
@@ -13,16 +13,29 @@ The primary goals of this framework are:
3. Do not sacrifice type safety.
4. Be platform agnostic so that Swift code can be written once and used by both the client and the server.
### Caveat
The big caveat is that, although the aim is to support the JSON API spec, this framework ends up being _naturally_ opinionated about certain things that the API Spec does not specify. These caveats are largely a side effect of attempting to write the library in a "Swifty" way.
If you find that something in the JSON API v1.0 Spec is not explicitly missing from the **Project Status** checklist but this library does not support it, please let me know! I want to keep working towards a library implementation that is useful in any application.
## Dev Environment
### Prerequisites
1. Swift 4.2+ and Swift Package Manager
### Xcode project
To create an Xcode project for JSONAPI, run
`swift package generate-xcodeproj`
## Project Status
### Decoding
#### Document
- [x] `data`
- [x] `included`
- [x] `errors` (untested)
- [ ] `meta`
- [x] `errors`
- [x] `meta`
- [ ] `jsonapi`
- [ ] `links`
- [x] `links`
#### Resource Object
- [x] `id`
@@ -37,14 +50,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,10 +76,14 @@ The primary goals of this framework are:
- [ ] `links`
- [ ] `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.
- [ ] Only allow `ToManyRelationship` and `ToOneRelationship` within `Relationships` struct.
#### Links Object
- [x] `href`
- [x] `meta`
### Entity Validator (using reflection)
- [x] Disallow optional array in `Attribute` (should be empty array, not `null`).
- [x] Only allow `TransformedAttribute` and its derivatives within `Attributes` struct.
- [x] Only allow `ToManyRelationship` and `ToOneRelationship` within `Relationships` struct.
### Strict Decoding/Encoding Settings
- [ ] Error (potentially while still encoding/decoding successfully) if an included entity is not related to a primary entity (Turned off by default).
@@ -71,17 +92,16 @@ 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.
- [x] Make it easier to construct `Attributes` and `Relationships` values in test cases (literal expressibility).
## Usage
### Prerequisites
1. Swift 4.2+ and Swift Package Manager
### `EntityDescription`
@@ -102,10 +122,10 @@ 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`
3. A `struct` of `Relationships` **- OR -** `typealias Relationships = NoRelationships`
Note that an `enum` type is used here for the `EntityDescription`; it could have been a `struct`, but `EntityDescription`s do not ever need to be created so an `enum` with no `case`s is a nice fit for the job.
@@ -180,7 +200,7 @@ let nullableRelative: ToOneRelationship<Person?>
An entity that does not have relationships can be described by adding the following to an `EntityDescription`:
```
typealias Relationships = NoRelatives
typealias Relationships = NoRelationships
```
`Relationship`s boil down to Ids of other entities. To access the Id of a related entity, you can use the shorthand `~>` operator with the `KeyPath` of the `Relationship` from which you want the Id. The friends of the above `Person` entity could be accessed as follows (type annotations for clarity):
@@ -225,31 +245,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, UnknownJSONAPIError>.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_.
@@ -275,3 +333,6 @@ extension String: CreatableRawIdType {
}
}
```
# JSONAPITestLib
JSONAPI comes with a test library to help you test your JSON API integration. The test library is called `JSONAPITestLib`. It provides literal expressibility for `Attribute`, `ToOneRelationship`, and `Id` in many situations so that you can easily write test `Entity` values into your unit tests. It also provides a `check()` function for each `Entity` type that can be used to catch problems with your JSONAPI structures that are not caught by Swift's type system. You can see the JSONAPITestLib in action in the Playground included with the JSONAPI repository.
+168 -17
View File
@@ -12,25 +12,114 @@
/// 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(Data)
public struct Data: Equatable {
let primary: ResourceBody
let includes: Includes<Include>
let meta: MetaType
let 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(data) = self else { return nil }
return data.primary
}
public var includes: Includes<Include>? {
guard case let .data(data) = self else { return nil }
return data.includes
}
public var meta: MetaType? {
switch self {
case .data(let data):
return data.meta
case .errors(_, meta: let metadata?, links: _):
return metadata
default:
return nil
}
}
public var links: LinksType? {
switch self {
case .data(let data):
return data.links
case .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(.init(primary: body, includes: includes, meta: meta, links: links))
}
}
extension JSONAPIDocument where IncludeType == NoIncludes {
public init(body: ResourceBody, meta: MetaType, links: LinksType) {
self.body = .data(.init(primary: body, includes: .none, meta: meta, links: links))
}
}
extension JSONAPIDocument where MetaType == NoMetadata {
public init(body: ResourceBody, includes: Includes<Include>, links: LinksType) {
self.body = .data(.init(primary: body, includes: includes, meta: .none, links: links))
}
}
extension JSONAPIDocument where LinksType == NoLinks {
public init(body: ResourceBody, includes: Includes<Include>, meta: MetaType) {
self.body = .data(.init(primary: body, includes: includes, meta: meta, links: .none))
}
}
extension JSONAPIDocument where IncludeType == NoIncludes, LinksType == NoLinks {
public init(body: ResourceBody, meta: MetaType) {
self.body = .data(.init(primary: body, includes: .none, meta: meta, links: .none))
}
}
extension JSONAPIDocument where IncludeType == NoIncludes, MetaType == NoMetadata {
public init(body: ResourceBody, links: LinksType) {
self.body = .data(.init(primary: body, includes: .none, meta: .none, links: links))
}
}
extension JSONAPIDocument where MetaType == NoMetadata, LinksType == NoLinks {
public init(body: ResourceBody, includes: Includes<Include>) {
self.body = .data(.init(primary: body, includes: includes, meta: .none, links: .none))
}
}
extension JSONAPIDocument where IncludeType == NoIncludes, MetaType == NoMetadata, LinksType == NoLinks {
public init(body: ResourceBody) {
self.body = .data(.init(primary: body, includes: .none, meta: .none, links: .none))
}
}
@@ -49,36 +138,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(.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)
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):
try container.encode(resourceBody, forKey: .data)
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(let data):
try container.encode(data.primary, forKey: .data)
if Include.self != NoIncludes.self {
try container.encode(includes, forKey: .included)
try container.encode(data.includes, forKey: .included)
}
if MetaType.self != NoMetadata.self {
try container.encode(data.meta, forKey: .meta)
}
if LinksType.self != NoLinks.self {
try container.encode(data.links, forKey: .links)
}
}
}
}
// MARK: - CustomStringConvertible
extension JSONAPIDocument: CustomStringConvertible {
public var description: String {
return "JSONAPIDocument(body: \(String(describing: body))"
}
}
+2 -2
View File
@@ -9,7 +9,7 @@ public protocol JSONAPIError: Swift.Error, Equatable, Codable {
static var unknown: Self { get }
}
public enum BasicJSONAPIError: JSONAPIError {
public enum UnknownJSONAPIError: JSONAPIError {
case unknownError
public init(from decoder: Decoder) throws {
@@ -21,7 +21,7 @@ public enum BasicJSONAPIError: JSONAPIError {
try container.encode("unknown")
}
public static var unknown: BasicJSONAPIError {
public static var unknown: UnknownJSONAPIError {
return .unknownError
}
}
+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
+32 -2
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 struct ManyResourceBody<Entity: JSONAPI.PrimaryResource>: 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 JSONAPIURL: Codable, Equatable {}
public struct Link<URL: JSONAPI.JSONAPIURL, 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() { }
}
+24 -17
View File
@@ -5,7 +5,10 @@
// Created by Mathew Polzin on 11/13/18.
//
public struct TransformAttribute<RawValue: Codable, Transformer: JSONAPI.Transformer>: Codable where Transformer.From == RawValue {
public protocol AttributeType: Codable {
}
public struct TransformedAttribute<RawValue: Codable, Transformer: JSONAPI.Transformer>: AttributeType where Transformer.From == RawValue {
private let rawValue: RawValue
public let value: Transformer.To
@@ -16,12 +19,29 @@ 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 {}
extension TransformedAttribute where Transformer == IdentityTransformer<RawValue> {
// If we are using the identity transform, we can skip the transform and guarantee no
// error is thrown.
public init(value: RawValue) {
rawValue = value
self.value = value
}
}
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 +78,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 }
}
+26 -17
View File
@@ -16,7 +16,7 @@ public typealias Attributes = Codable & Equatable
/// Can be used as `Relationships` Type for Entities that do not
/// have any Relationships.
public struct NoRelatives: Relationships {}
public struct NoRelationships: Relationships {}
/// Can be used as `Attributes` Type for Entities that do not
/// have any Attributes.
@@ -37,9 +37,12 @@ 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
typealias Attributes = Description.Attributes
typealias Relationships = Description.Relationships
}
/// An `Entity` is a single model type that can be
@@ -70,6 +73,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) {
@@ -91,27 +100,27 @@ extension Entity where Description.Attributes == NoAttributes, Identifier: Creat
}
}
extension Entity where Description.Relationships == NoRelatives {
extension Entity where Description.Relationships == NoRelationships {
public init(id: Identifier, attributes: Description.Attributes) {
self.init(id: id, attributes: attributes, relationships: NoRelatives())
self.init(id: id, attributes: attributes, relationships: NoRelationships())
}
}
extension Entity where Description.Relationships == NoRelatives, Identifier: CreatableIdType {
extension Entity where Description.Relationships == NoRelationships, Identifier: CreatableIdType {
public init(attributes: Description.Attributes) {
self.init(attributes: attributes, relationships: NoRelatives())
self.init(attributes: attributes, relationships: NoRelationships())
}
}
extension Entity where Description.Attributes == NoAttributes, Description.Relationships == NoRelatives {
extension Entity where Description.Attributes == NoAttributes, Description.Relationships == NoRelationships {
public init(id: Identifier) {
self.init(id: id, attributes: NoAttributes(), relationships: NoRelatives())
self.init(id: id, attributes: NoAttributes(), relationships: NoRelationships())
}
}
extension Entity where Description.Attributes == NoAttributes, Description.Relationships == NoRelatives, Identifier: CreatableIdType {
extension Entity where Description.Attributes == NoAttributes, Description.Relationships == NoRelationships, Identifier: CreatableIdType {
public init() {
self.init(attributes: NoAttributes(), relationships: NoRelatives())
self.init(attributes: NoAttributes(), relationships: NoRelationships())
}
}
@@ -129,21 +138,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 }
}
}
@@ -181,7 +190,7 @@ public extension Entity {
try container.encode(Entity.type, forKey: .type)
if Identifier.self != Unidentified.self {
if Identifier.self != Unidentified<Description>.self {
try container.encode(id, forKey: .id)
}
@@ -189,7 +198,7 @@ public extension Entity {
try container.encode(attributes, forKey: .attributes)
}
if Description.Relationships.self != NoRelatives.self {
if Description.Relationships.self != NoRelationships.self {
try container.encode(relationships, forKey: .relationships)
}
}
@@ -204,10 +213,10 @@ public extension Entity {
throw JSONAPIEncodingError.typeMismatch(expected: Description.type, found: type)
}
id = try (Unidentified() as? Identifier) ?? container.decode(Identifier.self, forKey: .id)
id = try (Unidentified<Description>() as? Identifier) ?? container.decode(Identifier.self, forKey: .id)
attributes = try (NoAttributes() as? Description.Attributes) ?? container.decode(Description.Attributes.self, forKey: .attributes)
relationships = try (NoRelatives() as? Description.Relationships) ?? container.decode(Description.Relationships.self, forKey: .relationships)
relationships = try (NoRelationships() as? Description.Relationships) ?? container.decode(Description.Relationships.self, forKey: .relationships)
}
}
+4 -3
View File
@@ -22,16 +22,17 @@ public protocol CreatableRawIdType: RawIdType {
extension String: RawIdType {}
public protocol Identifier: Codable, Equatable {}
public protocol Identifier: Codable, Equatable {
associatedtype EntityDescription: JSONAPI.EntityDescription
}
public struct Unidentified: Identifier, CustomStringConvertible {
public struct Unidentified<EntityDescription: JSONAPI.EntityDescription>: Identifier, CustomStringConvertible {
public init() {}
public var description: String { return "Id(Unidentified)" }
}
public protocol IdType: Identifier, CustomStringConvertible {
associatedtype EntityDescription: JSONAPI.EntityDescription
associatedtype RawType: RawIdType
var rawValue: RawType { get }
File diff suppressed because it is too large Load Diff
+16 -11
View File
@@ -5,16 +5,18 @@
// Created by Mathew Polzin on 8/31/18.
//
public protocol RelationshipType: Codable {}
/// An Entity relationship that can be encoded to or decoded from
/// a JSON API "Resource Linkage."
/// See https://jsonapi.org/format/#document-resource-object-linkage
/// A convenient typealias might make your code much more legible: `One<EntityDescription>`
public struct ToOneRelationship<Relatable: JSONAPI.OptionalRelatable>: Equatable, Codable {
public struct ToOneRelationship<Relatable: JSONAPI.OptionalRelatable>: RelationshipType, Equatable {
public let id: Relatable.WrappedIdentifier
public var ids: [Relatable.WrappedIdentifier] {
return [id]
public init(id: Relatable.WrappedIdentifier) {
self.id = id
}
}
@@ -25,8 +27,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
}
}
@@ -34,7 +36,7 @@ extension ToOneRelationship where Relatable.WrappedIdentifier == Optional<Relata
/// a JSON API "Resource Linkage."
/// See https://jsonapi.org/format/#document-resource-object-linkage
/// A convenient typealias might make your code much more legible: `Many<EntityDescription>`
public struct ToManyRelationship<Relatable: JSONAPI.Relatable>: Equatable, Codable {
public struct ToManyRelationship<Relatable: JSONAPI.Relatable>: RelationshipType, Equatable {
public let ids: [Relatable.Identifier]
@@ -92,6 +94,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 +175,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)))" }
}

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