Compare commits

..

3 Commits

Author SHA1 Message Date
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 107 additions and 17 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>
+53 -7
View File
@@ -52,6 +52,7 @@ 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)
- [Example](#example)
- [Preamble (Setup shared by server and client)](#preamble-setup-shared-by-server-and-client)
- [Server Pseudo-example](#server-pseudo-example)
@@ -143,7 +144,7 @@ An `EntityDescription` is the `JSONAPI` framework's representation of what the *
```
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 +158,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`
@@ -460,7 +461,7 @@ extension String: CreatableRawIdType {
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:
```
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 +471,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>
@@ -486,7 +487,7 @@ public enum EntityDescription2: JSONAPI.EntityDescription {
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:
```
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,6 +527,51 @@ 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.
```
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:
```
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.
## 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.
@@ -565,7 +611,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 +623,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>
+9
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`.
@@ -609,6 +609,26 @@ extension EntityTests {
}
}
// MARK: With a Meta Attribute
extension EntityTests {
func test_MetaEntityAccessWorks() {
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: - Test Types
extension EntityTests {
@@ -790,6 +810,22 @@ 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 IntToString: Transformer {
public static func transform(_ from: Int) -> String {
return String(from)
+1
View File
@@ -228,6 +228,7 @@ extension EntityTests {
("test_IntOver10_success", test_IntOver10_success),
("test_IntToString", test_IntToString),
("test_IntToString_encode", test_IntToString_encode),
("test_MetaEntityAccessWorks", test_MetaEntityAccessWorks),
("test_NonNullOptionalNullableAttribute", test_NonNullOptionalNullableAttribute),
("test_NonNullOptionalNullableAttribute_encode", test_NonNullOptionalNullableAttribute_encode),
("test_nullableRelationshipIsNull", test_nullableRelationshipIsNull),