Compare commits

...

121 Commits

Author SHA1 Message Date
Mathew Polzin d5a24c4adb Split Attribute out into its own Type (no longer just a type alias to TransformedAttribute) 2018-12-29 23:07:14 -08:00
Mathew Polzin 669d5d1342 Add ability to merge Document.Body.Data values 2018-12-28 19:11:24 -08:00
Mathew Polzin 923ab7d9f4 Add support for Poly8/Poly9 and Include8/Include9 2018-12-28 11:05:18 -08:00
Mathew Polzin 72769b6107 Fix small problem with sample code in README 2018-12-27 18:22:31 -08:00
Mathew Polzin 109e15d741 Add convenience method for default decoding of attributes. add tests for custom decoding and encoding as well as custom coding keys. add documentation. 2018-12-27 18:18:34 -08:00
Mathew Polzin 72180f64ef clarify some things about computed attribute properties. 2018-12-27 16:27:10 -08:00
Mathew Polzin fc962f9a0d Lift the constraint that Attributes and Relationships are Codable for EntityProxies. 2018-12-24 07:05:35 -08:00
Mathew Polzin 52eb123166 update documentation 2018-12-22 13:57:38 -08:00
Mathew Polzin 4ef147ec45 Update linuxmain 2018-12-22 13:49:10 -08:00
Mathew Polzin 61074ecc69 Add methods that make it easy to copy an entity with a new ID or copy an unidentified entity and give it an ID 2018-12-22 13:41:34 -08:00
Mathew Polzin 4dbcff6023 Add another way to initialize a nullable attribute to the tests 2018-12-22 13:10:05 -08:00
Mathew Polzin d6e82fab55 get id and type tests into the encoded entity test function 2018-12-21 08:53:55 -08:00
Mathew Polzin 5fa9848f02 test some attribute encoding a bit further 2018-12-21 08:45:51 -08:00
Mathew Polzin f38399a1d6 Add a bit of entity structure testing 2018-12-21 07:50:32 -08:00
Mathew Polzin 28f664326d Added a couple of tests 2018-12-12 21:05:07 -08:00
Mathew Polzin 3b7ef4aeb9 Add access to easy optional access to entire data on Document.Body 2018-12-12 20:05:29 -08:00
Mathew Polzin bb1ed30e89 Rename Document.Body.primaryData to primaryResource 2018-12-12 20:04:14 -08:00
Mathew Polzin dfdc266645 remove a bunch of convenience initializers that appeared to be giving the compiler some strife 2018-12-10 22:45:28 -08:00
Mathew Polzin 585cad0a83 Added literal initialization for nullable Attributes (TestLib) 2018-12-10 21:50:30 -08:00
Mathew Polzin 89abdd4cca fix example code in Playground 2018-12-10 21:11:48 -08:00
Mathew Polzin 67a43be26c update linuxmain 2018-12-08 20:05:00 -08:00
Mathew Polzin 8f279ce191 Add tests around nullable attributes and remove unnecessary encoding code 2018-12-08 20:04:11 -08:00
Mathew Polzin c9d388579f Made it much more convenient to work with Non-EntityType relationships. Discovered and fixed a bug where nullable relationships were encoded incorrectly. 2018-12-08 19:48:10 -08:00
Mathew Polzin 1061283905 loosen the grip entity had on Ids 2018-12-08 01:17:47 -08:00
Mathew Polzin 08949d0a93 Move encoding error type to its own file. Restructure Relatable and OptionalRelatable to not be dependent upon EntityDescription 2018-12-08 00:36:05 -08:00
Mathew Polzin 3047e2d23a Add access to computed attributes that are not AttributeType 2018-12-07 21:19:08 -08:00
Mathew Polzin 41a2a01788 Added support for relationship operator ~> to optional relationships 2018-12-07 20:59:39 -08:00
Mathew Polzin 53f7f55e07 Add Poly7 and Include7 because why not 2018-12-06 18:38:20 -08:00
Mathew Polzin d8d030286d Add a little bit of code doc 2018-12-06 18:12:56 -08:00
Mathew Polzin 005a981bf7 Add a couple of missing initializers to Entity 2018-12-05 22:51:12 -08:00
Mathew Polzin fad45203dd small refactor to consider ToOneRelationship with no meta or links to be even more synonymous with 'pointer' 2018-12-05 22:42:34 -08:00
Mathew Polzin 3468cb555a Remove Result dependency in favor of a super stripped down internally scoped Result enum. This won't interfere with other Result libraries and I was not exposing the Result anyway. 2018-12-05 22:02:59 -08:00
Mathew Polzin 8defe82536 Update README and linuxmain 2018-12-05 18:33:07 -08:00
Mathew Polzin 1f83216b03 Adding a few tests here and there 2018-12-05 18:31:53 -08:00
Mathew Polzin 6e6973c170 Adding test coverage for Relationship Meta and Links 2018-12-05 18:12:00 -08:00
Mathew Polzin 4f52cc2fd1 update README 2018-12-05 17:30:48 -08:00
Mathew Polzin 35d6cbb440 Add support for Relationship Meta and Links (untested) 2018-12-05 09:14:38 -08:00
Mathew Polzin d667e91a6a Add some missing initializers for Entity and fix Playground sources. 2018-12-03 19:08:09 -08:00
Mathew Polzin 75c8ce7405 Update linuxmain 2018-12-03 18:46:03 -08:00
Mathew Polzin 3a8237ec0f Add test coverage for Meta and Links on Entity 2018-12-03 18:45:07 -08:00
Mathew Polzin 49e33baa96 give all top-level-ish headers the JSONAPI prefix 2018-12-03 18:14:05 -08:00
Mathew Polzin f2a9b5a7b9 That should do the trick 2018-12-03 18:10:36 -08:00
Mathew Polzin 24baadd7a8 try to make headers unique 2018-12-03 18:09:40 -08:00
Mathew Polzin 8d18be6154 Update README after adding Meta and Links to Entity. 2018-12-03 18:04:53 -08:00
Mathew Polzin 8aa887b527 Add metadata and links to resource objects (i.e. Entity). untested. 2018-12-02 22:57:59 -08:00
Mathew Polzin 7d0a686dd9 update linuxmain 2018-12-02 21:39:45 -08:00
Mathew Polzin e4eb7816d7 Added an APIDescription type to Document that supports the JSON API Spec's JSON:API Object. 2018-12-02 21:35:09 -08:00
Mathew Polzin 482174f5b4 update linuxmain 2018-12-02 01:44:02 -08:00
Mathew Polzin 339480264e Add JSON:API Object to Document as the APIDescription type. Begin to add testing around this new type. 2018-12-02 01:43:27 -08:00
Mathew Polzin 5d666ec998 move entity validator (i.e. check function) section of project status into a TestLib subsection. 2018-12-01 09:58:07 -08:00
Mathew Polzin 9cacbe9b8b Revamp project status section and remove old todo list items. 2018-12-01 09:55:31 -08:00
Mathew Polzin 6663ad042e A cleanup and clarification pass at the README 2018-12-01 01:13:05 -08:00
Mathew Polzin 07402259c4 Add ability to specify that a SingleResourceBody should be optional or not (or specifically that its PrimaryResource is nullable or not). add tests. update documentation. 2018-12-01 00:36:39 -08:00
Mathew Polzin 8274ecb523 Add tests confirming that initializers work as expected without using any TestLib conveniences 2018-11-30 23:56:18 -08:00
Mathew Polzin 853c7e892a Fix playground bug caused by not running after removing a typealias 2018-11-30 20:18:08 -08:00
Mathew Polzin 04bf96a0cf update linuxmain 2018-11-29 20:13:21 -08:00
Mathew Polzin 8668311307 better poly proxy tests 2018-11-29 20:10:56 -08:00
Mathew Polzin 689517c633 Add ability to differentiate between things that _act_ like Entities and things that _are_ entities in the sense that the latter are persisted between client and server but the former is just a clientside convenience. 2018-11-29 20:03:44 -08:00
Mathew Polzin 65b80ee0eb Add ReversibleTransformer and tests. remove unneeded check in Entity.check(). 2018-11-29 18:39:59 -08:00
Mathew Polzin e91a03b396 Expose initializer for Document.Body.Data 2018-11-29 13:37:06 -08:00
Mathew Polzin 82088c7852 Remove BodyData which just aliased Body.Data 2018-11-29 13:15:07 -08:00
Mathew Polzin 34a4c8e7fc Add TestLib ability to represent to-one relationships with the values of their IDs. 2018-11-29 11:31:17 -08:00
Mathew Polzin 6aeb859c24 Remove now deleted test from linuxmain 2018-11-28 21:13:47 -08:00
Mathew Polzin 163ac94c51 I did some more type wrangling to finally get the Id type to specialize on Entity rather than EntityDescription. The compiler gets into trouble depending on which of a few semantically identical routes are taken, but I finally stumbled upon the correct combination of protocols and extensions to get the job done. this was always the ideal outcome, but I was not sure the Swift compiler would allow it. 2018-11-28 21:13:07 -08:00
Mathew Polzin e67b9fc142 Update playground and README for computed attributes. 2018-11-28 09:20:50 -08:00
Mathew Polzin a628992fcb Make Attribute a Functor to make computed attributes easier to write. 2018-11-28 09:09:23 -08:00
Mathew Polzin cf47f88a61 Add tests to confirm that Entities can safely have computed attributes or relationships. 2018-11-28 08:15:57 -08:00
Mathew Polzin 0425e2adcb rename folder in tests 2018-11-28 07:50:47 -08:00
Mathew Polzin d3763ba713 Add array literal expressibility for ToManyRelationship 2018-11-27 23:59:12 -08:00
Mathew Polzin fcc1796731 Expose Entity properties on EntityType protocol. 2018-11-27 18:58:48 -08:00
Mathew Polzin 921bcef05d Ids are now Hashable 2018-11-27 18:39:57 -08:00
Mathew Polzin 661ff6eca5 A little renaming and easier access to important types under the JSONAPIDocument protocol 2018-11-27 18:20:01 -08:00
Mathew Polzin e36180c9b9 Make properties of Body.Data public 2018-11-27 18:05:02 -08:00
Mathew Polzin abee0c4d0e Add body variable to JSONAPIDocument protocol 2018-11-27 17:55:56 -08:00
Mathew Polzin 9c8b2fbebb Renamed JSONAPIDocument to Document and created a protocol describing a JSONAPI Document. 2018-11-27 17:35:13 -08:00
Mathew Polzin a9ef71f383 indentation change 2018-11-27 17:15:15 -08:00
Mathew Polzin 232554ec51 Wrap JSONAPIDocument body up a bit nicer for use when the JSONAPIDocument does not represent errors. 2018-11-27 16:12:36 -08:00
Mathew Polzin 8f36bb40e3 Add note about the library being a bit 'opinionated' 2018-11-27 14:04:53 -08:00
Mathew Polzin 55f2f52676 minor README change 2018-11-27 13:59:01 -08:00
Mathew Polzin dd5f3b1737 Added check for nullable array attributes 2018-11-27 13:57:55 -08:00
Mathew Polzin 7d7b3d7f19 Update linuxmain 2018-11-27 13:34:22 -08:00
Mathew Polzin aedd5dc29b Add Entity validation via a function in JSONAPITestLib. 2018-11-27 13:33:55 -08:00
Mathew Polzin 3964202ea2 Mention JSONAPITestLib in the README 2018-11-27 11:53:21 -08:00
Mathew Polzin dcabafd583 Add a playground page to show off JSONAPITestLib and add nil literal expressibility to ToOneRelationship 2018-11-27 11:45:15 -08:00
Mathew Polzin 9df9efc2dc rename NoRelatives to NoRelationships to better pair with NoAttributes and NoLinks 2018-11-27 11:08:53 -08:00
Mathew Polzin a55a4cbed0 Added test library, attribute literal expressibility, and tests 2018-11-27 10:54:51 -08:00
Mathew Polzin 6f6ed87b4c Update included playground 2018-11-27 09:14:47 -08:00
Mathew Polzin 59ff145aa8 Rename the included Error type. 2018-11-27 09:11:13 -08:00
Mathew Polzin 20a685e67b Add goal of making it easier to construct Attributes values 2018-11-26 22:29:31 -08:00
Mathew Polzin 3d8d10584b Add typealiases to make accessing Entity Relationships and Attributes types more convenient 2018-11-26 22:27:10 -08:00
Mathew Polzin b93580c900 Allow ManyResourceBody to use any PrimaryResource type 2018-11-26 18:57:34 -08:00
Mathew Polzin 8927938d56 Rename URL to JSONAPIURL to avoid conflict with Foundation.URL 2018-11-26 18:29:24 -08:00
Mathew Polzin 372e2de721 update linuxmain 2018-11-25 20:07:47 -08:00
Mathew Polzin 1a2ba17f02 more documentation updates 2018-11-25 20:06:27 -08:00
Mathew Polzin 9f2c7aa2e4 Update README 2018-11-25 19:48:50 -08:00
Mathew Polzin 650937dbb9 improve test coverage of some JSONAPIDocument accessors. 2018-11-25 19:41:10 -08:00
Mathew Polzin b08cbdb0ae Add some tests around documents with links. 2018-11-25 19:12:30 -08:00
Mathew Polzin c30a2615f2 Added links to document but currently untested 2018-11-25 00:02:37 -08:00
Mathew Polzin 86e87ebf5d Add Links and tests 2018-11-24 22:46:53 -08:00
Mathew Polzin 987bf2b750 Update linuxmain 2018-11-23 20:54:41 -08:00
Mathew Polzin af1aca9cf4 More failure test cases. pretty ok test coverage at this point. 2018-11-23 20:44:12 -08:00
Mathew Polzin b97649fab6 Add some failure testing 2018-11-23 20:34:43 -08:00
Mathew Polzin 8114132189 Add attribute validation tests 2018-11-23 20:21:00 -08:00
Mathew Polzin b61a41c99b Allow EntityType and Poly to be PrimaryResource in JSONAPIDocument 2018-11-23 19:33:06 -08:00
Mathew Polzin 16b83ddbef lots of tests of Poly 2018-11-23 19:09:53 -08:00
Mathew Polzin 6dff02bd51 Update to version of Result that does not even import Foundation for tests 2018-11-23 08:58:13 -08:00
Mathew Polzin 2563edf419 Add subscript access to Poly types. Add empty test class for Poly type. Remove unneeded Foundation import. Small README change to read more clearly. 2018-11-23 08:49:55 -08:00
Mathew Polzin 98bb64268f Split Include1 through Include6 off to a new file and a type I am calling Poly. This new type will allow me to use the same structures to represent polymorphic relationships. 2018-11-22 22:52:21 -08:00
Mathew Polzin 399644b62a update readme 2018-11-22 22:23:44 -08:00
Mathew Polzin 3b1e5f7414 Switched to a fork of the Result repo that does not depend on Foundation (outside of tests) 2018-11-22 22:08:18 -08:00
Mathew Polzin bf842bd781 Remove a couple unneeded Foundation imports that snuck in 2018-11-22 21:35:58 -08:00
Mathew Polzin 7edb631f09 Update documentation 2018-11-22 21:32:44 -08:00
Mathew Polzin 3c5a356b44 Add a couple more tests around metadata. 2018-11-22 21:14:19 -08:00
Mathew Polzin f526963707 Update README and update linuxmain 2018-11-22 21:06:04 -08:00
Mathew Polzin c4032eb351 Added meta data to JSONAPIDocument. Added tests around JSONAPIDocument error finally. Needs a few more tests around metadata. 2018-11-22 21:04:58 -08:00
Mathew Polzin 7a3a1b8b65 Round off CustomStringConvertible implementations to make print output a lot easier to read. 2018-11-20 07:27:28 -08:00
Mathew Polzin b3a4591a5d comment out the rest of the once again relevant playground example 2018-11-18 21:51:22 -08:00
Mathew Polzin 23d057c47b Adding more custom string convertible implementations to reduce the verbosity of printing to terminal. expose JSONAPIDocument.Include so that its constructor can be used. 2018-11-18 21:47:00 -08:00
Mathew Polzin 3327a93df5 Moved Examples.swift into a Playground, added some initialization code required to create entities and documents in code. 2018-11-18 17:47:26 -08:00
Mathew Polzin 713fd2ba3a Add xcode workspace files to git ignore. 2018-11-18 15:53:00 -08:00
Mathew Polzin 072479c160 Move Transformer into its own class and add a Validator protocol based on Transformer 2018-11-18 15:49:06 -08:00
62 changed files with 9004 additions and 917 deletions
+1
View File
@@ -2,3 +2,4 @@
/.build
/Packages
/*.xcodeproj
/*.xcworkspace
-78
View File
@@ -1,78 +0,0 @@
//
// Examples.swift
// JSONAPI
//
// Created by Mathew Polzin on 11/12/18.
//
import Foundation
import JSONAPI
/*******
Please enjoy these examples, but allow me the forced casting and the lack of error checking for the sake of brevity.
********/
typealias ExampleEntity<Description: EntityDescription> = Entity<Description, Id<String, Description>>
// MARK: - A few resource objects (entities)
enum PersonDescription: EntityDescription {
static var type: String { return "people" }
struct Attributes: JSONAPI.Attributes {
let name: [String]
let favoriteColor: String
}
struct Relationships: JSONAPI.Relationships {
let friends: ToManyRelationship<Person>
let dogs: ToManyRelationship<Dog>
let home: ToOneRelationship<House>
}
}
typealias Person = ExampleEntity<PersonDescription>
enum DogDescription: EntityDescription {
static var type: String { return "dogs" }
struct Attributes: JSONAPI.Attributes {
let name: String
}
struct Relationships: JSONAPI.Relationships {
let owner: ToOneRelationship<Person?>
}
}
typealias Dog = ExampleEntity<DogDescription>
enum HouseDescription: EntityDescription {
static var type: String { return "houses" }
typealias Attributes = NoAttributes
typealias Relationships = NoRelatives
}
typealias House = ExampleEntity<HouseDescription>
// MARK: - Parse a response body with one Dog in it
typealias SingleDogResponse = JSONAPIDocument<SingleResourceBody<Dog>, NoIncludes, BasicJSONAPIError>
let dummyData = "".data(using: .utf8)!
let dogResponse = try! JSONDecoder().decode(SingleDogResponse.self, from: dummyData)
let dog = dogResponse.body.data?.primary.value
// MARK: Parse a response body with multiple people in it and dogs and houses included
typealias BatchPeopleResponse = JSONAPIDocument<ManyResourceBody<Person>, Include2<Dog, House>, BasicJSONAPIError>
let peopleResponse = try! JSONDecoder().decode(BatchPeopleResponse.self, from: dummyData)
let people = peopleResponse.body.data?.primary.values
let dogs = peopleResponse.body.data?.included[Dog.self]
let houses = peopleResponse.body.data?.included[House.self]
@@ -0,0 +1,24 @@
//: [Previous](@previous)
import Foundation
import JSONAPI
import JSONAPITestLib
/*******
Please enjoy these examples, but allow me the forced casting and the lack of error checking for the sake of brevity.
********/
// MARK: - Literal Expressibility
// The JSONAPITestLib provides literal expressibility for key types to
// make creating tests easier
let dog = Dog(id: "1234", attributes: Dog.Attributes(name: "Buddy"), relationships: Dog.Relationships(owner: nil), 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)
+142
View File
@@ -0,0 +1,142 @@
//
// Examples.swift
// JSONAPI
//
// Created by Mathew Polzin on 11/12/18.
//
import Foundation
import JSONAPI
/*******
Please enjoy these examples, but allow me the forced casting and the lack of error checking for the sake of brevity.
********/
// MARK: - String as CreatableRawIdType
var GlobalStringId: Int = 0
extension String: CreatableRawIdType {
public static func unique() -> String {
GlobalStringId += 1
return String(GlobalStringId)
}
}
// MARK: - 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 {
public static var type: String { return "people" }
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: 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, 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)
}
}
public enum DogDescription: EntityDescription {
public static var type: String { return "dogs" }
public struct Attributes: JSONAPI.Attributes {
public let name: Attribute<String>
public init(name: Attribute<String>) {
self.name = name
}
}
public struct Relationships: JSONAPI.Relationships {
public let owner: ToOne<Person?>
public init(owner: ToOne<Person?>) {
self.owner = owner
}
}
}
public typealias Dog = ExampleEntity<DogDescription>
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 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)
}
}
public enum HouseDescription: EntityDescription {
public static var type: String { return "houses" }
public typealias Attributes = NoAttributes
public typealias Relationships = NoRelationships
}
public typealias House = ExampleEntity<HouseDescription>
+7
View File
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<playground version='6.0' target-platform='macos' executeOnSourceChanges='false'>
<pages>
<page name='Test Library'/>
<page name='Usage'/>
</pages>
</playground>
+1 -9
View File
@@ -1,15 +1,7 @@
{
"object": {
"pins": [
{
"package": "Result",
"repositoryURL": "https://github.com/antitypical/Result.git",
"state": {
"branch": null,
"revision": "8fc088dcf72802801efeecba76ea8fb041fb773d",
"version": "4.0.0"
}
}
]
},
"version": 1
+8 -6
View File
@@ -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/antitypical/Result.git", from: "4.0.0")
],
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]
)
+299 -103
View File
File diff suppressed because it is too large Load Diff
@@ -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)
}
}
}
+283 -19
View File
@@ -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,29 +25,191 @@
/// API uses snake case, you will want to use
/// a conversion such as the one offerred by the
/// Foundation JSONEncoder/Decoder: `KeyDecodingStrategy`
public struct JSONAPIDocument<ResourceBody: JSONAPI.ResourceBody, Include: IncludeDecoder, Error: JSONAPIError>: Equatable {
public struct 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 meta: Meta?
// public let jsonApi: APIDescription?
// public let links: Links?
public enum Body: Equatable {
case errors([Error])
case data(primary: ResourceBody, included: Includes<Include>)
case errors([Error], meta: MetaType?, links: LinksType?)
case data(Data)
public struct Data: Equatable {
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 }
return true
}
public var data: (primary: ResourceBody, included: Includes<Include>)? {
guard case let .data(primary: body, included: includes) = self else { return nil }
return (primary: body, included: includes)
public var errors: [Error]? {
guard case let .errors(errors, meta: _, links: _) = self else { return nil }
return errors
}
public var data: Data? {
guard case let .data(data) = self else { return nil }
return data
}
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(data) = self else { return nil }
return data.includes
}
public var meta: MetaType? {
switch self {
case .data(let data):
return data.meta
case .errors(_, meta: let metadata?, links: _):
return metadata
default:
return nil
}
}
public var links: LinksType? {
switch self {
case .data(let data):
return data.links
case .errors(_, meta: _, links: let links?):
return links
default:
return nil
}
}
}
public init(apiDescription: APIDescription, errors: [Error], meta: MetaType? = nil, links: LinksType? = nil) {
body = .errors(errors, meta: meta, links: links)
self.apiDescription = apiDescription
}
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: Codable {
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 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 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 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 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 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 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 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
@@ -47,38 +222,127 @@ 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?
if let noMeta = NoMetadata() as? MetaType {
meta = noMeta
} else {
do {
meta = try container.decode(MetaType.self, forKey: .meta)
} catch {
meta = nil
}
}
let links: LinksType?
if let noLinks = NoLinks() as? LinksType {
links = noLinks
} else {
do {
links = try container.decode(LinksType.self, forKey: .links)
} catch {
links = nil
}
}
// If there are errors, there cannot be a body. Return errors and any metadata found.
if let errors = errors {
body = .errors(errors)
body = .errors(errors, meta: meta, links: links)
return
}
let data = try container.decode(ResourceBody.self, forKey: .data)
let data: PrimaryResourceBody
if let noData = NoResourceBody() as? PrimaryResourceBody {
data = noData
} else {
data = try container.decode(PrimaryResourceBody.self, forKey: .data)
}
let maybeIncludes = try container.decodeIfPresent(Includes<Include>.self, forKey: .included)
// TODO come back to this and make robust
guard let metaVal = meta else {
throw JSONAPIEncodingError.missingOrMalformedMetadata
}
guard let linksVal = links else {
throw JSONAPIEncodingError.missingOrMalformedLinks
}
body = .data(primary: data, included: maybeIncludes ?? Includes<Include>.none)
body = .data(.init(primary: data, includes: maybeIncludes ?? Includes<Include>.none, meta: metaVal, links: linksVal))
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: RootCodingKeys.self)
switch body {
case .errors(let errors):
case .errors(let errors, meta: let meta, links: let links):
var errContainer = container.nestedUnkeyedContainer(forKey: .errors)
for error in errors {
try errContainer.encode(error)
}
case .data(primary: let resourceBody, included: let includes):
try container.encode(resourceBody, forKey: .data)
if MetaType.self != NoMetadata.self,
let metaVal = meta {
try container.encode(metaVal, forKey: .meta)
}
if LinksType.self != NoLinks.self,
let linksVal = links {
try container.encode(linksVal, forKey: .links)
}
case .data(let data):
try container.encode(data.primary, forKey: .data)
if Include.self != NoIncludes.self {
try container.encode(includes, forKey: .included)
try container.encode(data.includes, forKey: .included)
}
if MetaType.self != NoMetadata.self {
try container.encode(data.meta, forKey: .meta)
}
if LinksType.self != NoLinks.self {
try container.encode(data.links, forKey: .links)
}
}
if APIDescription.self != NoAPIDescription.self {
try container.encode(apiDescription, forKey: .jsonapi)
}
}
}
// MARK: - CustomStringConvertible
extension Document: CustomStringConvertible {
public var description: String {
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))"
}
}
+2 -2
View File
@@ -9,7 +9,7 @@ public protocol JSONAPIError: Swift.Error, Equatable, Codable {
static var unknown: Self { get }
}
public enum BasicJSONAPIError: JSONAPIError {
public enum UnknownJSONAPIError: JSONAPIError {
case unknownError
public init(from decoder: Decoder) throws {
@@ -21,7 +21,7 @@ public enum BasicJSONAPIError: JSONAPIError {
try container.encode("unknown")
}
public static var unknown: BasicJSONAPIError {
public static var unknown: UnknownJSONAPIError {
return .unknownError
}
}
File diff suppressed because it is too large Load Diff
+61 -8
View File
@@ -1,28 +1,67 @@
//
// ResourceBody.swift
// PrimaryResourceBody.swift
// JSONAPI
//
// Created by Mathew Polzin on 11/10/18.
//
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.EntityType>: ResourceBody {
public let value: Entity?
public protocol AppendableResourceBody: ResourceBody {
func appending(_ other: Self) -> Self
}
public struct ManyResourceBody<Entity: JSONAPI.EntityType>: ResourceBody {
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.PrimaryResource>: AppendableResourceBody {
public let values: [Entity]
public init(entities: [Entity]) {
values = entities
}
public func appending(_ other: ManyResourceBody) -> ManyResourceBody {
return ManyResourceBody(entities: values + other.values)
}
}
// MARK: Decodable
/// Use NoResourceBody to indicate you expect a JSON API document to not
/// contain a "data" top-level key.
public struct NoResourceBody: ResourceBody {
public static var none: NoResourceBody { return NoResourceBody() }
}
// MARK: 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
}
@@ -32,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
}
@@ -59,3 +98,17 @@ extension ManyResourceBody {
}
}
}
// MARK: CustomStringConvertible
extension SingleResourceBody: CustomStringConvertible {
public var description: String {
return "PrimaryResourceBody(\(String(describing: value)))"
}
}
extension ManyResourceBody: CustomStringConvertible {
public var description: String {
return "PrimaryResourceBody(\(String(describing: values)))"
}
}
+14
View File
@@ -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
}
+67
View File
@@ -0,0 +1,67 @@
//
// Links.swift
// JSONAPI
//
// Created by Mathew Polzin on 11/24/18.
//
/// A Links structure should contain nothing but JSONAPI.Link properties.
public protocol Links: Codable, Equatable {}
/// Use NoLinks where no links should belong to a JSON API component
public struct NoLinks: Links, CustomStringConvertible {
public static var none: NoLinks { return NoLinks() }
public init() {}
public var description: String { return "No Links" }
}
public protocol JSONAPIURL: Codable, Equatable {}
public struct Link<URL: JSONAPI.JSONAPIURL, Meta: JSONAPI.Meta>: Equatable, Codable {
public let url: URL
public let meta: Meta
public 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 {
private enum CodingKeys: String, CodingKey {
case href
case meta
}
init(from decoder: Decoder) throws {
guard Meta.self == NoMetadata.self,
let noMeta = NoMetadata() as? Meta else {
let container = try decoder.container(keyedBy: CodingKeys.self)
meta = try container.decode(Meta.self, forKey: .meta)
url = try container.decode(URL.self, forKey: .href)
return
}
let container = try decoder.singleValueContainer()
url = try container.decode(URL.self)
meta = noMeta
}
func encode(to encoder: Encoder) throws {
guard Meta.self == NoMetadata.self else {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(url, forKey: .href)
try container.encode(meta, forKey: .meta)
return
}
var container = encoder.singleValueContainer()
try container.encode(url)
}
}
+28
View File
@@ -0,0 +1,28 @@
//
// Meta.swift
// JSONAPI
//
// Created by Mathew Polzin on 11/21/18.
//
/// Conform a type to this protocol to indicate it can be encoded to or decoded from
/// the meta data attached to a component of a JSON API document. Different meta data
/// can be stored all over the place: On the root document, on a resource object, on
/// link objects, etc.
///
/// JSON API Metadata is totally open ended. It can take whatever JSON-compliant structure
/// the server and client agree upon.
public protocol Meta: Codable, Equatable {
}
// We make Optional a Meta if it wraps a Meta so that Metadata can be specified as
// nullable.
extension Optional: Meta where Wrapped: Meta {}
public struct NoMetadata: Meta, 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))
}
}
+96 -22
View File
@@ -5,8 +5,18 @@
// Created by Mathew Polzin on 11/13/18.
//
public struct TransformAttribute<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,12 +26,64 @@ public struct TransformAttribute<RawValue: Codable, Transformer: JSONAPI.Transfo
}
}
public typealias Attribute<T: Codable> = TransformAttribute<T, IdentityTransformer<T>>
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 TransformAttribute: Equatable where Transformer.From: Equatable, Transformer.To: Equatable {}
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)))"
}
}
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
// 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 TransformAttribute {
extension TransformedAttribute {
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
@@ -46,28 +108,40 @@ extension TransformAttribute {
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)
}
}
// MARK: - Transformers
extension Attribute {
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
public protocol Transformer {
associatedtype From
associatedtype To
static func transform(_ from: From) throws -> To
// 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)
}
}
public enum IdentityTransformer<T>: Transformer {
public static func transform(_ from: T) throws -> T { return from }
// 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

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