Compare commits

...

51 Commits

Author SHA1 Message Date
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 99e97aadcc Merge branch 'master' into swift-5.1 2019-08-04 10:34:32 -07:00
Mathew Polzin cf7b68c026 Merge w/ master 2019-08-04 09:02:04 -07:00
Mathew Polzin 4b4ebb4214 merge w/ master 2019-07-30 17:52:42 -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 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
32 changed files with 1156 additions and 1047 deletions
@@ -37,7 +37,7 @@ typealias ToManyRelationship<Entity: Relatable> = JSONAPI.ToManyRelationship<Ent
// JSON:API Documents for this particular API to have Metadata, Links,
// 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, UnknownJSONAPIError>
typealias Document<PrimaryResourceBody: JSONAPI.ResourceBody, IncludeType: JSONAPI.Include> = JSONAPI.Document<PrimaryResourceBody, NoMetadata, NoLinks, IncludeType, NoAPIDescription, BasicJSONAPIError<String>>
// MARK: Entity Definitions
@@ -35,7 +35,7 @@ typealias ThingWithProperties = JSONAPI.ResourceObject<ThingWithPropertiesDescri
//
// 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, UnknownJSONAPIError>
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.
@@ -24,7 +24,7 @@ let dogOwner: Person.Identifier? = dogFromData.flatMap { $0 ~> \.owner }
// 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, UnknownJSONAPIError>
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 }
@@ -55,19 +55,15 @@ 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
@@ -75,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)")
}
+5 -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)
}
}
@@ -139,6 +139,6 @@ 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 BatchPeopleDocument = JSONAPI.Document<ManyResourceBody<Person>, NoMetadata, NoLinks, Include2<Dog, House>, NoAPIDescription, BasicJSONAPIError<String>>
+5 -4
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 = "1.0.0"
spec.name = "MP-JSONAPI"
spec.version = "2.3.0"
spec.summary = "Swift Codable JSON API framework."
# This description is used to generate tags and improve search results.
@@ -131,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": "38051821d7ef49e590e26e819a2fe447e50be9ff",
"version": "2.0.1"
"revision": "b24fd3b41bf3126d4c6dede3708135182172af60",
"version": "2.2.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", .upToNextMajor(from: "2.0.0")),
.package(url: "https://github.com/mattpolzin/Poly.git", .upToNextMajor(from: "2.2.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]
)
+90 -20
View File
@@ -1,21 +1,37 @@
# JSONAPI
[![MIT license](http://img.shields.io/badge/license-MIT-lightgrey.svg)](http://opensource.org/licenses/MIT) [![Swift 5.0](http://img.shields.io/badge/Swift-5.0-blue.svg)](https://swift.org) [![Build Status](https://app.bitrise.io/app/c8295b9589aa401e/status.svg?token=vzcyqWD5bQ4xqQfZsaVzNw&branch=master)](https://app.bitrise.io/app/c8295b9589aa401e)
[![MIT license](http://img.shields.io/badge/license-MIT-lightgrey.svg)](http://opensource.org/licenses/MIT) [![Swift 5.1](http://img.shields.io/badge/Swift-5.1-blue.svg)](https://swift.org) [![Build Status](https://app.bitrise.io/app/c8295b9589aa401e/status.svg?token=vzcyqWD5bQ4xqQfZsaVzNw&branch=master)](https://app.bitrise.io/app/c8295b9589aa401e)
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/
:warning: This library provides well-tested type safety when working with JSON:API 1.0, however the Swift compiler can sometimes have difficulty tracking down small typos when initializing `ResourceObjects`. Once the code is written correctly, it will compile, but tracking down the source of programmer errors can be an annoyance. This is mostly a concern when creating resource objects in-code (servers and test cases must do this). Writing a client that uses this framework to ingest JSON API Compliant API responses is much less painful. :warning:
:warning: This library provides well-tested type safety when working with JSON:API 1.0. However, the Swift compiler can sometimes have difficulty tracking down small typos when initializing `ResourceObjects`. Once the code is written correctly, it will compile, but tracking down the source of programmer errors can be an annoyance. This is mostly a concern when creating resource objects in-code (servers and test cases must do this). Writing a client that uses this framework to ingest JSON API Compliant API responses is much less painful.
## Quick Start
:warning: The following Google Colab examples have correct code, but there appears to be an bug in the branch of the Swift compiler currently being used by the Google Colab Swift notebooks such that the `JSONAPI` package cannot be pulled in and you cannot run the examples in-browser.
### Clientside
- [Basic Example](https://colab.research.google.com/drive/1IS7lRSBGoiW02Vd1nN_rfdDbZvTDj6Te)
- [Compound Example](https://colab.research.google.com/drive/1BdF0Kc7l2ixDfBZEL16FY6palweDszQU)
- [Metadata Example](https://colab.research.google.com/drive/10dEESwiE9I3YoyfzVeOVwOKUTEgLT3qr)
- [Custom Errors Example](https://colab.research.google.com/drive/1TIv6STzlHrkTf_-9Eu8sv8NoaxhZcFZH)
### Serverside
- [GET Example](https://colab.research.google.com/drive/1krbhzSfz8mwkBTQQnKUZJLEtYsJKSfYX)
- [POST Example](https://colab.research.google.com/drive/1z3n70LwRY7vLIgbsMghvnfHA67QiuqpQ)
### Combined
This library works well when used by both the server responsible for serialization and the client responsible for deserialization. Check out the [example](#example) further down in this README.
## Table of Contents
<!-- TOC depthFrom:1 depthTo:6 withLinks:1 updateOnSave:1 orderedList:0 -->
- [JSONAPI](#jsonapi)
- [Table of Contents](#table-of-contents)
- [Primary Goals](#primary-goals)
- [Caveat](#caveat)
- [Dev Environment](#dev-environment)
- [Prerequisites](#prerequisites)
- [Swift Package Manager](#swift-package-manager)
- [CocoaPods](#cocoapods)
- [Xcode project](#xcode-project)
- [Running the Playground](#running-the-playground)
@@ -51,6 +67,9 @@ See the JSON API Spec here: https://jsonapi.org/format/
- [`IncludeType`](#includetype)
- [`APIDescriptionType`](#apidescriptiontype)
- [`Error`](#error)
- [`UnknownJSONAPIError`](#unknownjsonapierror)
- [`BasicJSONAPIError`](#basicjsonapierror)
- [`GenericJSONAPIError`](#genericjsonapierror)
- [`JSONAPI.Meta`](#jsonapimeta)
- [`JSONAPI.Links`](#jsonapilinks)
- [`JSONAPI.RawIdType`](#jsonapirawidtype)
@@ -69,8 +88,6 @@ See the JSON API Spec here: https://jsonapi.org/format/
- [JSONAPI+Arbitrary](#jsonapiarbitrary)
- [JSONAPI+OpenAPI](#jsonapiopenapi)
<!-- /TOC -->
## Primary Goals
The primary goals of this framework are:
@@ -86,20 +103,28 @@ If you find something wrong with this library and it isn't already mentioned und
## Dev Environment
### Prerequisites
1. Swift 4.2+
1. Swift 5.1+
2. Swift Package Manager *OR* Cocoapods
### Swift Package Manager
Just include the following in your package's dependencies and add `JSONAPI` to the dependencies for any of your targets.
```
.package(url: "https://github.com/mattpolzin/JSONAPI.git", .upToNextMajor(from: "2.2.0"))
```
### CocoaPods
To use this framework in your project via Cocoapods instead of Swift Package Manager, add the following dependencies to your Podfile.
To use this framework in your project via Cocoapods, add the following dependencies to your Podfile.
```
pod 'Poly', :git => 'https://github.com/mattpolzin/Poly.git'
pod 'JSONAPI', :git => 'https://github.com/mattpolzin/JSONAPI.git'
pod 'MP-JSONAPI', :git => 'https://github.com/mattpolzin/JSONAPI.git'
```
### Xcode project
To create an Xcode project for JSONAPI, run
`swift package generate-xcodeproj`
With Xcode 11+ you can also just open the folder containing your clone of this repository and begin working.
### Running the Playground
To run the included Playground files, create an Xcode project using Swift Package Manager, then create an Xcode Workspace in the root of the repository and add both the generated Xcode project and the playground to the Workspace.
@@ -298,11 +323,18 @@ A resource object that does not have attributes can be described by adding the f
typealias Attributes = NoAttributes
```
`Attributes` can be accessed via the `subscript` operator of the `ResourceObject` type as follows:
As of Swift 5.1, `Attributes` can be accessed via dynamic member keypath lookup as follows:
```swift
let favoriteColor: String = person.favoriteColor
```
đź—’ `Attributes` can also be accessed via the older `subscript` operator as follows:
```swift
let favoriteColor: String = person[\.favoriteColor]
```
In both cases you retain type-safety. It is best practice to pick an attribute access syntax and stick with it. At some point in the future the syntax deemed less desirable may be deprecated.
#### `Transformer`
Sometimes you need to use a type that does not encode or decode itself in the way you need to represent it as a serialized JSON object. For example, the Swift `Foundation` type `Date` can encode/decode itself to `Double` out of the box, but you might want to represent dates as ISO 8601 compliant `String`s instead. The Foundation library `JSONDecoder` has a setting to make this adjustment, but for the sake of an example, you could create a `Transformer`.
@@ -341,7 +373,7 @@ You can also creator `Validators` and `ValidatedAttribute`s. A `Validator` is ju
#### Computed `Attribute`
You can add computed properties to your `ResourceObjectDescription.Attributes` struct if you would like to expose attributes that are not explicitly represented by the JSON. These computed properties do not have to be wrapped in `Attribute`, `ValidatedAttribute`, or `TransformedAttribute`. This allows computed attributes to be of types that are not `Codable`. Here's an example of how you might take the `person[\.name]` attribute from the example above and create a `fullName` computed property.
You can add computed properties to your `ResourceObjectDescription.Attributes` struct if you would like to expose attributes that are not explicitly represented by the JSON. These computed properties do not have to be wrapped in `Attribute`, `ValidatedAttribute`, or `TransformedAttribute`. This allows computed attributes to be of types that are not `Codable`. Here's an example of how you might take the `person.name` attribute from the example above and create a `fullName` computed property.
```swift
public var fullName: Attribute<String> {
@@ -349,7 +381,7 @@ public var fullName: Attribute<String> {
}
```
If your computed property is wrapped in a `AttributeType` then you can still use the default subscript operator to access it (as would be the case with the `person[\.fullName]` example above). However, if you add a property to the `Attributes` `struct` that is not wrapped in an `AttributeType`, you must either access it from its full path (`person.attributes.newThing`) or with the "direct" subscript accessor (`person[direct: \.newThing]`). This keeps the subscript access unambiguous enough for the compiler to be helpful prior to explicitly casting, comparing, or storing the result.
If your computed property is wrapped in a `AttributeType` then you can still use the default subscript operator to access it (as would be the case with the `person.fullName` example above). However, if you add a property to the `Attributes` `struct` that is not wrapped in an `AttributeType`, you must either access it from its full path (`person.attributes.newThing`) or with the "direct" subscript accessor (`person[direct: \.newThing]`). This keeps the subscript access unambiguous enough for the compiler to be helpful prior to explicitly casting, comparing, or storing the result.
### Copying/Mutating `ResourceObjects`
`ResourceObject` is a value type, so copying is its default behavior. There are two common mutations you might want to make when copying a `ResourceObject`:
@@ -374,7 +406,7 @@ The entirety of a JSON API request or response is encoded or decoded from- or to
```swift
let decoder = JSONDecoder()
let responseStructure = JSONAPI.Document<SingleResourceBody<Person>, NoMetadata, NoLinks, NoIncludes, UnknownJSONAPIError>.self
let responseStructure = JSONAPI.Document<SingleResourceBody<Person>, NoMetadata, NoLinks, NoIncludes, BasicJSONAPIError<String>>.self
let document = try decoder.decode(responseStructure, from: data)
```
@@ -423,7 +455,7 @@ The third generic type of a `JSONAPIDocument` is a `Links` struct. `Links` are d
#### `IncludeType`
The fourth generic type of a `JSONAPIDocument` is an `Include`. This type controls which types of `ResourceObject` are looked for when decoding the "included" part of the JSON API document. If you do not expect any included resource objects to be in the document, `NoIncludes` is the way to go. The `JSONAPI` framework provides `Include`s for up to six types of included resource objects. These are named `Include1`, `Include2`, `Include3`, and so on.
The fourth generic type of a `JSONAPIDocument` is an `Include`. This type controls which types of `ResourceObject` are looked for when decoding the "included" part of the JSON API document. If you do not expect any included resource objects to be in the document, `NoIncludes` is the way to go. The `JSONAPI` framework provides `Include`s for up to 10 types of included resource objects. These are named `Include1`, `Include2`, `Include3`, and so on.
**IMPORTANT**: The number trailing "Include" in these type names does not indicate a number of included resource objects, it indicates a number of _types_ of included resource objects. `Include1` can be used to decode any number of included resource objects as long as all the resource objects are of the same _type_.
@@ -441,7 +473,45 @@ You can supply any `JSONAPI.Meta` type as the metadata type of the API descripti
#### `Error`
The final generic type of a `JSONAPIDocument` is the `Error`. You should create an error type that can decode all the errors you expect your `JSONAPIDocument` to be able to decode. As prescribed by the **SPEC**, these errors will be found in the root document member `errors`.
The final generic type of a `JSONAPIDocument` is the `Error`.
You can either create an error type that can handle all the errors you expect your `JSONAPIDocument` to be able to encode/decode or use an out-of-box error type described here. As prescribed by the **SPEC**, these errors will be found under the root document key `errors`.
##### `UnknownJSONAPIError`
The `UnknownJSONAPIError` type will always succeed in parsing errors but it will not give you any information about what error occurred. You will generally get more bang for your buck out of the next error type described.
##### `BasicJSONAPIError`
The `BasicJSONAPIError` type will always succeed unless it is faced with an `id` field of an unexpected type, although it still "succeeds" in falling back to its `.unknown` case when that happens. This type extracts _most_ of the fields the **SPEC** describes [here](https://jsonapi.org/format/#error-objects). Because all of these fields are optional in the **SPEC**, they are optional on the `BasicJSONAPIError` type. You will have to create your own error type if you want to define certain fields as non-optional or parse metadata or links out of error objects.
đź—’Metadata and links are supported at the Document level for error responses, the are just not supported hanging off of the individual errors in the `errors` array of the response when using this error type.
The `BasicJSONAPIError` type is generic on one thing: The type it expects for the `id` field. If you expect integer `ids` back, you use `BasicJSONAPIError<Int>`. The same can be done for `String` or any other type that is both `Codable` and `Equatable`. You can even employ something like `AnyCodable` from *Flight-School* as your id field type. If you only need to handle a small subset of possible `id` field types, you can also use the `Poly` library that is already a dependency of `JSONAPI`. For example, you might expect a mix of `String` and `Int` ids for some reason: `BasicJSONAPIError<Either<Int, String>>`.
The two easiest ways to access the available properties of an error response are under the `payload` property of the error (this property is `nil` if the error was parsed as `.unknown`) or by asking the error for its `definedFields` dictionary.
As an example, let's say you have the following `Document` type that is destined for errors:
```swift
typealias ErrorDoc = JSONAPI.Document<NoResourceBody, NoMetadata, NoLinks, NoIncludes, NoAPIDescription, BasicJSONAPIError<String>>
```
And you've parsed an error response
```swift
let errorResponse = try! JSONDecoder().decode(ErrorDoc.self, from: mockErrorData)
```
You can get at the `Document` body and errors in a couple of different ways, but for one you can switch on the body:
```swift
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)")
}
```
##### `GenericJSONAPIError`
This type makes it simple to use your own error payload structures as `JSONAPIError` types. Simply define a `Codable` and `Equatable` struct and then use `GenericJSONAPIError<YourType>` as the error type for a `Document`.
### `JSONAPI.Meta`
@@ -491,12 +561,12 @@ There is a sparse fieldsets example included with this repository as a Playgroun
#### Sparse Fieldset `typealias` comparisons
You might have found a `typealias` like the following for encoding/decoding `JSONAPI.Document`s (note the primary resource body is a `JSONAPI.ResourceBody`):
```swift
typealias Document<PrimaryResourceBody: JSONAPI.ResourceBody, IncludeType: JSONAPI.Include> = JSONAPI.Document<PrimaryResourceBody, NoMetadata, NoLinks, IncludeType, NoAPIDescription, UnknownJSONAPIError>
typealias Document<PrimaryResourceBody: JSONAPI.ResourceBody, IncludeType: JSONAPI.Include> = JSONAPI.Document<PrimaryResourceBody, NoMetadata, NoLinks, IncludeType, NoAPIDescription, BasicJSONAPIError<String>>
```
In order to support sparse fieldsets (which are encode-only), the following companion `typealias` would be useful (note the primary resource body is a `JSONAPI.EncodableResourceBody`):
```swift
typealias SparseDocument<PrimaryResourceBody: JSONAPI.EncodableResourceBody, IncludeType: JSONAPI.Include> = JSONAPI.Document<PrimaryResourceBody, NoMetadata, NoLinks, IncludeType, NoAPIDescription, UnknownJSONAPIError>
typealias SparseDocument<PrimaryResourceBody: JSONAPI.EncodableResourceBody, IncludeType: JSONAPI.Include> = JSONAPI.Document<PrimaryResourceBody, NoMetadata, NoLinks, IncludeType, NoAPIDescription, BasicJSONAPIError<String>>
```
### Custom Attribute or Relationship Key Mapping
@@ -611,7 +681,7 @@ typealias User = JSONAPI.ResourceObject<UserDescription, NoMetadata, NoLinks, St
Given a value `user` of the above resource object type, you can access the `createdAt` attribute just like you would any other:
```swift
let createdAt = user[\.createdAt]
let createdAt = user.createdAt
```
This works because `createdAt` is defined in the form: `var {name}: ({ResourceObject}) -> {Value}` where `{ResourceObject}` is the `JSONAPI.ResourceObject` described by the `ResourceObjectDescription` containing the meta-attribute.
@@ -632,7 +702,7 @@ enum UserDescription: ResourceObjectDescription {
struct Relationships: JSONAPI.Relationships {
public var friend: (User) -> User.Identifier {
return { user in
return User.Identifier(rawValue: user[\.friend_id])
return User.Identifier(rawValue: user.friend_id)
}
}
}
@@ -684,7 +754,7 @@ typealias ToManyRelationship<Entity: Relatable> = JSONAPI.ToManyRelationship<Ent
// JSON:API Documents for this particular API to have Metadata, Links,
// 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, UnknownJSONAPIError>
typealias Document<PrimaryResourceBody: JSONAPI.ResourceBody, IncludeType: JSONAPI.Include> = JSONAPI.Document<PrimaryResourceBody, NoMetadata, NoLinks, IncludeType, NoAPIDescription, BasicJSONAPIError<String>>
// MARK: Entity Definitions
+16
View File
@@ -161,3 +161,19 @@ extension Includes where I: _Poly9 {
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 }
}
}
@@ -0,0 +1,96 @@
//
// 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 {
/// 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)
}
}
/// `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,65 @@
//
// 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 {
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 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
}
}
}
@@ -1,5 +1,5 @@
//
// Error.swift
// JSONAPIError.swift
// JSONAPI
//
// Created by Mathew Polzin on 11/10/18.
@@ -11,7 +11,10 @@ public protocol JSONAPIError: Swift.Error, Equatable, Codable {
/// `UnknownJSONAPIError` can actually be used in any sitaution
/// where you don't know what errors are possible _or_ you just don't
/// care what errors might show up.
/// care what errors might show up. If you don't know how the error
/// will be structured but you would like to have access to more
/// information the server might be providing in the error payload,
/// use `BasicJSONAPIError` instead.
public enum UnknownJSONAPIError: JSONAPIError {
case unknownError
@@ -24,7 +27,7 @@ public enum UnknownJSONAPIError: JSONAPIError {
try container.encode("unknown")
}
public static var unknown: UnknownJSONAPIError {
public static var unknown: Self {
return .unknownError
}
}
+2 -2
View File
@@ -17,9 +17,9 @@ public protocol AttributeType: Codable {
/// 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 {
public let rawValue: RawValue
public let value: Transformer.To
public init(rawValue: RawValue) throws {
self.rawValue = rawValue
value = try Transformer.transform(rawValue)
@@ -74,3 +74,13 @@ extension Poly8: PrimaryResource, OptionalPrimaryResource where A: PolyWrapped,
extension Poly9: EncodablePrimaryResource, OptionalEncodablePrimaryResource where A: EncodablePolyWrapped, B: EncodablePolyWrapped, C: EncodablePolyWrapped, D: EncodablePolyWrapped, E: EncodablePolyWrapped, F: EncodablePolyWrapped, G: EncodablePolyWrapped, H: EncodablePolyWrapped, I: EncodablePolyWrapped {}
extension Poly9: PrimaryResource, OptionalPrimaryResource where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped, E: PolyWrapped, F: PolyWrapped, G: PolyWrapped, H: PolyWrapped, I: PolyWrapped {}
// MARK: - 10 types
extension Poly10: EncodablePrimaryResource, OptionalEncodablePrimaryResource where A: EncodablePolyWrapped, B: EncodablePolyWrapped, C: EncodablePolyWrapped, D: EncodablePolyWrapped, E: EncodablePolyWrapped, F: EncodablePolyWrapped, G: EncodablePolyWrapped, H: EncodablePolyWrapped, I: EncodablePolyWrapped, J: EncodablePolyWrapped {}
extension Poly10: PrimaryResource, OptionalPrimaryResource where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped, E: PolyWrapped, F: PolyWrapped, G: PolyWrapped, H: PolyWrapped, I: PolyWrapped, J: PolyWrapped {}
// MARK: - 11 types
extension Poly11: EncodablePrimaryResource, OptionalEncodablePrimaryResource where A: EncodablePolyWrapped, B: EncodablePolyWrapped, C: EncodablePolyWrapped, D: EncodablePolyWrapped, E: EncodablePolyWrapped, F: EncodablePolyWrapped, G: EncodablePolyWrapped, H: EncodablePolyWrapped, I: EncodablePolyWrapped, J: EncodablePolyWrapped, K: EncodablePolyWrapped {}
extension Poly11: PrimaryResource, OptionalPrimaryResource where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped, E: PolyWrapped, F: PolyWrapped, G: PolyWrapped, H: PolyWrapped, I: PolyWrapped, J: PolyWrapped, K: PolyWrapped {}
+46 -13
View File
@@ -66,8 +66,9 @@ public protocol ResourceObjectProxyDescription: JSONTyped {
public protocol ResourceObjectDescription: ResourceObjectProxyDescription where Attributes: JSONAPI.Attributes, Relationships: JSONAPI.Relationships {}
/// ResourceObjectProxy is a protocol that can be used to create
/// types that _act_ like Entities but cannot be encoded
/// or decoded as Entities.
/// types that _act_ like ResourceObject but cannot be encoded
/// or decoded as ResourceObjects.
@dynamicMemberLookup
public protocol ResourceObjectProxy: Equatable, JSONTyped {
associatedtype Description: ResourceObjectProxyDescription
associatedtype EntityRawIdType: JSONAPI.MaybeRawId
@@ -91,7 +92,7 @@ public protocol ResourceObjectProxy: Equatable, JSONTyped {
}
extension ResourceObjectProxy {
/// The JSON API compliant "type" of this `Entity`.
/// The JSON API compliant "type" of this `ResourceObject`.
public static var jsonType: String { return Description.jsonType }
}
@@ -151,7 +152,7 @@ extension ResourceObject: CustomStringConvertible {
}
}
// MARK: Convenience initializers
// MARK: - Convenience initializers
extension ResourceObject where EntityRawIdType: CreatableRawIdType {
public init(attributes: Description.Attributes, relationships: Description.Relationships, meta: MetaType, links: LinksType) {
self.id = ResourceObject.Id()
@@ -402,7 +403,7 @@ extension ResourceObject where MetaType == NoMetadata, LinksType == NoLinks, Ent
}
*/
// MARK: Pointer for Relationships use.
// MARK: - Pointer for Relationships use
public extension ResourceObject where EntityRawIdType: JSONAPI.RawIdType {
/// A `ResourceObject.Pointer` is a `ToOneRelationship` with no metadata or links.
@@ -428,7 +429,7 @@ public extension ResourceObject where EntityRawIdType: JSONAPI.RawIdType {
}
}
// MARK: Identifying Unidentified Entities
// MARK: - Identifying Unidentified Entities
public extension ResourceObject where EntityRawIdType == Unidentified {
/// Create a new `ResourceObject` from this one with a newly created
/// unique Id of the given type.
@@ -449,31 +450,55 @@ public extension ResourceObject where EntityRawIdType: CreatableRawIdType {
}
}
// MARK: Attribute Access
// MARK: - Attribute Access
public extension ResourceObjectProxy {
// MARK: Keypath Subscript Lookup
/// Access the attribute at the given keypath. This just
/// allows you to write `resourceObject[\.propertyName]` instead
/// of `resourceObject.attributes.propertyName`.
/// of `resourceObject.attributes.propertyName.value`.
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 `resourceObject[\.propertyName]` instead
/// of `resourceObject.attributes.propertyName`.
/// of `resourceObject.attributes.propertyName.value`.
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 `resourceObject[\.propertyName]` instead
/// of `resourceObject.attributes.propertyName`.
/// of `resourceObject.attributes.propertyName.value`.
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 }
}
// MARK: Dynaminc Member Keypath Lookup
/// Access the attribute at the given keypath. This just
/// allows you to write `resourceObject[\.propertyName]` instead
/// of `resourceObject.attributes.propertyName.value`.
subscript<T: AttributeType>(dynamicMember 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 `resourceObject[\.propertyName]` instead
/// of `resourceObject.attributes.propertyName.value`.
subscript<T: AttributeType>(dynamicMember 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 `resourceObject[\.propertyName]` instead
/// of `resourceObject.attributes.propertyName.value`.
subscript<T: AttributeType, U>(dynamicMember path: KeyPath<Description.Attributes, T?>) -> U? where T.ValueType == U? {
return attributes[keyPath: path].flatMap { $0.value }
}
// MARK: Direct Keypath Subscript Lookup
/// Access the storage of the attribute at the given keypath. This just
/// allows you to write `resourceObject[direct: \.propertyName]` instead
/// of `resourceObject.attributes.propertyName`.
@@ -487,16 +512,24 @@ public extension ResourceObjectProxy {
}
}
// MARK: Meta-Attribute Access
// MARK: - Meta-Attribute Access
public extension ResourceObjectProxy {
// MARK: Keypath Subscript Lookup
/// 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: Dynamic Member Keypath Lookup
/// Access an attribute requiring a transformation on the RawValue _and_
/// a secondary transformation on this entity (self).
subscript<T>(dynamicMember path: KeyPath<Description.Attributes, (Self) -> T>) -> T {
return attributes[keyPath: path](self)
}
}
// MARK: Relationship Access
// MARK: - Relationship Access
public extension ResourceObjectProxy {
/// Access to an Id of a `ToOneRelationship`.
/// This allows you to write `resourceObject ~> \.other` instead
@@ -538,7 +571,7 @@ public extension ResourceObjectProxy {
}
}
// MARK: Meta-Relationship Access
// MARK: - Meta-Relationship Access
public extension ResourceObjectProxy {
/// Access to an Id of a `ToOneRelationship`.
/// This allows you to write `resourceObject ~> \.other` instead
@@ -16,6 +16,7 @@ class Attribute_FunctorTests: XCTestCase {
XCTAssertNotNil(entity)
XCTAssertEqual(entity?[\.computedString], "Frankie2")
XCTAssertEqual(entity?.computedString, "Frankie2")
}
func test_mapOptionalSuccess() {
@@ -24,6 +25,7 @@ class Attribute_FunctorTests: XCTestCase {
XCTAssertNotNil(entity)
XCTAssertEqual(entity?[\.computedNumber], 22)
XCTAssertEqual(entity?.computedNumber, 22)
}
func test_mapOptionalFailure() {
@@ -32,6 +34,7 @@ class Attribute_FunctorTests: XCTestCase {
XCTAssertNotNil(entity)
XCTAssertNil(entity?[\.computedNumber])
XCTAssertNil(entity?.computedNumber)
}
}
@@ -77,12 +77,37 @@ extension AttributeTests {
}
}
enum IntToString: Transformer {
enum IntToString: ReversibleTransformer {
public static func transform(_ from: Int) -> String {
return String(from)
}
public static func reverse(_ value: String) throws -> Int {
guard let intValue = Int(value) else {
fatalError("Reversed IntToString with invalid String value.")
}
return intValue
}
}
enum OptionalIntToOptionalString: ReversibleTransformer {
public static func transform(_ from: Int?) -> String? {
return from.map(String.init)
}
public static func reverse(_ value: String?) throws -> Int? {
guard let stringValue = value else {
return nil
}
guard let intValue = Int(stringValue) else {
fatalError("Reversed IntToString with invalid String value.")
}
return intValue
}
}
enum IntToInt: Transformer {
public static func transform(_ from: Int) -> Int {
return from + 100
@@ -15,6 +15,7 @@ class ComputedPropertiesTests: XCTestCase {
XCTAssertEqual(entity.id, "1234")
XCTAssertEqual(entity[\.name], "Sarah")
XCTAssertEqual(entity.name, "Sarah")
XCTAssertEqual(entity ~> \.other, "5678")
XCTAssertNoThrow(try TestType.check(entity))
}
@@ -27,6 +28,7 @@ class ComputedPropertiesTests: XCTestCase {
let entity = decoded(type: TestType.self, data: computed_property_attribute)
XCTAssertEqual(entity[\.computed], "Sarah2")
XCTAssertEqual(entity.computed, "Sarah2")
XCTAssertEqual(entity[direct: \.directSecretsOut], "shhhh")
}
@@ -14,7 +14,9 @@ class CustomAttributesTests: XCTestCase {
let entity = decoded(type: CustomAttributeEntity.self, data: customAttributeEntityData)
XCTAssertEqual(entity[\.firstName], "Cool")
XCTAssertEqual(entity.firstName, "Cool")
XCTAssertEqual(entity[\.name], "Cool Name")
XCTAssertEqual(entity.name, "Cool Name")
XCTAssertNoThrow(try CustomAttributeEntity.check(entity))
}
@@ -27,7 +29,9 @@ class CustomAttributesTests: XCTestCase {
let entity = decoded(type: CustomKeysEntity.self, data: customAttributeEntityData)
XCTAssertEqual(entity[\.firstNameSilly], "Cool")
XCTAssertEqual(entity.firstNameSilly, "Cool")
XCTAssertEqual(entity[\.lastNameSilly], "Name")
XCTAssertEqual(entity.lastNameSilly, "Name")
XCTAssertNoThrow(try CustomKeysEntity.check(entity))
}
@@ -0,0 +1,131 @@
//
// BasicJSONAPIErrorTests.swift
// JSONAPITests
//
// Created by Mathew Polzin on 9/29/19.
//
import Foundation
import JSONAPI
import XCTest
import Poly
final class BasicJSONAPIErrorTests: XCTestCase {
func test_initAndEquality() {
let unknown1 = BasicJSONAPIError<String>.unknown
let unknown2 = BasicJSONAPIError<String>.unknownError
XCTAssertEqual(unknown1, unknown2)
let unknown3 = BasicJSONAPIError<Int>.unknownError
XCTAssertEqual(unknown3, .unknown)
let _ = BasicJSONAPIError<Int>.error(.init(id: nil,
status: nil,
code: nil,
title: nil,
detail: nil,
source: nil))
let _ = BasicJSONAPIError<String>.error(.init(id: nil,
status: nil,
code: nil,
title: nil,
detail: nil,
source: nil))
let intError = BasicJSONAPIError<Int>.error(.init(id: 2,
status: nil,
code: nil,
title: nil,
detail: nil,
source: nil))
XCTAssertEqual(intError.payload?.id, 2)
XCTAssertNotEqual(intError, unknown3)
let stringError = BasicJSONAPIError<String>.error(.init(id: "hello",
status: nil,
code: nil,
title: nil,
detail: nil,
source: nil))
XCTAssertEqual(stringError.payload?.id, "hello")
XCTAssertNotEqual(stringError, unknown1)
let wellPopulatedError = BasicJSONAPIError<Int>.error(.init(id: 10,
status: "404",
code: "12",
title: "Missing",
detail: "Resource was not found",
source: .init(pointer: "/data/attributes/id", parameter: "id")))
XCTAssertEqual(wellPopulatedError.payload?.id, 10)
XCTAssertEqual(wellPopulatedError.payload?.status, "404")
XCTAssertEqual(wellPopulatedError.payload?.code, "12")
XCTAssertEqual(wellPopulatedError.payload?.title, "Missing")
XCTAssertEqual(wellPopulatedError.payload?.detail, "Resource was not found")
XCTAssertEqual(wellPopulatedError.payload?.source?.pointer, "/data/attributes/id")
XCTAssertEqual(wellPopulatedError.payload?.source?.parameter, "id")
XCTAssertNotEqual(wellPopulatedError, intError)
}
func test_definedFields() {
let unpopulatedError = BasicJSONAPIError<Int>.error(.init(id: nil,
status: nil,
code: nil,
title: nil,
detail: nil,
source: nil))
XCTAssertEqual(unpopulatedError.definedFields.count, 0)
let wellPopulatedError = BasicJSONAPIError<Int>.error(.init(id: 10,
status: "404",
code: "12",
title: "Missing",
detail: "Resource was not found",
source: .init(pointer: "/data/attributes/id", parameter: "id")))
XCTAssertEqual(wellPopulatedError.definedFields.count, 7)
XCTAssertEqual(wellPopulatedError.definedFields["id"], "10")
XCTAssertEqual(wellPopulatedError.definedFields["status"], "404")
XCTAssertEqual(wellPopulatedError.definedFields["code"], "12")
XCTAssertEqual(wellPopulatedError.definedFields["title"], "Missing")
XCTAssertEqual(wellPopulatedError.definedFields["detail"], "Resource was not found")
XCTAssertEqual(wellPopulatedError.definedFields["pointer"], "/data/attributes/id")
XCTAssertEqual(wellPopulatedError.definedFields["parameter"], "id")
}
func test_decodeAFewExamples() {
let datas = [
"""
{
"id": "hello"
}
""",
"""
{
"id": 1234
}
""",
"""
{
"status": "404",
"title": "Missing",
"links": {
"about": "https://google.com"
}
}
""",
"""
{
"status": 404
}
"""
].map { $0.data(using: .utf8)! }
let errors = datas
.map { decoded(type: BasicJSONAPIError<Either<Int, String>>.self, data: $0) }
XCTAssertEqual(errors[0].payload?.id, .init("hello"))
XCTAssertEqual(errors[1].payload?.id, .init(1234))
XCTAssertEqual(errors[2].payload?.status, "404")
XCTAssertEqual(errors[2].payload?.title, "Missing")
XCTAssertEqual(errors[3], .unknown)
}
}

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