mirror of
https://github.com/encounter/JSONAPI.git
synced 2026-07-10 12:18:40 -07:00
Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d5a24c4adb | |||
| 669d5d1342 | |||
| 923ab7d9f4 | |||
| 72769b6107 | |||
| 109e15d741 | |||
| 72180f64ef | |||
| fc962f9a0d | |||
| 52eb123166 | |||
| 4ef147ec45 | |||
| 61074ecc69 | |||
| 4dbcff6023 | |||
| d6e82fab55 | |||
| 5fa9848f02 | |||
| f38399a1d6 | |||
| 28f664326d | |||
| 3b7ef4aeb9 | |||
| bb1ed30e89 |
@@ -19,7 +19,14 @@ let singleDogData = try! JSONEncoder().encode(singleDogDocument)
|
||||
|
||||
// MARK: - Parse a request or response body with one Dog in it
|
||||
let dogResponse = try! JSONDecoder().decode(SingleDogDocument.self, from: singleDogData)
|
||||
let dogFromData = dogResponse.body.primaryData?.value
|
||||
let dogFromData = dogResponse.body.primaryResource?.value
|
||||
let dogOwner: Person.Identifier? = dogFromData.flatMap { $0 ~> \.owner }
|
||||
|
||||
// MARKL - Parse a request or response body with one Dog in it using an alternative model
|
||||
typealias AltSingleDogDocument = JSONAPI.Document<SingleResourceBody<AlternativeDog>, NoMetadata, NoLinks, NoIncludes, NoAPIDescription, UnknownJSONAPIError>
|
||||
let altDogResponse = try! JSONDecoder().decode(AltSingleDogDocument.self, from: singleDogData)
|
||||
let altDogFromData = altDogResponse.body.primaryResource?.value
|
||||
let altDogHuman: Person.Identifier? = altDogFromData.flatMap { $0 ~> \.human }
|
||||
|
||||
// MARK: - Create a request or response with multiple people and dogs and houses included
|
||||
let personIds = [Person.Identifier(), Person.Identifier()]
|
||||
@@ -36,7 +43,7 @@ let batchPeopleData = try! JSONEncoder().encode(batchPeopleDocument)
|
||||
// MARK: - Parse a request or response body with multiple people in it and dogs and houses included
|
||||
|
||||
let peopleResponse = try! JSONDecoder().decode(BatchPeopleDocument.self, from: batchPeopleData)
|
||||
let peopleFromData = peopleResponse.body.primaryData?.values
|
||||
let peopleFromData = peopleResponse.body.primaryResource?.values
|
||||
let dogsFromData = peopleResponse.body.includes?[Dog.self]
|
||||
let housesFromData = peopleResponse.body.includes?[House.self]
|
||||
|
||||
|
||||
@@ -64,7 +64,7 @@ public typealias Person = ExampleEntity<PersonDescription>
|
||||
|
||||
public extension Entity where Description == PersonDescription, MetaType == NoMetadata, LinksType == NoLinks, EntityRawIdType == String {
|
||||
public init(id: Person.Id? = nil,name: [String], favoriteColor: String, friends: [Person], dogs: [Dog], home: House) throws {
|
||||
self = try Person(id: id ?? Person.Id(), attributes: .init(name: .init(rawValue: name), favoriteColor: .init(rawValue: favoriteColor)), relationships: .init(friends: .init(entities: friends), dogs: .init(entities: dogs), home: .init(entity: home)), meta: .none, links: .none)
|
||||
self = Person(id: id ?? Person.Id(), attributes: .init(name: .init(value: name), favoriteColor: .init(value: favoriteColor)), relationships: .init(friends: .init(entities: friends), dogs: .init(entities: dogs), home: .init(entity: home)), meta: .none, links: .none)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,13 +91,41 @@ public enum DogDescription: EntityDescription {
|
||||
|
||||
public typealias Dog = ExampleEntity<DogDescription>
|
||||
|
||||
public enum AlternativeDogDescription: EntityDescription {
|
||||
|
||||
public static var type: String { return "dogs" }
|
||||
|
||||
public struct Attributes: JSONAPI.Attributes {
|
||||
public let name: Attribute<String>
|
||||
|
||||
public init(name: Attribute<String>) {
|
||||
self.name = name
|
||||
}
|
||||
}
|
||||
|
||||
public struct Relationships: JSONAPI.Relationships {
|
||||
public let human: ToOne<Person?>
|
||||
|
||||
public init(human: ToOne<Person?>) {
|
||||
self.human = human
|
||||
}
|
||||
|
||||
// define custom key mapping:
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case human = "owner"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public typealias AlternativeDog = ExampleEntity<AlternativeDogDescription>
|
||||
|
||||
public extension Entity where Description == DogDescription, MetaType == NoMetadata, LinksType == NoLinks, EntityRawIdType == String {
|
||||
public init(name: String, owner: Person?) throws {
|
||||
self = try Dog(attributes: .init(name: .init(rawValue: name)), relationships: DogDescription.Relationships(owner: .init(entity: owner)), meta: .none, links: .none)
|
||||
self = Dog(attributes: .init(name: .init(value: name)), relationships: DogDescription.Relationships(owner: .init(entity: owner)), meta: .none, links: .none)
|
||||
}
|
||||
|
||||
public init(name: String, owner: Person.Id) throws {
|
||||
self = try Dog(attributes: .init(name: .init(rawValue: name)), relationships: .init(owner: .init(id: owner)), meta: .none, links: .none)
|
||||
self = Dog(attributes: .init(name: .init(value: name)), relationships: .init(owner: .init(id: owner)), meta: .none, links: .none)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -67,7 +67,7 @@ Note that Playground support for importing non-system Frameworks is still a bit
|
||||
### JSONAPITestLib
|
||||
#### Entity Validator
|
||||
- [x] Disallow optional array in `Attribute` (should be empty array, not `null`).
|
||||
- [x] Only allow `TransformedAttribute` and its derivatives within `Attributes` struct.
|
||||
- [x] Only allow `TransformedAttribute` and its derivatives as stored properties within `Attributes` struct. Computed properties can still be any type because they do not get encoded or decoded.
|
||||
- [x] Only allow `ToManyRelationship` and `ToOneRelationship` within `Relationships` struct.
|
||||
|
||||
### Potential Improvements
|
||||
@@ -227,6 +227,11 @@ typealias Attributes = NoAttributes
|
||||
let favoriteColor: String = person[\.favoriteColor]
|
||||
```
|
||||
|
||||
NOTE: Because of support for computed properties that are not wrapped in `Attribute`, `TransformedAttribute`, or `ValidatedAttribute`, the compiler cannot always infer the type of thing you want back when using subscript attribute access. The following code is ambiguous about whether it should return a `String` or an `Attribute<String>`:
|
||||
```
|
||||
let favoriteColor = person[\.favoriteColor]
|
||||
```
|
||||
|
||||
#### `Transformer`
|
||||
|
||||
Sometimes you need to use a type that does not encode or decode itself in the way you need to represent it as a serialized JSON object. For example, the Swift `Foundation` type `Date` can encode/decode itself to `Double` out of the box, but you might want to represent dates as ISO 8601 compliant `String`s instead. The Foundation library `JSONDecoder` has a setting to make this adjustment, but for the sake of an example, you could create a `Transformer`.
|
||||
@@ -265,7 +270,7 @@ You can also creator `Validators` and `ValidatedAttribute`s. A `Validator` is ju
|
||||
|
||||
#### Computed `Attribute`
|
||||
|
||||
You can add computed properties to your `EntityDescription.Attributes` struct if you would like to expose attributes that are not explicitly represented by the JSON. These computed properties should still have the type `Attribute` because that way you can take advantage of the quick access provided by `Entity`'s subscript operator. Here's an example of how you might take the `Person[\.name]` attribute from the example above and create a `fullName` computed property.
|
||||
You can add computed properties to your `EntityDescription.Attributes` struct if you would like to expose attributes that are not explicitly represented by the JSON. These computed properties do not have to be wrapped in `Attribute`, `ValidatedAttribute`, or `TransformedAttribute`. This allows computed attributes to be of types that are not `Codable`. Here's an example of how you might take the `Person[\.name]` attribute from the example above and create a `fullName` computed property.
|
||||
|
||||
```
|
||||
public var fullName: Attribute<String> {
|
||||
@@ -273,6 +278,23 @@ public var fullName: Attribute<String> {
|
||||
}
|
||||
```
|
||||
|
||||
### Copying `Entities`
|
||||
`Entity` is a value type, so copying is its default behavior. There are two common mutations you might want to make when copying an `Entity`:
|
||||
1. Assigning a new `Identifier` to the copy of an identified `Entity`.
|
||||
2. Assigning a new `Identifier` to the copy of an unidentified `Entity`.
|
||||
|
||||
The above can be accomplished with code like the following:
|
||||
|
||||
```
|
||||
// use case 1
|
||||
let person1 = person.withNewIdentifier()
|
||||
|
||||
// use case 2
|
||||
let newlyIdentifiedPerson1 = unidentifiedPerson.identified(byType: String.self)
|
||||
|
||||
let newlyIdentifiedPerson2 = unidentifiedPerson.identified(by: "2232")
|
||||
```
|
||||
|
||||
### `JSONAPI.Document`
|
||||
|
||||
The entirety of a JSON API request or response is encoded or decoded from- or to a `Document`. As an example, a JSON API response containing one `Person` and no included entities could be decoded as follows:
|
||||
@@ -377,5 +399,75 @@ extension String: CreatableRawIdType {
|
||||
}
|
||||
```
|
||||
|
||||
### Custom Attribute or Relationship Key Mapping
|
||||
There is not anything special going on at the `JSONAPI.Attributes` and `JSONAPI.Relationships` levels, so you can easily provide custom key mappings by taking advantage of `Codable`'s `CodingKeys` pattern. Here are two models that will encode/decode equivalently but offer different naming in your codebase:
|
||||
```
|
||||
public enum EntityDescription1: JSONAPI.EntityDescription {
|
||||
public static var type: String { return "entity" }
|
||||
|
||||
public struct Attributes: JSONAPI.Attributes {
|
||||
public let coolProperty: Attribute<String>
|
||||
}
|
||||
|
||||
public typealias Relationships = NoRelationships
|
||||
}
|
||||
|
||||
public enum EntityDescription2: JSONAPI.EntityDescription {
|
||||
public static var type: String { return "entity" }
|
||||
|
||||
public struct Attributes: JSONAPI.Attributes {
|
||||
public let wholeOtherThing: Attribute<String>
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case wholeOtherThing = "coolProperty"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Custom Attribute Encode/Decode
|
||||
You can safely provide your own encoding or decoding functions for your Attributes struct if you need to as long as you are careful that your encode operation correctly reverses your decode operation. Although this is generally not necessary, `AttributeType` provides a convenience method to make your decoding a bit less boilerplate ridden. This is what it looks like:
|
||||
```
|
||||
public enum EntityDescription1: JSONAPI.EntityDescription {
|
||||
public static var type: String { return "entity" }
|
||||
|
||||
public struct Attributes: JSONAPI.Attributes {
|
||||
public let property1: Attribute<String>
|
||||
public let property2: Attribute<Int>
|
||||
public let property3: Attribute<String>
|
||||
|
||||
public let weirdThing: Attribute<String>
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case property1
|
||||
case property2
|
||||
case property3
|
||||
}
|
||||
}
|
||||
|
||||
public typealias Relationships = NoRelationships
|
||||
}
|
||||
|
||||
extension EntityDescription1.Attributes {
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
property1 = try .defaultDecoding(from: container, forKey: .property1)
|
||||
property2 = try .defaultDecoding(from: container, forKey: .property2)
|
||||
property3 = try .defaultDecoding(from: container, forKey: .property3)
|
||||
|
||||
weirdThing = .init(value: "hello world")
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
try container.encode(property1, forKey: .property1)
|
||||
try container.encode(property2, forKey: .property2)
|
||||
try container.encode(property3, forKey: .property3)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
# JSONAPITestLib
|
||||
The `JSONAPI` framework is packaged with a test library to help you test your `JSONAPI` integration. The test library is called `JSONAPITestLib`. It provides literal expressibility for `Attribute`, `ToOneRelationship`, and `Id` in many situations so that you can easily write test `Entity` values into your unit tests. It also provides a `check()` function for each `Entity` type that can be used to catch problems with your `JSONAPI` structures that are not caught by Swift's type system. You can see the `JSONAPITestLib` in action in the Playground included with the `JSONAPI` repository.
|
||||
|
||||
@@ -16,6 +16,8 @@ public struct APIDescription<Meta: JSONAPI.Meta>: APIDescriptionType {
|
||||
public let meta: Meta
|
||||
}
|
||||
|
||||
/// Can be used as `APIDescriptionType` for Documents that do not
|
||||
/// have any API Description (a.k.a. "JSON:API Object").
|
||||
public struct NoAPIDescription: APIDescriptionType, CustomStringConvertible {
|
||||
public typealias Meta = NoMetadata
|
||||
|
||||
|
||||
@@ -66,8 +66,13 @@ public struct Document<PrimaryResourceBody: JSONAPI.ResourceBody, MetaType: JSON
|
||||
guard case let .errors(errors, meta: _, links: _) = self else { return nil }
|
||||
return errors
|
||||
}
|
||||
|
||||
public var data: Data? {
|
||||
guard case let .data(data) = self else { return nil }
|
||||
return data
|
||||
}
|
||||
|
||||
public var primaryData: PrimaryResourceBody? {
|
||||
public var primaryResource: PrimaryResourceBody? {
|
||||
guard case let .data(data) = self else { return nil }
|
||||
return data.primary
|
||||
}
|
||||
@@ -184,6 +189,26 @@ extension Document where IncludeType == NoIncludes, MetaType == NoMetadata, Link
|
||||
}
|
||||
*/
|
||||
|
||||
extension Document.Body.Data where PrimaryResourceBody: AppendableResourceBody {
|
||||
public func merging(_ other: Document.Body.Data,
|
||||
combiningMetaWith metaMerge: (MetaType, MetaType) -> MetaType,
|
||||
combiningLinksWith linksMerge: (LinksType, LinksType) -> LinksType) -> Document.Body.Data {
|
||||
return Document.Body.Data(primary: primary.appending(other.primary),
|
||||
includes: includes.appending(other.includes),
|
||||
meta: metaMerge(meta, other.meta),
|
||||
links: linksMerge(links, other.links))
|
||||
}
|
||||
}
|
||||
|
||||
extension Document.Body.Data where PrimaryResourceBody: AppendableResourceBody, MetaType == NoMetadata, LinksType == NoLinks {
|
||||
public func merging(_ other: Document.Body.Data) -> Document.Body.Data {
|
||||
return merging(other,
|
||||
combiningMetaWith: { _, _ in .none },
|
||||
combiningLinksWith: { _, _ in .none })
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Codable
|
||||
extension Document {
|
||||
private enum RootCodingKeys: String, CodingKey {
|
||||
case data
|
||||
|
||||
@@ -50,6 +50,16 @@ public struct Includes<I: Include>: Codable, Equatable {
|
||||
}
|
||||
}
|
||||
|
||||
extension Includes {
|
||||
public func appending(_ other: Includes<I>) -> Includes {
|
||||
return Includes(values: values + other.values)
|
||||
}
|
||||
}
|
||||
|
||||
public func +<I: Include>(_ left: Includes<I>, _ right: Includes<I>) -> Includes<I> {
|
||||
return left.appending(right)
|
||||
}
|
||||
|
||||
extension Includes: CustomStringConvertible {
|
||||
public var description: String {
|
||||
return "Includes(\(String(describing: values))"
|
||||
@@ -123,3 +133,17 @@ extension Includes where I: _Poly7 {
|
||||
}
|
||||
|
||||
// MARK: - 8 includes
|
||||
public typealias Include8 = Poly8
|
||||
extension Includes where I: _Poly8 {
|
||||
public subscript(_ lookup: I.H.Type) -> [I.H] {
|
||||
return values.compactMap { $0.h }
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 9 includes
|
||||
public typealias Include9 = Poly9
|
||||
extension Includes where I: _Poly9 {
|
||||
public subscript(_ lookup: I.I.Type) -> [I.I] {
|
||||
return values.compactMap { $0.i }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,14 @@ extension Optional: MaybePrimaryResource where Wrapped: PrimaryResource {}
|
||||
public protocol ResourceBody: Codable, Equatable {
|
||||
}
|
||||
|
||||
public protocol AppendableResourceBody: ResourceBody {
|
||||
func appending(_ other: Self) -> Self
|
||||
}
|
||||
|
||||
public func +<R: AppendableResourceBody>(_ left: R, right: R) -> R {
|
||||
return left.appending(right)
|
||||
}
|
||||
|
||||
public struct SingleResourceBody<Entity: JSONAPI.MaybePrimaryResource>: ResourceBody {
|
||||
public let value: Entity
|
||||
|
||||
@@ -27,12 +35,16 @@ public struct SingleResourceBody<Entity: JSONAPI.MaybePrimaryResource>: Resource
|
||||
}
|
||||
}
|
||||
|
||||
public struct ManyResourceBody<Entity: JSONAPI.PrimaryResource>: ResourceBody {
|
||||
public struct ManyResourceBody<Entity: JSONAPI.PrimaryResource>: AppendableResourceBody {
|
||||
public let values: [Entity]
|
||||
|
||||
public init(entities: [Entity]) {
|
||||
values = entities
|
||||
}
|
||||
|
||||
public func appending(_ other: ManyResourceBody) -> ManyResourceBody {
|
||||
return ManyResourceBody(entities: values + other.values)
|
||||
}
|
||||
}
|
||||
|
||||
/// Use NoResourceBody to indicate you expect a JSON API document to not
|
||||
|
||||
@@ -19,3 +19,18 @@ public extension TransformedAttribute {
|
||||
return Attribute<T>(value: try transform(value))
|
||||
}
|
||||
}
|
||||
|
||||
public extension Attribute {
|
||||
/// Map an Attribute to a new wrapped type.
|
||||
/// Note that the resulting Attribute will have no transformer, even if the
|
||||
/// source Attribute has a transformer.
|
||||
/// You are mapping the output of the source transform into
|
||||
/// the RawValue of a new transformerless Attribute.
|
||||
///
|
||||
/// Generally, this is the most useful operation. The transformer gives you
|
||||
/// control over the decoding of the Attribute, but once the Attribute exists,
|
||||
/// mapping on it is most useful for creating computed Attribute properties.
|
||||
public func map<T: Codable>(_ transform: (ValueType) throws -> T) rethrows -> Attribute<T> {
|
||||
return Attribute<T>(value: try transform(value))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,12 +6,17 @@
|
||||
//
|
||||
|
||||
public protocol AttributeType: Codable {
|
||||
associatedtype RawValue: Codable
|
||||
associatedtype ValueType
|
||||
|
||||
var value: ValueType { get }
|
||||
}
|
||||
|
||||
// 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
|
||||
let rawValue: RawValue
|
||||
|
||||
public let value: Transformer.To
|
||||
|
||||
@@ -21,16 +26,19 @@ 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 == IdentityTransformer<RawValue> {
|
||||
// If we are using the identity transform, we can skip the transform and guarantee no
|
||||
// error is thrown.
|
||||
public init(value: RawValue) {
|
||||
rawValue = value
|
||||
self.value = value
|
||||
}
|
||||
}
|
||||
|
||||
extension TransformedAttribute where Transformer: ReversibleTransformer {
|
||||
/// Initialize a TransformedAttribute from its transformed value. The
|
||||
/// RawValue, which is what gets encoded/decoded, is determined using
|
||||
/// The Transformer's reverse function.
|
||||
public init(transformedValue: Transformer.To) throws {
|
||||
self.value = transformedValue
|
||||
rawValue = try Transformer.reverse(value)
|
||||
@@ -45,15 +53,35 @@ extension TransformedAttribute: CustomStringConvertible {
|
||||
|
||||
extension TransformedAttribute: Equatable where Transformer.From: Equatable, Transformer.To: Equatable {}
|
||||
|
||||
extension TransformedAttribute where Transformer == IdentityTransformer<RawValue> {
|
||||
// If we are using the identity transform, we can skip the transform and guarantee no
|
||||
// error is thrown.
|
||||
// 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 struct Attribute<RawValue: Codable>: AttributeType {
|
||||
let attribute: TransformedAttribute<RawValue, IdentityTransformer<RawValue>>
|
||||
|
||||
public var value: RawValue {
|
||||
return attribute.value
|
||||
}
|
||||
|
||||
public init(value: RawValue) {
|
||||
rawValue = value
|
||||
self.value = value
|
||||
attribute = .init(value: value)
|
||||
}
|
||||
}
|
||||
|
||||
extension Attribute: CustomStringConvertible {
|
||||
public var description: String {
|
||||
return "Attribute<\(String(describing: RawValue.self))>(\(String(describing: value)))"
|
||||
}
|
||||
}
|
||||
|
||||
extension Attribute: Equatable where RawValue: Equatable {}
|
||||
|
||||
// MARK: - Codable
|
||||
extension TransformedAttribute {
|
||||
public init(from decoder: Decoder) throws {
|
||||
@@ -84,3 +112,36 @@ extension TransformedAttribute {
|
||||
try container.encode(rawValue)
|
||||
}
|
||||
}
|
||||
|
||||
extension Attribute {
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.singleValueContainer()
|
||||
|
||||
// A little trickery follows. If the value is nil, the
|
||||
// container.decode(Value.self) will fail even if Value
|
||||
// is Optional. However, we can check if decoding nil
|
||||
// succeeds and then attempt to coerce nil to a Value
|
||||
// type at which point we can store nil in `value`.
|
||||
let anyNil: Any? = nil
|
||||
if container.decodeNil(),
|
||||
let val = anyNil as? RawValue {
|
||||
attribute = .init(value: val)
|
||||
} else {
|
||||
attribute = try container.decode(TransformedAttribute<RawValue, IdentityTransformer<RawValue>>.self)
|
||||
}
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.singleValueContainer()
|
||||
|
||||
try container.encode(attribute)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Attribute decoding and encoding defaults
|
||||
|
||||
extension AttributeType {
|
||||
public static func defaultDecoding<Container: KeyedDecodingContainerProtocol>(from container: Container, forKey key: Container.Key) throws -> Self {
|
||||
return try container.decode(Self.self, forKey: key)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,11 +8,11 @@
|
||||
/// A JSON API structure within an Entity that contains
|
||||
/// named properties of types `ToOneRelationship` and
|
||||
/// `ToManyRelationship`.
|
||||
public typealias Relationships = Codable & Equatable
|
||||
public protocol Relationships: Codable & Equatable {}
|
||||
|
||||
/// A JSON API structure within an Entity that contains
|
||||
/// properties of any types that are JSON encodable.
|
||||
public typealias Attributes = Codable & Equatable
|
||||
public protocol Attributes: Codable & Equatable {}
|
||||
|
||||
/// Can be used as `Relationships` Type for Entities that do not
|
||||
/// have any Relationships.
|
||||
@@ -32,21 +32,25 @@ public protocol JSONTyped {
|
||||
static var type: String { get }
|
||||
}
|
||||
|
||||
/// An `EntityProxyDescription` is an `EntityDescription`
|
||||
/// without Codable conformance.
|
||||
public protocol EntityProxyDescription: JSONTyped {
|
||||
associatedtype Attributes: Equatable
|
||||
associatedtype Relationships: Equatable
|
||||
}
|
||||
|
||||
/// 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: JSONTyped {
|
||||
associatedtype Attributes: JSONAPI.Attributes
|
||||
associatedtype Relationships: JSONAPI.Relationships
|
||||
}
|
||||
public protocol EntityDescription: EntityProxyDescription where Attributes: JSONAPI.Attributes, Relationships: JSONAPI.Relationships {}
|
||||
|
||||
/// 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, JSONTyped {
|
||||
associatedtype Description: EntityDescription
|
||||
associatedtype Description: EntityProxyDescription
|
||||
associatedtype EntityRawIdType: JSONAPI.MaybeRawId
|
||||
|
||||
typealias Id = JSONAPI.Id<EntityRawIdType, Self>
|
||||
@@ -75,7 +79,9 @@ extension EntityProxy {
|
||||
/// 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 EntityType: EntityProxy, PrimaryResource where Description: EntityDescription {
|
||||
associatedtype Meta: JSONAPI.Meta
|
||||
associatedtype Links: JSONAPI.Links
|
||||
}
|
||||
|
||||
public protocol IdentifiableEntityType: EntityType, Relatable where EntityRawIdType: JSONAPI.RawIdType {}
|
||||
@@ -85,6 +91,10 @@ public protocol IdentifiableEntityType: EntityType, Relatable where EntityRawIdT
|
||||
/// "Resource Object."
|
||||
/// See https://jsonapi.org/format/#document-resource-objects
|
||||
public struct Entity<Description: JSONAPI.EntityDescription, MetaType: JSONAPI.Meta, LinksType: JSONAPI.Links, EntityRawIdType: JSONAPI.MaybeRawId>: EntityType {
|
||||
|
||||
public typealias Meta = MetaType
|
||||
public typealias Links = LinksType
|
||||
|
||||
/// 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,
|
||||
@@ -377,10 +387,15 @@ extension Entity where MetaType == NoMetadata, LinksType == NoLinks, EntityRawId
|
||||
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
|
||||
/// This is just a convenient way to reference an Entity so that
|
||||
/// other Entities' Relationships can be built up from it.
|
||||
public typealias Pointer = ToOneRelationship<Entity, NoMetadata, NoLinks>
|
||||
|
||||
/// Entity.Pointers is a `ToManyRelationship` with no metadata or links.
|
||||
/// This is just a convenient way to reference a bunch of Entities so
|
||||
/// that other Entities' Relationships can be built up from them.
|
||||
public typealias Pointers = ToManyRelationship<Entity, NoMetadata, NoLinks>
|
||||
|
||||
/// Get a pointer to this entity that can be used as a
|
||||
/// relationship to another entity.
|
||||
public var pointer: Pointer {
|
||||
@@ -392,26 +407,47 @@ public extension Entity where EntityRawIdType: JSONAPI.RawIdType {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Identifying Unidentified Entities
|
||||
public extension Entity where EntityRawIdType == Unidentified {
|
||||
/// Create a new Entity from this one with a newly created
|
||||
/// unique Id of the given type.
|
||||
public func identified<RawIdType: CreatableRawIdType>(byType: RawIdType.Type) -> Entity<Description, MetaType, LinksType, RawIdType> {
|
||||
return .init(attributes: attributes, relationships: relationships, meta: meta, links: links)
|
||||
}
|
||||
|
||||
/// Create a new Entity from this one with the given Id.
|
||||
public func identified<RawIdType: JSONAPI.RawIdType>(by id: RawIdType) -> Entity<Description, MetaType, LinksType, RawIdType> {
|
||||
return .init(id: Entity<Description, MetaType, LinksType, RawIdType>.Identifier(rawValue: id), attributes: attributes, relationships: relationships, meta: meta, links: links)
|
||||
}
|
||||
}
|
||||
|
||||
public extension Entity where EntityRawIdType: CreatableRawIdType {
|
||||
/// Create a copy of this Entity with a new unique Id.
|
||||
public func withNewIdentifier() -> Entity {
|
||||
return Entity(attributes: attributes, relationships: relationships, meta: meta, links: links)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Attribute Access
|
||||
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 {
|
||||
subscript<T: AttributeType>(_ path: KeyPath<Description.Attributes, T>) -> T.ValueType {
|
||||
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? {
|
||||
subscript<T: AttributeType>(_ path: KeyPath<Description.Attributes, T?>) -> T.ValueType? {
|
||||
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, U>(_ path: KeyPath<Description.Attributes, TransformedAttribute<T, TFRM>?>) -> U? where TFRM.To == U? {
|
||||
subscript<T: AttributeType, U>(_ path: KeyPath<Description.Attributes, T?>) -> U? where T.ValueType == U? {
|
||||
// Implementation Note: Handles Transform that returns optional
|
||||
// type.
|
||||
return attributes[keyPath: path].flatMap { $0.value }
|
||||
@@ -520,9 +556,10 @@ public extension Entity {
|
||||
|
||||
let maybeUnidentified = Unidentified() as? EntityRawIdType
|
||||
id = try maybeUnidentified.map { Entity.Id(rawValue: $0) } ?? container.decode(Entity.Id.self, forKey: .id)
|
||||
|
||||
attributes = try (NoAttributes() as? Description.Attributes) ?? container.decode(Description.Attributes.self, forKey: .attributes)
|
||||
|
||||
|
||||
attributes = try (NoAttributes() as? Description.Attributes) ??
|
||||
container.decode(Description.Attributes.self, forKey: .attributes)
|
||||
|
||||
relationships = try (NoRelationships() as? Description.Relationships) ?? container.decode(Description.Relationships.self, forKey: .relationships)
|
||||
|
||||
meta = try (NoMetadata() as? MetaType) ?? container.decode(MetaType.self, forKey: .meta)
|
||||
|
||||
@@ -799,3 +799,360 @@ extension Poly7: CustomStringConvertible {
|
||||
return "Poly(\(str))"
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 8 types
|
||||
public protocol _Poly8: _Poly7 {
|
||||
associatedtype H: EntityType
|
||||
var h: H? { get }
|
||||
|
||||
init(_ h: H)
|
||||
}
|
||||
|
||||
public extension _Poly8 {
|
||||
subscript(_ lookup: H.Type) -> H? {
|
||||
return h
|
||||
}
|
||||
}
|
||||
|
||||
public enum Poly8<A: EntityType, B: EntityType, C: EntityType, D: EntityType, E: EntityType, F: EntityType, G: EntityType, H: EntityType>: _Poly8 {
|
||||
case a(A)
|
||||
case b(B)
|
||||
case c(C)
|
||||
case d(D)
|
||||
case e(E)
|
||||
case f(F)
|
||||
case g(G)
|
||||
case h(H)
|
||||
|
||||
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 var h: H? {
|
||||
guard case let .h(ret) = self else { return nil }
|
||||
return ret
|
||||
}
|
||||
|
||||
public init(_ h: H) {
|
||||
self = .h(h)
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.singleValueContainer()
|
||||
|
||||
let attempts = [
|
||||
try decode(A.self, from: container).map { Poly8.a($0) },
|
||||
try decode(B.self, from: container).map { Poly8.b($0) },
|
||||
try decode(C.self, from: container).map { Poly8.c($0) },
|
||||
try decode(D.self, from: container).map { Poly8.d($0) },
|
||||
try decode(E.self, from: container).map { Poly8.e($0) },
|
||||
try decode(F.self, from: container).map { Poly8.f($0) },
|
||||
try decode(G.self, from: container).map { Poly8.g($0) },
|
||||
try decode(H.self, from: container).map { Poly8.h($0) }]
|
||||
|
||||
let maybeVal: Poly8<A, B, C, D, E, F, G, H>? = attempts
|
||||
.compactMap { $0.value }
|
||||
.first
|
||||
|
||||
guard let val = maybeVal else {
|
||||
throw EncodingError.invalidValue(Poly8<A, B, C, D, E, F, G, H>.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)
|
||||
case .h(let h):
|
||||
try container.encode(h)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Poly8: 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)
|
||||
case .h(let h):
|
||||
str = String(describing: h)
|
||||
}
|
||||
|
||||
return "Poly(\(str))"
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 9 types
|
||||
public protocol _Poly9: _Poly8 {
|
||||
associatedtype I: EntityType
|
||||
var i: I? { get }
|
||||
|
||||
init(_ i: I)
|
||||
}
|
||||
|
||||
public extension _Poly9 {
|
||||
subscript(_ lookup: I.Type) -> I? {
|
||||
return i
|
||||
}
|
||||
}
|
||||
|
||||
public enum Poly9<A: EntityType, B: EntityType, C: EntityType, D: EntityType, E: EntityType, F: EntityType, G: EntityType, H: EntityType, I: EntityType>: _Poly9 {
|
||||
case a(A)
|
||||
case b(B)
|
||||
case c(C)
|
||||
case d(D)
|
||||
case e(E)
|
||||
case f(F)
|
||||
case g(G)
|
||||
case h(H)
|
||||
case i(I)
|
||||
|
||||
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 var h: H? {
|
||||
guard case let .h(ret) = self else { return nil }
|
||||
return ret
|
||||
}
|
||||
|
||||
public init(_ h: H) {
|
||||
self = .h(h)
|
||||
}
|
||||
|
||||
public var i: I? {
|
||||
guard case let .i(ret) = self else { return nil }
|
||||
return ret
|
||||
}
|
||||
|
||||
public init(_ i: I) {
|
||||
self = .i(i)
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.singleValueContainer()
|
||||
|
||||
let attempts = [
|
||||
try decode(A.self, from: container).map { Poly9.a($0) },
|
||||
try decode(B.self, from: container).map { Poly9.b($0) },
|
||||
try decode(C.self, from: container).map { Poly9.c($0) },
|
||||
try decode(D.self, from: container).map { Poly9.d($0) },
|
||||
try decode(E.self, from: container).map { Poly9.e($0) },
|
||||
try decode(F.self, from: container).map { Poly9.f($0) },
|
||||
try decode(G.self, from: container).map { Poly9.g($0) },
|
||||
try decode(H.self, from: container).map { Poly9.h($0) },
|
||||
try decode(I.self, from: container).map { Poly9.i($0) }]
|
||||
|
||||
let maybeVal: Poly9<A, B, C, D, E, F, G, H, I>? = attempts
|
||||
.compactMap { $0.value }
|
||||
.first
|
||||
|
||||
guard let val = maybeVal else {
|
||||
throw EncodingError.invalidValue(Poly9<A, B, C, D, E, F, G, H, I>.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)
|
||||
case .h(let h):
|
||||
try container.encode(h)
|
||||
case .i(let i):
|
||||
try container.encode(i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Poly9: 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)
|
||||
case .h(let h):
|
||||
str = String(describing: h)
|
||||
case .i(let i):
|
||||
str = String(describing: i)
|
||||
}
|
||||
|
||||
return "Poly(\(str))"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
// Created by Mathew Polzin on 11/17/18.
|
||||
//
|
||||
|
||||
/// A Transformer simply defines a static function that transforms a value.
|
||||
public protocol Transformer {
|
||||
associatedtype From
|
||||
associatedtype To
|
||||
@@ -12,10 +13,13 @@ public protocol Transformer {
|
||||
static func transform(_ value: From) throws -> To
|
||||
}
|
||||
|
||||
/// ReversibleTransformers define a function that reverses the transform
|
||||
/// operation.
|
||||
public protocol ReversibleTransformer: Transformer {
|
||||
static func reverse(_ value: To) throws -> From
|
||||
}
|
||||
|
||||
/// The IdentityTransformer does not perform any transformation on a value.
|
||||
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 }
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
|
||||
import JSONAPI
|
||||
|
||||
extension TransformedAttribute: ExpressibleByUnicodeScalarLiteral where RawValue: ExpressibleByUnicodeScalarLiteral, Transformer == IdentityTransformer<RawValue> {
|
||||
extension Attribute: ExpressibleByUnicodeScalarLiteral where RawValue: ExpressibleByUnicodeScalarLiteral {
|
||||
public typealias UnicodeScalarLiteralType = RawValue.UnicodeScalarLiteralType
|
||||
|
||||
public init(unicodeScalarLiteral value: RawValue.UnicodeScalarLiteralType) {
|
||||
@@ -9,7 +9,7 @@ extension TransformedAttribute: ExpressibleByUnicodeScalarLiteral where RawValue
|
||||
}
|
||||
}
|
||||
|
||||
extension TransformedAttribute: ExpressibleByExtendedGraphemeClusterLiteral where RawValue: ExpressibleByExtendedGraphemeClusterLiteral, Transformer == IdentityTransformer<RawValue> {
|
||||
extension Attribute: ExpressibleByExtendedGraphemeClusterLiteral where RawValue: ExpressibleByExtendedGraphemeClusterLiteral {
|
||||
public typealias ExtendedGraphemeClusterLiteralType = RawValue.ExtendedGraphemeClusterLiteralType
|
||||
|
||||
public init(extendedGraphemeClusterLiteral value: RawValue.ExtendedGraphemeClusterLiteralType) {
|
||||
@@ -17,7 +17,7 @@ extension TransformedAttribute: ExpressibleByExtendedGraphemeClusterLiteral wher
|
||||
}
|
||||
}
|
||||
|
||||
extension TransformedAttribute: ExpressibleByStringLiteral where RawValue: ExpressibleByStringLiteral, Transformer == IdentityTransformer<RawValue> {
|
||||
extension Attribute: ExpressibleByStringLiteral where RawValue: ExpressibleByStringLiteral {
|
||||
public typealias StringLiteralType = RawValue.StringLiteralType
|
||||
|
||||
public init(stringLiteral value: RawValue.StringLiteralType) {
|
||||
@@ -25,13 +25,13 @@ extension TransformedAttribute: ExpressibleByStringLiteral where RawValue: Expre
|
||||
}
|
||||
}
|
||||
|
||||
extension TransformedAttribute: ExpressibleByNilLiteral where RawValue: ExpressibleByNilLiteral, Transformer == IdentityTransformer<RawValue> {
|
||||
extension Attribute: ExpressibleByNilLiteral where RawValue: ExpressibleByNilLiteral {
|
||||
public init(nilLiteral: ()) {
|
||||
self.init(value: RawValue(nilLiteral: ()))
|
||||
}
|
||||
}
|
||||
|
||||
extension TransformedAttribute: ExpressibleByFloatLiteral where RawValue: ExpressibleByFloatLiteral, Transformer == IdentityTransformer<RawValue> {
|
||||
extension Attribute: ExpressibleByFloatLiteral where RawValue: ExpressibleByFloatLiteral {
|
||||
public typealias FloatLiteralType = RawValue.FloatLiteralType
|
||||
|
||||
public init(floatLiteral value: RawValue.FloatLiteralType) {
|
||||
@@ -47,7 +47,7 @@ extension Optional: ExpressibleByFloatLiteral where Wrapped: ExpressibleByFloatL
|
||||
}
|
||||
}
|
||||
|
||||
extension TransformedAttribute: ExpressibleByBooleanLiteral where RawValue: ExpressibleByBooleanLiteral, Transformer == IdentityTransformer<RawValue> {
|
||||
extension Attribute: ExpressibleByBooleanLiteral where RawValue: ExpressibleByBooleanLiteral {
|
||||
public typealias BooleanLiteralType = RawValue.BooleanLiteralType
|
||||
|
||||
public init(booleanLiteral value: BooleanLiteralType) {
|
||||
@@ -63,7 +63,7 @@ extension Optional: ExpressibleByBooleanLiteral where Wrapped: ExpressibleByBool
|
||||
}
|
||||
}
|
||||
|
||||
extension TransformedAttribute: ExpressibleByIntegerLiteral where RawValue: ExpressibleByIntegerLiteral, Transformer == IdentityTransformer<RawValue> {
|
||||
extension Attribute: ExpressibleByIntegerLiteral where RawValue: ExpressibleByIntegerLiteral {
|
||||
public typealias IntegerLiteralType = RawValue.IntegerLiteralType
|
||||
|
||||
public init(integerLiteral value: IntegerLiteralType) {
|
||||
@@ -91,7 +91,7 @@ public protocol DictionaryType {
|
||||
}
|
||||
extension Dictionary: DictionaryType {}
|
||||
|
||||
extension TransformedAttribute: ExpressibleByDictionaryLiteral where RawValue: DictionaryType, Transformer == IdentityTransformer<RawValue> {
|
||||
extension Attribute: ExpressibleByDictionaryLiteral where RawValue: DictionaryType {
|
||||
public typealias Key = RawValue.Key
|
||||
|
||||
public typealias Value = RawValue.Value
|
||||
@@ -121,7 +121,7 @@ public protocol ArrayType {
|
||||
extension Array: ArrayType {}
|
||||
extension ArraySlice: ArrayType {}
|
||||
|
||||
extension TransformedAttribute: ExpressibleByArrayLiteral where RawValue: ArrayType, Transformer == IdentityTransformer<RawValue> {
|
||||
extension Attribute: ExpressibleByArrayLiteral where RawValue: ArrayType {
|
||||
public typealias ArrayLiteralElement = RawValue.Element
|
||||
|
||||
public init(arrayLiteral elements: ArrayLiteralElement...) {
|
||||
|
||||
@@ -41,6 +41,7 @@ extension Optional: OptionalArray where Wrapped: ArrayType {}
|
||||
|
||||
private protocol AttributeTypeWithOptionalArray {}
|
||||
extension TransformedAttribute: AttributeTypeWithOptionalArray where RawValue: OptionalArray {}
|
||||
extension Attribute: AttributeTypeWithOptionalArray where RawValue: OptionalArray {}
|
||||
|
||||
private protocol OptionalRelationshipType {}
|
||||
extension Optional: OptionalRelationshipType where Wrapped: RelationshipType {}
|
||||
@@ -49,6 +50,10 @@ private protocol _RelationshipType {}
|
||||
extension ToOneRelationship: _RelationshipType {}
|
||||
extension ToManyRelationship: _RelationshipType {}
|
||||
|
||||
private protocol _AttributeType {}
|
||||
extension TransformedAttribute: _AttributeType {}
|
||||
extension Attribute: _AttributeType {}
|
||||
|
||||
public extension Entity {
|
||||
public static func check(_ entity: Entity) throws {
|
||||
var problems = [EntityCheckError]()
|
||||
@@ -60,7 +65,7 @@ public extension Entity {
|
||||
}
|
||||
|
||||
for attribute in attributesMirror.children {
|
||||
if attribute.value as? AttributeType == nil,
|
||||
if attribute.value as? _AttributeType == nil,
|
||||
attribute.value as? OptionalAttributeType == nil {
|
||||
problems.append(.nonAttribute(named: attribute.label ?? "unnamed"))
|
||||
}
|
||||
|
||||
@@ -10,12 +10,8 @@ import JSONAPI
|
||||
|
||||
class AttributeTests: XCTestCase {
|
||||
|
||||
func test_AttributeIsTransformedAttribute() {
|
||||
XCTAssertEqual(try TransformedAttribute<String, IdentityTransformer<String>>(rawValue: "hello"), try Attribute<String>(rawValue: "hello"))
|
||||
}
|
||||
|
||||
func test_AttributeNonThrowingConstructor() {
|
||||
XCTAssertEqual(try Attribute<String>(rawValue: "hello"), Attribute<String>(value: "hello"))
|
||||
func test_AttributeConstructor() {
|
||||
XCTAssertEqual(Attribute<String>(value: "hello").value, "hello")
|
||||
}
|
||||
|
||||
func test_TransformedAttributeNoThrow() {
|
||||
@@ -30,6 +26,18 @@ class AttributeTests: XCTestCase {
|
||||
XCTAssertNoThrow(try TransformedAttribute<String, TestTransformer>(transformedValue: 10))
|
||||
}
|
||||
|
||||
func test_EncodedPrimitives() {
|
||||
testEncodedPrimitive(attribute: Attribute<Int>(value: 10))
|
||||
testEncodedPrimitive(attribute: Attribute<Bool>(value: false))
|
||||
testEncodedPrimitive(attribute: Attribute<Double>(value: 10.2))
|
||||
|
||||
testEncodedPrimitive(attribute: try! TransformedAttribute<Int, IntToString>(rawValue: 10))
|
||||
testEncodedPrimitive(attribute: try! TransformedAttribute<Int, IntToInt>(rawValue: 10))
|
||||
testEncodedPrimitive(attribute: try! TransformedAttribute<Int, IntToDouble>(rawValue: 10))
|
||||
testEncodedPrimitive(attribute: try! TransformedAttribute<String, TestTransformer>(rawValue: "10"))
|
||||
testEncodedPrimitive(attribute: try! TransformedAttribute<String?, OptionalToString<String>>(rawValue: "10"))
|
||||
}
|
||||
|
||||
func test_NullableIsNullIfNil() {
|
||||
struct Wrapper: Codable {
|
||||
let dummy: Attribute<String?>
|
||||
@@ -68,4 +76,28 @@ extension AttributeTests {
|
||||
return String(value)
|
||||
}
|
||||
}
|
||||
|
||||
enum IntToString: Transformer {
|
||||
public static func transform(_ from: Int) -> String {
|
||||
return String(from)
|
||||
}
|
||||
}
|
||||
|
||||
enum IntToInt: Transformer {
|
||||
public static func transform(_ from: Int) -> Int {
|
||||
return from + 100
|
||||
}
|
||||
}
|
||||
|
||||
enum IntToDouble: Transformer {
|
||||
public static func transform(_ from: Int) -> Double {
|
||||
return Double(from)
|
||||
}
|
||||
}
|
||||
|
||||
enum OptionalToString<T>: Transformer {
|
||||
public static func transform(_ from: T?) -> String {
|
||||
return String(describing: from)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
//
|
||||
// CustomAttributesTests.swift
|
||||
// JSONAPITests
|
||||
//
|
||||
// Created by Mathew Polzin on 12/27/18.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import JSONAPI
|
||||
import JSONAPITestLib
|
||||
|
||||
class CustomAttributesTests: XCTestCase {
|
||||
func test_customDecode() {
|
||||
let entity = decoded(type: CustomAttributeEntity.self, data: customAttributeEntityData)
|
||||
|
||||
XCTAssertEqual(entity[\.firstName], "Cool")
|
||||
XCTAssertEqual(entity[\.name], "Cool Name")
|
||||
XCTAssertNoThrow(try CustomAttributeEntity.check(entity))
|
||||
}
|
||||
|
||||
func test_customEncode() {
|
||||
test_DecodeEncodeEquality(type: CustomAttributeEntity.self,
|
||||
data: customAttributeEntityData)
|
||||
}
|
||||
|
||||
func test_customKeysDecode() {
|
||||
let entity = decoded(type: CustomKeysEntity.self, data: customAttributeEntityData)
|
||||
|
||||
XCTAssertEqual(entity[\.firstNameSilly], "Cool")
|
||||
XCTAssertEqual(entity[\.lastNameSilly], "Name")
|
||||
XCTAssertNoThrow(try CustomKeysEntity.check(entity))
|
||||
}
|
||||
|
||||
func test_customKeysEncode() {
|
||||
test_DecodeEncodeEquality(type: CustomKeysEntity.self,
|
||||
data: customAttributeEntityData)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Test Types
|
||||
extension CustomAttributesTests {
|
||||
enum CustomAttributeEntityDescription: EntityDescription {
|
||||
public static var type: String { return "test1" }
|
||||
|
||||
public struct Attributes: JSONAPI.Attributes {
|
||||
let firstName: Attribute<String>
|
||||
public let name: Attribute<String>
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case firstName
|
||||
case lastName
|
||||
}
|
||||
}
|
||||
|
||||
public typealias Relationships = NoRelationships
|
||||
}
|
||||
|
||||
typealias CustomAttributeEntity = BasicEntity<CustomAttributeEntityDescription>
|
||||
|
||||
enum CustomKeysEntityDescription: EntityDescription {
|
||||
public static var type: String { return "test1" }
|
||||
|
||||
public struct Attributes: JSONAPI.Attributes {
|
||||
public let firstNameSilly: Attribute<String>
|
||||
public let lastNameSilly: Attribute<String>
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case firstNameSilly = "firstName"
|
||||
case lastNameSilly = "lastName"
|
||||
}
|
||||
}
|
||||
|
||||
public typealias Relationships = NoRelationships
|
||||
}
|
||||
|
||||
typealias CustomKeysEntity = BasicEntity<CustomKeysEntityDescription>
|
||||
}
|
||||
|
||||
extension CustomAttributesTests.CustomAttributeEntityDescription.Attributes {
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
firstName = try .defaultDecoding(from: container, forKey: .firstName)
|
||||
let lastName = try container.decode(String.self, forKey: .lastName)
|
||||
|
||||
name = firstName.map { "\($0) \(lastName)" }
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
try container.encode(firstName, forKey: .firstName)
|
||||
let lastName = String(name.value.split(separator: " ")[1])
|
||||
try container.encode(lastName, forKey: .lastName)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Test Data
|
||||
private let customAttributeEntityData = """
|
||||
{
|
||||
"type": "test1",
|
||||
"id": "1",
|
||||
"attributes": {
|
||||
"firstName": "Cool",
|
||||
"lastName": "Name"
|
||||
}
|
||||
}
|
||||
""".data(using: .utf8)!
|
||||
@@ -16,9 +16,9 @@ class DocumentTests: XCTestCase {
|
||||
|
||||
XCTAssertFalse(document.body.isError)
|
||||
XCTAssertNil(document.body.errors)
|
||||
XCTAssertNotNil(document.body.primaryData)
|
||||
XCTAssertNotNil(document.body.primaryResource)
|
||||
XCTAssertEqual(document.body.meta, NoMetadata())
|
||||
XCTAssertNil(document.body.primaryData?.value)
|
||||
XCTAssertNil(document.body.primaryResource?.value)
|
||||
XCTAssertEqual(document.body.includes?.count, 0)
|
||||
XCTAssertEqual(document.body.links, NoLinks())
|
||||
XCTAssertEqual(document.apiDescription, .none)
|
||||
@@ -35,9 +35,9 @@ class DocumentTests: XCTestCase {
|
||||
|
||||
XCTAssertFalse(document.body.isError)
|
||||
XCTAssertNil(document.body.errors)
|
||||
XCTAssertNotNil(document.body.primaryData)
|
||||
XCTAssertNotNil(document.body.primaryResource)
|
||||
XCTAssertEqual(document.body.meta, NoMetadata())
|
||||
XCTAssertNil(document.body.primaryData?.value)
|
||||
XCTAssertNil(document.body.primaryResource?.value)
|
||||
XCTAssertEqual(document.body.includes?.count, 0)
|
||||
XCTAssertEqual(document.body.links, NoLinks())
|
||||
XCTAssertEqual(document.apiDescription.version, "1.0")
|
||||
@@ -73,7 +73,8 @@ extension DocumentTests {
|
||||
|
||||
XCTAssertTrue(document.body.isError)
|
||||
XCTAssertEqual(document.body.meta, NoMetadata())
|
||||
XCTAssertNil(document.body.primaryData)
|
||||
XCTAssertNil(document.body.data)
|
||||
XCTAssertNil(document.body.primaryResource)
|
||||
XCTAssertNil(document.body.includes)
|
||||
|
||||
guard case let .errors(errors) = document.body else {
|
||||
@@ -98,7 +99,7 @@ extension DocumentTests {
|
||||
|
||||
XCTAssertTrue(document.body.isError)
|
||||
XCTAssertEqual(document.body.meta, NoMetadata())
|
||||
XCTAssertNil(document.body.primaryData)
|
||||
XCTAssertNil(document.body.primaryResource)
|
||||
XCTAssertNil(document.body.includes)
|
||||
XCTAssertEqual(document.apiDescription.version, "1.0")
|
||||
|
||||
@@ -123,7 +124,7 @@ extension DocumentTests {
|
||||
|
||||
XCTAssertTrue(document.body.isError)
|
||||
XCTAssertNil(document.body.meta)
|
||||
XCTAssertNil(document.body.primaryData)
|
||||
XCTAssertNil(document.body.primaryResource)
|
||||
XCTAssertNil(document.body.includes)
|
||||
|
||||
guard case let .errors(errors) = document.body else {
|
||||
@@ -146,7 +147,7 @@ extension DocumentTests {
|
||||
|
||||
XCTAssertTrue(document.body.isError)
|
||||
XCTAssertNil(document.body.meta)
|
||||
XCTAssertNil(document.body.primaryData)
|
||||
XCTAssertNil(document.body.primaryResource)
|
||||
XCTAssertNil(document.body.includes)
|
||||
XCTAssertEqual(document.apiDescription.version, "1.0")
|
||||
|
||||
@@ -171,7 +172,7 @@ extension DocumentTests {
|
||||
|
||||
XCTAssertTrue(document.body.isError)
|
||||
XCTAssertEqual(document.body.meta, NoMetadata())
|
||||
XCTAssertNil(document.body.primaryData)
|
||||
XCTAssertNil(document.body.primaryResource)
|
||||
XCTAssertNil(document.body.includes)
|
||||
|
||||
guard case let .errors(errors) = document.body else {
|
||||
@@ -196,7 +197,7 @@ extension DocumentTests {
|
||||
|
||||
XCTAssertTrue(document.body.isError)
|
||||
XCTAssertEqual(document.body.meta, NoMetadata())
|
||||
XCTAssertNil(document.body.primaryData)
|
||||
XCTAssertNil(document.body.primaryResource)
|
||||
XCTAssertNil(document.body.includes)
|
||||
XCTAssertEqual(document.apiDescription.version, "1.0")
|
||||
|
||||
@@ -222,7 +223,7 @@ extension DocumentTests {
|
||||
|
||||
XCTAssertTrue(document.body.isError)
|
||||
XCTAssertEqual(document.body.meta, TestPageMetadata(total: 70, limit: 40, offset: 10))
|
||||
XCTAssertNil(document.body.primaryData)
|
||||
XCTAssertNil(document.body.primaryResource)
|
||||
XCTAssertNil(document.body.includes)
|
||||
|
||||
guard case let .errors(errors) = document.body else {
|
||||
@@ -246,7 +247,7 @@ extension DocumentTests {
|
||||
|
||||
XCTAssertTrue(document.body.isError)
|
||||
XCTAssertEqual(document.body.meta, TestPageMetadata(total: 70, limit: 40, offset: 10))
|
||||
XCTAssertNil(document.body.primaryData)
|
||||
XCTAssertNil(document.body.primaryResource)
|
||||
XCTAssertNil(document.body.includes)
|
||||
XCTAssertEqual(document.apiDescription.version, "1.0")
|
||||
|
||||
@@ -271,7 +272,7 @@ extension DocumentTests {
|
||||
|
||||
XCTAssertTrue(document.body.isError)
|
||||
XCTAssertEqual(document.body.meta, TestPageMetadata(total: 70, limit: 40, offset: 10))
|
||||
XCTAssertNil(document.body.primaryData)
|
||||
XCTAssertNil(document.body.primaryResource)
|
||||
XCTAssertNil(document.body.includes)
|
||||
|
||||
guard case let .errors(errors) = document.body else {
|
||||
@@ -299,7 +300,7 @@ extension DocumentTests {
|
||||
|
||||
XCTAssertTrue(document.body.isError)
|
||||
XCTAssertEqual(document.body.meta, TestPageMetadata(total: 70, limit: 40, offset: 10))
|
||||
XCTAssertNil(document.body.primaryData)
|
||||
XCTAssertNil(document.body.primaryResource)
|
||||
XCTAssertNil(document.body.includes)
|
||||
XCTAssertEqual(document.apiDescription.version, "1.0")
|
||||
|
||||
@@ -327,7 +328,7 @@ extension DocumentTests {
|
||||
data: error_document_with_links)
|
||||
|
||||
XCTAssertTrue(document.body.isError)
|
||||
XCTAssertNil(document.body.primaryData)
|
||||
XCTAssertNil(document.body.primaryResource)
|
||||
XCTAssertNil(document.body.includes)
|
||||
|
||||
guard case let .errors(errors) = document.body else {
|
||||
@@ -353,7 +354,7 @@ extension DocumentTests {
|
||||
data: error_document_with_links_with_api_description)
|
||||
|
||||
XCTAssertTrue(document.body.isError)
|
||||
XCTAssertNil(document.body.primaryData)
|
||||
XCTAssertNil(document.body.primaryResource)
|
||||
XCTAssertNil(document.body.includes)
|
||||
XCTAssertEqual(document.apiDescription.version, "1.0")
|
||||
|
||||
@@ -380,7 +381,7 @@ extension DocumentTests {
|
||||
data: error_document_no_metadata)
|
||||
|
||||
XCTAssertTrue(document.body.isError)
|
||||
XCTAssertNil(document.body.primaryData)
|
||||
XCTAssertNil(document.body.primaryResource)
|
||||
XCTAssertNil(document.body.includes)
|
||||
|
||||
guard case let .errors(errors) = document.body else {
|
||||
@@ -403,7 +404,7 @@ extension DocumentTests {
|
||||
data: error_document_no_metadata_with_api_description)
|
||||
|
||||
XCTAssertTrue(document.body.isError)
|
||||
XCTAssertNil(document.body.primaryData)
|
||||
XCTAssertNil(document.body.primaryResource)
|
||||
XCTAssertNil(document.body.includes)
|
||||
XCTAssertEqual(document.apiDescription.version, "1.0")
|
||||
|
||||
@@ -523,8 +524,9 @@ extension DocumentTests {
|
||||
|
||||
XCTAssertFalse(document.body.isError)
|
||||
XCTAssertNil(document.body.errors)
|
||||
XCTAssertNotNil(document.body.primaryData)
|
||||
XCTAssertEqual(document.body.primaryData?.value.id.rawValue, "1")
|
||||
XCTAssertNotNil(document.body.primaryResource)
|
||||
XCTAssertEqual(document.body.primaryResource?.value.id.rawValue, "1")
|
||||
XCTAssertEqual(document.body.data?.primary, document.body.primaryResource)
|
||||
XCTAssertEqual(document.body.includes?.count, 0)
|
||||
XCTAssertEqual(document.body.meta, NoMetadata())
|
||||
}
|
||||
@@ -540,8 +542,8 @@ extension DocumentTests {
|
||||
|
||||
XCTAssertFalse(document.body.isError)
|
||||
XCTAssertNil(document.body.errors)
|
||||
XCTAssertNotNil(document.body.primaryData)
|
||||
XCTAssertEqual(document.body.primaryData?.value.id.rawValue, "1")
|
||||
XCTAssertNotNil(document.body.primaryResource)
|
||||
XCTAssertEqual(document.body.primaryResource?.value.id.rawValue, "1")
|
||||
XCTAssertEqual(document.body.includes?.count, 0)
|
||||
XCTAssertEqual(document.body.meta, NoMetadata())
|
||||
XCTAssertEqual(document.apiDescription.version, "1.0")
|
||||
@@ -558,8 +560,8 @@ extension DocumentTests {
|
||||
|
||||
XCTAssertFalse(document.body.isError)
|
||||
XCTAssertNil(document.body.errors)
|
||||
XCTAssertNotNil(document.body.primaryData)
|
||||
XCTAssertEqual(document.body.primaryData?.value?.id.rawValue, "1")
|
||||
XCTAssertNotNil(document.body.primaryResource)
|
||||
XCTAssertEqual(document.body.primaryResource?.value?.id.rawValue, "1")
|
||||
XCTAssertEqual(document.body.includes?.count, 0)
|
||||
XCTAssertEqual(document.body.meta, NoMetadata())
|
||||
}
|
||||
@@ -575,8 +577,8 @@ extension DocumentTests {
|
||||
|
||||
XCTAssertFalse(document.body.isError)
|
||||
XCTAssertNil(document.body.errors)
|
||||
XCTAssertNotNil(document.body.primaryData)
|
||||
XCTAssertEqual(document.body.primaryData?.value?.id.rawValue, "1")
|
||||
XCTAssertNotNil(document.body.primaryResource)
|
||||
XCTAssertEqual(document.body.primaryResource?.value?.id.rawValue, "1")
|
||||
XCTAssertEqual(document.body.includes?.count, 0)
|
||||
XCTAssertEqual(document.body.meta, NoMetadata())
|
||||
XCTAssertEqual(document.apiDescription.version, "1.0")
|
||||
@@ -593,8 +595,8 @@ extension DocumentTests {
|
||||
|
||||
XCTAssertFalse(document.body.isError)
|
||||
XCTAssertNil(document.body.errors)
|
||||
XCTAssertNotNil(document.body.primaryData)
|
||||
XCTAssertEqual(document.body.primaryData?.value.id.rawValue, "1")
|
||||
XCTAssertNotNil(document.body.primaryResource)
|
||||
XCTAssertEqual(document.body.primaryResource?.value.id.rawValue, "1")
|
||||
XCTAssertEqual(document.body.includes?.count, 0)
|
||||
XCTAssertEqual(document.body.meta, TestPageMetadata(total: 70, limit: 40, offset: 10))
|
||||
}
|
||||
@@ -610,8 +612,8 @@ extension DocumentTests {
|
||||
|
||||
XCTAssertFalse(document.body.isError)
|
||||
XCTAssertNil(document.body.errors)
|
||||
XCTAssertNotNil(document.body.primaryData)
|
||||
XCTAssertEqual(document.body.primaryData?.value.id.rawValue, "1")
|
||||
XCTAssertNotNil(document.body.primaryResource)
|
||||
XCTAssertEqual(document.body.primaryResource?.value.id.rawValue, "1")
|
||||
XCTAssertEqual(document.body.includes?.count, 0)
|
||||
XCTAssertEqual(document.body.meta, TestPageMetadata(total: 70, limit: 40, offset: 10))
|
||||
XCTAssertEqual(document.apiDescription.version, "1.0")
|
||||
@@ -628,8 +630,8 @@ extension DocumentTests {
|
||||
|
||||
XCTAssertFalse(document.body.isError)
|
||||
XCTAssertNil(document.body.errors)
|
||||
XCTAssertNotNil(document.body.primaryData)
|
||||
XCTAssertEqual(document.body.primaryData?.value.id.rawValue, "1")
|
||||
XCTAssertNotNil(document.body.primaryResource)
|
||||
XCTAssertEqual(document.body.primaryResource?.value.id.rawValue, "1")
|
||||
XCTAssertEqual(document.body.includes?.count, 0)
|
||||
XCTAssertEqual(document.body.meta, NoMetadata())
|
||||
XCTAssertEqual(document.body.links?.link.url, "https://website.com")
|
||||
@@ -650,8 +652,8 @@ extension DocumentTests {
|
||||
|
||||
XCTAssertFalse(document.body.isError)
|
||||
XCTAssertNil(document.body.errors)
|
||||
XCTAssertNotNil(document.body.primaryData)
|
||||
XCTAssertEqual(document.body.primaryData?.value.id.rawValue, "1")
|
||||
XCTAssertNotNil(document.body.primaryResource)
|
||||
XCTAssertEqual(document.body.primaryResource?.value.id.rawValue, "1")
|
||||
XCTAssertEqual(document.body.includes?.count, 0)
|
||||
XCTAssertEqual(document.body.meta, NoMetadata())
|
||||
XCTAssertEqual(document.body.links?.link.url, "https://website.com")
|
||||
@@ -673,8 +675,8 @@ extension DocumentTests {
|
||||
|
||||
XCTAssertFalse(document.body.isError)
|
||||
XCTAssertNil(document.body.errors)
|
||||
XCTAssertNotNil(document.body.primaryData)
|
||||
XCTAssertEqual(document.body.primaryData?.value.id.rawValue, "1")
|
||||
XCTAssertNotNil(document.body.primaryResource)
|
||||
XCTAssertEqual(document.body.primaryResource?.value.id.rawValue, "1")
|
||||
XCTAssertEqual(document.body.includes?.count, 0)
|
||||
XCTAssertEqual(document.body.meta, TestPageMetadata(total: 70, limit: 40, offset: 10))
|
||||
XCTAssertEqual(document.body.links?.link.url, "https://website.com")
|
||||
@@ -695,8 +697,8 @@ extension DocumentTests {
|
||||
|
||||
XCTAssertFalse(document.body.isError)
|
||||
XCTAssertNil(document.body.errors)
|
||||
XCTAssertNotNil(document.body.primaryData)
|
||||
XCTAssertEqual(document.body.primaryData?.value.id.rawValue, "1")
|
||||
XCTAssertNotNil(document.body.primaryResource)
|
||||
XCTAssertEqual(document.body.primaryResource?.value.id.rawValue, "1")
|
||||
XCTAssertEqual(document.body.includes?.count, 0)
|
||||
XCTAssertEqual(document.body.meta, TestPageMetadata(total: 70, limit: 40, offset: 10))
|
||||
XCTAssertEqual(document.body.links?.link.url, "https://website.com")
|
||||
@@ -726,8 +728,8 @@ extension DocumentTests {
|
||||
|
||||
XCTAssertFalse(document.body.isError)
|
||||
XCTAssertNil(document.body.errors)
|
||||
XCTAssertNotNil(document.body.primaryData)
|
||||
XCTAssertEqual(document.body.primaryData?.value.id.rawValue, "1")
|
||||
XCTAssertNotNil(document.body.primaryResource)
|
||||
XCTAssertEqual(document.body.primaryResource?.value.id.rawValue, "1")
|
||||
XCTAssertEqual(document.body.includes?.count, 1)
|
||||
XCTAssertEqual(document.body.includes?[Author.self].count, 1)
|
||||
XCTAssertEqual(document.body.includes?[Author.self][0].id.rawValue, "33")
|
||||
@@ -744,8 +746,8 @@ extension DocumentTests {
|
||||
|
||||
XCTAssertFalse(document.body.isError)
|
||||
XCTAssertNil(document.body.errors)
|
||||
XCTAssertNotNil(document.body.primaryData)
|
||||
XCTAssertEqual(document.body.primaryData?.value.id.rawValue, "1")
|
||||
XCTAssertNotNil(document.body.primaryResource)
|
||||
XCTAssertEqual(document.body.primaryResource?.value.id.rawValue, "1")
|
||||
XCTAssertEqual(document.body.includes?.count, 1)
|
||||
XCTAssertEqual(document.body.includes?[Author.self].count, 1)
|
||||
XCTAssertEqual(document.body.includes?[Author.self][0].id.rawValue, "33")
|
||||
@@ -763,8 +765,8 @@ extension DocumentTests {
|
||||
|
||||
XCTAssertFalse(document.body.isError)
|
||||
XCTAssertNil(document.body.errors)
|
||||
XCTAssertNotNil(document.body.primaryData)
|
||||
XCTAssertEqual(document.body.primaryData?.value.id.rawValue, "1")
|
||||
XCTAssertNotNil(document.body.primaryResource)
|
||||
XCTAssertEqual(document.body.primaryResource?.value.id.rawValue, "1")
|
||||
XCTAssertEqual(document.body.includes?.count, 1)
|
||||
XCTAssertEqual(document.body.includes?[Author.self].count, 1)
|
||||
XCTAssertEqual(document.body.includes?[Author.self][0].id.rawValue, "33")
|
||||
@@ -782,8 +784,8 @@ extension DocumentTests {
|
||||
|
||||
XCTAssertFalse(document.body.isError)
|
||||
XCTAssertNil(document.body.errors)
|
||||
XCTAssertNotNil(document.body.primaryData)
|
||||
XCTAssertEqual(document.body.primaryData?.value.id.rawValue, "1")
|
||||
XCTAssertNotNil(document.body.primaryResource)
|
||||
XCTAssertEqual(document.body.primaryResource?.value.id.rawValue, "1")
|
||||
XCTAssertEqual(document.body.includes?.count, 1)
|
||||
XCTAssertEqual(document.body.includes?[Author.self].count, 1)
|
||||
XCTAssertEqual(document.body.includes?[Author.self][0].id.rawValue, "33")
|
||||
@@ -802,8 +804,8 @@ extension DocumentTests {
|
||||
|
||||
XCTAssertFalse(document.body.isError)
|
||||
XCTAssertNil(document.body.errors)
|
||||
XCTAssertNotNil(document.body.primaryData)
|
||||
XCTAssertEqual(document.body.primaryData?.value.id.rawValue, "1")
|
||||
XCTAssertNotNil(document.body.primaryResource)
|
||||
XCTAssertEqual(document.body.primaryResource?.value.id.rawValue, "1")
|
||||
XCTAssertEqual(document.body.meta, TestPageMetadata(total: 70, limit: 40, offset: 10))
|
||||
XCTAssertEqual(document.body.links?.link.url, "https://website.com")
|
||||
XCTAssertEqual(document.body.links?.link.meta, NoMetadata())
|
||||
@@ -825,8 +827,8 @@ extension DocumentTests {
|
||||
|
||||
XCTAssertFalse(document.body.isError)
|
||||
XCTAssertNil(document.body.errors)
|
||||
XCTAssertNotNil(document.body.primaryData)
|
||||
XCTAssertEqual(document.body.primaryData?.value.id.rawValue, "1")
|
||||
XCTAssertNotNil(document.body.primaryResource)
|
||||
XCTAssertEqual(document.body.primaryResource?.value.id.rawValue, "1")
|
||||
XCTAssertEqual(document.body.meta, TestPageMetadata(total: 70, limit: 40, offset: 10))
|
||||
XCTAssertEqual(document.body.links?.link.url, "https://website.com")
|
||||
XCTAssertEqual(document.body.links?.link.meta, NoMetadata())
|
||||
@@ -850,8 +852,8 @@ extension DocumentTests {
|
||||
let article = Article(id: Id(rawValue: "1"), attributes: .none, relationships: .init(author: ToOneRelationship(id: Id(rawValue: "33"))), meta: .none, links: .none)
|
||||
let document = decoded(type: Document<SingleResourceBody<Poly2<Article, Author>>, NoMetadata, NoLinks, NoIncludes, NoAPIDescription, UnknownJSONAPIError>.self, data: single_document_no_includes)
|
||||
|
||||
XCTAssertEqual(document.body.primaryData?.value[Article.self], article)
|
||||
XCTAssertNil(document.body.primaryData?.value[Author.self])
|
||||
XCTAssertEqual(document.body.primaryResource?.value[Article.self], article)
|
||||
XCTAssertNil(document.body.primaryResource?.value[Author.self])
|
||||
}
|
||||
|
||||
func test_singleDocument_PolyPrimaryResource_encode() {
|
||||
@@ -862,8 +864,8 @@ extension DocumentTests {
|
||||
let article = Article(id: Id(rawValue: "1"), attributes: .none, relationships: .init(author: ToOneRelationship(id: Id(rawValue: "33"))), meta: .none, links: .none)
|
||||
let document = decoded(type: Document<SingleResourceBody<Poly2<Article, Author>>, NoMetadata, NoLinks, NoIncludes, TestAPIDescription, UnknownJSONAPIError>.self, data: single_document_no_includes_with_api_description)
|
||||
|
||||
XCTAssertEqual(document.body.primaryData?.value[Article.self], article)
|
||||
XCTAssertNil(document.body.primaryData?.value[Author.self])
|
||||
XCTAssertEqual(document.body.primaryResource?.value[Article.self], article)
|
||||
XCTAssertNil(document.body.primaryResource?.value[Author.self])
|
||||
XCTAssertEqual(document.apiDescription.version, "1.0")
|
||||
}
|
||||
|
||||
@@ -880,11 +882,11 @@ extension DocumentTests {
|
||||
|
||||
XCTAssertFalse(document.body.isError)
|
||||
XCTAssertNil(document.body.errors)
|
||||
XCTAssertNotNil(document.body.primaryData)
|
||||
XCTAssertEqual(document.body.primaryData?.values.count, 3)
|
||||
XCTAssertEqual(document.body.primaryData?.values[0].id.rawValue, "1")
|
||||
XCTAssertEqual(document.body.primaryData?.values[1].id.rawValue, "2")
|
||||
XCTAssertEqual(document.body.primaryData?.values[2].id.rawValue, "3")
|
||||
XCTAssertNotNil(document.body.primaryResource)
|
||||
XCTAssertEqual(document.body.primaryResource?.values.count, 3)
|
||||
XCTAssertEqual(document.body.primaryResource?.values[0].id.rawValue, "1")
|
||||
XCTAssertEqual(document.body.primaryResource?.values[1].id.rawValue, "2")
|
||||
XCTAssertEqual(document.body.primaryResource?.values[2].id.rawValue, "3")
|
||||
XCTAssertEqual(document.body.includes?.count, 0)
|
||||
}
|
||||
|
||||
@@ -899,11 +901,11 @@ extension DocumentTests {
|
||||
|
||||
XCTAssertFalse(document.body.isError)
|
||||
XCTAssertNil(document.body.errors)
|
||||
XCTAssertNotNil(document.body.primaryData)
|
||||
XCTAssertEqual(document.body.primaryData?.values.count, 3)
|
||||
XCTAssertEqual(document.body.primaryData?.values[0].id.rawValue, "1")
|
||||
XCTAssertEqual(document.body.primaryData?.values[1].id.rawValue, "2")
|
||||
XCTAssertEqual(document.body.primaryData?.values[2].id.rawValue, "3")
|
||||
XCTAssertNotNil(document.body.primaryResource)
|
||||
XCTAssertEqual(document.body.primaryResource?.values.count, 3)
|
||||
XCTAssertEqual(document.body.primaryResource?.values[0].id.rawValue, "1")
|
||||
XCTAssertEqual(document.body.primaryResource?.values[1].id.rawValue, "2")
|
||||
XCTAssertEqual(document.body.primaryResource?.values[2].id.rawValue, "3")
|
||||
XCTAssertEqual(document.body.includes?.count, 0)
|
||||
XCTAssertEqual(document.apiDescription.version, "1.0")
|
||||
}
|
||||
@@ -919,11 +921,11 @@ extension DocumentTests {
|
||||
|
||||
XCTAssertFalse(document.body.isError)
|
||||
XCTAssertNil(document.body.errors)
|
||||
XCTAssertNotNil(document.body.primaryData)
|
||||
XCTAssertEqual(document.body.primaryData?.values.count, 3)
|
||||
XCTAssertEqual(document.body.primaryData?.values[0].id.rawValue, "1")
|
||||
XCTAssertEqual(document.body.primaryData?.values[1].id.rawValue, "2")
|
||||
XCTAssertEqual(document.body.primaryData?.values[2].id.rawValue, "3")
|
||||
XCTAssertNotNil(document.body.primaryResource)
|
||||
XCTAssertEqual(document.body.primaryResource?.values.count, 3)
|
||||
XCTAssertEqual(document.body.primaryResource?.values[0].id.rawValue, "1")
|
||||
XCTAssertEqual(document.body.primaryResource?.values[1].id.rawValue, "2")
|
||||
XCTAssertEqual(document.body.primaryResource?.values[2].id.rawValue, "3")
|
||||
XCTAssertEqual(document.body.includes?.count, 3)
|
||||
XCTAssertEqual(document.body.includes?[Author.self].count, 3)
|
||||
XCTAssertEqual(document.body.includes?[Author.self][0].id.rawValue, "33")
|
||||
@@ -942,11 +944,11 @@ extension DocumentTests {
|
||||
|
||||
XCTAssertFalse(document.body.isError)
|
||||
XCTAssertNil(document.body.errors)
|
||||
XCTAssertNotNil(document.body.primaryData)
|
||||
XCTAssertEqual(document.body.primaryData?.values.count, 3)
|
||||
XCTAssertEqual(document.body.primaryData?.values[0].id.rawValue, "1")
|
||||
XCTAssertEqual(document.body.primaryData?.values[1].id.rawValue, "2")
|
||||
XCTAssertEqual(document.body.primaryData?.values[2].id.rawValue, "3")
|
||||
XCTAssertNotNil(document.body.primaryResource)
|
||||
XCTAssertEqual(document.body.primaryResource?.values.count, 3)
|
||||
XCTAssertEqual(document.body.primaryResource?.values[0].id.rawValue, "1")
|
||||
XCTAssertEqual(document.body.primaryResource?.values[1].id.rawValue, "2")
|
||||
XCTAssertEqual(document.body.primaryResource?.values[2].id.rawValue, "3")
|
||||
XCTAssertEqual(document.body.includes?.count, 3)
|
||||
XCTAssertEqual(document.body.includes?[Author.self].count, 3)
|
||||
XCTAssertEqual(document.body.includes?[Author.self][0].id.rawValue, "33")
|
||||
@@ -961,6 +963,55 @@ extension DocumentTests {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Merging
|
||||
extension DocumentTests {
|
||||
public func test_MergeBodyDataBasic(){
|
||||
let entity1 = Article(attributes: .none, relationships: .init(author: "2"), meta: .none, links: .none)
|
||||
let entity2 = Article(attributes: .none, relationships: .init(author: "3"), meta: .none, links: .none)
|
||||
|
||||
let bodyData1 = Document<ManyResourceBody<Article>, NoMetadata, NoLinks, NoIncludes, NoAPIDescription, UnknownJSONAPIError>.Body.Data(primary: .init(entities: [entity1]),
|
||||
includes: .none,
|
||||
meta: .none,
|
||||
links: .none)
|
||||
let bodyData2 = Document<ManyResourceBody<Article>, NoMetadata, NoLinks, NoIncludes, NoAPIDescription, UnknownJSONAPIError>.Body.Data(primary: .init(entities: [entity2]),
|
||||
includes: .none,
|
||||
meta: .none,
|
||||
links: .none)
|
||||
let combined = bodyData1.merging(bodyData2)
|
||||
|
||||
XCTAssertEqual(combined.primary.values, bodyData1.primary.values + bodyData2.primary.values)
|
||||
}
|
||||
|
||||
public func test_MergeBodyDataWithMergeFunctions() {
|
||||
let article1 = Article(attributes: .none, relationships: .init(author: "2"), meta: .none, links: .none)
|
||||
let author1 = Author(id: "2", attributes: .none, relationships: .none, meta: .none, links: .none)
|
||||
let article2 = Article(attributes: .none, relationships: .init(author: "3"), meta: .none, links: .none)
|
||||
let author2 = Author(id: "3", attributes: .none, relationships: .none, meta: .none, links: .none)
|
||||
|
||||
let bodyData1 = Document<ManyResourceBody<Article>, TestPageMetadata, NoLinks, Include1<Author>, NoAPIDescription, UnknownJSONAPIError>.Body.Data(primary: .init(entities: [article1]),
|
||||
includes: .init(values: [.init(author1)]),
|
||||
meta: .init(total: 50, limit: 5, offset: 5),
|
||||
links: .none)
|
||||
let bodyData2 = Document<ManyResourceBody<Article>, TestPageMetadata, NoLinks, Include1<Author>, NoAPIDescription, UnknownJSONAPIError>.Body.Data(primary: .init(entities: [article2]),
|
||||
includes: .init(values: [.init(author2)]),
|
||||
meta: .init(total: 60, limit: 5, offset: 5),
|
||||
links: .none)
|
||||
|
||||
let combined = bodyData1.merging(bodyData2,
|
||||
combiningMetaWith: { (meta1, meta2) in
|
||||
return .init(total: max(meta1.total, meta2.total), limit: max(meta1.limit, meta2.limit), offset: max(meta1.offset, meta2.offset))
|
||||
},
|
||||
combiningLinksWith: { _, _ in .none })
|
||||
|
||||
XCTAssertEqual(combined.meta.total, bodyData2.meta.total)
|
||||
XCTAssertEqual(combined.meta.limit, bodyData2.meta.limit)
|
||||
XCTAssertEqual(combined.meta.offset, bodyData1.meta.offset)
|
||||
|
||||
XCTAssertEqual(combined.includes, bodyData1.includes + bodyData2.includes)
|
||||
XCTAssertEqual(combined.primary, bodyData1.primary + bodyData2.primary)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Test Types
|
||||
extension DocumentTests {
|
||||
enum AuthorType: EntityDescription {
|
||||
|
||||
@@ -26,7 +26,10 @@ class EntityTests: XCTestCase {
|
||||
}
|
||||
|
||||
func test_optional_relationship_operator_access() {
|
||||
|
||||
let entity1 = TestEntity1(attributes: .none, relationships: .none, meta: .none, links: .none)
|
||||
let entity = TestEntity9(attributes: .none, relationships: .init(one: entity1.pointer, nullableOne: .init(entity: entity1, meta: .none, links: .none), optionalOne: .init(entity: entity1, meta: .none, links: .none), optionalNullableOne: nil, optionalMany: .init(entities: [entity1, entity1], meta: .none, links: .none)), meta: .none, links: .none)
|
||||
|
||||
XCTAssertEqual(entity ~> \.optionalOne, entity1.id)
|
||||
}
|
||||
|
||||
func test_toMany_relationship_operator_access() {
|
||||
@@ -39,7 +42,10 @@ class EntityTests: XCTestCase {
|
||||
}
|
||||
|
||||
func test_optionalToMany_relationship_opeartor_access() {
|
||||
let entity1 = TestEntity1(attributes: .none, relationships: .none, meta: .none, links: .none)
|
||||
let entity = TestEntity9(attributes: .none, relationships: .init(one: entity1.pointer, nullableOne: .init(entity: entity1, meta: .none, links: .none), optionalOne: nil, optionalNullableOne: nil, optionalMany: .init(entities: [entity1, entity1], meta: .none, links: .none)), meta: .none, links: .none)
|
||||
|
||||
XCTAssertEqual(entity ~> \.optionalMany, [entity1.id, entity1.id])
|
||||
}
|
||||
|
||||
func test_relationshipIds() {
|
||||
@@ -59,6 +65,12 @@ class EntityTests: XCTestCase {
|
||||
XCTAssertEqual(pointer.links.link1.url, "ok")
|
||||
}
|
||||
|
||||
func test_unidentifiedEntityAttributeAccess() {
|
||||
let entity = UnidentifiedTestEntity(attributes: .init(me: "hello"), relationships: .none, meta: .none, links: .none)
|
||||
|
||||
XCTAssertEqual(entity[\.me], "hello")
|
||||
}
|
||||
|
||||
func test_initialization() {
|
||||
let entity1 = TestEntity1(id: .init(rawValue: "wow"), attributes: .none, relationships: .none, meta: .none, links: .none)
|
||||
let entity2 = TestEntity2(id: .init(rawValue: "cool"), attributes: .none, relationships: .init(other: .init(entity: entity1)), meta: .none, links: .none)
|
||||
@@ -73,6 +85,7 @@ class EntityTests: XCTestCase {
|
||||
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)), relationships: .none, meta: .none, links: .none))
|
||||
let _ = TestEntity9(id: .init(rawValue: "9"), attributes: .none, relationships: .init(one: entity1.pointer, nullableOne: nil, optionalOne: nil, optionalNullableOne: nil, optionalMany: nil), meta: .none, links: .none)
|
||||
let _ = TestEntity9(id: .init(rawValue: "9"), attributes: .none, relationships: .init(one: entity1.pointer, nullableOne: .init(entity: nil), optionalOne: nil, optionalNullableOne: nil, optionalMany: nil), meta: .none, links: .none)
|
||||
let _ = TestEntity9(id: .init(rawValue: "9"), attributes: .none, relationships: .init(one: entity1.pointer, nullableOne: .init(id: nil), optionalOne: nil, optionalNullableOne: nil, optionalMany: nil), meta: .none, links: .none)
|
||||
let _ = TestEntity9(id: .init(rawValue: "9"), attributes: .none, relationships: .init(one: entity1.pointer, nullableOne: .init(entity: entity1, meta: .none, links: .none), optionalOne: nil, optionalNullableOne: nil, optionalMany: nil), meta: .none, links: .none)
|
||||
let _ = TestEntity9(id: .init(rawValue: "9"), attributes: .none, relationships: .init(one: entity1.pointer, nullableOne: nil, optionalOne: entity1.pointer, optionalNullableOne: nil, optionalMany: nil), meta: .none, links: .none)
|
||||
let _ = TestEntity9(id: .init(rawValue: "9"), attributes: .none, relationships: .init(one: entity1.pointer, nullableOne: nil, optionalOne: nil, optionalNullableOne: .init(entity: entity1, meta: .none, links: .none), optionalMany: nil), meta: .none, links: .none)
|
||||
@@ -88,6 +101,36 @@ class EntityTests: XCTestCase {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Identifying entity copies
|
||||
extension EntityTests {
|
||||
func test_copyIdentifiedByType() {
|
||||
let unidentifiedEntity = UnidentifiedTestEntity(attributes: .init(me: .init(value: "hello")), relationships: .none, meta: .none, links: .none)
|
||||
|
||||
let identifiedCopy = unidentifiedEntity.identified(byType: String.self)
|
||||
|
||||
XCTAssertEqual(unidentifiedEntity.attributes, identifiedCopy.attributes)
|
||||
XCTAssertEqual(unidentifiedEntity.relationships, identifiedCopy.relationships)
|
||||
}
|
||||
|
||||
func test_copyIdentifiedByValue() {
|
||||
let unidentifiedEntity = UnidentifiedTestEntity(attributes: .init(me: .init(value: "hello")), relationships: .none, meta: .none, links: .none)
|
||||
|
||||
let identifiedCopy = unidentifiedEntity.identified(by: "hello")
|
||||
|
||||
XCTAssertEqual(unidentifiedEntity.attributes, identifiedCopy.attributes)
|
||||
XCTAssertEqual(unidentifiedEntity.relationships, identifiedCopy.relationships)
|
||||
XCTAssertEqual(identifiedCopy.id, "hello")
|
||||
}
|
||||
|
||||
func test_copyWithNewId() {
|
||||
let identifiedEntity = TestEntity1(attributes: .none, relationships: .none, meta: .none, links: .none)
|
||||
|
||||
let identifiedCopy = identifiedEntity.withNewIdentifier()
|
||||
|
||||
XCTAssertNotEqual(identifiedEntity.id, identifiedCopy.id)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Encode/Decode
|
||||
extension EntityTests {
|
||||
|
||||
@@ -98,6 +141,8 @@ extension EntityTests {
|
||||
XCTAssert(type(of: entity.relationships) == NoRelationships.self)
|
||||
XCTAssert(type(of: entity.attributes) == NoAttributes.self)
|
||||
XCTAssertNoThrow(try TestEntity1.check(entity))
|
||||
|
||||
testEncoded(entity: entity)
|
||||
}
|
||||
|
||||
func test_EntityNoRelationshipsNoAttributes_encode() {
|
||||
@@ -113,6 +158,8 @@ extension EntityTests {
|
||||
|
||||
XCTAssertEqual(entity[\.floater], 123.321)
|
||||
XCTAssertNoThrow(try TestEntity5.check(entity))
|
||||
|
||||
testEncoded(entity: entity)
|
||||
}
|
||||
|
||||
func test_EntityNoRelationshipsSomeAttributes_encode() {
|
||||
@@ -128,6 +175,8 @@ extension EntityTests {
|
||||
|
||||
XCTAssertEqual((entity ~> \.others).map { $0.rawValue }, ["364B3B69-4DF1-467F-B52E-B0C9E44F666E"])
|
||||
XCTAssertNoThrow(try TestEntity3.check(entity))
|
||||
|
||||
testEncoded(entity: entity)
|
||||
}
|
||||
|
||||
func test_EntitySomeRelationshipsNoAttributes_encode() {
|
||||
@@ -143,6 +192,8 @@ extension EntityTests {
|
||||
XCTAssertEqual(entity[\.number], 992299)
|
||||
XCTAssertEqual((entity ~> \.other).rawValue, "2DF03B69-4B0A-467F-B52E-B0C9E44FCECF")
|
||||
XCTAssertNoThrow(try TestEntity4.check(entity))
|
||||
|
||||
testEncoded(entity: entity)
|
||||
}
|
||||
|
||||
func test_EntitySomeRelationshipsSomeAttributes_encode() {
|
||||
@@ -162,6 +213,8 @@ extension EntityTests {
|
||||
XCTAssertNil(entity[\.maybeHere])
|
||||
XCTAssertEqual(entity[\.maybeNull], "World")
|
||||
XCTAssertNoThrow(try TestEntity6.check(entity))
|
||||
|
||||
testEncoded(entity: entity)
|
||||
}
|
||||
|
||||
func test_entityOneOmittedAttribute_encode() {
|
||||
@@ -177,6 +230,8 @@ extension EntityTests {
|
||||
XCTAssertEqual(entity[\.maybeHere], "World")
|
||||
XCTAssertNil(entity[\.maybeNull])
|
||||
XCTAssertNoThrow(try TestEntity6.check(entity))
|
||||
|
||||
testEncoded(entity: entity)
|
||||
}
|
||||
|
||||
func test_entityOneNullAttribute_encode() {
|
||||
@@ -192,6 +247,8 @@ extension EntityTests {
|
||||
XCTAssertEqual(entity[\.maybeHere], "World")
|
||||
XCTAssertEqual(entity[\.maybeNull], "!")
|
||||
XCTAssertNoThrow(try TestEntity6.check(entity))
|
||||
|
||||
testEncoded(entity: entity)
|
||||
}
|
||||
|
||||
func test_entityAllAttribute_encode() {
|
||||
@@ -208,7 +265,7 @@ extension EntityTests {
|
||||
XCTAssertNil(entity[\.maybeNull])
|
||||
XCTAssertNoThrow(try TestEntity6.check(entity))
|
||||
|
||||
print(encodable: entity)
|
||||
testEncoded(entity: entity)
|
||||
}
|
||||
|
||||
func test_entityOneNullAndOneOmittedAttribute_encode() {
|
||||
@@ -229,7 +286,7 @@ extension EntityTests {
|
||||
XCTAssertNil(entity[\.maybeHereMaybeNull])
|
||||
XCTAssertNoThrow(try TestEntity7.check(entity))
|
||||
|
||||
print(encodable: entity)
|
||||
testEncoded(entity: entity)
|
||||
}
|
||||
|
||||
func test_NullOptionalNullableAttribute_encode() {
|
||||
@@ -244,6 +301,8 @@ extension EntityTests {
|
||||
XCTAssertEqual(entity[\.here], "Hello")
|
||||
XCTAssertEqual(entity[\.maybeHereMaybeNull], "World")
|
||||
XCTAssertNoThrow(try TestEntity7.check(entity))
|
||||
|
||||
testEncoded(entity: entity)
|
||||
}
|
||||
|
||||
func test_NonNullOptionalNullableAttribute_encode() {
|
||||
@@ -265,6 +324,8 @@ extension EntityTests {
|
||||
XCTAssertEqual(entity[\.doubleFromInt], 22.0)
|
||||
XCTAssertEqual(entity[\.nullToString], "nil")
|
||||
XCTAssertNoThrow(try TestEntity8.check(entity))
|
||||
|
||||
testEncoded(entity: entity)
|
||||
}
|
||||
|
||||
func test_IntToString_encode() {
|
||||
@@ -299,6 +360,8 @@ extension EntityTests {
|
||||
XCTAssertNil(entity ~> \.optionalOne)
|
||||
XCTAssertEqual((entity ~> \.optionalNullableOne)?.rawValue, "1229")
|
||||
XCTAssertNoThrow(try TestEntity9.check(entity))
|
||||
|
||||
testEncoded(entity: entity)
|
||||
}
|
||||
|
||||
func test_nullableRelationshipNotNullOrOmitted_encode() {
|
||||
@@ -314,6 +377,8 @@ extension EntityTests {
|
||||
XCTAssertEqual((entity ~> \.one).rawValue, "4459")
|
||||
XCTAssertNil(entity ~> \.optionalNullableOne)
|
||||
XCTAssertNoThrow(try TestEntity9.check(entity))
|
||||
|
||||
testEncoded(entity: entity)
|
||||
}
|
||||
|
||||
func test_nullableRelationshipNotNull_encode() {
|
||||
@@ -329,6 +394,8 @@ extension EntityTests {
|
||||
XCTAssertEqual((entity ~> \.one).rawValue, "4459")
|
||||
XCTAssertNil(entity ~> \.optionalNullableOne)
|
||||
XCTAssertNoThrow(try TestEntity9.check(entity))
|
||||
|
||||
testEncoded(entity: entity)
|
||||
}
|
||||
|
||||
func test_optionalNullableRelationshipNulled_encode() {
|
||||
@@ -344,6 +411,8 @@ extension EntityTests {
|
||||
XCTAssertEqual((entity ~> \.one).rawValue, "4452")
|
||||
XCTAssertNil(entity ~> \.optionalNullableOne)
|
||||
XCTAssertNoThrow(try TestEntity9.check(entity))
|
||||
|
||||
testEncoded(entity: entity)
|
||||
}
|
||||
|
||||
func test_nullableRelationshipIsNull_encode() {
|
||||
@@ -360,6 +429,8 @@ extension EntityTests {
|
||||
XCTAssertEqual((entity ~> \.optionalMany)?[0].rawValue, "332223")
|
||||
XCTAssertNil(entity ~> \.optionalNullableOne)
|
||||
XCTAssertNoThrow(try TestEntity9.check(entity))
|
||||
|
||||
testEncoded(entity: entity)
|
||||
}
|
||||
|
||||
func test_optionalToManyIsNotOmitted_encode() {
|
||||
@@ -377,6 +448,8 @@ extension EntityTests {
|
||||
|
||||
XCTAssertEqual((entity ~> \.selfRef).rawValue, "1")
|
||||
XCTAssertNoThrow(try TestEntity10.check(entity))
|
||||
|
||||
testEncoded(entity: entity)
|
||||
}
|
||||
|
||||
func test_RleationshipsOfSameType_encode() {
|
||||
@@ -395,6 +468,8 @@ extension EntityTests {
|
||||
XCTAssertNil(entity[\.me])
|
||||
XCTAssertEqual(entity.id, .unidentified)
|
||||
XCTAssertNoThrow(try UnidentifiedTestEntity.check(entity))
|
||||
|
||||
testEncoded(entity: entity)
|
||||
}
|
||||
|
||||
func test_UnidentifiedEntity_encode() {
|
||||
@@ -409,6 +484,8 @@ extension EntityTests {
|
||||
XCTAssertEqual(entity[\.me], "unknown")
|
||||
XCTAssertEqual(entity.id, .unidentified)
|
||||
XCTAssertNoThrow(try UnidentifiedTestEntity.check(entity))
|
||||
|
||||
testEncoded(entity: entity)
|
||||
}
|
||||
|
||||
func test_UnidentifiedEntityWithAttributes_encode() {
|
||||
@@ -429,6 +506,8 @@ extension EntityTests {
|
||||
XCTAssertEqual(entity.meta.x, "world")
|
||||
XCTAssertEqual(entity.meta.y, 5)
|
||||
XCTAssertNoThrow(try UnidentifiedTestEntityWithMeta.check(entity))
|
||||
|
||||
testEncoded(entity: entity)
|
||||
}
|
||||
|
||||
func test_UnidentifiedEntityWithAttributesAndMeta_encode() {
|
||||
@@ -444,6 +523,8 @@ extension EntityTests {
|
||||
XCTAssertEqual(entity.id, .unidentified)
|
||||
XCTAssertEqual(entity.links.link1, .init(url: "https://image.com/image.png"))
|
||||
XCTAssertNoThrow(try UnidentifiedTestEntityWithLinks.check(entity))
|
||||
|
||||
testEncoded(entity: entity)
|
||||
}
|
||||
|
||||
func test_UnidentifiedEntityWithAttributesAndLinks_encode() {
|
||||
@@ -461,6 +542,8 @@ extension EntityTests {
|
||||
XCTAssertEqual(entity.meta.y, 5)
|
||||
XCTAssertEqual(entity.links.link1, .init(url: "https://image.com/image.png"))
|
||||
XCTAssertNoThrow(try UnidentifiedTestEntityWithMetaAndLinks.check(entity))
|
||||
|
||||
testEncoded(entity: entity)
|
||||
}
|
||||
|
||||
func test_UnidentifiedEntityWithAttributesAndMetaAndLinks_encode() {
|
||||
@@ -478,6 +561,8 @@ extension EntityTests {
|
||||
XCTAssertEqual(entity.meta.x, "world")
|
||||
XCTAssertEqual(entity.meta.y, 5)
|
||||
XCTAssertNoThrow(try TestEntity4WithMeta.check(entity))
|
||||
|
||||
testEncoded(entity: entity)
|
||||
}
|
||||
|
||||
func test_EntitySomeRelationshipsSomeAttributesWithMeta_encode() {
|
||||
@@ -494,6 +579,8 @@ extension EntityTests {
|
||||
XCTAssertEqual((entity ~> \.other).rawValue, "2DF03B69-4B0A-467F-B52E-B0C9E44FCECF")
|
||||
XCTAssertEqual(entity.links.link1, .init(url: "https://image.com/image.png"))
|
||||
XCTAssertNoThrow(try TestEntity4WithLinks.check(entity))
|
||||
|
||||
testEncoded(entity: entity)
|
||||
}
|
||||
|
||||
func test_EntitySomeRelationshipsSomeAttributesWithLinks_encode() {
|
||||
@@ -512,6 +599,8 @@ extension EntityTests {
|
||||
XCTAssertEqual(entity.meta.y, 5)
|
||||
XCTAssertEqual(entity.links.link1, .init(url: "https://image.com/image.png"))
|
||||
XCTAssertNoThrow(try TestEntity4WithMetaAndLinks.check(entity))
|
||||
|
||||
testEncoded(entity: entity)
|
||||
}
|
||||
|
||||
func test_EntitySomeRelationshipsSomeAttributesWithMetaAndLinks_encode() {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
|
||||
import XCTest
|
||||
import JSONAPI
|
||||
@testable import JSONAPI
|
||||
|
||||
class IncludedTests: XCTestCase {
|
||||
|
||||
@@ -139,6 +139,57 @@ class IncludedTests: XCTestCase {
|
||||
test_DecodeEncodeEquality(type: Includes<Include7<TestEntity, TestEntity2, TestEntity3, TestEntity4, TestEntity5, TestEntity6, TestEntity7>>.self,
|
||||
data: seven_different_type_includes)
|
||||
}
|
||||
|
||||
func test_EightDifferentIncludes() {
|
||||
let includes = decoded(type: Includes<Include8<TestEntity, TestEntity2, TestEntity3, TestEntity4, TestEntity5, TestEntity6, TestEntity7, TestEntity8>>.self,
|
||||
data: eight_different_type_includes)
|
||||
|
||||
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)
|
||||
XCTAssertEqual(includes[TestEntity7.self].count, 1)
|
||||
XCTAssertEqual(includes[TestEntity8.self].count, 1)
|
||||
}
|
||||
|
||||
func test_EightDifferentIncludes_encode() {
|
||||
test_DecodeEncodeEquality(type: Includes<Include8<TestEntity, TestEntity2, TestEntity3, TestEntity4, TestEntity5, TestEntity6, TestEntity7, TestEntity8>>.self,
|
||||
data: eight_different_type_includes)
|
||||
}
|
||||
|
||||
func test_NineDifferentIncludes() {
|
||||
let includes = decoded(type: Includes<Include9<TestEntity, TestEntity2, TestEntity3, TestEntity4, TestEntity5, TestEntity6, TestEntity7, TestEntity8, TestEntity9>>.self,
|
||||
data: nine_different_type_includes)
|
||||
|
||||
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)
|
||||
XCTAssertEqual(includes[TestEntity7.self].count, 1)
|
||||
XCTAssertEqual(includes[TestEntity8.self].count, 1)
|
||||
XCTAssertEqual(includes[TestEntity9.self].count, 1)
|
||||
}
|
||||
|
||||
func test_NineDifferentIncludes_encode() {
|
||||
test_DecodeEncodeEquality(type: Includes<Include9<TestEntity, TestEntity2, TestEntity3, TestEntity4, TestEntity5, TestEntity6, TestEntity7, TestEntity8, TestEntity9>>.self,
|
||||
data: nine_different_type_includes)
|
||||
}
|
||||
}
|
||||
|
||||
extension IncludedTests {
|
||||
func test_appending() {
|
||||
let include1 = Includes<Include2<TestEntity8, TestEntity9>>(values: [.init(TestEntity8(attributes: .none, relationships: .none, meta: .none, links: .none)), .init(TestEntity9(attributes: .none, relationships: .none, meta: .none, links: .none)), .init(TestEntity8(attributes: .none, relationships: .none, meta: .none, links: .none))])
|
||||
|
||||
let include2 = Includes<Include2<TestEntity8, TestEntity9>>(values: [.init(TestEntity8(attributes: .none, relationships: .none, meta: .none, links: .none)), .init(TestEntity9(attributes: .none, relationships: .none, meta: .none, links: .none)), .init(TestEntity8(attributes: .none, relationships: .none, meta: .none, links: .none))])
|
||||
|
||||
let combined = include1 + include2
|
||||
|
||||
XCTAssertEqual(combined.values, include1.values + include2.values)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Test types
|
||||
@@ -232,4 +283,26 @@ extension IncludedTests {
|
||||
}
|
||||
|
||||
typealias TestEntity7 = BasicEntity<TestEntityType7>
|
||||
|
||||
enum TestEntityType8: EntityDescription {
|
||||
|
||||
typealias Attributes = NoAttributes
|
||||
|
||||
public static var type: String { return "test_entity8" }
|
||||
|
||||
typealias Relationships = NoRelationships
|
||||
}
|
||||
|
||||
typealias TestEntity8 = BasicEntity<TestEntityType8>
|
||||
|
||||
enum TestEntityType9: EntityDescription {
|
||||
|
||||
typealias Attributes = NoAttributes
|
||||
|
||||
public static var type: String { return "test_entity9" }
|
||||
|
||||
typealias Relationships = NoRelationships
|
||||
}
|
||||
|
||||
typealias TestEntity9 = BasicEntity<TestEntityType9>
|
||||
}
|
||||
|
||||
@@ -354,3 +354,161 @@ let seven_different_type_includes = """
|
||||
}
|
||||
]
|
||||
""".data(using: .utf8)!
|
||||
|
||||
let eight_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"
|
||||
},
|
||||
{
|
||||
"type": "test_entity7",
|
||||
"id": "364B3B69-4DF1-222F-B52E-B0C9E44F666E"
|
||||
},
|
||||
{
|
||||
"type": "test_entity8",
|
||||
"id": "364B3B69-4DF1-222F-B52E-B0C9E44F266F"
|
||||
}
|
||||
]
|
||||
""".data(using: .utf8)!
|
||||
|
||||
let nine_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"
|
||||
},
|
||||
{
|
||||
"type": "test_entity7",
|
||||
"id": "364B3B69-4DF1-222F-B52E-B0C9E44F666E"
|
||||
},
|
||||
{
|
||||
"type": "test_entity8",
|
||||
"id": "364B3B69-4DF1-222F-B52E-B0C9E44F266F"
|
||||
},
|
||||
{
|
||||
"type": "test_entity9",
|
||||
"id": "364B3B69-4DF1-218F-B52E-B0C9E44F2661"
|
||||
}
|
||||
]
|
||||
""".data(using: .utf8)!
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user