Initial commit of files taken from a package started as part of another project.

This commit is contained in:
Mathew Polzin
2018-11-12 20:36:03 -08:00
commit 34c82b135f
29 changed files with 2324 additions and 0 deletions
+126
View File
@@ -0,0 +1,126 @@
//
// AnyEntity.swift
// ElevatedCore
//
// Created by Mathew Polzin on 11/5/18.
//
import Foundation
//private class AnyRelationshipBase<Entity: ElevatedCore.Entity>: Relationship {
//
// init() {
// guard type(of: self) != AnyRelationshipBase.self else {
// fatalError("Must use subclasses")
// }
// }
//
// var ids: [Id<Entity>] {
// fatalError("Implemented by subclasses")
// }
//
// static func == (lhs: AnyRelationshipBase<Entity>, rhs: AnyRelationshipBase<Entity>) -> Bool {
// fatalError("Implemented by subclasses")
// }
//}
//
//private final class AnyRelationshipBox<Concrete: Relationship>: AnyRelationshipBase<Concrete.Entity> {
// let concrete: Concrete
//
// init(_ concrete: Concrete) {
// self.concrete = concrete
// super.init()
// }
//
// override func encode(to encoder: Encoder) throws {
// try concrete.encode(to: encoder)
// }
//
// override var ids: [Id<Concrete.Entity>] {
// return concrete.ids
// }
//
// static func == (lhs: AnyRelationshipBox<Concrete>, rhs: AnyRelationshipBox<Concrete>) -> Bool {
// return lhs.concrete == rhs.concrete
// }
//}
//
//public final class AnyRelationship<Entity: ElevatedCore.Entity>: Relationship {
// private let box: AnyRelationshipBase<Entity>
//
// public init<Concrete: Relationship>(_ concrete: Concrete) where Concrete.Entity == Entity {
// box = AnyRelationshipBox(concrete)
// }
//
// public func encode(to encoder: Encoder) throws {
// try box.encode(to: encoder)
// }
//
// public var ids: [Id<Entity>] {
// return box.ids
// }
//
// public static func == (lhs: AnyRelationship<Entity>, rhs: AnyRelationship<Entity>) -> Bool {
// return lhs.box == rhs.box
// }
//}
//
//private class AnyEntityBase<Attributes: Equatable & Encodable, Relationships: Equatable & Encodable, ID: IdType>: Entity {
//
// init() {
// guard Swift.type(of: self) != AnyEntityBase.self else {
// fatalError("Must use subclasses")
// }
// }
//
// class var type: String { fatalError("Implemented by subclasses") }
// var id: Id<AnyEntity> { fatalError("Implemented by subclasses") }
//
// var attributes: Attributes { fatalError("Implemented by subclasses") }
// var relationships: Relationships { fatalError("Implemented by subclasses") }
//
// static func == (lhs: AnyEntityBase<Attributes, Relationships, ID>, rhs: AnyEntityBase<Attributes, Relationships, ID>) -> Bool {
// fatalError("Implemented by subclasses")
// }
//}
//
//private final class AnyEntityBox<Concrete: Entity>: AnyEntityBase<Concrete.Attributes, Concrete.Relationships, Concrete.ID> {
//
// let concrete: Concrete
//
// init(_ concrete: Concrete) {
// self.concrete = concrete
// super.init()
// }
//
// override class var type: String {
// return Concrete.type
// }
//
// override var id: Id<AnyEntity> {
// return concrete.id
// }
//
// override var attributes: Concrete.Attributes {
// return concrete.attributes
// }
//
// override var relationships: Concrete.Relationships {
// return concrete.relationships
// }
//
// static func == (lhs: AnyEntityBox<Concrete>, rhs: AnyEntityBox<Concrete>) -> Bool {
// return lhs.concrete == rhs.concrete
// }
//}
//
//public final class AnyEntity<Attributes: Equatable & Encodable, Relationships: Equatable & Encodable, ID: IdType>: Entity {
// private let box: AnyEntityBase<Attributes, Relationships, ID>
//
// init<Concrete: Entity>(_ concrete: Concrete) where Concrete.Attributes == Attributes, Concrete.Relationships == Relationships, Concrete.ID == ID {
// box = AnyEntityBox(concrete)
// }
//
// public class var type: String { return Swift.type(of: box).type }
//}
@@ -0,0 +1,73 @@
//
// JSONAPIDocument.swift
// ElevatedCore
//
// Created by Mathew Polzin on 11/5/18.
//
import Foundation
/// A JSON API Document represents the entire body
/// of a JSON API request or the entire body of
/// a JSON API response.
/// Note that this type uses Camel case. If your
/// API uses snake case, you will want to use
/// a conversion such as the one offerred by the
/// Foundation JSONEncoder/Decoder: `KeyDecodingStrategy`
public struct JSONAPIDocument<Body: ResourceBody, Include: IncludeDecoder, Error: JSONAPIError & Decodable> {
public let body: Data
// public let meta: Meta?
// public let jsonApi: APIDescription?
// public let links: Links?
public enum Data {
case errors([Error])
case data(Body, included: Includes<Include>)
public var isError: Bool {
guard case .errors = self else { return false }
return true
}
public var data: (Body, included: Includes<Include>)? {
guard case let .data(body, included: includes) = self else { return nil }
return (body, included: includes)
}
}
}
extension JSONAPIDocument: Decodable {
private enum RootCodingKeys: String, CodingKey {
case data
case errors
case included
case meta
case links
case jsonapi
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: RootCodingKeys.self)
let maybeData = try container.decodeIfPresent(Body.self, forKey: .data)
let maybeIncludes = try container.decodeIfPresent(Includes<Include>.self, forKey: .included)
let errors = try container.decodeIfPresent([Error].self, forKey: .errors)
assert(!(maybeData != nil && errors != nil), "JSON API Spec dictates data and errors will not both be present.")
assert((maybeIncludes == nil || maybeData != nil), "JSON API Spec dictates that includes will not be present if data is not present.")
// TODO come back to this and make robust
if let errors = errors {
body = .errors(errors)
return
}
guard let data = maybeData else {
body = .errors([.unknown]) // TODO: this should be more descriptive
return
}
body = .data(data, included: maybeIncludes ?? Includes<Include>.none)
}
}
@@ -0,0 +1,25 @@
//
// JSONAPI_Error.swift
// ElevatedCore
//
// Created by Mathew Polzin on 11/10/18.
//
import Foundation
public protocol JSONAPIError: Swift.Error {
static var unknown: Self { get }
}
// TODO: remove temp error stuff below
public enum TmpError: JSONAPIError & Decodable {
case unknownError
public init(from decoder: Decoder) throws {
self = .unknown
}
public static var unknown: TmpError {
return .unknownError
}
}
@@ -0,0 +1,403 @@
//
// JSONAPI_Includes.swift
// JSONAPI
//
// Created by Mathew Polzin on 11/10/18.
//
import Foundation
import Result
public protocol IncludeDecoder: Decodable {}
public struct Includes<I: IncludeDecoder>: Decodable {
public static var none: Includes { return .init(values: []) }
let values: [I]
private init(values: [I]) {
self.values = values
}
public init(from decoder: Decoder) throws {
var container = try decoder.unkeyedContainer()
var valueAggregator = [I]()
while !container.isAtEnd {
valueAggregator.append(try container.decode(I.self))
}
values = valueAggregator
}
public var count: Int {
return values.count
}
}
// MARK: - Decoding
func decode<EntityType: JSONAPI.EntityType>(_ type: EntityType.Type, from container: SingleValueDecodingContainer) throws -> Result<Entity<EntityType>, EncodingError> {
let ret: Result<Entity<EntityType>, EncodingError>
do {
ret = try .success(container.decode(Entity<EntityType>.self))
} catch (let err as EncodingError) {
ret = .failure(err)
} catch (let err) {
ret = .failure(EncodingError.invalidValue(EntityType.self,
.init(codingPath: container.codingPath,
debugDescription: err.localizedDescription,
underlyingError: err)))
}
return ret
}
// MARK: - 0 includes
public protocol _Include0: IncludeDecoder { }
public struct Include0: _Include0 {
public init(from decoder: Decoder) throws {
}
}
public typealias NoIncludes = Include0
// MARK: - 1 include
public protocol _Include1: _Include0 {
associatedtype A: EntityType
var a: Entity<A>? { get }
}
public enum Include1<A: EntityType>: _Include1 {
case a(Entity<A>)
public var a: Entity<A>? {
guard case let .a(ret) = self else { return nil }
return ret
}
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
self = .a(try container.decode(Entity<A>.self))
}
}
extension Includes where I: _Include1 {
public subscript(_ lookup: I.A.Type) -> [Entity<I.A>] {
return values.compactMap { $0.a }
}
public subscript(_ lookup: Entity<I.A>.Type) -> [Entity<I.A>] {
return values.compactMap { $0.a}
}
}
// MARK: - 2 includes
public protocol _Include2: _Include1 {
associatedtype B: EntityType
var b: Entity<B>? { get }
}
public enum Include2<A: EntityType, B: EntityType>: _Include2 {
case a(Entity<A>)
case b(Entity<B>)
public var a: Entity<A>? {
guard case let .a(ret) = self else { return nil }
return ret
}
public var b: Entity<B>? {
guard case let .b(ret) = self else { return nil }
return ret
}
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let attempts = [
try decode(A.self, from: container).map { Include2.a($0) },
try decode(B.self, from: container).map { Include2.b($0) } ]
let maybeVal: Include2<A, B>? = attempts
.compactMap { $0.value }
.first
guard let val = maybeVal else {
throw EncodingError.invalidValue(Include2<A, B>.self, .init(codingPath: decoder.codingPath, debugDescription: "Failed to find an include of the expected type. Attempts: \(attempts.map { $0.error }.compactMap { $0 })"))
}
self = val
}
}
extension Includes where I: _Include2 {
public subscript(_ lookup: I.B.Type) -> [Entity<I.B>] {
return values.compactMap { $0.b }
}
public subscript(_ lookup: Entity<I.B>.Type) -> [Entity<I.B>] {
return values.compactMap { $0.b}
}
}
// MARK: - 3 includes
public protocol _Include3: _Include2 {
associatedtype C: EntityType
var c: Entity<C>? { get }
}
public enum Include3<A: EntityType, B: EntityType, C: EntityType>: _Include3 {
case a(Entity<A>)
case b(Entity<B>)
case c(Entity<C>)
public var a: Entity<A>? {
guard case let .a(ret) = self else { return nil }
return ret
}
public var b: Entity<B>? {
guard case let .b(ret) = self else { return nil }
return ret
}
public var c: Entity<C>? {
guard case let .c(ret) = self else { return nil }
return ret
}
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let attempts = [
try decode(A.self, from: container).map { Include3.a($0) },
try decode(B.self, from: container).map { Include3.b($0) },
try decode(C.self, from: container).map { Include3.c($0) }]
let maybeVal: Include3<A, B, C>? = attempts
.compactMap { $0.value }
.first
guard let val = maybeVal else {
throw EncodingError.invalidValue(Include3<A, B, C>.self, .init(codingPath: decoder.codingPath, debugDescription: "Failed to find an include of the expected type. Attempts: \(attempts.map { $0.error }.compactMap { $0 })"))
}
self = val
}
}
extension Includes where I: _Include3 {
public subscript(_ lookup: I.C.Type) -> [Entity<I.C>] {
return values.compactMap { $0.c }
}
public subscript(_ lookup: Entity<I.C>.Type) -> [Entity<I.C>] {
return values.compactMap { $0.c}
}
}
// MARK: - 4 includes
public protocol _Include4: _Include3 {
associatedtype D: EntityType
var d: Entity<D>? { get }
}
public enum Include4<A: EntityType, B: EntityType, C: EntityType, D: EntityType>: _Include4 {
case a(Entity<A>)
case b(Entity<B>)
case c(Entity<C>)
case d(Entity<D>)
public var a: Entity<A>? {
guard case let .a(ret) = self else { return nil }
return ret
}
public var b: Entity<B>? {
guard case let .b(ret) = self else { return nil }
return ret
}
public var c: Entity<C>? {
guard case let .c(ret) = self else { return nil }
return ret
}
public var d: Entity<D>? {
guard case let .d(ret) = self else { return nil }
return ret
}
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let attempts = [
try decode(A.self, from: container).map { Include4.a($0) },
try decode(B.self, from: container).map { Include4.b($0) },
try decode(C.self, from: container).map { Include4.c($0) },
try decode(D.self, from: container).map { Include4.d($0) }]
let maybeVal: Include4<A, B, C, D>? = attempts
.compactMap { $0.value }
.first
guard let val = maybeVal else {
throw EncodingError.invalidValue(Include4<A, B, C, D>.self, .init(codingPath: decoder.codingPath, debugDescription: "Failed to find an include of the expected type. Attempts: \(attempts.map { $0.error }.compactMap { $0 })"))
}
self = val
}
}
extension Includes where I: _Include4 {
public subscript(_ lookup: I.D.Type) -> [Entity<I.D>] {
return values.compactMap { $0.d }
}
public subscript(_ lookup: Entity<I.D>.Type) -> [Entity<I.D>] {
return values.compactMap { $0.d}
}
}
// MARK: - 5 includes
public protocol _Include5: _Include4 {
associatedtype E: EntityType
var e: Entity<E>? { get }
}
public enum Include5<A: EntityType, B: EntityType, C: EntityType, D: EntityType, E: EntityType>: _Include5 {
case a(Entity<A>)
case b(Entity<B>)
case c(Entity<C>)
case d(Entity<D>)
case e(Entity<E>)
public var a: Entity<A>? {
guard case let .a(ret) = self else { return nil }
return ret
}
public var b: Entity<B>? {
guard case let .b(ret) = self else { return nil }
return ret
}
public var c: Entity<C>? {
guard case let .c(ret) = self else { return nil }
return ret
}
public var d: Entity<D>? {
guard case let .d(ret) = self else { return nil }
return ret
}
public var e: Entity<E>? {
guard case let .e(ret) = self else { return nil }
return ret
}
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let attempts = [
try decode(A.self, from: container).map { Include5.a($0) },
try decode(B.self, from: container).map { Include5.b($0) },
try decode(C.self, from: container).map { Include5.c($0) },
try decode(D.self, from: container).map { Include5.d($0) },
try decode(E.self, from: container).map { Include5.e($0) }]
let maybeVal: Include5<A, B, C, D, E>? = attempts
.compactMap { $0.value }
.first
guard let val = maybeVal else {
throw EncodingError.invalidValue(Include5<A, B, C, D, E>.self, .init(codingPath: decoder.codingPath, debugDescription: "Failed to find an include of the expected type. Attempts: \(attempts.map { $0.error }.compactMap { $0 })"))
}
self = val
}
}
extension Includes where I: _Include5 {
public subscript(_ lookup: I.E.Type) -> [Entity<I.E>] {
return values.compactMap { $0.e }
}
public subscript(_ lookup: Entity<I.E>.Type) -> [Entity<I.E>] {
return values.compactMap { $0.e}
}
}
// MARK: - 6 includes
public protocol _Include6: _Include5 {
associatedtype F: EntityType
var f: Entity<F>? { get }
}
public enum Include6<A: EntityType, B: EntityType, C: EntityType, D: EntityType, E: EntityType, F: EntityType>: _Include6 {
case a(Entity<A>)
case b(Entity<B>)
case c(Entity<C>)
case d(Entity<D>)
case e(Entity<E>)
case f(Entity<F>)
public var a: Entity<A>? {
guard case let .a(ret) = self else { return nil }
return ret
}
public var b: Entity<B>? {
guard case let .b(ret) = self else { return nil }
return ret
}
public var c: Entity<C>? {
guard case let .c(ret) = self else { return nil }
return ret
}
public var d: Entity<D>? {
guard case let .d(ret) = self else { return nil }
return ret
}
public var e: Entity<E>? {
guard case let .e(ret) = self else { return nil }
return ret
}
public var f: Entity<F>? {
guard case let .f(ret) = self else { return nil }
return ret
}
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let attempts = [
try decode(A.self, from: container).map { Include6.a($0) },
try decode(B.self, from: container).map { Include6.b($0) },
try decode(C.self, from: container).map { Include6.c($0) },
try decode(D.self, from: container).map { Include6.d($0) },
try decode(E.self, from: container).map { Include6.e($0) },
try decode(F.self, from: container).map { Include6.f($0) }]
let maybeVal: Include6<A, B, C, D, E, F>? = attempts
.compactMap { $0.value }
.first
guard let val = maybeVal else {
throw EncodingError.invalidValue(Include6<A, B, C, D, E, F>.self, .init(codingPath: decoder.codingPath, debugDescription: "Failed to find an include of the expected type. Attempts: \(attempts.map { $0.error }.compactMap { $0 })"))
}
self = val
}
}
extension Includes where I: _Include6 {
public subscript(_ lookup: I.F.Type) -> [Entity<I.F>] {
return values.compactMap { $0.f }
}
public subscript(_ lookup: Entity<I.F>.Type) -> [Entity<I.F>] {
return values.compactMap { $0.f}
}
}
@@ -0,0 +1,40 @@
//
// JSONAPI_ResourceBody.swift
// ElevatedCore
//
// Created by Mathew Polzin on 11/10/18.
//
import Foundation
public protocol ResourceBody: Decodable {
typealias Single<EntityType: JSONAPI.EntityType> = SingleResourceBody<EntityType>
typealias Many<EntityType: JSONAPI.EntityType> = ManyResourceBody<EntityType>
}
public struct SingleResourceBody<EntityType: JSONAPI.EntityType>: ResourceBody {
public let value: Entity<EntityType>?
}
public struct ManyResourceBody<EntityType: JSONAPI.EntityType>: ResourceBody {
public let values: [Entity<EntityType>]
}
// MARK: Decodable
extension SingleResourceBody {
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
value = try container.decode(Entity<EntityType>.self)
}
}
extension ManyResourceBody {
public init(from decoder: Decoder) throws {
var container = try decoder.unkeyedContainer()
var valueAggregator = [Entity<EntityType>]()
while !container.isAtEnd {
valueAggregator.append(try container.decode(Entity<EntityType>.self))
}
values = valueAggregator
}
}
+3
View File
@@ -0,0 +1,3 @@
struct JSONAPI {
var text = "Hello, World!"
}
@@ -0,0 +1,159 @@
//
// Entity.swift
// ElevatedCore
//
// Created by Mathew Polzin on 7/24/18.
//
public typealias Relatives = Codable & Equatable
public typealias Attributes = Codable & Equatable
/// Can be used as Relationships Type for Entities that do not
/// have any Relationships.
public struct NoRelatives: Relatives {}
/// Can be used as Attributes Type for Entities that do not
/// have any Attributes.
public struct NoAttributes: Attributes {}
public protocol EntityType {
associatedtype Identifier: JSONAPI.Identifier
associatedtype AttributeType: Attributes
associatedtype RelatedType: Relatives
static var type: String { get }
}
public protocol IdentifiedEntityType: EntityType where Identifier: IdType {}
/// 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
/// Easiest to use with `protocol MyEntity: Entity, Identified, Related, Attributed where ID = UUID`.
public struct Entity<EntityType: JSONAPI.EntityType>: Codable, Equatable {
public static var type: String { return EntityType.type }
public let id: EntityType.Identifier
public let attributes: EntityType.AttributeType
public let relationships: EntityType.RelatedType
public init(id: EntityType.Identifier, attributes: EntityType.AttributeType, relationships: EntityType.RelatedType) {
self.id = id
self.attributes = attributes
self.relationships = relationships
}
public init(attributes: EntityType.AttributeType, relationships: EntityType.RelatedType) {
self.id = .init()
self.attributes = attributes
self.relationships = relationships
}
}
extension Entity where EntityType.AttributeType == NoAttributes {
public init(id: EntityType.Identifier, relationships: EntityType.RelatedType) {
self.init(id: id, attributes: NoAttributes(), relationships: relationships)
}
public init(relationships: EntityType.RelatedType) {
self.init(attributes: NoAttributes(), relationships: relationships)
}
}
extension Entity where EntityType.RelatedType == NoRelatives {
public init(id: EntityType.Identifier, attributes: EntityType.AttributeType) {
self.init(id: id, attributes: attributes, relationships: NoRelatives())
}
public init(attributes: EntityType.AttributeType) {
self.init(attributes: attributes, relationships: NoRelatives())
}
}
extension Entity where EntityType.AttributeType == NoAttributes, EntityType.RelatedType == NoRelatives {
public init(id: EntityType.Identifier) {
self.init(id: id, attributes: NoAttributes(), relationships: NoRelatives())
}
public init() {
self.init(attributes: NoAttributes(), relationships: NoRelatives())
}
}
//public protocol IdentifiedEntityType: JSONAPI.EntityType where IdentifiedEntityType.Identifier: IdType, Identifier.Entity == Self {}
public extension Entity where EntityType.Identifier: IdType {
/// Get a pointer to this entity that can be used as a
/// relationship to another entity.
public var pointer: ToOneRelationship<EntityType> {
return ToOneRelationship(entity: self)
}
}
// MARK: Attribute Access
public extension Entity {
subscript<T>(_ path: KeyPath<EntityType.AttributeType, T>) -> T {
return attributes[keyPath: path]
}
}
// MARK: Relationship Access
public extension Entity {
public static func ~><OtherEntityType: JSONAPI.EntityType>(entity: Entity<EntityType>, path: KeyPath<EntityType.RelatedType, ToOneRelationship<OtherEntityType>>) -> OtherEntityType.Identifier {
return entity.relationships[keyPath: path].id
}
public static func ~><OtherEntityType: JSONAPI.EntityType>(entity: Entity<EntityType>, path: KeyPath<EntityType.RelatedType, ToManyRelationship<OtherEntityType>>) -> [OtherEntityType.Identifier] {
return entity.relationships[keyPath: path].ids
}
}
infix operator ~>
// MARK: - Codable
private enum ResourceObjectCodingKeys: String, CodingKey {
case type = "type"
case id = "id"
case attributes = "attributes"
case relationships = "relationships"
}
public extension Entity {
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: ResourceObjectCodingKeys.self)
try container.encode(Entity.type, forKey: .type)
if EntityType.Identifier.self != Unidentified.self {
try container.encode(id, forKey: .id)
}
if EntityType.AttributeType.self != NoAttributes.self {
try container.encode(attributes, forKey: .attributes)
}
if EntityType.RelatedType.self != NoRelatives.self {
try container.encode(relationships, forKey: .relationships)
}
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: ResourceObjectCodingKeys.self)
let type = try container.decode(String.self, forKey: .type)
guard Entity.type == type else {
throw JSONAPIEncodingError.typeMismatch(expected: EntityType.type, found: type)
}
id = try (Unidentified() as? EntityType.Identifier) ?? container.decode(EntityType.Identifier.self, forKey: .id)
attributes = try (NoAttributes() as? EntityType.AttributeType) ?? container.decode(EntityType.AttributeType.self, forKey: .attributes)
relationships = try (NoRelatives() as? EntityType.RelatedType) ?? container.decode(EntityType.RelatedType.self, forKey: .relationships)
}
}
+67
View File
@@ -0,0 +1,67 @@
//
// Id.swift
// ElevatedCore
//
// Created by Mathew Polzin on 7/24/18.
//
import Foundation
/// Any type that you would like to be encoded to and
/// decoded from JSON API ids should conform to this
/// protocol. Conformance for `String` and `UUID`
/// is given by this library.
public protocol RawIdType: Codable, Equatable {
static func unique() -> Self
}
public protocol Identifier: Codable, Equatable {
init()
}
public struct Unidentified: Identifier {
public init() {}
}
public protocol IdType: Identifier {
associatedtype EntityType: JSONAPI.EntityType
associatedtype RawType: RawIdType
var rawValue: RawType { get }
}
extension UUID: RawIdType {
public static func unique() -> UUID {
return UUID()
}
}
extension String: RawIdType {
public static func unique() -> String {
return UUID().uuidString
}
}
/// An Entity ID. These IDs can be encoded to or decoded from
/// JSON API IDs.
public struct Id<RawType: RawIdType, EntityType: JSONAPI.EntityType>: IdType {
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)
}
public init() {
rawValue = .unique()
}
}
@@ -0,0 +1,120 @@
//
// Relationship.swift
// ElevatedCore
//
// Created by Mathew Polzin on 8/31/18.
//
import Foundation
/// An Entity relationship that can be encoded to or decoded from
/// a JSON API "Resource Linkage."
/// You should use the `ToOneRelationship` and `ToManyRelationship`
/// concrete types.
/// See https://jsonapi.org/format/#document-resource-object-linkage
public protocol Relationship: Equatable, Encodable {
associatedtype EntityType: JSONAPI.EntityType where EntityType.Identifier: IdType
var ids: [EntityType.Identifier] { get }
}
/// An Entity relationship that can be encoded to or decoded from
/// a JSON API "Resource Linkage."
/// See https://jsonapi.org/format/#document-resource-object-linkage
/// A convenient typealias might make your code much more legible: `One<Entity>`
public struct ToOneRelationship<EntityType: JSONAPI.EntityType>: Equatable, Relationship, Decodable where EntityType.Identifier: IdType {
public let id: EntityType.Identifier
public init(entity: Entity<EntityType>) {
id = entity.id
}
public var ids: [EntityType.Identifier] {
return [id]
}
}
/// An Entity relationship that can be encoded to or decoded from
/// a JSON API "Resource Linkage."
/// See https://jsonapi.org/format/#document-resource-object-linkage
/// A convenient typealias might make your code much more legible: `Many<Entity>`
public struct ToManyRelationship<EntityType: JSONAPI.EntityType>: Equatable, Relationship, Decodable where EntityType.Identifier: IdType {
public let ids: [EntityType.Identifier]
public init(entities: [Entity<EntityType>]) {
ids = entities.map { $0.id }
}
public init<T: Relationship>(relationships: [T]) where T.EntityType == EntityType {
ids = relationships.flatMap { $0.ids }
}
}
// MARK: Codable
private enum ResourceLinkageCodingKeys: String, CodingKey {
case data = "data"
}
private enum ResourceIdentifierCodingKeys: String, CodingKey {
case id = "id"
case entityType = "type"
}
public enum JSONAPIEncodingError: Swift.Error {
case typeMismatch(expected: String, found: String)
}
extension ToOneRelationship {
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: ResourceLinkageCodingKeys.self)
let identifier = try container.nestedContainer(keyedBy: ResourceIdentifierCodingKeys.self, forKey: .data)
let type = try identifier.decode(String.self, forKey: .entityType)
guard type == EntityType.type else {
throw JSONAPIEncodingError.typeMismatch(expected: EntityType.type, found: type)
}
id = try identifier.decode(EntityType.Identifier.self, forKey: .id)
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: ResourceLinkageCodingKeys.self)
var identifier = container.nestedContainer(keyedBy: ResourceIdentifierCodingKeys.self, forKey: .data)
try identifier.encode(id, forKey: .id)
try identifier.encode(EntityType.type, forKey: .entityType)
}
}
extension ToManyRelationship {
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: ResourceLinkageCodingKeys.self)
var identifiers = try container.nestedUnkeyedContainer(forKey: .data)
var newIds = [EntityType.Identifier]()
while !identifiers.isAtEnd {
let identifier = try identifiers.nestedContainer(keyedBy: ResourceIdentifierCodingKeys.self)
let type = try identifier.decode(String.self, forKey: .entityType)
guard type == EntityType.type else {
throw JSONAPIEncodingError.typeMismatch(expected: EntityType.type, found: type)
}
newIds.append(try identifier.decode(EntityType.Identifier.self, forKey: .id))
}
ids = newIds
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: ResourceLinkageCodingKeys.self)
var identifiers = container.nestedUnkeyedContainer(forKey: .data)
for id in ids {
var identifier = identifiers.nestedContainer(keyedBy: ResourceIdentifierCodingKeys.self)
try identifier.encode(id, forKey: .id)
try identifier.encode(EntityType.type, forKey: .entityType)
}
}
}