mirror of
https://github.com/encounter/JSONAPI.git
synced 2026-03-30 11:18:38 -07:00
Merge remote-tracking branch 'origin/master' into swift-5.1
This commit is contained in:
+72
-71
@@ -4,65 +4,66 @@ import Poly
|
||||
|
||||
// MARK: - Preamble (setup)
|
||||
|
||||
// We make String a CreatableRawIdType. This is actually done in
|
||||
// Make String a CreatableRawIdType. This is actually done in
|
||||
// this Playground's Entities.swift file, so it is commented out here.
|
||||
/*
|
||||
var GlobalStringId: Int = 0
|
||||
extension String: CreatableRawIdType {
|
||||
public static func unique() -> String {
|
||||
GlobalStringId += 1
|
||||
return String(GlobalStringId)
|
||||
}
|
||||
}
|
||||
*/
|
||||
var globalStringId: Int = 0
|
||||
extension String: CreatableRawIdType {
|
||||
public static func unique() -> String {
|
||||
globalStringId += 1
|
||||
return String(globalStringId)
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
// We create a typealias given that we do not expect JSON:API Resource
|
||||
// Create a typealias because we do not expect JSON:API Resource
|
||||
// Objects for this particular API to have Metadata or Links associated
|
||||
// with them. We also expect them to have String Identifiers.
|
||||
typealias JSONEntity<Description: ResourceObjectDescription> = JSONAPI.ResourceObject<Description, NoMetadata, NoLinks, String>
|
||||
|
||||
// Similarly, we create a typealias for unidentified entities. JSON:API
|
||||
// Similarly, create a typealias for unidentified entities. JSON:API
|
||||
// only allows unidentified entities (i.e. no "id" field) for client
|
||||
// requests that create new entities. In these situations, the server
|
||||
// is expected to assign the new entity a unique ID.
|
||||
typealias UnidentifiedJSONEntity<Description: ResourceObjectDescription> = JSONAPI.ResourceObject<Description, NoMetadata, NoLinks, Unidentified>
|
||||
|
||||
// We create typealiases given that we do not expect JSON:API Relationships
|
||||
// for this particular API to have Metadata or Links associated
|
||||
// with them.
|
||||
// Create relationship typealiases because we do not expect
|
||||
// JSON:API Relationships for this particular API to have
|
||||
// Metadata or Links associated with them.
|
||||
typealias ToOneRelationship<Entity: Identifiable> = JSONAPI.ToOneRelationship<Entity, NoMetadata, NoLinks>
|
||||
typealias ToManyRelationship<Entity: Relatable> = JSONAPI.ToManyRelationship<Entity, NoMetadata, NoLinks>
|
||||
|
||||
// We create a typealias for a Document given that we do not expect
|
||||
// Create a typealias for a Document because we do not expect
|
||||
// JSON:API Documents for this particular API to have Metadata, Links,
|
||||
// useful Errors, or a JSON:API Object (i.e. APIDescription).
|
||||
// useful Errors, or an APIDescription (The *SPEC* calls this
|
||||
// "API Description" the "JSON:API Object").
|
||||
typealias Document<PrimaryResourceBody: JSONAPI.ResourceBody, IncludeType: JSONAPI.Include> = JSONAPI.Document<PrimaryResourceBody, NoMetadata, NoLinks, IncludeType, NoAPIDescription, UnknownJSONAPIError>
|
||||
|
||||
// MARK: Entity Definitions
|
||||
|
||||
enum AuthorDescription: ResourceObjectDescription {
|
||||
public static var jsonType: String { return "authors" }
|
||||
public static var jsonType: String { return "authors" }
|
||||
|
||||
public struct Attributes: JSONAPI.Attributes {
|
||||
public let name: Attribute<String>
|
||||
}
|
||||
public struct Attributes: JSONAPI.Attributes {
|
||||
public let name: Attribute<String>
|
||||
}
|
||||
|
||||
public typealias Relationships = NoRelationships
|
||||
public typealias Relationships = NoRelationships
|
||||
}
|
||||
|
||||
typealias Author = JSONEntity<AuthorDescription>
|
||||
|
||||
enum ArticleDescription: ResourceObjectDescription {
|
||||
public static var jsonType: String { return "articles" }
|
||||
public static var jsonType: String { return "articles" }
|
||||
|
||||
public struct Attributes: JSONAPI.Attributes {
|
||||
public let title: Attribute<String>
|
||||
public let abstract: Attribute<String>
|
||||
}
|
||||
public struct Attributes: JSONAPI.Attributes {
|
||||
public let title: Attribute<String>
|
||||
public let abstract: Attribute<String>
|
||||
}
|
||||
|
||||
public struct Relationships: JSONAPI.Relationships {
|
||||
public let author: ToOneRelationship<Author>
|
||||
}
|
||||
public struct Relationships: JSONAPI.Relationships {
|
||||
public let author: ToOneRelationship<Author>
|
||||
}
|
||||
}
|
||||
|
||||
typealias Article = JSONEntity<ArticleDescription>
|
||||
@@ -83,38 +84,38 @@ typealias SingleArticleDocument = Document<SingleResourceBody<Article>, NoInclud
|
||||
// that creates a document. Note that this document is the entirety
|
||||
// of a JSON:API response body.
|
||||
func articleDocument(includeAuthor: Bool) -> Either<SingleArticleDocument, SingleArticleDocumentWithIncludes> {
|
||||
// Let's pretend all of this is coming from a database:
|
||||
// Let's pretend all of this is coming from a database:
|
||||
|
||||
let authorId = Author.Identifier(rawValue: "1234")
|
||||
let authorId = Author.Identifier(rawValue: "1234")
|
||||
|
||||
let article = Article(id: .init(rawValue: "5678"),
|
||||
attributes: .init(title: .init(value: "JSON:API in Swift"),
|
||||
abstract: .init(value: "Not yet written")),
|
||||
relationships: .init(author: .init(id: authorId)),
|
||||
meta: .none,
|
||||
links: .none)
|
||||
let article = Article(id: .init(rawValue: "5678"),
|
||||
attributes: .init(title: .init(value: "JSON:API in Swift"),
|
||||
abstract: .init(value: "Not yet written")),
|
||||
relationships: .init(author: .init(id: authorId)),
|
||||
meta: .none,
|
||||
links: .none)
|
||||
|
||||
let document = SingleArticleDocument(apiDescription: .none,
|
||||
body: .init(resourceObject: article),
|
||||
includes: .none,
|
||||
meta: .none,
|
||||
links: .none)
|
||||
let document = SingleArticleDocument(apiDescription: .none,
|
||||
body: .init(resourceObject: article),
|
||||
includes: .none,
|
||||
meta: .none,
|
||||
links: .none)
|
||||
|
||||
switch includeAuthor {
|
||||
case false:
|
||||
return .a(document)
|
||||
switch includeAuthor {
|
||||
case false:
|
||||
return .init(document)
|
||||
|
||||
case true:
|
||||
let author = Author(id: authorId,
|
||||
attributes: .init(name: .init(value: "Janice Bluff")),
|
||||
relationships: .none,
|
||||
meta: .none,
|
||||
links: .none)
|
||||
case true:
|
||||
let author = Author(id: authorId,
|
||||
attributes: .init(name: .init(value: "Janice Bluff")),
|
||||
relationships: .none,
|
||||
meta: .none,
|
||||
links: .none)
|
||||
|
||||
let includes: Includes<SingleArticleDocumentWithIncludes.Include> = .init(values: [.init(author)])
|
||||
let includes: Includes<SingleArticleDocumentWithIncludes.Include> = .init(values: [.init(author)])
|
||||
|
||||
return .b(document.including(.init(values: [.init(author)])))
|
||||
}
|
||||
return .init(document.including(.init(values: [.init(author)])))
|
||||
}
|
||||
}
|
||||
|
||||
let encoder = JSONEncoder()
|
||||
@@ -124,8 +125,8 @@ encoder.outputFormatting = .prettyPrinted
|
||||
let responseBody = articleDocument(includeAuthor: true)
|
||||
let responseData = try! encoder.encode(responseBody)
|
||||
|
||||
// Next step would be encoding and setting as the HTTP body of a response.
|
||||
// we will just print it out instead:
|
||||
// Next step would be setting the HTTP body of a response.
|
||||
// We will just print it out instead:
|
||||
print("-----")
|
||||
print(String(data: responseData, encoding: .utf8)!)
|
||||
|
||||
@@ -139,31 +140,31 @@ print(String(data: otherResponseData, encoding: .utf8)!)
|
||||
// MARK: - Client Pseudo-example
|
||||
|
||||
enum NetworkError: Swift.Error {
|
||||
case serverError
|
||||
case quantityMismatch
|
||||
case serverError
|
||||
case quantityMismatch
|
||||
}
|
||||
|
||||
// Skipping over all the API stuff, here's a chunk of code that will
|
||||
// decode a document. We will assume we have made a request for a
|
||||
// single article including the author.
|
||||
func docode(articleResponseData: Data) throws -> (article: Article, author: Author) {
|
||||
let decoder = JSONDecoder()
|
||||
decoder.keyDecodingStrategy = .convertFromSnakeCase
|
||||
let decoder = JSONDecoder()
|
||||
decoder.keyDecodingStrategy = .convertFromSnakeCase
|
||||
|
||||
let articleDocument = try decoder.decode(SingleArticleDocumentWithIncludes.self, from: articleResponseData)
|
||||
let articleDocument = try decoder.decode(SingleArticleDocumentWithIncludes.self, from: articleResponseData)
|
||||
|
||||
switch articleDocument.body {
|
||||
case .data(let data):
|
||||
let authors = data.includes[Author.self]
|
||||
switch articleDocument.body {
|
||||
case .data(let data):
|
||||
let authors = data.includes[Author.self]
|
||||
|
||||
guard authors.count == 1 else {
|
||||
throw NetworkError.quantityMismatch
|
||||
}
|
||||
guard authors.count == 1 else {
|
||||
throw NetworkError.quantityMismatch
|
||||
}
|
||||
|
||||
return (article: data.primary.value, author: authors[0])
|
||||
case .errors(let errors, meta: _, links: _):
|
||||
throw NetworkError.serverError
|
||||
}
|
||||
return (article: data.primary.value, author: authors[0])
|
||||
case .errors(let errors, meta: _, links: _):
|
||||
throw NetworkError.serverError
|
||||
}
|
||||
}
|
||||
|
||||
let response = try! docode(articleResponseData: responseData)
|
||||
|
||||
+2
-3
@@ -16,7 +16,7 @@ Pod::Spec.new do |spec|
|
||||
#
|
||||
|
||||
spec.name = "JSONAPI"
|
||||
spec.version = "0.30.0"
|
||||
spec.version = "0.31.1"
|
||||
spec.summary = "Swift Codable JSON API framework."
|
||||
|
||||
# This description is used to generate tags and improve search results.
|
||||
@@ -93,7 +93,7 @@ See the JSON API Spec here: https://jsonapi.org/format/
|
||||
# Not including the public_header_files will make all headers public.
|
||||
#
|
||||
|
||||
spec.source_files = "Sources", "Sources/**/*.{swift}"
|
||||
spec.source_files = "Sources/JSONAPI/**/*.{swift}"
|
||||
# spec.exclude_files = "Classes/Exclude"
|
||||
|
||||
# spec.public_header_files = "Classes/**/*.h"
|
||||
@@ -119,7 +119,6 @@ See the JSON API Spec here: https://jsonapi.org/format/
|
||||
# the lib prefix of their name.
|
||||
#
|
||||
|
||||
spec.framework = "Poly"
|
||||
# spec.frameworks = "SomeFramework", "AnotherFramework"
|
||||
|
||||
# spec.library = "iconv"
|
||||
|
||||
+2
-2
@@ -6,8 +6,8 @@
|
||||
"repositoryURL": "https://github.com/mattpolzin/Poly.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "d24d4c1214dd05f89eb1182a46592856dd0a0645",
|
||||
"version": "2.0.0"
|
||||
"revision": "38051821d7ef49e590e26e819a2fe447e50be9ff",
|
||||
"version": "2.0.1"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
+1
-1
@@ -18,7 +18,7 @@ let package = Package(
|
||||
targets: ["JSONAPITesting"])
|
||||
],
|
||||
dependencies: [
|
||||
.package(url: "https://github.com/mattpolzin/Poly.git", from: "2.0.0"),
|
||||
.package(url: "https://github.com/mattpolzin/Poly.git", .upToNextMajor(from: "2.0.0")),
|
||||
],
|
||||
targets: [
|
||||
.target(
|
||||
|
||||
@@ -108,75 +108,44 @@ Note that Playground support for importing non-system Frameworks is still a bit
|
||||
#### Document
|
||||
- `data`
|
||||
- [x] Encoding/Decoding
|
||||
- [x] Arbitrary
|
||||
- [x] OpenAPI
|
||||
- `included`
|
||||
- [x] Encoding/Decoding
|
||||
- [x] Arbitrary
|
||||
- [x] OpenAPI
|
||||
- `errors`
|
||||
- [x] Encoding/Decoding
|
||||
- [x] Arbitrary
|
||||
- [ ] OpenAPI
|
||||
- `meta`
|
||||
- [x] Encoding/Decoding
|
||||
- [x] Arbitrary
|
||||
- [ ] OpenAPI
|
||||
- `jsonapi` (i.e. API Information)
|
||||
- [x] Encoding/Decoding
|
||||
- [x] Arbitrary
|
||||
- [ ] OpenAPI
|
||||
- `links`
|
||||
- [x] Encoding/Decoding
|
||||
- [x] Arbitrary
|
||||
- [ ] OpenAPI
|
||||
|
||||
#### Resource Object
|
||||
- `id`
|
||||
- [x] Encoding/Decoding
|
||||
- [x] Arbitrary
|
||||
- [x] OpenAPI
|
||||
- `type`
|
||||
- [x] Encoding/Decoding
|
||||
- [x] OpenAPI
|
||||
- `attributes`
|
||||
- [x] Encoding/Decoding
|
||||
- [x] OpenAPI
|
||||
- `relationships`
|
||||
- [x] Encoding/Decoding
|
||||
- [x] OpenAPI
|
||||
- `links`
|
||||
- [x] Encoding/Decoding
|
||||
- [x] Arbitrary
|
||||
- [ ] OpenAPI
|
||||
- `meta`
|
||||
- [x] Encoding/Decoding
|
||||
- [x] Arbitrary
|
||||
- [ ] OpenAPI
|
||||
|
||||
#### Relationship Object
|
||||
- `data`
|
||||
- [x] Encoding/Decoding
|
||||
- [x] Arbitrary
|
||||
- [x] OpenAPI
|
||||
- `links`
|
||||
- [x] Encoding/Decoding
|
||||
- [ ] Arbitrary
|
||||
- [ ] OpenAPI
|
||||
- `meta`
|
||||
- [x] Encoding/Decoding
|
||||
- [ ] Arbitrary
|
||||
- [ ] OpenAPI
|
||||
|
||||
#### Links Object
|
||||
- `href`
|
||||
- [x] Encoding/Decoding
|
||||
- [ ] Arbitrary
|
||||
- [ ] OpenAPI
|
||||
- `meta`
|
||||
- [x] Encoding/Decoding
|
||||
- [ ] Arbitrary
|
||||
- [ ] OpenAPI
|
||||
|
||||
### Misc
|
||||
- [x] Support transforms on `Attributes` values (e.g. to support different representations of `Date`)
|
||||
@@ -194,7 +163,6 @@ Note that Playground support for importing non-system Frameworks is still a bit
|
||||
- [ ] (Maybe) Use `KeyPath` to specify `Includes` thus creating type safety around the relationship between a primary resource type and the types of included resources.
|
||||
- [ ] (Maybe) Replace `SingleResourceBody` and `ManyResourceBody` with support at the `Document` level to just interpret `PrimaryResource`, `PrimaryResource?`, or `[PrimaryResource]` as the same decoding/encoding strategies.
|
||||
- [ ] Support sideposting. JSONAPI spec might become opinionated in the future (https://github.com/json-api/json-api/pull/1197, https://github.com/json-api/json-api/issues/1215, https://github.com/json-api/json-api/issues/1216) but there is also an existing implementation to consider (https://jsonapi-suite.github.io/jsonapi_suite/ruby/writes/nested-writes). At this time, any sidepost implementation would be an awesome tertiary library to be used alongside the primary JSONAPI library. Maybe `JSONAPISideloading`.
|
||||
- [ ] Property-based testing (using `SwiftCheck`).
|
||||
- [ ] Error or warning if an included resource object is not related to a primary resource object or another included resource object (Turned off or at least not throwing by default).
|
||||
|
||||
## Usage
|
||||
@@ -203,7 +171,7 @@ In this documentation, in order to draw attention to the difference between the
|
||||
|
||||
### `JSONAPI.ResourceObjectDescription`
|
||||
|
||||
An `ResourceObjectDescription` is the `JSONAPI` framework's representation of what the **SPEC** calls a *Resource Object*. You might create the following `ResourceObjectDescription` to represent a person in a network of friends:
|
||||
A `ResourceObjectDescription` is the `JSONAPI` framework's representation of what the **SPEC** calls a *Resource Object*. You might create the following `ResourceObjectDescription` to represent a person in a network of friends:
|
||||
|
||||
```swift
|
||||
enum PersonDescription: IdentifiedResourceObjectDescription {
|
||||
@@ -220,7 +188,7 @@ enum PersonDescription: IdentifiedResourceObjectDescription {
|
||||
}
|
||||
```
|
||||
|
||||
The requirements of an `ResourceObjectDescription` are:
|
||||
The requirements of a `ResourceObjectDescription` are:
|
||||
1. A static `var` "jsonType" that matches the JSON type; The **SPEC** requires every *Resource Object* to have a "type".
|
||||
2. A `struct` of `Attributes` **- OR -** `typealias Attributes = NoAttributes`
|
||||
3. A `struct` of `Relationships` **- OR -** `typealias Relationships = NoRelationships`
|
||||
@@ -259,11 +227,11 @@ This readme doesn't go into detail on the **SPEC**, but the following *Resource
|
||||
|
||||
### `JSONAPI.ResourceObject`
|
||||
|
||||
Once you have an `ResourceObjectDescription`, you _create_, _encode_, and _decode_ `ResourceObjects` that "fit the description". If you have a `CreatableRawIdType` (see the section on `RawIdType`s below) then you can create new `ResourceObjects` that will automatically be given unique Ids, but even without a `CreatableRawIdType` you can encode, decode and work with resource objects.
|
||||
Once you have a `ResourceObjectDescription`, you _create_, _encode_, and _decode_ `ResourceObjects` that "fit the description". If you have a `CreatableRawIdType` (see the section on `RawIdType`s below) then you can create new `ResourceObjects` that will automatically be given unique Ids, but even without a `CreatableRawIdType` you can encode, decode and work with resource objects.
|
||||
|
||||
The `ResourceObject` and `ResourceObjectDescription` together with a `JSONAPI.Meta` type and a `JSONAPI.Links` type embody the rules and properties of a JSON API *Resource Object*.
|
||||
|
||||
An `ResourceObject` needs to be specialized on four generic types. The first is the `ResourceObjectDescription` described above. The others are a `Meta`, `Links`, and `MaybeRawId`.
|
||||
A `ResourceObject` needs to be specialized on four generic types. The first is the `ResourceObjectDescription` described above. The others are a `Meta`, `Links`, and `MaybeRawId`.
|
||||
|
||||
#### `Meta`
|
||||
|
||||
@@ -275,7 +243,7 @@ The third generic specialization on `ResourceObject` is `Links`. This is describ
|
||||
|
||||
#### `MaybeRawId`
|
||||
|
||||
The last generic specialization on `ResourceObject` is `MaybeRawId`. This is either a `RawIdType` that can be used to uniquely identify `ResourceObjects` or it is `Unidentified` which is used to indicate an `ResourceObject` does not have an `Id` (which is useful when a client is requesting that the server create an `ResourceObject` and assign it a new `Id`).
|
||||
The last generic specialization on `ResourceObject` is `MaybeRawId`. This is either a `RawIdType` that can be used to uniquely identify `ResourceObjects` or it is `Unidentified` which is used to indicate a `ResourceObject` does not have an `Id` (which is useful when a client is requesting that the server create a `ResourceObject` and assign it a new `Id`).
|
||||
|
||||
##### `RawIdType`
|
||||
|
||||
@@ -283,7 +251,7 @@ The raw type of `Id` to use for the `ResourceObject`. The actual `Id` of the `Re
|
||||
|
||||
Having the `ResourceObject` type associated with the `Id` makes it easy to store all of your resource objects in a hash broken out by `ResourceObject` type; You can pass `Ids` around and always know where to look for the `ResourceObject` to which the `Id` refers. This encapsulation provides some type safety because the Ids of two `ResourceObjects` with the "raw ID" of `"1"` but different types will not compare as equal.
|
||||
|
||||
A `RawIdType` is the underlying type that uniquely identifies an `ResourceObject`. This is often a `String` or a `UUID`.
|
||||
A `RawIdType` is the underlying type that uniquely identifies a `ResourceObject`. This is often a `String` or a `UUID`.
|
||||
|
||||
#### Convenient `typealiases`
|
||||
|
||||
@@ -305,7 +273,7 @@ Note that I am assuming an unidentified person is a "new" person. I suspect that
|
||||
|
||||
### `JSONAPI.Relationships`
|
||||
|
||||
There are two types of `Relationships`: `ToOneRelationship` and `ToManyRelationship`. An `ResourceObjectDescription`'s `Relationships` type can contain any number of `Relationship` properties of either of these types. Do not store anything other than `Relationship` properties in the `Relationships` struct of an `ResourceObjectDescription`.
|
||||
There are two types of `Relationships`: `ToOneRelationship` and `ToManyRelationship`. A `ResourceObjectDescription`'s `Relationships` type can contain any number of `Relationship` properties of either of these types. Do not store anything other than `Relationship` properties in the `Relationships` struct of a `ResourceObjectDescription`.
|
||||
|
||||
In addition to identifying resource objects by Id and type, `Relationships` can contain `Meta` or `Links` that follow the same rules as [`Meta`](#jsonapimeta) and [`Links`](#jsonapilinks) elsewhere in the JSON API Document.
|
||||
|
||||
@@ -314,7 +282,7 @@ To describe a relationship that may be omitted (i.e. the key is not even present
|
||||
let nullableRelative: ToOneRelationship<Person?, NoMetadata, NoLinks>
|
||||
```
|
||||
|
||||
An resource object that does not have relationships can be described by adding the following to an `ResourceObjectDescription`:
|
||||
A `ResourceObject` that does not have relationships can be described by adding the following to a `ResourceObjectDescription`:
|
||||
```swift
|
||||
typealias Relationships = NoRelationships
|
||||
```
|
||||
@@ -326,7 +294,7 @@ let friendIds: [Person.Identifier] = person ~> \.friends
|
||||
|
||||
### `JSONAPI.Attributes`
|
||||
|
||||
The `Attributes` of an `ResourceObjectDescription` can contain any JSON encodable/decodable types as long as they are wrapped in an `Attribute`, `ValidatedAttribute`, or `TransformedAttribute` `struct`.
|
||||
The `Attributes` of a `ResourceObjectDescription` can contain any JSON encodable/decodable types as long as they are wrapped in an `Attribute`, `ValidatedAttribute`, or `TransformedAttribute` `struct`.
|
||||
|
||||
To describe an attribute that may be omitted (i.e. the key might not even be in the JSON object), you make the entire `Attribute` optional:
|
||||
```swift
|
||||
@@ -338,7 +306,7 @@ To describe an attribute that is expected to exist but might have a `null` value
|
||||
let nullableAttribute: Attribute<String?>
|
||||
```
|
||||
|
||||
An resource object that does not have attributes can be described by adding the following to an `ResourceObjectDescription`:
|
||||
A resource object that does not have attributes can be described by adding the following to an `ResourceObjectDescription`:
|
||||
```swift
|
||||
typealias Attributes = NoAttributes
|
||||
```
|
||||
@@ -396,8 +364,8 @@ public var fullName: Attribute<String> {
|
||||
|
||||
If your computed property is wrapped in a `AttributeType` then you can still use the default subscript operator to access it (as would be the case with the `person[\.fullName]` example above). However, if you add a property to the `Attributes` `struct` that is not wrapped in an `AttributeType`, you must either access it from its full path (`person.attributes.newThing`) or with the "direct" subscript accessor (`person[direct: \.newThing]`). This keeps the subscript access unambiguous enough for the compiler to be helpful prior to explicitly casting, comparing, or storing the result.
|
||||
|
||||
### Copying `ResourceObjects`
|
||||
`ResourceObject` is a value type, so copying is its default behavior. There are two common mutations you might want to make when copying an `ResourceObject`:
|
||||
### Copying/Mutating `ResourceObjects`
|
||||
`ResourceObject` is a value type, so copying is its default behavior. There are two common mutations you might want to make when copying a `ResourceObject`:
|
||||
1. Assigning a new `Identifier` to the copy of an identified `ResourceObject`.
|
||||
2. Assigning a new `Identifier` to the copy of an unidentified `ResourceObject`.
|
||||
|
||||
@@ -494,6 +462,8 @@ A `Meta` struct is totally open-ended. It is described by the **SPEC** as a plac
|
||||
|
||||
You can specify `NoMetadata` if the part of the document being described should not contain any `Meta`.
|
||||
|
||||
If you need to support metadata with structure that is not pre-determined, consider an "Any Codable" type such as that found at https://github.com/Flight-School/AnyCodable.
|
||||
|
||||
### `JSONAPI.Links`
|
||||
|
||||
A `Links` struct must contain only `Link` properties. Each `Link` property can either be a `URL` or a `URL` and some `Meta`. Each part of the document has some suggested common `Links` to include but generally any link can be included.
|
||||
@@ -540,6 +510,8 @@ public enum ResourceObjectDescription2: JSONAPI.ResourceObjectDescription {
|
||||
case wholeOtherThing = "coolProperty"
|
||||
}
|
||||
}
|
||||
|
||||
public typealias Relationships = NoRelationships
|
||||
}
|
||||
```
|
||||
|
||||
@@ -590,7 +562,7 @@ extension ResourceObjectDescription1.Attributes {
|
||||
### Meta-Attributes
|
||||
This advanced feature may not ever be useful, but if you find yourself in the situation of dealing with an API that does not 100% follow the **SPEC** then you might find meta-attributes are just the thing to make your resource objects more natural to work with.
|
||||
|
||||
Suppose, for example, you are presented with the unfortunate situation where a piece of information you need is only available as part of the `Id` of an resource object. Perhaps a user's `Id` is formatted "{integer}-{createdAt}" where "createdAt" is the unix timestamp when the user account was created. The following `UserDescription` will expose what you need as an attribute. Realistically, the following example code is still terrible for its error handling. Using a `Result` type and/or invariants would clean things up substantially.
|
||||
Suppose, for example, you are presented with the unfortunate situation where a piece of information you need is only available as part of the `Id` of a resource object. Perhaps a user's `Id` is formatted "{integer}-{createdAt}" where "createdAt" is the unix timestamp when the user account was created. The following `UserDescription` will expose what you need as an attribute. Realistically, the following example code is still terrible for its error handling. Using a `Result` type and/or invariants would clean things up substantially.
|
||||
|
||||
```swift
|
||||
enum UserDescription: ResourceObjectDescription {
|
||||
@@ -670,38 +642,39 @@ The following serves as a sort of pseudo-example. It skips server/client impleme
|
||||
|
||||
### Preamble (Setup shared by server and client)
|
||||
```swift
|
||||
// We make String a CreatableRawIdType.
|
||||
var GlobalStringId: Int = 0
|
||||
// Make String a CreatableRawIdType.
|
||||
var globalStringId: Int = 0
|
||||
extension String: CreatableRawIdType {
|
||||
public static func unique() -> String {
|
||||
GlobalStringId += 1
|
||||
return String(GlobalStringId)
|
||||
globalStringId += 1
|
||||
return String(globalStringId)
|
||||
}
|
||||
}
|
||||
|
||||
// We create a typealias given that we do not expect JSON:API Resource
|
||||
// Create a typealias because we do not expect JSON:API Resource
|
||||
// Objects for this particular API to have Metadata or Links associated
|
||||
// with them. We also expect them to have String Identifiers.
|
||||
typealias JSONResourceObject<Description: ResourceObjectDescription> = JSONAPI.ResourceObject<Description, NoMetadata, NoLinks, String>
|
||||
typealias JSONEntity<Description: ResourceObjectDescription> = JSONAPI.ResourceObject<Description, NoMetadata, NoLinks, String>
|
||||
|
||||
// Similarly, we create a typealias for unidentified resource objects. JSON:API
|
||||
// only allows unidentified resource objects (i.e. no "id" field) for client
|
||||
// requests that create new resource objects. In these situations, the server
|
||||
// is expected to assign the new resource object a unique ID.
|
||||
typealias UnidentifiedJSONResourceObject<Description: ResourceObjectDescription> = JSONAPI.ResourceObject<Description, NoMetadata, NoLinks, Unidentified>
|
||||
// Similarly, create a typealias for unidentified entities. JSON:API
|
||||
// only allows unidentified entities (i.e. no "id" field) for client
|
||||
// requests that create new entities. In these situations, the server
|
||||
// is expected to assign the new entity a unique ID.
|
||||
typealias UnidentifiedJSONEntity<Description: ResourceObjectDescription> = JSONAPI.ResourceObject<Description, NoMetadata, NoLinks, Unidentified>
|
||||
|
||||
// We create typealiases given that we do not expect JSON:API Relationships
|
||||
// for this particular API to have Metadata or Links associated
|
||||
// with them.
|
||||
typealias ToOneRelationship<ResourceObject: Identifiable> = JSONAPI.ToOneRelationship<ResourceObject, NoMetadata, NoLinks>
|
||||
typealias ToManyRelationship<ResourceObject: Relatable> = JSONAPI.ToManyRelationship<ResourceObject, NoMetadata, NoLinks>
|
||||
// Create relationship typealiases because we do not expect
|
||||
// JSON:API Relationships for this particular API to have
|
||||
// Metadata or Links associated with them.
|
||||
typealias ToOneRelationship<Entity: Identifiable> = JSONAPI.ToOneRelationship<Entity, NoMetadata, NoLinks>
|
||||
typealias ToManyRelationship<Entity: Relatable> = JSONAPI.ToManyRelationship<Entity, NoMetadata, NoLinks>
|
||||
|
||||
// We create a typealias for a Document given that we do not expect
|
||||
// Create a typealias for a Document because we do not expect
|
||||
// JSON:API Documents for this particular API to have Metadata, Links,
|
||||
// useful Errors, or a JSON:API Object (i.e. APIDescription).
|
||||
// useful Errors, or an APIDescription (The *SPEC* calls this
|
||||
// "API Description" the "JSON:API Object").
|
||||
typealias Document<PrimaryResourceBody: JSONAPI.ResourceBody, IncludeType: JSONAPI.Include> = JSONAPI.Document<PrimaryResourceBody, NoMetadata, NoLinks, IncludeType, NoAPIDescription, UnknownJSONAPIError>
|
||||
|
||||
// MARK: ResourceObject Definitions
|
||||
// MARK: Entity Definitions
|
||||
|
||||
enum AuthorDescription: ResourceObjectDescription {
|
||||
public static var jsonType: String { return "authors" }
|
||||
@@ -713,7 +686,7 @@ enum AuthorDescription: ResourceObjectDescription {
|
||||
public typealias Relationships = NoRelationships
|
||||
}
|
||||
|
||||
typealias Author = JSONResourceObject<AuthorDescription>
|
||||
typealias Author = JSONEntity<AuthorDescription>
|
||||
|
||||
enum ArticleDescription: ResourceObjectDescription {
|
||||
public static var jsonType: String { return "articles" }
|
||||
@@ -728,7 +701,7 @@ enum ArticleDescription: ResourceObjectDescription {
|
||||
}
|
||||
}
|
||||
|
||||
typealias Article = JSONResourceObject<ArticleDescription>
|
||||
typealias Article = JSONEntity<ArticleDescription>
|
||||
|
||||
// MARK: Document Definitions
|
||||
|
||||
@@ -737,47 +710,48 @@ typealias Article = JSONResourceObject<ArticleDescription>
|
||||
typealias SingleArticleDocumentWithIncludes = Document<SingleResourceBody<Article>, Include1<Author>>
|
||||
|
||||
// ... and a typealias to represent a document containing one Article and
|
||||
// not including any related resource objects.
|
||||
// not including any related entities.
|
||||
typealias SingleArticleDocument = Document<SingleResourceBody<Article>, NoIncludes>
|
||||
```
|
||||
|
||||
### Server Pseudo-example
|
||||
```swift
|
||||
// Skipping over all the API and database stuff, here's a chunk of code
|
||||
// that creates a document. Note that this document is the entirety
|
||||
// of a JSON:API response body.
|
||||
func articleDocument(includeAuthor: Bool) -> Either<SingleArticleDocument, SingleArticleDocumentWithIncludes> {
|
||||
// Let's pretend all of this is coming from a database:
|
||||
// Let's pretend all of this is coming from a database:
|
||||
|
||||
let authorId = Author.Identifier(rawValue: "1234")
|
||||
let authorId = Author.Identifier(rawValue: "1234")
|
||||
|
||||
let article = Article(id: .init(rawValue: "5678"),
|
||||
attributes: .init(title: .init(value: "JSON:API in Swift"),
|
||||
abstract: .init(value: "Not yet written")),
|
||||
relationships: .init(author: .init(id: authorId)),
|
||||
meta: .none,
|
||||
links: .none)
|
||||
let article = Article(id: .init(rawValue: "5678"),
|
||||
attributes: .init(title: .init(value: "JSON:API in Swift"),
|
||||
abstract: .init(value: "Not yet written")),
|
||||
relationships: .init(author: .init(id: authorId)),
|
||||
meta: .none,
|
||||
links: .none)
|
||||
|
||||
let document = SingleArticleDocument(apiDescription: .none,
|
||||
body: .init(resourceObject: article),
|
||||
includes: .none,
|
||||
meta: .none,
|
||||
links: .none)
|
||||
let document = SingleArticleDocument(apiDescription: .none,
|
||||
body: .init(resourceObject: article),
|
||||
includes: .none,
|
||||
meta: .none,
|
||||
links: .none)
|
||||
|
||||
switch includeAuthor {
|
||||
case false:
|
||||
return .a(document)
|
||||
switch includeAuthor {
|
||||
case false:
|
||||
return .init(document)
|
||||
|
||||
case true:
|
||||
let author = Author(id: authorId,
|
||||
attributes: .init(name: .init(value: "Janice Bluff")),
|
||||
relationships: .none,
|
||||
meta: .none,
|
||||
links: .none)
|
||||
case true:
|
||||
let author = Author(id: authorId,
|
||||
attributes: .init(name: .init(value: "Janice Bluff")),
|
||||
relationships: .none,
|
||||
meta: .none,
|
||||
links: .none)
|
||||
|
||||
let includes: Includes<SingleArticleDocumentWithIncludes.Include> = .init(values: [.init(author)])
|
||||
let includes: Includes<SingleArticleDocumentWithIncludes.Include> = .init(values: [.init(author)])
|
||||
|
||||
return .b(document.including(.init(values: [.init(author)])))
|
||||
}
|
||||
return .init(document.including(.init(values: [.init(author)])))
|
||||
}
|
||||
}
|
||||
|
||||
let encoder = JSONEncoder()
|
||||
@@ -787,8 +761,8 @@ encoder.outputFormatting = .prettyPrinted
|
||||
let responseBody = articleDocument(includeAuthor: true)
|
||||
let responseData = try! encoder.encode(responseBody)
|
||||
|
||||
// Next step would be encoding and setting as the HTTP body of a response.
|
||||
// we will just print it out instead:
|
||||
// Next step would be setting the HTTP body of a response.
|
||||
// We will just print it out instead:
|
||||
print("-----")
|
||||
print(String(data: responseData, encoding: .utf8)!)
|
||||
|
||||
@@ -803,31 +777,31 @@ print(String(data: otherResponseData, encoding: .utf8)!)
|
||||
### Client Pseudo-example
|
||||
```swift
|
||||
enum NetworkError: Swift.Error {
|
||||
case serverError
|
||||
case quantityMismatch
|
||||
case serverError
|
||||
case quantityMismatch
|
||||
}
|
||||
|
||||
// Skipping over all the API stuff, here's a chunk of code that will
|
||||
// decode a document. We will assume we have made a request for a
|
||||
// single article including the author.
|
||||
func docode(articleResponseData: Data) throws -> (article: Article, author: Author) {
|
||||
let decoder = JSONDecoder()
|
||||
decoder.keyDecodingStrategy = .convertFromSnakeCase
|
||||
let decoder = JSONDecoder()
|
||||
decoder.keyDecodingStrategy = .convertFromSnakeCase
|
||||
|
||||
let articleDocument = try decoder.decode(SingleArticleDocumentWithIncludes.self, from: articleResponseData)
|
||||
let articleDocument = try decoder.decode(SingleArticleDocumentWithIncludes.self, from: articleResponseData)
|
||||
|
||||
switch articleDocument.body {
|
||||
case .data(let data):
|
||||
let authors = data.includes[Author.self]
|
||||
switch articleDocument.body {
|
||||
case .data(let data):
|
||||
let authors = data.includes[Author.self]
|
||||
|
||||
guard authors.count == 1 else {
|
||||
throw NetworkError.quantityMismatch
|
||||
}
|
||||
guard authors.count == 1 else {
|
||||
throw NetworkError.quantityMismatch
|
||||
}
|
||||
|
||||
return (article: data.primary.value, author: authors[0])
|
||||
case .errors(let errors, meta: _, links: _):
|
||||
throw NetworkError.serverError
|
||||
}
|
||||
return (article: data.primary.value, author: authors[0])
|
||||
case .errors(let errors, meta: _, links: _):
|
||||
throw NetworkError.serverError
|
||||
}
|
||||
}
|
||||
|
||||
let response = try! docode(articleResponseData: responseData)
|
||||
|
||||
@@ -0,0 +1,215 @@
|
||||
//
|
||||
// EmptyObjectDecoder.swift
|
||||
// JSONAPI
|
||||
//
|
||||
// Created by Mathew Polzin on 7/2/19.
|
||||
//
|
||||
|
||||
/// `EmptyObjectDecoder` exists internally for the sole purpose of
|
||||
/// allowing certain fallback logic paths to attempt to create `Decodable`
|
||||
/// types from empty containers (specifically in a way that is agnostic
|
||||
/// of any given encoding). In other words, this serves the same purpose
|
||||
/// as `JSONDecoder().decode(Thing.self, from: "{}".data(using: .utf8)!)`
|
||||
/// without needing to use a third party or `Foundation` library decoder.
|
||||
struct EmptyObjectDecoder: Decoder {
|
||||
|
||||
var codingPath: [CodingKey] = []
|
||||
|
||||
var userInfo: [CodingUserInfoKey : Any] = [:]
|
||||
|
||||
func container<Key>(keyedBy type: Key.Type) throws -> KeyedDecodingContainer<Key> where Key : CodingKey {
|
||||
return KeyedDecodingContainer(EmptyKeyedContainer())
|
||||
}
|
||||
|
||||
func unkeyedContainer() throws -> UnkeyedDecodingContainer {
|
||||
return EmptyUnkeyedContainer()
|
||||
}
|
||||
|
||||
func singleValueContainer() throws -> SingleValueDecodingContainer {
|
||||
throw EmptyObjectDecodingError.emptyObjectCannotBeSingleValue
|
||||
}
|
||||
}
|
||||
|
||||
enum EmptyObjectDecodingError: Swift.Error {
|
||||
case emptyObjectCannotBeSingleValue
|
||||
case emptyObjectCannotBeUnkeyedValues
|
||||
case emptyObjectCannotHaveKeyedValues
|
||||
case emptyObjectCannotHaveNestedContainers
|
||||
case emptyObjectCannotHaveSuper
|
||||
}
|
||||
|
||||
struct EmptyUnkeyedContainer: UnkeyedDecodingContainer {
|
||||
var codingPath: [CodingKey] { return [] }
|
||||
|
||||
var count: Int? { return 0 }
|
||||
|
||||
var isAtEnd: Bool { return true }
|
||||
|
||||
var currentIndex: Int { return 0 }
|
||||
|
||||
mutating func decodeNil() throws -> Bool {
|
||||
throw EmptyObjectDecodingError.emptyObjectCannotBeUnkeyedValues
|
||||
}
|
||||
|
||||
mutating func decode(_ type: Bool.Type) throws -> Bool {
|
||||
throw EmptyObjectDecodingError.emptyObjectCannotBeUnkeyedValues
|
||||
}
|
||||
|
||||
mutating func decode(_ type: String.Type) throws -> String {
|
||||
throw EmptyObjectDecodingError.emptyObjectCannotBeUnkeyedValues
|
||||
}
|
||||
|
||||
mutating func decode(_ type: Double.Type) throws -> Double {
|
||||
throw EmptyObjectDecodingError.emptyObjectCannotBeUnkeyedValues
|
||||
}
|
||||
|
||||
mutating func decode(_ type: Float.Type) throws -> Float {
|
||||
throw EmptyObjectDecodingError.emptyObjectCannotBeUnkeyedValues
|
||||
}
|
||||
|
||||
mutating func decode(_ type: Int.Type) throws -> Int {
|
||||
throw EmptyObjectDecodingError.emptyObjectCannotBeUnkeyedValues
|
||||
}
|
||||
|
||||
mutating func decode(_ type: Int8.Type) throws -> Int8 {
|
||||
throw EmptyObjectDecodingError.emptyObjectCannotBeUnkeyedValues
|
||||
}
|
||||
|
||||
mutating func decode(_ type: Int16.Type) throws -> Int16 {
|
||||
throw EmptyObjectDecodingError.emptyObjectCannotBeUnkeyedValues
|
||||
}
|
||||
|
||||
mutating func decode(_ type: Int32.Type) throws -> Int32 {
|
||||
throw EmptyObjectDecodingError.emptyObjectCannotBeUnkeyedValues
|
||||
}
|
||||
|
||||
mutating func decode(_ type: Int64.Type) throws -> Int64 {
|
||||
throw EmptyObjectDecodingError.emptyObjectCannotBeUnkeyedValues
|
||||
}
|
||||
|
||||
mutating func decode(_ type: UInt.Type) throws -> UInt {
|
||||
throw EmptyObjectDecodingError.emptyObjectCannotBeUnkeyedValues
|
||||
}
|
||||
|
||||
mutating func decode(_ type: UInt8.Type) throws -> UInt8 {
|
||||
throw EmptyObjectDecodingError.emptyObjectCannotBeUnkeyedValues
|
||||
}
|
||||
|
||||
mutating func decode(_ type: UInt16.Type) throws -> UInt16 {
|
||||
throw EmptyObjectDecodingError.emptyObjectCannotBeUnkeyedValues
|
||||
}
|
||||
|
||||
mutating func decode(_ type: UInt32.Type) throws -> UInt32 {
|
||||
throw EmptyObjectDecodingError.emptyObjectCannotBeUnkeyedValues
|
||||
}
|
||||
|
||||
mutating func decode(_ type: UInt64.Type) throws -> UInt64 {
|
||||
throw EmptyObjectDecodingError.emptyObjectCannotBeUnkeyedValues
|
||||
}
|
||||
|
||||
mutating func decode<T>(_ type: T.Type) throws -> T where T : Decodable {
|
||||
throw EmptyObjectDecodingError.emptyObjectCannotBeUnkeyedValues
|
||||
}
|
||||
|
||||
mutating func nestedContainer<NestedKey>(keyedBy type: NestedKey.Type) throws -> KeyedDecodingContainer<NestedKey> where NestedKey : CodingKey {
|
||||
throw EmptyObjectDecodingError.emptyObjectCannotHaveNestedContainers
|
||||
}
|
||||
|
||||
mutating func nestedUnkeyedContainer() throws -> UnkeyedDecodingContainer {
|
||||
throw EmptyObjectDecodingError.emptyObjectCannotHaveNestedContainers
|
||||
}
|
||||
|
||||
mutating func superDecoder() throws -> Decoder {
|
||||
throw EmptyObjectDecodingError.emptyObjectCannotHaveSuper
|
||||
}
|
||||
}
|
||||
|
||||
struct EmptyKeyedContainer<Key: CodingKey>: KeyedDecodingContainerProtocol {
|
||||
var codingPath: [CodingKey] { return [] }
|
||||
|
||||
var allKeys: [Key] { return [] }
|
||||
|
||||
func contains(_ key: Key) -> Bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func decodeNil(forKey key: Key) throws -> Bool {
|
||||
throw EmptyObjectDecodingError.emptyObjectCannotHaveKeyedValues
|
||||
}
|
||||
|
||||
func decode(_ type: Bool.Type, forKey key: Key) throws -> Bool {
|
||||
throw EmptyObjectDecodingError.emptyObjectCannotHaveKeyedValues
|
||||
}
|
||||
|
||||
func decode(_ type: String.Type, forKey key: Key) throws -> String {
|
||||
throw EmptyObjectDecodingError.emptyObjectCannotHaveKeyedValues
|
||||
}
|
||||
|
||||
func decode(_ type: Double.Type, forKey key: Key) throws -> Double {
|
||||
throw EmptyObjectDecodingError.emptyObjectCannotHaveKeyedValues
|
||||
}
|
||||
|
||||
func decode(_ type: Float.Type, forKey key: Key) throws -> Float {
|
||||
throw EmptyObjectDecodingError.emptyObjectCannotHaveKeyedValues
|
||||
}
|
||||
|
||||
func decode(_ type: Int.Type, forKey key: Key) throws -> Int {
|
||||
throw EmptyObjectDecodingError.emptyObjectCannotHaveKeyedValues
|
||||
}
|
||||
|
||||
func decode(_ type: Int8.Type, forKey key: Key) throws -> Int8 {
|
||||
throw EmptyObjectDecodingError.emptyObjectCannotHaveKeyedValues
|
||||
}
|
||||
|
||||
func decode(_ type: Int16.Type, forKey key: Key) throws -> Int16 {
|
||||
throw EmptyObjectDecodingError.emptyObjectCannotHaveKeyedValues
|
||||
}
|
||||
|
||||
func decode(_ type: Int32.Type, forKey key: Key) throws -> Int32 {
|
||||
throw EmptyObjectDecodingError.emptyObjectCannotHaveKeyedValues
|
||||
}
|
||||
|
||||
func decode(_ type: Int64.Type, forKey key: Key) throws -> Int64 {
|
||||
throw EmptyObjectDecodingError.emptyObjectCannotHaveKeyedValues
|
||||
}
|
||||
|
||||
func decode(_ type: UInt.Type, forKey key: Key) throws -> UInt {
|
||||
throw EmptyObjectDecodingError.emptyObjectCannotHaveKeyedValues
|
||||
}
|
||||
|
||||
func decode(_ type: UInt8.Type, forKey key: Key) throws -> UInt8 {
|
||||
throw EmptyObjectDecodingError.emptyObjectCannotHaveKeyedValues
|
||||
}
|
||||
|
||||
func decode(_ type: UInt16.Type, forKey key: Key) throws -> UInt16 {
|
||||
throw EmptyObjectDecodingError.emptyObjectCannotHaveKeyedValues
|
||||
}
|
||||
|
||||
func decode(_ type: UInt32.Type, forKey key: Key) throws -> UInt32 {
|
||||
throw EmptyObjectDecodingError.emptyObjectCannotHaveKeyedValues
|
||||
}
|
||||
|
||||
func decode(_ type: UInt64.Type, forKey key: Key) throws -> UInt64 {
|
||||
throw EmptyObjectDecodingError.emptyObjectCannotHaveKeyedValues
|
||||
}
|
||||
|
||||
func decode<T>(_ type: T.Type, forKey key: Key) throws -> T where T : Decodable {
|
||||
throw EmptyObjectDecodingError.emptyObjectCannotHaveKeyedValues
|
||||
}
|
||||
|
||||
func nestedContainer<NestedKey>(keyedBy type: NestedKey.Type, forKey key: Key) throws -> KeyedDecodingContainer<NestedKey> where NestedKey : CodingKey {
|
||||
throw EmptyObjectDecodingError.emptyObjectCannotHaveNestedContainers
|
||||
}
|
||||
|
||||
func nestedUnkeyedContainer(forKey key: Key) throws -> UnkeyedDecodingContainer {
|
||||
throw EmptyObjectDecodingError.emptyObjectCannotHaveNestedContainers
|
||||
}
|
||||
|
||||
func superDecoder() throws -> Decoder {
|
||||
throw EmptyObjectDecodingError.emptyObjectCannotHaveSuper
|
||||
}
|
||||
|
||||
func superDecoder(forKey key: Key) throws -> Decoder {
|
||||
throw EmptyObjectDecodingError.emptyObjectCannotHaveSuper
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@
|
||||
// Created by Mathew Polzin on 7/24/18.
|
||||
//
|
||||
|
||||
|
||||
/// A JSON API structure within an ResourceObject that contains
|
||||
/// named properties of types `ToOneRelationship` and
|
||||
/// `ToManyRelationship`.
|
||||
@@ -582,7 +583,6 @@ public extension ResourceObject {
|
||||
}
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
|
||||
let container = try decoder.container(keyedBy: ResourceObjectCodingKeys.self)
|
||||
|
||||
let type = try container.decode(String.self, forKey: .type)
|
||||
@@ -597,7 +597,9 @@ public extension ResourceObject {
|
||||
attributes = try (NoAttributes() as? Description.Attributes) ??
|
||||
container.decode(Description.Attributes.self, forKey: .attributes)
|
||||
|
||||
relationships = try (NoRelationships() as? Description.Relationships) ?? container.decode(Description.Relationships.self, forKey: .relationships)
|
||||
relationships = try (NoRelationships() as? Description.Relationships)
|
||||
?? container.decodeIfPresent(Description.Relationships.self, forKey: .relationships)
|
||||
?? Description.Relationships(from: EmptyObjectDecoder())
|
||||
|
||||
meta = try (NoMetadata() as? MetaType) ?? container.decode(MetaType.self, forKey: .meta)
|
||||
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
#if !canImport(ObjectiveC)
|
||||
import XCTest
|
||||
|
||||
extension Attribute_LiteralTests {
|
||||
static let __allTests = [
|
||||
// DO NOT MODIFY: This is autogenerated, use:
|
||||
// `swift test --generate-linuxmain`
|
||||
// to regenerate.
|
||||
static let __allTests__Attribute_LiteralTests = [
|
||||
("test_ArrayLiteral", test_ArrayLiteral),
|
||||
("test_BooleanLiteral", test_BooleanLiteral),
|
||||
("test_DictionaryLiteral", test_DictionaryLiteral),
|
||||
@@ -32,7 +36,10 @@ extension Attribute_LiteralTests {
|
||||
}
|
||||
|
||||
extension EntityCheckTests {
|
||||
static let __allTests = [
|
||||
// DO NOT MODIFY: This is autogenerated, use:
|
||||
// `swift test --generate-linuxmain`
|
||||
// to regenerate.
|
||||
static let __allTests__EntityCheckTests = [
|
||||
("test_failsWithBadAttribute", test_failsWithBadAttribute),
|
||||
("test_failsWithBadRelationship", test_failsWithBadRelationship),
|
||||
("test_failsWithEnumAttributes", test_failsWithEnumAttributes),
|
||||
@@ -42,27 +49,32 @@ extension EntityCheckTests {
|
||||
}
|
||||
|
||||
extension Id_LiteralTests {
|
||||
static let __allTests = [
|
||||
// DO NOT MODIFY: This is autogenerated, use:
|
||||
// `swift test --generate-linuxmain`
|
||||
// to regenerate.
|
||||
static let __allTests__Id_LiteralTests = [
|
||||
("test_IntegerLiteral", test_IntegerLiteral),
|
||||
("test_StringLiteral", test_StringLiteral),
|
||||
]
|
||||
}
|
||||
|
||||
extension Relationship_LiteralTests {
|
||||
static let __allTests = [
|
||||
// DO NOT MODIFY: This is autogenerated, use:
|
||||
// `swift test --generate-linuxmain`
|
||||
// to regenerate.
|
||||
static let __allTests__Relationship_LiteralTests = [
|
||||
("test_ArrayLiteral", test_ArrayLiteral),
|
||||
("test_NilLiteral", test_NilLiteral),
|
||||
("test_StringLiteral", test_StringLiteral),
|
||||
]
|
||||
}
|
||||
|
||||
#if !os(macOS)
|
||||
public func __allTests() -> [XCTestCaseEntry] {
|
||||
return [
|
||||
testCase(Attribute_LiteralTests.__allTests),
|
||||
testCase(EntityCheckTests.__allTests),
|
||||
testCase(Id_LiteralTests.__allTests),
|
||||
testCase(Relationship_LiteralTests.__allTests),
|
||||
testCase(Attribute_LiteralTests.__allTests__Attribute_LiteralTests),
|
||||
testCase(EntityCheckTests.__allTests__EntityCheckTests),
|
||||
testCase(Id_LiteralTests.__allTests__Id_LiteralTests),
|
||||
testCase(Relationship_LiteralTests.__allTests__Relationship_LiteralTests),
|
||||
]
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -31,7 +31,7 @@ class EntityTests: XCTestCase {
|
||||
|
||||
XCTAssertEqual(entity ~> \.optionalOne, Optional(entity1.id))
|
||||
}
|
||||
|
||||
|
||||
func test_toMany_relationship_operator_access() {
|
||||
let entity1 = TestEntity1(attributes: .none, relationships: .none, meta: .none, links: .none)
|
||||
let entity2 = TestEntity1(attributes: .none, relationships: .none, meta: .none, links: .none)
|
||||
@@ -403,6 +403,16 @@ extension EntityTests {
|
||||
data: entity_optional_nullable_nulled_relationship)
|
||||
}
|
||||
|
||||
func test_optionalNullableRelationshipOmitted() {
|
||||
let entity = decoded(type: TestEntity12.self,
|
||||
data: entity_all_relationships_optional_and_omitted)
|
||||
|
||||
XCTAssertNil(entity ~> \.optionalOne)
|
||||
XCTAssertNil(entity ~> \.optionalNullableOne)
|
||||
XCTAssertNil(entity ~> \.optionalMany)
|
||||
XCTAssertNoThrow(try TestEntity12.check(entity))
|
||||
}
|
||||
|
||||
func test_nullableRelationshipIsNull() {
|
||||
let entity = decoded(type: TestEntity9.self,
|
||||
data: entity_nulled_relationship)
|
||||
@@ -806,6 +816,28 @@ extension EntityTests {
|
||||
|
||||
typealias TestEntity11 = BasicEntity<TestEntityType11>
|
||||
|
||||
enum TestEntityType12: ResourceObjectDescription {
|
||||
public static var jsonType: String { return "twelfth_test_entities" }
|
||||
|
||||
typealias Attributes = NoAttributes
|
||||
|
||||
public struct Relationships: JSONAPI.Relationships {
|
||||
public init() {
|
||||
optionalOne = nil
|
||||
optionalNullableOne = nil
|
||||
optionalMany = nil
|
||||
}
|
||||
|
||||
let optionalOne: ToOneRelationship<TestEntity1, NoMetadata, NoLinks>?
|
||||
|
||||
let optionalNullableOne: ToOneRelationship<TestEntity1?, NoMetadata, NoLinks>?
|
||||
|
||||
let optionalMany: ToManyRelationship<TestEntity1, NoMetadata, NoLinks>?
|
||||
}
|
||||
}
|
||||
|
||||
typealias TestEntity12 = BasicEntity<TestEntityType12>
|
||||
|
||||
enum UnidentifiedTestEntityType: ResourceObjectDescription {
|
||||
public static var jsonType: String { return "unidentified_test_entities" }
|
||||
|
||||
|
||||
@@ -383,6 +383,16 @@ let entity_valid_validated_attribute = """
|
||||
}
|
||||
""".data(using: .utf8)!
|
||||
|
||||
let entity_all_relationships_optional_and_omitted = """
|
||||
{
|
||||
"id": "1",
|
||||
"type": "twelfth_test_entities",
|
||||
"attributes": {
|
||||
"number": 10
|
||||
}
|
||||
}
|
||||
""".data(using: .utf8)!
|
||||
|
||||
let entity_unidentified = """
|
||||
{
|
||||
"type": "unidentified_test_entities",
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
#if !canImport(ObjectiveC)
|
||||
import XCTest
|
||||
|
||||
extension APIDescriptionTests {
|
||||
static let __allTests = [
|
||||
// DO NOT MODIFY: This is autogenerated, use:
|
||||
// `swift test --generate-linuxmain`
|
||||
// to regenerate.
|
||||
static let __allTests__APIDescriptionTests = [
|
||||
("test_empty", test_empty),
|
||||
("test_failsMissingMeta", test_failsMissingMeta),
|
||||
("test_NoDescriptionString", test_NoDescriptionString),
|
||||
@@ -12,7 +16,10 @@ extension APIDescriptionTests {
|
||||
}
|
||||
|
||||
extension AttributeTests {
|
||||
static let __allTests = [
|
||||
// DO NOT MODIFY: This is autogenerated, use:
|
||||
// `swift test --generate-linuxmain`
|
||||
// to regenerate.
|
||||
static let __allTests__AttributeTests = [
|
||||
("test_AttributeConstructor", test_AttributeConstructor),
|
||||
("test_EncodedPrimitives", test_EncodedPrimitives),
|
||||
("test_NullableIsEqualToNonNullableIfNotNil", test_NullableIsEqualToNonNullableIfNotNil),
|
||||
@@ -24,7 +31,10 @@ extension AttributeTests {
|
||||
}
|
||||
|
||||
extension Attribute_FunctorTests {
|
||||
static let __allTests = [
|
||||
// DO NOT MODIFY: This is autogenerated, use:
|
||||
// `swift test --generate-linuxmain`
|
||||
// to regenerate.
|
||||
static let __allTests__Attribute_FunctorTests = [
|
||||
("test_mapGuaranteed", test_mapGuaranteed),
|
||||
("test_mapOptionalFailure", test_mapOptionalFailure),
|
||||
("test_mapOptionalSuccess", test_mapOptionalSuccess),
|
||||
@@ -32,7 +42,10 @@ extension Attribute_FunctorTests {
|
||||
}
|
||||
|
||||
extension ComputedPropertiesTests {
|
||||
static let __allTests = [
|
||||
// DO NOT MODIFY: This is autogenerated, use:
|
||||
// `swift test --generate-linuxmain`
|
||||
// to regenerate.
|
||||
static let __allTests__ComputedPropertiesTests = [
|
||||
("test_ComputedAttributeAccess", test_ComputedAttributeAccess),
|
||||
("test_ComputedNonAttributeAccess", test_ComputedNonAttributeAccess),
|
||||
("test_ComputedRelationshipAccess", test_ComputedRelationshipAccess),
|
||||
@@ -42,7 +55,10 @@ extension ComputedPropertiesTests {
|
||||
}
|
||||
|
||||
extension CustomAttributesTests {
|
||||
static let __allTests = [
|
||||
// DO NOT MODIFY: This is autogenerated, use:
|
||||
// `swift test --generate-linuxmain`
|
||||
// to regenerate.
|
||||
static let __allTests__CustomAttributesTests = [
|
||||
("test_customDecode", test_customDecode),
|
||||
("test_customEncode", test_customEncode),
|
||||
("test_customKeysDecode", test_customKeysDecode),
|
||||
@@ -51,7 +67,10 @@ extension CustomAttributesTests {
|
||||
}
|
||||
|
||||
extension DocumentTests {
|
||||
static let __allTests = [
|
||||
// DO NOT MODIFY: This is autogenerated, use:
|
||||
// `swift test --generate-linuxmain`
|
||||
// to regenerate.
|
||||
static let __allTests__DocumentTests = [
|
||||
("test_errorDocumentFailsWithNoAPIDescription", test_errorDocumentFailsWithNoAPIDescription),
|
||||
("test_errorDocumentNoMeta", test_errorDocumentNoMeta),
|
||||
("test_errorDocumentNoMeta_encode", test_errorDocumentNoMeta_encode),
|
||||
@@ -154,7 +173,10 @@ extension DocumentTests {
|
||||
}
|
||||
|
||||
extension EntityTests {
|
||||
static let __allTests = [
|
||||
// DO NOT MODIFY: This is autogenerated, use:
|
||||
// `swift test --generate-linuxmain`
|
||||
// to regenerate.
|
||||
static let __allTests__EntityTests = [
|
||||
("test_copyIdentifiedByType", test_copyIdentifiedByType),
|
||||
("test_copyIdentifiedByValue", test_copyIdentifiedByValue),
|
||||
("test_copyWithNewId", test_copyWithNewId),
|
||||
@@ -202,6 +224,7 @@ extension EntityTests {
|
||||
("test_optional_relationship_operator_access", test_optional_relationship_operator_access),
|
||||
("test_optionalNullableRelationshipNulled", test_optionalNullableRelationshipNulled),
|
||||
("test_optionalNullableRelationshipNulled_encode", test_optionalNullableRelationshipNulled_encode),
|
||||
("test_optionalNullableRelationshipOmitted", test_optionalNullableRelationshipOmitted),
|
||||
("test_optionalToMany_relationship_opeartor_access", test_optionalToMany_relationship_opeartor_access),
|
||||
("test_optionalToManyIsNotOmitted", test_optionalToManyIsNotOmitted),
|
||||
("test_optionalToManyIsNotOmitted_encode", test_optionalToManyIsNotOmitted_encode),
|
||||
@@ -227,7 +250,10 @@ extension EntityTests {
|
||||
}
|
||||
|
||||
extension IncludedTests {
|
||||
static let __allTests = [
|
||||
// DO NOT MODIFY: This is autogenerated, use:
|
||||
// `swift test --generate-linuxmain`
|
||||
// to regenerate.
|
||||
static let __allTests__IncludedTests = [
|
||||
("test_appending", test_appending),
|
||||
("test_EightDifferentIncludes", test_EightDifferentIncludes),
|
||||
("test_EightDifferentIncludes_encode", test_EightDifferentIncludes_encode),
|
||||
@@ -256,7 +282,10 @@ extension IncludedTests {
|
||||
}
|
||||
|
||||
extension LinksTests {
|
||||
static let __allTests = [
|
||||
// DO NOT MODIFY: This is autogenerated, use:
|
||||
// `swift test --generate-linuxmain`
|
||||
// to regenerate.
|
||||
static let __allTests__LinksTests = [
|
||||
("test_linkFailsIfMetaNotFound", test_linkFailsIfMetaNotFound),
|
||||
("test_linkWithMetadata", test_linkWithMetadata),
|
||||
("test_linkWithMetadata_encode", test_linkWithMetadata_encode),
|
||||
@@ -270,7 +299,10 @@ extension LinksTests {
|
||||
}
|
||||
|
||||
extension NonJSONAPIRelatableTests {
|
||||
static let __allTests = [
|
||||
// DO NOT MODIFY: This is autogenerated, use:
|
||||
// `swift test --generate-linuxmain`
|
||||
// to regenerate.
|
||||
static let __allTests__NonJSONAPIRelatableTests = [
|
||||
("test_initialization1", test_initialization1),
|
||||
("test_initialization2_all_relationships_missing", test_initialization2_all_relationships_missing),
|
||||
("test_initialization2_all_relationships_there", test_initialization2_all_relationships_there),
|
||||
@@ -278,7 +310,10 @@ extension NonJSONAPIRelatableTests {
|
||||
}
|
||||
|
||||
extension PolyProxyTests {
|
||||
static let __allTests = [
|
||||
// DO NOT MODIFY: This is autogenerated, use:
|
||||
// `swift test --generate-linuxmain`
|
||||
// to regenerate.
|
||||
static let __allTests__PolyProxyTests = [
|
||||
("test_AsymmetricEncodeDecodeUserA", test_AsymmetricEncodeDecodeUserA),
|
||||
("test_AsymmetricEncodeDecodeUserB", test_AsymmetricEncodeDecodeUserB),
|
||||
("test_generalReasonableness", test_generalReasonableness),
|
||||
@@ -289,7 +324,10 @@ extension PolyProxyTests {
|
||||
}
|
||||
|
||||
extension PolyTests {
|
||||
static let __allTests = [
|
||||
// DO NOT MODIFY: This is autogenerated, use:
|
||||
// `swift test --generate-linuxmain`
|
||||
// to regenerate.
|
||||
static let __allTests__PolyTests = [
|
||||
("test_init_Poly0", test_init_Poly0),
|
||||
("test_init_Poly1", test_init_Poly1),
|
||||
("test_init_Poly2", test_init_Poly2),
|
||||
@@ -324,7 +362,10 @@ extension PolyTests {
|
||||
}
|
||||
|
||||
extension RelationshipTests {
|
||||
static let __allTests = [
|
||||
// DO NOT MODIFY: This is autogenerated, use:
|
||||
// `swift test --generate-linuxmain`
|
||||
// to regenerate.
|
||||
static let __allTests__RelationshipTests = [
|
||||
("test_initToManyWithEntities", test_initToManyWithEntities),
|
||||
("test_initToManyWithRelationships", test_initToManyWithRelationships),
|
||||
("test_ToManyRelationship", test_ToManyRelationship),
|
||||
@@ -351,7 +392,10 @@ extension RelationshipTests {
|
||||
}
|
||||
|
||||
extension ResourceBodyTests {
|
||||
static let __allTests = [
|
||||
// DO NOT MODIFY: This is autogenerated, use:
|
||||
// `swift test --generate-linuxmain`
|
||||
// to regenerate.
|
||||
static let __allTests__ResourceBodyTests = [
|
||||
("test_initializers", test_initializers),
|
||||
("test_manyResourceBody", test_manyResourceBody),
|
||||
("test_manyResourceBody_encode", test_manyResourceBody_encode),
|
||||
@@ -363,23 +407,22 @@ extension ResourceBodyTests {
|
||||
]
|
||||
}
|
||||
|
||||
#if !os(macOS)
|
||||
public func __allTests() -> [XCTestCaseEntry] {
|
||||
return [
|
||||
testCase(APIDescriptionTests.__allTests),
|
||||
testCase(AttributeTests.__allTests),
|
||||
testCase(Attribute_FunctorTests.__allTests),
|
||||
testCase(ComputedPropertiesTests.__allTests),
|
||||
testCase(CustomAttributesTests.__allTests),
|
||||
testCase(DocumentTests.__allTests),
|
||||
testCase(EntityTests.__allTests),
|
||||
testCase(IncludedTests.__allTests),
|
||||
testCase(LinksTests.__allTests),
|
||||
testCase(NonJSONAPIRelatableTests.__allTests),
|
||||
testCase(PolyProxyTests.__allTests),
|
||||
testCase(PolyTests.__allTests),
|
||||
testCase(RelationshipTests.__allTests),
|
||||
testCase(ResourceBodyTests.__allTests),
|
||||
testCase(APIDescriptionTests.__allTests__APIDescriptionTests),
|
||||
testCase(AttributeTests.__allTests__AttributeTests),
|
||||
testCase(Attribute_FunctorTests.__allTests__Attribute_FunctorTests),
|
||||
testCase(ComputedPropertiesTests.__allTests__ComputedPropertiesTests),
|
||||
testCase(CustomAttributesTests.__allTests__CustomAttributesTests),
|
||||
testCase(DocumentTests.__allTests__DocumentTests),
|
||||
testCase(EntityTests.__allTests__EntityTests),
|
||||
testCase(IncludedTests.__allTests__IncludedTests),
|
||||
testCase(LinksTests.__allTests__LinksTests),
|
||||
testCase(NonJSONAPIRelatableTests.__allTests__NonJSONAPIRelatableTests),
|
||||
testCase(PolyProxyTests.__allTests__PolyProxyTests),
|
||||
testCase(PolyTests.__allTests__PolyTests),
|
||||
testCase(RelationshipTests.__allTests__RelationshipTests),
|
||||
testCase(ResourceBodyTests.__allTests__ResourceBodyTests),
|
||||
]
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import XCTest
|
||||
|
||||
import JSONAPITests
|
||||
import JSONAPITestingTests
|
||||
import JSONAPITests
|
||||
|
||||
var tests = [XCTestCaseEntry]()
|
||||
tests += JSONAPITests.__allTests()
|
||||
tests += JSONAPITestingTests.__allTests()
|
||||
tests += JSONAPITests.__allTests()
|
||||
|
||||
XCTMain(tests)
|
||||
|
||||
Reference in New Issue
Block a user