commit 34c82b135f0d1af6fb2304285265101740387e4d Author: Mathew Polzin Date: Mon Nov 12 20:36:03 2018 -0800 Initial commit of files taken from a package started as part of another project. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..02c0875 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.DS_Store +/.build +/Packages +/*.xcodeproj diff --git a/Package.swift b/Package.swift new file mode 100644 index 0000000..ebffd2a --- /dev/null +++ b/Package.swift @@ -0,0 +1,28 @@ +// swift-tools-version:4.2 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "JSONAPI", + products: [ + // Products define the executables and libraries produced by a package, and make them visible to other packages. + .library( + name: "JSONAPI", + targets: ["JSONAPI"]), + ], + dependencies: [ + // Dependencies declare other packages that this package depends on. + // .package(url: /* package url */, from: "1.0.0"), + ], + targets: [ + // Targets are the basic building blocks of a package. A target can define a module or a test suite. + // Targets can depend on other targets in this package, and on products in packages which this package depends on. + .target( + name: "JSONAPI", + dependencies: []), + .testTarget( + name: "JSONAPITests", + dependencies: ["JSONAPI"]), + ] +) diff --git a/README.md b/README.md new file mode 100644 index 0000000..c3c6505 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# JSONAPI + +A description of this package. diff --git a/Sources/JSONAPI/AnyEntity.swift b/Sources/JSONAPI/AnyEntity.swift new file mode 100644 index 0000000..88f889c --- /dev/null +++ b/Sources/JSONAPI/AnyEntity.swift @@ -0,0 +1,126 @@ +// +// AnyEntity.swift +// ElevatedCore +// +// Created by Mathew Polzin on 11/5/18. +// + +import Foundation + +//private class AnyRelationshipBase: Relationship { +// +// init() { +// guard type(of: self) != AnyRelationshipBase.self else { +// fatalError("Must use subclasses") +// } +// } +// +// var ids: [Id] { +// fatalError("Implemented by subclasses") +// } +// +// static func == (lhs: AnyRelationshipBase, rhs: AnyRelationshipBase) -> Bool { +// fatalError("Implemented by subclasses") +// } +//} +// +//private final class AnyRelationshipBox: AnyRelationshipBase { +// 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] { +// return concrete.ids +// } +// +// static func == (lhs: AnyRelationshipBox, rhs: AnyRelationshipBox) -> Bool { +// return lhs.concrete == rhs.concrete +// } +//} +// +//public final class AnyRelationship: Relationship { +// private let box: AnyRelationshipBase +// +// public init(_ 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] { +// return box.ids +// } +// +// public static func == (lhs: AnyRelationship, rhs: AnyRelationship) -> Bool { +// return lhs.box == rhs.box +// } +//} +// +//private class AnyEntityBase: 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 { fatalError("Implemented by subclasses") } +// +// var attributes: Attributes { fatalError("Implemented by subclasses") } +// var relationships: Relationships { fatalError("Implemented by subclasses") } +// +// static func == (lhs: AnyEntityBase, rhs: AnyEntityBase) -> Bool { +// fatalError("Implemented by subclasses") +// } +//} +// +//private final class AnyEntityBox: AnyEntityBase { +// +// let concrete: Concrete +// +// init(_ concrete: Concrete) { +// self.concrete = concrete +// super.init() +// } +// +// override class var type: String { +// return Concrete.type +// } +// +// override var id: Id { +// return concrete.id +// } +// +// override var attributes: Concrete.Attributes { +// return concrete.attributes +// } +// +// override var relationships: Concrete.Relationships { +// return concrete.relationships +// } +// +// static func == (lhs: AnyEntityBox, rhs: AnyEntityBox) -> Bool { +// return lhs.concrete == rhs.concrete +// } +//} +// +//public final class AnyEntity: Entity { +// private let box: AnyEntityBase +// +// init(_ 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 } +//} + diff --git a/Sources/JSONAPI/Document/JSONAPI_Document.swift b/Sources/JSONAPI/Document/JSONAPI_Document.swift new file mode 100644 index 0000000..3fb522b --- /dev/null +++ b/Sources/JSONAPI/Document/JSONAPI_Document.swift @@ -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 { + 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) + + public var isError: Bool { + guard case .errors = self else { return false } + return true + } + + public var data: (Body, included: Includes)? { + 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.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.none) + } +} diff --git a/Sources/JSONAPI/Document/JSONAPI_Error.swift b/Sources/JSONAPI/Document/JSONAPI_Error.swift new file mode 100644 index 0000000..be9cd0d --- /dev/null +++ b/Sources/JSONAPI/Document/JSONAPI_Error.swift @@ -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 + } +} diff --git a/Sources/JSONAPI/Document/JSONAPI_Includes.swift b/Sources/JSONAPI/Document/JSONAPI_Includes.swift new file mode 100644 index 0000000..c927379 --- /dev/null +++ b/Sources/JSONAPI/Document/JSONAPI_Includes.swift @@ -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: 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(_ type: EntityType.Type, from container: SingleValueDecodingContainer) throws -> Result, EncodingError> { + let ret: Result, EncodingError> + do { + ret = try .success(container.decode(Entity.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? { get } +} +public enum Include1: _Include1 { + case a(Entity) + + public var a: Entity? { + 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.self)) + } +} + +extension Includes where I: _Include1 { + public subscript(_ lookup: I.A.Type) -> [Entity] { + return values.compactMap { $0.a } + } + + public subscript(_ lookup: Entity.Type) -> [Entity] { + return values.compactMap { $0.a} + } +} + +// MARK: - 2 includes +public protocol _Include2: _Include1 { + associatedtype B: EntityType + var b: Entity? { get } +} +public enum Include2: _Include2 { + case a(Entity) + case b(Entity) + + public var a: Entity? { + guard case let .a(ret) = self else { return nil } + return ret + } + + public var b: Entity? { + 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? = attempts + .compactMap { $0.value } + .first + + guard let val = maybeVal else { + throw EncodingError.invalidValue(Include2.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] { + return values.compactMap { $0.b } + } + + public subscript(_ lookup: Entity.Type) -> [Entity] { + return values.compactMap { $0.b} + } +} + +// MARK: - 3 includes +public protocol _Include3: _Include2 { + associatedtype C: EntityType + var c: Entity? { get } +} +public enum Include3: _Include3 { + case a(Entity) + case b(Entity) + case c(Entity) + + public var a: Entity? { + guard case let .a(ret) = self else { return nil } + return ret + } + + public var b: Entity? { + guard case let .b(ret) = self else { return nil } + return ret + } + + public var c: Entity? { + 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? = attempts + .compactMap { $0.value } + .first + + guard let val = maybeVal else { + throw EncodingError.invalidValue(Include3.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] { + return values.compactMap { $0.c } + } + + public subscript(_ lookup: Entity.Type) -> [Entity] { + return values.compactMap { $0.c} + } +} + +// MARK: - 4 includes +public protocol _Include4: _Include3 { + associatedtype D: EntityType + var d: Entity? { get } +} +public enum Include4: _Include4 { + case a(Entity) + case b(Entity) + case c(Entity) + case d(Entity) + + public var a: Entity? { + guard case let .a(ret) = self else { return nil } + return ret + } + + public var b: Entity? { + guard case let .b(ret) = self else { return nil } + return ret + } + + public var c: Entity? { + guard case let .c(ret) = self else { return nil } + return ret + } + + public var d: Entity? { + 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? = attempts + .compactMap { $0.value } + .first + + guard let val = maybeVal else { + throw EncodingError.invalidValue(Include4.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] { + return values.compactMap { $0.d } + } + + public subscript(_ lookup: Entity.Type) -> [Entity] { + return values.compactMap { $0.d} + } +} + +// MARK: - 5 includes +public protocol _Include5: _Include4 { + associatedtype E: EntityType + var e: Entity? { get } +} +public enum Include5: _Include5 { + case a(Entity) + case b(Entity) + case c(Entity) + case d(Entity) + case e(Entity) + + public var a: Entity? { + guard case let .a(ret) = self else { return nil } + return ret + } + + public var b: Entity? { + guard case let .b(ret) = self else { return nil } + return ret + } + + public var c: Entity? { + guard case let .c(ret) = self else { return nil } + return ret + } + + public var d: Entity? { + guard case let .d(ret) = self else { return nil } + return ret + } + + public var e: Entity? { + 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? = attempts + .compactMap { $0.value } + .first + + guard let val = maybeVal else { + throw EncodingError.invalidValue(Include5.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] { + return values.compactMap { $0.e } + } + + public subscript(_ lookup: Entity.Type) -> [Entity] { + return values.compactMap { $0.e} + } +} + +// MARK: - 6 includes +public protocol _Include6: _Include5 { + associatedtype F: EntityType + var f: Entity? { get } +} +public enum Include6: _Include6 { + case a(Entity) + case b(Entity) + case c(Entity) + case d(Entity) + case e(Entity) + case f(Entity) + + public var a: Entity? { + guard case let .a(ret) = self else { return nil } + return ret + } + + public var b: Entity? { + guard case let .b(ret) = self else { return nil } + return ret + } + + public var c: Entity? { + guard case let .c(ret) = self else { return nil } + return ret + } + + public var d: Entity? { + guard case let .d(ret) = self else { return nil } + return ret + } + + public var e: Entity? { + guard case let .e(ret) = self else { return nil } + return ret + } + + public var f: Entity? { + 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? = attempts + .compactMap { $0.value } + .first + + guard let val = maybeVal else { + throw EncodingError.invalidValue(Include6.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] { + return values.compactMap { $0.f } + } + + public subscript(_ lookup: Entity.Type) -> [Entity] { + return values.compactMap { $0.f} + } +} diff --git a/Sources/JSONAPI/Document/JSONAPI_ResourceBody.swift b/Sources/JSONAPI/Document/JSONAPI_ResourceBody.swift new file mode 100644 index 0000000..f04de5d --- /dev/null +++ b/Sources/JSONAPI/Document/JSONAPI_ResourceBody.swift @@ -0,0 +1,40 @@ +// +// JSONAPI_ResourceBody.swift +// ElevatedCore +// +// Created by Mathew Polzin on 11/10/18. +// + +import Foundation + +public protocol ResourceBody: Decodable { + typealias Single = SingleResourceBody + typealias Many = ManyResourceBody +} + +public struct SingleResourceBody: ResourceBody { + public let value: Entity? +} + +public struct ManyResourceBody: ResourceBody { + public let values: [Entity] +} + +// MARK: Decodable +extension SingleResourceBody { + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + value = try container.decode(Entity.self) + } +} + +extension ManyResourceBody { + public init(from decoder: Decoder) throws { + var container = try decoder.unkeyedContainer() + var valueAggregator = [Entity]() + while !container.isAtEnd { + valueAggregator.append(try container.decode(Entity.self)) + } + values = valueAggregator + } +} diff --git a/Sources/JSONAPI/JSONAPI.swift b/Sources/JSONAPI/JSONAPI.swift new file mode 100644 index 0000000..864d386 --- /dev/null +++ b/Sources/JSONAPI/JSONAPI.swift @@ -0,0 +1,3 @@ +struct JSONAPI { + var text = "Hello, World!" +} diff --git a/Sources/JSONAPI/Resource/JSONAPI_Entity.swift b/Sources/JSONAPI/Resource/JSONAPI_Entity.swift new file mode 100644 index 0000000..be70dd2 --- /dev/null +++ b/Sources/JSONAPI/Resource/JSONAPI_Entity.swift @@ -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: 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 { + return ToOneRelationship(entity: self) + } +} + +// MARK: Attribute Access +public extension Entity { + subscript(_ path: KeyPath) -> T { + return attributes[keyPath: path] + } +} + +// MARK: Relationship Access +public extension Entity { + public static func ~>(entity: Entity, path: KeyPath>) -> OtherEntityType.Identifier { + return entity.relationships[keyPath: path].id + } + + public static func ~>(entity: Entity, path: KeyPath>) -> [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) + } +} diff --git a/Sources/JSONAPI/Resource/JSONAPI_Id.swift b/Sources/JSONAPI/Resource/JSONAPI_Id.swift new file mode 100644 index 0000000..a27cc8e --- /dev/null +++ b/Sources/JSONAPI/Resource/JSONAPI_Id.swift @@ -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: 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() + } +} diff --git a/Sources/JSONAPI/Resource/JSONAPI_Relationship.swift b/Sources/JSONAPI/Resource/JSONAPI_Relationship.swift new file mode 100644 index 0000000..8d9de41 --- /dev/null +++ b/Sources/JSONAPI/Resource/JSONAPI_Relationship.swift @@ -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` +public struct ToOneRelationship: Equatable, Relationship, Decodable where EntityType.Identifier: IdType { + public let id: EntityType.Identifier + + public init(entity: Entity) { + 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` +public struct ToManyRelationship: Equatable, Relationship, Decodable where EntityType.Identifier: IdType { + public let ids: [EntityType.Identifier] + + public init(entities: [Entity]) { + ids = entities.map { $0.id } + } + + public init(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) + } + } +} diff --git a/Tests/JSONAPITests/Document/DocumentTests.swift b/Tests/JSONAPITests/Document/DocumentTests.swift new file mode 100644 index 0000000..9af2d1a --- /dev/null +++ b/Tests/JSONAPITests/Document/DocumentTests.swift @@ -0,0 +1,100 @@ +// +// DocumentTests.swift +// JSONAPITests +// +// Created by Mathew Polzin on 11/12/18. +// + +import XCTest +import JSONAPI + +class DocumentTests: XCTestCase { + + func test_singleDocumentNoIncludes() { + let document = try? JSONDecoder().decode(JSONAPIDocument, Include0, TmpError>.self, from: single_document_no_includes) + + XCTAssertNotNil(document) + + guard let d = document else { return } + + XCTAssertFalse(d.body.isError) + XCTAssertNotNil(d.body.data) + XCTAssertEqual(d.body.data?.0.value?.id.rawValue, "1") + XCTAssertEqual(d.body.data?.included.count, 0) + } + + func test_singleDocumentSomeIncludes() { + let document = try? JSONDecoder().decode(JSONAPIDocument, Include1, TmpError>.self, from: single_document_some_includes) + + XCTAssertNotNil(document) + + guard let d = document else { return } + + XCTAssertFalse(d.body.isError) + XCTAssertNotNil(d.body.data) + XCTAssertEqual(d.body.data?.0.value?.id.rawValue, "1") + XCTAssertEqual(d.body.data?.included.count, 1) + XCTAssertEqual(d.body.data?.included[Author.self].count, 1) + XCTAssertEqual(d.body.data?.included[Author.self][0].id.rawValue, "33") + } + + func test_manyDocumentNoIncludes() { + let document = try? JSONDecoder().decode(JSONAPIDocument, Include0, TmpError>.self, from: many_document_no_includes) + + XCTAssertNotNil(document) + + guard let d = document else { return } + + XCTAssertFalse(d.body.isError) + XCTAssertNotNil(d.body.data) + XCTAssertEqual(d.body.data?.0.values.count, 3) + XCTAssertEqual(d.body.data?.0.values[0].id.rawValue, "1") + XCTAssertEqual(d.body.data?.0.values[1].id.rawValue, "2") + XCTAssertEqual(d.body.data?.0.values[2].id.rawValue, "3") + XCTAssertEqual(d.body.data?.included.count, 0) + } + + func test_manyDocumentSomeIncludes() { + let document = try? JSONDecoder().decode(JSONAPIDocument, Include1, TmpError>.self, from: many_document_some_includes) + + XCTAssertNotNil(document) + + guard let d = document else { return } + + XCTAssertFalse(d.body.isError) + XCTAssertNotNil(d.body.data) + XCTAssertEqual(d.body.data?.0.values.count, 3) + XCTAssertEqual(d.body.data?.0.values[0].id.rawValue, "1") + XCTAssertEqual(d.body.data?.0.values[1].id.rawValue, "2") + XCTAssertEqual(d.body.data?.0.values[2].id.rawValue, "3") + XCTAssertEqual(d.body.data?.included.count, 3) + XCTAssertEqual(d.body.data?.included[Author.self].count, 3) + XCTAssertEqual(d.body.data?.included[Author.self][0].id.rawValue, "33") + XCTAssertEqual(d.body.data?.included[Author.self][1].id.rawValue, "22") + XCTAssertEqual(d.body.data?.included[Author.self][2].id.rawValue, "11") + } + + enum AuthorType: EntityType { + static var type: String { return "authors" } + + typealias Identifier = Id + typealias AttributeType = NoAttributes + typealias RelatedType = NoRelatives + } + + typealias Author = Entity + + enum ArticleType: EntityType { + static var type: String { return "articles" } + + typealias Identifier = Id + typealias AttributeType = NoAttributes + typealias RelatedType = Relationships + + struct Relationships: Relatives { + let author: ToOneRelationship + } + } + + typealias Article = Entity +} diff --git a/Tests/JSONAPITests/Document/stubs/DocumentStubs.swift b/Tests/JSONAPITests/Document/stubs/DocumentStubs.swift new file mode 100644 index 0000000..6233b8e --- /dev/null +++ b/Tests/JSONAPITests/Document/stubs/DocumentStubs.swift @@ -0,0 +1,148 @@ +// +// DocumentStubs.swift +// JSONAPITests +// +// Created by Mathew Polzin on 11/12/18. +// + +import Foundation + +let single_document_no_includes = """ +{ + "data": { + "id": "1", + "type": "articles", + "relationships": { + "author": { + "data": { + "type": "authors", + "id": "33" + } + } + } + } +} +""".data(using: .utf8)! + +let single_document_some_includes = """ +{ + "data": { + "id": "1", + "type": "articles", + "relationships": { + "author": { + "data": { + "type": "authors", + "id": "33" + } + } + } + }, + "included": [ + { + "id": "33", + "type": "authors" + } + ] +} +""".data(using: .utf8)! + +let many_document_no_includes = """ +{ + "data": [ + { + "id": "1", + "type": "articles", + "relationships": { + "author": { + "data": { + "type": "authors", + "id": "33" + } + } + } + }, + { + "id": "2", + "type": "articles", + "relationships": { + "author": { + "data": { + "type": "authors", + "id": "22" + } + } + } + }, + { + "id": "3", + "type": "articles", + "relationships": { + "author": { + "data": { + "type": "authors", + "id": "11" + } + } + } + } + ] +} +""".data(using: .utf8)! + +let many_document_some_includes = """ +{ + "data": [ + { + "id": "1", + "type": "articles", + "relationships": { + "author": { + "data": { + "type": "authors", + "id": "33" + } + } + } + }, + { + "id": "2", + "type": "articles", + "relationships": { + "author": { + "data": { + "type": "authors", + "id": "22" + } + } + } + }, + { + "id": "3", + "type": "articles", + "relationships": { + "author": { + "data": { + "type": "authors", + "id": "11" + } + } + } + } + ], + "included": [ + { + "id": "33", + "type": "authors" + }, + { + "id": "22", + "type": "authors" + }, + { + "id": "11", + "type": "authors" + } + ] +} +""".data(using: .utf8)! diff --git a/Tests/JSONAPITests/Entity/EntityTests.swift b/Tests/JSONAPITests/Entity/EntityTests.swift new file mode 100644 index 0000000..ce263a8 --- /dev/null +++ b/Tests/JSONAPITests/Entity/EntityTests.swift @@ -0,0 +1,168 @@ +// +// EntityTests.swift +// ElevatedCoreTests +// +// Created by Mathew Polzin on 7/25/18. +// + +import XCTest +import Foundation +import JSONAPI + +class EntityTests: XCTestCase { + + func test_relationship_access() { + let entity1 = TestEntity1() + let entity2 = TestEntity2(other: entity1.pointer) + + XCTAssertEqual(entity2.relationships.other, entity1.pointer) + } + + func test_relationship_operator_access() { + let entity1 = TestEntity1() + let entity2 = TestEntity2(other: entity1.pointer) + + XCTAssertEqual(entity2 ~> \.other, entity1.id) + } + + func test_toMany_relationship_operator_access() { + let entity1 = TestEntity1() + let entity2 = TestEntity1() + let entity4 = TestEntity1() + let entity3 = TestEntity3(others: .init(relationships: [entity1.pointer, entity2.pointer, entity4.pointer])) + + XCTAssertEqual(entity3 ~> \.others, [entity1.id, entity2.id, entity4.id]) + } + + func test_relationshipIds() { + let entity1 = TestEntity1() + let entity2 = TestEntity2(other: entity1.pointer) + + XCTAssertEqual(entity2.relationships.other.ids, [entity1.id]) + } + + func test_EntityNoRelationshipsNoAttributes() { + let entity = try? JSONDecoder().decode(TestEntity1.self, from: entity_no_relationships_no_attributes) + + XCTAssertNotNil(entity) + XCTAssert(type(of: entity?.relationships) == NoRelatives?.self) + XCTAssert(type(of: entity?.attributes) == NoAttributes?.self) + } + + func test_EntityNoRelationshipsSomeAttributes() { + let entity = try? JSONDecoder().decode(TestEntity5.self, from: entity_no_relationships_some_attributes) + + XCTAssertNotNil(entity) + XCTAssert(type(of: entity?.relationships) == NoRelatives?.self) + + guard let e = entity else { return } + + XCTAssertEqual(e[\.floater], 123.321) + } + + func test_EntitySomeRelationshipsNoAttributes() { + let entity = try? JSONDecoder().decode(TestEntity3.self, from: entity_some_relationships_no_attributes) + + XCTAssertNotNil(entity) + XCTAssert(type(of: entity?.attributes) == NoAttributes?.self) + + guard let e = entity else { return } + + XCTAssertEqual((e ~> \.others).map { $0.rawValue.uuidString }, ["364B3B69-4DF1-467F-B52E-B0C9E44F666E"]) + } + + func test_EntitySomeRelationshipsSomeAttributes() { + let entity = try? JSONDecoder().decode(TestEntity4.self, from: entity_some_relationships_some_attributes) + + XCTAssertNotNil(entity) + + guard let e = entity else { return } + + XCTAssertEqual(e[\.word], "coolio") + XCTAssertEqual(e[\.number], 992299) + XCTAssertEqual((e ~> \.other).rawValue.uuidString, "2DF03B69-4B0A-467F-B52E-B0C9E44FCECF") + } + + enum TestEntityType1: EntityType { + static var type: String { return "test_entities"} + + typealias Identifier = Id + typealias AttributeType = NoAttributes + typealias RelatedType = NoRelatives + } + + typealias TestEntity1 = Entity + + enum TestEntityType2: EntityType { + static var type: String { return "second_test_entities"} + + typealias Identifier = Id + typealias RelatedType = Relationships + typealias AttributeType = NoAttributes + + struct Relationships: Relatives { + let other: ToOneRelationship + } + } + + typealias TestEntity2 = Entity + + enum TestEntityType3: EntityType { + static var type: String { return "third_test_entities"} + + typealias Identifier = Id + typealias RelatedType = Relationships + typealias AttributeType = NoAttributes + + struct Relationships: Relatives { + let others: ToManyRelationship + } + } + + typealias TestEntity3 = Entity + + enum TestEntityType4: EntityType { + static var type: String { return "fourth_test_entities"} + + typealias Identifier = Id + typealias RelatedType = Relationships + typealias AttributeType = Atts + + struct Relationships: Relatives { + let other: ToOneRelationship + } + + struct Atts: Attributes { + let word: String + let number: Int + } + } + + typealias TestEntity4 = Entity + + enum TestEntityType5: EntityType { + static var type: String { return "fifth_test_entities"} + + typealias Identifier = Id + typealias RelatedType = NoRelatives + typealias AttributeType = Atts + + struct Atts: Attributes { + let floater: Double + } + } + + typealias TestEntity5 = Entity +} + +extension Entity where EntityType == EntityTests.TestEntityType2 { + init(other: ToOneRelationship) { + self.init(relationships: .init(other: other)) + } +} + +extension Entity where EntityType == EntityTests.TestEntityType3 { + init(others: ToManyRelationship) { + self.init(relationships: .init(others: others)) + } +} diff --git a/Tests/JSONAPITests/Entity/stubs/EntityStubs.swift b/Tests/JSONAPITests/Entity/stubs/EntityStubs.swift new file mode 100644 index 0000000..b773fec --- /dev/null +++ b/Tests/JSONAPITests/Entity/stubs/EntityStubs.swift @@ -0,0 +1,59 @@ +// +// EntityStubs.swift +// JSONAPITests +// +// Created by Mathew Polzin on 11/12/18. +// + +import Foundation + +let entity_no_relationships_no_attributes = """ +{ + "id": "A24B3B69-4DF1-467F-B52E-B0C9E44F436A", + "type": "test_entities" +} +""".data(using: .utf8)! + +let entity_no_relationships_some_attributes = """ +{ +"id": "2DF03B69-4B0A-467F-B52E-B0C9E44FCECF", +"type": "fifth_test_entities", +"attributes": { +"floater": 123.321 +} +} +""".data(using: .utf8)! + +let entity_some_relationships_no_attributes = """ +{ +"id": "11113B69-4DF1-467F-B52E-B0C9E44FC444", +"type": "third_test_entities", +"relationships": { +"others": { +"data": [{ +"type": "test_entities", +"id": "364B3B69-4DF1-467F-B52E-B0C9E44F666E" +}] +} +} +} +""".data(using: .utf8)! + +let entity_some_relationships_some_attributes = """ +{ +"id": "90F03B69-4DF1-467F-B52E-B0C9E44FC333", +"type": "fourth_test_entities", +"attributes": { +"word": "coolio", +"number": 992299 +}, +"relationships": { +"other": { +"data": { +"type": "second_test_entities", +"id": "2DF03B69-4B0A-467F-B52E-B0C9E44FCECF" +} +} +} +} +""".data(using: .utf8)! diff --git a/Tests/JSONAPITests/Includes/IncludeTests.swift b/Tests/JSONAPITests/Includes/IncludeTests.swift new file mode 100644 index 0000000..1c1d1fc --- /dev/null +++ b/Tests/JSONAPITests/Includes/IncludeTests.swift @@ -0,0 +1,204 @@ + +import XCTest +import JSONAPI + +class IncludedTests: XCTestCase { + + let decoder = JSONDecoder() + + func test_OneInclude() { + let maybeIncludes = try? decoder.decode(Includes>.self, from: one_include) + + XCTAssertNotNil(maybeIncludes) + + guard let includes = maybeIncludes else { + return + } + + XCTAssertEqual(includes[TestEntity.self].count, 1) + } + + func test_TwoSameIncludes() { + let maybeIncludes = try? decoder.decode(Includes>.self, from: two_same_type_includes) + + XCTAssertNotNil(maybeIncludes) + + guard let includes = maybeIncludes else { + return + } + + XCTAssertEqual(includes[TestEntity.self].count, 2) + } + + func test_TwoDifferentIncludes() { + let maybeIncludes = try? decoder.decode(Includes>.self, from: two_different_type_includes) + + XCTAssertNotNil(maybeIncludes) + + guard let includes = maybeIncludes else { + return + } + + XCTAssertEqual(includes[TestEntity.self].count, 1) + XCTAssertEqual(includes[TestEntity2.self].count, 1) + } + + func test_ThreeDifferentIncludes() { + let maybeIncludes = try? decoder.decode(Includes>.self, from: three_different_type_includes) + + XCTAssertNotNil(maybeIncludes) + + guard let includes = maybeIncludes else { + return + } + + XCTAssertEqual(includes[TestEntity.self].count, 1) + XCTAssertEqual(includes[TestEntity2.self].count, 1) + XCTAssertEqual(includes[TestEntity4.self].count, 1) + } + + func test_FourDifferentIncludes() { + let maybeIncludes = try? decoder.decode(Includes>.self, from: four_different_type_includes) + + XCTAssertNotNil(maybeIncludes) + + guard let includes = maybeIncludes else { + return + } + + XCTAssertEqual(includes[TestEntity.self].count, 1) + XCTAssertEqual(includes[TestEntity2.self].count, 1) + XCTAssertEqual(includes[TestEntity4.self].count, 1) + XCTAssertEqual(includes[TestEntity6.self].count, 1) + } + + func test_FiveDifferentIncludes() { + let maybeIncludes = try? decoder.decode(Includes>.self, from: five_different_type_includes) + + XCTAssertNotNil(maybeIncludes) + + guard let includes = maybeIncludes else { + return + } + + XCTAssertEqual(includes[TestEntity.self].count, 1) + XCTAssertEqual(includes[TestEntity2.self].count, 1) + XCTAssertEqual(includes[TestEntity3.self].count, 1) + XCTAssertEqual(includes[TestEntity4.self].count, 1) + XCTAssertEqual(includes[TestEntity6.self].count, 1) + } + + func test_SixDifferentIncludes() { + let maybeIncludes = try? decoder.decode(Includes>.self, from: six_different_type_includes) + + XCTAssertNotNil(maybeIncludes) + + guard let includes = maybeIncludes else { + return + } + + XCTAssertEqual(includes[TestEntity.self].count, 1) + XCTAssertEqual(includes[TestEntity2.self].count, 1) + XCTAssertEqual(includes[TestEntity3.self].count, 1) + XCTAssertEqual(includes[TestEntity4.self].count, 1) + XCTAssertEqual(includes[TestEntity5.self].count, 1) + XCTAssertEqual(includes[TestEntity6.self].count, 1) + } +} + +extension IncludedTests { + enum TestEntityType: EntityType { + typealias Identifier = Id + + typealias AttributeType = Atts + + typealias RelatedType = NoRelatives + + public static var type: String { return "test_entity1" } + + public struct Atts: Attributes { + let foo: String + let bar: Int + } + } + + typealias TestEntity = Entity + + enum TestEntityType2: EntityType { + typealias Identifier = Id + + typealias AttributeType = Atts + + typealias RelatedType = Relationships + + public static var type: String { return "test_entity2" } + + public struct Relationships: Relatives { + let entity1: ToOneRelationship + } + + public struct Atts: Attributes { + let foo: String + let bar: Int + } + } + + typealias TestEntity2 = Entity + + enum TestEntityType3: EntityType { + typealias Identifier = Id + + typealias AttributeType = NoAttributes + + typealias RelatedType = Relationships + + public static var type: String { return "test_entity3" } + + public struct Relationships: Relatives { + let entity1: ToOneRelationship + let entity2: ToManyRelationship + } + } + + typealias TestEntity3 = Entity + + enum TestEntityType4: EntityType { + typealias Identifier = Id + + typealias AttributeType = NoAttributes + + typealias RelatedType = NoRelatives + + public static var type: String { return "test_entity4" } + } + + typealias TestEntity4 = Entity + + enum TestEntityType5: EntityType { + typealias Identifier = Id + + typealias AttributeType = NoAttributes + + typealias RelatedType = NoRelatives + + public static var type: String { return "test_entity5" } + } + + typealias TestEntity5 = Entity + + enum TestEntityType6: EntityType { + typealias Identifier = Id + + typealias AttributeType = NoAttributes + + typealias RelatedType = Relationships + + public static var type: String { return "test_entity6" } + + struct Relationships: Relatives { + let entity4: ToOneRelationship + } + } + + typealias TestEntity6 = Entity +} diff --git a/Tests/JSONAPITests/Includes/stubs/IncludeStubs.swift b/Tests/JSONAPITests/Includes/stubs/IncludeStubs.swift new file mode 100644 index 0000000..77bec1b --- /dev/null +++ b/Tests/JSONAPITests/Includes/stubs/IncludeStubs.swift @@ -0,0 +1,285 @@ +// +// IncludeStubs.swift +// ElevatedCore +// +// Created by Mathew Polzin on 11/10/18. +// + +import Foundation + +let one_include = """ +[ +{ +"type": "test_entity1", +"id": "2DF03B69-4B0A-467F-B52E-B0C9E44FCECF", +"attributes": { +"foo": "Hello", +"bar": 123 +} +} +] +""".data(using: .utf8)! + +let two_same_type_includes = """ +[ +{ +"type": "test_entity1", +"id": "2DF03B69-4B0A-467F-B52E-B0C9E44FCECF", +"attributes": { +"foo": "Hello", +"bar": 123 +} +}, +{ +"type": "test_entity1", +"id": "90F03B69-4DF1-467F-B52E-B0C9E44FC333", +"attributes": { +"foo": "World", +"bar": 456 +} +} +] + +""".data(using: .utf8)! + +let two_different_type_includes = """ +[ +{ +"type": "test_entity1", +"id": "2DF03B69-4B0A-467F-B52E-B0C9E44FCECF", +"attributes": { +"foo": "Hello", +"bar": 123 +} +}, +{ +"type": "test_entity2", +"id": "90F03B69-4DF1-467F-B52E-B0C9E44FC333", +"attributes": { +"foo": "World", +"bar": 456 +}, +"relationships": { +"entity1": { +"data": { +"type": "test_entity1", +"id": "2DF03B69-4B0A-467F-B52E-B0C9E44FCECF" +} +} +} +} +] + +""".data(using: .utf8)! + +let three_different_type_includes = """ +[ + { + "type": "test_entity1", + "id": "2DF03B69-4B0A-467F-B52E-B0C9E44FCECF", + "attributes": { + "foo": "Hello", + "bar": 123 + } + }, + { + "type": "test_entity2", + "id": "90F03B69-4DF1-467F-B52E-B0C9E44FC333", + "attributes": { + "foo": "World", + "bar": 456 + }, + "relationships": { + "entity1": { + "data": { + "type": "test_entity1", + "id": "2DF03B69-4B0A-467F-B52E-B0C9E44FCECF" + } + } + } + }, + { + "type": "test_entity4", + "id": "364B3B69-4DF1-467F-B52E-B0C9E44F666E" + } +] +""".data(using: .utf8)! + +let four_different_type_includes = """ +[ + { + "type": "test_entity1", + "id": "2DF03B69-4B0A-467F-B52E-B0C9E44FCECF", + "attributes": { + "foo": "Hello", + "bar": 123 + } + }, + { + "type": "test_entity2", + "id": "90F03B69-4DF1-467F-B52E-B0C9E44FC333", + "attributes": { + "foo": "World", + "bar": 456 + }, + "relationships": { + "entity1": { + "data": { + "type": "test_entity1", + "id": "2DF03B69-4B0A-467F-B52E-B0C9E44FCECF" + } + } + } + }, + { + "type": "test_entity6", + "id": "11113B69-4DF1-467F-B52E-B0C9E44FC444", + "relationships": { + "entity4": { + "data": { + "type": "test_entity4", + "id": "364B3B69-4DF1-467F-B52E-B0C9E44F666E" + } + } + } + }, + { + "type": "test_entity4", + "id": "364B3B69-4DF1-467F-B52E-B0C9E44F666E" + } +] +""".data(using: .utf8)! + +let five_different_type_includes = """ +[ + { + "type": "test_entity1", + "id": "2DF03B69-4B0A-467F-B52E-B0C9E44FCECF", + "attributes": { + "foo": "Hello", + "bar": 123 + } + }, + { + "type": "test_entity2", + "id": "90F03B69-4DF1-467F-B52E-B0C9E44FC333", + "attributes": { + "foo": "World", + "bar": 456 + }, + "relationships": { + "entity1": { + "data": { + "type": "test_entity1", + "id": "2DF03B69-4B0A-467F-B52E-B0C9E44FCECF" + } + } + } + }, + { + "type": "test_entity3", + "id": "11223B69-4DF1-467F-B52E-B0C9E44FC443", + "relationships": { + "entity1": { + "data": { + "type": "test_entity1", + "id": "2DF03B69-4B0A-467F-B52E-B0C9E44FCECF" + } + }, + "entity2": { + "data": [ + { + "type": "test_entity2", + "id": "90F03B69-4DF1-467F-B52E-B0C9E44FC333" + } + ] + } + } + }, + { + "type": "test_entity6", + "id": "11113B69-4DF1-467F-B52E-B0C9E44FC444", + "relationships": { + "entity4": { + "data": { + "type": "test_entity4", + "id": "364B3B69-4DF1-467F-B52E-B0C9E44F666E" + } + } + } + }, + { + "type": "test_entity4", + "id": "364B3B69-4DF1-467F-B52E-B0C9E44F666E" + } +] +""".data(using: .utf8)! + +let six_different_type_includes = """ +[ + { + "type": "test_entity1", + "id": "2DF03B69-4B0A-467F-B52E-B0C9E44FCECF", + "attributes": { + "foo": "Hello", + "bar": 123 + } + }, + { + "type": "test_entity2", + "id": "90F03B69-4DF1-467F-B52E-B0C9E44FC333", + "attributes": { + "foo": "World", + "bar": 456 + }, + "relationships": { + "entity1": { + "data": { + "type": "test_entity1", + "id": "2DF03B69-4B0A-467F-B52E-B0C9E44FCECF" + } + } + } + }, + { + "type": "test_entity3", + "id": "11223B69-4DF1-467F-B52E-B0C9E44FC443", + "relationships": { + "entity1": { + "data": { + "type": "test_entity1", + "id": "2DF03B69-4B0A-467F-B52E-B0C9E44FCECF" + } + }, + "entity2": { + "data": [ + { + "type": "test_entity2", + "id": "90F03B69-4DF1-467F-B52E-B0C9E44FC333" + } + ] + } + } + }, + { + "type": "test_entity6", + "id": "11113B69-4DF1-467F-B52E-B0C9E44FC444", + "relationships": { + "entity4": { + "data": { + "type": "test_entity4", + "id": "364B3B69-4DF1-467F-B52E-B0C9E44F666E" + } + } + } + }, + { + "type": "test_entity5", + "id": "A24B3B69-4DF1-467F-B52E-B0C9E44F436A" + }, + { + "type": "test_entity4", + "id": "364B3B69-4DF1-467F-B52E-B0C9E44F666E" + } +] +""".data(using: .utf8)! diff --git a/Tests/JSONAPITests/Includes/stubs/one_include.json b/Tests/JSONAPITests/Includes/stubs/one_include.json new file mode 100644 index 0000000..9d2a3a4 --- /dev/null +++ b/Tests/JSONAPITests/Includes/stubs/one_include.json @@ -0,0 +1,10 @@ +[ + { + "type": "test_entity1", + "id": "2DF03B69-4B0A-467F-B52E-B0C9E44FCECF", + "attributes": { + "foo": "Hello", + "bar": 123 + } + } +] diff --git a/Tests/JSONAPITests/Includes/stubs/three_different_type_includes.json b/Tests/JSONAPITests/Includes/stubs/three_different_type_includes.json new file mode 100644 index 0000000..edd1ba8 --- /dev/null +++ b/Tests/JSONAPITests/Includes/stubs/three_different_type_includes.json @@ -0,0 +1,30 @@ +[ + { + "type": "test_entity1", + "id": "2DF03B69-4B0A-467F-B52E-B0C9E44FCECF", + "attributes": { + "foo": "Hello", + "bar": 123 + } + }, + { + "type": "test_entity2", + "id": "90F03B69-4DF1-467F-B52E-B0C9E44FC333", + "attributes": { + "foo": "World", + "bar": 456 + }, + "relationships": { + "entity1": { + "data": { + "type": "test_entity1", + "id": "2DF03B69-4B0A-467F-B52E-B0C9E44FCECF" + } + } + } + }, + { + "type": "test_entity4", + "id": "364B3B69-4DF1-467F-B52E-B0C9E44F666E" + } +] diff --git a/Tests/JSONAPITests/Includes/stubs/two_different_type_includes.json b/Tests/JSONAPITests/Includes/stubs/two_different_type_includes.json new file mode 100644 index 0000000..6ef8e14 --- /dev/null +++ b/Tests/JSONAPITests/Includes/stubs/two_different_type_includes.json @@ -0,0 +1,26 @@ +[ + { + "type": "test_entity1", + "id": "2DF03B69-4B0A-467F-B52E-B0C9E44FCECF", + "attributes": { + "foo": "Hello", + "bar": 123 + } + }, + { + "type": "test_entity2", + "id": "90F03B69-4DF1-467F-B52E-B0C9E44FC333", + "attributes": { + "foo": "World", + "bar": 456 + }, + "relationships": { + "entity1": { + "data": { + "type": "test_entity1", + "id": "2DF03B69-4B0A-467F-B52E-B0C9E44FCECF" + } + } + } + } +] diff --git a/Tests/JSONAPITests/Includes/stubs/two_same_type_includes.json b/Tests/JSONAPITests/Includes/stubs/two_same_type_includes.json new file mode 100644 index 0000000..d1dfb42 --- /dev/null +++ b/Tests/JSONAPITests/Includes/stubs/two_same_type_includes.json @@ -0,0 +1,18 @@ +[ + { + "type": "test_entity1", + "id": "2DF03B69-4B0A-467F-B52E-B0C9E44FCECF", + "attributes": { + "foo": "Hello", + "bar": 123 + } + }, + { + "type": "test_entity1", + "id": "90F03B69-4DF1-467F-B52E-B0C9E44FC333", + "attributes": { + "foo": "World", + "bar": 456 + } + } +] diff --git a/Tests/JSONAPITests/JSONAPITests.swift b/Tests/JSONAPITests/JSONAPITests.swift new file mode 100644 index 0000000..2792f48 --- /dev/null +++ b/Tests/JSONAPITests/JSONAPITests.swift @@ -0,0 +1,15 @@ +import XCTest +@testable import JSONAPI + +final class JSONAPITests: XCTestCase { + func testExample() { + // This is an example of a functional test case. + // Use XCTAssert and related functions to verify your tests produce the correct + // results. + XCTAssertEqual(JSONAPI().text, "Hello, World!") + } + + static var allTests = [ + ("testExample", testExample), + ] +} diff --git a/Tests/JSONAPITests/Relationships/RelationshipTests.swift b/Tests/JSONAPITests/Relationships/RelationshipTests.swift new file mode 100644 index 0000000..e07148c --- /dev/null +++ b/Tests/JSONAPITests/Relationships/RelationshipTests.swift @@ -0,0 +1,63 @@ +// +// RelationshipTests.swift +// JSONAPITests +// +// Created by Mathew Polzin on 11/12/18. +// + +import XCTest +import JSONAPI + +class RelationshipTests: XCTestCase { + + func test_initToManyWithEntities() { + let entity1 = TestEntity1() + let entity2 = TestEntity1() + let entity3 = TestEntity1() + let entity4 = TestEntity1() + let relationship = ToManyRelationship(entities: [entity1, entity2, entity3, entity4]) + + XCTAssertEqual(relationship.ids.count, 4) + XCTAssertEqual(relationship.ids, [entity1, entity2, entity3, entity4].map { $0.id }) + } + + func test_initToManyWithRelationships() { + let entity1 = TestEntity1() + let entity2 = TestEntity1() + let entity3 = TestEntity1() + let entity4 = TestEntity1() + let relationship = ToManyRelationship(relationships: [entity1.pointer, entity2.pointer, entity3.pointer, entity4.pointer]) + + XCTAssertEqual(relationship.ids.count, 4) + XCTAssertEqual(relationship.ids, [entity1, entity2, entity3, entity4].map { $0.id }) + } + + func test_ToOneRelationship() { + let relationship = try? JSONDecoder().decode(ToOneRelationship.self, from: to_one_relationship) + + XCTAssertNotNil(relationship) + + XCTAssertEqual(relationship?.id.rawValue.uuidString, "2DF03B69-4B0A-467F-B52E-B0C9E44FCECF") + XCTAssertEqual(relationship?.ids.count, 1) + } + + func test_ToManyRelationship() { + let relationship = try? JSONDecoder().decode(ToManyRelationship.self, from: to_many_relationship) + + XCTAssertNotNil(relationship) + + XCTAssertEqual(relationship?.ids.map { $0.rawValue.uuidString }, ["2DF03B69-4B0A-467F-B52E-B0C9E44FCECF", "90F03B69-4DF1-467F-B52E-B0C9E44FC333", "2DF03B69-4B0A-467F-B52E-B0C9E44FCECF"]) + } + + enum TestEntityType1: EntityType { + typealias Identifier = Id + + typealias AttributeType = NoAttributes + + typealias RelatedType = NoRelatives + + public static var type: String { return "test_entity1" } + } + + typealias TestEntity1 = Entity +} diff --git a/Tests/JSONAPITests/Relationships/stubs/RelationshipStubs.swift b/Tests/JSONAPITests/Relationships/stubs/RelationshipStubs.swift new file mode 100644 index 0000000..8bdc9d6 --- /dev/null +++ b/Tests/JSONAPITests/Relationships/stubs/RelationshipStubs.swift @@ -0,0 +1,36 @@ +// +// RelationshipStubs.swift +// JSONAPITests +// +// Created by Mathew Polzin on 11/12/18. +// + +import Foundation + +let to_one_relationship = """ +{ + "data": { + "type": "test_entity1", + "id": "2DF03B69-4B0A-467F-B52E-B0C9E44FCECF" + } +} +""".data(using: .utf8)! + +let to_many_relationship = """ +{ + "data": [ + { + "type": "test_entity1", + "id": "2DF03B69-4B0A-467F-B52E-B0C9E44FCECF" + }, + { + "type": "test_entity1", + "id": "90F03B69-4DF1-467F-B52E-B0C9E44FC333" + }, + { + "type": "test_entity1", + "id": "2DF03B69-4B0A-467F-B52E-B0C9E44FCECF" + } + ] +} +""".data(using: .utf8)! diff --git a/Tests/JSONAPITests/ResourceBody/ResourceBodyTests.swift b/Tests/JSONAPITests/ResourceBody/ResourceBodyTests.swift new file mode 100644 index 0000000..95f10ef --- /dev/null +++ b/Tests/JSONAPITests/ResourceBody/ResourceBodyTests.swift @@ -0,0 +1,51 @@ +// +// ResourceBodyTests.swift +// JSONAPITests +// +// Created by Mathew Polzin on 11/12/18. +// + +import XCTest +import JSONAPI + +class ResourceBodyTests: XCTestCase { + + func test_singleResourceBody() { + let body = try? JSONDecoder().decode(SingleResourceBody.self, from: single_resource_body) + + XCTAssertNotNil(body) + + guard let b = body else { return } + + XCTAssertEqual(b.value, Article(id: Id(rawValue: "1"), + attributes: ArticleType.Attributes(title: "JSON:API paints my bikeshed!"))) + } + + func test_manyResourceBody() { + let body = try? JSONDecoder().decode(ManyResourceBody.self, from: many_resource_body) + + XCTAssertNotNil(body) + + guard let b = body else { return } + + XCTAssertEqual(b.values, [ + Article(id: .init(rawValue: "1"), attributes: .init(title: "JSON:API paints my bikeshed!")), + Article(id: .init(rawValue: "2"), attributes: .init(title: "Sick")), + Article(id: .init(rawValue: "3"), attributes: .init(title: "Hello World")) + ]) + } + + enum ArticleType: EntityType { + public static var type: String { return "articles" } + + typealias Identifier = Id + typealias RelatedType = NoRelatives + typealias AttributeType = Attributes + + struct Attributes: JSONAPI.Attributes { + let title: String + } + } + + typealias Article = Entity +} diff --git a/Tests/JSONAPITests/ResourceBody/stubs/ResourceBudyStubs.swift b/Tests/JSONAPITests/ResourceBody/stubs/ResourceBudyStubs.swift new file mode 100644 index 0000000..11166db --- /dev/null +++ b/Tests/JSONAPITests/ResourceBody/stubs/ResourceBudyStubs.swift @@ -0,0 +1,44 @@ +// +// ResourceBudyStubs.swift +// JSONAPITests +// +// Created by Mathew Polzin on 11/12/18. +// + +import Foundation + +let single_resource_body = """ +{ + "type": "articles", + "id": "1", + "attributes": { + "title": "JSON:API paints my bikeshed!" + } +} +""".data(using: .utf8)! + +let many_resource_body = """ +[ + { + "type": "articles", + "id": "1", + "attributes": { + "title": "JSON:API paints my bikeshed!" + } + }, + { + "type": "articles", + "id": "2", + "attributes": { + "title": "Sick" + } + }, + { + "type": "articles", + "id": "3", + "attributes": { + "title": "Hello World" + } + } +] +""".data(using: .utf8)! diff --git a/Tests/JSONAPITests/XCTestManifests.swift b/Tests/JSONAPITests/XCTestManifests.swift new file mode 100644 index 0000000..75a36eb --- /dev/null +++ b/Tests/JSONAPITests/XCTestManifests.swift @@ -0,0 +1,9 @@ +import XCTest + +#if !os(macOS) +public func allTests() -> [XCTestCaseEntry] { + return [ + testCase(JSONAPITests.allTests), + ] +} +#endif \ No newline at end of file diff --git a/Tests/LinuxMain.swift b/Tests/LinuxMain.swift new file mode 100644 index 0000000..a0d7210 --- /dev/null +++ b/Tests/LinuxMain.swift @@ -0,0 +1,7 @@ +import XCTest + +import JSONAPITests + +var tests = [XCTestCaseEntry]() +tests += JSONAPITests.allTests() +XCTMain(tests) \ No newline at end of file