Compare commits

..

105 Commits

Author SHA1 Message Date
Mathew Polzin cd75e9649b Update cocoapods version, update readme to mention cocoapods support. 2019-04-14 21:01:20 -07:00
Mathew Polzin c60dfc1584 Testing out podspec 2019-04-14 20:49:59 -07:00
Mathew Polzin abe16dda0d A bit more clarification/correction 2019-02-07 18:56:22 -08:00
Mathew Polzin 5257fd79fa Remove redundant public scoping on functions within public extensions. This becomes a warning with the Swift 5 compiler. 2019-02-07 18:49:58 -08:00
Mathew Polzin f7a81df451 Fix inaccuracy and add a bit of clarity to README section on Entity Ids. 2019-02-07 18:43:57 -08:00
Mathew Polzin c8fbc09582 Remove Playground page that used the OpenAPI support that has now moved into its own repository. 2019-01-30 18:34:52 -08:00
Mathew Polzin a88bb77bd0 update linuxmain 2019-01-30 18:00:51 -08:00
Mathew Polzin 247848e788 Update README 2019-01-30 18:00:27 -08:00
Mathew Polzin 7adbdb4505 Move JSONAPIArbitrary and JSONAPIOpenAPI into their own packages. 2019-01-30 17:54:26 -08:00
Mathew Polzin ea663bb229 Move Sampleable into its own Package. 2019-01-29 00:44:31 -08:00
Mathew Polzin f48c385f99 super minor code doc tweak 2019-01-28 23:51:29 -08:00
Mathew Polzin c96bfadd82 Had not updated linuxmain in a while. pretty sure I'll have problems building on Linux at least with the stuff going on in JSONAPI+OpenAPI. 2019-01-28 23:46:56 -08:00
Mathew Polzin 88be701062 Add OpenAPI support for TransformedAttributes 2019-01-28 23:45:28 -08:00
Mathew Polzin 1e85e1d2a3 fix open API playground. 2019-01-27 22:55:32 -08:00
Mathew Polzin 11a7727ac9 Fix Include OpenAPI support. Fix bug with single include being considered 'OneOf' even though that is an overcomplication of the schema. Add tests for single include type and two include types on document 2019-01-27 22:47:01 -08:00
Mathew Polzin e8bfbc881b Added a couple of test cases for JSONDocuments 2019-01-27 22:24:13 -08:00
Mathew Polzin af757a2fac Add emphasis to README line. 2019-01-27 21:46:22 -08:00
Mathew Polzin 84955872f8 Finish writing Date Attribute test cases and add support for Optional Date OpenAPI Node guesses. 2019-01-27 21:39:18 -08:00
Mathew Polzin cb2800abd4 Add support for RequestBody on an OpenAPI Operation. 2019-01-27 13:57:04 -08:00
Mathew Polzin 85d5fef3c8 Fix bug where some references were correctly encoded as objects and others were just encoded as strings 2019-01-27 13:22:18 -08:00
Mathew Polzin e4ef61fd56 bugfix: OpenAPI path components should begin with a slash. 2019-01-27 12:46:13 -08:00
Mathew Polzin 75ec4f156e Fix a third dictionary-as-array bug 2019-01-26 20:04:11 -08:00
Mathew Polzin b45fc73188 Merge pull request #15 from mattpolzin/feature/OpenAPISchema
Feature/open api schema
2019-01-26 19:52:15 -08:00
Mathew Polzin 3b73dc9989 Fix super shitty bug caused by Apples implementation of Dictionary's conformance to Encodable sometimes encoding the dictionary as an array. 2019-01-26 19:50:20 -08:00
Mathew Polzin b2c81026f4 remove 'id' from Unidentified Entity OpenAPI node. 2019-01-26 18:55:47 -08:00
Mathew Polzin 8d057b4398 Merge pull request #14 from mattpolzin/feature/OpenAPISchema
Just enough OpenAPI Schema stuff to be dangerous
2019-01-25 18:25:19 -08:00
Mathew Polzin c8421cdd58 just throw that json extension on the filename to make everything super real 2019-01-25 18:20:04 -08:00
Mathew Polzin cde10a8491 Finish implementing remaining first wave of encodable conformances for OpenAPI 2019-01-25 18:17:58 -08:00
Mathew Polzin ad05d3908a Add a simple test OpenAPISchema and start to tweak things to get it workable. 2019-01-25 12:49:59 -08:00
Mathew Polzin 23b2b2e04f merge w/ master 2019-01-25 12:01:01 -08:00
Mathew Polzin 2988503d7d Add Sampleable conformance to Unidentified. Rename file slightly. 2019-01-25 11:59:05 -08:00
Mathew Polzin 5ea83b07c1 Hopefully remove some ambiguity. 2019-01-24 19:09:46 -08:00
Mathew Polzin 2b59f54067 Fix bug causing a supplied encoder to be used for generating an example but not for helping determine the correct Date formatting. 2019-01-24 17:47:44 -08:00
Mathew Polzin 58a7c82436 Restructure files a bit. Make Date handling relatively robust compared to my first pass at it. Make the failure to construct a generic open API node type throw an error rather than silently omit the node. 2019-01-24 17:25:34 -08:00
Mathew Polzin dc30cb3b9e Make Attribute Sampleable where its RawValue is Sampleable. Allow Sampleable things to provide a best guess for their node type based on the result of encoding and then deserializing them. 2019-01-24 00:47:24 -08:00
Mathew Polzin 7045373708 update and make sure all Playground pages run 2019-01-23 23:24:53 -08:00
Mathew Polzin 952fe8ba7e Remove encoder requirement to almost all Open API Node constructors. Made a new protocol for the few places where an encoder did need to be passed in. 2019-01-23 23:21:16 -08:00
Mathew Polzin 951c04ad44 Add Sampleable conformances. Make blanket JSONEncoder change I am not happy with; will try to walk back requirement that encoder gets passed to all functions creating OpenAPI Nodes 2019-01-23 22:21:27 -08:00
Mathew Polzin 57df6b147e Add Sampleable conformance to UnknownJSONAPIError. Add Sampleable conformance to all Include types and the Includes type. 2019-01-23 11:52:16 -08:00
Mathew Polzin a88844fe5e Switch to Poly 1.0.0 (same commit as previously used but by version rather than branch) 2019-01-23 00:34:14 -08:00
Mathew Polzin d1cf19f9fe Neck deep in stubbing out OpenAPI types 2019-01-22 22:14:36 -08:00
Mathew Polzin 59835fbe11 Add JSON Reference and OpenAPIComponents (initially just with schemas) 2019-01-22 16:08:47 -08:00
Mathew Polzin 850a713dcb Merge pull request #13 from mattpolzin/feature/OpenAPISchema
Add Include support to OpenAPI schema of JSONAPI Document.
2019-01-22 12:01:40 -08:00
Mathew Polzin e3c637a41e update README and add associated type labels to patterns for clarity 2019-01-22 11:57:18 -08:00
Mathew Polzin 9cbc626410 Add Include support to OpenAPI schema of JSONAPI Document. 2019-01-22 11:53:32 -08:00
Mathew Polzin 6dd14daf53 Merge pull request #12 from mattpolzin/feature/OpenAPISchema
Testing, Documentation, more Arbitrary conformance, "example" support.
2019-01-22 08:21:22 -08:00
Mathew Polzin 744e08acf4 Update linuxmain 2019-01-21 23:00:13 -08:00
Mathew Polzin 5da4a963d4 Update README and code documentation slightly 2019-01-21 22:50:19 -08:00
Mathew Polzin 5433dddc81 Add support for 'const' via OpenAPI 3.0 workaround suggested by others: Use 'enum' with one value. Add Sampleable support for requesting samples representing 'success' and 'failure' if available. 2019-01-21 22:47:03 -08:00
Mathew Polzin 95f9d8084d Add Arbitrary conformance for UnknownJSONAPIError. Fix weird encoding exception that I cannot quite figure out or explain at the moment. 2019-01-21 21:56:33 -08:00
Mathew Polzin d6911f170c Add example to JSONNode 2019-01-21 20:57:54 -08:00
Mathew Polzin 9972d13a4e Document zip(with:) on optionals 2019-01-21 15:37:41 -08:00
Mathew Polzin 4dc63167d6 Rename a couple of files 2019-01-21 15:35:54 -08:00
Mathew Polzin 845d085455 Fill out a fair bit of OpenAPI Entity testing 2019-01-21 15:32:38 -08:00
Mathew Polzin 45eb7d06bd Merge branch 'master' into feature/OpenAPISchema 2019-01-21 13:18:59 -08:00
Mathew Polzin 6ce962cec9 Merge pull request #11 from mattpolzin/feature/Arbitrary
Add Arbitrary support for the rest of JSONAPI's key types.
2019-01-21 13:16:46 -08:00
Mathew Polzin 477dae0a73 Add Arbitrary support for the rest of JSONAPI's key types. 2019-01-21 13:13:09 -08:00
Mathew Polzin 4afe4cfb31 Rename first JSONAPI+Testing header to Testing to remove conflict in GitHub Markdown links. 2019-01-21 11:52:02 -08:00
Mathew Polzin 65144d27ae Merge pull request #10 from mattpolzin/feature/OpenAPISchema
Feature/open api schema
2019-01-21 11:50:02 -08:00
Mathew Polzin 1ec4b21c34 Add missing case in Sampleable generic OpenAPINode implementation. Add info on new auxiliary frameworks to the README. 2019-01-20 19:18:10 -08:00
Mathew Polzin 15e13e2cc7 Remove unused error and add a bit of code documentation 2019-01-20 18:54:37 -08:00
Mathew Polzin 1d8ceef317 Update README, add a bit more code documentation 2019-01-20 18:52:21 -08:00
Mathew Polzin e546614c15 merge with Arbitrary branch 2019-01-20 18:41:54 -08:00
Mathew Polzin 8939d637d4 Add placeholder test to arbitrary test target. 2019-01-20 18:34:24 -08:00
Mathew Polzin e3b005b89a Update README 2019-01-20 18:25:28 -08:00
Mathew Polzin 2de6580519 Add some tests around Attribute OpenAPI descriptions 2019-01-20 18:18:35 -08:00
Mathew Polzin 875d938b95 Add id and type properties to Entity OpenAPI Schema 2019-01-20 16:19:26 -08:00
Mathew Polzin 1ed34b00dc Add todos for Entity Id and type. Add example usage of OpenAPI entity schema support in Playground 2019-01-20 16:10:31 -08:00
Mathew Polzin 531a7f35bd Cleanup and a bit of documentation 2019-01-20 15:50:46 -08:00
Mathew Polzin dc42ec27fc Not crazy about how I got there, but now it is relatively easy to print arbitrary enum's allCases as the list of possible values in the format specced out by OpenAPI. 2019-01-20 15:39:54 -08:00
Mathew Polzin cf746e182f currently in a pretty broken state with support for enumerations being turned into allowed values via reflection. I think I am going to have to give up type safety if I want to use reflection and keep things open ended 2019-01-19 15:30:09 -08:00
Mathew Polzin 52d2e9819d Update README project status to include OpenAPI and Arbitrary work. 2019-01-15 09:40:49 -08:00
Mathew Polzin 81b78cd749 Add more Arbitrary conformances 2019-01-15 09:27:09 -08:00
Mathew Polzin 3a0ede2372 Get rid of OpenAPI namespace because a compiler bug was making it more hassle than it was worth. 2019-01-14 23:46:39 -08:00
Mathew Polzin cb04a01e71 Add some todos and delete an old file 2019-01-14 23:29:49 -08:00
Mathew Polzin 3c29244540 fill out some of the relationship tests 2019-01-14 23:14:25 -08:00
Mathew Polzin a9e1e49a79 Add test helpers for Open API test target 2019-01-14 21:18:49 -08:00
Mathew Polzin 9e6e713ad2 Add Codable for the parts of OpenAPI that are built so far. 2019-01-14 21:17:07 -08:00
Mathew Polzin 308f168a8c Trying to map out the Open API Schema Object using Swift enums 2019-01-13 23:24:02 -08:00
Mathew Polzin 8726bcc9fa Update all references to JSONAPITestLib to reflect renaming 2019-01-13 18:35:20 -08:00
Mathew Polzin 25bda795a7 Rename JSONAPITestLib to JSONAPITesting to fit with naming convention I am adopting going forward. Added OpenAPI JSON schema types. Added default schema types for a handful of Swift primitives. 2019-01-13 18:28:41 -08:00
Mathew Polzin 14a0256885 Add new targets 2019-01-13 17:27:10 -08:00
Mathew Polzin b2041e9bc8 Update to bleeding edge Poly library 2019-01-13 07:47:20 -08:00
Mathew Polzin fd9a626551 Ditch unused test constant. 2019-01-13 07:42:13 -08:00
Mathew Polzin 69bb40aa00 Move JSONAPITestLib tests into their own test target. 2019-01-13 07:16:46 -08:00
Mathew Polzin 060e0e3898 Move PolyWrapped out of Poly and into JSONAPI. 2019-01-12 21:51:51 -08:00
Mathew Polzin b374367234 Move Poly into its own library. 2019-01-12 21:30:20 -08:00
Mathew Polzin b363db30cd Add MIT license docuemnt 2019-01-12 18:41:04 -08:00
Mathew Polzin 6904416623 Begin making Poly more useful in a broader context than JSONAPI. 2019-01-12 17:08:46 -08:00
Mathew Polzin c04d3301b6 Add Meta-Relationship access. 2019-01-08 21:23:17 -08:00
Mathew Polzin 5a64bebc99 Merge branch 'master' of github.com:mattpolzin/JSONAPI 2019-01-08 20:13:11 -08:00
Mathew Polzin 790b0fbf52 Change capitalization 2019-01-06 18:20:18 -08:00
Mathew Polzin 539ecc451a Remove swift syntax highlighting from JSON snippet. 2019-01-03 22:26:25 -08:00
Mathew Polzin 7eb1b05eae Add swift syntax highlighting to README. 2019-01-03 22:23:17 -08:00
Mathew Polzin 3408263c2a Add warning note to README 2019-01-03 22:09:43 -08:00
Mathew Polzin 1d6e5d3810 Added Meta-Attribute support and documentation 2019-01-02 22:49:38 -08:00
Mathew Polzin d68404db36 update README to reflect change made in v0.12.0 2019-01-02 19:46:30 -08:00
Mathew Polzin 4f7db98a87 Update playground files to work with v0.12.0 changes 2019-01-02 19:42:32 -08:00
Mathew Polzin 072b081ac3 Breaking - Rename static var type: String to static var jsonType:String to avoid unnecessary conflict with Swift.type(of:) 2019-01-02 19:35:50 -08:00
Mathew Polzin 897410492d Add much more substantial example to README and mirror it in the included playground. Add convenient methods for adding includes to a Document. Make Poly less picky about what type of things it contains (don't need to be entities anymore). Typealias Either to Poly2 because they are isomorphic. 2019-01-02 17:14:58 -08:00
Mathew Polzin b46f5166ea APIDescription gets a public initializer. A new playground page explores the full potential of the Framework, in a sense. 2018-12-31 17:19:51 -08:00
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
53 changed files with 2306 additions and 1089 deletions
@@ -0,0 +1,174 @@
import Foundation
import JSONAPI
import Poly
// MARK: - Preamble (setup)
// We make String a CreatableRawIdType. This is actually done in
// this Playground's Entities.swift file, so it is commented out here.
/*
var GlobalStringId: Int = 0
extension String: CreatableRawIdType {
public static func unique() -> String {
GlobalStringId += 1
return String(GlobalStringId)
}
}
*/
// We create a typealias given that we do not expect JSON:API Resource
// Objects for this particular API to have Metadata or Links associated
// with them. We also expect them to have String Identifiers.
typealias JSONEntity<Description: EntityDescription> = JSONAPI.Entity<Description, NoMetadata, NoLinks, String>
// Similarly, we create a typealias for unidentified entities. JSON:API
// only allows unidentified entities (i.e. no "id" field) for client
// requests that create new entities. In these situations, the server
// is expected to assign the new entity a unique ID.
typealias UnidentifiedJSONEntity<Description: EntityDescription> = JSONAPI.Entity<Description, NoMetadata, NoLinks, Unidentified>
// We create typealiases given that we do not expect JSON:API Relationships
// for this particular API to have Metadata or Links associated
// with them.
typealias ToOneRelationship<Entity: Identifiable> = JSONAPI.ToOneRelationship<Entity, NoMetadata, NoLinks>
typealias ToManyRelationship<Entity: Relatable> = JSONAPI.ToManyRelationship<Entity, NoMetadata, NoLinks>
// We create a typealias for a Document given that we do not expect
// JSON:API Documents for this particular API to have Metadata, Links,
// useful Errors, or a JSON:API Object (i.e. APIDescription).
typealias Document<PrimaryResourceBody: JSONAPI.ResourceBody, IncludeType: JSONAPI.Include> = JSONAPI.Document<PrimaryResourceBody, NoMetadata, NoLinks, IncludeType, NoAPIDescription, UnknownJSONAPIError>
// MARK: Entity Definitions
enum AuthorDescription: EntityDescription {
public static var jsonType: String { return "authors" }
public struct Attributes: JSONAPI.Attributes {
public let name: Attribute<String>
}
public typealias Relationships = NoRelationships
}
typealias Author = JSONEntity<AuthorDescription>
enum ArticleDescription: EntityDescription {
public static var jsonType: String { return "articles" }
public struct Attributes: JSONAPI.Attributes {
public let title: Attribute<String>
public let abstract: Attribute<String>
}
public struct Relationships: JSONAPI.Relationships {
public let author: ToOneRelationship<Author>
}
}
typealias Article = JSONEntity<ArticleDescription>
// MARK: Document Definitions
// We create a typealias to represent a document containing one Article
// and including its Author
typealias SingleArticleDocumentWithIncludes = Document<SingleResourceBody<Article>, Include1<Author>>
// ... and a typealias to represent a document containing one Article and
// not including any related entities.
typealias SingleArticleDocument = Document<SingleResourceBody<Article>, NoIncludes>
// MARK: - Server Pseudo-example
// Skipping over all the API and database stuff, here's a chunk of code
// that creates a document. Note that this document is the entirety
// of a JSON:API response body.
func articleDocument(includeAuthor: Bool) -> Either<SingleArticleDocument, SingleArticleDocumentWithIncludes> {
// Let's pretend all of this is coming from a database:
let authorId = Author.Identifier(rawValue: "1234")
let article = Article(id: .init(rawValue: "5678"),
attributes: .init(title: .init(value: "JSON:API in Swift"),
abstract: .init(value: "Not yet written")),
relationships: .init(author: .init(id: authorId)),
meta: .none,
links: .none)
let document = SingleArticleDocument(apiDescription: .none,
body: .init(entity: article),
includes: .none,
meta: .none,
links: .none)
switch includeAuthor {
case false:
return .a(document)
case true:
let author = Author(id: authorId,
attributes: .init(name: .init(value: "Janice Bluff")),
relationships: .none,
meta: .none,
links: .none)
let includes: Includes<SingleArticleDocumentWithIncludes.Include> = .init(values: [.init(author)])
return .b(document.including(.init(values: [.init(author)])))
}
}
let encoder = JSONEncoder()
encoder.keyEncodingStrategy = .convertToSnakeCase
encoder.outputFormatting = .prettyPrinted
let responseBody = articleDocument(includeAuthor: true)
let responseData = try! encoder.encode(responseBody)
// Next step would be encoding and setting as the HTTP body of a response.
// we will just print it out instead:
print("-----")
print(String(data: responseData, encoding: .utf8)!)
// ... and if we had received a request for an article without
// including the author:
let otherResponseBody = articleDocument(includeAuthor: false)
let otherResponseData = try! encoder.encode(otherResponseBody)
print("-----")
print(String(data: otherResponseData, encoding: .utf8)!)
// MARK: - Client Pseudo-example
enum NetworkError: Swift.Error {
case serverError
case quantityMismatch
}
// Skipping over all the API stuff, here's a chunk of code that will
// decode a document. We will assume we have made a request for a
// single article including the author.
func docode(articleResponseData: Data) throws -> (article: Article, author: Author) {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
let articleDocument = try decoder.decode(SingleArticleDocumentWithIncludes.self, from: articleResponseData)
switch articleDocument.body {
case .data(let data):
let authors = data.includes[Author.self]
guard authors.count == 1 else {
throw NetworkError.quantityMismatch
}
return (article: data.primary.value, author: authors[0])
case .errors(let errors, meta: _, links: _):
throw NetworkError.serverError
}
}
let response = try! docode(articleResponseData: responseData)
// Next step would be to do something useful with the article and author but we will print them instead.
print("-----")
print(response.article)
print(response.author)
@@ -0,0 +1,213 @@
import Foundation
import JSONAPI
/*
This is not realistically what JSONAPI code will look like. It makes
every bit of sense to typealias the long-winded generics of this
framework to make working with them more concise and readable.
The purpose of this example, then, is not to show "good" JSONAPI
code, but rather to generate a JSONAPI example document that uses
all the various (often optional) parts of a JSONAPI document.
The JSON example produced in this manner can be used to compare the
nomenclature of this library with the nomenclature of the JSON:API Spec.
*/
// MARK: - Extensions
extension Foundation.URL: JSONAPIURL {}
// extension String: CreatableRawIdType {} found in playground Entities.swift file.
// MARK: - Definitions
/// Metadata associated with entities
struct EntityMetadata: JSONAPI.Meta {
let creationDate: Date
let updatedDate: Date
}
/// Metadata associated with links
struct LinkMeta: JSONAPI.Meta {
/// Don't trust this link past the expiry date.
let expiry: Date
}
/// Links associated with entities
struct EntityLinks: JSONAPI.Links {
let `self`: Link<Foundation.URL, LinkMeta>
init(selfLink: Link<Foundation.URL, LinkMeta>) {
self.`self` = selfLink
}
}
/// Metadata associated with relationships
struct ToManyRelationshipMetadata: JSONAPI.Meta {
/// Pagination for the relationship (to support really large
/// to-many relationships)
let pagination: Pagination
struct Pagination: Codable, Equatable {
let total: Int
let limit: Int
let offset: Int
}
}
/// Links associated with relationships
struct ToManyRelationshipLinks: JSONAPI.Links {
/// next page of relationships.
let next: Link<Foundation.URL, LinkMeta>
}
/// Description of an Author entity.
enum AuthorDescription: EntityDescription {
static var jsonType: String { return "authors" }
struct Attributes: JSONAPI.Attributes {
let name: Attribute<String>
}
struct Relationships: JSONAPI.Relationships {
let articles: ToManyRelationship<Article, ToManyRelationshipMetadata, ToManyRelationshipLinks>
}
}
typealias Author = JSONAPI.Entity<AuthorDescription, EntityMetadata, EntityLinks, String>
/// Description of an Article entity.
enum ArticleDescription: EntityDescription {
static var jsonType: String { return "articles" }
struct Attributes: JSONAPI.Attributes {
let title: Attribute<String>
}
struct Relationships: JSONAPI.Relationships {
/// The primary attributed author of the article.
let primaryAuthor: ToOneRelationship<Author, NoMetadata, NoLinks>
/// All authors excluding the primary author.
/// It is customary to print the primary author's
/// name first, followed by the other authors.
let otherAuthors: ToManyRelationship<Author, ToManyRelationshipMetadata, ToManyRelationshipLinks>
}
}
typealias Article = JSONAPI.Entity<ArticleDescription, EntityMetadata, EntityLinks, String>
/// Metadata associated with the API Description
struct APIDescriptionMetadata: JSONAPI.Meta {
/// A clever codename for this API version.
let codename: String
}
/// Metadata associated with any Document
struct DocumentMetadata: JSONAPI.Meta {
/// Assuming caching is in play, this is when
/// the cached document being returned was created.
let updatedDate: Date
}
/// Links associated with an Article Document
struct SingleArticleDocumentLinks: JSONAPI.Links {
let otherArticlesByPrimaryAuthor: Link<Foundation.URL, LinkMeta>
}
/// Error that can occur when retrieving a Single Article Document
enum ArticleDocumentError: String, JSONAPIError, Codable {
case unknownError = "unknown"
case missing = "missing"
static var unknown: ArticleDocumentError {
return .unknownError
}
}
typealias SingleArticleDocument = JSONAPI.Document<SingleResourceBody<Article>, DocumentMetadata, SingleArticleDocumentLinks, Include1<Author>, APIDescription<APIDescriptionMetadata>, ArticleDocumentError>
// MARK: - Instantiations
let authorId1 = Author.Identifier()
let authorId2 = Author.Identifier()
let authorId3 = Author.Identifier()
let now = Date()
let tomorrow = Calendar.current.date(byAdding: .day, value: 1, to: now)!
let tomorrowEntityMeta = EntityMetadata(creationDate: Calendar.current.date(byAdding: .day, value: -10, to: now)!,
updatedDate: Calendar.current.date(byAdding: .day, value: -2, to: now)!)
let articleLinks = EntityLinks(selfLink: .init(url: URL(string: "https://articles.com/article/1234")!,
meta: .init(expiry: tomorrow)))
let article = Article(attributes: .init(title: .init(value: "How to read JSONAPI")),
relationships: .init(primaryAuthor: .init(id: authorId1),
otherAuthors: .init(ids: [authorId2, authorId3],
meta: .init(pagination: .init(total: 2,
limit: 50,
offset: 0)),
links: .init(next: .init(url: URL(string: "https://articles.com/article/1234?otherAuthors[offset]=50")!,
meta: .init(expiry: tomorrow))))),
meta: tomorrowEntityMeta,
links: articleLinks)
let author1Links = EntityLinks(selfLink: .init(url: URL(string: "https://articles.com/author/\(authorId1.rawValue)")!,
meta: .init(expiry: tomorrow)))
let author1 = Author(id: authorId1,
attributes: .init(name: .init(value: "James Kinney")),
relationships: .init(articles: .init(ids: [article.id, Article.Identifier(), Article.Identifier()],
meta: .init(pagination: .init(total: 3,
limit: 50,
offset: 0)),
links: .init(next: .init(url: URL(string: "https://articles.com/author/\(authorId1.rawValue)?articles[offset]=50")!, meta: .init(expiry: tomorrow))))),
meta: tomorrowEntityMeta,
links: author1Links)
let author2Links = EntityLinks(selfLink: .init(url: URL(string: "https://articles.com/author/\(authorId2.rawValue)")!,
meta: .init(expiry: tomorrow)))
let author2 = Author(id: authorId2,
attributes: .init(name: .init(value: "James Kinney")),
relationships: .init(articles: .init(ids: [article.id, Article.Identifier()],
meta: .init(pagination: .init(total: 2,
limit: 50,
offset: 0)),
links: .init(next: .init(url: URL(string: "https://articles.com/author/\(authorId2.rawValue)?articles[offset]=50")!, meta: .init(expiry: tomorrow))))),
meta: tomorrowEntityMeta,
links: author2Links)
let author3Links = EntityLinks(selfLink: .init(url: URL(string: "https://articles.com/author/\(authorId3.rawValue)")!,
meta: .init(expiry: tomorrow)))
let author3 = Author(id: authorId3,
attributes: .init(name: .init(value: "James Kinney")),
relationships: .init(articles: .init(ids: [article.id],
meta: .init(pagination: .init(total: 1,
limit: 50,
offset: 0)),
links: .init(next: .init(url: URL(string: "https://articles.com/author/\(authorId3.rawValue)?articles[offset]=50")!, meta: .init(expiry: tomorrow))))),
meta: tomorrowEntityMeta,
links: author3Links)
let apiDescription = APIDescription<APIDescriptionMetadata>(version: "1.2",
meta: APIDescriptionMetadata(codename: "cobra"))
let documentMetadata = DocumentMetadata(updatedDate: Date())
let documentLinks = SingleArticleDocumentLinks(otherArticlesByPrimaryAuthor: .init(url: URL(string: "https://articles.com/articles?author=\(authorId1.rawValue)")!,
meta: .init(expiry: tomorrow)))
let singleArticleDocument = SingleArticleDocument(apiDescription: apiDescription,
body: .init(entity: article),
includes: .init(values: [.init(author1), .init(author2), .init(author3)]),
meta: documentMetadata,
links: documentLinks)
// MARK: - Encoding
let encoder = JSONEncoder()
encoder.keyEncodingStrategy = .convertToSnakeCase
encoder.dateEncodingStrategy = .iso8601
encoder.outputFormatting = .prettyPrinted
let jsonData = try! encoder.encode(singleArticleDocument)
// MARK: - Printing JSON
print(String(data: jsonData, encoding: .utf8)!)
@@ -2,7 +2,7 @@
import Foundation
import JSONAPI
import JSONAPITestLib
import JSONAPITesting
/*******
@@ -11,12 +11,12 @@ Please enjoy these examples, but allow me the forced casting and the lack of err
********/
// MARK: - Literal Expressibility
// The JSONAPITestLib provides literal expressibility for key types to
// The JSONAPITesting framework 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
// The JSONAPITesting framework 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
@@ -11,8 +11,6 @@ Please enjoy these examples, but allow me the forced casting and the lack of err
// 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)
@@ -34,8 +32,6 @@ let dogs = try! [Dog(name: "Buddy", owner: personIds[0]), Dog(name: "Joy", owner
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)
+12 -10
View File
@@ -31,7 +31,7 @@ public typealias ToMany<E: Relatable> = ToManyRelationship<E, NoMetadata, NoLink
// MARK: - A few resource objects (entities)
public enum PersonDescription: EntityDescription {
public static var type: String { return "people" }
public static var jsonType: String { return "people" }
public struct Attributes: JSONAPI.Attributes {
public let name: Attribute<[String]>
@@ -63,14 +63,14 @@ public enum PersonDescription: EntityDescription {
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 = try Person(id: id ?? Person.Id(), attributes: .init(name: .init(rawValue: name), favoriteColor: .init(rawValue: favoriteColor)), relationships: .init(friends: .init(entities: friends), dogs: .init(entities: dogs), home: .init(entity: home)), meta: .none, links: .none)
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 static var jsonType: String { return "dogs" }
public struct Attributes: JSONAPI.Attributes {
public let name: Attribute<String>
@@ -93,7 +93,7 @@ public typealias Dog = ExampleEntity<DogDescription>
public enum AlternativeDogDescription: EntityDescription {
public static var type: String { return "dogs" }
public static var jsonType: String { return "dogs" }
public struct Attributes: JSONAPI.Attributes {
public let name: Attribute<String>
@@ -120,18 +120,18 @@ public enum AlternativeDogDescription: EntityDescription {
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 = try Dog(attributes: .init(name: .init(rawValue: name)), relationships: DogDescription.Relationships(owner: .init(entity: owner)), meta: .none, links: .none)
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 = try Dog(attributes: .init(name: .init(rawValue: name)), relationships: .init(owner: .init(id: owner)), meta: .none, links: .none)
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 static var jsonType: String { return "houses" }
public typealias Attributes = NoAttributes
public typealias Relationships = NoRelationships
@@ -139,4 +139,6 @@ public enum HouseDescription: EntityDescription {
public typealias House = ExampleEntity<HouseDescription>
public typealias SingleDogDocument = JSONAPI.Document<SingleResourceBody<Dog>, NoMetadata, NoLinks, NoIncludes, NoAPIDescription, UnknownJSONAPIError>
public typealias BatchPeopleDocument = JSONAPI.Document<ManyResourceBody<Person>, NoMetadata, NoLinks, Include2<Dog, House>, NoAPIDescription, UnknownJSONAPIError>
+2
View File
@@ -3,5 +3,7 @@
<pages>
<page name='Test Library'/>
<page name='Usage'/>
<page name='Full Client &amp; Server Example'/>
<page name='Full Document Verbose Generation'/>
</pages>
</playground>
+141
View File
@@ -0,0 +1,141 @@
#
# Be sure to run `pod spec lint JSONAPI.podspec' to ensure this is a
# valid spec and to remove all comments including this before submitting the spec.
#
# To learn more about Podspec attributes see https://docs.cocoapods.org/specification.html
# To see working Podspecs in the CocoaPods repo see https://github.com/CocoaPods/Specs/
#
Pod::Spec.new do |spec|
# ――― Spec Metadata ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――― #
#
# These will help people to find your library, and whilst it
# can feel like a chore to fill in it's definitely to your advantage. The
# summary should be tweet-length, and the description more in depth.
#
spec.name = "JSONAPI"
spec.version = "0.19.0"
spec.summary = "Swift Codable JSON API framework."
# This description is used to generate tags and improve search results.
# * Think: What does it do? Why did you write it? What is the focus?
# * Try to keep it short, snappy and to the point.
# * Write the description between the DESC delimiters below.
# * Finally, don't worry about the indent, CocoaPods strips it!
spec.description = <<-DESC
A Swift package for encoding to- and decoding from JSON API compliant requests and responses.
See the JSON API Spec here: https://jsonapi.org/format/
DESC
spec.homepage = "https://github.com/mattpolzin/JSONAPI"
# spec.screenshots = "www.example.com/screenshots_1.gif", "www.example.com/screenshots_2.gif"
# ――― Spec License ――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― #
#
# Licensing your code is important. See https://choosealicense.com for more info.
# CocoaPods will detect a license file if there is a named LICENSE*
# Popular ones are 'MIT', 'BSD' and 'Apache License, Version 2.0'.
#
# spec.license = "MIT"
spec.license = { :type => "MIT", :file => "LICENSE.txt" }
# ――― Author Metadata ――――――――――――――――――――――――――――――――――――――――――――――――――――――――― #
#
# Specify the authors of the library, with email addresses. Email addresses
# of the authors are extracted from the SCM log. E.g. $ git log. CocoaPods also
# accepts just a name if you'd rather not provide an email address.
#
# Specify a social_media_url where others can refer to, for example a twitter
# profile URL.
#
spec.author = { "Mathew Polzin" => "matt.polzin@gmail.com" }
# Or just: spec.author = "Mathew Polzin"
# spec.authors = { "Mathew Polzin" => "matt.polzin@gmail.com" }
# spec.social_media_url = "https://twitter.com/Mathew Polzin"
# ――― Platform Specifics ――――――――――――――――――――――――――――――――――――――――――――――――――――――― #
#
# If this Pod runs only on iOS or OS X, then specify the platform and
# the deployment target. You can optionally include the target after the platform.
#
# spec.platform = :ios
# spec.platform = :ios, "5.0"
# When using multiple platforms
spec.ios.deployment_target = "8.0"
spec.osx.deployment_target = "10.9"
# spec.watchos.deployment_target = "2.0"
# spec.tvos.deployment_target = "9.0"
# ――― Source Location ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――― #
#
# Specify the location from where the source should be retrieved.
# Supports git, hg, bzr, svn and HTTP.
#
spec.source = { :git => "https://github.com/mattpolzin/JSONAPI.git", :tag => "#{spec.version}" }
# ――― Source Code ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― #
#
# CocoaPods is smart about how it includes source code. For source files
# giving a folder will include any swift, h, m, mm, c & cpp files.
# For header files it will include any header in the folder.
# Not including the public_header_files will make all headers public.
#
spec.source_files = "Sources", "Sources/**/*.{swift}"
# spec.exclude_files = "Classes/Exclude"
# spec.public_header_files = "Classes/**/*.h"
# ――― Resources ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― #
#
# A list of resources included with the Pod. These are copied into the
# target bundle with a build phase script. Anything else will be cleaned.
# You can preserve files from being cleaned, please don't preserve
# non-essential files like tests, examples and documentation.
#
# spec.resource = "icon.png"
# spec.resources = "Resources/*.png"
# spec.preserve_paths = "FilesToSave", "MoreFilesToSave"
# ――― Project Linking ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――― #
#
# Link your library with frameworks, or libraries. Libraries do not include
# the lib prefix of their name.
#
spec.framework = "Poly"
# spec.frameworks = "SomeFramework", "AnotherFramework"
# spec.library = "iconv"
# spec.libraries = "iconv", "xml2"
# ――― Project Settings ――――――――――――――――――――――――――――――――――――――――――――――――――――――――― #
#
# If your library depends on compiler flags you can set them in the xcconfig hash
# where they will only apply to your library. If you depend on other Podspecs
# you can include multiple dependencies to ensure it works.
spec.swift_version = "4.2"
# spec.requires_arc = true
# spec.xcconfig = { "HEADER_SEARCH_PATHS" => "$(SDKROOT)/usr/include/libxml2" }
spec.dependency "Poly", "~> 1.1"
end
+7
View File
@@ -0,0 +1,7 @@
Copyright 2018 Mathew Polzin
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+9 -1
View File
@@ -1,7 +1,15 @@
{
"object": {
"pins": [
{
"package": "Poly",
"repositoryURL": "https://github.com/mattpolzin/Poly.git",
"state": {
"branch": null,
"revision": "77f45b8963a51c02d71fc4075eba5cff47ff0d07",
"version": "1.0.0"
}
}
]
},
"version": 1
+9 -5
View File
@@ -10,21 +10,25 @@ let package = Package(
name: "JSONAPI",
targets: ["JSONAPI"]),
.library(
name: "JSONAPITestLib",
targets: ["JSONAPITestLib"])
name: "JSONAPITesting",
targets: ["JSONAPITesting"])
],
dependencies: [
.package(url: "https://github.com/mattpolzin/Poly.git", from: "1.0.0"),
],
targets: [
.target(
name: "JSONAPI",
dependencies: []),
dependencies: ["Poly"]),
.target(
name: "JSONAPITestLib",
name: "JSONAPITesting",
dependencies: ["JSONAPI"]),
.testTarget(
name: "JSONAPITests",
dependencies: ["JSONAPITestLib"])
dependencies: ["JSONAPI", "JSONAPITesting"]),
.testTarget(
name: "JSONAPITestingTests",
dependencies: ["JSONAPI", "JSONAPITesting"])
],
swiftLanguageVersions: [.v4_2]
)
+438 -60
View File
File diff suppressed because it is too large Load Diff
@@ -14,8 +14,15 @@ public protocol APIDescriptionType: Codable, Equatable {
public struct APIDescription<Meta: JSONAPI.Meta>: APIDescriptionType {
public let version: String
public let meta: Meta
public init(version: String, meta: Meta) {
self.version = version
self.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
+76 -1
View File
@@ -5,6 +5,8 @@
// Created by Mathew Polzin on 11/5/18.
//
import Poly
public protocol JSONAPIDocument: Codable, Equatable {
associatedtype PrimaryResourceBody: JSONAPI.ResourceBody
associatedtype MetaType: JSONAPI.Meta
@@ -110,11 +112,16 @@ public struct Document<PrimaryResourceBody: JSONAPI.ResourceBody, MetaType: JSON
self.apiDescription = apiDescription
}
public init(apiDescription: APIDescription, body: PrimaryResourceBody, includes: Includes<Include>, meta: MetaType, links: LinksType) {
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) {
@@ -189,6 +196,74 @@ extension Document where IncludeType == NoIncludes, MetaType == NoMetadata, Link
}
*/
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 })
}
}
extension Document where IncludeType == NoIncludes {
/// Create a new Document with the given includes.
public func including<I: JSONAPI.Include>(_ includes: Includes<I>) -> Document<PrimaryResourceBody, MetaType, LinksType, I, APIDescription, Error> {
// Note that if IncludeType is NoIncludes, then we allow anything
// to be included, but if IncludeType already specifies a type
// of thing to be expected then we lock that down.
// See: Document.including() where IncludeType: _Poly1
switch body {
case .data(let data):
return .init(apiDescription: apiDescription,
body: data.primary,
includes: includes,
meta: data.meta,
links: data.links)
case .errors(let errors, meta: let meta, links: let links):
return .init(apiDescription: apiDescription,
errors: errors,
meta: meta,
links: links)
}
}
}
// extending where _Poly1 means all non-zero _Poly arities are included
extension Document where IncludeType: _Poly1 {
/// Create a new Document adding the given includes. This does not
/// remove existing includes; it is additive.
public func including(_ includes: Includes<IncludeType>) -> Document {
// Note that if IncludeType is NoIncludes, then we allow anything
// to be included, but if IncludeType already specifies a type
// of thing to be expected then we lock that down.
// See: Document.including() where IncludeType == NoIncludes
switch body {
case .data(let data):
return .init(apiDescription: apiDescription,
body: data.primary,
includes: data.includes + includes,
meta: data.meta,
links: data.links)
case .errors(let errors, meta: let meta, links: let links):
return .init(apiDescription: apiDescription,
errors: errors,
meta: meta,
links: links)
}
}
}
// MARK: - Codable
extension Document {
private enum RootCodingKeys: String, CodingKey {
case data
+27 -1
View File
@@ -5,7 +5,9 @@
// Created by Mathew Polzin on 11/10/18.
//
public typealias Include = Poly
import Poly
public typealias Include = JSONPoly
public struct Includes<I: Include>: Codable, Equatable {
public static var none: Includes { return .init(values: []) }
@@ -50,6 +52,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))"
@@ -123,3 +135,17 @@ extension Includes where I: _Poly7 {
}
// 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 }
}
}
+13 -1
View File
@@ -19,6 +19,14 @@ extension Optional: MaybePrimaryResource where Wrapped: PrimaryResource {}
public protocol ResourceBody: Codable, Equatable {
}
public protocol AppendableResourceBody: ResourceBody {
func appending(_ other: Self) -> Self
}
public func +<R: AppendableResourceBody>(_ left: R, right: R) -> R {
return left.appending(right)
}
public struct SingleResourceBody<Entity: JSONAPI.MaybePrimaryResource>: ResourceBody {
public let value: Entity
@@ -27,12 +35,16 @@ public struct SingleResourceBody<Entity: JSONAPI.MaybePrimaryResource>: Resource
}
}
public struct ManyResourceBody<Entity: JSONAPI.PrimaryResource>: 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
@@ -15,7 +15,22 @@ public extension TransformedAttribute {
/// 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> {
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.
func map<T: Codable>(_ transform: (ValueType) throws -> T) rethrows -> Attribute<T> {
return Attribute<T>(value: try transform(value))
}
}
+67 -14
View File
@@ -6,12 +6,17 @@
//
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 rawValue: RawValue
public let value: Transformer.To
@@ -21,16 +26,19 @@ public struct TransformedAttribute<RawValue: Codable, Transformer: JSONAPI.Trans
}
}
// 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 typealias Attribute<T: Codable> = TransformedAttribute<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 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)
@@ -45,15 +53,35 @@ extension TransformedAttribute: CustomStringConvertible {
extension TransformedAttribute: Equatable where Transformer.From: Equatable, Transformer.To: Equatable {}
extension TransformedAttribute where Transformer == IdentityTransformer<RawValue> {
// If we are using the identity transform, we can skip the transform and guarantee no
// error is thrown.
// 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) {
rawValue = value
self.value = value
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 {
public init(from decoder: Decoder) throws {
@@ -85,6 +113,31 @@ extension TransformedAttribute {
}
}
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 {
+57 -23
View File
@@ -20,16 +20,24 @@ public struct NoRelationships: Relationships {
public static var none: NoRelationships { return .init() }
}
extension NoRelationships: CustomStringConvertible {
public var description: String { return "No Relationships" }
}
/// Can be used as `Attributes` Type for Entities that do not
/// have any Attributes.
public struct NoAttributes: Attributes {
public static var none: NoAttributes { return .init() }
}
extension NoAttributes: CustomStringConvertible {
public var description: String { return "No Attributes" }
}
/// Something that is JSONTyped provides a String representation
/// of its type.
public protocol JSONTyped {
static var type: String { get }
static var jsonType: String { get }
}
/// An `EntityProxyDescription` is an `EntityDescription`
@@ -73,7 +81,7 @@ public protocol EntityProxy: Equatable, JSONTyped {
extension EntityProxy {
/// The JSON API compliant "type" of this `Entity`.
public static var type: String { return Description.type }
public static var jsonType: String { return Description.jsonType }
}
/// EntityType is the protocol that Entity conforms to. This
@@ -128,7 +136,7 @@ extension Entity: Identifiable, IdentifiableEntityType, Relatable where EntityRa
extension Entity: CustomStringConvertible {
public var description: String {
return "Entity<\(Entity.type)>(id: \(String(describing: id)), attributes: \(String(describing: attributes)), relationships: \(String(describing: relationships)))"
return "Entity<\(Entity.jsonType)>(id: \(String(describing: id)), attributes: \(String(describing: attributes)), relationships: \(String(describing: relationships)))"
}
}
@@ -389,20 +397,20 @@ public extension Entity where EntityRawIdType: JSONAPI.RawIdType {
/// An Entity.Pointer is a `ToOneRelationship` with no metadata or links.
/// This is just a convenient way to reference an Entity so that
/// other Entities' Relationships can be built up from it.
public typealias Pointer = ToOneRelationship<Entity, NoMetadata, NoLinks>
typealias Pointer = ToOneRelationship<Entity, NoMetadata, NoLinks>
/// Entity.Pointers is a `ToManyRelationship` with no metadata or links.
/// This is just a convenient way to reference a bunch of Entities so
/// that other Entities' Relationships can be built up from them.
public typealias Pointers = ToManyRelationship<Entity, NoMetadata, NoLinks>
typealias Pointers = ToManyRelationship<Entity, NoMetadata, NoLinks>
/// Get a pointer to this entity that can be used as a
/// relationship to another entity.
public var pointer: Pointer {
var pointer: Pointer {
return Pointer(entity: self)
}
public func pointer<MType: JSONAPI.Meta, LType: JSONAPI.Links>(withMeta meta: MType, links: LType) -> ToOneRelationship<Entity, MType, LType> {
func pointer<MType: JSONAPI.Meta, LType: JSONAPI.Links>(withMeta meta: MType, links: LType) -> ToOneRelationship<Entity, MType, LType> {
return ToOneRelationship(entity: self, meta: meta, links: links)
}
}
@@ -411,19 +419,19 @@ public extension Entity where EntityRawIdType: JSONAPI.RawIdType {
public extension Entity where EntityRawIdType == Unidentified {
/// Create a new Entity from this one with a newly created
/// unique Id of the given type.
public func identified<RawIdType: CreatableRawIdType>(byType: RawIdType.Type) -> Entity<Description, MetaType, LinksType, RawIdType> {
func identified<RawIdType: CreatableRawIdType>(byType: RawIdType.Type) -> Entity<Description, MetaType, LinksType, RawIdType> {
return .init(attributes: attributes, relationships: relationships, meta: meta, links: links)
}
/// Create a new Entity from this one with the given Id.
public func identified<RawIdType: JSONAPI.RawIdType>(by id: RawIdType) -> Entity<Description, MetaType, LinksType, RawIdType> {
func identified<RawIdType: JSONAPI.RawIdType>(by id: RawIdType) -> Entity<Description, MetaType, LinksType, RawIdType> {
return .init(id: Entity<Description, MetaType, LinksType, RawIdType>.Identifier(rawValue: id), attributes: attributes, relationships: relationships, meta: meta, links: links)
}
}
public extension Entity where EntityRawIdType: CreatableRawIdType {
/// Create a copy of this Entity with a new unique Id.
public func withNewIdentifier() -> Entity {
func withNewIdentifier() -> Entity {
return Entity(attributes: attributes, relationships: relationships, meta: meta, links: links)
}
}
@@ -433,21 +441,21 @@ public extension EntityProxy {
/// Access the attribute at the given keypath. This just
/// allows you to write `entity[\.propertyName]` instead
/// of `entity.relationships.propertyName`.
subscript<T, TFRM: Transformer>(_ path: KeyPath<Description.Attributes, TransformedAttribute<T, TFRM>>) -> TFRM.To {
subscript<T: AttributeType>(_ path: KeyPath<Description.Attributes, T>) -> T.ValueType {
return attributes[keyPath: path].value
}
/// Access the attribute at the given keypath. This just
/// allows you to write `entity[\.propertyName]` instead
/// of `entity.relationships.propertyName`.
subscript<T, TFRM: Transformer>(_ path: KeyPath<Description.Attributes, TransformedAttribute<T, TFRM>?>) -> TFRM.To? {
subscript<T: AttributeType>(_ path: KeyPath<Description.Attributes, T?>) -> T.ValueType? {
return attributes[keyPath: path]?.value
}
/// Access the attribute at the given keypath. This just
/// allows you to write `entity[\.propertyName]` instead
/// of `entity.relationships.propertyName`.
subscript<T, TFRM: Transformer, U>(_ path: KeyPath<Description.Attributes, TransformedAttribute<T, TFRM>?>) -> U? where TFRM.To == U? {
subscript<T: AttributeType, U>(_ path: KeyPath<Description.Attributes, T?>) -> U? where T.ValueType == U? {
// Implementation Note: Handles Transform that returns optional
// type.
return attributes[keyPath: path].flatMap { $0.value }
@@ -463,19 +471,28 @@ public extension EntityProxy {
}
}
// MARK: Meta-Attribute Access
public extension EntityProxy {
/// Access an attribute requiring a transformation on the RawValue _and_
/// a secondary transformation on this entity (self).
subscript<T>(_ path: KeyPath<Description.Attributes, (Self) -> T>) -> T {
return attributes[keyPath: path](self)
}
}
// MARK: Relationship Access
public extension EntityProxy {
/// Access to an Id of a `ToOneRelationship`.
/// This allows you to write `entity ~> \.other` instead
/// of `entity.relationships.other.id`.
public static func ~><OtherEntity: Identifiable, MType: JSONAPI.Meta, LType: JSONAPI.Links>(entity: Self, path: KeyPath<Description.Relationships, ToOneRelationship<OtherEntity, MType, LType>>) -> OtherEntity.Identifier {
static func ~><OtherEntity: Identifiable, MType: JSONAPI.Meta, LType: JSONAPI.Links>(entity: Self, path: KeyPath<Description.Relationships, ToOneRelationship<OtherEntity, MType, LType>>) -> OtherEntity.Identifier {
return entity.relationships[keyPath: path].id
}
/// Access to an Id of an optional `ToOneRelationship`.
/// This allows you to write `entity ~> \.other` instead
/// of `entity.relationships.other?.id`.
public static func ~><OtherEntity: OptionalRelatable, MType: JSONAPI.Meta, LType: JSONAPI.Links>(entity: Self, path: KeyPath<Description.Relationships, ToOneRelationship<OtherEntity, MType, LType>?>) -> OtherEntity.Identifier {
static func ~><OtherEntity: OptionalRelatable, MType: JSONAPI.Meta, LType: JSONAPI.Links>(entity: Self, path: KeyPath<Description.Relationships, ToOneRelationship<OtherEntity, MType, LType>?>) -> OtherEntity.Identifier {
// Implementation Note: This signature applies to `ToOneRelationship<E?, _, _>?`
// whereas the one below applies to `ToOneRelationship<E, _, _>?`
return entity.relationships[keyPath: path]?.id
@@ -484,7 +501,7 @@ public extension EntityProxy {
/// Access to an Id of an optional `ToOneRelationship`.
/// This allows you to write `entity ~> \.other` instead
/// of `entity.relationships.other?.id`.
public static func ~><OtherEntity: Relatable, MType: JSONAPI.Meta, LType: JSONAPI.Links>(entity: Self, path: KeyPath<Description.Relationships, ToOneRelationship<OtherEntity, MType, LType>?>) -> OtherEntity.Identifier? {
static func ~><OtherEntity: Relatable, MType: JSONAPI.Meta, LType: JSONAPI.Links>(entity: Self, path: KeyPath<Description.Relationships, ToOneRelationship<OtherEntity, MType, LType>?>) -> OtherEntity.Identifier? {
// Implementation Note: This signature applies to `ToOneRelationship<E, _, _>?`
// whereas the one above applies to `ToOneRelationship<E?, _, _>?`
return entity.relationships[keyPath: path]?.id
@@ -493,18 +510,35 @@ public extension EntityProxy {
/// Access to all Ids of a `ToManyRelationship`.
/// This allows you to write `entity ~> \.others` instead
/// of `entity.relationships.others.ids`.
public static func ~><OtherEntity: Relatable, MType: JSONAPI.Meta, LType: JSONAPI.Links>(entity: Self, path: KeyPath<Description.Relationships, ToManyRelationship<OtherEntity, MType, LType>>) -> [OtherEntity.Identifier] {
static func ~><OtherEntity: Relatable, MType: JSONAPI.Meta, LType: JSONAPI.Links>(entity: Self, path: KeyPath<Description.Relationships, ToManyRelationship<OtherEntity, MType, LType>>) -> [OtherEntity.Identifier] {
return entity.relationships[keyPath: path].ids
}
/// Access to all Ids of an optional `ToManyRelationship`.
/// This allows you to write `entity ~> \.others` instead
/// of `entity.relationships.others?.ids`.
public static func ~><OtherEntity: Relatable, MType: JSONAPI.Meta, LType: JSONAPI.Links>(entity: Self, path: KeyPath<Description.Relationships, ToManyRelationship<OtherEntity, MType, LType>?>) -> [OtherEntity.Identifier]? {
static func ~><OtherEntity: Relatable, MType: JSONAPI.Meta, LType: JSONAPI.Links>(entity: Self, path: KeyPath<Description.Relationships, ToManyRelationship<OtherEntity, MType, LType>?>) -> [OtherEntity.Identifier]? {
return entity.relationships[keyPath: path]?.ids
}
}
// MARK: Meta-Relationship Access
public extension EntityProxy {
/// Access to an Id of a `ToOneRelationship`.
/// This allows you to write `entity ~> \.other` instead
/// of `entity.relationships.other.id`.
static func ~><Identifier: IdType>(entity: Self, path: KeyPath<Description.Relationships, (Self) -> Identifier>) -> Identifier {
return entity.relationships[keyPath: path](entity)
}
/// Access to all Ids of a `ToManyRelationship`.
/// This allows you to write `entity ~> \.others` instead
/// of `entity.relationships.others.ids`.
static func ~><Identifier: IdType>(entity: Self, path: KeyPath<Description.Relationships, (Self) -> [Identifier]>) -> [Identifier] {
return entity.relationships[keyPath: path](entity)
}
}
infix operator ~>
// MARK: - Codable
@@ -518,10 +552,10 @@ private enum ResourceObjectCodingKeys: String, CodingKey {
}
public extension Entity {
public func encode(to encoder: Encoder) throws {
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: ResourceObjectCodingKeys.self)
try container.encode(Entity.type, forKey: .type)
try container.encode(Entity.jsonType, forKey: .type)
if EntityRawIdType.self != Unidentified.self {
try container.encode(id, forKey: .id)
@@ -544,14 +578,14 @@ public extension Entity {
}
}
public init(from decoder: Decoder) throws {
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: ResourceObjectCodingKeys.self)
let type = try container.decode(String.self, forKey: .type)
guard Entity.type == type else {
throw JSONAPIEncodingError.typeMismatch(expected: Description.type, found: type)
guard Entity.jsonType == type else {
throw JSONAPIEncodingError.typeMismatch(expected: Description.jsonType, found: type)
}
let maybeUnidentified = Unidentified() as? EntityRawIdType
@@ -0,0 +1,57 @@
//
// Poly+PrimaryResource.swift
// JSONAPI
//
// Created by Mathew Polzin on 11/22/18.
//
import Poly
/// Poly is a protocol to which types that
/// are polymorphic belong to. Specifically,
/// Poly1, Poly2, Poly3, etc. types conform
/// to the Poly protocol. These types allow
/// typesafe grouping of a number of
/// disparate types under one roof for
/// the purposes of JSON API compliant
/// encoding or decoding.
public typealias JSONPoly = Poly & PrimaryResource
public typealias PolyWrapped = Codable & Equatable
extension Poly0: PrimaryResource {
public init(from decoder: Decoder) throws {
throw JSONAPIEncodingError.illegalDecoding("Attempted to decode Poly0, which should represent a thing that is not expected to be found in a document.")
}
public func encode(to encoder: Encoder) throws {
throw JSONAPIEncodingError.illegalEncoding("Attempted to encode Poly0, which should represent a thing that is not expected to be found in a document.")
}
}
// MARK: - 1 type
extension Poly1: PrimaryResource, MaybePrimaryResource where A: PolyWrapped {}
// MARK: - 2 types
extension Poly2: PrimaryResource, MaybePrimaryResource where A: PolyWrapped, B: PolyWrapped {}
// MARK: - 3 types
extension Poly3: PrimaryResource, MaybePrimaryResource where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped {}
// MARK: - 4 types
extension Poly4: PrimaryResource, MaybePrimaryResource where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped {}
// MARK: - 5 types
extension Poly5: PrimaryResource, MaybePrimaryResource where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped, E: PolyWrapped {}
// MARK: - 6 types
extension Poly6: PrimaryResource, MaybePrimaryResource where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped, E: PolyWrapped, F: PolyWrapped {}
// MARK: - 7 types
extension Poly7: PrimaryResource, MaybePrimaryResource where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped, E: PolyWrapped, F: PolyWrapped, G: PolyWrapped {}
// MARK: - 8 types
extension Poly8: PrimaryResource, MaybePrimaryResource where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped, E: PolyWrapped, F: PolyWrapped, G: PolyWrapped, H: PolyWrapped {}
// MARK: - 9 types
extension Poly9: PrimaryResource, MaybePrimaryResource where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped, E: PolyWrapped, F: PolyWrapped, G: PolyWrapped, H: PolyWrapped, I: PolyWrapped {}
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