mirror of
https://github.com/encounter/JSONAPI.git
synced 2026-07-10 12:18:40 -07:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6aeb859c24 | |||
| 163ac94c51 | |||
| e67b9fc142 | |||
| a628992fcb | |||
| cf47f88a61 | |||
| 0425e2adcb | |||
| d3763ba713 | |||
| fcc1796731 | |||
| 921bcef05d | |||
| 661ff6eca5 | |||
| e36180c9b9 | |||
| abee0c4d0e |
@@ -40,10 +40,24 @@ let peopleFromData = peopleResponse.body.primaryData?.values
|
||||
let dogsFromData = peopleResponse.body.includes?[Dog.self]
|
||||
let housesFromData = peopleResponse.body.includes?[House.self]
|
||||
|
||||
print("-----")
|
||||
print(peopleResponse)
|
||||
print("-----")
|
||||
|
||||
// MARK: - Pass successfully parsed body to other parts of the code
|
||||
|
||||
if case let .data(bodyData) = peopleResponse.body {
|
||||
print(bodyData)
|
||||
print("first person's name: \(bodyData.primary.values[0][\.fullName])")
|
||||
} else {
|
||||
print("no body data")
|
||||
}
|
||||
|
||||
// MARK: - Work in the abstract
|
||||
|
||||
func process<T: JSONAPIDocument>(document: T) {
|
||||
guard case let .data(body) = document.body else {
|
||||
return
|
||||
}
|
||||
let x: T.BodyData = body
|
||||
}
|
||||
process(document: peopleResponse)
|
||||
|
||||
@@ -24,7 +24,7 @@ extension String: CreatableRawIdType {
|
||||
}
|
||||
|
||||
// MARK: - Entity typealias for convenience
|
||||
public typealias ExampleEntity<Description: EntityDescription> = Entity<Description, Id<String, Description>>
|
||||
public typealias ExampleEntity<Description: EntityDescription> = Entity<Description, String>
|
||||
|
||||
// MARK: - A few resource objects (entities)
|
||||
public enum PersonDescription: EntityDescription {
|
||||
@@ -35,6 +35,10 @@ public enum PersonDescription: EntityDescription {
|
||||
public let name: Attribute<[String]>
|
||||
public let favoriteColor: Attribute<String>
|
||||
|
||||
public var fullName: Attribute<String> {
|
||||
return name.map { $0.joined(separator: " ") }
|
||||
}
|
||||
|
||||
public init(name: Attribute<[String]>, favoriteColor: Attribute<String>) {
|
||||
self.name = name
|
||||
self.favoriteColor = favoriteColor
|
||||
@@ -56,9 +60,9 @@ public enum PersonDescription: EntityDescription {
|
||||
|
||||
public typealias Person = ExampleEntity<PersonDescription>
|
||||
|
||||
public extension Entity where Description == PersonDescription, Identifier == Id<String, PersonDescription> {
|
||||
public init(id: Person.Identifier? = nil,name: [String], favoriteColor: String, friends: [Person], dogs: [Dog], home: House) throws {
|
||||
self = try Person(id: id ?? Person.Identifier(), attributes: .init(name: .init(rawValue: name), favoriteColor: .init(rawValue: favoriteColor)), relationships: .init(friends: .init(entities: friends), dogs: .init(entities: dogs), home: .init(entity: home)))
|
||||
public extension Entity where Description == PersonDescription, EntityRawIdType == String {
|
||||
public init(id: Person.Id? = nil,name: [String], favoriteColor: String, friends: [Person], dogs: [Dog], home: House) throws {
|
||||
self = try Person(id: id ?? Person.Id(), attributes: .init(name: .init(rawValue: name), favoriteColor: .init(rawValue: favoriteColor)), relationships: .init(friends: .init(entities: friends), dogs: .init(entities: dogs), home: .init(entity: home)))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,12 +89,12 @@ public enum DogDescription: EntityDescription {
|
||||
|
||||
public typealias Dog = ExampleEntity<DogDescription>
|
||||
|
||||
public extension Entity where Description == DogDescription, Identifier == Id<String, DogDescription> {
|
||||
public extension Entity where Description == DogDescription, EntityRawIdType == String {
|
||||
public init(name: String, owner: Person?) throws {
|
||||
self = try Dog(attributes: .init(name: .init(rawValue: name)), relationships: DogDescription.Relationships(owner: .init(entity: owner)))
|
||||
}
|
||||
|
||||
public init(name: String, owner: Person.Identifier) throws {
|
||||
public init(name: String, owner: Person.Id) throws {
|
||||
self = try Dog(attributes: .init(name: .init(rawValue: name)), relationships: .init(owner: .init(id: owner)))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,6 +100,7 @@ To create an Xcode project for JSONAPI, run
|
||||
- [x] Roll my own `Result` or find an alternative that doesn't use `Foundation`.
|
||||
- [ ] Create more descriptive errors that are easier to use for troubleshooting.
|
||||
- [x] Make it easier to construct `Attributes` and `Relationships` values in test cases (literal expressibility).
|
||||
- [x] Make `TransformedAttribute` a Functor.
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -169,13 +170,13 @@ An `Entity` needs to be specialized on two generic types. The first is the `Enti
|
||||
|
||||
#### `IdType`
|
||||
|
||||
An `IdType` packages up two pieces of information: A unique identifier of a given `RawIdType` and the `EntityDescription` of the type of entity the Id identifies. Having the `EntityDescription` type associated with the Id makes it easy to store all of your entities in a local hash broken out by `EntityDescription`; You can pass Ids around and always know where to look for the `Entity` to which the Id refers. `RawIdType`s are documented below.
|
||||
An `IdType` packages up two pieces of information: A unique identifier of a given `RawIdType` and the `Entity` type that the Id identifies. Having the `Entity` type associated with the Id makes it easy to store all of your entities in a local hash broken out by `Entity` type; You can pass Ids around and always know where to look for the `Entity` to which the Id refers. `RawIdType`s are documented below.
|
||||
|
||||
#### 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:
|
||||
```
|
||||
public typealias Entity<Description: JSONAPI.EntityDescription> = JSONAPI.Entity<Description, Id<String, Description>>
|
||||
public typealias Entity<Description: JSONAPI.EntityDescription> = JSONAPI.Entity<Description, String>
|
||||
|
||||
public typealias NewEntity<Description: JSONAPI.EntityDescription> = JSONAPI.Entity<Description, Unidentified>
|
||||
```
|
||||
@@ -256,6 +257,16 @@ Note that the first generic parameter of `TransformAttribute` is the type you ex
|
||||
|
||||
You can also creator `Validator`s and `ValidatedAttribute`s. A `Validator` is just a `Transformer` that by convention does not perform a transformation. It simply `throws` if an attribute value is invalid.
|
||||
|
||||
#### Computed `Attribute`
|
||||
|
||||
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 should still have the type `Attribute` because that way you can take advantage of the quick access provided by `Entity`'s subscript operator. Here's an example of how you might take the `Person[\.name]` attribute from the example above and create a `fullName` computed property.
|
||||
|
||||
```
|
||||
public var fullName: Attribute<String> {
|
||||
return name.map { $0.joined(separator: " ") }
|
||||
}
|
||||
```
|
||||
|
||||
### `JSONAPIDocument`
|
||||
|
||||
The entirety of a JSON API request or response is encoded or decoded from- or to a `JSONAPIDocument`. As an example, a JSON API response containing one `Person` and no included entities could be decoded as follows:
|
||||
|
||||
@@ -6,11 +6,16 @@
|
||||
//
|
||||
|
||||
public protocol JSONAPIDocument: Codable, Equatable {
|
||||
associatedtype ResourceBody: JSONAPI.ResourceBody
|
||||
associatedtype PrimaryResourceBody: JSONAPI.ResourceBody
|
||||
associatedtype MetaType: JSONAPI.Meta
|
||||
associatedtype LinksType: JSONAPI.Links
|
||||
associatedtype IncludeType: JSONAPI.Include
|
||||
associatedtype Error: JSONAPIError
|
||||
|
||||
typealias Body = Document<PrimaryResourceBody, MetaType, LinksType, IncludeType, Error>.Body
|
||||
typealias BodyData = Body.Data
|
||||
|
||||
var body: Body { get }
|
||||
}
|
||||
|
||||
/// A JSON API Document represents the entire body
|
||||
@@ -20,7 +25,7 @@ public protocol JSONAPIDocument: Codable, Equatable {
|
||||
/// API uses snake case, you will want to use
|
||||
/// a conversion such as the one offerred by the
|
||||
/// Foundation JSONEncoder/Decoder: `KeyDecodingStrategy`
|
||||
public struct Document<ResourceBody: JSONAPI.ResourceBody, MetaType: JSONAPI.Meta, LinksType: JSONAPI.Links, IncludeType: JSONAPI.Include, Error: JSONAPIError>: JSONAPIDocument {
|
||||
public struct Document<PrimaryResourceBody: JSONAPI.ResourceBody, MetaType: JSONAPI.Meta, LinksType: JSONAPI.Links, IncludeType: JSONAPI.Include, Error: JSONAPIError>: JSONAPIDocument {
|
||||
public typealias Include = IncludeType
|
||||
|
||||
public let body: Body
|
||||
@@ -31,10 +36,10 @@ public struct Document<ResourceBody: JSONAPI.ResourceBody, MetaType: JSONAPI.Met
|
||||
case data(Data)
|
||||
|
||||
public struct Data: Equatable {
|
||||
let primary: ResourceBody
|
||||
let includes: Includes<Include>
|
||||
let meta: MetaType
|
||||
let links: LinksType
|
||||
public let primary: PrimaryResourceBody
|
||||
public let includes: Includes<Include>
|
||||
public let meta: MetaType
|
||||
public let links: LinksType
|
||||
}
|
||||
|
||||
public var isError: Bool {
|
||||
@@ -47,7 +52,7 @@ public struct Document<ResourceBody: JSONAPI.ResourceBody, MetaType: JSONAPI.Met
|
||||
return errors
|
||||
}
|
||||
|
||||
public var primaryData: ResourceBody? {
|
||||
public var primaryData: PrimaryResourceBody? {
|
||||
guard case let .data(data) = self else { return nil }
|
||||
return data.primary
|
||||
}
|
||||
@@ -84,49 +89,49 @@ public struct Document<ResourceBody: JSONAPI.ResourceBody, MetaType: JSONAPI.Met
|
||||
body = .errors(errors, meta: meta, links: links)
|
||||
}
|
||||
|
||||
public init(body: ResourceBody, includes: Includes<Include>, meta: MetaType, links: LinksType) {
|
||||
public init(body: PrimaryResourceBody, includes: Includes<Include>, meta: MetaType, links: LinksType) {
|
||||
self.body = .data(.init(primary: body, includes: includes, meta: meta, links: links))
|
||||
}
|
||||
}
|
||||
|
||||
extension Document where IncludeType == NoIncludes {
|
||||
public init(body: ResourceBody, meta: MetaType, links: LinksType) {
|
||||
public init(body: PrimaryResourceBody, meta: MetaType, links: LinksType) {
|
||||
self.body = .data(.init(primary: body, includes: .none, meta: meta, links: links))
|
||||
}
|
||||
}
|
||||
|
||||
extension Document where MetaType == NoMetadata {
|
||||
public init(body: ResourceBody, includes: Includes<Include>, links: LinksType) {
|
||||
public init(body: PrimaryResourceBody, includes: Includes<Include>, links: LinksType) {
|
||||
self.body = .data(.init(primary: body, includes: includes, meta: .none, links: links))
|
||||
}
|
||||
}
|
||||
|
||||
extension Document where LinksType == NoLinks {
|
||||
public init(body: ResourceBody, includes: Includes<Include>, meta: MetaType) {
|
||||
public init(body: PrimaryResourceBody, includes: Includes<Include>, meta: MetaType) {
|
||||
self.body = .data(.init(primary: body, includes: includes, meta: meta, links: .none))
|
||||
}
|
||||
}
|
||||
|
||||
extension Document where IncludeType == NoIncludes, LinksType == NoLinks {
|
||||
public init(body: ResourceBody, meta: MetaType) {
|
||||
public init(body: PrimaryResourceBody, meta: MetaType) {
|
||||
self.body = .data(.init(primary: body, includes: .none, meta: meta, links: .none))
|
||||
}
|
||||
}
|
||||
|
||||
extension Document where IncludeType == NoIncludes, MetaType == NoMetadata {
|
||||
public init(body: ResourceBody, links: LinksType) {
|
||||
public init(body: PrimaryResourceBody, links: LinksType) {
|
||||
self.body = .data(.init(primary: body, includes: .none, meta: .none, links: links))
|
||||
}
|
||||
}
|
||||
|
||||
extension Document where MetaType == NoMetadata, LinksType == NoLinks {
|
||||
public init(body: ResourceBody, includes: Includes<Include>) {
|
||||
public init(body: PrimaryResourceBody, includes: Includes<Include>) {
|
||||
self.body = .data(.init(primary: body, includes: includes, meta: .none, links: .none))
|
||||
}
|
||||
}
|
||||
|
||||
extension Document where IncludeType == NoIncludes, MetaType == NoMetadata, LinksType == NoLinks {
|
||||
public init(body: ResourceBody) {
|
||||
public init(body: PrimaryResourceBody) {
|
||||
self.body = .data(.init(primary: body, includes: .none, meta: .none, links: .none))
|
||||
}
|
||||
}
|
||||
@@ -174,11 +179,11 @@ extension Document {
|
||||
return
|
||||
}
|
||||
|
||||
let data: ResourceBody
|
||||
if let noData = NoResourceBody() as? ResourceBody {
|
||||
let data: PrimaryResourceBody
|
||||
if let noData = NoResourceBody() as? PrimaryResourceBody {
|
||||
data = noData
|
||||
} else {
|
||||
data = try container.decode(ResourceBody.self, forKey: .data)
|
||||
data = try container.decode(PrimaryResourceBody.self, forKey: .data)
|
||||
}
|
||||
|
||||
let maybeIncludes = try container.decodeIfPresent(Includes<Include>.self, forKey: .included)
|
||||
@@ -238,6 +243,23 @@ extension Document {
|
||||
|
||||
extension Document: CustomStringConvertible {
|
||||
public var description: String {
|
||||
return "Document(body: \(String(describing: body))"
|
||||
return "Document(\(String(describing: body)))"
|
||||
}
|
||||
}
|
||||
|
||||
extension Document.Body: CustomStringConvertible {
|
||||
public var description: String {
|
||||
switch self {
|
||||
case .errors(let errors, meta: let meta, links: let links):
|
||||
return "errors: \(String(describing: errors)), meta: \(String(describing: meta)), links: \(String(describing: links))"
|
||||
case .data(let data):
|
||||
return String(describing: data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Document.Body.Data: CustomStringConvertible {
|
||||
public var description: String {
|
||||
return "primary: \(String(describing: primary)), includes: \(String(describing: includes)), meta: \(String(describing: meta)), links: \(String(describing: links))"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
//
|
||||
// ResourceBody.swift
|
||||
// PrimaryResourceBody.swift
|
||||
// JSONAPI
|
||||
//
|
||||
// Created by Mathew Polzin on 11/10/18.
|
||||
@@ -80,12 +80,12 @@ extension ManyResourceBody {
|
||||
|
||||
extension SingleResourceBody: CustomStringConvertible {
|
||||
public var description: String {
|
||||
return "ResourceBody(\(String(describing: value)))"
|
||||
return "PrimaryResourceBody(\(String(describing: value)))"
|
||||
}
|
||||
}
|
||||
|
||||
extension ManyResourceBody: CustomStringConvertible {
|
||||
public var description: String {
|
||||
return "ResourceBody(\(String(describing: values)))"
|
||||
return "PrimaryResourceBody(\(String(describing: values)))"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,9 +9,11 @@
|
||||
public protocol Links: Codable, Equatable {}
|
||||
|
||||
/// Use NoLinks where no links should belong to a JSON API component
|
||||
public struct NoLinks: Links {
|
||||
public struct NoLinks: Links, CustomStringConvertible {
|
||||
public static var none: NoLinks { return NoLinks() }
|
||||
public init() {}
|
||||
|
||||
public var description: String { return "No Links" }
|
||||
}
|
||||
|
||||
public protocol JSONAPIURL: Codable, Equatable {}
|
||||
|
||||
@@ -19,8 +19,10 @@ public protocol Meta: Codable, Equatable {
|
||||
// nullable.
|
||||
extension Optional: Meta where Wrapped: Meta {}
|
||||
|
||||
public struct NoMetadata: Meta {
|
||||
public struct NoMetadata: Meta, CustomStringConvertible {
|
||||
public static var none: NoMetadata { return NoMetadata() }
|
||||
|
||||
public init() { }
|
||||
|
||||
public var description: String { return "No Metadata" }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
//
|
||||
// Attribute+Functor.swift
|
||||
// JSONAPI
|
||||
//
|
||||
// Created by Mathew Polzin on 11/28/18.
|
||||
//
|
||||
|
||||
public extension TransformedAttribute {
|
||||
/// Map an Attribute to a new wrapped type.
|
||||
/// Note that the resulting Attribute will have no transformer, even if the
|
||||
/// source Attribute has a transformer.
|
||||
/// You are mapping the output of the source transform into
|
||||
/// the RawValue of a new transformerless Attribute.
|
||||
///
|
||||
/// Generally, this is the most useful operation. The transformer gives you
|
||||
/// control over the decoding of the Attribute, but once the Attribute exists,
|
||||
/// mapping on it is most useful for creating computed Attribute properties.
|
||||
public func map<T: Codable>(_ transform: (Transformer.To) throws -> T) rethrows -> Attribute<T> {
|
||||
return Attribute<T>(value: try transform(value))
|
||||
}
|
||||
}
|
||||
@@ -39,17 +39,33 @@ public protocol EntityDescription {
|
||||
/// specialization.
|
||||
public protocol EntityType: PrimaryResource {
|
||||
associatedtype Description: EntityDescription
|
||||
associatedtype Identifier: Equatable & Codable
|
||||
associatedtype EntityRawIdType: JSONAPI.MaybeRawId
|
||||
|
||||
typealias Id = JSONAPI.Id<EntityRawIdType, Self>
|
||||
|
||||
typealias Attributes = Description.Attributes
|
||||
typealias Relationships = Description.Relationships
|
||||
|
||||
/// The `Entity`'s Id. This can be of type `Unidentified` if
|
||||
/// the entity is being created clientside and the
|
||||
/// server is being asked to create a unique Id. Otherwise,
|
||||
/// this should be of a type conforming to `IdType`.
|
||||
var id: Id { get }
|
||||
|
||||
/// The JSON API compliant attributes of this `Entity`.
|
||||
var attributes: Attributes { get }
|
||||
|
||||
/// The JSON API compliant relationships of this `Entity`.
|
||||
var relationships: Relationships { get }
|
||||
}
|
||||
|
||||
public protocol IdentifiableEntityType: EntityType, Relatable where EntityRawIdType: JSONAPI.RawIdType {}
|
||||
|
||||
/// An `Entity` is a single model type that can be
|
||||
/// encoded to or decoded from a JSON API
|
||||
/// "Resource Object."
|
||||
/// See https://jsonapi.org/format/#document-resource-objects
|
||||
public struct Entity<Description: JSONAPI.EntityDescription, Identifier: JSONAPI.Identifier>: EntityType {
|
||||
public struct Entity<Description: JSONAPI.EntityDescription, EntityRawIdType: JSONAPI.MaybeRawId>: EntityType {
|
||||
|
||||
/// The JSON API compliant "type" of this `Entity`.
|
||||
public static var type: String { return Description.type }
|
||||
@@ -58,7 +74,7 @@ public struct Entity<Description: JSONAPI.EntityDescription, Identifier: JSONAPI
|
||||
/// the entity is being created clientside and the
|
||||
/// server is being asked to create a unique Id. Otherwise,
|
||||
/// this should be of a type conforming to `IdType`.
|
||||
public let id: Identifier
|
||||
public let id: Entity.Id
|
||||
|
||||
/// The JSON API compliant attributes of this `Entity`.
|
||||
public let attributes: Description.Attributes
|
||||
@@ -66,13 +82,18 @@ public struct Entity<Description: JSONAPI.EntityDescription, Identifier: JSONAPI
|
||||
/// The JSON API compliant relationships of this `Entity`.
|
||||
public let relationships: Description.Relationships
|
||||
|
||||
public init(id: Identifier, attributes: Description.Attributes, relationships: Description.Relationships) {
|
||||
public init(id: Entity.Id, attributes: Description.Attributes, relationships: Description.Relationships) {
|
||||
self.id = id
|
||||
self.attributes = attributes
|
||||
self.relationships = relationships
|
||||
}
|
||||
}
|
||||
|
||||
extension Entity: IdentifiableEntityType, Relatable, WrappedRelatable where EntityRawIdType: JSONAPI.RawIdType {
|
||||
public typealias Identifier = Entity.Id
|
||||
public typealias WrappedIdentifier = Identifier
|
||||
}
|
||||
|
||||
extension Entity: CustomStringConvertible {
|
||||
public var description: String {
|
||||
return "Entity<\(Entity.type)>(id: \(String(describing: id)), attributes: \(String(describing: attributes)), relationships: \(String(describing: relationships)))"
|
||||
@@ -80,52 +101,52 @@ extension Entity: CustomStringConvertible {
|
||||
}
|
||||
|
||||
// MARK: Convenience initializers
|
||||
extension Entity where Identifier: CreatableIdType {
|
||||
extension Entity where EntityRawIdType: CreatableRawIdType {
|
||||
public init(attributes: Description.Attributes, relationships: Description.Relationships) {
|
||||
self.id = Identifier()
|
||||
self.id = Entity.Id()
|
||||
self.attributes = attributes
|
||||
self.relationships = relationships
|
||||
}
|
||||
}
|
||||
|
||||
extension Entity where Description.Attributes == NoAttributes {
|
||||
public init(id: Identifier, relationships: Description.Relationships) {
|
||||
public init(id: Entity.Id, relationships: Description.Relationships) {
|
||||
self.init(id: id, attributes: NoAttributes(), relationships: relationships)
|
||||
}
|
||||
}
|
||||
|
||||
extension Entity where Description.Attributes == NoAttributes, Identifier: CreatableIdType {
|
||||
extension Entity where Description.Attributes == NoAttributes, EntityRawIdType: CreatableRawIdType {
|
||||
public init(relationships: Description.Relationships) {
|
||||
self.init(attributes: NoAttributes(), relationships: relationships)
|
||||
}
|
||||
}
|
||||
|
||||
extension Entity where Description.Relationships == NoRelationships {
|
||||
public init(id: Identifier, attributes: Description.Attributes) {
|
||||
public init(id: Entity.Id, attributes: Description.Attributes) {
|
||||
self.init(id: id, attributes: attributes, relationships: NoRelationships())
|
||||
}
|
||||
}
|
||||
|
||||
extension Entity where Description.Relationships == NoRelationships, Identifier: CreatableIdType {
|
||||
extension Entity where Description.Relationships == NoRelationships, EntityRawIdType: CreatableRawIdType {
|
||||
public init(attributes: Description.Attributes) {
|
||||
self.init(attributes: attributes, relationships: NoRelationships())
|
||||
}
|
||||
}
|
||||
|
||||
extension Entity where Description.Attributes == NoAttributes, Description.Relationships == NoRelationships {
|
||||
public init(id: Identifier) {
|
||||
public init(id: Entity.Id) {
|
||||
self.init(id: id, attributes: NoAttributes(), relationships: NoRelationships())
|
||||
}
|
||||
}
|
||||
|
||||
extension Entity where Description.Attributes == NoAttributes, Description.Relationships == NoRelationships, Identifier: CreatableIdType {
|
||||
extension Entity where Description.Attributes == NoAttributes, Description.Relationships == NoRelationships, EntityRawIdType: CreatableRawIdType {
|
||||
public init() {
|
||||
self.init(attributes: NoAttributes(), relationships: NoRelationships())
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Pointer for Relationships use.
|
||||
public extension Entity where Identifier: IdType {
|
||||
public extension Entity where EntityRawIdType: JSONAPI.RawIdType {
|
||||
/// Get a pointer to this entity that can be used as a
|
||||
/// relationship to another entity.
|
||||
public var pointer: ToOneRelationship<Entity> {
|
||||
@@ -190,7 +211,7 @@ public extension Entity {
|
||||
|
||||
try container.encode(Entity.type, forKey: .type)
|
||||
|
||||
if Identifier.self != Unidentified<Description>.self {
|
||||
if EntityRawIdType.self != Unidentified.self {
|
||||
try container.encode(id, forKey: .id)
|
||||
}
|
||||
|
||||
@@ -212,8 +233,9 @@ public extension Entity {
|
||||
guard Entity.type == type else {
|
||||
throw JSONAPIEncodingError.typeMismatch(expected: Description.type, found: type)
|
||||
}
|
||||
|
||||
id = try (Unidentified<Description>() as? Identifier) ?? container.decode(Identifier.self, forKey: .id)
|
||||
|
||||
let maybeUnidentified = Unidentified() as? EntityRawIdType
|
||||
id = try maybeUnidentified.map { Entity.Id(rawValue: $0) } ?? container.decode(Entity.Id.self, forKey: .id)
|
||||
|
||||
attributes = try (NoAttributes() as? Description.Attributes) ?? container.decode(Description.Attributes.self, forKey: .attributes)
|
||||
|
||||
|
||||
@@ -5,10 +5,16 @@
|
||||
// Created by Mathew Polzin on 7/24/18.
|
||||
//
|
||||
|
||||
/// All types that are RawIdType and additionally
|
||||
/// Unidentified conform to this protocol. You
|
||||
/// should not add conformance to this protocol
|
||||
/// directly.
|
||||
public protocol MaybeRawId: Codable, Equatable {}
|
||||
|
||||
/// Any type that you would like to be encoded to and
|
||||
/// decoded from JSON API ids should conform to this
|
||||
/// protocol. Conformance for `String` is given.
|
||||
public protocol RawIdType: Codable, Equatable {}
|
||||
public protocol RawIdType: MaybeRawId, Hashable {}
|
||||
|
||||
/// If you would like to be able to create new
|
||||
/// Entities with Ids backed by a RawIdType then
|
||||
@@ -22,19 +28,18 @@ public protocol CreatableRawIdType: RawIdType {
|
||||
|
||||
extension String: RawIdType {}
|
||||
|
||||
public protocol Identifier: Codable, Equatable {
|
||||
associatedtype EntityDescription: JSONAPI.EntityDescription
|
||||
}
|
||||
|
||||
public struct Unidentified<EntityDescription: JSONAPI.EntityDescription>: Identifier, CustomStringConvertible {
|
||||
public struct Unidentified: MaybeRawId, CustomStringConvertible {
|
||||
public init() {}
|
||||
|
||||
public var description: String { return "Id(Unidentified)" }
|
||||
public var description: String { return "Unidentified" }
|
||||
}
|
||||
|
||||
public protocol IdType: Identifier, CustomStringConvertible {
|
||||
associatedtype RawType: RawIdType
|
||||
|
||||
public protocol MaybeId: Codable {
|
||||
associatedtype EntityType: JSONAPI.EntityType
|
||||
associatedtype RawType: MaybeRawId
|
||||
}
|
||||
|
||||
public protocol IdType: MaybeId, CustomStringConvertible, Hashable where RawType: RawIdType {
|
||||
var rawValue: RawType { get }
|
||||
}
|
||||
|
||||
@@ -48,27 +53,33 @@ public protocol CreatableIdType: IdType {
|
||||
|
||||
/// An Entity ID. These IDs can be encoded to or decoded from
|
||||
/// JSON API IDs.
|
||||
public struct Id<RawType: RawIdType, EntityDescription: JSONAPI.EntityDescription>: IdType {
|
||||
public struct Id<RawType: MaybeRawId, EntityType: JSONAPI.EntityType>: Codable, Equatable, MaybeId {
|
||||
|
||||
public let rawValue: RawType
|
||||
|
||||
public init(rawValue: RawType) {
|
||||
self.rawValue = rawValue
|
||||
}
|
||||
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.singleValueContainer()
|
||||
rawValue = try container.decode(RawType.self)
|
||||
}
|
||||
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.singleValueContainer()
|
||||
try container.encode(rawValue)
|
||||
}
|
||||
}
|
||||
|
||||
extension Id: Hashable, CustomStringConvertible, IdType where RawType: RawIdType {}
|
||||
|
||||
extension Id: CreatableIdType where RawType: CreatableRawIdType {
|
||||
public init() {
|
||||
rawValue = .unique()
|
||||
}
|
||||
}
|
||||
|
||||
extension Id where RawType == Unidentified {
|
||||
public static var unidentified: Id { return .init(rawValue: Unidentified()) }
|
||||
}
|
||||
|
||||
@@ -98,7 +98,7 @@ extension Poly1: CustomStringConvertible {
|
||||
case .a(let a):
|
||||
str = String(describing: a)
|
||||
}
|
||||
return "Include(\(str))"
|
||||
return "Poly(\(str))"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -177,7 +177,7 @@ extension Poly2: CustomStringConvertible {
|
||||
case .b(let b):
|
||||
str = String(describing: b)
|
||||
}
|
||||
return "Include(\(str))"
|
||||
return "Poly(\(str))"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -271,7 +271,7 @@ extension Poly3: CustomStringConvertible {
|
||||
case .c(let c):
|
||||
str = String(describing: c)
|
||||
}
|
||||
return "Include(\(str))"
|
||||
return "Poly(\(str))"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -380,7 +380,7 @@ extension Poly4: CustomStringConvertible {
|
||||
case .d(let d):
|
||||
str = String(describing: d)
|
||||
}
|
||||
return "Include(\(str))"
|
||||
return "Poly(\(str))"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -504,7 +504,7 @@ extension Poly5: CustomStringConvertible {
|
||||
case .e(let e):
|
||||
str = String(describing: e)
|
||||
}
|
||||
return "Include(\(str))"
|
||||
return "Poly(\(str))"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -643,6 +643,6 @@ extension Poly6: CustomStringConvertible {
|
||||
case .f(let f):
|
||||
str = String(describing: f)
|
||||
}
|
||||
return "Include(\(str))"
|
||||
return "Poly(\(str))"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,13 +21,13 @@ public struct ToOneRelationship<Relatable: JSONAPI.OptionalRelatable>: Relations
|
||||
}
|
||||
|
||||
extension ToOneRelationship where Relatable.WrappedIdentifier == Relatable.Identifier {
|
||||
public init(entity: Entity<Relatable.Description, Relatable.Identifier>) {
|
||||
public init<E: EntityType>(entity: E) where E.Description == Relatable.Description, E.Id == Relatable.Identifier {
|
||||
id = entity.id
|
||||
}
|
||||
}
|
||||
|
||||
extension ToOneRelationship where Relatable.WrappedIdentifier == Optional<Relatable.Identifier> {
|
||||
public init(entity: Entity<Relatable.Description, Relatable.Identifier>?) {
|
||||
extension ToOneRelationship where Relatable.WrappedIdentifier == Relatable.Identifier? {
|
||||
public init<E: EntityType>(entity: E?) where E.Description == Relatable.Description, E.Id == Relatable.Identifier {
|
||||
id = entity?.id
|
||||
}
|
||||
}
|
||||
@@ -40,6 +40,10 @@ public struct ToManyRelationship<Relatable: JSONAPI.Relatable>: RelationshipType
|
||||
|
||||
public let ids: [Relatable.Identifier]
|
||||
|
||||
public init(ids: [Relatable.Identifier]) {
|
||||
self.ids = ids
|
||||
}
|
||||
|
||||
public init<T: JSONAPI.Relatable>(relationships: [ToOneRelationship<T>]) where T.WrappedIdentifier == Relatable.Identifier {
|
||||
ids = relationships.map { $0.id }
|
||||
}
|
||||
@@ -54,7 +58,7 @@ public struct ToManyRelationship<Relatable: JSONAPI.Relatable>: RelationshipType
|
||||
}
|
||||
|
||||
extension ToManyRelationship {
|
||||
public init(entities: [Entity<Relatable.Description, Relatable.Identifier>]) {
|
||||
public init<E: EntityType>(entities: [E]) where E.Description == Relatable.Description, E.Id == Relatable.Identifier {
|
||||
ids = entities.map { $0.id }
|
||||
}
|
||||
}
|
||||
@@ -72,10 +76,6 @@ public typealias OptionalRelatable = WrappedRelatable
|
||||
/// has an IdType Identifier
|
||||
public protocol Relatable: WrappedRelatable {}
|
||||
|
||||
extension Entity: Relatable, WrappedRelatable where Identifier: JSONAPI.IdType {
|
||||
public typealias WrappedIdentifier = Identifier
|
||||
}
|
||||
|
||||
extension Optional: OptionalRelatable where Wrapped: Relatable {
|
||||
public typealias Description = Wrapped.Description
|
||||
public typealias Identifier = Wrapped.Identifier
|
||||
@@ -180,5 +180,5 @@ extension ToOneRelationship: CustomStringConvertible {
|
||||
}
|
||||
|
||||
extension ToManyRelationship: CustomStringConvertible {
|
||||
public var description: String { return "Relationship(\(String(describing: ids)))" }
|
||||
public var description: String { return "Relationship([\(ids.map(String.init(describing:)).joined(separator: ", "))])" }
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ public extension Entity {
|
||||
public static func check(_ entity: Entity) throws {
|
||||
var problems = [EntityCheckError]()
|
||||
|
||||
if Swift.type(of: entity.id).EntityDescription.self != Description.self {
|
||||
if Swift.type(of: entity.id).EntityType.Description.self != Description.self {
|
||||
problems.append(.badId)
|
||||
}
|
||||
|
||||
|
||||
@@ -12,3 +12,11 @@ extension ToOneRelationship: ExpressibleByNilLiteral where Relatable.WrappedIden
|
||||
self.init(id: Relatable.WrappedIdentifier(nilLiteral: ()))
|
||||
}
|
||||
}
|
||||
|
||||
extension ToManyRelationship: ExpressibleByArrayLiteral {
|
||||
public typealias ArrayLiteralElement = Relatable.Identifier
|
||||
|
||||
public init(arrayLiteral elements: ArrayLiteralElement...) {
|
||||
self.init(ids: elements)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
//
|
||||
// Attribute+FunctorTests.swift
|
||||
// JSONAPITests
|
||||
//
|
||||
// Created by Mathew Polzin on 11/28/18.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
import JSONAPI
|
||||
import JSONAPITestLib
|
||||
|
||||
class Attribute_FunctorTests: XCTestCase {
|
||||
func test_mapGuaranteed() {
|
||||
let entity = try? TestType(attributes: .init(name: "Frankie", number: .init(rawValue: 22.0)))
|
||||
|
||||
XCTAssertNotNil(entity)
|
||||
|
||||
XCTAssertEqual(entity?[\.computedString], "Frankie2")
|
||||
}
|
||||
|
||||
func test_mapOptionalSuccess() {
|
||||
let entity = try? TestType(attributes: .init(name: "Frankie", number: .init(rawValue: 22.0)))
|
||||
|
||||
XCTAssertNotNil(entity)
|
||||
|
||||
XCTAssertEqual(entity?[\.computedNumber], 22)
|
||||
}
|
||||
|
||||
func test_mapOptionalFailure() {
|
||||
let entity = try? TestType(attributes: .init(name: "Frankie", number: .init(rawValue: 22.5)))
|
||||
|
||||
XCTAssertNotNil(entity)
|
||||
|
||||
XCTAssertNil(entity?[\.computedNumber])
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Test types
|
||||
extension Attribute_FunctorTests {
|
||||
enum TestTypeDescription: EntityDescription {
|
||||
public static var type: String { return "test" }
|
||||
|
||||
public struct Attributes: JSONAPI.Attributes {
|
||||
let name: Attribute<String>
|
||||
let number: TransformedAttribute<Double, DoubleToString>
|
||||
var computedString: Attribute<String> {
|
||||
return name.map { $0 + "2" }
|
||||
}
|
||||
var computedNumber: Attribute<Int>? {
|
||||
return try? number.map { string in
|
||||
let num = Double(string).flatMap { Int(exactly: $0) }
|
||||
guard let ret = num else {
|
||||
throw DecodingError.typeMismatch(Int.self, .init(codingPath: [], debugDescription: "String was not an Int."))
|
||||
}
|
||||
return ret
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public typealias Relationships = NoRelationships
|
||||
}
|
||||
|
||||
typealias TestType = Entity<TestTypeDescription>
|
||||
|
||||
enum DoubleToString: Transformer {
|
||||
public static func transform(_ from: Double) -> String {
|
||||
return String(from)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
//
|
||||
// ComputedPropertiesTests.swift
|
||||
// JSONAPITests
|
||||
//
|
||||
// Created by Mathew Polzin on 11/28/18.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
import JSONAPI
|
||||
import JSONAPITestLib
|
||||
|
||||
class ComputedPropertiesTests: XCTestCase {
|
||||
func test_DecodeIgnoresComputed() {
|
||||
let entity = decoded(type: TestType.self, data: computed_property_attribute)
|
||||
|
||||
XCTAssertEqual(entity.id, "1234")
|
||||
XCTAssertEqual(entity[\.name], "Sarah")
|
||||
XCTAssertEqual(entity ~> \.other, "5678")
|
||||
XCTAssertNoThrow(try TestType.check(entity))
|
||||
}
|
||||
|
||||
func test_EncodeIgnoresComputed() {
|
||||
test_DecodeEncodeEquality(type: TestType.self, data: computed_property_attribute)
|
||||
}
|
||||
|
||||
func test_ComputedAttributeAccess() {
|
||||
let entity = decoded(type: TestType.self, data: computed_property_attribute)
|
||||
|
||||
XCTAssertEqual(entity[\.computed], "Sarah2")
|
||||
}
|
||||
|
||||
func test_ComputedRelationshipAccess() {
|
||||
let entity = decoded(type: TestType.self, data: computed_property_attribute)
|
||||
|
||||
XCTAssertEqual(entity ~> \.computed, "5678")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Test types
|
||||
extension ComputedPropertiesTests {
|
||||
public enum TestTypeDescription: EntityDescription {
|
||||
public static var type: String { return "test" }
|
||||
|
||||
public struct Attributes: JSONAPI.Attributes {
|
||||
public let name: Attribute<String>
|
||||
public var computed: Attribute<String> {
|
||||
return name.map { $0 + "2" }
|
||||
}
|
||||
}
|
||||
|
||||
public struct Relationships: JSONAPI.Relationships {
|
||||
public let other: ToOneRelationship<TestType>
|
||||
|
||||
public var computed: ToOneRelationship<TestType> {
|
||||
return other
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public typealias TestType = Entity<TestTypeDescription>
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
//
|
||||
// ComputedPropertiesStubs.swift
|
||||
// JSONAPITests
|
||||
//
|
||||
// Created by Mathew Polzin on 11/28/18.
|
||||
//
|
||||
|
||||
let computed_property_attribute = """
|
||||
{
|
||||
"id": "1234",
|
||||
"type": "test",
|
||||
"attributes": {
|
||||
"name": "Sarah"
|
||||
},
|
||||
"relationships": {
|
||||
"other": {
|
||||
"data": {
|
||||
"id": "5678",
|
||||
"type": "test"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
""".data(using: .utf8)!
|
||||
@@ -294,7 +294,7 @@ extension EntityTests {
|
||||
data: entity_unidentified)
|
||||
|
||||
XCTAssertNil(entity[\.me])
|
||||
XCTAssertEqual(entity.id, Unidentified())
|
||||
XCTAssertEqual(entity.id, .unidentified)
|
||||
XCTAssertNoThrow(try UnidentifiedTestEntity.check(entity))
|
||||
}
|
||||
|
||||
@@ -308,7 +308,7 @@ extension EntityTests {
|
||||
data: entity_unidentified_with_attributes)
|
||||
|
||||
XCTAssertEqual(entity[\.me], "unknown")
|
||||
XCTAssertEqual(entity.id, Unidentified())
|
||||
XCTAssertEqual(entity.id, .unidentified)
|
||||
XCTAssertNoThrow(try UnidentifiedTestEntity.check(entity))
|
||||
}
|
||||
|
||||
@@ -524,13 +524,13 @@ extension EntityTests {
|
||||
}
|
||||
}
|
||||
|
||||
extension Entity where Description == EntityTests.TestEntityType2, Identifier: CreatableIdType {
|
||||
extension Entity where Description == EntityTests.TestEntityType2, EntityRawIdType: CreatableRawIdType {
|
||||
init(other: ToOneRelationship<EntityTests.TestEntity1>) {
|
||||
self.init(relationships: .init(other: other))
|
||||
}
|
||||
}
|
||||
|
||||
extension Entity where Description == EntityTests.TestEntityType3, Identifier: CreatableIdType {
|
||||
extension Entity where Description == EntityTests.TestEntityType3, EntityRawIdType: CreatableRawIdType {
|
||||
init(others: ToManyRelationship<EntityTests.TestEntity1>) {
|
||||
self.init(relationships: .init(others: others))
|
||||
}
|
||||
|
||||
+2
-7
@@ -12,11 +12,6 @@ import JSONAPITestLib
|
||||
// Successes are fairly well-checked by the EntityTests. We will confirm failure cases are working
|
||||
// in this file.
|
||||
class EntityCheckTests: XCTestCase {
|
||||
func test_FailsWithBadId() {
|
||||
let entity = BadIdEntity()
|
||||
XCTAssertThrowsError(try BadIdEntity.check(entity))
|
||||
}
|
||||
|
||||
func test_failsWithEnumAttributes() {
|
||||
let entity = EnumAttributesEntity(attributes: .hello)
|
||||
XCTAssertThrowsError(try EnumAttributesEntity.check(entity))
|
||||
@@ -61,7 +56,7 @@ extension EntityCheckTests {
|
||||
public typealias Relationships = NoRelationships
|
||||
}
|
||||
|
||||
public typealias BadIdEntity = JSONAPI.Entity<OkDescription, JSONAPI.Id<String, OtherOkDescription>>
|
||||
public typealias OtherOkEntity = Entity<OtherOkDescription>
|
||||
|
||||
enum EnumAttributesDescription: EntityDescription {
|
||||
public static var type: String { return "hello" }
|
||||
@@ -121,7 +116,7 @@ extension EntityCheckTests {
|
||||
|
||||
public struct Relationships: JSONAPI.Relationships {
|
||||
let x: ToOneRelationship<OkEntity>
|
||||
let y: Id<String, OkDescription>
|
||||
let y: Id<String, OkEntity>
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user