mirror of
https://github.com/encounter/JSONAPI.git
synced 2026-07-10 12:18:40 -07:00
Compare commits
92 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d5a24c4adb | |||
| 669d5d1342 | |||
| 923ab7d9f4 | |||
| 72769b6107 | |||
| 109e15d741 | |||
| 72180f64ef | |||
| fc962f9a0d | |||
| 52eb123166 | |||
| 4ef147ec45 | |||
| 61074ecc69 | |||
| 4dbcff6023 | |||
| d6e82fab55 | |||
| 5fa9848f02 | |||
| f38399a1d6 | |||
| 28f664326d | |||
| 3b7ef4aeb9 | |||
| bb1ed30e89 | |||
| dfdc266645 | |||
| 585cad0a83 | |||
| 89abdd4cca | |||
| 67a43be26c | |||
| 8f279ce191 | |||
| c9d388579f | |||
| 1061283905 | |||
| 08949d0a93 | |||
| 3047e2d23a | |||
| 41a2a01788 | |||
| 53f7f55e07 | |||
| d8d030286d | |||
| 005a981bf7 | |||
| fad45203dd | |||
| 3468cb555a | |||
| 8defe82536 | |||
| 1f83216b03 | |||
| 6e6973c170 | |||
| 4f52cc2fd1 | |||
| 35d6cbb440 | |||
| d667e91a6a | |||
| 75c8ce7405 | |||
| 3a8237ec0f | |||
| 49e33baa96 | |||
| f2a9b5a7b9 | |||
| 24baadd7a8 | |||
| 8d18be6154 | |||
| 8aa887b527 | |||
| 7d0a686dd9 | |||
| e4eb7816d7 | |||
| 482174f5b4 | |||
| 339480264e | |||
| 5d666ec998 | |||
| 9cacbe9b8b | |||
| 6663ad042e | |||
| 07402259c4 | |||
| 8274ecb523 | |||
| 853c7e892a | |||
| 04bf96a0cf | |||
| 8668311307 | |||
| 689517c633 | |||
| 65b80ee0eb | |||
| e91a03b396 | |||
| 82088c7852 | |||
| 34a4c8e7fc | |||
| 6aeb859c24 | |||
| 163ac94c51 | |||
| e67b9fc142 | |||
| a628992fcb | |||
| cf47f88a61 | |||
| 0425e2adcb | |||
| d3763ba713 | |||
| fcc1796731 | |||
| 921bcef05d | |||
| 661ff6eca5 | |||
| e36180c9b9 | |||
| abee0c4d0e | |||
| 9c8b2fbebb | |||
| a9ef71f383 | |||
| 232554ec51 | |||
| 8f36bb40e3 | |||
| 55f2f52676 | |||
| dd5f3b1737 | |||
| 7d7b3d7f19 | |||
| aedd5dc29b | |||
| 3964202ea2 | |||
| dcabafd583 | |||
| 9df9efc2dc | |||
| a55a4cbed0 | |||
| 6f6ed87b4c | |||
| 59ff145aa8 | |||
| 20a685e67b | |||
| 3d8d10584b | |||
| b93580c900 | |||
| 8927938d56 |
@@ -1,41 +0,0 @@
|
||||
|
||||
import Foundation
|
||||
import JSONAPI
|
||||
|
||||
/*******
|
||||
|
||||
Please enjoy these examples, but allow me the forced casting and the lack of error checking for the sake of brevity.
|
||||
|
||||
********/
|
||||
|
||||
// MARK: - Create a request or response body with one Dog in it
|
||||
let dogFromCode = try! Dog(name: "Buddy", owner: nil)
|
||||
|
||||
typealias SingleDogDocument = JSONAPIDocument<SingleResourceBody<Dog>, NoIncludes, BasicJSONAPIError>
|
||||
|
||||
let singleDogDocument = SingleDogDocument(body: SingleResourceBody(entity: dogFromCode))
|
||||
|
||||
let singleDogData = try! JSONEncoder().encode(singleDogDocument)
|
||||
|
||||
// MARK: - Parse a request or response body with one Dog in it
|
||||
let dogResponse = try! JSONDecoder().decode(SingleDogDocument.self, from: singleDogData)
|
||||
let dogFromData = dogResponse.body.data?.primary.value
|
||||
|
||||
// MARK: - Create a request or response with multiple people and dogs and houses included
|
||||
let personIds = [Person.Identifier(), Person.Identifier()]
|
||||
let dogs = try! [Dog(name: "Buddy", owner: personIds[0]), Dog(name: "Joy", owner: personIds[0]), Dog(name: "Travis", owner: personIds[1])]
|
||||
let houses = [House(), House()]
|
||||
let people = try! [Person(id: personIds[0], name: ["Gary", "Doe"], favoriteColor: "Orange-Red", friends: [], dogs: [dogs[0], dogs[1]], home: houses[0]), Person(id: personIds[1], name: ["Elise", "Joy"], favoriteColor: "Red", friends: [], dogs: [dogs[2]], home: houses[1])]
|
||||
|
||||
typealias BatchPeopleDocument = JSONAPIDocument<ManyResourceBody<Person>, Include2<Dog, House>, BasicJSONAPIError>
|
||||
|
||||
let includes = dogs.map { BatchPeopleDocument.Include($0) } + houses.map { BatchPeopleDocument.Include($0) }
|
||||
let batchPeopleDocument = BatchPeopleDocument(body: .init(entities: people), includes: .init(values: includes))
|
||||
let batchPeopleData = try! JSONEncoder().encode(batchPeopleDocument)
|
||||
|
||||
// MARK: - Parse a request or response body with multiple people in it and dogs and houses included
|
||||
|
||||
let peopleResponse = try! JSONDecoder().decode(BatchPeopleDocument.self, from: batchPeopleData)
|
||||
let peopleFromData = peopleResponse.body.data?.primary.values
|
||||
let dogsFromData = peopleResponse.body.data?.included[Dog.self]
|
||||
let housesFromData = peopleResponse.body.data?.included[House.self]
|
||||
@@ -0,0 +1,24 @@
|
||||
//: [Previous](@previous)
|
||||
|
||||
import Foundation
|
||||
import JSONAPI
|
||||
import JSONAPITestLib
|
||||
|
||||
/*******
|
||||
|
||||
Please enjoy these examples, but allow me the forced casting and the lack of error checking for the sake of brevity.
|
||||
|
||||
********/
|
||||
|
||||
// MARK: - Literal Expressibility
|
||||
// The JSONAPITestLib provides literal expressibility for key types to
|
||||
// make creating tests easier
|
||||
let dog = Dog(id: "1234", attributes: Dog.Attributes(name: "Buddy"), relationships: Dog.Relationships(owner: nil), meta: .none, links: .none)
|
||||
|
||||
// MARK: - JSON API structure checking
|
||||
// The JSONAPITestLib provides a `check` function for each Entity type
|
||||
// that uses reflection to catch mistakes that are not forbidden by
|
||||
// Swift's type system but will result in unexpected results when
|
||||
// encoding/decoding. It is a good idea to add a `check` to each of
|
||||
// your unit tests that create Entities.
|
||||
try Dog.check(dog)
|
||||
@@ -0,0 +1,70 @@
|
||||
|
||||
import Foundation
|
||||
import JSONAPI
|
||||
|
||||
/*******
|
||||
|
||||
Please enjoy these examples, but allow me the forced casting and the lack of error checking for the sake of brevity.
|
||||
|
||||
********/
|
||||
|
||||
// MARK: - Create a request or response body with one Dog in it
|
||||
let dogFromCode = try! Dog(name: "Buddy", owner: nil)
|
||||
|
||||
typealias SingleDogDocument = JSONAPI.Document<SingleResourceBody<Dog>, NoMetadata, NoLinks, NoIncludes, NoAPIDescription, UnknownJSONAPIError>
|
||||
|
||||
let singleDogDocument = SingleDogDocument(apiDescription: .none, body: .init(entity: dogFromCode), includes: .none, meta: .none, links: .none)
|
||||
|
||||
let singleDogData = try! JSONEncoder().encode(singleDogDocument)
|
||||
|
||||
// MARK: - Parse a request or response body with one Dog in it
|
||||
let dogResponse = try! JSONDecoder().decode(SingleDogDocument.self, from: singleDogData)
|
||||
let dogFromData = dogResponse.body.primaryResource?.value
|
||||
let dogOwner: Person.Identifier? = dogFromData.flatMap { $0 ~> \.owner }
|
||||
|
||||
// MARKL - Parse a request or response body with one Dog in it using an alternative model
|
||||
typealias AltSingleDogDocument = JSONAPI.Document<SingleResourceBody<AlternativeDog>, NoMetadata, NoLinks, NoIncludes, NoAPIDescription, UnknownJSONAPIError>
|
||||
let altDogResponse = try! JSONDecoder().decode(AltSingleDogDocument.self, from: singleDogData)
|
||||
let altDogFromData = altDogResponse.body.primaryResource?.value
|
||||
let altDogHuman: Person.Identifier? = altDogFromData.flatMap { $0 ~> \.human }
|
||||
|
||||
// MARK: - Create a request or response with multiple people and dogs and houses included
|
||||
let personIds = [Person.Identifier(), Person.Identifier()]
|
||||
let dogs = try! [Dog(name: "Buddy", owner: personIds[0]), Dog(name: "Joy", owner: personIds[0]), Dog(name: "Travis", owner: personIds[1])]
|
||||
let houses = [House(attributes: .none, relationships: .none, meta: .none, links: .none), House(attributes: .none, relationships: .none, meta: .none, links: .none)]
|
||||
let people = try! [Person(id: personIds[0], name: ["Gary", "Doe"], favoriteColor: "Orange-Red", friends: [], dogs: [dogs[0], dogs[1]], home: houses[0]), Person(id: personIds[1], name: ["Elise", "Joy"], favoriteColor: "Red", friends: [], dogs: [dogs[2]], home: houses[1])]
|
||||
|
||||
typealias BatchPeopleDocument = JSONAPI.Document<ManyResourceBody<Person>, NoMetadata, NoLinks, Include2<Dog, House>, NoAPIDescription, UnknownJSONAPIError>
|
||||
|
||||
let includes = dogs.map { BatchPeopleDocument.Include($0) } + houses.map { BatchPeopleDocument.Include($0) }
|
||||
let batchPeopleDocument = BatchPeopleDocument(apiDescription: .none, body: .init(entities: people), includes: .init(values: includes), meta: .none, links: .none)
|
||||
let batchPeopleData = try! JSONEncoder().encode(batchPeopleDocument)
|
||||
|
||||
// MARK: - Parse a request or response body with multiple people in it and dogs and houses included
|
||||
|
||||
let peopleResponse = try! JSONDecoder().decode(BatchPeopleDocument.self, from: batchPeopleData)
|
||||
let peopleFromData = peopleResponse.body.primaryResource?.values
|
||||
let dogsFromData = peopleResponse.body.includes?[Dog.self]
|
||||
let housesFromData = peopleResponse.body.includes?[House.self]
|
||||
|
||||
print("-----")
|
||||
print(peopleResponse)
|
||||
print("-----")
|
||||
|
||||
// MARK: - Pass successfully parsed body to other parts of the code
|
||||
|
||||
if case let .data(bodyData) = peopleResponse.body {
|
||||
print("first person's name: \(bodyData.primary.values[0][\.fullName])")
|
||||
} else {
|
||||
print("no body data")
|
||||
}
|
||||
|
||||
// MARK: - Work in the abstract
|
||||
|
||||
func process<T: JSONAPIDocument>(document: T) {
|
||||
guard case let .data(body) = document.body else {
|
||||
return
|
||||
}
|
||||
let x: T.Body.Data = body
|
||||
}
|
||||
process(document: peopleResponse)
|
||||
@@ -23,8 +23,10 @@ extension String: CreatableRawIdType {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Entity typealias for convenience
|
||||
public typealias ExampleEntity<Description: EntityDescription> = Entity<Description, Id<String, Description>>
|
||||
// MARK: - typealiases for convenience
|
||||
public typealias ExampleEntity<Description: EntityDescription> = Entity<Description, NoMetadata, NoLinks, String>
|
||||
public typealias ToOne<E: Identifiable> = ToOneRelationship<E, NoMetadata, NoLinks>
|
||||
public typealias ToMany<E: Relatable> = ToManyRelationship<E, NoMetadata, NoLinks>
|
||||
|
||||
// MARK: - A few resource objects (entities)
|
||||
public enum PersonDescription: EntityDescription {
|
||||
@@ -34,20 +36,35 @@ public enum PersonDescription: EntityDescription {
|
||||
public struct Attributes: JSONAPI.Attributes {
|
||||
public let name: Attribute<[String]>
|
||||
public let favoriteColor: Attribute<String>
|
||||
|
||||
public var fullName: Attribute<String> {
|
||||
return name.map { $0.joined(separator: " ") }
|
||||
}
|
||||
|
||||
public init(name: Attribute<[String]>, favoriteColor: Attribute<String>) {
|
||||
self.name = name
|
||||
self.favoriteColor = favoriteColor
|
||||
}
|
||||
}
|
||||
|
||||
public struct Relationships: JSONAPI.Relationships {
|
||||
public let friends: ToManyRelationship<Person>
|
||||
public let dogs: ToManyRelationship<Dog>
|
||||
public let home: ToOneRelationship<House>
|
||||
public let friends: ToMany<Person>
|
||||
public let dogs: ToMany<Dog>
|
||||
public let home: ToOne<House>
|
||||
|
||||
public init(friends: ToMany<Person>, dogs: ToMany<Dog>, home: ToOne<House>) {
|
||||
self.friends = friends
|
||||
self.dogs = dogs
|
||||
self.home = home
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public typealias Person = ExampleEntity<PersonDescription>
|
||||
|
||||
public extension Entity where Description == PersonDescription, Identifier == Id<String, PersonDescription> {
|
||||
public init(id: Person.Identifier? = nil,name: [String], favoriteColor: String, friends: [Person], dogs: [Dog], home: House) throws {
|
||||
self = try Person(id: id ?? Person.Identifier(), attributes: .init(name: .init(rawValue: name), favoriteColor: .init(rawValue: favoriteColor)), relationships: .init(friends: .init(entities: friends), dogs: .init(entities: dogs), home: .init(entity: home)))
|
||||
public extension Entity where Description == PersonDescription, MetaType == NoMetadata, LinksType == NoLinks, EntityRawIdType == String {
|
||||
public init(id: Person.Id? = nil,name: [String], favoriteColor: String, friends: [Person], dogs: [Dog], home: House) throws {
|
||||
self = Person(id: id ?? Person.Id(), attributes: .init(name: .init(value: name), favoriteColor: .init(value: favoriteColor)), relationships: .init(friends: .init(entities: friends), dogs: .init(entities: dogs), home: .init(entity: home)), meta: .none, links: .none)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,22 +74,58 @@ public enum DogDescription: EntityDescription {
|
||||
|
||||
public struct Attributes: JSONAPI.Attributes {
|
||||
public let name: Attribute<String>
|
||||
|
||||
public init(name: Attribute<String>) {
|
||||
self.name = name
|
||||
}
|
||||
}
|
||||
|
||||
public struct Relationships: JSONAPI.Relationships {
|
||||
public let owner: ToOneRelationship<Person?>
|
||||
public let owner: ToOne<Person?>
|
||||
|
||||
public init(owner: ToOne<Person?>) {
|
||||
self.owner = owner
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public typealias Dog = ExampleEntity<DogDescription>
|
||||
|
||||
public extension Entity where Description == DogDescription, Identifier == Id<String, DogDescription> {
|
||||
public init(name: String, owner: Person?) throws {
|
||||
self = try Dog(attributes: .init(name: .init(rawValue: name)), relationships: DogDescription.Relationships(owner: .init(entity: owner)))
|
||||
public enum AlternativeDogDescription: EntityDescription {
|
||||
|
||||
public static var type: String { return "dogs" }
|
||||
|
||||
public struct Attributes: JSONAPI.Attributes {
|
||||
public let name: Attribute<String>
|
||||
|
||||
public init(name: Attribute<String>) {
|
||||
self.name = name
|
||||
}
|
||||
}
|
||||
|
||||
public init(name: String, owner: Person.Identifier) throws {
|
||||
self = try Dog(attributes: .init(name: .init(rawValue: name)), relationships: .init(owner: .init(id: owner)))
|
||||
public struct Relationships: JSONAPI.Relationships {
|
||||
public let human: ToOne<Person?>
|
||||
|
||||
public init(human: ToOne<Person?>) {
|
||||
self.human = human
|
||||
}
|
||||
|
||||
// define custom key mapping:
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case human = "owner"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public typealias AlternativeDog = ExampleEntity<AlternativeDogDescription>
|
||||
|
||||
public extension Entity where Description == DogDescription, MetaType == NoMetadata, LinksType == NoLinks, EntityRawIdType == String {
|
||||
public init(name: String, owner: Person?) throws {
|
||||
self = Dog(attributes: .init(name: .init(value: name)), relationships: DogDescription.Relationships(owner: .init(entity: owner)), meta: .none, links: .none)
|
||||
}
|
||||
|
||||
public init(name: String, owner: Person.Id) throws {
|
||||
self = Dog(attributes: .init(name: .init(value: name)), relationships: .init(owner: .init(id: owner)), meta: .none, links: .none)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,7 +134,7 @@ public enum HouseDescription: EntityDescription {
|
||||
public static var type: String { return "houses" }
|
||||
|
||||
public typealias Attributes = NoAttributes
|
||||
public typealias Relationships = NoRelatives
|
||||
public typealias Relationships = NoRelationships
|
||||
}
|
||||
|
||||
public typealias House = ExampleEntity<HouseDescription>
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<playground version='5.0' target-platform='macos' executeOnSourceChanges='false'>
|
||||
<timeline fileName='timeline.xctimeline'/>
|
||||
<playground version='6.0' target-platform='macos' executeOnSourceChanges='false'>
|
||||
<pages>
|
||||
<page name='Test Library'/>
|
||||
<page name='Usage'/>
|
||||
</pages>
|
||||
</playground>
|
||||
+1
-9
@@ -1,15 +1,7 @@
|
||||
{
|
||||
"object": {
|
||||
"pins": [
|
||||
{
|
||||
"package": "Result",
|
||||
"repositoryURL": "https://github.com/mattpolzin/Result",
|
||||
"state": {
|
||||
"branch": "master",
|
||||
"revision": "b98e238da6ea030fa7862ae6fd6500552370019c",
|
||||
"version": null
|
||||
}
|
||||
}
|
||||
|
||||
]
|
||||
},
|
||||
"version": 1
|
||||
|
||||
+8
-6
@@ -6,23 +6,25 @@ import PackageDescription
|
||||
let package = Package(
|
||||
name: "JSONAPI",
|
||||
products: [
|
||||
// Products define the executables and libraries produced by a package, and make them visible to other packages.
|
||||
.library(
|
||||
name: "JSONAPI",
|
||||
targets: ["JSONAPI"]),
|
||||
.library(
|
||||
name: "JSONAPITestLib",
|
||||
targets: ["JSONAPITestLib"])
|
||||
],
|
||||
dependencies: [
|
||||
.package(url: "https://github.com/mattpolzin/Result", .branch("master"))
|
||||
],
|
||||
targets: [
|
||||
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
|
||||
// Targets can depend on other targets in this package, and on products in packages which this package depends on.
|
||||
.target(
|
||||
name: "JSONAPI",
|
||||
dependencies: ["Result"]),
|
||||
dependencies: []),
|
||||
.target(
|
||||
name: "JSONAPITestLib",
|
||||
dependencies: ["JSONAPI"]),
|
||||
.testTarget(
|
||||
name: "JSONAPITests",
|
||||
dependencies: ["JSONAPI"]),
|
||||
dependencies: ["JSONAPITestLib"])
|
||||
],
|
||||
swiftLanguageVersions: [.v4_2]
|
||||
)
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
//
|
||||
// APIDescription.swift
|
||||
// JSONAPI
|
||||
//
|
||||
// Created by Mathew Polzin on 12/1/18.
|
||||
//
|
||||
|
||||
/// This is what the JSON API Spec calls the "JSON:API Object"
|
||||
public protocol APIDescriptionType: Codable, Equatable {
|
||||
associatedtype Meta
|
||||
}
|
||||
|
||||
/// This is what the JSON API Spec calls the "JSON:API Object"
|
||||
public struct APIDescription<Meta: JSONAPI.Meta>: APIDescriptionType {
|
||||
public let version: String
|
||||
public let meta: Meta
|
||||
}
|
||||
|
||||
/// Can be used as `APIDescriptionType` for Documents that do not
|
||||
/// have any API Description (a.k.a. "JSON:API Object").
|
||||
public struct NoAPIDescription: APIDescriptionType, CustomStringConvertible {
|
||||
public typealias Meta = NoMetadata
|
||||
|
||||
public init() {}
|
||||
|
||||
public static var none: NoAPIDescription { return .init() }
|
||||
|
||||
public var description: String { return "No JSON:API Object" }
|
||||
}
|
||||
|
||||
extension APIDescription {
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case version
|
||||
case meta
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
// The spec says that if a version is not specified, it should be assumed to be at least 1.0
|
||||
version = (try? container.decode(String.self, forKey: .version)) ?? "1.0"
|
||||
|
||||
if let metaVal = NoMetadata() as? Meta {
|
||||
meta = metaVal
|
||||
} else {
|
||||
meta = try container.decode(Meta.self, forKey: .meta)
|
||||
}
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
try container.encode(version, forKey: .version)
|
||||
|
||||
if Meta.self != NoMetadata.self {
|
||||
try container.encode(meta, forKey: .meta)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,19 @@
|
||||
// Created by Mathew Polzin on 11/5/18.
|
||||
//
|
||||
|
||||
public protocol JSONAPIDocument: Codable, Equatable {
|
||||
associatedtype PrimaryResourceBody: JSONAPI.ResourceBody
|
||||
associatedtype MetaType: JSONAPI.Meta
|
||||
associatedtype LinksType: JSONAPI.Links
|
||||
associatedtype IncludeType: JSONAPI.Include
|
||||
associatedtype APIDescription: APIDescriptionType
|
||||
associatedtype Error: JSONAPIError
|
||||
|
||||
typealias Body = Document<PrimaryResourceBody, MetaType, LinksType, IncludeType, APIDescription, Error>.Body
|
||||
|
||||
var body: Body { get }
|
||||
}
|
||||
|
||||
/// A JSON API Document represents the entire body
|
||||
/// of a JSON API request or the entire body of
|
||||
/// a JSON API response.
|
||||
@@ -12,15 +25,37 @@
|
||||
/// API uses snake case, you will want to use
|
||||
/// a conversion such as the one offerred by the
|
||||
/// Foundation JSONEncoder/Decoder: `KeyDecodingStrategy`
|
||||
public struct JSONAPIDocument<ResourceBody: JSONAPI.ResourceBody, MetaType: JSONAPI.Meta, LinksType: JSONAPI.Links, IncludeType: JSONAPI.Include, Error: JSONAPIError>: Equatable {
|
||||
public struct Document<PrimaryResourceBody: JSONAPI.ResourceBody, MetaType: JSONAPI.Meta, LinksType: JSONAPI.Links, IncludeType: JSONAPI.Include, APIDescription: APIDescriptionType, Error: JSONAPIError>: JSONAPIDocument {
|
||||
public typealias Include = IncludeType
|
||||
|
||||
/// The JSON API Spec calls this the JSON:API Object. It contains version
|
||||
/// and metadata information about the API itself.
|
||||
public let apiDescription: APIDescription
|
||||
|
||||
/// The Body of the Document. This body is either one or more errors
|
||||
/// with links and metadata attempted to parse but not guaranteed or
|
||||
/// it is a successful data struct containing all the primary and
|
||||
/// included resources, the metadata, and the links that this
|
||||
/// document type specifies.
|
||||
public let body: Body
|
||||
// public let jsonApi: APIDescription?
|
||||
|
||||
public enum Body: Equatable {
|
||||
case errors([Error], meta: MetaType?, links: LinksType?)
|
||||
case data(primary: ResourceBody, included: Includes<Include>, meta: MetaType, links: LinksType)
|
||||
case data(Data)
|
||||
|
||||
public struct Data: Equatable {
|
||||
public let primary: PrimaryResourceBody
|
||||
public let includes: Includes<Include>
|
||||
public let meta: MetaType
|
||||
public let links: LinksType
|
||||
|
||||
public init(primary: PrimaryResourceBody, includes: Includes<Include>, meta: MetaType, links: LinksType) {
|
||||
self.primary = primary
|
||||
self.includes = includes
|
||||
self.meta = meta
|
||||
self.links = links
|
||||
}
|
||||
}
|
||||
|
||||
public var isError: Bool {
|
||||
guard case .errors = self else { return false }
|
||||
@@ -31,21 +66,27 @@ public struct JSONAPIDocument<ResourceBody: JSONAPI.ResourceBody, MetaType: JSON
|
||||
guard case let .errors(errors, meta: _, links: _) = self else { return nil }
|
||||
return errors
|
||||
}
|
||||
|
||||
public var data: Data? {
|
||||
guard case let .data(data) = self else { return nil }
|
||||
return data
|
||||
}
|
||||
|
||||
public var primaryData: ResourceBody? {
|
||||
guard case let .data(primary: body, included: _, meta: _, links: _) = self else { return nil }
|
||||
return body
|
||||
public var primaryResource: PrimaryResourceBody? {
|
||||
guard case let .data(data) = self else { return nil }
|
||||
return data.primary
|
||||
}
|
||||
|
||||
public var includes: Includes<Include>? {
|
||||
guard case let .data(primary: _, included: includes, meta: _, links: _) = self else { return nil }
|
||||
return includes
|
||||
guard case let .data(data) = self else { return nil }
|
||||
return data.includes
|
||||
}
|
||||
|
||||
public var meta: MetaType? {
|
||||
switch self {
|
||||
case .data(primary: _, included: _, meta: let metadata, links: _),
|
||||
.errors(_, meta: let metadata?, links: _):
|
||||
case .data(let data):
|
||||
return data.meta
|
||||
case .errors(_, meta: let metadata?, links: _):
|
||||
return metadata
|
||||
default:
|
||||
return nil
|
||||
@@ -54,8 +95,9 @@ public struct JSONAPIDocument<ResourceBody: JSONAPI.ResourceBody, MetaType: JSON
|
||||
|
||||
public var links: LinksType? {
|
||||
switch self {
|
||||
case .data(primary: _, included: _, meta: _, links: let links),
|
||||
.errors(_, meta: _, links: let links?):
|
||||
case .data(let data):
|
||||
return data.links
|
||||
case .errors(_, meta: _, links: let links?):
|
||||
return links
|
||||
default:
|
||||
return nil
|
||||
@@ -63,58 +105,111 @@ public struct JSONAPIDocument<ResourceBody: JSONAPI.ResourceBody, MetaType: JSON
|
||||
}
|
||||
}
|
||||
|
||||
public init(errors: [Error], meta: MetaType? = nil, links: LinksType? = nil) {
|
||||
public init(apiDescription: APIDescription, errors: [Error], meta: MetaType? = nil, links: LinksType? = nil) {
|
||||
body = .errors(errors, meta: meta, links: links)
|
||||
self.apiDescription = apiDescription
|
||||
}
|
||||
|
||||
public init(body: ResourceBody, includes: Includes<Include>, meta: MetaType, links: LinksType) {
|
||||
self.body = .data(primary: body, included: includes, meta: meta, links: links)
|
||||
public init(apiDescription: APIDescription, body: PrimaryResourceBody, includes: Includes<Include>, meta: MetaType, links: LinksType) {
|
||||
self.body = .data(.init(primary: body, includes: includes, meta: meta, links: links))
|
||||
self.apiDescription = apiDescription
|
||||
}
|
||||
}
|
||||
/*
|
||||
extension Document where IncludeType == NoIncludes {
|
||||
public init(apiDescription: APIDescription, body: PrimaryResourceBody, meta: MetaType, links: LinksType) {
|
||||
self.init(apiDescription: apiDescription, body: body, includes: .none, meta: meta, links: links)
|
||||
}
|
||||
}
|
||||
|
||||
extension JSONAPIDocument where IncludeType == NoIncludes {
|
||||
public init(body: ResourceBody, meta: MetaType, links: LinksType) {
|
||||
self.body = .data(primary: body, included: .none, meta: meta, links: links)
|
||||
extension Document where MetaType == NoMetadata {
|
||||
public init(apiDescription: APIDescription, body: PrimaryResourceBody, includes: Includes<Include>, links: LinksType) {
|
||||
self.init(apiDescription: apiDescription, body: body, includes: includes, meta: .none, links: links)
|
||||
}
|
||||
}
|
||||
|
||||
extension JSONAPIDocument where MetaType == NoMetadata {
|
||||
public init(body: ResourceBody, includes: Includes<Include>, links: LinksType) {
|
||||
self.body = .data(primary: body, included: includes, meta: .none, links: links)
|
||||
extension Document where LinksType == NoLinks {
|
||||
public init(apiDescription: APIDescription, body: PrimaryResourceBody, includes: Includes<Include>, meta: MetaType) {
|
||||
self.init(apiDescription: apiDescription, body: body, includes: includes, meta: meta, links: .none)
|
||||
}
|
||||
}
|
||||
|
||||
extension JSONAPIDocument where LinksType == NoLinks {
|
||||
public init(body: ResourceBody, includes: Includes<Include>, meta: MetaType) {
|
||||
self.body = .data(primary: body, included: includes, meta: meta, links: .none)
|
||||
extension Document where APIDescription == NoAPIDescription {
|
||||
public init(body: PrimaryResourceBody, includes: Includes<Include>, meta: MetaType, links: LinksType) {
|
||||
self.init(apiDescription: .none, body: body, includes: includes, meta: meta, links: links)
|
||||
}
|
||||
}
|
||||
|
||||
extension JSONAPIDocument where IncludeType == NoIncludes, LinksType == NoLinks {
|
||||
public init(body: ResourceBody, meta: MetaType) {
|
||||
self.body = .data(primary: body, included: .none, meta: meta, links: .none)
|
||||
extension Document where IncludeType == NoIncludes, LinksType == NoLinks {
|
||||
public init(apiDescription: APIDescription, body: PrimaryResourceBody, meta: MetaType) {
|
||||
self.init(apiDescription: apiDescription, body: body, meta: meta, links: .none)
|
||||
}
|
||||
}
|
||||
|
||||
extension JSONAPIDocument where IncludeType == NoIncludes, MetaType == NoMetadata {
|
||||
public init(body: ResourceBody, links: LinksType) {
|
||||
self.body = .data(primary: body, included: .none, meta: .none, links: links)
|
||||
extension Document where IncludeType == NoIncludes, MetaType == NoMetadata {
|
||||
public init(apiDescription: APIDescription, body: PrimaryResourceBody, links: LinksType) {
|
||||
self.init(apiDescription: apiDescription, body: body, meta: .none, links: links)
|
||||
}
|
||||
}
|
||||
|
||||
extension JSONAPIDocument where MetaType == NoMetadata, LinksType == NoLinks {
|
||||
public init(body: ResourceBody, includes: Includes<Include>) {
|
||||
self.body = .data(primary: body, included: includes, meta: .none, links: .none)
|
||||
extension Document where IncludeType == NoIncludes, APIDescription == NoAPIDescription {
|
||||
public init(body: PrimaryResourceBody, meta: MetaType, links: LinksType) {
|
||||
self.init(apiDescription: .none, body: body, meta: meta, links: links)
|
||||
}
|
||||
}
|
||||
|
||||
extension JSONAPIDocument where IncludeType == NoIncludes, MetaType == NoMetadata, LinksType == NoLinks {
|
||||
public init(body: ResourceBody) {
|
||||
self.body = .data(primary: body, included: .none, meta: .none, links: .none)
|
||||
extension Document where MetaType == NoMetadata, LinksType == NoLinks {
|
||||
public init(apiDescription: APIDescription, body: PrimaryResourceBody, includes: Includes<Include>) {
|
||||
self.init(apiDescription: apiDescription, body: body, includes: includes, links: .none)
|
||||
}
|
||||
}
|
||||
|
||||
extension JSONAPIDocument: Codable {
|
||||
extension Document where MetaType == NoMetadata, APIDescription == NoAPIDescription {
|
||||
public init(body: PrimaryResourceBody, includes: Includes<Include>, links: LinksType) {
|
||||
self.init(apiDescription: .none, body: body, includes: includes, links: links)
|
||||
}
|
||||
}
|
||||
|
||||
extension Document where IncludeType == NoIncludes, MetaType == NoMetadata, LinksType == NoLinks {
|
||||
public init(apiDescription: APIDescription, body: PrimaryResourceBody) {
|
||||
self.init(apiDescription: apiDescription, body: body, includes: .none)
|
||||
}
|
||||
}
|
||||
|
||||
extension Document where MetaType == NoMetadata, LinksType == NoLinks, APIDescription == NoAPIDescription {
|
||||
public init(body: PrimaryResourceBody, includes: Includes<Include>) {
|
||||
self.init(apiDescription: .none, body: body, includes: includes)
|
||||
}
|
||||
}
|
||||
|
||||
extension Document where IncludeType == NoIncludes, MetaType == NoMetadata, LinksType == NoLinks, APIDescription == NoAPIDescription {
|
||||
public init(body: PrimaryResourceBody) {
|
||||
self.init(apiDescription: .none, body: body)
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
extension Document.Body.Data where PrimaryResourceBody: AppendableResourceBody {
|
||||
public func merging(_ other: Document.Body.Data,
|
||||
combiningMetaWith metaMerge: (MetaType, MetaType) -> MetaType,
|
||||
combiningLinksWith linksMerge: (LinksType, LinksType) -> LinksType) -> Document.Body.Data {
|
||||
return Document.Body.Data(primary: primary.appending(other.primary),
|
||||
includes: includes.appending(other.includes),
|
||||
meta: metaMerge(meta, other.meta),
|
||||
links: linksMerge(links, other.links))
|
||||
}
|
||||
}
|
||||
|
||||
extension Document.Body.Data where PrimaryResourceBody: AppendableResourceBody, MetaType == NoMetadata, LinksType == NoLinks {
|
||||
public func merging(_ other: Document.Body.Data) -> Document.Body.Data {
|
||||
return merging(other,
|
||||
combiningMetaWith: { _, _ in .none },
|
||||
combiningLinksWith: { _, _ in .none })
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Codable
|
||||
extension Document {
|
||||
private enum RootCodingKeys: String, CodingKey {
|
||||
case data
|
||||
case errors
|
||||
@@ -127,6 +222,12 @@ extension JSONAPIDocument: Codable {
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: RootCodingKeys.self)
|
||||
|
||||
if let noData = NoAPIDescription() as? APIDescription {
|
||||
apiDescription = noData
|
||||
} else {
|
||||
apiDescription = try container.decode(APIDescription.self, forKey: .jsonapi)
|
||||
}
|
||||
|
||||
let errors = try container.decodeIfPresent([Error].self, forKey: .errors)
|
||||
|
||||
let meta: MetaType?
|
||||
@@ -157,11 +258,11 @@ extension JSONAPIDocument: Codable {
|
||||
return
|
||||
}
|
||||
|
||||
let data: ResourceBody
|
||||
if let noData = NoResourceBody() as? ResourceBody {
|
||||
let data: PrimaryResourceBody
|
||||
if let noData = NoResourceBody() as? PrimaryResourceBody {
|
||||
data = noData
|
||||
} else {
|
||||
data = try container.decode(ResourceBody.self, forKey: .data)
|
||||
data = try container.decode(PrimaryResourceBody.self, forKey: .data)
|
||||
}
|
||||
|
||||
let maybeIncludes = try container.decodeIfPresent(Includes<Include>.self, forKey: .included)
|
||||
@@ -175,7 +276,7 @@ extension JSONAPIDocument: Codable {
|
||||
throw JSONAPIEncodingError.missingOrMalformedLinks
|
||||
}
|
||||
|
||||
body = .data(primary: data, included: maybeIncludes ?? Includes<Include>.none, meta: metaVal, links: linksVal)
|
||||
body = .data(.init(primary: data, includes: maybeIncludes ?? Includes<Include>.none, meta: metaVal, links: linksVal))
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
@@ -199,28 +300,49 @@ extension JSONAPIDocument: Codable {
|
||||
try container.encode(linksVal, forKey: .links)
|
||||
}
|
||||
|
||||
case .data(primary: let resourceBody, included: let includes, let meta, links: let links):
|
||||
try container.encode(resourceBody, forKey: .data)
|
||||
case .data(let data):
|
||||
try container.encode(data.primary, forKey: .data)
|
||||
|
||||
if Include.self != NoIncludes.self {
|
||||
try container.encode(includes, forKey: .included)
|
||||
try container.encode(data.includes, forKey: .included)
|
||||
}
|
||||
|
||||
if MetaType.self != NoMetadata.self {
|
||||
try container.encode(meta, forKey: .meta)
|
||||
try container.encode(data.meta, forKey: .meta)
|
||||
}
|
||||
|
||||
if LinksType.self != NoLinks.self {
|
||||
try container.encode(links, forKey: .links)
|
||||
try container.encode(data.links, forKey: .links)
|
||||
}
|
||||
}
|
||||
|
||||
if APIDescription.self != NoAPIDescription.self {
|
||||
try container.encode(apiDescription, forKey: .jsonapi)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - CustomStringConvertible
|
||||
|
||||
extension JSONAPIDocument: CustomStringConvertible {
|
||||
extension Document: CustomStringConvertible {
|
||||
public var description: String {
|
||||
return "JSONAPIDocument(body: \(String(describing: body))"
|
||||
return "Document(\(String(describing: body)))"
|
||||
}
|
||||
}
|
||||
|
||||
extension Document.Body: CustomStringConvertible {
|
||||
public var description: String {
|
||||
switch self {
|
||||
case .errors(let errors, meta: let meta, links: let links):
|
||||
return "errors: \(String(describing: errors)), meta: \(String(describing: meta)), links: \(String(describing: links))"
|
||||
case .data(let data):
|
||||
return String(describing: data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Document.Body.Data: CustomStringConvertible {
|
||||
public var description: String {
|
||||
return "primary: \(String(describing: primary)), includes: \(String(describing: includes)), meta: \(String(describing: meta)), links: \(String(describing: links))"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ public protocol JSONAPIError: Swift.Error, Equatable, Codable {
|
||||
static var unknown: Self { get }
|
||||
}
|
||||
|
||||
public enum BasicJSONAPIError: JSONAPIError {
|
||||
public enum UnknownJSONAPIError: JSONAPIError {
|
||||
case unknownError
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
@@ -21,7 +21,7 @@ public enum BasicJSONAPIError: JSONAPIError {
|
||||
try container.encode("unknown")
|
||||
}
|
||||
|
||||
public static var unknown: BasicJSONAPIError {
|
||||
public static var unknown: UnknownJSONAPIError {
|
||||
return .unknownError
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,6 +50,16 @@ public struct Includes<I: Include>: Codable, Equatable {
|
||||
}
|
||||
}
|
||||
|
||||
extension Includes {
|
||||
public func appending(_ other: Includes<I>) -> Includes {
|
||||
return Includes(values: values + other.values)
|
||||
}
|
||||
}
|
||||
|
||||
public func +<I: Include>(_ left: Includes<I>, _ right: Includes<I>) -> Includes<I> {
|
||||
return left.appending(right)
|
||||
}
|
||||
|
||||
extension Includes: CustomStringConvertible {
|
||||
public var description: String {
|
||||
return "Includes(\(String(describing: values))"
|
||||
@@ -115,4 +125,25 @@ extension Includes where I: _Poly6 {
|
||||
}
|
||||
|
||||
// MARK: - 7 includes
|
||||
public typealias Include7 = Poly7
|
||||
extension Includes where I: _Poly7 {
|
||||
public subscript(_ lookup: I.G.Type) -> [I.G] {
|
||||
return values.compactMap { $0.g }
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 8 includes
|
||||
public typealias Include8 = Poly8
|
||||
extension Includes where I: _Poly8 {
|
||||
public subscript(_ lookup: I.H.Type) -> [I.H] {
|
||||
return values.compactMap { $0.h }
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 9 includes
|
||||
public typealias Include9 = Poly9
|
||||
extension Includes where I: _Poly9 {
|
||||
public subscript(_ lookup: I.I.Type) -> [I.I] {
|
||||
return values.compactMap { $0.i }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,29 +1,50 @@
|
||||
//
|
||||
// ResourceBody.swift
|
||||
// PrimaryResourceBody.swift
|
||||
// JSONAPI
|
||||
//
|
||||
// Created by Mathew Polzin on 11/10/18.
|
||||
//
|
||||
|
||||
public protocol PrimaryResource: Equatable, Codable {}
|
||||
public protocol MaybePrimaryResource: Equatable, Codable {}
|
||||
|
||||
/// A PrimaryResource is a type that can be used in the body of a JSON API
|
||||
/// document as the primary resource.
|
||||
public protocol PrimaryResource: MaybePrimaryResource {}
|
||||
|
||||
extension Optional: MaybePrimaryResource where Wrapped: PrimaryResource {}
|
||||
|
||||
/// A ResourceBody is a representation of the body of the JSON API Document.
|
||||
/// It can either be one resource (which can be specified as optional or not)
|
||||
/// or it can contain many resources (and array with zero or more entries).
|
||||
public protocol ResourceBody: Codable, Equatable {
|
||||
}
|
||||
|
||||
public struct SingleResourceBody<Entity: JSONAPI.PrimaryResource>: ResourceBody {
|
||||
public let value: Entity?
|
||||
public protocol AppendableResourceBody: ResourceBody {
|
||||
func appending(_ other: Self) -> Self
|
||||
}
|
||||
|
||||
public init(entity: Entity?) {
|
||||
public func +<R: AppendableResourceBody>(_ left: R, right: R) -> R {
|
||||
return left.appending(right)
|
||||
}
|
||||
|
||||
public struct SingleResourceBody<Entity: JSONAPI.MaybePrimaryResource>: ResourceBody {
|
||||
public let value: Entity
|
||||
|
||||
public init(entity: Entity) {
|
||||
self.value = entity
|
||||
}
|
||||
}
|
||||
|
||||
public struct ManyResourceBody<Entity: JSONAPI.EntityType>: ResourceBody {
|
||||
public struct ManyResourceBody<Entity: JSONAPI.PrimaryResource>: AppendableResourceBody {
|
||||
public let values: [Entity]
|
||||
|
||||
public init(entities: [Entity]) {
|
||||
values = entities
|
||||
}
|
||||
|
||||
public func appending(_ other: ManyResourceBody) -> ManyResourceBody {
|
||||
return ManyResourceBody(entities: values + other.values)
|
||||
}
|
||||
}
|
||||
|
||||
/// Use NoResourceBody to indicate you expect a JSON API document to not
|
||||
@@ -32,13 +53,15 @@ public struct NoResourceBody: ResourceBody {
|
||||
public static var none: NoResourceBody { return NoResourceBody() }
|
||||
}
|
||||
|
||||
// MARK: Decodable
|
||||
// MARK: Codable
|
||||
extension SingleResourceBody {
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.singleValueContainer()
|
||||
|
||||
if container.decodeNil() {
|
||||
value = nil
|
||||
let anyNil: Any? = nil
|
||||
if container.decodeNil(),
|
||||
let val = anyNil as? Entity {
|
||||
value = val
|
||||
return
|
||||
}
|
||||
|
||||
@@ -48,7 +71,7 @@ extension SingleResourceBody {
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.singleValueContainer()
|
||||
|
||||
if value == nil {
|
||||
if (value as Any?) == nil {
|
||||
try container.encodeNil()
|
||||
return
|
||||
}
|
||||
@@ -80,12 +103,12 @@ extension ManyResourceBody {
|
||||
|
||||
extension SingleResourceBody: CustomStringConvertible {
|
||||
public var description: String {
|
||||
return "ResourceBody(\(String(describing: value)))"
|
||||
return "PrimaryResourceBody(\(String(describing: value)))"
|
||||
}
|
||||
}
|
||||
|
||||
extension ManyResourceBody: CustomStringConvertible {
|
||||
public var description: String {
|
||||
return "ResourceBody(\(String(describing: values)))"
|
||||
return "PrimaryResourceBody(\(String(describing: values)))"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
//
|
||||
// EncodingError.swift
|
||||
// JSONAPI
|
||||
//
|
||||
// Created by Mathew Polzin on 12/7/18.
|
||||
//
|
||||
|
||||
public enum JSONAPIEncodingError: Swift.Error {
|
||||
case typeMismatch(expected: String, found: String)
|
||||
case illegalEncoding(String)
|
||||
case illegalDecoding(String)
|
||||
case missingOrMalformedMetadata
|
||||
case missingOrMalformedLinks
|
||||
}
|
||||
@@ -9,16 +9,29 @@
|
||||
public protocol Links: Codable, Equatable {}
|
||||
|
||||
/// Use NoLinks where no links should belong to a JSON API component
|
||||
public struct NoLinks: Links {
|
||||
public struct NoLinks: Links, CustomStringConvertible {
|
||||
public static var none: NoLinks { return NoLinks() }
|
||||
public init() {}
|
||||
|
||||
public var description: String { return "No Links" }
|
||||
}
|
||||
|
||||
public protocol URL: Codable, Equatable {}
|
||||
public protocol JSONAPIURL: Codable, Equatable {}
|
||||
|
||||
public struct Link<URL: JSONAPI.URL, Meta: JSONAPI.Meta>: Equatable, Codable {
|
||||
public struct Link<URL: JSONAPI.JSONAPIURL, Meta: JSONAPI.Meta>: Equatable, Codable {
|
||||
public let url: URL
|
||||
public let meta: Meta
|
||||
|
||||
public init(url: URL, meta: Meta) {
|
||||
self.url = url
|
||||
self.meta = meta
|
||||
}
|
||||
}
|
||||
|
||||
extension Link where Meta == NoMetadata {
|
||||
public init(url: URL) {
|
||||
self.init(url: url, meta: .none)
|
||||
}
|
||||
}
|
||||
|
||||
public extension Link {
|
||||
|
||||
@@ -19,8 +19,10 @@ public protocol Meta: Codable, Equatable {
|
||||
// nullable.
|
||||
extension Optional: Meta where Wrapped: Meta {}
|
||||
|
||||
public struct NoMetadata: Meta {
|
||||
public struct NoMetadata: Meta, CustomStringConvertible {
|
||||
public static var none: NoMetadata { return NoMetadata() }
|
||||
|
||||
public init() { }
|
||||
|
||||
public var description: String { return "No Metadata" }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
//
|
||||
// Attribute+Functor.swift
|
||||
// JSONAPI
|
||||
//
|
||||
// Created by Mathew Polzin on 11/28/18.
|
||||
//
|
||||
|
||||
public extension TransformedAttribute {
|
||||
/// Map an Attribute to a new wrapped type.
|
||||
/// Note that the resulting Attribute will have no transformer, even if the
|
||||
/// source Attribute has a transformer.
|
||||
/// You are mapping the output of the source transform into
|
||||
/// the RawValue of a new transformerless Attribute.
|
||||
///
|
||||
/// Generally, this is the most useful operation. The transformer gives you
|
||||
/// control over the decoding of the Attribute, but once the Attribute exists,
|
||||
/// mapping on it is most useful for creating computed Attribute properties.
|
||||
public func map<T: Codable>(_ transform: (Transformer.To) throws -> T) rethrows -> Attribute<T> {
|
||||
return Attribute<T>(value: try transform(value))
|
||||
}
|
||||
}
|
||||
|
||||
public extension Attribute {
|
||||
/// Map an Attribute to a new wrapped type.
|
||||
/// Note that the resulting Attribute will have no transformer, even if the
|
||||
/// source Attribute has a transformer.
|
||||
/// You are mapping the output of the source transform into
|
||||
/// the RawValue of a new transformerless Attribute.
|
||||
///
|
||||
/// Generally, this is the most useful operation. The transformer gives you
|
||||
/// control over the decoding of the Attribute, but once the Attribute exists,
|
||||
/// mapping on it is most useful for creating computed Attribute properties.
|
||||
public func map<T: Codable>(_ transform: (ValueType) throws -> T) rethrows -> Attribute<T> {
|
||||
return Attribute<T>(value: try transform(value))
|
||||
}
|
||||
}
|
||||
@@ -5,8 +5,18 @@
|
||||
// Created by Mathew Polzin on 11/13/18.
|
||||
//
|
||||
|
||||
public struct TransformedAttribute<RawValue: Codable, Transformer: JSONAPI.Transformer>: Codable where Transformer.From == RawValue {
|
||||
private let rawValue: RawValue
|
||||
public protocol AttributeType: Codable {
|
||||
associatedtype RawValue: Codable
|
||||
associatedtype ValueType
|
||||
|
||||
var value: ValueType { get }
|
||||
}
|
||||
|
||||
// MARK: TransformedAttribute
|
||||
|
||||
/// A TransformedAttribute takes a Codable type and attempts to turn it into another type.
|
||||
public struct TransformedAttribute<RawValue: Codable, Transformer: JSONAPI.Transformer>: AttributeType where Transformer.From == RawValue {
|
||||
let rawValue: RawValue
|
||||
|
||||
public let value: Transformer.To
|
||||
|
||||
@@ -16,6 +26,25 @@ public struct TransformedAttribute<RawValue: Codable, Transformer: JSONAPI.Trans
|
||||
}
|
||||
}
|
||||
|
||||
extension TransformedAttribute where Transformer == IdentityTransformer<RawValue> {
|
||||
// If we are using the identity transform, we can skip the transform and guarantee no
|
||||
// error is thrown.
|
||||
public init(value: RawValue) {
|
||||
rawValue = value
|
||||
self.value = value
|
||||
}
|
||||
}
|
||||
|
||||
extension TransformedAttribute where Transformer: ReversibleTransformer {
|
||||
/// Initialize a TransformedAttribute from its transformed value. The
|
||||
/// RawValue, which is what gets encoded/decoded, is determined using
|
||||
/// The Transformer's reverse function.
|
||||
public init(transformedValue: Transformer.To) throws {
|
||||
self.value = transformedValue
|
||||
rawValue = try Transformer.reverse(value)
|
||||
}
|
||||
}
|
||||
|
||||
extension TransformedAttribute: CustomStringConvertible {
|
||||
public var description: String {
|
||||
return "Attribute<\(String(describing: Transformer.From.self)) -> \(String(describing: Transformer.To.self))>(\(String(describing: value)))"
|
||||
@@ -24,9 +53,34 @@ extension TransformedAttribute: CustomStringConvertible {
|
||||
|
||||
extension TransformedAttribute: Equatable where Transformer.From: Equatable, Transformer.To: Equatable {}
|
||||
|
||||
// MARK: ValidatedAttribute
|
||||
|
||||
/// A ValidatedAttribute does not transform its raw value, but it throws
|
||||
/// an error if the raw value does not match expectations.
|
||||
public typealias ValidatedAttribute<RawValue: Codable, Validator: JSONAPI.Validator> = TransformedAttribute<RawValue, Validator> where RawValue == Validator.From
|
||||
|
||||
public typealias Attribute<T: Codable> = TransformedAttribute<T, IdentityTransformer<T>>
|
||||
// MARK: Attribute
|
||||
|
||||
/// An Attribute simply represents a type that can be encoded and decoded.
|
||||
public struct Attribute<RawValue: Codable>: AttributeType {
|
||||
let attribute: TransformedAttribute<RawValue, IdentityTransformer<RawValue>>
|
||||
|
||||
public var value: RawValue {
|
||||
return attribute.value
|
||||
}
|
||||
|
||||
public init(value: RawValue) {
|
||||
attribute = .init(value: value)
|
||||
}
|
||||
}
|
||||
|
||||
extension Attribute: CustomStringConvertible {
|
||||
public var description: String {
|
||||
return "Attribute<\(String(describing: RawValue.self))>(\(String(describing: value)))"
|
||||
}
|
||||
}
|
||||
|
||||
extension Attribute: Equatable where RawValue: Equatable {}
|
||||
|
||||
// MARK: - Codable
|
||||
extension TransformedAttribute {
|
||||
@@ -54,15 +108,40 @@ extension TransformedAttribute {
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.singleValueContainer()
|
||||
|
||||
// See note in decode above about the weirdness
|
||||
// going on here.
|
||||
let anyNil: Any? = nil
|
||||
if let _ = anyNil as? Transformer.From,
|
||||
(rawValue as Any?) == nil {
|
||||
try container.encodeNil()
|
||||
}
|
||||
|
||||
|
||||
try container.encode(rawValue)
|
||||
}
|
||||
}
|
||||
|
||||
extension Attribute {
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.singleValueContainer()
|
||||
|
||||
// A little trickery follows. If the value is nil, the
|
||||
// container.decode(Value.self) will fail even if Value
|
||||
// is Optional. However, we can check if decoding nil
|
||||
// succeeds and then attempt to coerce nil to a Value
|
||||
// type at which point we can store nil in `value`.
|
||||
let anyNil: Any? = nil
|
||||
if container.decodeNil(),
|
||||
let val = anyNil as? RawValue {
|
||||
attribute = .init(value: val)
|
||||
} else {
|
||||
attribute = try container.decode(TransformedAttribute<RawValue, IdentityTransformer<RawValue>>.self)
|
||||
}
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.singleValueContainer()
|
||||
|
||||
try container.encode(attribute)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Attribute decoding and encoding defaults
|
||||
|
||||
extension AttributeType {
|
||||
public static func defaultDecoding<Container: KeyedDecodingContainerProtocol>(from container: Container, forKey key: Container.Key) throws -> Self {
|
||||
return try container.decode(Self.self, forKey: key)
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -5,10 +5,16 @@
|
||||
// Created by Mathew Polzin on 7/24/18.
|
||||
//
|
||||
|
||||
/// All types that are RawIdType and additionally
|
||||
/// Unidentified conform to this protocol. You
|
||||
/// should not add conformance to this protocol
|
||||
/// directly.
|
||||
public protocol MaybeRawId: Codable, Equatable {}
|
||||
|
||||
/// Any type that you would like to be encoded to and
|
||||
/// decoded from JSON API ids should conform to this
|
||||
/// protocol. Conformance for `String` is given.
|
||||
public protocol RawIdType: Codable, Equatable {}
|
||||
public protocol RawIdType: MaybeRawId, Hashable {}
|
||||
|
||||
/// If you would like to be able to create new
|
||||
/// Entities with Ids backed by a RawIdType then
|
||||
@@ -22,19 +28,37 @@ public protocol CreatableRawIdType: RawIdType {
|
||||
|
||||
extension String: RawIdType {}
|
||||
|
||||
public protocol Identifier: Codable, Equatable {}
|
||||
|
||||
public struct Unidentified: Identifier, CustomStringConvertible {
|
||||
public struct Unidentified: MaybeRawId, CustomStringConvertible {
|
||||
public init() {}
|
||||
|
||||
public var description: String { return "Id(Unidentified)" }
|
||||
public var description: String { return "Unidentified" }
|
||||
}
|
||||
|
||||
public protocol IdType: Identifier, CustomStringConvertible {
|
||||
associatedtype EntityDescription: JSONAPI.EntityDescription
|
||||
associatedtype RawType: RawIdType
|
||||
|
||||
public protocol OptionalId: Codable {
|
||||
associatedtype IdentifiableType: JSONAPI.JSONTyped
|
||||
associatedtype RawType: MaybeRawId
|
||||
|
||||
var rawValue: RawType { get }
|
||||
init(rawValue: RawType)
|
||||
}
|
||||
|
||||
public protocol IdType: OptionalId, CustomStringConvertible, Hashable where RawType: RawIdType {}
|
||||
|
||||
extension Optional: MaybeRawId where Wrapped: Codable & Equatable {}
|
||||
extension Optional: OptionalId where Wrapped: IdType {
|
||||
public typealias IdentifiableType = Wrapped.IdentifiableType
|
||||
public typealias RawType = Wrapped.RawType?
|
||||
|
||||
public var rawValue: Wrapped.RawType? {
|
||||
guard case .some(let value) = self else {
|
||||
return nil
|
||||
}
|
||||
return value.rawValue
|
||||
}
|
||||
|
||||
public init(rawValue: Wrapped.RawType?) {
|
||||
self = rawValue.map { Wrapped(rawValue: $0) }
|
||||
}
|
||||
}
|
||||
|
||||
public extension IdType {
|
||||
@@ -47,27 +71,38 @@ public protocol CreatableIdType: IdType {
|
||||
|
||||
/// An Entity ID. These IDs can be encoded to or decoded from
|
||||
/// JSON API IDs.
|
||||
public struct Id<RawType: RawIdType, EntityDescription: JSONAPI.EntityDescription>: IdType {
|
||||
public struct Id<RawType: MaybeRawId, IdentifiableType: JSONAPI.JSONTyped>: Equatable, OptionalId {
|
||||
|
||||
public let rawValue: RawType
|
||||
|
||||
public init(rawValue: RawType) {
|
||||
self.rawValue = rawValue
|
||||
}
|
||||
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.singleValueContainer()
|
||||
rawValue = try container.decode(RawType.self)
|
||||
let rawValue = try container.decode(RawType.self)
|
||||
self.init(rawValue: rawValue)
|
||||
}
|
||||
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.singleValueContainer()
|
||||
try container.encode(rawValue)
|
||||
}
|
||||
}
|
||||
|
||||
extension Id: Hashable, CustomStringConvertible, IdType where RawType: RawIdType {
|
||||
public static func id(from rawValue: RawType) -> Id<RawType, IdentifiableType> {
|
||||
return Id(rawValue: rawValue)
|
||||
}
|
||||
}
|
||||
|
||||
extension Id: CreatableIdType where RawType: CreatableRawIdType {
|
||||
public init() {
|
||||
rawValue = .unique()
|
||||
}
|
||||
}
|
||||
|
||||
extension Id where RawType == Unidentified {
|
||||
public static var unidentified: Id { return .init(rawValue: Unidentified()) }
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user