mirror of
https://github.com/encounter/JSONAPI.git
synced 2026-03-30 11:18:38 -07:00
Add some more documentation, remove unneded Foundation imports.
This commit is contained in:
+19
-1
@@ -5,7 +5,25 @@
|
||||
// Created by Mathew Polzin on 11/12/18.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import JSONAPI
|
||||
|
||||
enum PersonDescription: IdentifiedEntityDescription {
|
||||
static var type: String { return "people" }
|
||||
|
||||
typealias Identifier = Id<String, PersonDescription>
|
||||
|
||||
struct Attributes: JSONAPI.Attributes {
|
||||
let name: [String]
|
||||
let favoriteColor: String
|
||||
}
|
||||
|
||||
struct Relationships: JSONAPI.Relationships {
|
||||
let friends: ToManyRelationship<PersonDescription>
|
||||
}
|
||||
}
|
||||
|
||||
typealias Person = Entity<PersonDescription>
|
||||
|
||||
func tmp() {
|
||||
let x: Person.Identifier
|
||||
}
|
||||
|
||||
@@ -1,3 +1,201 @@
|
||||
# JSONAPI
|
||||
|
||||
A description of this package.
|
||||
A Swift package for encoding and decoding to *JSON API* compliant requests and responses.
|
||||
|
||||
See the JSON API Spec here: https://jsonapi.org/format/
|
||||
|
||||
## Primary Goals
|
||||
|
||||
The primary goals of this framework are:
|
||||
1. Allow creation of Swift types that are easy to use in code but also can be encoded to- or decoded from *JSON API* compliant payloads without lots of boilerplate code.
|
||||
2. Leverage `Codable` to avoid additional outside dependencies and get operability with non-JSON encoders/decoders for free.
|
||||
3. Do not sacrifice type safety.
|
||||
|
||||
## Project Status
|
||||
|
||||
### Decoding
|
||||
#### Document
|
||||
[x] `data`
|
||||
[x] `included`
|
||||
[x] `errors` (untested)
|
||||
[ ] `meta`
|
||||
[ ] `jsonapi`
|
||||
[ ] `links`
|
||||
|
||||
#### Resource Object
|
||||
[x] `id`
|
||||
[x] `type`
|
||||
[x] `attributes`
|
||||
[x] `relationships`
|
||||
[ ] `links`
|
||||
[ ] `meta`
|
||||
|
||||
#### Relationship Object
|
||||
[x] `data`
|
||||
[ ] `links`
|
||||
[ ] `meta`
|
||||
|
||||
### Encoding
|
||||
#### Document
|
||||
[ ] `data`
|
||||
[ ] `included`
|
||||
[ ] `errors`
|
||||
[ ] `meta`
|
||||
[ ] `jsonapi`
|
||||
[ ] `links`
|
||||
|
||||
#### Resource Object
|
||||
[x] `id` (untested)
|
||||
[x] `type` (untested)
|
||||
[x] `attributes` (untested)
|
||||
[x] `relationships` (untested)
|
||||
[ ] `links`
|
||||
[ ] `meta`
|
||||
|
||||
#### Relationship Object
|
||||
[x] `data` (untested)
|
||||
[ ] `links`
|
||||
[ ] `meta`
|
||||
|
||||
### Misc
|
||||
[ ] `EntityType` validator (using reflection).
|
||||
[ ] Property-based testing (using `SwiftCheck`).
|
||||
|
||||
## Usage
|
||||
### Prerequisites
|
||||
1. Swift 4.2+ and Swift Package Manager
|
||||
|
||||
### `EntityDescription`
|
||||
|
||||
An `EntityDescription` is the `JSONAPI` framework's specification for what the JSON API spec calls a *Resource Objects*. You might create the following `EntityDescription` to represent a person in a network of friends:
|
||||
|
||||
```
|
||||
enum PersonDescription: IdentifiedEntityDescription {
|
||||
static var type: String { return "people" }
|
||||
|
||||
typealias Identifier = Id<String, PersonDescription>
|
||||
|
||||
struct Attributes: JSONAPI.Attributes {
|
||||
let name: [String]
|
||||
let favoriteColor: String
|
||||
}
|
||||
|
||||
struct Relationships: JSONAPI.Relationships {
|
||||
let friends: ToManyRelationship<PersonDescription>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Note that an `enum` type is used here; 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.
|
||||
|
||||
This readme doesn't go into detail on the JSON API Spec, but the following JSON API *Resource Object* would be described by the above `PersonDescription`:
|
||||
|
||||
```
|
||||
{
|
||||
"type": "people",
|
||||
"id": "9",
|
||||
"attributes": {
|
||||
"name": [
|
||||
"Jane",
|
||||
"Doe"
|
||||
],
|
||||
"favoriteColor": "Green"
|
||||
},
|
||||
"relationships": {
|
||||
"friends": {
|
||||
"data": [
|
||||
{
|
||||
"id": "7",
|
||||
"type": "people"
|
||||
},
|
||||
{
|
||||
"id": "8",
|
||||
"type": "people"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### `Entity`
|
||||
|
||||
Once you have an `EntityDescription`, you _create_, _encode_, and _decode_ `Entity`s that "fit the description". If you have a `CreatableRawIdType` (see the section on `RawIdType`s below) then you can create new `Entity<PersonDescription>`s, but even without a `CreatableRawIdType` you can decode and work with entities.
|
||||
|
||||
The `Entity` and `EntityDescription` together embody the rules and properties of a JSON API *Resource Object*.
|
||||
|
||||
It can be nice to create a `typealias` for each type of entity you want to work with:
|
||||
|
||||
```
|
||||
typealias Person = Entity<PersonDescription>
|
||||
```
|
||||
|
||||
### `Relationships`
|
||||
|
||||
There are two types of `Relationship`s: `ToOneRelationship` and `ToManyRelationship`. An `EntityDescription`'s `Relationships` type can contain any number of `Relationship`s of either of these types. Do not store anything other than `Relationship`s in the `Relationships` type of an `EntityDescription`.
|
||||
|
||||
An entity that does not have relationships can be described by adding the following to an `EntityDescription`:
|
||||
```
|
||||
typealias Relationships = NoRelatives
|
||||
```
|
||||
|
||||
`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):
|
||||
```
|
||||
let friendIds: [Person.Identifier] = person ~> \.friends
|
||||
```
|
||||
|
||||
### `Attributes`
|
||||
|
||||
The `Attributes` of an `EntityDescription` can contain any JSON encodable/decodable types. This is the place to store all attributes of an entity.
|
||||
|
||||
An entity that does not have attributes can be described by adding the following to an `EntityDescription`:
|
||||
```
|
||||
typealias Attributes = NoAttributes
|
||||
```
|
||||
|
||||
`Attributes` can be accessed via the `subscript` operator of the `Entity` type as follows:
|
||||
```
|
||||
let favoriteColor: String = person[\.favoriteColor]
|
||||
```
|
||||
|
||||
### `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<PersonDescription>, NoIncludes, BasicJSONAPIError>.self
|
||||
|
||||
let document = try decoder.decode(responseStructure, from: data)
|
||||
```
|
||||
|
||||
#### `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`.
|
||||
|
||||
#### `IncludeDecoder`
|
||||
|
||||
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.
|
||||
|
||||
To specify that we expect friends of a person to be included in the above example `JSONAPIDocument`, we would use `Include1<PersonDescription>` instead of `NoIncludes`.
|
||||
|
||||
#### `Error`
|
||||
|
||||
The final generic type of a `JSONAPIDocument` is the `Error`. You should create an error type that can decode all the errors you expect your `JSONAPIDocument` to be able to decode. As prescribed by the JSON API Spec, these errors will be found in the root document member `errors`.
|
||||
|
||||
### `RawIdType`
|
||||
|
||||
If you want to create new `JSONAPI.Entity` values and assign them Ids then you will need to conform at least one type to `CreatableRawIdType`. Doing so is easy; here are two example conformances for `UUID` and `String` (via `UUID`):
|
||||
```
|
||||
extension UUID: CreatableRawIdType {
|
||||
public static func unique() -> UUID {
|
||||
return UUID()
|
||||
}
|
||||
}
|
||||
|
||||
extension String: CreatableRawIdType {
|
||||
public static func unique() -> String {
|
||||
return UUID().uuidString
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -5,8 +5,6 @@
|
||||
// Created by Mathew Polzin on 11/5/18.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// A JSON API Document represents the entire body
|
||||
/// of a JSON API request or the entire body of
|
||||
/// a JSON API response.
|
||||
|
||||
@@ -5,8 +5,6 @@
|
||||
// Created by Mathew Polzin on 11/10/18.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public protocol JSONAPIError: Swift.Error {
|
||||
static var unknown: Self { get }
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
// Created by Mathew Polzin on 11/10/18.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Result
|
||||
|
||||
public protocol IncludeDecoder: Decodable {}
|
||||
@@ -37,7 +36,7 @@ public struct Includes<I: IncludeDecoder>: Decodable {
|
||||
|
||||
// MARK: - Decoding
|
||||
|
||||
func decode<EntityType: JSONAPI.EntityType>(_ type: EntityType.Type, from container: SingleValueDecodingContainer) throws -> Result<Entity<EntityType>, EncodingError> {
|
||||
func decode<EntityType: JSONAPI.EntityDescription>(_ type: EntityType.Type, from container: SingleValueDecodingContainer) throws -> Result<Entity<EntityType>, EncodingError> {
|
||||
let ret: Result<Entity<EntityType>, EncodingError>
|
||||
do {
|
||||
ret = try .success(container.decode(Entity<EntityType>.self))
|
||||
@@ -64,10 +63,10 @@ public typealias NoIncludes = Include0
|
||||
|
||||
// MARK: - 1 include
|
||||
public protocol _Include1: _Include0 {
|
||||
associatedtype A: EntityType
|
||||
associatedtype A: EntityDescription
|
||||
var a: Entity<A>? { get }
|
||||
}
|
||||
public enum Include1<A: EntityType>: _Include1 {
|
||||
public enum Include1<A: EntityDescription>: _Include1 {
|
||||
case a(Entity<A>)
|
||||
|
||||
public var a: Entity<A>? {
|
||||
@@ -94,10 +93,10 @@ extension Includes where I: _Include1 {
|
||||
|
||||
// MARK: - 2 includes
|
||||
public protocol _Include2: _Include1 {
|
||||
associatedtype B: EntityType
|
||||
associatedtype B: EntityDescription
|
||||
var b: Entity<B>? { get }
|
||||
}
|
||||
public enum Include2<A: EntityType, B: EntityType>: _Include2 {
|
||||
public enum Include2<A: EntityDescription, B: EntityDescription>: _Include2 {
|
||||
case a(Entity<A>)
|
||||
case b(Entity<B>)
|
||||
|
||||
@@ -142,10 +141,10 @@ extension Includes where I: _Include2 {
|
||||
|
||||
// MARK: - 3 includes
|
||||
public protocol _Include3: _Include2 {
|
||||
associatedtype C: EntityType
|
||||
associatedtype C: EntityDescription
|
||||
var c: Entity<C>? { get }
|
||||
}
|
||||
public enum Include3<A: EntityType, B: EntityType, C: EntityType>: _Include3 {
|
||||
public enum Include3<A: EntityDescription, B: EntityDescription, C: EntityDescription>: _Include3 {
|
||||
case a(Entity<A>)
|
||||
case b(Entity<B>)
|
||||
case c(Entity<C>)
|
||||
@@ -197,10 +196,10 @@ extension Includes where I: _Include3 {
|
||||
|
||||
// MARK: - 4 includes
|
||||
public protocol _Include4: _Include3 {
|
||||
associatedtype D: EntityType
|
||||
associatedtype D: EntityDescription
|
||||
var d: Entity<D>? { get }
|
||||
}
|
||||
public enum Include4<A: EntityType, B: EntityType, C: EntityType, D: EntityType>: _Include4 {
|
||||
public enum Include4<A: EntityDescription, B: EntityDescription, C: EntityDescription, D: EntityDescription>: _Include4 {
|
||||
case a(Entity<A>)
|
||||
case b(Entity<B>)
|
||||
case c(Entity<C>)
|
||||
@@ -259,10 +258,10 @@ extension Includes where I: _Include4 {
|
||||
|
||||
// MARK: - 5 includes
|
||||
public protocol _Include5: _Include4 {
|
||||
associatedtype E: EntityType
|
||||
associatedtype E: EntityDescription
|
||||
var e: Entity<E>? { get }
|
||||
}
|
||||
public enum Include5<A: EntityType, B: EntityType, C: EntityType, D: EntityType, E: EntityType>: _Include5 {
|
||||
public enum Include5<A: EntityDescription, B: EntityDescription, C: EntityDescription, D: EntityDescription, E: EntityDescription>: _Include5 {
|
||||
case a(Entity<A>)
|
||||
case b(Entity<B>)
|
||||
case c(Entity<C>)
|
||||
@@ -328,10 +327,10 @@ extension Includes where I: _Include5 {
|
||||
|
||||
// MARK: - 6 includes
|
||||
public protocol _Include6: _Include5 {
|
||||
associatedtype F: EntityType
|
||||
associatedtype F: EntityDescription
|
||||
var f: Entity<F>? { get }
|
||||
}
|
||||
public enum Include6<A: EntityType, B: EntityType, C: EntityType, D: EntityType, E: EntityType, F: EntityType>: _Include6 {
|
||||
public enum Include6<A: EntityDescription, B: EntityDescription, C: EntityDescription, D: EntityDescription, E: EntityDescription, F: EntityDescription>: _Include6 {
|
||||
case a(Entity<A>)
|
||||
case b(Entity<B>)
|
||||
case c(Entity<C>)
|
||||
|
||||
@@ -5,18 +5,16 @@
|
||||
// Created by Mathew Polzin on 11/10/18.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public protocol ResourceBody: Decodable {
|
||||
typealias Single<EntityType: JSONAPI.EntityType> = SingleResourceBody<EntityType>
|
||||
typealias Many<EntityType: JSONAPI.EntityType> = ManyResourceBody<EntityType>
|
||||
typealias Single<EntityType: JSONAPI.EntityDescription> = SingleResourceBody<EntityType>
|
||||
typealias Many<EntityType: JSONAPI.EntityDescription> = ManyResourceBody<EntityType>
|
||||
}
|
||||
|
||||
public struct SingleResourceBody<EntityType: JSONAPI.EntityType>: ResourceBody {
|
||||
public struct SingleResourceBody<EntityType: JSONAPI.EntityDescription>: ResourceBody {
|
||||
public let value: Entity<EntityType>?
|
||||
}
|
||||
|
||||
public struct ManyResourceBody<EntityType: JSONAPI.EntityType>: ResourceBody {
|
||||
public struct ManyResourceBody<EntityType: JSONAPI.EntityDescription>: ResourceBody {
|
||||
public let values: [Entity<EntityType>]
|
||||
}
|
||||
|
||||
|
||||
@@ -22,40 +22,42 @@ public struct NoRelatives: Relationships {}
|
||||
/// have any Attributes.
|
||||
public struct NoAttributes: Attributes {}
|
||||
|
||||
/// An `EntityType` describes a JSON API
|
||||
/// An `EntityDescription` describes a JSON API
|
||||
/// Resource Object. The Resource Object
|
||||
/// itself is encoded and decoded as an
|
||||
/// `Entity`, which gets specialized on an
|
||||
/// `EntityType`.
|
||||
public protocol EntityType {
|
||||
/// `EntityDescription`.
|
||||
public protocol EntityDescription {
|
||||
associatedtype Identifier: JSONAPI.Identifier
|
||||
associatedtype AttributeType: Attributes
|
||||
associatedtype RelatedType: Relationships
|
||||
associatedtype Attributes: JSONAPI.Attributes
|
||||
associatedtype Relationships: JSONAPI.Relationships
|
||||
|
||||
static var type: String { get }
|
||||
}
|
||||
|
||||
/// Shorthand for an `EntityType` that has an Id.
|
||||
/// The only times you would not want an `EntityType`
|
||||
/// to not be an `IdentifiedEntityType` are when you
|
||||
/// Shorthand for an `EntityDescription` that has an Id.
|
||||
/// The only times you would not want an `EntityDescription`
|
||||
/// to not be an `IdentifiedEntityDescription` are when you
|
||||
/// are a client making a request to create a new `Entity`
|
||||
/// or you are a server receiving a request to create a
|
||||
/// new `Entity`.
|
||||
public protocol IdentifiedEntityType: EntityType where Identifier: IdType {}
|
||||
public protocol IdentifiedEntityDescription: EntityDescription where Identifier: IdType {}
|
||||
|
||||
/// Shorthand for an `EntityType` that does not have an Id.
|
||||
/// The only times you would not want an `EntityType`
|
||||
/// to not be an `IdentifiedEntityType` are when you
|
||||
/// Shorthand for an `EntityDescription` that does not have an Id.
|
||||
/// The only times you would not want an `EntityDescription`
|
||||
/// to not be an `IdentifiedEntityDescription` are when you
|
||||
/// are a client making a request to create a new `Entity`
|
||||
/// or you are a server receiving a request to create a
|
||||
/// new `Entity`.
|
||||
public protocol UnidentifiedEntityType: EntityType where Identifier == Unidentified {}
|
||||
public protocol UnidentifiedEntityDescription: EntityDescription where Identifier == Unidentified {}
|
||||
|
||||
/// An `Entity` is a single model type that can be
|
||||
/// encoded to or decoded from a JSON API
|
||||
/// "Resource Object."
|
||||
/// See https://jsonapi.org/format/#document-resource-objects
|
||||
public struct Entity<EntityType: JSONAPI.EntityType>: Codable, Equatable {
|
||||
public struct Entity<EntityType: JSONAPI.EntityDescription>: Codable, Equatable {
|
||||
public typealias Identifier = EntityType.Identifier
|
||||
|
||||
/// The JSON API compliant "type" of this `Entity`.
|
||||
public static var type: String { return EntityType.type }
|
||||
|
||||
@@ -63,59 +65,67 @@ public struct Entity<EntityType: JSONAPI.EntityType>: Codable, Equatable {
|
||||
/// the entity is being created clientside and the
|
||||
/// server is being asked to create a unique Id. Otherwise,
|
||||
/// this should be of a type conforming to `IdType`.
|
||||
public let id: EntityType.Identifier
|
||||
public let id: Identifier
|
||||
|
||||
/// The JSON API compliant attributes of this `Entity`.
|
||||
public let attributes: EntityType.AttributeType
|
||||
public let attributes: EntityType.Attributes
|
||||
|
||||
/// The JSON API compliant relationships of this `Entity`.
|
||||
public let relationships: EntityType.RelatedType
|
||||
public let relationships: EntityType.Relationships
|
||||
|
||||
public init(id: EntityType.Identifier, attributes: EntityType.AttributeType, relationships: EntityType.RelatedType) {
|
||||
public init(id: EntityType.Identifier, attributes: EntityType.Attributes, relationships: EntityType.Relationships) {
|
||||
self.id = id
|
||||
self.attributes = attributes
|
||||
self.relationships = relationships
|
||||
}
|
||||
|
||||
public init(attributes: EntityType.AttributeType, relationships: EntityType.RelatedType) {
|
||||
self.id = .init()
|
||||
}
|
||||
|
||||
// MARK: Convenience initializers
|
||||
extension Entity where EntityType.Identifier: CreatableIdType {
|
||||
public init(attributes: EntityType.Attributes, relationships: EntityType.Relationships) {
|
||||
self.id = EntityType.Identifier()
|
||||
self.attributes = attributes
|
||||
self.relationships = relationships
|
||||
}
|
||||
}
|
||||
|
||||
extension Entity where EntityType.AttributeType == NoAttributes {
|
||||
public init(id: EntityType.Identifier, relationships: EntityType.RelatedType) {
|
||||
extension Entity where EntityType.Attributes == NoAttributes {
|
||||
public init(id: EntityType.Identifier, relationships: EntityType.Relationships) {
|
||||
self.init(id: id, attributes: NoAttributes(), relationships: relationships)
|
||||
}
|
||||
|
||||
public init(relationships: EntityType.RelatedType) {
|
||||
}
|
||||
|
||||
extension Entity where EntityType.Attributes == NoAttributes, EntityType.Identifier: CreatableIdType {
|
||||
public init(relationships: EntityType.Relationships) {
|
||||
self.init(attributes: NoAttributes(), relationships: relationships)
|
||||
}
|
||||
}
|
||||
|
||||
extension Entity where EntityType.RelatedType == NoRelatives {
|
||||
public init(id: EntityType.Identifier, attributes: EntityType.AttributeType) {
|
||||
extension Entity where EntityType.Relationships == NoRelatives {
|
||||
public init(id: EntityType.Identifier, attributes: EntityType.Attributes) {
|
||||
self.init(id: id, attributes: attributes, relationships: NoRelatives())
|
||||
}
|
||||
|
||||
public init(attributes: EntityType.AttributeType) {
|
||||
}
|
||||
|
||||
extension Entity where EntityType.Relationships == NoRelatives, EntityType.Identifier: CreatableIdType {
|
||||
public init(attributes: EntityType.Attributes) {
|
||||
self.init(attributes: attributes, relationships: NoRelatives())
|
||||
}
|
||||
}
|
||||
|
||||
extension Entity where EntityType.AttributeType == NoAttributes, EntityType.RelatedType == NoRelatives {
|
||||
extension Entity where EntityType.Attributes == NoAttributes, EntityType.Relationships == NoRelatives {
|
||||
public init(id: EntityType.Identifier) {
|
||||
self.init(id: id, attributes: NoAttributes(), relationships: NoRelatives())
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension Entity where EntityType.Attributes == NoAttributes, EntityType.Relationships == NoRelatives, EntityType.Identifier: CreatableIdType {
|
||||
public init() {
|
||||
self.init(attributes: NoAttributes(), relationships: NoRelatives())
|
||||
}
|
||||
}
|
||||
|
||||
//public protocol IdentifiedEntityType: JSONAPI.EntityType where IdentifiedEntityType.Identifier: IdType, Identifier.Entity == Self {}
|
||||
|
||||
// MARK: Pointer for Relationships use.
|
||||
public extension Entity where EntityType.Identifier: IdType {
|
||||
/// Get a pointer to this entity that can be used as a
|
||||
/// relationship to another entity.
|
||||
@@ -126,18 +136,27 @@ public extension Entity where EntityType.Identifier: IdType {
|
||||
|
||||
// MARK: Attribute Access
|
||||
public extension Entity {
|
||||
subscript<T>(_ path: KeyPath<EntityType.AttributeType, T>) -> T {
|
||||
/// Access the attribute at the given keypath. This just
|
||||
/// allows you to write `entity[\.propertyName]` instead
|
||||
/// of `entity.relationships.propertyName`.
|
||||
subscript<T>(_ path: KeyPath<EntityType.Attributes, T>) -> T {
|
||||
return attributes[keyPath: path]
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Relationship Access
|
||||
public extension Entity {
|
||||
public static func ~><OtherEntityType: JSONAPI.EntityType>(entity: Entity<EntityType>, path: KeyPath<EntityType.RelatedType, ToOneRelationship<OtherEntityType>>) -> OtherEntityType.Identifier {
|
||||
/// Access to an Id of a `ToOneRelationship`.
|
||||
/// This allows you to write `entity ~> \.other` instead
|
||||
/// of `entity.relationships.other.id`.
|
||||
public static func ~><OtherEntityType: JSONAPI.EntityDescription>(entity: Entity<EntityType>, path: KeyPath<EntityType.Relationships, ToOneRelationship<OtherEntityType>>) -> OtherEntityType.Identifier {
|
||||
return entity.relationships[keyPath: path].id
|
||||
}
|
||||
|
||||
public static func ~><OtherEntityType: JSONAPI.EntityType>(entity: Entity<EntityType>, path: KeyPath<EntityType.RelatedType, ToManyRelationship<OtherEntityType>>) -> [OtherEntityType.Identifier] {
|
||||
/// Access to all Ids of a `ToManyRelationship`.
|
||||
/// This allows you to write `entity ~> \.others` instead
|
||||
/// of `entity.relationships.others.ids`.
|
||||
public static func ~><OtherEntityType: JSONAPI.EntityDescription>(entity: Entity<EntityType>, path: KeyPath<EntityType.Relationships, ToManyRelationship<OtherEntityType>>) -> [OtherEntityType.Identifier] {
|
||||
return entity.relationships[keyPath: path].ids
|
||||
}
|
||||
}
|
||||
@@ -162,11 +181,11 @@ public extension Entity {
|
||||
try container.encode(id, forKey: .id)
|
||||
}
|
||||
|
||||
if EntityType.AttributeType.self != NoAttributes.self {
|
||||
if EntityType.Attributes.self != NoAttributes.self {
|
||||
try container.encode(attributes, forKey: .attributes)
|
||||
}
|
||||
|
||||
if EntityType.RelatedType.self != NoRelatives.self {
|
||||
if EntityType.Relationships.self != NoRelatives.self {
|
||||
try container.encode(relationships, forKey: .relationships)
|
||||
}
|
||||
}
|
||||
@@ -183,8 +202,8 @@ public extension Entity {
|
||||
|
||||
id = try (Unidentified() as? EntityType.Identifier) ?? container.decode(EntityType.Identifier.self, forKey: .id)
|
||||
|
||||
attributes = try (NoAttributes() as? EntityType.AttributeType) ?? container.decode(EntityType.AttributeType.self, forKey: .attributes)
|
||||
attributes = try (NoAttributes() as? EntityType.Attributes) ?? container.decode(EntityType.Attributes.self, forKey: .attributes)
|
||||
|
||||
relationships = try (NoRelatives() as? EntityType.RelatedType) ?? container.decode(EntityType.RelatedType.self, forKey: .relationships)
|
||||
relationships = try (NoRelatives() as? EntityType.Relationships) ?? container.decode(EntityType.Relationships.self, forKey: .relationships)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,46 +5,43 @@
|
||||
// Created by Mathew Polzin on 7/24/18.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// Any type that you would like to be encoded to and
|
||||
/// decoded from JSON API ids should conform to this
|
||||
/// protocol. Conformance for `String` and `UUID`
|
||||
/// is given by this library.
|
||||
public protocol RawIdType: Codable, Equatable {
|
||||
/// protocol. Conformance for `String` is given.
|
||||
public protocol RawIdType: Codable, Equatable {}
|
||||
|
||||
/// If you would like to be able to create new
|
||||
/// Entities with Ids backed by a RawIdType then
|
||||
/// your Id type should conform to
|
||||
/// `CreatableRawIdType`.
|
||||
/// Conformances for `String` and `UUID`
|
||||
/// are given in the README for this library.
|
||||
public protocol CreatableRawIdType: RawIdType {
|
||||
static func unique() -> Self
|
||||
}
|
||||
|
||||
public protocol Identifier: Codable, Equatable {
|
||||
init()
|
||||
}
|
||||
extension String: RawIdType {}
|
||||
|
||||
public protocol Identifier: Codable, Equatable {}
|
||||
|
||||
public struct Unidentified: Identifier {
|
||||
public init() {}
|
||||
}
|
||||
|
||||
public protocol IdType: Identifier {
|
||||
associatedtype EntityType: JSONAPI.EntityType
|
||||
associatedtype EntityType: JSONAPI.EntityDescription
|
||||
associatedtype RawType: RawIdType
|
||||
|
||||
var rawValue: RawType { get }
|
||||
}
|
||||
|
||||
extension UUID: RawIdType {
|
||||
public static func unique() -> UUID {
|
||||
return UUID()
|
||||
}
|
||||
}
|
||||
|
||||
extension String: RawIdType {
|
||||
public static func unique() -> String {
|
||||
return UUID().uuidString
|
||||
}
|
||||
public protocol CreatableIdType: IdType {
|
||||
init()
|
||||
}
|
||||
|
||||
/// An Entity ID. These IDs can be encoded to or decoded from
|
||||
/// JSON API IDs.
|
||||
public struct Id<RawType: RawIdType, EntityType: JSONAPI.EntityType>: IdType {
|
||||
public struct Id<RawType: RawIdType, EntityType: JSONAPI.EntityDescription>: IdType {
|
||||
public let rawValue: RawType
|
||||
|
||||
public init(rawValue: RawType) {
|
||||
@@ -60,7 +57,9 @@ public struct Id<RawType: RawIdType, EntityType: JSONAPI.EntityType>: IdType {
|
||||
var container = encoder.singleValueContainer()
|
||||
try container.encode(rawValue)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension Id: CreatableIdType where RawType: CreatableRawIdType {
|
||||
public init() {
|
||||
rawValue = .unique()
|
||||
}
|
||||
|
||||
@@ -5,23 +5,21 @@
|
||||
// Created by Mathew Polzin on 8/31/18.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// An Entity relationship that can be encoded to or decoded from
|
||||
/// a JSON API "Resource Linkage."
|
||||
/// You should use the `ToOneRelationship` and `ToManyRelationship`
|
||||
/// concrete types.
|
||||
/// See https://jsonapi.org/format/#document-resource-object-linkage
|
||||
public protocol Relationship: Equatable, Encodable {
|
||||
associatedtype EntityType: JSONAPI.EntityType where EntityType.Identifier: IdType
|
||||
associatedtype EntityType: JSONAPI.EntityDescription where EntityType.Identifier: IdType
|
||||
var ids: [EntityType.Identifier] { get }
|
||||
}
|
||||
|
||||
/// 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<Entity>`
|
||||
public struct ToOneRelationship<EntityType: JSONAPI.EntityType>: Equatable, Relationship, Decodable where EntityType.Identifier: IdType {
|
||||
/// A convenient typealias might make your code much more legible: `One<EntityDescription>`
|
||||
public struct ToOneRelationship<EntityType: JSONAPI.EntityDescription>: Equatable, Relationship, Decodable where EntityType.Identifier: IdType {
|
||||
public let id: EntityType.Identifier
|
||||
|
||||
public init(entity: Entity<EntityType>) {
|
||||
@@ -36,8 +34,8 @@ public struct ToOneRelationship<EntityType: JSONAPI.EntityType>: Equatable, Rela
|
||||
/// 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: `Many<Entity>`
|
||||
public struct ToManyRelationship<EntityType: JSONAPI.EntityType>: Equatable, Relationship, Decodable where EntityType.Identifier: IdType {
|
||||
/// A convenient typealias might make your code much more legible: `Many<EntityDescription>`
|
||||
public struct ToManyRelationship<EntityType: JSONAPI.EntityDescription>: Equatable, Relationship, Decodable where EntityType.Identifier: IdType {
|
||||
public let ids: [EntityType.Identifier]
|
||||
|
||||
public init(entities: [Entity<EntityType>]) {
|
||||
@@ -47,6 +45,10 @@ public struct ToManyRelationship<EntityType: JSONAPI.EntityType>: Equatable, Rel
|
||||
public init<T: Relationship>(relationships: [T]) where T.EntityType == EntityType {
|
||||
ids = relationships.flatMap { $0.ids }
|
||||
}
|
||||
|
||||
public static var none: ToManyRelationship {
|
||||
return .init(entities: [])
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Codable
|
||||
|
||||
@@ -74,22 +74,21 @@ class DocumentTests: XCTestCase {
|
||||
XCTAssertEqual(d.body.data?.included[Author.self][2].id.rawValue, "11")
|
||||
}
|
||||
|
||||
enum AuthorType: EntityType {
|
||||
enum AuthorType: EntityDescription {
|
||||
static var type: String { return "authors" }
|
||||
|
||||
typealias Identifier = Id<String, AuthorType>
|
||||
typealias AttributeType = NoAttributes
|
||||
typealias RelatedType = NoRelatives
|
||||
typealias Attributes = NoAttributes
|
||||
typealias Relationships = NoRelatives
|
||||
}
|
||||
|
||||
typealias Author = Entity<AuthorType>
|
||||
|
||||
enum ArticleType: EntityType {
|
||||
enum ArticleType: EntityDescription {
|
||||
static var type: String { return "articles" }
|
||||
|
||||
typealias Identifier = Id<String, ArticleType>
|
||||
typealias AttributeType = NoAttributes
|
||||
typealias RelatedType = Relationships
|
||||
typealias Attributes = NoAttributes
|
||||
|
||||
struct Relationships: JSONAPI.Relationships {
|
||||
let author: ToOneRelationship<AuthorType>
|
||||
|
||||
@@ -5,8 +5,6 @@
|
||||
// Created by Mathew Polzin on 11/12/18.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
let single_document_no_includes = """
|
||||
{
|
||||
"data": {
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
//
|
||||
|
||||
import XCTest
|
||||
import Foundation
|
||||
import JSONAPI
|
||||
|
||||
class EntityTests: XCTestCase {
|
||||
@@ -68,7 +67,7 @@ class EntityTests: XCTestCase {
|
||||
|
||||
guard let e = entity else { return }
|
||||
|
||||
XCTAssertEqual((e ~> \.others).map { $0.rawValue.uuidString }, ["364B3B69-4DF1-467F-B52E-B0C9E44F666E"])
|
||||
XCTAssertEqual((e ~> \.others).map { $0.rawValue }, ["364B3B69-4DF1-467F-B52E-B0C9E44F666E"])
|
||||
}
|
||||
|
||||
func test_EntitySomeRelationshipsSomeAttributes() {
|
||||
@@ -80,25 +79,24 @@ class EntityTests: XCTestCase {
|
||||
|
||||
XCTAssertEqual(e[\.word], "coolio")
|
||||
XCTAssertEqual(e[\.number], 992299)
|
||||
XCTAssertEqual((e ~> \.other).rawValue.uuidString, "2DF03B69-4B0A-467F-B52E-B0C9E44FCECF")
|
||||
XCTAssertEqual((e ~> \.other).rawValue, "2DF03B69-4B0A-467F-B52E-B0C9E44FCECF")
|
||||
}
|
||||
|
||||
enum TestEntityType1: EntityType {
|
||||
enum TestEntityType1: EntityDescription {
|
||||
static var type: String { return "test_entities"}
|
||||
|
||||
typealias Identifier = Id<UUID, TestEntityType1>
|
||||
typealias AttributeType = NoAttributes
|
||||
typealias RelatedType = NoRelatives
|
||||
typealias Identifier = Id<String, TestEntityType1>
|
||||
typealias Attributes = NoAttributes
|
||||
typealias Relationships = NoRelatives
|
||||
}
|
||||
|
||||
typealias TestEntity1 = Entity<TestEntityType1>
|
||||
|
||||
enum TestEntityType2: EntityType {
|
||||
enum TestEntityType2: EntityDescription {
|
||||
static var type: String { return "second_test_entities"}
|
||||
|
||||
typealias Identifier = Id<UUID, TestEntityType2>
|
||||
typealias RelatedType = Relationships
|
||||
typealias AttributeType = NoAttributes
|
||||
typealias Identifier = Id<String, TestEntityType2>
|
||||
typealias Attributes = NoAttributes
|
||||
|
||||
struct Relationships: JSONAPI.Relationships {
|
||||
let other: ToOneRelationship<TestEntityType1>
|
||||
@@ -107,12 +105,11 @@ class EntityTests: XCTestCase {
|
||||
|
||||
typealias TestEntity2 = Entity<TestEntityType2>
|
||||
|
||||
enum TestEntityType3: EntityType {
|
||||
enum TestEntityType3: EntityDescription {
|
||||
static var type: String { return "third_test_entities"}
|
||||
|
||||
typealias Identifier = Id<UUID, TestEntityType3>
|
||||
typealias RelatedType = Relationships
|
||||
typealias AttributeType = NoAttributes
|
||||
typealias Identifier = Id<String, TestEntityType3>
|
||||
typealias Attributes = NoAttributes
|
||||
|
||||
struct Relationships: JSONAPI.Relationships {
|
||||
let others: ToManyRelationship<TestEntityType1>
|
||||
@@ -121,18 +118,16 @@ class EntityTests: XCTestCase {
|
||||
|
||||
typealias TestEntity3 = Entity<TestEntityType3>
|
||||
|
||||
enum TestEntityType4: EntityType {
|
||||
enum TestEntityType4: EntityDescription {
|
||||
static var type: String { return "fourth_test_entities"}
|
||||
|
||||
typealias Identifier = Id<UUID, TestEntityType4>
|
||||
typealias RelatedType = Relationships
|
||||
typealias AttributeType = Atts
|
||||
typealias Identifier = Id<String, TestEntityType4>
|
||||
|
||||
struct Relationships: JSONAPI.Relationships {
|
||||
let other: ToOneRelationship<TestEntityType2>
|
||||
}
|
||||
|
||||
struct Atts: Attributes {
|
||||
struct Attributes: JSONAPI.Attributes {
|
||||
let word: String
|
||||
let number: Int
|
||||
}
|
||||
@@ -140,14 +135,13 @@ class EntityTests: XCTestCase {
|
||||
|
||||
typealias TestEntity4 = Entity<TestEntityType4>
|
||||
|
||||
enum TestEntityType5: EntityType {
|
||||
enum TestEntityType5: EntityDescription {
|
||||
static var type: String { return "fifth_test_entities"}
|
||||
|
||||
typealias Identifier = Id<UUID, TestEntityType5>
|
||||
typealias RelatedType = NoRelatives
|
||||
typealias AttributeType = Atts
|
||||
typealias Identifier = Id<String, TestEntityType5>
|
||||
typealias Relationships = NoRelatives
|
||||
|
||||
struct Atts: Attributes {
|
||||
struct Attributes: JSONAPI.Attributes {
|
||||
let floater: Double
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,8 +5,6 @@
|
||||
// Created by Mathew Polzin on 11/12/18.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
let entity_no_relationships_no_attributes = """
|
||||
{
|
||||
"id": "A24B3B69-4DF1-467F-B52E-B0C9E44F436A",
|
||||
|
||||
@@ -107,16 +107,14 @@ class IncludedTests: XCTestCase {
|
||||
}
|
||||
|
||||
extension IncludedTests {
|
||||
enum TestEntityType: EntityType {
|
||||
typealias Identifier = Id<UUID, TestEntityType>
|
||||
enum TestEntityType: EntityDescription {
|
||||
typealias Identifier = Id<String, TestEntityType>
|
||||
|
||||
typealias AttributeType = Atts
|
||||
|
||||
typealias RelatedType = NoRelatives
|
||||
typealias Relationships = NoRelatives
|
||||
|
||||
public static var type: String { return "test_entity1" }
|
||||
|
||||
public struct Atts: Attributes {
|
||||
public struct Attributes: JSONAPI.Attributes {
|
||||
let foo: String
|
||||
let bar: Int
|
||||
}
|
||||
@@ -124,12 +122,8 @@ extension IncludedTests {
|
||||
|
||||
typealias TestEntity = Entity<TestEntityType>
|
||||
|
||||
enum TestEntityType2: EntityType {
|
||||
typealias Identifier = Id<UUID, TestEntityType2>
|
||||
|
||||
typealias AttributeType = Atts
|
||||
|
||||
typealias RelatedType = Relationships
|
||||
enum TestEntityType2: EntityDescription {
|
||||
typealias Identifier = Id<String, TestEntityType2>
|
||||
|
||||
public static var type: String { return "test_entity2" }
|
||||
|
||||
@@ -137,7 +131,7 @@ extension IncludedTests {
|
||||
let entity1: ToOneRelationship<TestEntityType>
|
||||
}
|
||||
|
||||
public struct Atts: Attributes {
|
||||
public struct Attributes: JSONAPI.Attributes {
|
||||
let foo: String
|
||||
let bar: Int
|
||||
}
|
||||
@@ -145,12 +139,10 @@ extension IncludedTests {
|
||||
|
||||
typealias TestEntity2 = Entity<TestEntityType2>
|
||||
|
||||
enum TestEntityType3: EntityType {
|
||||
typealias Identifier = Id<UUID, TestEntityType3>
|
||||
enum TestEntityType3: EntityDescription {
|
||||
typealias Identifier = Id<String, TestEntityType3>
|
||||
|
||||
typealias AttributeType = NoAttributes
|
||||
|
||||
typealias RelatedType = Relationships
|
||||
typealias Attributes = NoAttributes
|
||||
|
||||
public static var type: String { return "test_entity3" }
|
||||
|
||||
@@ -162,36 +154,34 @@ extension IncludedTests {
|
||||
|
||||
typealias TestEntity3 = Entity<TestEntityType3>
|
||||
|
||||
enum TestEntityType4: EntityType {
|
||||
typealias Identifier = Id<UUID, TestEntityType4>
|
||||
enum TestEntityType4: EntityDescription {
|
||||
typealias Identifier = Id<String, TestEntityType4>
|
||||
|
||||
typealias AttributeType = NoAttributes
|
||||
typealias Attributes = NoAttributes
|
||||
|
||||
typealias RelatedType = NoRelatives
|
||||
typealias Relationships = NoRelatives
|
||||
|
||||
public static var type: String { return "test_entity4" }
|
||||
}
|
||||
|
||||
typealias TestEntity4 = Entity<TestEntityType4>
|
||||
|
||||
enum TestEntityType5: EntityType {
|
||||
typealias Identifier = Id<UUID, TestEntityType5>
|
||||
enum TestEntityType5: EntityDescription {
|
||||
typealias Identifier = Id<String, TestEntityType5>
|
||||
|
||||
typealias AttributeType = NoAttributes
|
||||
typealias Attributes = NoAttributes
|
||||
|
||||
typealias RelatedType = NoRelatives
|
||||
typealias Relationships = NoRelatives
|
||||
|
||||
public static var type: String { return "test_entity5" }
|
||||
}
|
||||
|
||||
typealias TestEntity5 = Entity<TestEntityType5>
|
||||
|
||||
enum TestEntityType6: EntityType {
|
||||
typealias Identifier = Id<UUID, TestEntityType6>
|
||||
enum TestEntityType6: EntityDescription {
|
||||
typealias Identifier = Id<String, TestEntityType6>
|
||||
|
||||
typealias AttributeType = NoAttributes
|
||||
|
||||
typealias RelatedType = Relationships
|
||||
typealias Attributes = NoAttributes
|
||||
|
||||
public static var type: String { return "test_entity6" }
|
||||
|
||||
|
||||
@@ -5,8 +5,6 @@
|
||||
// Created by Mathew Polzin on 11/10/18.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
let one_include = """
|
||||
[
|
||||
{
|
||||
|
||||
@@ -37,7 +37,7 @@ class RelationshipTests: XCTestCase {
|
||||
|
||||
XCTAssertNotNil(relationship)
|
||||
|
||||
XCTAssertEqual(relationship?.id.rawValue.uuidString, "2DF03B69-4B0A-467F-B52E-B0C9E44FCECF")
|
||||
XCTAssertEqual(relationship?.id.rawValue, "2DF03B69-4B0A-467F-B52E-B0C9E44FCECF")
|
||||
XCTAssertEqual(relationship?.ids.count, 1)
|
||||
}
|
||||
|
||||
@@ -46,15 +46,15 @@ class RelationshipTests: XCTestCase {
|
||||
|
||||
XCTAssertNotNil(relationship)
|
||||
|
||||
XCTAssertEqual(relationship?.ids.map { $0.rawValue.uuidString }, ["2DF03B69-4B0A-467F-B52E-B0C9E44FCECF", "90F03B69-4DF1-467F-B52E-B0C9E44FC333", "2DF03B69-4B0A-467F-B52E-B0C9E44FCECF"])
|
||||
XCTAssertEqual(relationship?.ids.map { $0.rawValue }, ["2DF03B69-4B0A-467F-B52E-B0C9E44FCECF", "90F03B69-4DF1-467F-B52E-B0C9E44FC333", "2DF03B69-4B0A-467F-B52E-B0C9E44FCECF"])
|
||||
}
|
||||
|
||||
enum TestEntityType1: EntityType {
|
||||
typealias Identifier = Id<UUID, TestEntityType1>
|
||||
enum TestEntityType1: EntityDescription {
|
||||
typealias Identifier = Id<String, TestEntityType1>
|
||||
|
||||
typealias AttributeType = NoAttributes
|
||||
typealias Attributes = NoAttributes
|
||||
|
||||
typealias RelatedType = NoRelatives
|
||||
typealias Relationships = NoRelatives
|
||||
|
||||
public static var type: String { return "test_entity1" }
|
||||
}
|
||||
|
||||
@@ -5,8 +5,6 @@
|
||||
// Created by Mathew Polzin on 11/12/18.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
let to_one_relationship = """
|
||||
{
|
||||
"data": {
|
||||
|
||||
@@ -35,12 +35,11 @@ class ResourceBodyTests: XCTestCase {
|
||||
])
|
||||
}
|
||||
|
||||
enum ArticleType: EntityType {
|
||||
enum ArticleType: EntityDescription {
|
||||
public static var type: String { return "articles" }
|
||||
|
||||
typealias Identifier = Id<String, ArticleType>
|
||||
typealias RelatedType = NoRelatives
|
||||
typealias AttributeType = Attributes
|
||||
typealias Relationships = NoRelatives
|
||||
|
||||
struct Attributes: JSONAPI.Attributes {
|
||||
let title: String
|
||||
|
||||
@@ -5,8 +5,6 @@
|
||||
// Created by Mathew Polzin on 11/12/18.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
let single_resource_body = """
|
||||
{
|
||||
"type": "articles",
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
//
|
||||
// String+CreatableRawIdType.swift
|
||||
// JSONAPITests
|
||||
//
|
||||
// Created by Mathew Polzin on 11/12/18.
|
||||
//
|
||||
|
||||
import JSONAPI
|
||||
|
||||
private var uniqueStringCounter = 0
|
||||
|
||||
extension String: CreatableRawIdType {
|
||||
public static func unique() -> String {
|
||||
uniqueStringCounter += 1
|
||||
return String(uniqueStringCounter)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user