Compare commits

..

7 Commits

13 changed files with 339 additions and 30 deletions
+7 -1
View File
@@ -13,7 +13,6 @@ public protocol JSONAPIDocument: Codable, Equatable {
associatedtype Error: JSONAPIError
typealias Body = Document<PrimaryResourceBody, MetaType, LinksType, IncludeType, Error>.Body
typealias BodyData = Body.Data
var body: Body { get }
}
@@ -40,6 +39,13 @@ public struct Document<PrimaryResourceBody: JSONAPI.ResourceBody, MetaType: JSON
public let includes: Includes<Include>
public let meta: MetaType
public let links: LinksType
public init(primary: PrimaryResourceBody, includes: Includes<Include>, meta: MetaType, links: LinksType) {
self.primary = primary
self.includes = includes
self.meta = meta
self.links = links
}
}
public var isError: Bool {
+1 -1
View File
@@ -32,7 +32,7 @@ public struct NoResourceBody: ResourceBody {
public static var none: NoResourceBody { return NoResourceBody() }
}
// MARK: Decodable
// MARK: Codable
extension SingleResourceBody {
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
+7
View File
@@ -19,6 +19,13 @@ public struct TransformedAttribute<RawValue: Codable, Transformer: JSONAPI.Trans
}
}
extension TransformedAttribute where Transformer: ReversibleTransformer {
public init(transformedValue: Transformer.To) throws {
self.value = transformedValue
rawValue = try Transformer.reverse(value)
}
}
extension TransformedAttribute: CustomStringConvertible {
public var description: String {
return "Attribute<\(String(describing: Transformer.From.self)) -> \(String(describing: Transformer.To.self))>(\(String(describing: value)))"
+27 -16
View File
@@ -16,11 +16,15 @@ public typealias Attributes = Codable & Equatable
/// Can be used as `Relationships` Type for Entities that do not
/// have any Relationships.
public struct NoRelationships: Relationships {}
public struct NoRelationships: Relationships {
public static var none: NoRelationships { return .init() }
}
/// Can be used as `Attributes` Type for Entities that do not
/// have any Attributes.
public struct NoAttributes: Attributes {}
public struct NoAttributes: Attributes {
public static var none: NoAttributes { return .init() }
}
/// An `EntityDescription` describes a JSON API
/// Resource Object. The Resource Object
@@ -34,10 +38,10 @@ public protocol EntityDescription {
static var type: String { get }
}
/// EntityType is the protocol that Entity conforms to. This
/// protocol lets other types accept any Entity as a generic
/// specialization.
public protocol EntityType: PrimaryResource {
/// EntityProxy is a protocol that can be used to create
/// types that _act_ like Entities but cannot be encoded
/// or decoded as Entities.
public protocol EntityProxy: Equatable {
associatedtype Description: EntityDescription
associatedtype EntityRawIdType: JSONAPI.MaybeRawId
@@ -59,6 +63,17 @@ public protocol EntityType: PrimaryResource {
var relationships: Relationships { get }
}
extension EntityProxy {
/// The JSON API compliant "type" of this `Entity`.
public static var type: String { return Description.type }
}
/// EntityType is the protocol that Entity conforms to. This
/// protocol lets other types accept any Entity as a generic
/// specialization.
public protocol EntityType: EntityProxy, PrimaryResource {
}
public protocol IdentifiableEntityType: EntityType, Relatable where EntityRawIdType: JSONAPI.RawIdType {}
/// An `Entity` is a single model type that can be
@@ -66,10 +81,6 @@ public protocol IdentifiableEntityType: EntityType, Relatable where EntityRawIdT
/// "Resource Object."
/// See https://jsonapi.org/format/#document-resource-objects
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 }
/// 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,
@@ -155,21 +166,21 @@ public extension Entity where EntityRawIdType: JSONAPI.RawIdType {
}
// MARK: Attribute Access
public extension Entity {
public extension EntityProxy {
/// Access the attribute at the given keypath. This just
/// allows you to write `entity[\.propertyName]` instead
/// of `entity.relationships.propertyName`.
subscript<T, TFRM: Transformer>(_ path: KeyPath<Description.Attributes, TransformedAttribute<T, TFRM>>) -> TFRM.To {
return attributes[keyPath: path].value
}
/// Access the attribute at the given keypath. This just
/// allows you to write `entity[\.propertyName]` instead
/// of `entity.relationships.propertyName`.
subscript<T, TFRM: Transformer>(_ path: KeyPath<Description.Attributes, TransformedAttribute<T, TFRM>?>) -> TFRM.To? {
return attributes[keyPath: path]?.value
}
/// Access the attribute at the given keypath. This just
/// allows you to write `entity[\.propertyName]` instead
/// of `entity.relationships.propertyName`.
@@ -179,18 +190,18 @@ public extension Entity {
}
// MARK: Relationship Access
public extension Entity {
public extension EntityProxy {
/// Access to an Id of a `ToOneRelationship`.
/// This allows you to write `entity ~> \.other` instead
/// of `entity.relationships.other.id`.
public static func ~><OtherEntity: OptionalRelatable>(entity: Entity, path: KeyPath<Description.Relationships, ToOneRelationship<OtherEntity>>) -> OtherEntity.WrappedIdentifier {
public static func ~><OtherEntity: OptionalRelatable>(entity: Self, path: KeyPath<Description.Relationships, ToOneRelationship<OtherEntity>>) -> OtherEntity.WrappedIdentifier {
return entity.relationships[keyPath: path].id
}
/// Access to all Ids of a `ToManyRelationship`.
/// This allows you to write `entity ~> \.others` instead
/// of `entity.relationships.others.ids`.
public static func ~><OtherEntity: Relatable>(entity: Entity, path: KeyPath<Description.Relationships, ToManyRelationship<OtherEntity>>) -> [OtherEntity.Identifier] {
public static func ~><OtherEntity: Relatable>(entity: Self, path: KeyPath<Description.Relationships, ToManyRelationship<OtherEntity>>) -> [OtherEntity.Identifier] {
return entity.relationships[keyPath: path].ids
}
}
+2 -2
View File
@@ -35,7 +35,7 @@ public struct Unidentified: MaybeRawId, CustomStringConvertible {
}
public protocol MaybeId: Codable {
associatedtype EntityType: JSONAPI.EntityType
associatedtype EntityType: JSONAPI.EntityProxy
associatedtype RawType: MaybeRawId
}
@@ -53,7 +53,7 @@ public protocol CreatableIdType: IdType {
/// An Entity ID. These IDs can be encoded to or decoded from
/// JSON API IDs.
public struct Id<RawType: MaybeRawId, EntityType: JSONAPI.EntityType>: Codable, Equatable, MaybeId {
public struct Id<RawType: MaybeRawId, EntityType: JSONAPI.EntityProxy>: Codable, Equatable, MaybeId {
public let rawValue: RawType
+19 -5
View File
@@ -9,11 +9,16 @@ public protocol Transformer {
associatedtype From
associatedtype To
static func transform(_ from: From) throws -> To
static func transform(_ value: From) throws -> To
}
public enum IdentityTransformer<T>: Transformer {
public static func transform(_ from: T) throws -> T { return from }
public protocol ReversibleTransformer: Transformer {
static func reverse(_ value: To) throws -> From
}
public enum IdentityTransformer<T>: ReversibleTransformer {
public static func transform(_ value: T) throws -> T { return value }
public static func reverse(_ value: T) throws -> T { return value }
}
// MARK: - Validator
@@ -22,10 +27,19 @@ public enum IdentityTransformer<T>: Transformer {
/// is passed to it but it does not change the type of the value. Any
/// Transformer will perform validation in one sense so a Validator is
/// really just semantic sugar (it can provide clarity in its use).
public protocol Validator: Transformer where From == To {}
/// To enforce the semantics, any change of the value in your implementation
/// of `Validator.transform()` will be ignored.
public protocol Validator: ReversibleTransformer where From == To {
}
extension Validator {
public static func reverse(_ value: To) throws -> To {
let _ = try transform(value)
return value
}
public static func validate(_ value: To) throws -> To {
return try transform(value)
let _ = try transform(value)
return value
}
}
+14 -5
View File
@@ -8,12 +8,25 @@
import JSONAPI
public enum EntityCheckError: Swift.Error {
/// The attributes should live in a struct, not
/// another type class.
case attributesNotStruct
/// The relationships should live in a struct, not
/// another type class.
case relationshipsNotStruct
/// All stored properties on an Attributes struct should
/// be one of the supplied Attribute types.
case nonAttribute(named: String)
/// All stored properties on a Relationships struct should
/// be one of the supplied Relationship types.
case nonRelationship(named: String)
/// It is explicitly stated by the JSON spec
/// a "none" value for arrays is an empty array, not `nil`.
case nullArray(named: String)
case badId
}
public struct EntityCheckErrors: Swift.Error {
@@ -36,10 +49,6 @@ public extension Entity {
public static func check(_ entity: Entity) throws {
var problems = [EntityCheckError]()
if Swift.type(of: entity.id).EntityType.Description.self != Description.self {
problems.append(.badId)
}
let attributesMirror = Mirror(reflecting: entity.attributes)
if attributesMirror.displayStyle != .`struct` {
@@ -0,0 +1,30 @@
//
// Optional+Literal.swift
// JSONAPITestLib
//
// Created by Mathew Polzin on 11/29/18.
//
extension Optional: ExpressibleByUnicodeScalarLiteral where Wrapped: ExpressibleByUnicodeScalarLiteral {
public typealias UnicodeScalarLiteralType = Wrapped.UnicodeScalarLiteralType
public init(unicodeScalarLiteral value: UnicodeScalarLiteralType) {
self = .some(Wrapped(unicodeScalarLiteral: value))
}
}
extension Optional: ExpressibleByExtendedGraphemeClusterLiteral where Wrapped: ExpressibleByExtendedGraphemeClusterLiteral {
public typealias ExtendedGraphemeClusterLiteralType = Wrapped.ExtendedGraphemeClusterLiteralType
public init(extendedGraphemeClusterLiteral value: ExtendedGraphemeClusterLiteralType) {
self = .some(Wrapped(extendedGraphemeClusterLiteral: value))
}
}
extension Optional: ExpressibleByStringLiteral where Wrapped: ExpressibleByStringLiteral {
public typealias StringLiteralType = Wrapped.StringLiteralType
public init(stringLiteral value: StringLiteralType) {
self = .some(Wrapped(stringLiteral: value))
}
}
@@ -9,10 +9,35 @@ import JSONAPI
extension ToOneRelationship: ExpressibleByNilLiteral where Relatable.WrappedIdentifier: ExpressibleByNilLiteral {
public init(nilLiteral: ()) {
self.init(id: Relatable.WrappedIdentifier(nilLiteral: ()))
}
}
extension ToOneRelationship: ExpressibleByUnicodeScalarLiteral where Relatable.WrappedIdentifier: ExpressibleByUnicodeScalarLiteral {
public typealias UnicodeScalarLiteralType = Relatable.WrappedIdentifier.UnicodeScalarLiteralType
public init(unicodeScalarLiteral value: UnicodeScalarLiteralType) {
self.init(id: Relatable.WrappedIdentifier(unicodeScalarLiteral: value))
}
}
extension ToOneRelationship: ExpressibleByExtendedGraphemeClusterLiteral where Relatable.WrappedIdentifier: ExpressibleByExtendedGraphemeClusterLiteral {
public typealias ExtendedGraphemeClusterLiteralType = Relatable.WrappedIdentifier.ExtendedGraphemeClusterLiteralType
public init(extendedGraphemeClusterLiteral value: ExtendedGraphemeClusterLiteralType) {
self.init(id: Relatable.WrappedIdentifier(extendedGraphemeClusterLiteral: value))
}
}
extension ToOneRelationship: ExpressibleByStringLiteral where Relatable.WrappedIdentifier: ExpressibleByStringLiteral {
public typealias StringLiteralType = Relatable.WrappedIdentifier.StringLiteralType
public init(stringLiteral value: StringLiteralType) {
self.init(id: Relatable.WrappedIdentifier(stringLiteral: value))
}
}
extension ToManyRelationship: ExpressibleByArrayLiteral {
public typealias ArrayLiteralElement = Relatable.Identifier
@@ -18,4 +18,31 @@ class AttributeTests: XCTestCase {
XCTAssertEqual(try Attribute<String>(rawValue: "hello"), Attribute<String>(value: "hello"))
}
func test_TransformedAttributeNoThrow() {
XCTAssertNoThrow(try TransformedAttribute<String, TestTransformer>(rawValue: "10"))
}
func test_TransformedAttributeThrows() {
XCTAssertThrowsError(try TransformedAttribute<String, TestTransformer>(rawValue: "10.3"))
}
func test_TransformedAttributeReversNoThrow() {
XCTAssertNoThrow(try TransformedAttribute<String, TestTransformer>(transformedValue: 10))
}
}
// MARK: Test types
extension AttributeTests {
enum TestTransformer: ReversibleTransformer {
public static func transform(_ value: String) throws -> Int {
guard let ret = Int(value) else {
throw DecodingError.typeMismatch(Int.self, .init(codingPath: [], debugDescription: "Expected Int from String."))
}
return ret
}
public static func reverse(_ value: Int) throws -> String {
return String(value)
}
}
}
@@ -18,6 +18,11 @@ class Relationship_LiteralTests: XCTestCase {
func test_ArrayLiteral() {
XCTAssertEqual(ToManyRelationship<TestEntity>(ids: ["1", "2", "3"]), ["1", "2", "3"])
}
func test_StringLiteral() {
XCTAssertEqual(ToOneRelationship<TestEntity>(id: "123"), "123")
XCTAssertEqual(ToOneRelationship<TestEntity?>(id: "123"), "123")
}
}
// MARK: - Test types
@@ -0,0 +1,159 @@
//
// PolyProxyTests.swift
// JSONAPITests
//
// Created by Mathew Polzin on 11/29/18.
//
import XCTest
import JSONAPI
public class PolyProxyTests: XCTestCase {
func test_generalReasonableness() {
XCTAssertNotEqual(decoded(type: User.self, data: poly_user_stub_1), decoded(type: User.self, data: poly_user_stub_2))
XCTAssertEqual(User.type, "users")
}
func test_UserADecode() {
let polyUserA = decoded(type: User.self, data: poly_user_stub_1)
let userA = decoded(type: UserA.self, data: poly_user_stub_1)
XCTAssertEqual(polyUserA.userA, userA)
XCTAssertNil(polyUserA.userB)
XCTAssertEqual(polyUserA[\.name], "Ken Moore")
XCTAssertEqual(polyUserA.id, "1")
XCTAssertEqual(polyUserA.relationships, .none)
}
func test_UserAAndBEncodeEquality() {
test_DecodeEncodeEquality(type: User.self, data: poly_user_stub_1)
test_DecodeEncodeEquality(type: User.self, data: poly_user_stub_2)
}
func test_AsymmetricEncodeDecodeUserA() {
let userA = decoded(type: UserA.self, data: poly_user_stub_1)
let polyUserA = decoded(type: User.self, data: poly_user_stub_1)
let encodedPoly = try! JSONEncoder().encode(polyUserA)
XCTAssertEqual(decoded(type: UserA.self, data: encodedPoly), userA)
}
func test_AsymmetricEncodeDecodeUserB() {
let userB = decoded(type: UserB.self, data: poly_user_stub_2)
let polyUserB = decoded(type: User.self, data: poly_user_stub_2)
let encodedPoly = try! JSONEncoder().encode(polyUserB)
XCTAssertEqual(decoded(type: UserB.self, data: encodedPoly), userB)
}
func test_UserBDecode() {
let polyUserB = decoded(type: User.self, data: poly_user_stub_2)
let userB = decoded(type: UserB.self, data: poly_user_stub_2)
XCTAssertEqual(polyUserB.userB, userB)
XCTAssertNil(polyUserB.userA)
XCTAssertEqual(polyUserB[\.name], "Ken Less")
XCTAssertEqual(polyUserB.id, "2")
XCTAssertEqual(polyUserB.relationships, .none)
}
}
// MARK: - Test types
public extension PolyProxyTests {
public enum UserDescription1: EntityDescription {
public static var type: String { return "users" }
public struct Attributes: JSONAPI.Attributes {
let firstName: Attribute<String>
let lastName: Attribute<String>
}
public typealias Relationships = NoRelationships
}
public enum UserDescription2: EntityDescription {
public static var type: String { return "users" }
public struct Attributes: JSONAPI.Attributes {
let name: Attribute<[String]>
}
public typealias Relationships = NoRelationships
}
public typealias UserA = Entity<UserDescription1>
public typealias UserB = Entity<UserDescription2>
public typealias User = Poly2<UserA, UserB>
}
extension Poly2: EntityProxy where A == PolyProxyTests.UserA, B == PolyProxyTests.UserB {
public var userA: PolyProxyTests.UserA? {
return a
}
public var userB: PolyProxyTests.UserB? {
return b
}
public var id: Id<EntityRawIdType, PolyProxyTests.User> {
switch self {
case .a(let a):
return Id(rawValue: a.id.rawValue)
case .b(let b):
return Id(rawValue: b.id.rawValue)
}
}
public var attributes: SharedUserDescription.Attributes {
switch self {
case .a(let a):
return .init(name: .init(value: "\(a[\.firstName]) \(a[\.lastName])"))
case .b(let b):
return .init(name: .init(value: b[\.name].joined(separator: " ")))
}
}
public var relationships: NoRelationships {
return .none
}
public enum SharedUserDescription: EntityDescription {
public static var type: String { return A.type }
public struct Attributes: JSONAPI.Attributes {
let name: Attribute<String>
}
public typealias Relationships = NoRelationships
}
public typealias Description = SharedUserDescription
public typealias EntityRawIdType = A.EntityRawIdType
}
// MARK: - Test stubs
private let poly_user_stub_1 = """
{
"id": "1",
"type": "users",
"attributes": {
"firstName": "Ken",
"lastName": "Moore"
}
}
""".data(using: .utf8)!
private let poly_user_stub_2 = """
{
"id": "2",
"type": "users",
"attributes": {
"name": ["Ken", "Less"]
}
}
""".data(using: .utf8)!
+16
View File
@@ -4,6 +4,9 @@ extension AttributeTests {
static let __allTests = [
("test_AttributeIsTransformedAttribute", test_AttributeIsTransformedAttribute),
("test_AttributeNonThrowingConstructor", test_AttributeNonThrowingConstructor),
("test_TransformedAttributeNoThrow", test_TransformedAttributeNoThrow),
("test_TransformedAttributeReversNoThrow", test_TransformedAttributeReversNoThrow),
("test_TransformedAttributeThrows", test_TransformedAttributeThrows),
]
}
@@ -180,6 +183,17 @@ extension LinksTests {
]
}
extension PolyProxyTests {
static let __allTests = [
("test_AsymmetricEncodeDecodeUserA", test_AsymmetricEncodeDecodeUserA),
("test_AsymmetricEncodeDecodeUserB", test_AsymmetricEncodeDecodeUserB),
("test_generalReasonableness", test_generalReasonableness),
("test_UserAAndBEncodeEquality", test_UserAAndBEncodeEquality),
("test_UserADecode", test_UserADecode),
("test_UserBDecode", test_UserBDecode),
]
}
extension PolyTests {
static let __allTests = [
("test_init_Poly0", test_init_Poly0),
@@ -223,6 +237,7 @@ extension Relationship_LiteralTests {
static let __allTests = [
("test_ArrayLiteral", test_ArrayLiteral),
("test_NilLiteral", test_NilLiteral),
("test_StringLiteral", test_StringLiteral),
]
}
@@ -250,6 +265,7 @@ public func __allTests() -> [XCTestCaseEntry] {
testCase(Id_LiteralTests.__allTests),
testCase(IncludedTests.__allTests),
testCase(LinksTests.__allTests),
testCase(PolyProxyTests.__allTests),
testCase(PolyTests.__allTests),
testCase(RelationshipTests.__allTests),
testCase(Relationship_LiteralTests.__allTests),