Compare commits

..

9 Commits

Author SHA1 Message Date
Mathew Polzin c04d3301b6 Add Meta-Relationship access. 2019-01-08 21:23:17 -08:00
Mathew Polzin 5a64bebc99 Merge branch 'master' of github.com:mattpolzin/JSONAPI 2019-01-08 20:13:11 -08:00
Mathew Polzin 790b0fbf52 Change capitalization 2019-01-06 18:20:18 -08:00
Mathew Polzin 539ecc451a Remove swift syntax highlighting from JSON snippet. 2019-01-03 22:26:25 -08:00
Mathew Polzin 7eb1b05eae Add swift syntax highlighting to README. 2019-01-03 22:23:17 -08:00
Mathew Polzin 3408263c2a Add warning note to README 2019-01-03 22:09:43 -08:00
Mathew Polzin 1d6e5d3810 Added Meta-Attribute support and documentation 2019-01-02 22:49:38 -08:00
Mathew Polzin d68404db36 update README to reflect change made in v0.12.0 2019-01-02 19:46:30 -08:00
Mathew Polzin 4f7db98a87 Update playground files to work with v0.12.0 changes 2019-01-02 19:42:32 -08:00
8 changed files with 215 additions and 41 deletions
@@ -40,7 +40,7 @@ typealias Document<PrimaryResourceBody: JSONAPI.ResourceBody, IncludeType: JSONA
// MARK: Entity Definitions
enum AuthorDescription: EntityDescription {
public static var type: String { return "authors" }
public static var jsonType: String { return "authors" }
public struct Attributes: JSONAPI.Attributes {
public let name: Attribute<String>
@@ -52,7 +52,7 @@ enum AuthorDescription: EntityDescription {
typealias Author = JSONEntity<AuthorDescription>
enum ArticleDescription: EntityDescription {
public static var type: String { return "articles" }
public static var jsonType: String { return "articles" }
public struct Attributes: JSONAPI.Attributes {
public let title: Attribute<String>
@@ -65,7 +65,7 @@ struct ToManyRelationshipLinks: JSONAPI.Links {
/// Description of an Author entity.
enum AuthorDescription: EntityDescription {
static var type: String { return "authors" }
static var jsonType: String { return "authors" }
struct Attributes: JSONAPI.Attributes {
let name: Attribute<String>
@@ -80,7 +80,7 @@ typealias Author = JSONAPI.Entity<AuthorDescription, EntityMetadata, EntityLinks
/// Description of an Article entity.
enum ArticleDescription: EntityDescription {
static var type: String { return "articles" }
static var jsonType: String { return "articles" }
struct Attributes: JSONAPI.Attributes {
let title: Attribute<String>
+4 -4
View File
@@ -31,7 +31,7 @@ public typealias ToMany<E: Relatable> = ToManyRelationship<E, NoMetadata, NoLink
// MARK: - A few resource objects (entities)
public enum PersonDescription: EntityDescription {
public static var type: String { return "people" }
public static var jsonType: String { return "people" }
public struct Attributes: JSONAPI.Attributes {
public let name: Attribute<[String]>
@@ -70,7 +70,7 @@ public extension Entity where Description == PersonDescription, MetaType == NoMe
public enum DogDescription: EntityDescription {
public static var type: String { return "dogs" }
public static var jsonType: String { return "dogs" }
public struct Attributes: JSONAPI.Attributes {
public let name: Attribute<String>
@@ -93,7 +93,7 @@ public typealias Dog = ExampleEntity<DogDescription>
public enum AlternativeDogDescription: EntityDescription {
public static var type: String { return "dogs" }
public static var jsonType: String { return "dogs" }
public struct Attributes: JSONAPI.Attributes {
public let name: Attribute<String>
@@ -131,7 +131,7 @@ public extension Entity where Description == DogDescription, MetaType == NoMetad
public enum HouseDescription: EntityDescription {
public static var type: String { return "houses" }
public static var jsonType: String { return "houses" }
public typealias Attributes = NoAttributes
public typealias Relationships = NoRelationships
-2
View File
@@ -3,7 +3,5 @@
<pages>
<page name='Test Library'/>
<page name='Usage'/>
<page name='Full Document Verbose Generation'/>
<page name='Full Client &amp; Server Example'/>
</pages>
</playground>
+113 -31
View File
@@ -5,6 +5,8 @@ A Swift package for encoding to- and decoding from **JSON API** compliant reques
See the JSON API Spec here: https://jsonapi.org/format/
:warning: Although I find the type-safety of this framework appealing, the Swift compiler currently has enough trouble with it that it can become difficult to reason about errors produced by small typos. Similarly, auto-complete fails to provide reasonable suggestions much of the time. If you get the code right, everything compiles, otherwise it can suck to figure out what is wrong. This is mostly a concern when creating entities in-code (servers and test suites must do this). Writing a client that uses this framework to ingest JSON API Compliant API responses is much less painful. :warning:
## Table of Contents
<!-- TOC depthFrom:2 depthTo:6 withLinks:1 updateOnSave:1 orderedList:0 -->
@@ -52,6 +54,8 @@ See the JSON API Spec here: https://jsonapi.org/format/
- [`JSONAPI.RawIdType`](#jsonapirawidtype)
- [Custom Attribute or Relationship Key Mapping](#custom-attribute-or-relationship-key-mapping)
- [Custom Attribute Encode/Decode](#custom-attribute-encodedecode)
- [Meta-Attributes](#meta-attributes)
- [Meta-Relationships](#meta-relationships)
- [Example](#example)
- [Preamble (Setup shared by server and client)](#preamble-setup-shared-by-server-and-client)
- [Server Pseudo-example](#server-pseudo-example)
@@ -141,9 +145,9 @@ In this documentation, in order to draw attention to the difference between the
An `EntityDescription` is the `JSONAPI` framework's representation of what the **SPEC** calls a *Resource Object*. You might create the following `EntityDescription` to represent a person in a network of friends:
```
```swift
enum PersonDescription: IdentifiedEntityDescription {
static var type: String { return "people" }
static var jsonType: String { return "people" }
struct Attributes: JSONAPI.Attributes {
let name: Attribute<[String]>
@@ -157,7 +161,7 @@ enum PersonDescription: IdentifiedEntityDescription {
```
The requirements of an `EntityDescription` are:
1. A static `var` "type" that matches the JSON type; The **SPEC** requires every *Resource Object* to have a "type".
1. A static `var` "jsonType" that matches the JSON type; The **SPEC** requires every *Resource Object* to have a "type".
2. A `struct` of `Attributes` **- OR -** `typealias Attributes = NoAttributes`
3. A `struct` of `Relationships` **- OR -** `typealias Relationships = NoRelationships`
@@ -224,14 +228,14 @@ A `RawIdType` is the underlying type that uniquely identifies an `Entity`. This
#### Convenient `typealiases`
Often you can use one `RawIdType` for many if not all of your `Entities`. That means you can save yourself some boilerplate by using `typealias`es like the following:
```
```swift
public typealias Entity<Description: JSONAPI.EntityDescription, Meta: JSONAPI.Meta, Links: JSONAPI.Links> = JSONAPI.Entity<Description, Meta, Links, String>
public typealias NewEntity<Description: JSONAPI.EntityDescription, Meta: JSONAPI.Meta, Links: JSONAPI.Links> = JSONAPI.Entity<Description, Meta, Links, Unidentified>
```
It can also be nice to create a `typealias` for each type of entity you want to work with:
```
```swift
typealias Person = Entity<PersonDescription, NoMetadata, NoLinks>
typealias NewPerson = NewEntity<PersonDescription, NoMetadata, NoLinks>
@@ -246,17 +250,17 @@ There are two types of `Relationships`: `ToOneRelationship` and `ToManyRelations
In addition to identifying entities by Id and type, `Relationships` can contain `Meta` or `Links` that follow the same rules as [`Meta`](#jsonapimeta) and [`Links`](#jsonapilinks) elsewhere in the JSON API Document.
To describe a relationship that may be omitted (i.e. the key is not even present in the JSON object), you make the entire `ToOneRelationship` or `ToManyRelationship` optional. However, this is not recommended because you can also represent optional relationships as nullable which means the key is always present. A `ToManyRelationship` can naturally represent the absence of related values with an empty array, so `ToManyRelationship` does not support nullability at all. A `ToOneRelationship` can be marked as nullable (i.e. the value could be either `null` or a resource identifier) like this:
```
```swift
let nullableRelative: ToOneRelationship<Person?, NoMetadata, NoLinks>
```
An entity that does not have relationships can be described by adding the following to an `EntityDescription`:
```
```swift
typealias Relationships = NoRelationships
```
`Relationship` values boil down to `Ids` of other entities. To access the `Id` of a related `Entity`, you can use the custom `~>` operator with the `KeyPath` of the `Relationship` from which you want the `Id`. The friends of the above `Person` `Entity` can be accessed as follows (type annotations for clarity):
```
```swift
let friendIds: [Person.Identifier] = person ~> \.friends
```
@@ -265,27 +269,27 @@ let friendIds: [Person.Identifier] = person ~> \.friends
The `Attributes` of an `EntityDescription` can contain any JSON encodable/decodable types as long as they are wrapped in an `Attribute`, `ValidatedAttribute`, or `TransformedAttribute` `struct`.
To describe an attribute that may be omitted (i.e. the key might not even be in the JSON object), you make the entire `Attribute` optional:
```
```swift
let optionalAttribute: Attribute<String>?
```
To describe an attribute that is expected to exist but might have a `null` value, you make the value within the `Attribute` optional:
```
```swift
let nullableAttribute: Attribute<String?>
```
An entity that does not have attributes can be described by adding the following to an `EntityDescription`:
```
```swift
typealias Attributes = NoAttributes
```
`Attributes` can be accessed via the `subscript` operator of the `Entity` type as follows:
```
```swift
let favoriteColor: String = person[\.favoriteColor]
```
NOTE: Because of support for computed properties that are not wrapped in `Attribute`, `TransformedAttribute`, or `ValidatedAttribute`, the compiler cannot always infer the type of thing you want back when using subscript attribute access. The following code is ambiguous about whether it should return a `String` or an `Attribute<String>`:
```
```swift
let favoriteColor = person[\.favoriteColor]
```
@@ -294,7 +298,7 @@ let favoriteColor = person[\.favoriteColor]
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`.
A `Transformer` just provides one static function that transforms one type to another. You might define one for an ISO 8601 compliant `Date` like this:
```
```swift
enum ISODateTransformer: Transformer {
public static func transform(_ value: String) throws -> Date {
// parse Date out of input and return
@@ -303,14 +307,14 @@ enum ISODateTransformer: Transformer {
```
Then you define the attribute as a `TransformedAttribute` instead of an `Attribute`:
```
```swift
let date: TransformedAttribute<String, ISODateTransformer>
```
Note that the first generic parameter of `TransformAttribute` is the type you expect to decode from JSON, not the type you want to end up with after transformation.
If you make your `Transformer` a `ReversibleTransformer` then your life will be a bit easier when you construct `TransformedAttributes` because you have access to initializers for both the pre- and post-transformed value types. Continuing with the above example of a `ISODateTransformer`:
```
```swift
extension ISODateTransformer: ReversibleTransformer {
public static func reverse(_ value: Date) throws -> String {
// serialize Date to a String
@@ -329,7 +333,7 @@ You can also creator `Validators` and `ValidatedAttribute`s. A `Validator` is ju
You can add computed properties to your `EntityDescription.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> {
return name.map { $0.joined(separator: " ") }
}
@@ -342,7 +346,7 @@ public var fullName: Attribute<String> {
The above can be accomplished with code like the following:
```
```swift
// use case 1
let person1 = person.withNewIdentifier()
@@ -355,7 +359,7 @@ let newlyIdentifiedPerson2 = unidentifiedPerson.identified(by: "2232")
### `JSONAPI.Document`
The entirety of a JSON API request or response is encoded or decoded from- or to a `Document`. As an example, a JSON API response containing one `Person` and no included entities could be decoded as follows:
```
```swift
let decoder = JSONDecoder()
let responseStructure = JSONAPI.Document<SingleResourceBody<Person>, NoMetadata, NoLinks, NoIncludes, UnknownJSONAPIError>.self
@@ -391,7 +395,7 @@ The second generic type of a `JSONAPIDocument` is a `Meta`. This `Meta` follows
```
You would then create the following `Meta` type:
```
```swift
struct PageMetadata: JSONAPI.Meta {
let total: Int
let limit: Int
@@ -442,7 +446,7 @@ You can specify `NoLinks` if the part of the document being described should not
### `JSONAPI.RawIdType`
If you want to create new `JSONAPI.Entity` values and assign them Ids then you will need to conform at least one type to `CreatableRawIdType`. Doing so is easy; here are two example conformances for `UUID` and `String` (via `UUID`):
```
```swift
extension UUID: CreatableRawIdType {
public static func unique() -> UUID {
return UUID()
@@ -458,9 +462,9 @@ extension String: CreatableRawIdType {
### Custom Attribute or Relationship Key Mapping
There is not anything special going on at the `JSONAPI.Attributes` and `JSONAPI.Relationships` levels, so you can easily provide custom key mappings by taking advantage of `Codable`'s `CodingKeys` pattern. Here are two models that will encode/decode equivalently but offer different naming in your codebase:
```
```swift
public enum EntityDescription1: JSONAPI.EntityDescription {
public static var type: String { return "entity" }
public static var jsonType: String { return "entity" }
public struct Attributes: JSONAPI.Attributes {
public let coolProperty: Attribute<String>
@@ -470,7 +474,7 @@ public enum EntityDescription1: JSONAPI.EntityDescription {
}
public enum EntityDescription2: JSONAPI.EntityDescription {
public static var type: String { return "entity" }
public static var jsonType: String { return "entity" }
public struct Attributes: JSONAPI.Attributes {
public let wholeOtherThing: Attribute<String>
@@ -484,9 +488,9 @@ public enum EntityDescription2: JSONAPI.EntityDescription {
### Custom Attribute Encode/Decode
You can safely provide your own encoding or decoding functions for your Attributes struct if you need to as long as you are careful that your encode operation correctly reverses your decode operation. Although this is generally not necessary, `AttributeType` provides a convenience method to make your decoding a bit less boilerplate ridden. This is what it looks like:
```
```swift
public enum EntityDescription1: JSONAPI.EntityDescription {
public static var type: String { return "entity" }
public static var jsonType: String { return "entity" }
public struct Attributes: JSONAPI.Attributes {
public let property1: Attribute<String>
@@ -526,11 +530,89 @@ extension EntityDescription1.Attributes {
}
```
### Meta-Attributes
This advanced feature may not ever be useful, but if you find yourself in the situation of dealing with an API that does not 100% follow the **SPEC** then you might find meta-attributes are just the thing to make your entities more natural to work with.
Suppose, for example, you are presented with the unfortunate situation where a piece of information you need is only available as part of the `Id` of an entity. Perhaps a user's `Id` is formatted "{integer}-{createdAt}" where "createdAt" is the unix timestamp when the user account was created. The following `UserDescription` will expose what you need as an attribute. Realistically, this code is still terrible for its error handling. Using a `Result` type and/or invariants would clean things up substantially.
```swift
enum UserDescription: EntityDescription {
public static var jsonType: String { return "users" }
struct Attributes: JSONAPI.Attributes {
var createdAt: (User) -> Date {
return { user in
let components = user.id.rawValue.split(separator: "-")
guard components.count == 2 else {
assertionFailure()
return Date()
}
let timestamp = TimeInterval(components[1])
guard let date = timestamp.map(Date.init(timeIntervalSince1970:)) else {
assertionFailure()
return Date()
}
return date
}
}
}
typealias Relationships = NoRelationships
}
typealias User = JSONAPI.Entity<UserDescription, NoMetadata, NoLinks, String>
```
Given a value `user` of the above entity type, you can access the `createdAt` attribute just like you would any other:
```swift
let createdAt = user[\.createdAt]
```
This works because `createdAt` is defined in the form: `var {name}: ({Entity}) -> {Value}` where `{Entity}` is the `JSONAPI.Entity` described by the `EntityDescription` containing the meta-attribute.
### Meta-Relationships
This advanced feature may not ever be useful, but if you find yourself in the situation of dealing with an API that does not 100% follow the **SPEC** then you might find meta-relationships are just the thing to make your entities more natural to work with.
Similarly to Meta-Attributes, Meta-Relationships allow you to represent non-compliant relationships as computed relationship properties. In the following example, a relationship is created from some attributes on the JSON model.
```swift
enum UserDescription: EntityDescription {
public static var jsonType: String { return "users" }
struct Attributes: JSONAPI.Attributes {
let friend_id: Attribute<String>
}
struct Relationships: JSONAPI.Relationships {
public var friend: (User) -> User.Identifier {
return { user in
return User.Identifier(rawValue: user[\.friend_id])
}
}
}
}
typealias User = JSONAPI.Entity<UserDescription, NoMetadata, NoLinks, String>
```
Given a value `user` of the above entity type, you can access the `friend` relationship just like you would any other:
```swift
let friendId = user ~> \.friend
```
This works because `friend` is defined in the form: `var {name}: ({Entity}) -> {Identifier}` where `{Entity}` is the `JSONAPI.Entity` described by the `EntityDescription` containing the meta-relationship.
## Example
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.
### Preamble (Setup shared by server and client)
```
```swift
// We make String a CreatableRawIdType.
var GlobalStringId: Int = 0
extension String: CreatableRawIdType {
@@ -565,7 +647,7 @@ typealias Document<PrimaryResourceBody: JSONAPI.ResourceBody, IncludeType: JSONA
// MARK: Entity Definitions
enum AuthorDescription: EntityDescription {
public static var type: String { return "authors" }
public static var jsonType: String { return "authors" }
public struct Attributes: JSONAPI.Attributes {
public let name: Attribute<String>
@@ -577,7 +659,7 @@ enum AuthorDescription: EntityDescription {
typealias Author = JSONEntity<AuthorDescription>
enum ArticleDescription: EntityDescription {
public static var type: String { return "articles" }
public static var jsonType: String { return "articles" }
public struct Attributes: JSONAPI.Attributes {
public let title: Attribute<String>
@@ -602,7 +684,7 @@ typealias SingleArticleDocumentWithIncludes = Document<SingleResourceBody<Articl
typealias SingleArticleDocument = Document<SingleResourceBody<Article>, NoIncludes>
```
### Server Pseudo-example
```
```swift
// Skipping over all the API and database stuff, here's a chunk of code
// that creates a document. Note that this document is the entirety
// of a JSON:API response body.
@@ -662,7 +744,7 @@ print(String(data: otherResponseData, encoding: .utf8)!)
```
### Client Pseudo-example
```
```swift
enum NetworkError: Swift.Error {
case serverError
case quantityMismatch
+26
View File
@@ -471,6 +471,15 @@ public extension EntityProxy {
}
}
// MARK: Meta-Attribute Access
public extension EntityProxy {
/// 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: Relationship Access
public extension EntityProxy {
/// Access to an Id of a `ToOneRelationship`.
@@ -513,6 +522,23 @@ public extension EntityProxy {
}
}
// MARK: Meta-Relationship Access
public extension EntityProxy {
/// Access to an Id of a `ToOneRelationship`.
/// This allows you to write `entity ~> \.other` instead
/// of `entity.relationships.other.id`.
public static func ~><Identifier: IdType>(entity: Self, path: KeyPath<Description.Relationships, (Self) -> Identifier>) -> Identifier {
return entity.relationships[keyPath: path](entity)
}
/// Access to all Ids of a `ToManyRelationship`.
/// This allows you to write `entity ~> \.others` instead
/// of `entity.relationships.others.ids`.
public static func ~><Identifier: IdType>(entity: Self, path: KeyPath<Description.Relationships, (Self) -> [Identifier]>) -> [Identifier] {
return entity.relationships[keyPath: path](entity)
}
}
infix operator ~>
// MARK: - Codable
@@ -609,6 +609,40 @@ extension EntityTests {
}
}
// MARK: With a Meta Attribute
extension EntityTests {
func test_MetaEntityAttributeAccessWorks() {
let entity1 = TestEntityWithMetaAttribute(id: "even",
attributes: .init(),
relationships: .none,
meta: .none,
links: .none)
let entity2 = TestEntityWithMetaAttribute(id: "odd",
attributes: .init(),
relationships: .none,
meta: .none,
links: .none)
XCTAssertEqual(entity1[\.metaAttribute], true)
XCTAssertEqual(entity2[\.metaAttribute], false)
}
}
// MARK: With a Meta Relationship
extension EntityTests {
func test_MetaEntityRelationshipAccessWorks() {
let entity1 = TestEntityWithMetaRelationship(id: "even",
attributes: .none,
relationships: .init(),
meta: .none,
links: .none)
XCTAssertEqual(entity1 ~> \.metaRelationship, "hello")
}
}
// MARK: - Test Types
extension EntityTests {
@@ -790,6 +824,38 @@ extension EntityTests {
typealias UnidentifiedTestEntityWithMetaAndLinks = NewEntity<UnidentifiedTestEntityType, TestEntityMeta, TestEntityLinks>
enum TestEntityWithMetaAttributeDescription: EntityDescription {
public static var jsonType: String { return "meta_attribute_entity" }
struct Attributes: JSONAPI.Attributes {
var metaAttribute: (TestEntityWithMetaAttribute) -> Bool {
return { entity in
(entity.id.rawValue.count % 2) == 0
}
}
}
typealias Relationships = NoRelationships
}
typealias TestEntityWithMetaAttribute = BasicEntity<TestEntityWithMetaAttributeDescription>
enum TestEntityWithMetaRelationshipDescription: EntityDescription {
public static var jsonType: String { return "meta_relationship_entity" }
typealias Attributes = NoAttributes
struct Relationships: JSONAPI.Relationships {
var metaRelationship: (TestEntityWithMetaRelationship) -> TestEntity1.Identifier {
return { entity in
return TestEntity1.Identifier(rawValue: "hello")
}
}
}
}
typealias TestEntityWithMetaRelationship = BasicEntity<TestEntityWithMetaRelationshipDescription>
enum IntToString: Transformer {
public static func transform(_ from: Int) -> String {
return String(from)
+2
View File
@@ -228,6 +228,8 @@ extension EntityTests {
("test_IntOver10_success", test_IntOver10_success),
("test_IntToString", test_IntToString),
("test_IntToString_encode", test_IntToString_encode),
("test_MetaEntityAttributeAccessWorks", test_MetaEntityAttributeAccessWorks),
("test_MetaEntityRelationshipAccessWorks", test_MetaEntityRelationshipAccessWorks),
("test_NonNullOptionalNullableAttribute", test_NonNullOptionalNullableAttribute),
("test_NonNullOptionalNullableAttribute_encode", test_NonNullOptionalNullableAttribute_encode),
("test_nullableRelationshipIsNull", test_nullableRelationshipIsNull),