Skip to content

Commit b1e4568

Browse files
authored
Merge branch 'master' into update-podspec-for-v-4
2 parents 03136bf + c703bff commit b1e4568

File tree

5 files changed

+364
-284
lines changed

5 files changed

+364
-284
lines changed

JSONAPI.playground/Pages/Full Client & Server Example.xcplaygroundpage/Contents.swift

Lines changed: 45 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -72,50 +72,62 @@ typealias Article = JSONEntity<ArticleDescription>
7272

7373
// We create a typealias to represent a document containing one Article
7474
// and including its Author
75-
typealias SingleArticleDocumentWithIncludes = Document<SingleResourceBody<Article>, Include1<Author>>
75+
typealias SingleArticleDocument = Document<SingleResourceBody<Article>, Include1<Author>>
7676

77-
// ... and a typealias to represent a document containing one Article and
78-
// not including any related entities.
79-
typealias SingleArticleDocument = Document<SingleResourceBody<Article>, NoIncludes>
77+
// ... and a typealias to represent a batch document containing any number of Articles
78+
typealias ManyArticleDocument = Document<ManyResourceBody<Article>, Include1<Author>>
8079

8180
// MARK: - Server Pseudo-example
8281

8382
// Skipping over all the API and database stuff, here's a chunk of code
8483
// that creates a document. Note that this document is the entirety
8584
// of a JSON:API response body.
86-
func articleDocument(includeAuthor: Bool) -> Either<SingleArticleDocument, SingleArticleDocumentWithIncludes> {
85+
func article(includeAuthor: Bool) -> CompoundResource<Article, SingleArticleDocument.Include> {
8786
// Let's pretend all of this is coming from a database:
8887

8988
let authorId = Author.Id(rawValue: "1234")
9089

91-
let article = Article(id: .init(rawValue: "5678"),
92-
attributes: .init(title: .init(value: "JSON:API in Swift"),
93-
abstract: .init(value: "Not yet written")),
94-
relationships: .init(author: .init(id: authorId)),
95-
meta: .none,
96-
links: .none)
97-
98-
let document = SingleArticleDocument(apiDescription: .none,
99-
body: .init(resourceObject: article),
100-
includes: .none,
101-
meta: .none,
102-
links: .none)
103-
104-
switch includeAuthor {
105-
case false:
106-
return .init(document)
107-
108-
case true:
109-
let author = Author(id: authorId,
110-
attributes: .init(name: .init(value: "Janice Bluff")),
111-
relationships: .none,
112-
meta: .none,
113-
links: .none)
114-
115-
let includes: Includes<SingleArticleDocumentWithIncludes.Include> = .init(values: [.init(author)])
116-
117-
return .init(document.including(includes))
90+
let article = Article(
91+
id: .init(rawValue: "5678"),
92+
attributes: .init(
93+
title: .init(value: "JSON:API in Swift"),
94+
abstract: .init(value: "Not yet written")
95+
),
96+
relationships: .init(author: .init(id: authorId)),
97+
meta: .none,
98+
links: .none
99+
)
100+
101+
let authorInclude: SingleArticleDocument.Include?
102+
if includeAuthor {
103+
let author = Author(
104+
id: authorId,
105+
attributes: .init(name: .init(value: "Janice Bluff")),
106+
relationships: .none,
107+
meta: .none,
108+
links: .none
109+
)
110+
authorInclude = .init(author)
111+
} else {
112+
authorInclude = nil
118113
}
114+
115+
return CompoundResource(
116+
primary: article,
117+
relatives: authorInclude.map { [$0] } ?? []
118+
)
119+
}
120+
121+
func articleDocument(includeAuthor: Bool) -> SingleArticleDocument {
122+
123+
let compoundResource = article(includeAuthor: includeAuthor)
124+
125+
return SingleArticleDocument(
126+
apiDescription: .none,
127+
resource: compoundResource,
128+
meta: .none,
129+
links: .none
130+
)
119131
}
120132

121133
let encoder = JSONEncoder()
@@ -151,7 +163,7 @@ func docode(articleResponseData: Data) throws -> (article: Article, author: Auth
151163
let decoder = JSONDecoder()
152164
decoder.keyDecodingStrategy = .convertFromSnakeCase
153165

154-
let articleDocument = try decoder.decode(SingleArticleDocumentWithIncludes.self, from: articleResponseData)
166+
let articleDocument = try decoder.decode(SingleArticleDocument.self, from: articleResponseData)
155167

156168
switch articleDocument.body {
157169
case .data(let data):

README.md

Lines changed: 11 additions & 235 deletions
Original file line numberDiff line numberDiff line change
@@ -5,25 +5,24 @@ A Swift package for encoding to- and decoding from **JSON API** compliant reques
55

66
See the JSON API Spec here: https://jsonapi.org/format/
77

8-
: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`. Correct code will always compile, but tracking down the source of programmer errors can be an annoyance. This is mostly a concern when creating resource objects in-code (i.e. declaratively) like you might for unit testing. Writing a client that uses this framework to ingest and decode JSON API Compliant API responses is much less painful.
9-
108
## Quick Start
119

12-
:warning: The following Google Colab examples have correct code, but from time to time the Google Colab Swift compiler may be buggy and produce incorrect or erroneous results. Just keep that in mind if you run the code as you read through the Colab examples.
10+
:warning: The following Google Colab examples have correct code, but from time to time the Google Colab Swift compiler may be buggy and claim it cannot build the JSONAPI library.
1311

1412
### Clientside
1513
- [Basic Example](https://colab.research.google.com/drive/1IS7lRSBGoiW02Vd1nN_rfdDbZvTDj6Te)
1614
- [Compound Example](https://colab.research.google.com/drive/1BdF0Kc7l2ixDfBZEL16FY6palweDszQU)
1715
- [Metadata Example](https://colab.research.google.com/drive/10dEESwiE9I3YoyfzVeOVwOKUTEgLT3qr)
1816
- [Custom Errors Example](https://colab.research.google.com/drive/1TIv6STzlHrkTf_-9Eu8sv8NoaxhZcFZH)
1917
- [PATCH Example](https://colab.research.google.com/drive/16KY-0BoLQKiSUh9G7nYmHzB8b2vhXA2U)
18+
- [Resource Storage Example](https://colab.research.google.com/drive/196eCnBlf2xz8pT4lW--ur6eWSVAjpF6b?usp=sharing)
2019

2120
### Serverside
2221
- [GET Example](https://colab.research.google.com/drive/1krbhzSfz8mwkBTQQnKUZJLEtYsJKSfYX)
2322
- [POST Example](https://colab.research.google.com/drive/1z3n70LwRY7vLIgbsMghvnfHA67QiuqpQ)
2423

2524
### Client+Server
26-
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.
25+
This library works well when used by both the server responsible for serialization and the client responsible for deserialization. Check out the [example](./documentation/client-server-example.md).
2726

2827
## Table of Contents
2928
- JSONAPI
@@ -34,8 +33,8 @@ This library works well when used by both the server responsible for serializati
3433
- [Xcode project](#xcode-project)
3534
- [CocoaPods](#cocoapods)
3635
- [Running the Playground](#running-the-playground)
37-
- [Project Status](#project-status)
38-
- [Example](#example)
36+
- [Project Status](./documentation/project-status.md)
37+
- [Server & Client Example](./documentation/client-server-example.md)
3938
- [Usage](./documentation/usage.md)
4039
- [JSONAPI+Testing](#jsonapitesting)
4140
- [Literal Expressibility](#literal-expressibility)
@@ -67,14 +66,11 @@ If you find something wrong with this library and it isn't already mentioned und
6766
### Swift Package Manager
6867
Just include the following in your package's dependencies and add `JSONAPI` to the dependencies for any of your targets.
6968
```swift
70-
.package(url: "https://github.com/mattpolzin/JSONAPI", from: "4.0.0-alpha.1")
69+
.package(url: "https://github.com/mattpolzin/JSONAPI", from: "4.0.0")
7170
```
7271

7372
### Xcode project
74-
To create an Xcode project for JSONAPI, run
75-
`swift package generate-xcodeproj`
76-
77-
With Xcode 11+ you can also just open the folder containing your clone of this repository and begin working.
73+
With Xcode 11+, you can open the folder containing this repository. There is no need for an Xcode project, but you can generate one with `swift package generate-xcodeproj`.
7874

7975
### CocoaPods
8076
To use this framework in your project via Cocoapods, add the following dependencies to your Podfile.
@@ -86,232 +82,12 @@ pod 'MP-JSONAPI', :git => 'https://github.com/mattpolzin/JSONAPI.git'
8682
### Running the Playground
8783
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.
8884

89-
Note that Playground support for importing non-system Frameworks is still a bit touchy as of Swift 4.2. Sometimes building, cleaning and building, or commenting out and then uncommenting import statements (especially in the Entities.swift Playground Source file) can get things working for me when I am getting an error about `JSONAPI` not being found.
90-
91-
## Project Status
92-
93-
### JSON:API
94-
#### Document
95-
- [x] `data`
96-
- [x] `included`
97-
- [x] `errors`
98-
- [x] `meta`
99-
- [x] `jsonapi` (i.e. API Information)
100-
- [x] `links`
101-
102-
#### Resource Object
103-
- [x] `id`
104-
- [x] `type`
105-
- [x] `attributes`
106-
- [x] `relationships`
107-
- [x] `links`
108-
- [x] `meta`
109-
110-
#### Relationship Object
111-
- [x] `data`
112-
- [x] `links`
113-
- [x] `meta`
114-
115-
#### Links Object
116-
- [x] `href`
117-
- [x] `meta`
118-
119-
### Misc
120-
- [x] Support transforms on `Attributes` values (e.g. to support different representations of `Date`)
121-
- [x] Support validation on `Attributes`.
122-
- [x] Support sparse fieldsets (encoding only). A client can likely just define a new model to represent a sparse population of another model in a very specific use case for decoding purposes. On the server side, sparse fieldsets of Resource Objects can be encoded without creating one model for every possible sparse fieldset.
123-
124-
### Testing
125-
#### Resource Object Validator
126-
- [x] Disallow optional array in `Attribute` (should be empty array, not `null`).
127-
- [x] Only allow `TransformedAttribute` and its derivatives as stored properties within `Attributes` struct. Computed properties can still be any type because they do not get encoded or decoded.
128-
- [x] Only allow `MetaRelationship`, `ToManyRelationship` and `ToOneRelationship` within `Relationships` struct.
129-
130-
### Potential Improvements
131-
These ideas could be implemented in future versions.
132-
133-
- [ ] (Maybe) Use `KeyPath` to specify `Includes` thus creating type safety around the relationship between a primary resource type and the types of included resources.
134-
- [ ] (Maybe) Replace `SingleResourceBody` and `ManyResourceBody` with support at the `Document` level to just interpret `PrimaryResource`, `PrimaryResource?`, or `[PrimaryResource]` as the same decoding/encoding strategies.
135-
- [ ] Support sideposting. JSONAPI spec might become opinionated in the future (https://github.com/json-api/json-api/pull/1197, https://github.com/json-api/json-api/issues/1215, https://github.com/json-api/json-api/issues/1216) but there is also an existing implementation to consider (https://jsonapi-suite.github.io/jsonapi_suite/ruby/writes/nested-writes). At this time, any sidepost implementation would be an awesome tertiary library to be used alongside the primary JSONAPI library. Maybe `JSONAPISideloading`.
136-
- [ ] Error or warning if an included resource object is not related to a primary resource object or another included resource object (Turned off or at least not throwing by default).
137-
138-
## Example
139-
The following serves as a sort of pseudo-example. It skips server/client implementation details not related to JSON:API but still gives a more complete picture of what an implementation using this framework might look like. You can play with this example code in the Playground provided with this repo.
140-
141-
### Preamble (Setup shared by server and client)
142-
```swift
143-
// Make String a CreatableRawIdType.
144-
var globalStringId: Int = 0
145-
extension String: CreatableRawIdType {
146-
public static func unique() -> String {
147-
globalStringId += 1
148-
return String(globalStringId)
149-
}
150-
}
151-
152-
// Create a typealias because we do not expect JSON:API Resource
153-
// Objects for this particular API to have Metadata or Links associated
154-
// with them. We also expect them to have String Ids.
155-
typealias JSONEntity<Description: ResourceObjectDescription> = JSONAPI.ResourceObject<Description, NoMetadata, NoLinks, String>
156-
157-
// Similarly, create a typealias for unidentified entities. JSON:API
158-
// only allows unidentified entities (i.e. no "id" field) for client
159-
// requests that create new entities. In these situations, the server
160-
// is expected to assign the new entity a unique ID.
161-
typealias UnidentifiedJSONEntity<Description: ResourceObjectDescription> = JSONAPI.ResourceObject<Description, NoMetadata, NoLinks, Unidentified>
162-
163-
// Create relationship typealiases because we do not expect
164-
// JSON:API Relationships for this particular API to have
165-
// Metadata or Links associated with them.
166-
typealias ToOneRelationship<Entity: JSONAPIIdentifiable> = JSONAPI.ToOneRelationship<Entity, NoMetadata, NoLinks>
167-
typealias ToManyRelationship<Entity: Relatable> = JSONAPI.ToManyRelationship<Entity, NoMetadata, NoLinks>
168-
169-
// Create a typealias for a Document because we do not expect
170-
// JSON:API Documents for this particular API to have Metadata, Links,
171-
// useful Errors, or an APIDescription (The *SPEC* calls this
172-
// "API Description" the "JSON:API Object").
173-
typealias Document<PrimaryResourceBody: JSONAPI.CodableResourceBody, IncludeType: JSONAPI.Include> = JSONAPI.Document<PrimaryResourceBody, NoMetadata, NoLinks, IncludeType, NoAPIDescription, BasicJSONAPIError<String>>
174-
175-
// MARK: Entity Definitions
176-
177-
enum AuthorDescription: ResourceObjectDescription {
178-
public static var jsonType: String { return "authors" }
179-
180-
public struct Attributes: JSONAPI.Attributes {
181-
public let name: Attribute<String>
182-
}
183-
184-
public typealias Relationships = NoRelationships
185-
}
186-
187-
typealias Author = JSONEntity<AuthorDescription>
188-
189-
enum ArticleDescription: ResourceObjectDescription {
190-
public static var jsonType: String { return "articles" }
191-
192-
public struct Attributes: JSONAPI.Attributes {
193-
public let title: Attribute<String>
194-
public let abstract: Attribute<String>
195-
}
196-
197-
public struct Relationships: JSONAPI.Relationships {
198-
public let author: ToOneRelationship<Author>
199-
}
200-
}
201-
202-
typealias Article = JSONEntity<ArticleDescription>
203-
204-
// MARK: Document Definitions
205-
206-
// We create a typealias to represent a document containing one Article
207-
// and including its Author
208-
typealias SingleArticleDocumentWithIncludes = Document<SingleResourceBody<Article>, Include1<Author>>
209-
210-
// ... and a typealias to represent a document containing one Article and
211-
// not including any related entities.
212-
typealias SingleArticleDocument = Document<SingleResourceBody<Article>, NoIncludes>
213-
```
214-
215-
### Server Pseudo-example
216-
```swift
217-
// Skipping over all the API and database stuff, here's a chunk of code
218-
// that creates a document. Note that this document is the entirety
219-
// of a JSON:API response body.
220-
func articleDocument(includeAuthor: Bool) -> Either<SingleArticleDocument, SingleArticleDocumentWithIncludes> {
221-
// Let's pretend all of this is coming from a database:
222-
223-
let authorId = Author.Id(rawValue: "1234")
224-
225-
let article = Article(id: .init(rawValue: "5678"),
226-
attributes: .init(title: .init(value: "JSON:API in Swift"),
227-
abstract: .init(value: "Not yet written")),
228-
relationships: .init(author: .init(id: authorId)),
229-
meta: .none,
230-
links: .none)
231-
232-
let document = SingleArticleDocument(apiDescription: .none,
233-
body: .init(resourceObject: article),
234-
includes: .none,
235-
meta: .none,
236-
links: .none)
237-
238-
switch includeAuthor {
239-
case false:
240-
return .init(document)
241-
242-
case true:
243-
let author = Author(id: authorId,
244-
attributes: .init(name: .init(value: "Janice Bluff")),
245-
relationships: .none,
246-
meta: .none,
247-
links: .none)
248-
249-
let includes: Includes<SingleArticleDocumentWithIncludes.Include> = .init(values: [.init(author)])
250-
251-
return .init(document.including(includes))
252-
}
253-
}
254-
255-
let encoder = JSONEncoder()
256-
encoder.keyEncodingStrategy = .convertToSnakeCase
257-
encoder.outputFormatting = .prettyPrinted
258-
259-
let responseBody = articleDocument(includeAuthor: true)
260-
let responseData = try! encoder.encode(responseBody)
261-
262-
// Next step would be setting the HTTP body of a response.
263-
// We will just print it out instead:
264-
print("-----")
265-
print(String(data: responseData, encoding: .utf8)!)
266-
267-
// ... and if we had received a request for an article without
268-
// including the author:
269-
let otherResponseBody = articleDocument(includeAuthor: false)
270-
let otherResponseData = try! encoder.encode(otherResponseBody)
271-
print("-----")
272-
print(String(data: otherResponseData, encoding: .utf8)!)
273-
```
274-
275-
### Client Pseudo-example
276-
```swift
277-
enum NetworkError: Swift.Error {
278-
case serverError
279-
case quantityMismatch
280-
}
281-
282-
// Skipping over all the API stuff, here's a chunk of code that will
283-
// decode a document. We will assume we have made a request for a
284-
// single article including the author.
285-
func docode(articleResponseData: Data) throws -> (article: Article, author: Author) {
286-
let decoder = JSONDecoder()
287-
decoder.keyDecodingStrategy = .convertFromSnakeCase
288-
289-
let articleDocument = try decoder.decode(SingleArticleDocumentWithIncludes.self, from: articleResponseData)
290-
291-
switch articleDocument.body {
292-
case .data(let data):
293-
let authors = data.includes[Author.self]
294-
295-
guard authors.count == 1 else {
296-
throw NetworkError.quantityMismatch
297-
}
298-
299-
return (article: data.primary.value, author: authors[0])
300-
case .errors(let errors, meta: _, links: _):
301-
throw NetworkError.serverError
302-
}
303-
}
304-
305-
let response = try! docode(articleResponseData: responseData)
306-
307-
// Next step would be to do something useful with the article and author but we will print them instead.
308-
print("-----")
309-
print(response.article)
310-
print(response.author)
311-
```
85+
Note that Playground support for importing non-system Frameworks is still a bit touchy as of Swift 4.2. Sometimes building, cleaning and building, or commenting out and then uncommenting import statements (especially in the` Entities.swift` Playground Source file) can get things working for me when I am getting an error about `JSONAPI` not being found.
31286

31387
## Deeper Dive
314-
See the [usage documentation](./documentation/usage.md).
88+
- [Project Status](./documentation/project-status.md)
89+
- [Server & Client Example](./documentation/client-server-example.md)
90+
- [Usage Documentation](./documentation/usage.md)
31591

31692
# JSONAPI+Testing
31793
The `JSONAPI` framework is packaged with a test library to help you test your `JSONAPI` integration. The test library is called `JSONAPITesting`. You can see `JSONAPITesting` in action in the Playground included with the `JSONAPI` repository.

0 commit comments

Comments
 (0)