Compare commits

...

139 Commits

Author SHA1 Message Date
Mathew Polzin 19636a47f0 Merge pull request #51 from mattpolzin/feature/primary-resource-body-improvements
Feature/primary resource body improvements
2019-11-06 23:46:13 -08:00
Mathew Polzin 0538de48cb rename PolyWrapped to CodablePolyWrapped (missed when I did other similar renaming 2019-11-06 23:41:02 -08:00
Mathew Polzin 024fe2d452 Down to one sequence of compare functions for all documents 2019-11-06 23:21:22 -08:00
Mathew Polzin 832161628b go from 3 specializations of all document related compare functions down to 2. 2019-11-06 23:05:21 -08:00
Mathew Polzin f37f44cfda add comparable protocol 2019-11-06 21:42:13 -08:00
Mathew Polzin ae7e0f528a abstract away document comparison 2019-11-06 00:19:27 -08:00
Mathew Polzin 9f42e5fa3f Merge pull request #48 from mattpolzin/feature/indentation
Feature/indentation
2019-11-05 23:03:03 -08:00
Mathew Polzin 3e96adf775 merge and fix conflicts 2019-11-05 23:00:18 -08:00
Mathew Polzin 83233a7e1a Merge pull request #46 from mattpolzin/feature/testing-comparisons
Feature/testing comparisons
2019-11-05 22:47:47 -08:00
Mathew Polzin adcc6bfb10 fix error after merge 2019-11-05 22:43:40 -08:00
Mathew Polzin 6f9a2df605 Merge remote-tracking branch 'origin/alpha/3x' into feature/testing-comparisons 2019-11-05 22:41:53 -08:00
Mathew Polzin c24b70b49f Merge pull request #45 from mattpolzin/feature/deprecate-subscript
Feature/deprecate subscript
2019-11-05 22:40:37 -08:00
Mathew Polzin 7b56e35a91 Merge pull request #47 from mattpolzin/feature/document-improvements
Feature/document improvements
2019-11-05 22:38:42 -08:00
Mathew Polzin e6f82c6052 indentation 2019-11-05 22:37:48 -08:00
Mathew Polzin d6f01d6c1d indentation 2019-11-05 22:27:59 -08:00
Mathew Polzin a7f6ed5845 update README 2019-11-05 21:03:51 -08:00
Mathew Polzin 87271b93f9 deprecate subscript attribute accessor in favor of key path dynamic member lookup 2019-11-05 20:48:04 -08:00
Mathew Polzin 706346e3a6 just reformatting and rearranging 2019-11-05 20:04:47 -08:00
Mathew Polzin 33a5ff41a0 beginning to do some breaking change cleanup 2019-11-05 19:50:52 -08:00
Mathew Polzin 0fe5c53ada generate linuxmain 2019-11-05 19:04:47 -08:00
Mathew Polzin f7bfa91ccc lots of comparison code, a few small breaking changes 2019-11-05 19:03:51 -08:00
Mathew Polzin 1a15ab6f9d Fix return type of success document after adding includes 2019-10-27 16:21:41 -07:00
Mathew Polzin eb85620379 regenerate swift test manifest 2019-10-20 22:30:15 -07:00
Mathew Polzin 57f85476c0 Bump podspec version 2019-10-20 22:29:22 -07:00
Mathew Polzin ea5c0b8601 Add Document.ErrorDocument and Document.SuccessDocument types 2019-10-20 22:23:52 -07:00
Mathew Polzin b46429a0ad Update README.md 2019-10-15 23:37:02 -07:00
Mathew Polzin dbc6ecc6d8 Update README.md 2019-10-12 21:39:29 -07:00
Mathew Polzin dbbacab105 Bump podspec version 2019-10-12 19:30:24 -07:00
Mathew Polzin 4eb36d4594 Merge pull request #41 from mattpolzin/feature/resource-object-replacing
Resource Object replacing and tapping
2019-10-12 19:27:29 -07:00
Mathew Polzin 43e02351de Add documentation on replacing and tapping attributes and relationships 2019-10-12 18:22:48 -07:00
Mathew Polzin 0b307bd3bc Add replacement and tapping functions for attributes and relationships. 2019-10-12 17:54:28 -07:00
Mathew Polzin 662a84ccf0 Add examples around modifying attributes/relationships on resource object and then preparing a new request (i.e. for PATCHing) 2019-10-12 09:35:01 -07:00
Mathew Polzin 44f9bca7dc Merge pull request #39 from mattpolzin/feature/add-poly-11
Add Include11 Type
2019-10-02 20:10:56 -07:00
Mathew Polzin 774b53b9b6 bump podspec version and bump podspec Poly dependency. 2019-10-02 20:05:28 -07:00
Mathew Polzin 654d3bfd2b Update Poly, add Include11 type. 2019-10-02 20:02:11 -07:00
Mathew Polzin c26d6d99c0 Update README.md 2019-09-29 17:21:43 -07:00
Mathew Polzin 43dcc4fb12 Update README.md 2019-09-29 17:21:08 -07:00
Mathew Polzin abae975f59 Update README.md 2019-09-29 17:20:42 -07:00
Mathew Polzin 0e6b2a7771 Update README.md 2019-09-29 17:14:25 -07:00
Mathew Polzin 194a58ae56 Merge pull request #37 from mattpolzin/feature/basic-error
Feature/basic error
2019-09-29 17:01:25 -07:00
Mathew Polzin f1d6b22f61 Add playground example, add/update documentation, correct visibility of new error payload properties to public. 2019-09-29 16:49:38 -07:00
Mathew Polzin 3057992348 bump podspec version, update Playground examples 2019-09-29 15:57:54 -07:00
Mathew Polzin d4806ff557 Add a few decode examples 2019-09-29 15:36:16 -07:00
Mathew Polzin b0801f7cee Add tests for BasicJSONAPIError and tweak documentation 2019-09-29 15:20:08 -07:00
Mathew Polzin 88c5d400aa Add generic and basic error types. add tests for generic type. 2019-09-29 14:56:04 -07:00
Mathew Polzin 6cd5aeaba6 Update README.md
Update max number of `Include` types in documentation.
2019-09-16 17:16:42 -07:00
Mathew Polzin 7e28cd2606 Merge pull request #35 from mattpolzin/add-include-10
Add include10 Type
2019-09-16 17:13:49 -07:00
Mathew Polzin cf6fa39548 bump Podspec version 2019-09-16 09:58:57 -07:00
Mathew Polzin a24f15dc4e regenerate linuxmain 2019-09-16 09:38:36 -07:00
Mathew Polzin 87e9ee0606 remove tests that are duplicates of those in the Poly package. I just never deleted this file when the Poly stuff moved into its own package. Add support for Include10. 2019-09-16 09:37:34 -07:00
Mathew Polzin 5ed45078a1 Update README.md
Fix incorrect Swift version requirement and suggested SPM dependency version.
2019-09-14 16:44:46 -07:00
Mathew Polzin 34dc351810 Merge pull request #34 from mattpolzin/swift-5.1
Swift 5.1 tools dependency, access to attributes via dynamicMemberLookup
2019-09-14 16:28:24 -07:00
Mathew Polzin 3ff1b867ca Bump spec version to account for breaking changes with configs and swift tools version 2019-09-14 15:53:48 -07:00
Mathew Polzin ae51be20d8 Merge remote-tracking branch 'origin/master' into swift-5.1 2019-09-14 15:51:05 -07:00
Mathew Polzin c142e93fec Merge pull request #33 from chrisballinger/patch-1
Change Pod name to avoid conflict. Resolves #32
2019-09-14 15:49:37 -07:00
Chris Ballinger e99c6625f8 Update README.md 2019-09-14 10:07:53 -07:00
Chris Ballinger e9048a4861 Change Pod name to avoid conflict. Resolves #32 2019-09-13 15:07:37 -07:00
Mathew Polzin ff06c36b36 uncomment out language version declaration in package manifest, 'fix' indentation. 2019-09-05 22:22:55 -07:00
Mathew Polzin 8eb4c14bb3 Merge remote-tracking branch 'origin/master' into swift-5.1 2019-09-05 21:39:27 -07:00
Mathew Polzin baee85fb1b Update README.md 2019-09-02 16:07:10 -07:00
Mathew Polzin 44f21b4430 Update README.md 2019-09-02 16:04:06 -07:00
Mathew Polzin f7f36c6e7a Update README.md 2019-09-02 15:59:24 -07:00
Mathew Polzin af7eaeb684 Update README.md 2019-09-02 15:10:43 -07:00
Mathew Polzin 0cc2368331 Update README.md
Add instructions for adding as Swift Package Manager dependency.
2019-09-02 15:05:23 -07:00
Mathew Polzin 6c54845525 Update README.md 2019-09-02 15:00:07 -07:00
Mathew Polzin 51b981c108 Update README.md 2019-09-02 13:25:57 -07:00
Mathew Polzin b33c7f4ab5 Update README.md 2019-08-29 09:33:03 -07:00
Mathew Polzin bf8245c7b8 Update README.md 2019-08-29 09:30:59 -07:00
Mathew Polzin 7bd4021790 Update README.md 2019-08-28 20:21:50 -07:00
Mathew Polzin 771641b82c Update README.md 2019-08-28 18:55:42 -07:00
Mathew Polzin 198e5a9800 Update README.md 2019-08-28 18:55:11 -07:00
Mathew Polzin 9db534dea0 Update README.md
Add basic example Google Colab.
2019-08-27 00:48:11 -07:00
Mathew Polzin 2b409e464e merge w/ master 2019-08-23 17:53:33 -07:00
Mathew Polzin 7824bd8f68 Merge pull request #30 from mattpolzin/release/1.0
Release/1.0
2019-08-14 17:39:42 -07:00
Mathew Polzin 89217f7187 bump podspec version 2019-08-14 17:31:43 -07:00
Mathew Polzin 86a9345fdd Adding some documentation and making SparseFieldEncoder internal because it does not need to be public. 2019-08-14 09:22:06 -07:00
Mathew Polzin 8f9ec11f27 Housekeeping in the README 2019-08-14 08:57:57 -07:00
Mathew Polzin 9a9403bd98 Merge pull request #28 from mattpolzin/sparse-encoding
Sparse encoding
2019-08-12 07:35:49 -07:00
Mathew Polzin 453ce4b3a8 whitespace changes -- trying to switch from one whitespace standard to another in cohesive chunks to fit Xcode 11 default. 2019-08-06 09:19:06 -07:00
Mathew Polzin 9a0bba8d07 Rename AppendableResourceBody to Appendable. It is used in conjunction with ResourceBody but does not conform to it anymore. 2019-08-06 09:12:02 -07:00
Mathew Polzin d5b4aa70c7 Update linuxmain 2019-08-06 09:04:15 -07:00
Mathew Polzin 1df891ce06 Add test that uses JSONAPIDocument protocol in generic context 2019-08-06 08:55:49 -07:00
Mathew Polzin 6ba217f553 More sparse field encoder tests 2019-08-06 08:27:22 -07:00
Mathew Polzin e23ec090ed Starting to add SparseFieldEncoder tests 2019-08-05 19:44:09 -07:00
Mathew Polzin ed23349351 Update README 2019-08-05 18:45:13 -07:00
Mathew Polzin 6c8646a1b4 whitespace 2019-08-05 18:09:29 -07:00
Mathew Polzin 3a60ac5fe2 Add playground page showing off sparse fieldset encoding 2019-08-05 17:59:38 -07:00
Mathew Polzin 045e88f4d4 More code documentation 2019-08-05 16:28:02 -07:00
Mathew Polzin fe1f4c6c19 A bit of code documentation 2019-08-05 16:15:56 -07:00
Mathew Polzin 83f7a7b60e Add sparse document tests 2019-08-05 16:02:40 -07:00
Mathew Polzin 61f2edb59a Add tests for sparse fieldset includes 2019-08-05 15:21:59 -07:00
Mathew Polzin 32d584099f Update linuxmain 2019-08-05 14:51:48 -07:00
Mathew Polzin 9a07cf7066 Add test class placeholder 2019-08-05 14:51:18 -07:00
Mathew Polzin 0c7c7edcab Add sparse fieldset resource body tests 2019-08-05 14:48:08 -07:00
Mathew Polzin a596ecaecc A stab at separating out decoding enough to make it possible to use encode-only sparse fieldsets with JSONDocument 2019-08-05 09:23:44 -07:00
Mathew Polzin 265cffe8f0 Rename MaybePrimaryResource to OptionalPrimaryResource because I use 'Maybe' elsewhere to indicate a type-level distinction whereas this is a value-level distinction that really is just 'Optional' at play. 2019-08-05 08:17:38 -07:00
Mathew Polzin b98fb08353 Adding a bit of code doc 2019-08-05 08:13:18 -07:00
Mathew Polzin 61e00c2de5 small rename 2019-08-05 08:01:42 -07:00
Mathew Polzin e0b53236bb Add sparse fields method to ResourceObject and test it. 2019-08-04 23:03:56 -07:00
Mathew Polzin 2f3a619284 Spacing difference 2019-08-04 18:47:26 -07:00
Mathew Polzin 5aef44c3b3 Add Sparse Fieldset support for Attributes 2019-08-04 18:44:28 -07:00
Mathew Polzin 99e97aadcc Merge branch 'master' into swift-5.1 2019-08-04 10:34:32 -07:00
Mathew Polzin 17e2ce3138 Merge pull request #27 from mattpolzin/round-off-empty-decoder-tests
Cover the EmptyObjectDecoder unkeyed container with tests
2019-08-04 10:30:57 -07:00
Mathew Polzin 049dc1f7b5 Cover the EmptyObjectDecoder unkeyed container with tests 2019-08-04 10:28:48 -07:00
Mathew Polzin cf7b68c026 Merge w/ master 2019-08-04 09:02:04 -07:00
Mathew Polzin 223c39cec0 Merge pull request #26 from mattpolzin/testcov1
Test coverage PR
2019-08-02 09:34:36 -07:00
Mathew Polzin 5335f24e9b Add test coverage for EmptyObjectDecoder's keyed container 2019-08-02 09:30:30 -07:00
Mathew Polzin 7ec185bfe8 Add a couple tests and a bit of code doc. 2019-08-02 08:45:38 -07:00
Mathew Polzin e0e6723222 Make Xcode Beta happy by explicitly creating optionals where they used to be implied. 2019-08-02 07:27:17 -07:00
Mathew Polzin 4b4ebb4214 merge w/ master 2019-07-30 17:52:42 -07:00
Mathew Polzin 60cd515fd6 just filling in a couple small holes in test coverage 2019-07-30 17:44:06 -07:00
Mathew Polzin 396f8453d1 swap out broken nil checks (not harming anything, but not working as evidenced by not getting hit by any test cases) 2019-07-29 21:00:44 -07:00
Mathew Polzin f5eb343bd4 Complete test coverage of APIDescription 2019-07-29 20:41:22 -07:00
Mathew Polzin c4f96c0376 Updated example in playground/documentation after realizing I was not using a variable I had created. 2019-07-28 13:10:42 -07:00
Mathew Polzin 4d3597ef0e Update README.md 2019-07-27 21:30:14 -07:00
Mathew Polzin fb72817de6 bump swift version badge to swift 5.1 2019-07-25 09:36:35 -07:00
Mathew Polzin 569cec05cf ditch language version argument in package file that is not accepted by swift test. update linuxmain. 2019-07-25 09:27:56 -07:00
Mathew Polzin 78b9b12592 Update README 2019-07-25 09:23:01 -07:00
Mathew Polzin 9143281290 Introduce dynamic member keypath lookup as alternative to existing subscript keypath lookup for attributes. also did a bit of tidying up. 2019-07-25 09:12:17 -07:00
Mathew Polzin badfa30668 Merge remote-tracking branch 'origin/master' into swift-5.1 2019-07-25 07:49:22 -07:00
Mathew Polzin 0144a2ee80 Remove prior experimentation with property wrappers -- feature was not baked when that experimentation was done. 2019-07-25 07:49:17 -07:00
Mathew Polzin 880894d026 small fix for example in README 2019-07-25 07:38:21 -07:00
Mathew Polzin 7b5b17918c Update linuxmain, slightly update wording and indentation on example at bottom of README. 2019-07-24 19:43:11 -07:00
Mathew Polzin c996e7447c Update README.md 2019-07-23 21:11:59 -07:00
Mathew Polzin 61fa56e84b bump podspec version after two cocoapods fixes. 2019-07-16 22:19:50 -07:00
Mathew Polzin a6bf3e0ae0 Merge pull request #22 from chrisballinger/fix-podspec
Remove Poly from podspec frameworks
2019-07-16 22:15:04 -07:00
Mathew Polzin 620f6989d2 Merge pull request #23 from chrisballinger/fix-issue-20
Do not include testing source files in podspec
2019-07-16 22:01:29 -07:00
Chris Ballinger 715db33138 Do not include testing source files in podspec 2019-07-16 17:11:36 -07:00
Chris Ballinger e99abd26b3 Remove Poly from podspec frameworks 2019-07-16 16:16:32 -07:00
Mathew Polzin 099d17aa58 Update README.md
Removing check boxes for `Arbitrary` conformance and `OpenAPI` integration. Both of these "features" are maintained in separate libraries.
2019-07-03 07:28:31 -07:00
Mathew Polzin 92d1239a98 Merge pull request #19 from gufo/optional-relationships
Allow omitting `relationships` if all are optional
2019-07-03 07:19:14 -07:00
Janko Luin b7ce3d226e Apply EmptyObjectDecoder rather than empty JSON
Co-Authored-By: Mathew Polzin <matt.polzin@gmail.com>
2019-07-03 08:32:44 +02:00
Janko Luin 5b56d596e2 Merge remote-tracking branch 'upstream/master' into optional-relationships 2019-07-03 08:30:17 +02:00
Mathew Polzin e820f34253 change podspec version in anticipation of next release 2019-07-02 18:12:29 -07:00
Mathew Polzin c75912ab79 switch Poly versioning to 'upToNextMajor' and updated the resolved version. 2019-07-02 18:09:06 -07:00
Mathew Polzin e4481c9e4f noticed the README had some typos I missed when I changed Entity to ResourceObject. 2019-07-02 18:05:22 -07:00
Mathew Polzin e9b9a2bd78 Add Empty Object Decoder to be used in upcoming release. 2019-07-02 17:36:54 -07:00
Janko Luin fc78958f76 Allow omitting relationships if all are optional
When all relationships are optional, the `relationships` key is also
optional and not required in the structure.

I'm not super happy with importing Foundation and creating new objects
any time a key is missing, but ultimately none of my attempts at
conditional generics worked out for me.
2019-07-02 17:02:55 +02:00
Mathew Polzin c7b97567a9 update swift tools version in package file, add some property wrappers, add some tests for wrappers. its all broken but worth holding onto for now. 2019-06-20 22:35:55 -07:00
81 changed files with 7911 additions and 3053 deletions
@@ -4,65 +4,66 @@ import Poly
// MARK: - Preamble (setup)
// We make String a CreatableRawIdType. This is actually done in
// Make String a CreatableRawIdType. This is actually done in
// this Playground's Entities.swift file, so it is commented out here.
/*
var GlobalStringId: Int = 0
extension String: CreatableRawIdType {
public static func unique() -> String {
GlobalStringId += 1
return String(GlobalStringId)
}
}
*/
var globalStringId: Int = 0
extension String: CreatableRawIdType {
public static func unique() -> String {
globalStringId += 1
return String(globalStringId)
}
}
*/
// We create a typealias given that we do not expect JSON:API Resource
// Create a typealias because we do not expect JSON:API Resource
// Objects for this particular API to have Metadata or Links associated
// with them. We also expect them to have String Identifiers.
typealias JSONEntity<Description: ResourceObjectDescription> = JSONAPI.ResourceObject<Description, NoMetadata, NoLinks, String>
// Similarly, we create a typealias for unidentified entities. JSON:API
// Similarly, create a typealias for unidentified entities. JSON:API
// only allows unidentified entities (i.e. no "id" field) for client
// requests that create new entities. In these situations, the server
// is expected to assign the new entity a unique ID.
typealias UnidentifiedJSONEntity<Description: ResourceObjectDescription> = JSONAPI.ResourceObject<Description, NoMetadata, NoLinks, Unidentified>
// We create typealiases given that we do not expect JSON:API Relationships
// for this particular API to have Metadata or Links associated
// with them.
// Create relationship typealiases because we do not expect
// JSON:API Relationships for this particular API to have
// Metadata or Links associated with them.
typealias ToOneRelationship<Entity: Identifiable> = JSONAPI.ToOneRelationship<Entity, NoMetadata, NoLinks>
typealias ToManyRelationship<Entity: Relatable> = JSONAPI.ToManyRelationship<Entity, NoMetadata, NoLinks>
// We create a typealias for a Document given that we do not expect
// Create a typealias for a Document because we do not expect
// JSON:API Documents for this particular API to have Metadata, Links,
// useful Errors, or a JSON:API Object (i.e. APIDescription).
typealias Document<PrimaryResourceBody: JSONAPI.ResourceBody, IncludeType: JSONAPI.Include> = JSONAPI.Document<PrimaryResourceBody, NoMetadata, NoLinks, IncludeType, NoAPIDescription, UnknownJSONAPIError>
// useful Errors, or an APIDescription (The *SPEC* calls this
// "API Description" the "JSON:API Object").
typealias Document<PrimaryResourceBody: JSONAPI.ResourceBody, IncludeType: JSONAPI.Include> = JSONAPI.Document<PrimaryResourceBody, NoMetadata, NoLinks, IncludeType, NoAPIDescription, BasicJSONAPIError<String>>
// MARK: Entity Definitions
enum AuthorDescription: ResourceObjectDescription {
public static var jsonType: String { return "authors" }
public static var jsonType: String { return "authors" }
public struct Attributes: JSONAPI.Attributes {
public let name: Attribute<String>
}
public struct Attributes: JSONAPI.Attributes {
public let name: Attribute<String>
}
public typealias Relationships = NoRelationships
public typealias Relationships = NoRelationships
}
typealias Author = JSONEntity<AuthorDescription>
enum ArticleDescription: ResourceObjectDescription {
public static var jsonType: String { return "articles" }
public static var jsonType: String { return "articles" }
public struct Attributes: JSONAPI.Attributes {
public let title: Attribute<String>
public let abstract: Attribute<String>
}
public struct Attributes: JSONAPI.Attributes {
public let title: Attribute<String>
public let abstract: Attribute<String>
}
public struct Relationships: JSONAPI.Relationships {
public let author: ToOneRelationship<Author>
}
public struct Relationships: JSONAPI.Relationships {
public let author: ToOneRelationship<Author>
}
}
typealias Article = JSONEntity<ArticleDescription>
@@ -83,38 +84,38 @@ typealias SingleArticleDocument = Document<SingleResourceBody<Article>, NoInclud
// that creates a document. Note that this document is the entirety
// of a JSON:API response body.
func articleDocument(includeAuthor: Bool) -> Either<SingleArticleDocument, SingleArticleDocumentWithIncludes> {
// Let's pretend all of this is coming from a database:
// Let's pretend all of this is coming from a database:
let authorId = Author.Identifier(rawValue: "1234")
let authorId = Author.Identifier(rawValue: "1234")
let article = Article(id: .init(rawValue: "5678"),
attributes: .init(title: .init(value: "JSON:API in Swift"),
abstract: .init(value: "Not yet written")),
relationships: .init(author: .init(id: authorId)),
meta: .none,
links: .none)
let article = Article(id: .init(rawValue: "5678"),
attributes: .init(title: .init(value: "JSON:API in Swift"),
abstract: .init(value: "Not yet written")),
relationships: .init(author: .init(id: authorId)),
meta: .none,
links: .none)
let document = SingleArticleDocument(apiDescription: .none,
body: .init(resourceObject: article),
includes: .none,
meta: .none,
links: .none)
let document = SingleArticleDocument(apiDescription: .none,
body: .init(resourceObject: article),
includes: .none,
meta: .none,
links: .none)
switch includeAuthor {
case false:
return .a(document)
switch includeAuthor {
case false:
return .init(document)
case true:
let author = Author(id: authorId,
attributes: .init(name: .init(value: "Janice Bluff")),
relationships: .none,
meta: .none,
links: .none)
case true:
let author = Author(id: authorId,
attributes: .init(name: .init(value: "Janice Bluff")),
relationships: .none,
meta: .none,
links: .none)
let includes: Includes<SingleArticleDocumentWithIncludes.Include> = .init(values: [.init(author)])
let includes: Includes<SingleArticleDocumentWithIncludes.Include> = .init(values: [.init(author)])
return .b(document.including(.init(values: [.init(author)])))
}
return .init(document.including(includes))
}
}
let encoder = JSONEncoder()
@@ -124,8 +125,8 @@ encoder.outputFormatting = .prettyPrinted
let responseBody = articleDocument(includeAuthor: true)
let responseData = try! encoder.encode(responseBody)
// Next step would be encoding and setting as the HTTP body of a response.
// we will just print it out instead:
// Next step would be setting the HTTP body of a response.
// We will just print it out instead:
print("-----")
print(String(data: responseData, encoding: .utf8)!)
@@ -139,31 +140,31 @@ print(String(data: otherResponseData, encoding: .utf8)!)
// MARK: - Client Pseudo-example
enum NetworkError: Swift.Error {
case serverError
case quantityMismatch
case serverError
case quantityMismatch
}
// Skipping over all the API stuff, here's a chunk of code that will
// decode a document. We will assume we have made a request for a
// single article including the author.
func docode(articleResponseData: Data) throws -> (article: Article, author: Author) {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
let articleDocument = try decoder.decode(SingleArticleDocumentWithIncludes.self, from: articleResponseData)
let articleDocument = try decoder.decode(SingleArticleDocumentWithIncludes.self, from: articleResponseData)
switch articleDocument.body {
case .data(let data):
let authors = data.includes[Author.self]
switch articleDocument.body {
case .data(let data):
let authors = data.includes[Author.self]
guard authors.count == 1 else {
throw NetworkError.quantityMismatch
}
guard authors.count == 1 else {
throw NetworkError.quantityMismatch
}
return (article: data.primary.value, author: authors[0])
case .errors(let errors, meta: _, links: _):
throw NetworkError.serverError
}
return (article: data.primary.value, author: authors[0])
case .errors(let errors, meta: _, links: _):
throw NetworkError.serverError
}
}
let response = try! docode(articleResponseData: responseData)
@@ -0,0 +1,124 @@
//: [Previous](@previous)
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.
This playground focuses on receiving a resource, making some changes, and then creating a request body for a PATCH request.
As with all examples in these playround pages, no actual networking code will be provided.
********/
// Mock up a server response
let mockDogData = """
{
"data": {
"id": "1234",
"type": "dogs",
"attributes": {
"name": "Sparky"
},
"relationships": {
"owner": {
"data": null
}
}
}
}
""".data(using: .utf8)!
//
// MARK: - EXAMPLE 1 (Mutable Attributes)
//
// pretend to have requested a Dog and received the mock data
// now parse it.
let parsedResponse = try! JSONDecoder().decode(MutableDogDocument.self, from: mockDogData)
// extract our Dog (skipping over any robustness to handle errors)
var dog = parsedResponse.body.primaryResource!.value
print("Received dog named: \(dog.name)")
// change the dog's name
let changedDog = dog.tappingAttributes { $0.name = .init(value: "Julia") }
// create a document to be used as a request body for a PATCH request
let patchRequest = MutableDogDocument(apiDescription: .none,
body: .init(resourceObject: changedDog),
includes: .none,
meta: .none,
links: .none)
// encode and send off to server
let encodedPatchRequest = try! JSONEncoder().encode(patchRequest)
print("----")
print(String(data: encodedPatchRequest, encoding:.utf8)!)
//
// MARK: - EXAMPLE 2 (Immutable Attributes)
//
print()
print("####")
print()
// pretend to have requested a Dog and received the mock data
// now parse it.
let parsedResponse2 = try! JSONDecoder().decode(SingleDogDocument.self, from: mockDogData)
// extract our Dog (skipping over any robustness to handle errors)
var dog2 = parsedResponse2.body.primaryResource!.value
print("Received dog named: \(dog2.name)")
// change the dog's name
let changedDog2 = dog2.replacingAttributes { _ in
return .init(name: .init(value: "Nigel"))
}
// create a document to be used as a request body for a PATCH request
let patchRequest2 = SingleDogDocument(apiDescription: .none,
body: .init(resourceObject: changedDog2),
includes: .none,
meta: .none,
links: .none)
// encode and send off to server
let encodedPatchRequest2 = try! JSONEncoder().encode(patchRequest2)
print("----")
print(String(data: encodedPatchRequest2, encoding:.utf8)!)
//
// MARK: - EXAMPLE 3 (Change relationship)
//
print()
print("####")
print()
// pretend to have requested a Dog and received the mock data
// now parse it.
let parsedResponse3 = try! JSONDecoder().decode(SingleDogDocument.self, from: mockDogData)
// extract our Dog (skipping over any robustness to handle errors)
var dog3 = parsedResponse2.body.primaryResource!.value
print("Received dog with owner: \(dog3 ~> \.owner)")
// give the dog an owner
let changedDog3 = dog3.replacingRelationships { _ in
return .init(owner: .init(id: Id(rawValue: "1")))
}
// create a document to be used as a request body for a PATCH request
let patchRequest3 = SingleDogDocument(apiDescription: .none,
body: .init(resourceObject: changedDog3),
includes: .none,
meta: .none,
links: .none)
// encode and send off to server
let encodedPatchRequest3 = try! JSONEncoder().encode(patchRequest3)
print("----")
print(String(data: encodedPatchRequest3, encoding:.utf8)!)
@@ -0,0 +1,72 @@
import JSONAPI
import Foundation
// MARK: - Resource Object
enum ThingWithPropertiesDescription: JSONAPI.ResourceObjectDescription {
static let jsonType: String = "thing"
//
// NOTE: `JSONAPI.SparsableAttributes` as opposed to `JSONAPI.Attributes`
//
struct Attributes: JSONAPI.SparsableAttributes {
let stringThing: Attribute<String>
let numberThing: Attribute<Double>
let boolThing: Attribute<Bool?>
//
// NOTE: Special implementation of `CodingKeys`
//
enum CodingKeys: String, JSONAPI.SparsableCodingKey {
case stringThing
case numberThing
case boolThing
}
}
typealias Relationships = NoRelationships
}
typealias ThingWithProperties = JSONAPI.ResourceObject<ThingWithPropertiesDescription, NoMetadata, NoLinks, String>
// MARK: - Document
//
// NOTE: Using `JSONAPI.EncodableResourceBody` which means the document type will be `Encodable` but not `Decodable`.
//
typealias Document<PrimaryResourceBody: JSONAPI.EncodableResourceBody, IncludeType: JSONAPI.Include> = JSONAPI.Document<PrimaryResourceBody, NoMetadata, NoLinks, IncludeType, NoAPIDescription, BasicJSONAPIError<String>>
//
// NOTE: Using `JSONAPI.EncodablePrimaryResource` which means the `ResourceBody` will be `Encodable` but not `Decodable.
//
typealias SingleDocument<T: JSONAPI.EncodablePrimaryResource> = Document<SingleResourceBody<T>, NoIncludes>
// MARK: - Resource Initialization
let resource = ThingWithProperties(id: .init(rawValue: "1234"),
attributes: .init(stringThing: .init(value: "hello world"),
numberThing: .init(value: 10),
boolThing: .init(value: nil)),
relationships: .none,
meta: .none,
links: .none)
//
// NOTE: Creating a sparse resource that will only encode
// the attribute named "stringThing"
//
let sparseResource = resource.sparse(with: [.stringThing])
// MARK: - Encoding
let encoder = JSONEncoder()
let sparseResourceDoc = SingleDocument(apiDescription: .none,
body: .init(resourceObject: sparseResource),
includes: .none,
meta: .none,
links: .none)
let data = try! encoder.encode(sparseResourceDoc)
print(String(data: data, encoding: .utf8)!)
@@ -8,6 +8,7 @@ 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)
@@ -15,17 +16,20 @@ let singleDogDocument = SingleDogDocument(apiDescription: .none, body: .init(res
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>
// MARK: - 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, BasicJSONAPIError<String>>
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])]
@@ -36,6 +40,7 @@ let includes = dogs.map { BatchPeopleDocument.Include($0) } + houses.map { Batch
let batchPeopleDocument = BatchPeopleDocument(apiDescription: .none, body: .init(resourceObjects: 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)
@@ -47,20 +52,18 @@ print("-----")
print(peopleResponse)
print("-----")
// MARK: - Pass successfully parsed body to other parts of the code
/*
---- CRASHING IN XCODE 10.2 PLAYGROUND ----
if case let .data(bodyData) = peopleResponse.body {
print("first person's name: \(bodyData.primary.values[0][\.fullName])")
print("first person's name: \(bodyData.primary.values[0].fullName)")
} else {
print("no body data")
}
*/
// MARK: - Work in the abstract
print("-----")
func process<T: JSONAPIDocument>(document: T) {
guard case let .data(body) = document.body else {
return
@@ -68,3 +71,29 @@ func process<T: JSONAPIDocument>(document: T) {
let x: T.Body.Data = body
}
process(document: peopleResponse)
// MARK: - Work with errors
typealias ErrorDoc = JSONAPI.Document<NoResourceBody, NoMetadata, NoLinks, NoIncludes, NoAPIDescription, BasicJSONAPIError<String>>
let mockErrorData =
"""
{
"errors": [
{
"status": "500",
"title": "Internal Server Error",
"detail": "Server fell over while parsing your request."
}
]
}
""".data(using: .utf8)!
let errorResponse = try! JSONDecoder().decode(ErrorDoc.self, from: mockErrorData)
switch errorResponse.body {
case .data:
print("cool, data!")
case .errors(let errors, let meta, let links):
let errorDetails = errors.compactMap { $0.payload?.detail }
print("error details: \(errorDetails)")
}
+30 -5
View File
@@ -15,11 +15,11 @@ Please enjoy these examples, but allow me the forced casting and the lack of err
********/
// MARK: - String as CreatableRawIdType
var GlobalStringId: Int = 0
var globalStringId: Int = 0
extension String: CreatableRawIdType {
public static func unique() -> String {
GlobalStringId += 1
return String(GlobalStringId)
globalStringId += 1
return String(globalStringId)
}
}
@@ -119,6 +119,29 @@ public enum AlternativeDogDescription: ResourceObjectDescription {
public typealias AlternativeDog = ExampleEntity<AlternativeDogDescription>
public enum MutableDogDescription: ResourceObjectDescription {
public static var jsonType: String { return "dogs" }
public struct Attributes: JSONAPI.Attributes {
public var name: Attribute<String>
public init(name: Attribute<String>) {
self.name = name
}
}
public struct Relationships: JSONAPI.Relationships {
public var owner: ToOne<Person?>
public init(owner: ToOne<Person?>) {
self.owner = owner
}
}
}
public typealias MutableDog = ExampleEntity<MutableDogDescription>
public extension ResourceObject where Description == DogDescription, MetaType == NoMetadata, LinksType == NoLinks, EntityRawIdType == String {
init(name: String, owner: Person?) throws {
self = Dog(attributes: .init(name: .init(value: name)), relationships: DogDescription.Relationships(owner: .init(resourceObject: owner)), meta: .none, links: .none)
@@ -139,6 +162,8 @@ public enum HouseDescription: ResourceObjectDescription {
public typealias House = ExampleEntity<HouseDescription>
public typealias SingleDogDocument = JSONAPI.Document<SingleResourceBody<Dog>, NoMetadata, NoLinks, NoIncludes, NoAPIDescription, UnknownJSONAPIError>
public typealias SingleDogDocument = JSONAPI.Document<SingleResourceBody<Dog>, NoMetadata, NoLinks, NoIncludes, NoAPIDescription, BasicJSONAPIError<String>>
public typealias BatchPeopleDocument = JSONAPI.Document<ManyResourceBody<Person>, NoMetadata, NoLinks, Include2<Dog, House>, NoAPIDescription, UnknownJSONAPIError>
public typealias MutableDogDocument = JSONAPI.Document<SingleResourceBody<MutableDog>, NoMetadata, NoLinks, NoIncludes, NoAPIDescription, BasicJSONAPIError<String>>
public typealias BatchPeopleDocument = JSONAPI.Document<ManyResourceBody<Person>, NoMetadata, NoLinks, Include2<Dog, House>, NoAPIDescription, BasicJSONAPIError<String>>
+2
View File
@@ -5,5 +5,7 @@
<page name='Usage'/>
<page name='Full Client &amp; Server Example'/>
<page name='Full Document Verbose Generation'/>
<page name='Sparse Fieldsets Example'/>
<page name='PATCHing'/>
</pages>
</playground>
+6 -6
View File
@@ -15,8 +15,8 @@ Pod::Spec.new do |spec|
# summary should be tweet-length, and the description more in depth.
#
spec.name = "JSONAPI"
spec.version = "0.30.0"
spec.name = "MP-JSONAPI"
spec.version = "2.5.0"
spec.summary = "Swift Codable JSON API framework."
# This description is used to generate tags and improve search results.
@@ -93,7 +93,7 @@ See the JSON API Spec here: https://jsonapi.org/format/
# Not including the public_header_files will make all headers public.
#
spec.source_files = "Sources", "Sources/**/*.{swift}"
spec.source_files = "Sources/JSONAPI/**/*.{swift}"
# spec.exclude_files = "Classes/Exclude"
# spec.public_header_files = "Classes/**/*.h"
@@ -119,7 +119,6 @@ See the JSON API Spec here: https://jsonapi.org/format/
# the lib prefix of their name.
#
spec.framework = "Poly"
# spec.frameworks = "SomeFramework", "AnotherFramework"
# spec.library = "iconv"
@@ -132,10 +131,11 @@ See the JSON API Spec here: https://jsonapi.org/format/
# 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 = "5.0"
spec.swift_version = "5.1"
spec.module_name = "JSONAPI"
# spec.requires_arc = true
# spec.xcconfig = { "HEADER_SEARCH_PATHS" => "$(SDKROOT)/usr/include/libxml2" }
spec.dependency "Poly", "~> 2.0"
spec.dependency "Poly", "~> 2.2"
end
+2 -2
View File
@@ -6,8 +6,8 @@
"repositoryURL": "https://github.com/mattpolzin/Poly.git",
"state": {
"branch": null,
"revision": "d24d4c1214dd05f89eb1182a46592856dd0a0645",
"version": "2.0.0"
"revision": "18cd995be5c28c4dfdc1464e54ee0efb03e215bf",
"version": "2.3.0"
}
}
]
+11 -11
View File
@@ -1,4 +1,4 @@
// swift-tools-version:5.0
// swift-tools-version:5.1
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
@@ -6,27 +6,27 @@ import PackageDescription
let package = Package(
name: "JSONAPI",
platforms: [
.macOS(.v10_10),
.iOS(.v10)
.macOS(.v10_10),
.iOS(.v10)
],
products: [
.library(
name: "JSONAPI",
targets: ["JSONAPI"]),
.library(
name: "JSONAPITesting",
targets: ["JSONAPITesting"])
.library(
name: "JSONAPITesting",
targets: ["JSONAPITesting"])
],
dependencies: [
.package(url: "https://github.com/mattpolzin/Poly.git", from: "2.0.0"),
.package(url: "https://github.com/mattpolzin/Poly.git", .upToNextMajor(from: "2.3.0")),
],
targets: [
.target(
name: "JSONAPI",
dependencies: ["Poly"]),
.target(
name: "JSONAPITesting",
dependencies: ["JSONAPI"]),
.target(
name: "JSONAPITesting",
dependencies: ["JSONAPI"]),
.testTarget(
name: "JSONAPITests",
dependencies: ["JSONAPI", "JSONAPITesting"]),
@@ -34,5 +34,5 @@ let package = Package(
name: "JSONAPITestingTests",
dependencies: ["JSONAPI", "JSONAPITesting"])
],
swiftLanguageVersions: [.v5]
swiftLanguageVersions: [.v5]
)
+262 -167
View File
File diff suppressed because it is too large Load Diff
+32 -32
View File
@@ -7,58 +7,58 @@
/// This is what the JSON API Spec calls the "JSON:API Object"
public protocol APIDescriptionType: Codable, Equatable {
associatedtype Meta
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
public let version: String
public let meta: Meta
public init(version: String, meta: Meta) {
self.version = version
self.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
public typealias Meta = NoMetadata
public init() {}
public init() {}
public static var none: NoAPIDescription { return .init() }
public static var none: NoAPIDescription { return .init() }
public var description: String { return "No JSON:API Object" }
public var description: String { return "No JSON:API Object" }
}
extension APIDescription {
private enum CodingKeys: String, CodingKey {
case version
case meta
}
private enum CodingKeys: String, CodingKey {
case version
case meta
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
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"
// 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)
}
}
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)
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(version, forKey: .version)
try container.encode(version, forKey: .version)
if Meta.self != NoMetadata.self {
try container.encode(meta, forKey: .meta)
}
}
if Meta.self != NoMetadata.self {
try container.encode(meta, forKey: .meta)
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,11 @@
//
// DocumentDecodingErro.swift
//
//
// Created by Mathew Polzin on 10/20/19.
//
public enum JSONAPIDocumentDecodingError: Swift.Error {
case foundErrorDocumentWhenExpectingSuccess
case foundSuccessDocumentWhenExpectingError
}
-27
View File
@@ -1,27 +0,0 @@
//
// Error.swift
// JSONAPI
//
// Created by Mathew Polzin on 11/10/18.
//
public protocol JSONAPIError: Swift.Error, Equatable, Codable {
static var unknown: Self { get }
}
public enum UnknownJSONAPIError: JSONAPIError {
case unknownError
public init(from decoder: Decoder) throws {
self = .unknown
}
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode("unknown")
}
public static var unknown: UnknownJSONAPIError {
return .unknownError
}
}
+104 -76
View File
@@ -7,71 +7,83 @@
import Poly
public typealias Include = JSONPoly
public typealias Include = EncodableJSONPoly
public struct Includes<I: Include>: Codable, Equatable {
public static var none: Includes { return .init(values: []) }
let values: [I]
public init(values: [I]) {
self.values = values
}
public init(from decoder: Decoder) throws {
var container = try decoder.unkeyedContainer()
// If not parsing includes, no need to loop over them.
guard I.self != NoIncludes.self else {
values = []
return
}
var valueAggregator = [I]()
while !container.isAtEnd {
valueAggregator.append(try container.decode(I.self))
}
values = valueAggregator
}
/// A structure holding zero or more included Resource Objects.
/// The resources are accessed by their type using a subscript.
///
/// If you have
///
/// let includes: Includes<Include2<Thing1, Thing2>> = ...
///
/// then you can access all `Thing1` included resources with
///
/// let includedThings = includes[Thing1.self]
public struct Includes<I: Include>: Encodable, Equatable {
public static var none: Includes { return .init(values: []) }
public func encode(to encoder: Encoder) throws {
var container = encoder.unkeyedContainer()
public let values: [I]
guard I.self != NoIncludes.self else {
throw JSONAPIEncodingError.illegalEncoding("Attempting to encode Include0, which should be represented by the absense of an 'included' entry altogether.")
}
public init(values: [I]) {
self.values = values
}
for value in values {
try container.encode(value)
}
}
public var count: Int {
return values.count
}
public func encode(to encoder: Encoder) throws {
var container = encoder.unkeyedContainer()
guard I.self != NoIncludes.self else {
throw JSONAPIEncodingError.illegalEncoding("Attempting to encode Include0, which should be represented by the absense of an 'included' entry altogether.")
}
for value in values {
try container.encode(value)
}
}
public var count: Int {
return values.count
}
}
extension Includes: Decodable where I: Decodable {
public init(from decoder: Decoder) throws {
var container = try decoder.unkeyedContainer()
// If not parsing includes, no need to loop over them.
guard I.self != NoIncludes.self else {
values = []
return
}
var valueAggregator = [I]()
while !container.isAtEnd {
valueAggregator.append(try container.decode(I.self))
}
values = valueAggregator
}
}
extension Includes {
public func appending(_ other: Includes<I>) -> Includes {
return Includes(values: values + other.values)
}
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)
return left.appending(right)
}
extension Includes: CustomStringConvertible {
public var description: String {
return "Includes(\(String(describing: values))"
}
public var description: String {
return "Includes(\(String(describing: values))"
}
}
extension Includes where I == NoIncludes {
public init() {
values = []
}
public init() {
values = []
}
}
// MARK: - 0 includes
@@ -81,71 +93,87 @@ public typealias NoIncludes = Include0
// MARK: - 1 include
public typealias Include1 = Poly1
extension Includes where I: _Poly1 {
public subscript(_ lookup: I.A.Type) -> [I.A] {
return values.compactMap { $0.a }
}
public subscript(_ lookup: I.A.Type) -> [I.A] {
return values.compactMap { $0.a }
}
}
// MARK: - 2 includes
public typealias Include2 = Poly2
extension Includes where I: _Poly2 {
public subscript(_ lookup: I.B.Type) -> [I.B] {
return values.compactMap { $0.b }
}
public subscript(_ lookup: I.B.Type) -> [I.B] {
return values.compactMap { $0.b }
}
}
// MARK: - 3 includes
public typealias Include3 = Poly3
extension Includes where I: _Poly3 {
public subscript(_ lookup: I.C.Type) -> [I.C] {
return values.compactMap { $0.c }
}
public subscript(_ lookup: I.C.Type) -> [I.C] {
return values.compactMap { $0.c }
}
}
// MARK: - 4 includes
public typealias Include4 = Poly4
extension Includes where I: _Poly4 {
public subscript(_ lookup: I.D.Type) -> [I.D] {
return values.compactMap { $0.d }
}
public subscript(_ lookup: I.D.Type) -> [I.D] {
return values.compactMap { $0.d }
}
}
// MARK: - 5 includes
public typealias Include5 = Poly5
extension Includes where I: _Poly5 {
public subscript(_ lookup: I.E.Type) -> [I.E] {
return values.compactMap { $0.e }
}
public subscript(_ lookup: I.E.Type) -> [I.E] {
return values.compactMap { $0.e }
}
}
// MARK: - 6 includes
public typealias Include6 = Poly6
extension Includes where I: _Poly6 {
public subscript(_ lookup: I.F.Type) -> [I.F] {
return values.compactMap { $0.f }
}
public subscript(_ lookup: I.F.Type) -> [I.F] {
return values.compactMap { $0.f }
}
}
// MARK: - 7 includes
public typealias Include7 = Poly7
extension Includes where I: _Poly7 {
public subscript(_ lookup: I.G.Type) -> [I.G] {
return values.compactMap { $0.g }
}
public subscript(_ lookup: I.G.Type) -> [I.G] {
return values.compactMap { $0.g }
}
}
// MARK: - 8 includes
public typealias Include8 = Poly8
extension Includes where I: _Poly8 {
public subscript(_ lookup: I.H.Type) -> [I.H] {
return values.compactMap { $0.h }
}
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 }
}
public subscript(_ lookup: I.I.Type) -> [I.I] {
return values.compactMap { $0.i }
}
}
// MARK: - 10 includes
public typealias Include10 = Poly10
extension Includes where I: _Poly10 {
public subscript(_ lookup: I.J.Type) -> [I.J] {
return values.compactMap { $0.j }
}
}
// MARK: - 11 includes
public typealias Include11 = Poly11
extension Includes where I: _Poly11 {
public subscript(_ lookup: I.K.Type) -> [I.K] {
return values.compactMap { $0.k }
}
}
+99 -66
View File
@@ -5,110 +5,143 @@
// Created by Mathew Polzin on 11/10/18.
//
public protocol MaybePrimaryResource: Equatable, Codable {}
/// This protocol allows for a `SingleResourceBody` to contain a `null`
/// data object where `ManyResourceBody` cannot (because an empty
/// array should be used for no results).
public protocol OptionalEncodablePrimaryResource: Equatable, Encodable {}
/// A PrimaryResource is a type that can be used in the body of a JSON API
/// An `EncodablePrimaryResource` is a `CodablePrimaryResource` that only supports encoding.
public protocol EncodablePrimaryResource: OptionalEncodablePrimaryResource {}
/// This protocol allows for `SingleResourceBody` to contain a `null`
/// data object where `ManyResourceBody` cannot (because an empty
/// array should be used for no results).
public protocol OptionalCodablePrimaryResource: OptionalEncodablePrimaryResource, Decodable {}
/// A `CodablePrimaryResource` is a type that can be used in the body of a JSON API
/// document as the primary resource.
public protocol PrimaryResource: MaybePrimaryResource {}
public protocol CodablePrimaryResource: EncodablePrimaryResource, OptionalCodablePrimaryResource {}
extension Optional: MaybePrimaryResource where Wrapped: PrimaryResource {}
extension Optional: OptionalEncodablePrimaryResource where Wrapped: EncodablePrimaryResource {}
/// A ResourceBody is a representation of the body of the JSON API Document.
extension Optional: OptionalCodablePrimaryResource where Wrapped: CodablePrimaryResource {}
/// An `EncodableResourceBody` is a `ResourceBody` that only supports being
/// encoded. It is actually weaker than `ResourceBody`, which supports both encoding
/// and decoding.
public protocol EncodableResourceBody: Equatable, Encodable {
associatedtype PrimaryResource
}
/// A `CodableResourceBody` 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 protocol CodableResourceBody: Decodable, EncodableResourceBody {}
/// A `ResourceBody` that has the ability to take on more primary
/// resources by appending another similarly typed `ResourceBody`.
public protocol ResourceBodyAppendable {
func appending(_ other: Self) -> Self
}
public protocol AppendableResourceBody: ResourceBody {
func appending(_ other: Self) -> Self
public func +<R: ResourceBodyAppendable>(_ left: R, right: R) -> R {
return left.appending(right)
}
public func +<R: AppendableResourceBody>(_ left: R, right: R) -> R {
return left.appending(right)
/// A type allowing for a document body containing 1 primary resource.
/// If the `Entity` specialization is an `Optional` type, the body can contain
/// 0 or 1 primary resources.
public struct SingleResourceBody<PrimaryResource: JSONAPI.OptionalEncodablePrimaryResource>: EncodableResourceBody {
public let value: PrimaryResource
public init(resourceObject: PrimaryResource) {
self.value = resourceObject
}
}
public struct SingleResourceBody<Entity: JSONAPI.MaybePrimaryResource>: ResourceBody {
public let value: Entity
/// A type allowing for a document body containing 0 or more primary resources.
public struct ManyResourceBody<PrimaryResource: JSONAPI.EncodablePrimaryResource>: EncodableResourceBody, ResourceBodyAppendable {
public let values: [PrimaryResource]
public init(resourceObject: Entity) {
self.value = resourceObject
}
}
public init(resourceObjects: [PrimaryResource]) {
values = resourceObjects
}
public struct ManyResourceBody<Entity: JSONAPI.PrimaryResource>: AppendableResourceBody {
public let values: [Entity]
public init(resourceObjects: [Entity]) {
values = resourceObjects
}
public func appending(_ other: ManyResourceBody) -> ManyResourceBody {
return ManyResourceBody(resourceObjects: values + other.values)
}
public func appending(_ other: ManyResourceBody) -> ManyResourceBody {
return ManyResourceBody(resourceObjects: values + other.values)
}
}
/// 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() }
public struct NoResourceBody: CodableResourceBody {
public typealias PrimaryResource = Void
public static var none: NoResourceBody { return NoResourceBody() }
}
// MARK: Codable
extension SingleResourceBody {
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
let anyNil: Any? = nil
if container.decodeNil(),
let val = anyNil as? Entity {
value = val
return
}
let anyNil: Any? = nil
let nilValue = anyNil as? PrimaryResource
guard value != nilValue else {
try container.encodeNil()
return
}
value = try container.decode(Entity.self)
}
try container.encode(value)
}
}
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
extension SingleResourceBody: Decodable, CodableResourceBody where PrimaryResource: OptionalCodablePrimaryResource {
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if (value as Any?) == nil {
try container.encodeNil()
return
}
let anyNil: Any? = nil
if container.decodeNil(),
let val = anyNil as? PrimaryResource {
value = val
return
}
try container.encode(value)
}
value = try container.decode(PrimaryResource.self)
}
}
extension ManyResourceBody {
public init(from decoder: Decoder) throws {
var container = try decoder.unkeyedContainer()
var valueAggregator = [Entity]()
while !container.isAtEnd {
valueAggregator.append(try container.decode(Entity.self))
}
values = valueAggregator
}
public func encode(to encoder: Encoder) throws {
var container = encoder.unkeyedContainer()
public func encode(to encoder: Encoder) throws {
var container = encoder.unkeyedContainer()
for value in values {
try container.encode(value)
}
}
}
for value in values {
try container.encode(value)
}
}
extension ManyResourceBody: Decodable, CodableResourceBody where PrimaryResource: CodablePrimaryResource {
public init(from decoder: Decoder) throws {
var container = try decoder.unkeyedContainer()
var valueAggregator = [PrimaryResource]()
while !container.isAtEnd {
valueAggregator.append(try container.decode(PrimaryResource.self))
}
values = valueAggregator
}
}
// MARK: CustomStringConvertible
extension SingleResourceBody: CustomStringConvertible {
public var description: String {
return "PrimaryResourceBody(\(String(describing: value)))"
}
public var description: String {
return "PrimaryResourceBody(\(String(describing: value)))"
}
}
extension ManyResourceBody: CustomStringConvertible {
public var description: String {
return "PrimaryResourceBody(\(String(describing: values)))"
}
public var description: String {
return "PrimaryResourceBody(\(String(describing: values)))"
}
}
+215
View File
@@ -0,0 +1,215 @@
//
// EmptyObjectDecoder.swift
// JSONAPI
//
// Created by Mathew Polzin on 7/2/19.
//
/// `EmptyObjectDecoder` exists internally for the sole purpose of
/// allowing certain fallback logic paths to attempt to create `Decodable`
/// types from empty containers (specifically in a way that is agnostic
/// of any given encoding). In other words, this serves the same purpose
/// as `JSONDecoder().decode(Thing.self, from: "{}".data(using: .utf8)!)`
/// without needing to use a third party or `Foundation` library decoder.
struct EmptyObjectDecoder: Decoder {
var codingPath: [CodingKey] = []
var userInfo: [CodingUserInfoKey : Any] = [:]
func container<Key>(keyedBy type: Key.Type) throws -> KeyedDecodingContainer<Key> where Key : CodingKey {
return KeyedDecodingContainer(EmptyKeyedContainer())
}
func unkeyedContainer() throws -> UnkeyedDecodingContainer {
return EmptyUnkeyedContainer()
}
func singleValueContainer() throws -> SingleValueDecodingContainer {
throw EmptyObjectDecodingError.emptyObjectCannotBeSingleValue
}
}
enum EmptyObjectDecodingError: Swift.Error {
case emptyObjectCannotBeSingleValue
case emptyObjectCannotBeUnkeyedValues
case emptyObjectCannotHaveKeyedValues
case emptyObjectCannotHaveNestedContainers
case emptyObjectCannotHaveSuper
}
struct EmptyUnkeyedContainer: UnkeyedDecodingContainer {
var codingPath: [CodingKey] { return [] }
var count: Int? { return 0 }
var isAtEnd: Bool { return true }
var currentIndex: Int { return 0 }
mutating func decodeNil() throws -> Bool {
throw EmptyObjectDecodingError.emptyObjectCannotBeUnkeyedValues
}
mutating func decode(_ type: Bool.Type) throws -> Bool {
throw EmptyObjectDecodingError.emptyObjectCannotBeUnkeyedValues
}
mutating func decode(_ type: String.Type) throws -> String {
throw EmptyObjectDecodingError.emptyObjectCannotBeUnkeyedValues
}
mutating func decode(_ type: Double.Type) throws -> Double {
throw EmptyObjectDecodingError.emptyObjectCannotBeUnkeyedValues
}
mutating func decode(_ type: Float.Type) throws -> Float {
throw EmptyObjectDecodingError.emptyObjectCannotBeUnkeyedValues
}
mutating func decode(_ type: Int.Type) throws -> Int {
throw EmptyObjectDecodingError.emptyObjectCannotBeUnkeyedValues
}
mutating func decode(_ type: Int8.Type) throws -> Int8 {
throw EmptyObjectDecodingError.emptyObjectCannotBeUnkeyedValues
}
mutating func decode(_ type: Int16.Type) throws -> Int16 {
throw EmptyObjectDecodingError.emptyObjectCannotBeUnkeyedValues
}
mutating func decode(_ type: Int32.Type) throws -> Int32 {
throw EmptyObjectDecodingError.emptyObjectCannotBeUnkeyedValues
}
mutating func decode(_ type: Int64.Type) throws -> Int64 {
throw EmptyObjectDecodingError.emptyObjectCannotBeUnkeyedValues
}
mutating func decode(_ type: UInt.Type) throws -> UInt {
throw EmptyObjectDecodingError.emptyObjectCannotBeUnkeyedValues
}
mutating func decode(_ type: UInt8.Type) throws -> UInt8 {
throw EmptyObjectDecodingError.emptyObjectCannotBeUnkeyedValues
}
mutating func decode(_ type: UInt16.Type) throws -> UInt16 {
throw EmptyObjectDecodingError.emptyObjectCannotBeUnkeyedValues
}
mutating func decode(_ type: UInt32.Type) throws -> UInt32 {
throw EmptyObjectDecodingError.emptyObjectCannotBeUnkeyedValues
}
mutating func decode(_ type: UInt64.Type) throws -> UInt64 {
throw EmptyObjectDecodingError.emptyObjectCannotBeUnkeyedValues
}
mutating func decode<T>(_ type: T.Type) throws -> T where T : Decodable {
throw EmptyObjectDecodingError.emptyObjectCannotBeUnkeyedValues
}
mutating func nestedContainer<NestedKey>(keyedBy type: NestedKey.Type) throws -> KeyedDecodingContainer<NestedKey> where NestedKey : CodingKey {
throw EmptyObjectDecodingError.emptyObjectCannotHaveNestedContainers
}
mutating func nestedUnkeyedContainer() throws -> UnkeyedDecodingContainer {
throw EmptyObjectDecodingError.emptyObjectCannotHaveNestedContainers
}
mutating func superDecoder() throws -> Decoder {
throw EmptyObjectDecodingError.emptyObjectCannotHaveSuper
}
}
struct EmptyKeyedContainer<Key: CodingKey>: KeyedDecodingContainerProtocol {
var codingPath: [CodingKey] { return [] }
var allKeys: [Key] { return [] }
func contains(_ key: Key) -> Bool {
return false
}
func decodeNil(forKey key: Key) throws -> Bool {
throw EmptyObjectDecodingError.emptyObjectCannotHaveKeyedValues
}
func decode(_ type: Bool.Type, forKey key: Key) throws -> Bool {
throw EmptyObjectDecodingError.emptyObjectCannotHaveKeyedValues
}
func decode(_ type: String.Type, forKey key: Key) throws -> String {
throw EmptyObjectDecodingError.emptyObjectCannotHaveKeyedValues
}
func decode(_ type: Double.Type, forKey key: Key) throws -> Double {
throw EmptyObjectDecodingError.emptyObjectCannotHaveKeyedValues
}
func decode(_ type: Float.Type, forKey key: Key) throws -> Float {
throw EmptyObjectDecodingError.emptyObjectCannotHaveKeyedValues
}
func decode(_ type: Int.Type, forKey key: Key) throws -> Int {
throw EmptyObjectDecodingError.emptyObjectCannotHaveKeyedValues
}
func decode(_ type: Int8.Type, forKey key: Key) throws -> Int8 {
throw EmptyObjectDecodingError.emptyObjectCannotHaveKeyedValues
}
func decode(_ type: Int16.Type, forKey key: Key) throws -> Int16 {
throw EmptyObjectDecodingError.emptyObjectCannotHaveKeyedValues
}
func decode(_ type: Int32.Type, forKey key: Key) throws -> Int32 {
throw EmptyObjectDecodingError.emptyObjectCannotHaveKeyedValues
}
func decode(_ type: Int64.Type, forKey key: Key) throws -> Int64 {
throw EmptyObjectDecodingError.emptyObjectCannotHaveKeyedValues
}
func decode(_ type: UInt.Type, forKey key: Key) throws -> UInt {
throw EmptyObjectDecodingError.emptyObjectCannotHaveKeyedValues
}
func decode(_ type: UInt8.Type, forKey key: Key) throws -> UInt8 {
throw EmptyObjectDecodingError.emptyObjectCannotHaveKeyedValues
}
func decode(_ type: UInt16.Type, forKey key: Key) throws -> UInt16 {
throw EmptyObjectDecodingError.emptyObjectCannotHaveKeyedValues
}
func decode(_ type: UInt32.Type, forKey key: Key) throws -> UInt32 {
throw EmptyObjectDecodingError.emptyObjectCannotHaveKeyedValues
}
func decode(_ type: UInt64.Type, forKey key: Key) throws -> UInt64 {
throw EmptyObjectDecodingError.emptyObjectCannotHaveKeyedValues
}
func decode<T>(_ type: T.Type, forKey key: Key) throws -> T where T : Decodable {
throw EmptyObjectDecodingError.emptyObjectCannotHaveKeyedValues
}
func nestedContainer<NestedKey>(keyedBy type: NestedKey.Type, forKey key: Key) throws -> KeyedDecodingContainer<NestedKey> where NestedKey : CodingKey {
throw EmptyObjectDecodingError.emptyObjectCannotHaveNestedContainers
}
func nestedUnkeyedContainer(forKey key: Key) throws -> UnkeyedDecodingContainer {
throw EmptyObjectDecodingError.emptyObjectCannotHaveNestedContainers
}
func superDecoder() throws -> Decoder {
throw EmptyObjectDecodingError.emptyObjectCannotHaveSuper
}
func superDecoder(forKey key: Key) throws -> Decoder {
throw EmptyObjectDecodingError.emptyObjectCannotHaveSuper
}
}
+5 -5
View File
@@ -6,9 +6,9 @@
//
public enum JSONAPIEncodingError: Swift.Error {
case typeMismatch(expected: String, found: String)
case illegalEncoding(String)
case illegalDecoding(String)
case missingOrMalformedMetadata
case missingOrMalformedLinks
case typeMismatch(expected: String, found: String)
case illegalEncoding(String)
case illegalDecoding(String)
case missingOrMalformedMetadata
case missingOrMalformedLinks
}
@@ -0,0 +1,103 @@
//
// BasicJSONAPIError.swift
// JSONAPI
//
// Created by Mathew Polzin on 9/29/19.
//
/// Most of the JSON:API Spec defined Error fields.
public struct BasicJSONAPIErrorPayload<IdType: Codable & Equatable>: Codable, Equatable, ErrorDictType, CustomStringConvertible {
/// a unique identifier for this particular occurrence of the problem
public let id: IdType?
// public let links: Links? // we skip this for now to avoid adding complexity to using this basic type.
/// the HTTP status code applicable to this problem
public let status: String?
/// an application-specific error code
public let code: String?
/// a short, human-readable summary of the problem that SHOULD NOT change from occurrence to occurrence of the problem, except for purposes of localization
public let title: String?
/// a human-readable explanation specific to this occurrence of the problem. Like `title`, this field’s value can be localized
public let detail: String?
/// an object containing references to the source of the error
public let source: Source?
// public let meta: Meta? // we skip this for now to avoid adding complexity to using this basic type
public init(id: IdType? = nil,
status: String? = nil,
code: String? = nil,
title: String? = nil,
detail: String? = nil,
source: Source? = nil) {
self.id = id
self.status = status
self.code = code
self.title = title
self.detail = detail
self.source = source
}
public struct Source: Codable, Equatable {
/// a JSON Pointer [RFC6901] to the associated entity in the request document [e.g. "/data" for a primary data object, or "/data/attributes/title" for a specific attribute].
public let pointer: String?
/// which URI query parameter caused the error
public let parameter: String?
public init(pointer: String? = nil,
parameter: String? = nil) {
self.pointer = pointer
self.parameter = parameter
}
}
public var definedFields: [String: String] {
let keysAndValues = [
id.map { ("id", String(describing: $0)) },
status.map { ("status", $0) },
code.map { ("code", $0) },
title.map { ("title", $0) },
detail.map { ("detail", $0) },
source.flatMap { $0.pointer.map { ("pointer", $0) } },
source.flatMap { $0.parameter.map { ("parameter", $0) } }
].compactMap { $0 }
return Dictionary(uniqueKeysWithValues: keysAndValues)
}
public var description: String {
return definedFields.map { "\($0.key): \($0.value)" }.sorted().joined(separator: ", ")
}
}
/// `BasicJSONAPIError` optionally decodes many possible fields
/// specified by the JSON:API 1.0 Spec. It gives no type-guarantees of what
/// will be non-nil, but could provide good diagnostic information when
/// you do not know what error structure to expect.
///
/// ```
/// Fields:
/// - id
/// - status
/// - code
/// - title
/// - detail
/// - source
/// - pointer
/// - parameter
/// ```
///
/// The JSON:API Spec does not dictate the type of this particular Id field,
/// so you must specify whether to expect, for example, an `Int` or a `String`
/// in the id field.
///
/// Something like `AnyCodable` from *Flight-School* could be
/// a good option if you do not know what to expect. You could also use
/// `Either<Int, String>` (provided by the `Poly` package that is
/// already a dependency of `JSONAPI`).
///
/// - Important: The `definedFields` property will include fields
/// with non-nil values in a flattened way. There will be no `source` key
/// but there will be `pointer` and `parameter` keys (if those values
/// are non-nil).
public typealias BasicJSONAPIError<IdType: Codable & Equatable> = GenericJSONAPIError<BasicJSONAPIErrorPayload<IdType>>
@@ -0,0 +1,74 @@
//
// GenericJSONAPIError.swift
// JSONAPI
//
// Created by Mathew Polzin on 9/29/19.
//
/// `GenericJSONAPIError` can be used to specify whatever error
/// payload you expect to need to parse in responses and handle any
/// other payload structure as `.unknownError`.
public enum GenericJSONAPIError<ErrorPayload: Codable & Equatable>: JSONAPIError, CustomStringConvertible {
case unknownError
case error(ErrorPayload)
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
do {
self = .error(try container.decode(ErrorPayload.self))
} catch {
self = .unknown
}
}
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch self {
case .error(let payload):
try container.encode(payload)
case .unknownError:
try container.encode("unknown")
}
}
public static var unknown: Self {
return .unknownError
}
public var description: String {
switch self {
case .unknownError:
return "unknown error"
case .error(let payload):
return String(describing: payload)
}
}
}
public extension GenericJSONAPIError {
var payload: ErrorPayload? {
switch self {
case .unknownError:
return nil
case .error(let payload):
return payload
}
}
}
public protocol ErrorDictType {
var definedFields: [String: String] { get }
}
extension GenericJSONAPIError: ErrorDictType where ErrorPayload: ErrorDictType {
/// Get a dictionary of all defined fields and their values.
public var definedFields: [String: String] {
switch self {
case .unknownError:
return [:]
case .error(let basicPayload):
return basicPayload.definedFields
}
}
}

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