Compare commits

...

17 Commits

Author SHA1 Message Date
Mathew Polzin 67a43be26c update linuxmain 2018-12-08 20:05:00 -08:00
Mathew Polzin 8f279ce191 Add tests around nullable attributes and remove unnecessary encoding code 2018-12-08 20:04:11 -08:00
Mathew Polzin c9d388579f Made it much more convenient to work with Non-EntityType relationships. Discovered and fixed a bug where nullable relationships were encoded incorrectly. 2018-12-08 19:48:10 -08:00
Mathew Polzin 1061283905 loosen the grip entity had on Ids 2018-12-08 01:17:47 -08:00
Mathew Polzin 08949d0a93 Move encoding error type to its own file. Restructure Relatable and OptionalRelatable to not be dependent upon EntityDescription 2018-12-08 00:36:05 -08:00
Mathew Polzin 3047e2d23a Add access to computed attributes that are not AttributeType 2018-12-07 21:19:08 -08:00
Mathew Polzin 41a2a01788 Added support for relationship operator ~> to optional relationships 2018-12-07 20:59:39 -08:00
Mathew Polzin 53f7f55e07 Add Poly7 and Include7 because why not 2018-12-06 18:38:20 -08:00
Mathew Polzin d8d030286d Add a little bit of code doc 2018-12-06 18:12:56 -08:00
Mathew Polzin 005a981bf7 Add a couple of missing initializers to Entity 2018-12-05 22:51:12 -08:00
Mathew Polzin fad45203dd small refactor to consider ToOneRelationship with no meta or links to be even more synonymous with 'pointer' 2018-12-05 22:42:34 -08:00
Mathew Polzin 3468cb555a Remove Result dependency in favor of a super stripped down internally scoped Result enum. This won't interfere with other Result libraries and I was not exposing the Result anyway. 2018-12-05 22:02:59 -08:00
Mathew Polzin 8defe82536 Update README and linuxmain 2018-12-05 18:33:07 -08:00
Mathew Polzin 1f83216b03 Adding a few tests here and there 2018-12-05 18:31:53 -08:00
Mathew Polzin 6e6973c170 Adding test coverage for Relationship Meta and Links 2018-12-05 18:12:00 -08:00
Mathew Polzin 4f52cc2fd1 update README 2018-12-05 17:30:48 -08:00
Mathew Polzin 35d6cbb440 Add support for Relationship Meta and Links (untested) 2018-12-05 09:14:38 -08:00
35 changed files with 1382 additions and 170 deletions
+9 -7
View File
@@ -23,8 +23,10 @@ extension String: CreatableRawIdType {
}
}
// MARK: - Entity typealias for convenience
// MARK: - typealiases for convenience
public typealias ExampleEntity<Description: EntityDescription> = Entity<Description, NoMetadata, NoLinks, String>
public typealias ToOne<E: OptionalRelatable> = ToOneRelationship<E, NoMetadata, NoLinks>
public typealias ToMany<E: Relatable> = ToManyRelationship<E, NoMetadata, NoLinks>
// MARK: - A few resource objects (entities)
public enum PersonDescription: EntityDescription {
@@ -46,11 +48,11 @@ public enum PersonDescription: EntityDescription {
}
public struct Relationships: JSONAPI.Relationships {
public let friends: ToManyRelationship<Person>
public let dogs: ToManyRelationship<Dog>
public let home: ToOneRelationship<House>
public let friends: ToMany<Person>
public let dogs: ToMany<Dog>
public let home: ToOne<House>
public init(friends: ToManyRelationship<Person>, dogs: ToManyRelationship<Dog>, home: ToOneRelationship<House>) {
public init(friends: ToMany<Person>, dogs: ToMany<Dog>, home: ToOne<House>) {
self.friends = friends
self.dogs = dogs
self.home = home
@@ -79,9 +81,9 @@ public enum DogDescription: EntityDescription {
}
public struct Relationships: JSONAPI.Relationships {
public let owner: ToOneRelationship<Person?>
public let owner: ToOne<Person?>
public init(owner: ToOneRelationship<Person?>) {
public init(owner: ToOne<Person?>) {
self.owner = owner
}
}
+1 -9
View File
@@ -1,15 +1,7 @@
{
"object": {
"pins": [
{
"package": "Result",
"repositoryURL": "https://github.com/mattpolzin/Result",
"state": {
"branch": "master",
"revision": "b98e238da6ea030fa7862ae6fd6500552370019c",
"version": null
}
}
]
},
"version": 1
+1 -3
View File
@@ -14,13 +14,11 @@ let package = Package(
targets: ["JSONAPITestLib"])
],
dependencies: [
// antitypical/Result without the Foundation requirement:
.package(url: "https://github.com/mattpolzin/Result", .branch("master"))
],
targets: [
.target(
name: "JSONAPI",
dependencies: ["Result"]),
dependencies: []),
.target(
name: "JSONAPITestLib",
dependencies: ["JSONAPI"]),
+6 -4
View File
@@ -52,8 +52,8 @@ Note that Playground support for importing non-system Frameworks is still a bit
#### Relationship Object
- [x] `data`
- [ ] `links`
- [ ] `meta`
- [x] `links`
- [x] `meta`
#### Links Object
- [x] `href`
@@ -186,9 +186,11 @@ Note that I am assuming an unidentified person is a "new" person. I suspect that
There are two types of `Relationships`: `ToOneRelationship` and `ToManyRelationship`. An `EntityDescription`'s `Relationships` type can contain any number of `Relationship` properties of either of these types. Do not store anything other than `Relationship` properties in the `Relationships` struct of an `EntityDescription`.
To describe a relationship that may be omitted (i.e. the key is not even present in the JSON object), you make the entire `ToOneRelationship` or `ToManyRelationship` optional. However, this is not recommended because you can also represent optional relationships as nullable which means the key is always present. A `ToManyRelationship` can naturally represent the absence of related values with an empty array, so `ToManyRelationship` does not support nullability at all. A `ToOneRelationship` can be marked as nullable (i.e. the value might be `null` or it might be a resource identifier) like this:
In addition to identifying entities by Id and type, `Relationships` can contain `Meta` or `Links` that follow the same rules as [`Meta`](#jsonapimeta) and [`Links`](#jsonapilinks) elsewhere in the JSON API Document.
To describe a relationship that may be omitted (i.e. the key is not even present in the JSON object), you make the entire `ToOneRelationship` or `ToManyRelationship` optional. However, this is not recommended because you can also represent optional relationships as nullable which means the key is always present. A `ToManyRelationship` can naturally represent the absence of related values with an empty array, so `ToManyRelationship` does not support nullability at all. A `ToOneRelationship` can be marked as nullable (i.e. the value could be either `null` or a resource identifier) like this:
```
let nullableRelative: ToOneRelationship<Person?>
let nullableRelative: ToOneRelationship<Person?, NoMetadata, NoLinks>
```
An entity that does not have relationships can be described by adding the following to an `EntityDescription`:
+7
View File
@@ -115,4 +115,11 @@ extension Includes where I: _Poly6 {
}
// MARK: - 7 includes
public typealias Include7 = Poly7
extension Includes where I: _Poly7 {
public subscript(_ lookup: I.G.Type) -> [I.G] {
return values.compactMap { $0.g }
}
}
// MARK: - 8 includes
+14
View File
@@ -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
}
+12 -13
View File
@@ -8,6 +8,8 @@
public protocol AttributeType: Codable {
}
// MARK: TransformedAttribute
/// A TransformedAttribute takes a Codable type and attempts to turn it into another type.
public struct TransformedAttribute<RawValue: Codable, Transformer: JSONAPI.Transformer>: AttributeType where Transformer.From == RawValue {
private let rawValue: RawValue
@@ -19,6 +21,15 @@ public struct TransformedAttribute<RawValue: Codable, Transformer: JSONAPI.Trans
}
}
// MARK: ValidatedAttribute
/// A ValidatedAttribute does not transform its raw value, but it throws
/// an error if the raw value does not match expectations.
public typealias ValidatedAttribute<RawValue: Codable, Validator: JSONAPI.Validator> = TransformedAttribute<RawValue, Validator> where RawValue == Validator.From
// MARK: Attribute
/// An Attribute simply represents a type that can be encoded and decoded.
public typealias Attribute<T: Codable> = TransformedAttribute<T, IdentityTransformer<T>>
extension TransformedAttribute where Transformer: ReversibleTransformer {
public init(transformedValue: Transformer.To) throws {
self.value = transformedValue
@@ -43,10 +54,6 @@ extension TransformedAttribute where Transformer == IdentityTransformer<RawValue
}
}
public typealias ValidatedAttribute<RawValue: Codable, Validator: JSONAPI.Validator> = TransformedAttribute<RawValue, Validator> where RawValue == Validator.From
public typealias Attribute<T: Codable> = TransformedAttribute<T, IdentityTransformer<T>>
// MARK: - Codable
extension TransformedAttribute {
public init(from decoder: Decoder) throws {
@@ -73,15 +80,7 @@ extension TransformedAttribute {
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
// See note in decode above about the weirdness
// going on here.
let anyNil: Any? = nil
if let _ = anyNil as? Transformer.From,
(rawValue as Any?) == nil {
try container.encodeNil()
}
try container.encode(rawValue)
}
}
+77 -10
View File
@@ -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,8 @@ public struct Entity<Description: JSONAPI.EntityDescription, MetaType: JSONAPI.M
}
}
extension Entity: IdentifiableEntityType, Relatable, WrappedRelatable where EntityRawIdType: JSONAPI.RawIdType {
extension Entity: Identifiable, IdentifiableEntityType, Relatable where EntityRawIdType: JSONAPI.RawIdType {
public typealias Identifier = Entity.Id
public typealias WrappedIdentifier = Identifier
}
extension Entity: CustomStringConvertible {
@@ -326,6 +329,12 @@ extension Entity where MetaType == NoMetadata, EntityRawIdType: CreatableRawIdTy
}
}
extension Entity where MetaType == NoMetadata, EntityRawIdType == Unidentified {
public init(attributes: Description.Attributes, relationships: Description.Relationships, links: LinksType) {
self.init(attributes: attributes, relationships: relationships, meta: .none, links: links)
}
}
extension Entity where LinksType == NoLinks {
public init(id: Entity.Id, attributes: Description.Attributes, relationships: Description.Relationships, meta: MetaType) {
self.init(id: id, attributes: attributes, relationships: relationships, meta: meta, links: .none)
@@ -338,6 +347,12 @@ extension Entity where LinksType == NoLinks, EntityRawIdType: CreatableRawIdType
}
}
extension Entity where LinksType == NoLinks, EntityRawIdType == Unidentified {
public init(attributes: Description.Attributes, relationships: Description.Relationships, meta: MetaType) {
self.init(attributes: attributes, relationships: relationships, meta: meta, links: .none)
}
}
extension Entity where MetaType == NoMetadata, LinksType == NoLinks {
public init(id: Entity.Id, attributes: Description.Attributes, relationships: Description.Relationships) {
self.init(id: id, attributes: attributes, relationships: relationships, meta: .none, links: .none)
@@ -350,12 +365,28 @@ extension Entity where MetaType == NoMetadata, LinksType == NoLinks, EntityRawId
}
}
extension Entity where MetaType == NoMetadata, LinksType == NoLinks, EntityRawIdType == Unidentified {
public init(attributes: Description.Attributes, relationships: Description.Relationships) {
self.init(attributes: attributes, relationships: relationships, meta: .none, links: .none)
}
}
// MARK: Pointer for Relationships use.
public extension Entity where EntityRawIdType: JSONAPI.RawIdType {
/// An Entity.Pointer is a `ToOneRelationship` with no metadata or links.
/// This is just a convenient way to reference an Entity given that
/// other Entities' Relationships can be built up from it.
public typealias Pointer = ToOneRelationship<Entity, NoMetadata, NoLinks>
/// Get a pointer to this entity that can be used as a
/// relationship to another entity.
public var pointer: ToOneRelationship<Entity> {
return ToOneRelationship(entity: self)
public var pointer: Pointer {
return Pointer(entity: self)
}
public func pointer<MType: JSONAPI.Meta, LType: JSONAPI.Links>(withMeta meta: MType, links: LType) -> ToOneRelationship<Entity, MType, LType> {
return ToOneRelationship(entity: self, meta: meta, links: links)
}
}
@@ -379,8 +410,19 @@ public extension EntityProxy {
/// allows you to write `entity[\.propertyName]` instead
/// of `entity.relationships.propertyName`.
subscript<T, TFRM: Transformer, U>(_ path: KeyPath<Description.Attributes, TransformedAttribute<T, TFRM>?>) -> U? where TFRM.To == U? {
// Implementation Note: Handles Transform that returns optional
// type.
return attributes[keyPath: path].flatMap { $0.value }
}
/// Access the computed attribute at the given keypath. This just
/// allows you to write `entity[\.propertyName]` instead
/// of `entity.relationships.propertyName`.
subscript<T>(_ path: KeyPath<Description.Attributes, T>) -> T {
// Implementation Note: Handles attributes that are not
// AttributeType. These should only exist as computed properties.
return attributes[keyPath: path]
}
}
// MARK: Relationship Access
@@ -388,16 +430,41 @@ 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: Self, path: KeyPath<Description.Relationships, ToOneRelationship<OtherEntity>>) -> OtherEntity.WrappedIdentifier {
public static func ~><OtherEntity: Identifiable, MType: JSONAPI.Meta, LType: JSONAPI.Links>(entity: Self, path: KeyPath<Description.Relationships, ToOneRelationship<OtherEntity, MType, LType>>) -> OtherEntity.Identifier {
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 ~><OtherEntity: OptionalRelatable, MType: JSONAPI.Meta, LType: JSONAPI.Links>(entity: Self, path: KeyPath<Description.Relationships, ToOneRelationship<OtherEntity, MType, LType>?>) -> OtherEntity.Identifier {
// Implementation Note: This signature applies to `ToOneRelationship<E?, _, _>?`
// whereas the one below applies to `ToOneRelationship<E, _, _>?`
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 ~><OtherEntity: Relatable, MType: JSONAPI.Meta, LType: JSONAPI.Links>(entity: Self, path: KeyPath<Description.Relationships, ToOneRelationship<OtherEntity, MType, LType>?>) -> OtherEntity.Identifier? {
// Implementation Note: This signature applies to `ToOneRelationship<E, _, _>?`
// whereas the one above applies to `ToOneRelationship<E?, _, _>?`
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: Self, path: KeyPath<Description.Relationships, ToManyRelationship<OtherEntity>>) -> [OtherEntity.Identifier] {
public static func ~><OtherEntity: Relatable, MType: JSONAPI.Meta, LType: JSONAPI.Links>(entity: Self, path: KeyPath<Description.Relationships, ToManyRelationship<OtherEntity, MType, LType>>) -> [OtherEntity.Identifier] {
return entity.relationships[keyPath: path].ids
}
/// Access to all Ids of an optional `ToManyRelationship`.
/// This allows you to write `entity ~> \.others` instead
/// of `entity.relationships.others?.ids`.
public static func ~><OtherEntity: Relatable, MType: JSONAPI.Meta, LType: JSONAPI.Links>(entity: Self, path: KeyPath<Description.Relationships, ToManyRelationship<OtherEntity, MType, LType>?>) -> [OtherEntity.Identifier]? {
return entity.relationships[keyPath: path]?.ids
}
}
infix operator ~>
+30 -7
View File
@@ -34,13 +34,31 @@ public struct Unidentified: MaybeRawId, CustomStringConvertible {
public var description: String { return "Unidentified" }
}
public protocol MaybeId: Codable {
associatedtype EntityType: JSONAPI.EntityProxy
public protocol OptionalId: Codable {
associatedtype IdentifiableType: JSONAPI.JSONTyped
associatedtype RawType: MaybeRawId
var rawValue: RawType { get }
init(rawValue: RawType)
}
public protocol IdType: MaybeId, CustomStringConvertible, Hashable where RawType: RawIdType {
var rawValue: RawType { get }
public protocol IdType: OptionalId, CustomStringConvertible, Hashable where RawType: RawIdType {}
extension Optional: MaybeRawId where Wrapped: Codable & Equatable {}
extension Optional: OptionalId where Wrapped: IdType {
public typealias IdentifiableType = Wrapped.IdentifiableType
public typealias RawType = Wrapped.RawType?
public var rawValue: Wrapped.RawType? {
guard case .some(let value) = self else {
return nil
}
return value.rawValue
}
public init(rawValue: Wrapped.RawType?) {
self = rawValue.map { Wrapped(rawValue: $0) }
}
}
public extension IdType {
@@ -53,7 +71,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.EntityProxy>: Codable, Equatable, MaybeId {
public struct Id<RawType: MaybeRawId, IdentifiableType: JSONAPI.JSONTyped>: Equatable, OptionalId {
public let rawValue: RawType
@@ -63,7 +81,8 @@ public struct Id<RawType: MaybeRawId, EntityType: JSONAPI.EntityProxy>: Codable,
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
rawValue = try container.decode(RawType.self)
let rawValue = try container.decode(RawType.self)
self.init(rawValue: rawValue)
}
public func encode(to encoder: Encoder) throws {
@@ -72,7 +91,11 @@ public struct Id<RawType: MaybeRawId, EntityType: JSONAPI.EntityProxy>: Codable,
}
}
extension Id: Hashable, CustomStringConvertible, IdType where RawType: RawIdType {}
extension Id: Hashable, CustomStringConvertible, IdType where RawType: RawIdType {
public static func id(from rawValue: RawType) -> Id<RawType, IdentifiableType> {
return Id(rawValue: rawValue)
}
}
extension Id: CreatableIdType where RawType: CreatableRawIdType {
public init() {
+156 -3
View File
@@ -5,8 +5,6 @@
// Created by Mathew Polzin on 11/22/18.
//
import Result
/// Poly is a protocol to which types that
/// are polymorphic belong to. Specifically,
/// Poly1, Poly2, Poly3, etc. types conform
@@ -28,7 +26,7 @@ private func decode<Entity: JSONAPI.EntityType>(_ type: Entity.Type, from contai
} catch (let err) {
ret = .failure(DecodingError.typeMismatch(Entity.Description.self,
.init(codingPath: container.codingPath,
debugDescription: err.localizedDescription,
debugDescription: String(describing: err),
underlyingError: err)))
}
return ret
@@ -646,3 +644,158 @@ extension Poly6: CustomStringConvertible {
return "Poly(\(str))"
}
}
// MARK: - 7 types
public protocol _Poly7: _Poly6 {
associatedtype G: EntityType
var g: G? { get }
init(_ g: G)
}
public extension _Poly7 {
subscript(_ lookup: G.Type) -> G? {
return g
}
}
public enum Poly7<A: EntityType, B: EntityType, C: EntityType, D: EntityType, E: EntityType, F: EntityType, G: EntityType>: _Poly7 {
case a(A)
case b(B)
case c(C)
case d(D)
case e(E)
case f(F)
case g(G)
public var a: A? {
guard case let .a(ret) = self else { return nil }
return ret
}
public init(_ a: A) {
self = .a(a)
}
public var b: B? {
guard case let .b(ret) = self else { return nil }
return ret
}
public init(_ b: B) {
self = .b(b)
}
public var c: C? {
guard case let .c(ret) = self else { return nil }
return ret
}
public init(_ c: C) {
self = .c(c)
}
public var d: D? {
guard case let .d(ret) = self else { return nil }
return ret
}
public init(_ d: D) {
self = .d(d)
}
public var e: E? {
guard case let .e(ret) = self else { return nil }
return ret
}
public init(_ e: E) {
self = .e(e)
}
public var f: F? {
guard case let .f(ret) = self else { return nil }
return ret
}
public init(_ f: F) {
self = .f(f)
}
public var g: G? {
guard case let .g(ret) = self else { return nil }
return ret
}
public init(_ g: G) {
self = .g(g)
}
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let attempts = [
try decode(A.self, from: container).map { Poly7.a($0) },
try decode(B.self, from: container).map { Poly7.b($0) },
try decode(C.self, from: container).map { Poly7.c($0) },
try decode(D.self, from: container).map { Poly7.d($0) },
try decode(E.self, from: container).map { Poly7.e($0) },
try decode(F.self, from: container).map { Poly7.f($0) },
try decode(G.self, from: container).map { Poly7.g($0) }]
let maybeVal: Poly7<A, B, C, D, E, F, G>? = attempts
.compactMap { $0.value }
.first
guard let val = maybeVal else {
throw EncodingError.invalidValue(Poly7<A, B, C, D, E, F, G>.self, .init(codingPath: decoder.codingPath, debugDescription: "Failed to find an include of the expected type. Attempts: \(attempts.map { $0.error }.compactMap { $0 })"))
}
self = val
}
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch self {
case .a(let a):
try container.encode(a)
case .b(let b):
try container.encode(b)
case .c(let c):
try container.encode(c)
case .d(let d):
try container.encode(d)
case .e(let e):
try container.encode(e)
case .f(let f):
try container.encode(f)
case .g(let g):
try container.encode(g)
}
}
}
extension Poly7: CustomStringConvertible {
public var description: String {
let str: String
switch self {
case .a(let a):
str = String(describing: a)
case .b(let b):
str = String(describing: b)
case .c(let c):
str = String(describing: c)
case .d(let d):
str = String(describing: d)
case .e(let e):
str = String(describing: e)
case .f(let f):
str = String(describing: f)
case .g(let g):
str = String(describing: g)
}
return "Poly(\(str))"
}
}
+157 -58
View File
@@ -5,30 +5,59 @@
// Created by Mathew Polzin on 8/31/18.
//
public protocol RelationshipType: Codable {}
public protocol RelationshipType {
associatedtype LinksType
associatedtype MetaType
var links: LinksType { get }
var meta: MetaType { 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<EntityDescription>`
public struct ToOneRelationship<Relatable: JSONAPI.OptionalRelatable>: RelationshipType, Equatable {
public struct ToOneRelationship<Identifiable: JSONAPI.Identifiable, MetaType: JSONAPI.Meta, LinksType: JSONAPI.Links>: RelationshipType, Equatable {
public let id: Relatable.WrappedIdentifier
public let id: Identifiable.Identifier
public init(id: Relatable.WrappedIdentifier) {
public let meta: MetaType
public let links: LinksType
public init(id: Identifiable.Identifier, meta: MetaType, links: LinksType) {
self.id = id
self.meta = meta
self.links = links
}
}
extension ToOneRelationship where Relatable.WrappedIdentifier == Relatable.Identifier {
public init<E: EntityType>(entity: E) where E.Description == Relatable.Description, E.Id == Relatable.Identifier {
id = entity.id
extension ToOneRelationship where MetaType == NoMetadata, LinksType == NoLinks {
public init(id: Identifiable.Identifier) {
self.init(id: id, meta: .none, links: .none)
}
}
extension ToOneRelationship where Relatable.WrappedIdentifier == Relatable.Identifier? {
public init<E: EntityType>(entity: E?) where E.Description == Relatable.Description, E.Id == Relatable.Identifier {
id = entity?.id
extension ToOneRelationship {
public init<E: EntityType>(entity: E, meta: MetaType, links: LinksType) where E.Id == Identifiable.Identifier {
self.init(id: entity.id, meta: meta, links: links)
}
}
extension ToOneRelationship where MetaType == NoMetadata, LinksType == NoLinks {
public init<E: EntityType>(entity: E) where E.Id == Identifiable.Identifier {
self.init(id: entity.id, meta: .none, links: .none)
}
}
extension ToOneRelationship where Identifiable: OptionalRelatable {
public init<E: EntityType>(entity: E?, meta: MetaType, links: LinksType) where E.Id == Identifiable.Wrapped.Identifier {
self.init(id: entity?.id, meta: meta, links: links)
}
}
extension ToOneRelationship where Identifiable: OptionalRelatable, MetaType == NoMetadata, LinksType == NoLinks {
public init<E: EntityType>(entity: E?) where E.Id == Identifiable.Wrapped.Identifier {
self.init(id: entity?.id, meta: .none, links: .none)
}
}
@@ -36,73 +65,105 @@ extension ToOneRelationship where Relatable.WrappedIdentifier == Relatable.Ident
/// 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<EntityDescription>`
public struct ToManyRelationship<Relatable: JSONAPI.Relatable>: RelationshipType, Equatable {
public struct ToManyRelationship<Relatable: JSONAPI.Relatable, MetaType: JSONAPI.Meta, LinksType: JSONAPI.Links>: RelationshipType, Equatable {
public let ids: [Relatable.Identifier]
public init(ids: [Relatable.Identifier]) {
public let meta: MetaType
public let links: LinksType
public init(ids: [Relatable.Identifier], meta: MetaType, links: LinksType) {
self.ids = ids
self.meta = meta
self.links = links
}
public init<T: JSONAPI.Relatable>(relationships: [ToOneRelationship<T>]) where T.WrappedIdentifier == Relatable.Identifier {
ids = relationships.map { $0.id }
public init<T: JSONAPI.Identifiable>(pointers: [ToOneRelationship<T, NoMetadata, NoLinks>], meta: MetaType, links: LinksType) where T.Identifier == Relatable.Identifier {
ids = pointers.map { $0.id }
self.meta = meta
self.links = links
}
private init() {
ids = []
public init<E: EntityType>(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)
}
}
extension ToManyRelationship where MetaType == NoMetadata, LinksType == NoLinks {
public init(ids: [Relatable.Identifier]) {
self.init(ids: ids, meta: .none, links: .none)
}
public init<T: JSONAPI.Identifiable>(pointers: [ToOneRelationship<T, NoMetadata, NoLinks>]) where T.Identifier == Relatable.Identifier {
self.init(pointers: pointers, meta: .none, links: .none)
}
public static var none: ToManyRelationship {
return .init()
return .none(withMeta: .none, links: .none)
}
public init<E: EntityType>(entities: [E]) where E.Id == Relatable.Identifier {
self.init(entities: entities, meta: .none, links: .none)
}
}
extension ToManyRelationship {
public init<E: EntityType>(entities: [E]) where E.Description == Relatable.Description, E.Id == Relatable.Identifier {
ids = entities.map { $0.id }
}
public protocol Identifiable: JSONTyped {
associatedtype Identifier: Equatable
}
/// The WrappedRelatable (a.k.a OptionalRelatable) protocol
/// describes Optional<T: Relatable> 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: Identifiable where 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: Identifiable where Identifier == Wrapped.Identifier? {
associatedtype Wrapped: JSONAPI.Relatable
}
extension Optional: Identifiable, OptionalRelatable, JSONTyped where Wrapped: JSONAPI.Relatable {
public typealias Identifier = Wrapped.Identifier?
public static var type: String { return Wrapped.type }
}
// MARK: Codable
private enum ResourceLinkageCodingKeys: String, CodingKey {
case data = "data"
case meta = "meta"
case links = "links"
}
private enum ResourceIdentifierCodingKeys: String, CodingKey {
case id = "id"
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 {
extension ToOneRelationship: Codable where Identifiable.Identifier: OptionalId {
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: ResourceLinkageCodingKeys.self)
if let noMeta = NoMetadata() as? MetaType {
meta = noMeta
} else {
meta = try container.decode(MetaType.self, forKey: .meta)
}
if let noLinks = NoLinks() as? LinksType {
links = noLinks
} else {
links = try container.decode(LinksType.self, forKey: .links)
}
// A little trickery follows. If the id is nil, the
// container.decode(Identifier.self) will fail even if Identifier
// is Optional. However, we can check if decoding nil
@@ -110,7 +171,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? Identifiable.Identifier {
id = val
return
}
@@ -119,11 +180,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 == Identifiable.type else {
throw JSONAPIEncodingError.typeMismatch(expected: Identifiable.type, found: type)
}
id = try identifier.decode(Relatable.WrappedIdentifier.self, forKey: .id)
id = Identifiable.Identifier(rawValue: try identifier.decode(Identifiable.Identifier.RawType.self, forKey: .id))
}
public func encode(to encoder: Encoder) throws {
@@ -133,17 +194,46 @@ extension ToOneRelationship {
try container.encodeNil(forKey: .data)
}
if MetaType.self != NoMetadata.self {
try container.encode(meta, forKey: .meta)
}
if LinksType.self != NoLinks.self {
try container.encode(links, forKey: .links)
}
// If id is nil, instead of {id: , type: } we will just
// encode `null`
let anyNil: Any? = nil
let nilId = anyNil as? Identifiable.Identifier
guard id != nilId else {
try container.encodeNil(forKey: .data)
return
}
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(id.rawValue, forKey: .id)
try identifier.encode(Identifiable.type, forKey: .entityType)
}
}
extension ToManyRelationship {
extension ToManyRelationship: Codable {
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: ResourceLinkageCodingKeys.self)
if let noMeta = NoMetadata() as? MetaType {
meta = noMeta
} else {
meta = try container.decode(MetaType.self, forKey: .meta)
}
if let noLinks = NoLinks() as? LinksType {
links = noLinks
} else {
links = try container.decode(LinksType.self, forKey: .links)
}
var identifiers = try container.nestedUnkeyedContainer(forKey: .data)
var newIds = [Relatable.Identifier]()
@@ -152,24 +242,33 @@ 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))
newIds.append(Relatable.Identifier(rawValue: try identifier.decode(Relatable.Identifier.RawType.self, forKey: .id)))
}
ids = newIds
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: ResourceLinkageCodingKeys.self)
if MetaType.self != NoMetadata.self {
try container.encode(meta, forKey: .meta)
}
if LinksType.self != NoLinks.self {
try container.encode(links, forKey: .links)
}
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(Relatable.Description.type, forKey: .entityType)
try identifier.encode(id.rawValue, forKey: .id)
try identifier.encode(Relatable.type, forKey: .entityType)
}
}
}
+45
View File
@@ -0,0 +1,45 @@
//
// Result.swift
// JSONAPI
//
// Created by Mathew Polzin on 12/5/18.
//
enum Result<T, E: Swift.Error> {
case success(T)
case failure(E)
var value: T? {
guard case .success(let val) = self else {
return nil
}
return val
}
var error: E? {
guard case .failure(let err) = self else {
return nil
}
return err
}
func map<U>(_ transform: (T) -> U) -> Result<U, E> {
switch self {
case .failure(let err):
return .failure(err)
case .success(let val):
return .success(transform(val))
}
}
}
extension Result: CustomStringConvertible where T: CustomStringConvertible, E: CustomStringConvertible {
var description: String {
switch self {
case .success(let val):
return String(describing: val)
case .failure(let err):
return String(describing: err)
}
}
}
+6 -1
View File
@@ -45,6 +45,10 @@ extension TransformedAttribute: AttributeTypeWithOptionalArray where RawValue: O
private protocol OptionalRelationshipType {}
extension Optional: OptionalRelationshipType where Wrapped: RelationshipType {}
private protocol _RelationshipType {}
extension ToOneRelationship: _RelationshipType {}
extension ToManyRelationship: _RelationshipType {}
public extension Entity {
public static func check(_ entity: Entity) throws {
var problems = [EntityCheckError]()
@@ -72,7 +76,8 @@ public extension Entity {
}
for relationship in relationshipsMirror.children {
if relationship.value as? RelationshipType == nil {
if relationship.value as? _RelationshipType == nil,
relationship.value as? OptionalRelationshipType == nil {
problems.append(.nonRelationship(named: relationship.label ?? "unnamed"))
}
}
@@ -7,38 +7,38 @@
import JSONAPI
extension ToOneRelationship: ExpressibleByNilLiteral where Relatable.WrappedIdentifier: ExpressibleByNilLiteral {
extension ToOneRelationship: ExpressibleByNilLiteral where Identifiable.Identifier: ExpressibleByNilLiteral, MetaType == NoMetadata, LinksType == NoLinks {
public init(nilLiteral: ()) {
self.init(id: Relatable.WrappedIdentifier(nilLiteral: ()))
self.init(id: Identifiable.Identifier(nilLiteral: ()))
}
}
extension ToOneRelationship: ExpressibleByUnicodeScalarLiteral where Relatable.WrappedIdentifier: ExpressibleByUnicodeScalarLiteral {
public typealias UnicodeScalarLiteralType = Relatable.WrappedIdentifier.UnicodeScalarLiteralType
extension ToOneRelationship: ExpressibleByUnicodeScalarLiteral where Identifiable.Identifier: ExpressibleByUnicodeScalarLiteral, MetaType == NoMetadata, LinksType == NoLinks {
public typealias UnicodeScalarLiteralType = Identifiable.Identifier.UnicodeScalarLiteralType
public init(unicodeScalarLiteral value: UnicodeScalarLiteralType) {
self.init(id: Relatable.WrappedIdentifier(unicodeScalarLiteral: value))
self.init(id: Identifiable.Identifier(unicodeScalarLiteral: value))
}
}
extension ToOneRelationship: ExpressibleByExtendedGraphemeClusterLiteral where Relatable.WrappedIdentifier: ExpressibleByExtendedGraphemeClusterLiteral {
public typealias ExtendedGraphemeClusterLiteralType = Relatable.WrappedIdentifier.ExtendedGraphemeClusterLiteralType
extension ToOneRelationship: ExpressibleByExtendedGraphemeClusterLiteral where Identifiable.Identifier: ExpressibleByExtendedGraphemeClusterLiteral, MetaType == NoMetadata, LinksType == NoLinks {
public typealias ExtendedGraphemeClusterLiteralType = Identifiable.Identifier.ExtendedGraphemeClusterLiteralType
public init(extendedGraphemeClusterLiteral value: ExtendedGraphemeClusterLiteralType) {
self.init(id: Relatable.WrappedIdentifier(extendedGraphemeClusterLiteral: value))
self.init(id: Identifiable.Identifier(extendedGraphemeClusterLiteral: value))
}
}
extension ToOneRelationship: ExpressibleByStringLiteral where Relatable.WrappedIdentifier: ExpressibleByStringLiteral {
public typealias StringLiteralType = Relatable.WrappedIdentifier.StringLiteralType
extension ToOneRelationship: ExpressibleByStringLiteral where Identifiable.Identifier: ExpressibleByStringLiteral, MetaType == NoMetadata, LinksType == NoLinks {
public typealias StringLiteralType = Identifiable.Identifier.StringLiteralType
public init(stringLiteral value: StringLiteralType) {
self.init(id: Relatable.WrappedIdentifier(stringLiteral: value))
self.init(id: Identifiable.Identifier(stringLiteral: value))
}
}
extension ToManyRelationship: ExpressibleByArrayLiteral {
extension ToManyRelationship: ExpressibleByArrayLiteral where MetaType == NoMetadata, LinksType == NoLinks {
public typealias ArrayLiteralElement = Relatable.Identifier
public init(arrayLiteral elements: ArrayLiteralElement...) {
@@ -10,6 +10,10 @@ import JSONAPI
class APIDescriptionTests: XCTestCase {
func test_NoDescriptionString() {
XCTAssertEqual(String(describing: NoAPIDescription()), "No JSON:API Object")
}
func test_empty() {
let description = decoded(type: APIDescription<NoMetadata>.self, data: api_description_empty)
@@ -29,6 +29,29 @@ class AttributeTests: XCTestCase {
func test_TransformedAttributeReversNoThrow() {
XCTAssertNoThrow(try TransformedAttribute<String, TestTransformer>(transformedValue: 10))
}
func test_NullableIsNullIfNil() {
struct Wrapper: Codable {
let dummy: Attribute<String?>
}
let data = encoded(value: Wrapper(dummy: .init(value: nil)))
let string = String(data: data, encoding: .utf8)!
XCTAssertEqual(string, "{\"dummy\":null}")
}
func test_NullableIsEqualToNonNullableIfNotNil() {
struct Wrapper1: Codable {
let dummy: Attribute<String?>
}
struct Wrapper2: Codable {
let dummy: Attribute<String>
}
let data1 = encoded(value: Wrapper1(dummy: .init(value: "hello")))
let data2 = encoded(value: Wrapper2(dummy: .init(value: "hello")))
XCTAssertEqual(data1, data2)
}
}
// MARK: Test types
@@ -27,6 +27,13 @@ class ComputedPropertiesTests: XCTestCase {
let entity = decoded(type: TestType.self, data: computed_property_attribute)
XCTAssertEqual(entity[\.computed], "Sarah2")
XCTAssertEqual(entity[\.secretsOut], "shhhh")
}
func test_ComputedNonAttributeAccess() {
let entity = decoded(type: TestType.self, data: computed_property_attribute)
XCTAssertEqual(entity[\.computed2], "Sarah2")
}
func test_ComputedRelationshipAccess() {
@@ -43,15 +50,25 @@ extension ComputedPropertiesTests {
public struct Attributes: JSONAPI.Attributes {
public let name: Attribute<String>
private let secret: Attribute<String>
public var computed: Attribute<String> {
return name.map { $0 + "2" }
}
public var computed2: String {
return computed.value
}
public var secretsOut: String {
return secret.value
}
}
public struct Relationships: JSONAPI.Relationships {
public let other: ToOneRelationship<TestType>
public let other: ToOneRelationship<TestType, NoMetadata, NoLinks>
public var computed: ToOneRelationship<TestType> {
public var computed: ToOneRelationship<TestType, NoMetadata, NoLinks> {
return other
}
}
@@ -10,7 +10,8 @@ let computed_property_attribute = """
"id": "1234",
"type": "test",
"attributes": {
"name": "Sarah"
"name": "Sarah",
"secret": "shhhh"
},
"relationships": {
"other": {
@@ -978,7 +978,7 @@ extension DocumentTests {
typealias Attributes = NoAttributes
struct Relationships: JSONAPI.Relationships {
let author: ToOneRelationship<Author>
let author: ToOneRelationship<Author, NoMetadata, NoLinks>
}
}
+97 -17
View File
@@ -24,15 +24,23 @@ class EntityTests: XCTestCase {
XCTAssertEqual(entity2 ~> \.other, entity1.id)
}
func test_optional_relationship_operator_access() {
}
func test_toMany_relationship_operator_access() {
let entity1 = TestEntity1()
let entity2 = TestEntity1()
let entity4 = TestEntity1()
let entity3 = TestEntity3(relationships: .init(others: .init(relationships: [entity1.pointer, entity2.pointer, entity4.pointer])))
let entity3 = TestEntity3(relationships: .init(others: .init(pointers: [entity1.pointer, entity2.pointer, entity4.pointer])))
XCTAssertEqual(entity3 ~> \.others, [entity1.id, entity2.id, entity4.id])
}
func test_optionalToMany_relationship_opeartor_access() {
}
func test_relationshipIds() {
let entity1 = TestEntity1()
@@ -41,16 +49,34 @@ class EntityTests: XCTestCase {
XCTAssertEqual(entity2.relationships.other.id, entity1.id)
}
func test_pointerWithMetaAndLinks() {
let entity = TestEntity4WithMetaAndLinks(attributes: .init(word: "hello", number: 10, array: []), relationships: .init(other: .init(id: "2")), meta: .init(x: "world", y: nil), links: .init(link1: .init(url: "ok")))
let pointer = entity.pointer(withMeta: TestEntityMeta(x: "world", y: nil), links: TestEntityLinks(link1: .init(url: "ok")))
XCTAssertEqual(pointer.id, entity.id)
XCTAssertEqual(pointer.meta.x, "world")
XCTAssertEqual(pointer.links.link1.url, "ok")
}
func test_initialization() {
let entity1 = TestEntity1(id: .init(rawValue: "wow"))
let entity2 = TestEntity2(id: .init(rawValue: "cool"), relationships: .init(other: .init(entity: entity1)))
let _ = TestEntity2(id: .init(rawValue: "cool"), attributes: .none, relationships: .init(other: .init(entity: entity1)))
let _ = TestEntity2(id: .init(rawValue: "cool"), relationships: .init(other: .init(entity: entity1)), meta: .none)
let _ = TestEntity3(id: .init(rawValue: "3"), relationships: .init(others: .init(ids: [.init(rawValue: "10"), .init(rawValue: "20"), entity1.id])))
let _ = TestEntity3(id: .init(rawValue: "3"), relationships: .init(others: .none))
let _ = TestEntity4(id: .init(rawValue: "4"), attributes: .init(word: .init(value: "hello"), number: .init(value: 10), array: .init(value: [10.2, 10.3])), relationships: .init(other: entity2.pointer))
let _ = TestEntity5(id: .init(rawValue: "5"), attributes: .init(floater: .init(value: 10.2)))
let _ = TestEntity6(id: .init(rawValue: "6"), attributes: .init(here: .init(value: "here"), maybeHere: nil, maybeNull: .init(value: nil)))
let _ = TestEntity7(id: .init(rawValue: "7"), attributes: .init(here: .init(value: "hello"), maybeHereMaybeNull: .init(value: "world")))
XCTAssertNoThrow(try TestEntity8(id: .init(rawValue: "8"), attributes: .init(string: .init(value: "hello"), int: .init(value: 10), stringFromInt: .init(rawValue: 20), plus: .init(rawValue: 30), doubleFromInt: .init(rawValue: 32), omitted: nil, nullToString: .init(rawValue: nil))))
let _ = TestEntity9(id: .init(rawValue: "9"), relationships: .init(one: entity1.pointer, nullableOne: nil))
let _ = TestEntity9(id: .init(rawValue: "9"), relationships: .init(one: entity1.pointer, nullableOne: nil, optionalOne: nil, optionalNullableOne: nil, optionalMany: nil))
let _ = TestEntity9(id: .init(rawValue: "9"), relationships: .init(one: entity1.pointer, nullableOne: .init(entity: nil), optionalOne: nil, optionalNullableOne: nil, optionalMany: nil))
let _ = TestEntity9(id: .init(rawValue: "9"), relationships: .init(one: entity1.pointer, nullableOne: .init(entity: entity1, meta: .none, links: .none), optionalOne: nil, optionalNullableOne: nil, optionalMany: nil))
let _ = TestEntity9(id: .init(rawValue: "9"), relationships: .init(one: entity1.pointer, nullableOne: nil, optionalOne: entity1.pointer, optionalNullableOne: nil, optionalMany: nil))
let _ = TestEntity9(id: .init(rawValue: "9"), relationships: .init(one: entity1.pointer, nullableOne: nil, optionalOne: nil, optionalNullableOne: .init(entity: entity1, meta: .none, links: .none), optionalMany: nil))
let _ = TestEntity9(id: .init(rawValue: "9"), relationships: .init(one: entity1.pointer, nullableOne: nil, optionalOne: nil, optionalNullableOne: .init(entity: entity1, meta: .none, links: .none), optionalMany: .init(entities: [], meta: .none, links: .none)))
let e10id1 = TestEntity10.Identifier(rawValue: "hello")
let e10id2 = TestEntity10.Id(rawValue: "world")
let e10id3: TestEntity10.Id = "!"
@@ -181,6 +207,8 @@ extension EntityTests {
XCTAssertNil(entity[\.maybeHere])
XCTAssertNil(entity[\.maybeNull])
XCTAssertNoThrow(try TestEntity6.check(entity))
print(encodable: entity)
}
func test_entityOneNullAndOneOmittedAttribute_encode() {
@@ -200,6 +228,8 @@ extension EntityTests {
XCTAssertEqual(entity[\.here], "Hello")
XCTAssertNil(entity[\.maybeHereMaybeNull])
XCTAssertNoThrow(try TestEntity7.check(entity))
print(encodable: entity)
}
func test_NullOptionalNullableAttribute_encode() {
@@ -260,18 +290,50 @@ extension EntityTests {
// MARK: Relationship omission and nullification
extension EntityTests {
func test_nullableRelationshipNotNull() {
func test_nullableRelationshipNotNullOrOmitted() {
let entity = decoded(type: TestEntity9.self,
data: entity_omitted_relationship)
data: entity_optional_not_omitted_relationship)
XCTAssertEqual((entity ~> \.nullableOne)?.rawValue, "3323")
XCTAssertEqual((entity ~> \.one).rawValue, "4459")
XCTAssertNil(entity ~> \.optionalOne)
XCTAssertEqual((entity ~> \.optionalNullableOne)?.rawValue, "1229")
XCTAssertNoThrow(try TestEntity9.check(entity))
}
func test_nullableRelationshipNotNullOrOmitted_encode() {
test_DecodeEncodeEquality(type: TestEntity9.self,
data: entity_optional_not_omitted_relationship)
}
func test_nullableRelationshipNotNull() {
let entity = decoded(type: TestEntity9.self,
data: entity_omitted_relationship)
XCTAssertEqual((entity ~> \.nullableOne)?.rawValue, "3323")
XCTAssertEqual((entity ~> \.one).rawValue, "4459")
XCTAssertNil(entity ~> \.optionalNullableOne)
XCTAssertNoThrow(try TestEntity9.check(entity))
}
func test_nullableRelationshipNotNull_encode() {
test_DecodeEncodeEquality(type: TestEntity9.self,
data: entity_omitted_relationship)
data: entity_omitted_relationship)
}
func test_optionalNullableRelationshipNulled() {
let entity = decoded(type: TestEntity9.self,
data: entity_optional_nullable_nulled_relationship)
XCTAssertEqual((entity ~> \.nullableOne)?.rawValue, "3323")
XCTAssertEqual((entity ~> \.one).rawValue, "4459")
XCTAssertNil(entity ~> \.optionalNullableOne)
XCTAssertNoThrow(try TestEntity9.check(entity))
}
func test_optionalNullableRelationshipNulled_encode() {
test_DecodeEncodeEquality(type: TestEntity9.self,
data: entity_optional_nullable_nulled_relationship)
}
func test_nullableRelationshipIsNull() {
@@ -280,6 +342,7 @@ extension EntityTests {
XCTAssertNil(entity ~> \.nullableOne)
XCTAssertEqual((entity ~> \.one).rawValue, "4452")
XCTAssertNil(entity ~> \.optionalNullableOne)
XCTAssertNoThrow(try TestEntity9.check(entity))
}
@@ -287,6 +350,22 @@ extension EntityTests {
test_DecodeEncodeEquality(type: TestEntity9.self,
data: entity_nulled_relationship)
}
func test_optionalToManyIsNotOmitted() {
let entity = decoded(type: TestEntity9.self,
data: entity_optional_to_many_relationship_not_omitted)
XCTAssertEqual((entity ~> \.nullableOne)?.rawValue, "3323")
XCTAssertEqual((entity ~> \.one).rawValue, "4459")
XCTAssertEqual((entity ~> \.optionalMany)?[0].rawValue, "332223")
XCTAssertNil(entity ~> \.optionalNullableOne)
XCTAssertNoThrow(try TestEntity9.check(entity))
}
func test_optionalToManyIsNotOmitted_encode() {
test_DecodeEncodeEquality(type: TestEntity9.self,
data: entity_optional_to_many_relationship_not_omitted)
}
}
// MARK: Relationships of same type as root entity
@@ -459,7 +538,7 @@ extension EntityTests {
typealias Attributes = NoAttributes
struct Relationships: JSONAPI.Relationships {
let other: ToOneRelationship<TestEntity1>
let other: ToOneRelationship<TestEntity1, NoMetadata, NoLinks>
}
}
@@ -471,7 +550,7 @@ extension EntityTests {
typealias Attributes = NoAttributes
struct Relationships: JSONAPI.Relationships {
let others: ToManyRelationship<TestEntity1>
let others: ToManyRelationship<TestEntity1, NoMetadata, NoLinks>
}
}
@@ -481,7 +560,7 @@ extension EntityTests {
static var type: String { return "fourth_test_entities"}
struct Relationships: JSONAPI.Relationships {
let other: ToOneRelationship<TestEntity2>
let other: ToOneRelationship<TestEntity2, NoMetadata, NoLinks>
}
struct Attributes: JSONAPI.Attributes {
@@ -562,17 +641,18 @@ extension EntityTests {
typealias Attributes = NoAttributes
public struct Relationships: JSONAPI.Relationships {
let one: ToOneRelationship<TestEntity1>
let one: ToOneRelationship<TestEntity1, NoMetadata, NoLinks>
let nullableOne: ToOneRelationship<TestEntity1?>
let nullableOne: ToOneRelationship<TestEntity1?, NoMetadata, NoLinks>
let optionalOne: ToOneRelationship<TestEntity1, NoMetadata, NoLinks>?
let optionalNullableOne: ToOneRelationship<TestEntity1?, NoMetadata, NoLinks>?
let optionalMany: ToManyRelationship<TestEntity1, NoMetadata, NoLinks>?
// a nullable many is not allowed. it should
// just be an empty array.
// omitted relationships are not allowed either,
// so ToOneRelationship<TestEntity1>? (with the
// question on the relationship, not the entity)
// is not a thing.
}
}
@@ -584,8 +664,8 @@ extension EntityTests {
typealias Attributes = NoAttributes
public struct Relationships: JSONAPI.Relationships {
let selfRef: ToOneRelationship<TestEntity10>
let selfRefs: ToManyRelationship<TestEntity10>
let selfRef: ToOneRelationship<TestEntity10, NoMetadata, NoLinks>
let selfRefs: ToManyRelationship<TestEntity10, NoMetadata, NoLinks>
}
}

Some files were not shown because too many files have changed in this diff Show More