Skip to content

Latest commit

 

History

History
174 lines (136 loc) · 5.06 KB

File metadata and controls

174 lines (136 loc) · 5.06 KB

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.

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.

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

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.

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

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

Now let's parse a mocked Person response.

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.

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.

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 ")).")