Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 11 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,20 @@ See the JSON API Spec here: https://jsonapi.org/format/

## Quick Start

: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.

### 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)
- [PATCH Example](https://colab.research.google.com/drive/16KY-0BoLQKiSUh9G7nYmHzB8b2vhXA2U)
- [Resource Storage Example](https://colab.research.google.com/drive/196eCnBlf2xz8pT4lW--ur6eWSVAjpF6b?usp=sharing) (using [JSONAPI-ResourceStorage](#jsonapi-resourcestorage))
- [Basic Example](./documentation/examples/basic-example.md)
- [Compound Example](./documentation/examples/compound-example.md)
- [Metadata Example](./documentation/examples/metadata-example.md)
- [Custom Errors Example](./documentation/examples/custom-errors-example.md)
- [PATCH Example](./documentation/examples/patch-example.md)
- [Resource Storage Example](./documentation/examples/resource-storage-example.md) (using [JSONAPI-ResourceStorage](#jsonapi-resourcestorage))

### Serverside
- [GET Example](https://colab.research.google.com/drive/1krbhzSfz8mwkBTQQnKUZJLEtYsJKSfYX)
- [POST Example](https://colab.research.google.com/drive/1z3n70LwRY7vLIgbsMghvnfHA67QiuqpQ)
- [GET Example](./documentation/examples/serverside-get-example.md)
- [POST Example](./documentation/examples/serverside-post-example.md)

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

## Table of Contents
- JSONAPI
Expand All @@ -34,7 +32,7 @@ This library works well when used by both the server responsible for serializati
- [CocoaPods](#cocoapods)
- [Running the Playground](#running-the-playground)
- [Project Status](./documentation/project-status.md)
- [Server & Client Example](./documentation/client-server-example.md)
- [Server & Client Example](./documentation/examples/client-server-example.md)
- [Usage](./documentation/usage.md)
- [JSONAPI+Testing](#jsonapitesting)
- [Literal Expressibility](#literal-expressibility)
Expand Down Expand Up @@ -91,7 +89,7 @@ Note that Playground support for importing non-system Frameworks is still a bit

## Deeper Dive
- [Project Status](./documentation/project-status.md)
- [Server & Client Example](./documentation/client-server-example.md)
- [Server & Client Example](./documentation/examples/client-server-example.md)
- [Usage Documentation](./documentation/usage.md)

# JSONAPI+Testing
Expand Down
174 changes: 174 additions & 0 deletions documentation/examples/basic-example.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@

# JSONAPI Basic Example

We are about to walk through a basic example to show how easy it is to set up a
simple model. Information on creating models that take advantage of more of the
features from the JSON:API Specification can be found in the [README](https://github.com/mattpolzin/JSONAPI/blob/main/README.md).

The `JSONAPI` framework relies heavily on generic types so the first step will
be to alias away some of the JSON:API features we do not need for our simple
example.

```swift
/// Our Resource objects will not have any metadata or links and they will be identified by Strings.
typealias Resource<Description: JSONAPI.ResourceObjectDescription> = JSONAPI.ResourceObject<Description, NoMetadata, NoLinks, String>

/// Our JSON:API Documents will similarly have no metadata or links associated with them. Additionally, there will be no included resources.
typealias SingleDocument<Resource: ResourceObjectType> = JSONAPI.Document<SingleResourceBody<Resource>, NoMetadata, NoLinks, NoIncludes, NoAPIDescription, UnknownJSONAPIError>

typealias BatchDocument<Resource: ResourceObjectType> = JSONAPI.Document<ManyResourceBody<Resource>, NoMetadata, NoLinks, NoIncludes, NoAPIDescription, UnknownJSONAPIError>
```

The next step is to create `ResourceObjectDescriptions` and `ResourceObjects`.
For our simple example, let's create a `Person` and a `Dog`.

```swift
struct PersonDescription: ResourceObjectDescription {
// by common convention, we will use the plural form
// of the noun as the JSON:API "type"
static let jsonType: String = "people"

struct Attributes: JSONAPI.Attributes {
let firstName: Attribute<String>
let lastName: Attribute<String>

// we mark this attribute as "nullable" because the user can choose
// not to specify an age if they would like to.
let age: Attribute<Int?>
}

struct Relationships: JSONAPI.Relationships {
// we will define "Dog" next
let pets: ToManyRelationship<Dog, NoIdMetadata, NoMetadata, NoLinks>
}
}

// this typealias is optional, but it makes working with resource objects much
// more user friendly.
typealias Person = Resource<PersonDescription>

struct DogDescription: ResourceObjectDescription {
static let jsonType: String = "dogs"

struct Attributes: JSONAPI.Attributes {
let name: Attribute<String>
}

// we could relate dogs back to their owners, but for the sake of this example
// we will instead show how you would create a resource with no relationships.
typealias Relationships = NoRelationships
}

typealias Dog = Resource<DogDescription>
```

At this point we have two objects that can decode JSON:API responses. To
illustrate we can mock up a dog response and parse it.

```swift
// snag Foundation for JSONDecoder
import Foundation

let mockBatchDogResponse =
"""
{
"data": [
{
"type": "dogs",
"id": "123",
"attributes": {
"name": "Sparky"
}
},
{
"type": "dogs",
"id": "456",
"attributes": {
"name": "Charlie Dog"
}
}
]
}
""".data(using: .utf8)!

let decoder = JSONDecoder()

let dogsDocument = try! decoder.decode(BatchDocument<Dog>.self, from: mockBatchDogResponse)

let dogs = dogsDocument.body.primaryResource!.values

print("dogs parsed: \(dogs.count ?? 0)")
```

To illustrate `ResourceObject` property access, we can loop over the dogs and
print their names.

```swift
for dog in dogs {
print(dog.name)
}
```

Now let's parse a mocked `Person` response.

```swift
let mockSinglePersonResponse =
"""
{
"data": {
"type": "people",
"id": "88223",
"attributes": {
"first_name": "Lisa",
"last_name": "Offenbrook",
"age": null
},
"relationships": {
"pets": {
"data": [
{
"type": "dogs",
"id": "123"
},
{
"type": "dogs",
"id": "456"
}
]
}
}
}
}
""".data(using: .utf8)!

decoder.keyDecodingStrategy = .convertFromSnakeCase

let personDocument = try! decoder.decode(SingleDocument<Person>.self, from: mockSinglePersonResponse)
```

Our `Person` object has both attributes and relationships. Generally what we care
about when accessing relationships is actually the Id(s) of the resource(s); the
loop below shows off how to access those Ids.

```swift
let person = personDocument.body.primaryResource!.value

let relatedDogIds = person ~> \.pets

print("related dog Ids: \(relatedDogIds)")
```

To wrap things up, let's throw our dog resources into a local cache and tie
things together a bit. There are many ways to go about storing or caching
resources clientside. For additional examples of more robust solutions, take a
look at [JSONAPI-ResourceStorage](https://github.com/mattpolzin/JSONAPI-ResourceStorage).

```swift
let dogCache = Dictionary(uniqueKeysWithValues: zip(dogs.map { $0.id }, dogs))

func cachedDog(_ id: Dog.Id) -> Dog? { return dogCache[id] }

print("Our person's name is \(person.firstName) \(person.lastName).")
print("They have \((person ~> \.pets).count) pets named \((person ~> \.pets).compactMap(cachedDog).map { $0.name }.joined(separator: " and ")).")
```

134 changes: 134 additions & 0 deletions documentation/examples/compound-example.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
# JSONAPI Compound Example

We are about to walk through an example to show how easy it is to parse JSON:API
includes. Information on creating models that take advantage of more of the
features from the JSON:API Specification can be found in the [README](https://github.com/mattpolzin/JSONAPI/blob/main/README.md).

We will begin by quickly redefining the same types of `ResourceObjects` from the
[Basic Example](https://github.com/mattpolzin/JSONAPI/blob/main/documentation/basic-example.md).

```swift
typealias Resource<Description: JSONAPI.ResourceObjectDescription> = JSONAPI.ResourceObject<Description, NoMetadata, NoLinks, String>

struct PersonDescription: ResourceObjectDescription {

static let jsonType: String = "people"

struct Attributes: JSONAPI.Attributes {
let firstName: Attribute<String>
let lastName: Attribute<String>

let age: Attribute<Int?>
}

struct Relationships: JSONAPI.Relationships {
let pets: ToManyRelationship<Dog, NoIdMetadata, NoMetadata, NoLinks>
}
}

typealias Person = Resource<PersonDescription>

struct DogDescription: ResourceObjectDescription {
static let jsonType: String = "dogs"

struct Attributes: JSONAPI.Attributes {
let name: Attribute<String>
}

typealias Relationships = NoRelationships
}

typealias Dog = Resource<DogDescription>
```

Next we will create similar `typealiases` for single and batch documents as we did
in the **Basic Example**, but we will allow for an include type to be specified.

```swift
/// Our JSON:API Documents will still have no metadata or links associated with them but they will allow us to specify an include type later.
typealias SingleDocument<Resource: ResourceObjectType, Include: JSONAPI.Include> = JSONAPI.Document<SingleResourceBody<Resource>, NoMetadata, NoLinks, Include, NoAPIDescription, UnknownJSONAPIError>

typealias BatchDocument<Resource: ResourceObjectType, Include: JSONAPI.Include> = JSONAPI.Document<ManyResourceBody<Resource>, NoMetadata, NoLinks, Include, NoAPIDescription, UnknownJSONAPIError>
```

Now let's define a mock response containing a single person and including any
dogs that are related to that person.

```swift
// snag Foundation for Data and JSONDecoder
import Foundation

let mockSinglePersonResponse =
"""
{
"data": {
"type": "people",
"id": "88223",
"attributes": {
"first_name": "Lisa",
"last_name": "Offenbrook",
"age": null
},
"relationships": {
"pets": {
"data": [
{
"type": "dogs",
"id": "123"
},
{
"type": "dogs",
"id": "456"
}
]
}
}
},
"included": [
{
"type": "dogs",
"id": "123",
"attributes": {
"name": "Sparky"
}
},
{
"type": "dogs",
"id": "456",
"attributes": {
"name": "Charlie Dog"
}
}
]
}
""".data(using: .utf8)!
```

Parsing the above response looks almost identical to in the **Basic Example**. The
key thing to note is that instead of specifying `NoIncludes` we specify
`Include1<Dog>` below. This does not mean "include one dog," it means "include one
type of thing, with that type being `Dog`." The `JSONAPI` framework comes with
built-in support for `Include2<...>`, `Include3<...>` and many more. If you wanted to include
both `Person` and `Dog` resources (perhaps because your primary `Person` resource had
a "friends" relationship), you would use `Include2<Person, Dog>`.

```swift
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase

let includeDocument = try! decoder.decode(SingleDocument<Person, Include1<Dog>>.self, from: mockSinglePersonResponse)
```

The `Person` is pulled out as before with `Document.body.primaryResource`. The dogs
can be accessed from `Document.body.includes`; note that because multiple types of
things can be included, we must specify that we want things of type `Dog` by using
the `JSONAPI.Includes` subscript operator.

```swift
let person = includeDocument.body.primaryResource!.value
let dogs = includeDocument.body.includes![Dog.self]

print("Parsed person named \(person.firstName) \(person.lastName)")
print("Parsed dogs named \(dogs.map { $0.name }.joined(separator: " and "))")
```

Loading