Compare commits

...

2 Commits

Author SHA1 Message Date
Mathew Polzin d5a24c4adb Split Attribute out into its own Type (no longer just a type alias to TransformedAttribute) 2018-12-29 23:07:14 -08:00
Mathew Polzin 669d5d1342 Add ability to merge Document.Body.Data values 2018-12-28 19:11:24 -08:00
17 changed files with 291 additions and 45 deletions
+3 -3
View File
@@ -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)
}
}
@@ -121,11 +121,11 @@ 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)
}
}
@@ -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
+20
View File
@@ -189,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
+10
View File
@@ -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))"
+13 -1
View File
@@ -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))
}
}
+66 -13
View File
@@ -6,9 +6,14 @@
//
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 {
let rawValue: RawValue
@@ -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 {
@@ -85,6 +113,31 @@ extension TransformedAttribute {
}
}
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 {
+3 -3
View File
@@ -433,21 +433,21 @@ 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 }
@@ -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...) {
+6 -1
View File
@@ -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() {
@@ -73,6 +73,7 @@ extension DocumentTests {
XCTAssertTrue(document.body.isError)
XCTAssertEqual(document.body.meta, NoMetadata())
XCTAssertNil(document.body.data)
XCTAssertNil(document.body.primaryResource)
XCTAssertNil(document.body.includes)
@@ -525,6 +526,7 @@ extension DocumentTests {
XCTAssertNil(document.body.errors)
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())
}
@@ -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 {
+13 -1
View File
@@ -1,6 +1,6 @@
import XCTest
import JSONAPI
@testable import JSONAPI
class IncludedTests: XCTestCase {
@@ -180,6 +180,18 @@ class IncludedTests: XCTestCase {
}
}
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
extension IncludedTests {
enum TestEntityType: EntityDescription {
@@ -25,7 +25,10 @@ class ResourceBodyTests: XCTestCase {
data: single_resource_body)
XCTAssertEqual(body.value, Article(id: Id<String, Article>(rawValue: "1"),
attributes: ArticleType.Attributes(title: try! .init(rawValue: "JSON:API paints my bikeshed!")), relationships: .none, meta: .none, links: .none))
attributes: ArticleType.Attributes(title: .init(value: "JSON:API paints my bikeshed!")),
relationships: .none,
meta: .none,
links: .none))
}
func test_singleResourceBody_encode() {
@@ -38,9 +41,21 @@ class ResourceBodyTests: XCTestCase {
data: many_resource_body)
XCTAssertEqual(body.values, [
Article(id: .init(rawValue: "1"), attributes: try! .init(title: .init(rawValue: "JSON:API paints my bikeshed!")), relationships: .none, meta: .none, links: .none),
Article(id: .init(rawValue: "2"), attributes: try! .init(title: .init(rawValue: "Sick")), relationships: .none, meta: .none, links: .none),
Article(id: .init(rawValue: "3"), attributes: try! .init(title: .init(rawValue: "Hello World")), relationships: .none, meta: .none, links: .none)
Article(id: .init(rawValue: "1"),
attributes: .init(title: .init(value: "JSON:API paints my bikeshed!")),
relationships: .none,
meta: .none,
links: .none),
Article(id: .init(rawValue: "2"),
attributes: .init(title: .init(value: "Sick")),
relationships: .none,
meta: .none,
links: .none),
Article(id: .init(rawValue: "3"),
attributes: .init(title: .init(value: "Hello World")),
relationships: .none,
meta: .none,
links: .none)
])
}
@@ -61,6 +76,31 @@ class ResourceBodyTests: XCTestCase {
data: many_resource_body_empty)
}
func test_manyResourceBodyMerge() {
let body1 = ManyResourceBody(entities: [
Article(attributes: .init(title: "hello"),
relationships: .none,
meta: .none,
links: .none),
Article(attributes: .init(title: "world"),
relationships: .none,
meta: .none,
links: .none)
])
let body2 = ManyResourceBody(entities: [
Article(attributes: .init(title: "once more"),
relationships: .none,
meta: .none,
links: .none)
])
let combined = body1 + body2
XCTAssertEqual(combined.values.count, 3)
XCTAssertEqual(combined.values, body1.values + body2.values)
}
enum ArticleType: EntityDescription {
public static var type: String { return "articles" }
@@ -10,7 +10,7 @@ import XCTest
@testable import JSONAPI
import JSONAPITestLib
private struct Wrapper<Value: Equatable & Codable, Transform: Transformer>: Codable where Value == Transform.From {
private struct TransformedWrapper<Value: Equatable & Codable, Transform: Transformer>: Codable where Value == Transform.From {
let x: TransformedAttribute<Value, Transform>
init(x: TransformedAttribute<Value, Transform>) {
@@ -18,10 +18,18 @@ private struct Wrapper<Value: Equatable & Codable, Transform: Transformer>: Coda
}
}
private struct Wrapper<Value: Equatable & Codable>: Codable {
let x: Attribute<Value>
init(x: Attribute<Value>) {
self.x = x
}
}
/// This function attempts to just cast to the type, so it only works
/// for Attributes of primitive types (primitive to JSON).
func testEncodedPrimitive<Value: Equatable & Codable, Transform: Transformer>(attribute: TransformedAttribute<Value, Transform>) {
let encodedAttributeData = encoded(value: Wrapper<Value, Transform>(x: attribute))
let encodedAttributeData = encoded(value: TransformedWrapper<Value, Transform>(x: attribute))
let wrapperObject = try! JSONSerialization.jsonObject(with: encodedAttributeData, options: []) as! [String: Any]
let jsonObject = wrapperObject["x"]
@@ -32,3 +40,18 @@ func testEncodedPrimitive<Value: Equatable & Codable, Transform: Transformer>(at
XCTAssertEqual(attribute.rawValue, jsonAttribute)
}
/// This function attempts to just cast to the type, so it only works
/// for Attributes of primitive types (primitive to JSON).
func testEncodedPrimitive<Value: Equatable & Codable>(attribute: Attribute<Value>) {
let encodedAttributeData = encoded(value: Wrapper<Value>(x: attribute))
let wrapperObject = try! JSONSerialization.jsonObject(with: encodedAttributeData, options: []) as! [String: Any]
let jsonObject = wrapperObject["x"]
guard let jsonAttribute = jsonObject as? Value else {
XCTFail("Attribute did not encode to the correct type")
return
}
XCTAssertEqual(attribute.value, jsonAttribute)
}
+5 -2
View File
@@ -13,8 +13,7 @@ extension APIDescriptionTests {
extension AttributeTests {
static let __allTests = [
("test_AttributeIsTransformedAttribute", test_AttributeIsTransformedAttribute),
("test_AttributeNonThrowingConstructor", test_AttributeNonThrowingConstructor),
("test_AttributeConstructor", test_AttributeConstructor),
("test_EncodedPrimitives", test_EncodedPrimitives),
("test_NullableIsEqualToNonNullableIfNotNil", test_NullableIsEqualToNonNullableIfNotNil),
("test_NullableIsNullIfNil", test_NullableIsNullIfNil),
@@ -97,6 +96,8 @@ extension DocumentTests {
("test_manyDocumentSomeIncludes_encode", test_manyDocumentSomeIncludes_encode),
("test_manyDocumentSomeIncludesWithAPIDescription", test_manyDocumentSomeIncludesWithAPIDescription),
("test_manyDocumentSomeIncludesWithAPIDescription_encode", test_manyDocumentSomeIncludesWithAPIDescription_encode),
("test_MergeBodyDataBasic", test_MergeBodyDataBasic),
("test_MergeBodyDataWithMergeFunctions", test_MergeBodyDataWithMergeFunctions),
("test_metaDataDocument", test_metaDataDocument),
("test_metaDataDocument_encode", test_metaDataDocument_encode),
("test_metaDataDocumentFailsIfMissingAPIDescription", test_metaDataDocumentFailsIfMissingAPIDescription),
@@ -269,6 +270,7 @@ extension Id_LiteralTests {
extension IncludedTests {
static let __allTests = [
("test_appending", test_appending),
("test_EightDifferentIncludes", test_EightDifferentIncludes),
("test_EightDifferentIncludes_encode", test_EightDifferentIncludes_encode),
("test_FiveDifferentIncludes", test_FiveDifferentIncludes),
@@ -405,6 +407,7 @@ extension ResourceBodyTests {
("test_manyResourceBody_encode", test_manyResourceBody_encode),
("test_manyResourceBodyEmpty", test_manyResourceBodyEmpty),
("test_manyResourceBodyEmpty_encode", test_manyResourceBodyEmpty_encode),
("test_manyResourceBodyMerge", test_manyResourceBodyMerge),
("test_singleResourceBody", test_singleResourceBody),
("test_singleResourceBody_encode", test_singleResourceBody_encode),
]