diff --git a/Sources/JSONAPI/EncodingError.swift b/Sources/JSONAPI/EncodingError.swift new file mode 100644 index 0000000..1d8145f --- /dev/null +++ b/Sources/JSONAPI/EncodingError.swift @@ -0,0 +1,14 @@ +// +// EncodingError.swift +// JSONAPI +// +// Created by Mathew Polzin on 12/7/18. +// + +public enum JSONAPIEncodingError: Swift.Error { + case typeMismatch(expected: String, found: String) + case illegalEncoding(String) + case illegalDecoding(String) + case missingOrMalformedMetadata + case missingOrMalformedLinks +} diff --git a/Sources/JSONAPI/Resource/Entity.swift b/Sources/JSONAPI/Resource/Entity.swift index 1ab2e8e..048ccb8 100644 --- a/Sources/JSONAPI/Resource/Entity.swift +++ b/Sources/JSONAPI/Resource/Entity.swift @@ -26,22 +26,26 @@ public struct NoAttributes: Attributes { public static var none: NoAttributes { return .init() } } +/// Something that is JSONTyped provides a String representation +/// of its type. +public protocol JSONTyped { + static var type: String { get } +} + /// An `EntityDescription` describes a JSON API /// Resource Object. The Resource Object /// itself is encoded and decoded as an /// `Entity`, which gets specialized on an /// `EntityDescription`. -public protocol EntityDescription { +public protocol EntityDescription: JSONTyped { associatedtype Attributes: JSONAPI.Attributes associatedtype Relationships: JSONAPI.Relationships - - static var type: String { get } } /// 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 { +public protocol EntityProxy: Equatable, JSONTyped { associatedtype Description: EntityDescription associatedtype EntityRawIdType: JSONAPI.MaybeRawId @@ -108,9 +112,10 @@ public struct Entity \.other` instead /// of `entity.relationships.other.id`. - public static func ~>(entity: Self, path: KeyPath>) -> OtherEntity.WrappedIdentifier { + public static func ~>(entity: Self, path: KeyPath>) -> OtherEntity.WrappedId { return entity.relationships[keyPath: path].id } /// Access to an Id of an optional `ToOneRelationship`. /// This allows you to write `entity ~> \.other` instead /// of `entity.relationships.other?.id`. - public static func ~>(entity: Self, path: KeyPath?>) -> OtherEntity.WrappedIdentifier where OtherEntity.WrappedIdentifier == OtherEntity.Identifier? { + public static func ~>(entity: Self, path: KeyPath?>) -> OtherEntity.WrappedId where OtherEntity.WrappedId == OtherEntity.Wrapped.Identifier? { // Implementation Note: This signature applies to `ToOneRelationship?` // whereas the one below applies to `ToOneRelationship?` return entity.relationships[keyPath: path]?.id @@ -443,7 +448,7 @@ public extension EntityProxy { /// Access to an Id of an optional `ToOneRelationship`. /// This allows you to write `entity ~> \.other` instead /// of `entity.relationships.other?.id`. - public static func ~>(entity: Self, path: KeyPath?>) -> OtherEntity.Identifier? where OtherEntity.WrappedIdentifier == OtherEntity.Identifier { + public static func ~>(entity: Self, path: KeyPath?>) -> OtherEntity.Identifier? where OtherEntity.WrappedId == OtherEntity.Identifier { // Implementation Note: This signature applies to `ToOneRelationship?` // whereas the one above applies to `ToOneRelationship?` return entity.relationships[keyPath: path]?.id diff --git a/Sources/JSONAPI/Resource/Id.swift b/Sources/JSONAPI/Resource/Id.swift index 5cdebc8..274f7d6 100644 --- a/Sources/JSONAPI/Resource/Id.swift +++ b/Sources/JSONAPI/Resource/Id.swift @@ -74,6 +74,10 @@ public struct Id: Codable, extension Id: Hashable, CustomStringConvertible, IdType where RawType: RawIdType {} +extension Id: WrappedIdType where RawType: RawIdType { + public typealias Identifier = Id +} + extension Id: CreatableIdType where RawType: CreatableRawIdType { public init() { rawValue = .unique() diff --git a/Sources/JSONAPI/Resource/Relationship.swift b/Sources/JSONAPI/Resource/Relationship.swift index 85b5a7f..8260cbe 100644 --- a/Sources/JSONAPI/Resource/Relationship.swift +++ b/Sources/JSONAPI/Resource/Relationship.swift @@ -17,14 +17,14 @@ public protocol RelationshipType: Codable { /// 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: RelationshipType, Equatable { +public struct ToOneRelationship: RelationshipType, Equatable { - public let id: Relatable.WrappedIdentifier + public let id: OptionalRelatable.WrappedId public let meta: MetaType public let links: LinksType - public init(id: Relatable.WrappedIdentifier, meta: MetaType, links: LinksType) { + public init(id: OptionalRelatable.WrappedId, meta: MetaType, links: LinksType) { self.id = id self.meta = meta self.links = links @@ -32,31 +32,31 @@ public struct ToOneRelationship(entity: E, meta: MetaType, links: LinksType) where E.Description == Relatable.Description, E.Id == Relatable.Identifier { +extension ToOneRelationship { + public init(entity: E, meta: MetaType, links: LinksType) where E.Id == OptionalRelatable.WrappedId { self.init(id: entity.id, meta: meta, links: links) } } -extension ToOneRelationship where Relatable.WrappedIdentifier == Relatable.Identifier, MetaType == NoMetadata, LinksType == NoLinks { - public init(entity: E) where E.Description == Relatable.Description, E.Id == Relatable.Identifier { +extension ToOneRelationship where MetaType == NoMetadata, LinksType == NoLinks { + public init(entity: E) where E.Id == OptionalRelatable.WrappedId { self.init(id: entity.id, meta: .none, links: .none) } } -extension ToOneRelationship where Relatable.WrappedIdentifier == Relatable.Identifier? { - public init(entity: E?, meta: MetaType, links: LinksType) where E.Description == Relatable.Description, E.Id == Relatable.Identifier { +extension ToOneRelationship where OptionalRelatable.WrappedId == OptionalRelatable.Wrapped.Identifier? { + public init(entity: E?, meta: MetaType, links: LinksType) where E.Id == OptionalRelatable.Wrapped.Identifier { self.init(id: entity?.id, meta: meta, links: links) } } -extension ToOneRelationship where Relatable.WrappedIdentifier == Relatable.Identifier?, MetaType == NoMetadata, LinksType == NoLinks { - public init(entity: E?) where E.Description == Relatable.Description, E.Id == Relatable.Identifier { +extension ToOneRelationship where OptionalRelatable.WrappedId == OptionalRelatable.Wrapped.Identifier?, MetaType == NoMetadata, LinksType == NoLinks { + public init(entity: E?) where E.Id == OptionalRelatable.Wrapped.Identifier { self.init(id: entity?.id, meta: .none, links: .none) } } @@ -78,20 +78,20 @@ public struct ToManyRelationship(pointers: [ToOneRelationship], meta: MetaType, links: LinksType) where T.WrappedIdentifier == Relatable.Identifier { + public init(pointers: [ToOneRelationship], meta: MetaType, links: LinksType) where T.WrappedId == Relatable.Identifier { ids = pointers.map { $0.id } self.meta = meta self.links = links } - public init(entities: [E], meta: MetaType, links: LinksType) where E.Description == Relatable.Description, E.Id == Relatable.Identifier { + public init(entities: [E], meta: MetaType, links: LinksType) where E.Id == Relatable.Identifier { self.init(ids: entities.map { $0.id }, meta: meta, links: links) } private init(meta: MetaType, links: LinksType) { self.init(ids: [], meta: meta, links: links) } - + public static func none(withMeta meta: MetaType, links: LinksType) -> ToManyRelationship { return ToManyRelationship(meta: meta, links: links) } @@ -103,7 +103,7 @@ extension ToManyRelationship where MetaType == NoMetadata, LinksType == NoLinks self.init(ids: ids, meta: .none, links: .none) } - public init(pointers: [ToOneRelationship]) where T.WrappedIdentifier == Relatable.Identifier { + public init(pointers: [ToOneRelationship]) where T.WrappedId == Relatable.Identifier { self.init(pointers: pointers, meta: .none, links: .none) } @@ -111,28 +111,36 @@ extension ToManyRelationship where MetaType == NoMetadata, LinksType == NoLinks return .none(withMeta: .none, links: .none) } - public init(entities: [E]) where E.Description == Relatable.Description, E.Id == Relatable.Identifier { + public init(entities: [E]) where E.Id == Relatable.Identifier { self.init(entities: entities, meta: .none, links: .none) } } -/// The WrappedRelatable (a.k.a OptionalRelatable) protocol -/// describes Optional and Relatable types. -public protocol WrappedRelatable: Codable, Equatable { - associatedtype Description: EntityDescription - associatedtype Identifier: JSONAPI.IdType - associatedtype WrappedIdentifier: Codable, Equatable -} -public typealias OptionalRelatable = WrappedRelatable - /// The Relatable protocol describes anything that /// has an IdType Identifier -public protocol Relatable: WrappedRelatable {} +public protocol Relatable: JSONTyped { + associatedtype Identifier: JSONAPI.IdType +} -extension Optional: OptionalRelatable where Wrapped: Relatable { - public typealias Description = Wrapped.Description - public typealias Identifier = Wrapped.Identifier - public typealias WrappedIdentifier = Identifier? +/// OptionalRelatable just describes an Optional +/// with a Reltable Wrapped type. +public protocol OptionalRelatable: JSONTyped { + associatedtype Wrapped: JSONAPI.Relatable + associatedtype WrappedId: WrappedIdType where WrappedId.Identifier == Wrapped.Identifier +} + +public protocol WrappedIdType: Codable, Equatable { + associatedtype Identifier: JSONAPI.IdType +} + +extension Optional: WrappedIdType where Wrapped: IdType { + public typealias Identifier = Wrapped +} + +extension Optional: OptionalRelatable, JSONTyped where Wrapped: JSONAPI.Relatable { + public typealias WrappedId = Wrapped.Identifier? + + public static var type: String { return Wrapped.type } } // MARK: Codable @@ -146,14 +154,6 @@ private enum ResourceIdentifierCodingKeys: String, CodingKey { case entityType = "type" } -public enum JSONAPIEncodingError: Swift.Error { - case typeMismatch(expected: String, found: String) - case illegalEncoding(String) - case illegalDecoding(String) - case missingOrMalformedMetadata - case missingOrMalformedLinks -} - extension ToOneRelationship { public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: ResourceLinkageCodingKeys.self) @@ -177,7 +177,7 @@ extension ToOneRelationship { // type at which point we can store nil in `id`. let anyNil: Any? = nil if try container.decodeNil(forKey: .data), - let val = anyNil as? Relatable.WrappedIdentifier { + let val = anyNil as? OptionalRelatable.WrappedId { id = val return } @@ -186,11 +186,11 @@ extension ToOneRelationship { let type = try identifier.decode(String.self, forKey: .entityType) - guard type == Relatable.Description.type else { - throw JSONAPIEncodingError.typeMismatch(expected: Relatable.Description.type, found: type) + guard type == OptionalRelatable.type else { + throw JSONAPIEncodingError.typeMismatch(expected: OptionalRelatable.type, found: type) } - id = try identifier.decode(Relatable.WrappedIdentifier.self, forKey: .id) + id = try identifier.decode(OptionalRelatable.WrappedId.self, forKey: .id) } public func encode(to encoder: Encoder) throws { @@ -211,7 +211,7 @@ extension ToOneRelationship { var identifier = container.nestedContainer(keyedBy: ResourceIdentifierCodingKeys.self, forKey: .data) try identifier.encode(id, forKey: .id) - try identifier.encode(Relatable.Description.type, forKey: .entityType) + try identifier.encode(OptionalRelatable.type, forKey: .entityType) } } @@ -239,8 +239,8 @@ extension ToManyRelationship { let type = try identifier.decode(String.self, forKey: .entityType) - guard type == Relatable.Description.type else { - throw JSONAPIEncodingError.typeMismatch(expected: Relatable.Description.type, found: type) + guard type == Relatable.type else { + throw JSONAPIEncodingError.typeMismatch(expected: Relatable.type, found: type) } newIds.append(try identifier.decode(Relatable.Identifier.self, forKey: .id)) @@ -265,7 +265,7 @@ extension ToManyRelationship { var identifier = identifiers.nestedContainer(keyedBy: ResourceIdentifierCodingKeys.self) try identifier.encode(id, forKey: .id) - try identifier.encode(Relatable.Description.type, forKey: .entityType) + try identifier.encode(Relatable.type, forKey: .entityType) } } } diff --git a/Sources/JSONAPITestLib/Relationship+Literal.swift b/Sources/JSONAPITestLib/Relationship+Literal.swift index 384efad..700c847 100644 --- a/Sources/JSONAPITestLib/Relationship+Literal.swift +++ b/Sources/JSONAPITestLib/Relationship+Literal.swift @@ -7,34 +7,34 @@ import JSONAPI -extension ToOneRelationship: ExpressibleByNilLiteral where Relatable.WrappedIdentifier: ExpressibleByNilLiteral, MetaType == NoMetadata, LinksType == NoLinks { +extension ToOneRelationship: ExpressibleByNilLiteral where OptionalRelatable.WrappedId: ExpressibleByNilLiteral, MetaType == NoMetadata, LinksType == NoLinks { public init(nilLiteral: ()) { - self.init(id: Relatable.WrappedIdentifier(nilLiteral: ())) + self.init(id: OptionalRelatable.WrappedId(nilLiteral: ())) } } -extension ToOneRelationship: ExpressibleByUnicodeScalarLiteral where Relatable.WrappedIdentifier: ExpressibleByUnicodeScalarLiteral, MetaType == NoMetadata, LinksType == NoLinks { - public typealias UnicodeScalarLiteralType = Relatable.WrappedIdentifier.UnicodeScalarLiteralType +extension ToOneRelationship: ExpressibleByUnicodeScalarLiteral where OptionalRelatable.WrappedId: ExpressibleByUnicodeScalarLiteral, MetaType == NoMetadata, LinksType == NoLinks { + public typealias UnicodeScalarLiteralType = OptionalRelatable.WrappedId.UnicodeScalarLiteralType public init(unicodeScalarLiteral value: UnicodeScalarLiteralType) { - self.init(id: Relatable.WrappedIdentifier(unicodeScalarLiteral: value)) + self.init(id: OptionalRelatable.WrappedId(unicodeScalarLiteral: value)) } } -extension ToOneRelationship: ExpressibleByExtendedGraphemeClusterLiteral where Relatable.WrappedIdentifier: ExpressibleByExtendedGraphemeClusterLiteral, MetaType == NoMetadata, LinksType == NoLinks { - public typealias ExtendedGraphemeClusterLiteralType = Relatable.WrappedIdentifier.ExtendedGraphemeClusterLiteralType +extension ToOneRelationship: ExpressibleByExtendedGraphemeClusterLiteral where OptionalRelatable.WrappedId: ExpressibleByExtendedGraphemeClusterLiteral, MetaType == NoMetadata, LinksType == NoLinks { + public typealias ExtendedGraphemeClusterLiteralType = OptionalRelatable.WrappedId.ExtendedGraphemeClusterLiteralType public init(extendedGraphemeClusterLiteral value: ExtendedGraphemeClusterLiteralType) { - self.init(id: Relatable.WrappedIdentifier(extendedGraphemeClusterLiteral: value)) + self.init(id: OptionalRelatable.WrappedId(extendedGraphemeClusterLiteral: value)) } } -extension ToOneRelationship: ExpressibleByStringLiteral where Relatable.WrappedIdentifier: ExpressibleByStringLiteral, MetaType == NoMetadata, LinksType == NoLinks { - public typealias StringLiteralType = Relatable.WrappedIdentifier.StringLiteralType +extension ToOneRelationship: ExpressibleByStringLiteral where OptionalRelatable.WrappedId: ExpressibleByStringLiteral, MetaType == NoMetadata, LinksType == NoLinks { + public typealias StringLiteralType = OptionalRelatable.WrappedId.StringLiteralType public init(stringLiteral value: StringLiteralType) { - self.init(id: Relatable.WrappedIdentifier(stringLiteral: value)) + self.init(id: OptionalRelatable.WrappedId(stringLiteral: value)) } } diff --git a/Tests/JSONAPITests/Computed Properties/ComputedPropertiesTests.swift b/Tests/JSONAPITests/Computed Properties/ComputedPropertiesTests.swift index 16b7e1f..50290a9 100644 --- a/Tests/JSONAPITests/Computed Properties/ComputedPropertiesTests.swift +++ b/Tests/JSONAPITests/Computed Properties/ComputedPropertiesTests.swift @@ -27,6 +27,7 @@ class ComputedPropertiesTests: XCTestCase { let entity = decoded(type: TestType.self, data: computed_property_attribute) XCTAssertEqual(entity[\.computed], "Sarah2") + XCTAssertEqual(entity[\.secretsOut], "shhhh") } func test_ComputedNonAttributeAccess() { @@ -49,6 +50,8 @@ extension ComputedPropertiesTests { public struct Attributes: JSONAPI.Attributes { public let name: Attribute + private let secret: Attribute + public var computed: Attribute { return name.map { $0 + "2" } } @@ -56,6 +59,10 @@ extension ComputedPropertiesTests { public var computed2: String { return computed.value } + + public var secretsOut: String { + return secret.value + } } public struct Relationships: JSONAPI.Relationships { diff --git a/Tests/JSONAPITests/Computed Properties/stubs/ComputedPropertiesStubs.swift b/Tests/JSONAPITests/Computed Properties/stubs/ComputedPropertiesStubs.swift index 11ef3a0..1507d95 100644 --- a/Tests/JSONAPITests/Computed Properties/stubs/ComputedPropertiesStubs.swift +++ b/Tests/JSONAPITests/Computed Properties/stubs/ComputedPropertiesStubs.swift @@ -10,7 +10,8 @@ let computed_property_attribute = """ "id": "1234", "type": "test", "attributes": { - "name": "Sarah" + "name": "Sarah", + "secret": "shhhh" }, "relationships": { "other": { diff --git a/Tests/JSONAPITests/NonJSONAPIRelatable/NonJSONAPIRelatableTests.swift b/Tests/JSONAPITests/NonJSONAPIRelatable/NonJSONAPIRelatableTests.swift new file mode 100644 index 0000000..5e70c20 --- /dev/null +++ b/Tests/JSONAPITests/NonJSONAPIRelatable/NonJSONAPIRelatableTests.swift @@ -0,0 +1,56 @@ +// +// NonJSONAPIRelatableTests.swift +// JSONAPITests +// +// Created by Mathew Polzin on 12/7/18. +// + +import XCTest +import JSONAPI + +class NonJSONAPIRelatableTests: XCTestCase { + +} + +// MARK: - Test Types +extension NonJSONAPIRelatableTests { +// enum TestEntityDescription: EntityDescription { +// static var type: String { return "test" } +// +// typealias Attributes = NoAttributes +// +// struct Relationships: JSONAPI.Relationships { +// let one: ToOneRelationship +// let many: ToManyRelationship +// } +// } + +// enum NonJSONAPIEntityDescription: EntityDescription { +// static var type: String { return "other" } +// +// typealias Attributes = NoAttributes +// typealias Relationships = NoRelationships +// } + +// struct NonJSONAPIEntity: Relatable, OptionalRelatable { +// typealias Description = NonJSONAPIEntityDescription +// typealias EntityRawIdType = String +// typealias Identifier = NonJSONAPIEntity.Id +// typealias WrappedIdentifier = NonJSONAPIEntity.Id +// +// let id: Id +// +// let attributes: NoAttributes +// let relationships: NoRelationships +// +// struct Id: IdType { +// var rawValue: NonJSONAPIRelatableTests.NonJSONAPIEntity.Id.RawType +// +// typealias EntityType = <#type#> +// +// typealias RawType = String +// +// let rawValue: String +// } +// } +} diff --git a/Tests/JSONAPITests/Poly/PolyProxyTests.swift b/Tests/JSONAPITests/Poly/PolyProxyTests.swift index 01e5947..514bfe8 100644 --- a/Tests/JSONAPITests/Poly/PolyProxyTests.swift +++ b/Tests/JSONAPITests/Poly/PolyProxyTests.swift @@ -89,7 +89,7 @@ public extension PolyProxyTests { public typealias User = Poly2 } -extension Poly2: EntityProxy where A == PolyProxyTests.UserA, B == PolyProxyTests.UserB { +extension Poly2: EntityProxy, JSONTyped where A == PolyProxyTests.UserA, B == PolyProxyTests.UserB { public var userA: PolyProxyTests.UserA? { return a