diff --git a/JSONAPI.playground/Pages/Usage.xcplaygroundpage/Contents.swift b/JSONAPI.playground/Pages/Usage.xcplaygroundpage/Contents.swift index f05dac4..bd94b45 100644 --- a/JSONAPI.playground/Pages/Usage.xcplaygroundpage/Contents.swift +++ b/JSONAPI.playground/Pages/Usage.xcplaygroundpage/Contents.swift @@ -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, 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] diff --git a/JSONAPI.playground/Sources/Entities.swift b/JSONAPI.playground/Sources/Entities.swift index 5f0f24f..d221a01 100644 --- a/JSONAPI.playground/Sources/Entities.swift +++ b/JSONAPI.playground/Sources/Entities.swift @@ -91,6 +91,34 @@ public enum DogDescription: EntityDescription { public typealias Dog = ExampleEntity +public enum AlternativeDogDescription: EntityDescription { + + public static var type: String { return "dogs" } + + public struct Attributes: JSONAPI.Attributes { + public let name: Attribute + + public init(name: Attribute) { + self.name = name + } + } + + public struct Relationships: JSONAPI.Relationships { + public let human: ToOne + + public init(human: ToOne) { + self.human = human + } + + // define custom key mapping: + enum CodingKeys: String, CodingKey { + case human = "owner" + } + } +} + +public typealias AlternativeDog = ExampleEntity + 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) diff --git a/README.md b/README.md index 6b4f516..eeeac35 100644 --- a/README.md +++ b/README.md @@ -399,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 + } + + 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 + + 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 + public let property2: Attribute + public let property3: Attribute + + public let weirdThing: Attribute + + 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 = "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. diff --git a/Sources/JSONAPI/Resource/Attribute.swift b/Sources/JSONAPI/Resource/Attribute.swift index 823a8af..2b351fd 100644 --- a/Sources/JSONAPI/Resource/Attribute.swift +++ b/Sources/JSONAPI/Resource/Attribute.swift @@ -84,3 +84,11 @@ extension TransformedAttribute { try container.encode(rawValue) } } + +// MARK: Attribute decoding and encoding defaults + +extension AttributeType { + public static func defaultDecoding(from container: Container, forKey key: Container.Key) throws -> Self { + return try container.decode(Self.self, forKey: key) + } +} diff --git a/Sources/JSONAPI/Resource/Entity.swift b/Sources/JSONAPI/Resource/Entity.swift index 2d5d2a3..b28de51 100644 --- a/Sources/JSONAPI/Resource/Entity.swift +++ b/Sources/JSONAPI/Resource/Entity.swift @@ -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. @@ -556,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) diff --git a/Tests/JSONAPITests/Custom Attributes Tests/CustomAttributesTests.swift b/Tests/JSONAPITests/Custom Attributes Tests/CustomAttributesTests.swift new file mode 100644 index 0000000..9454069 --- /dev/null +++ b/Tests/JSONAPITests/Custom Attributes Tests/CustomAttributesTests.swift @@ -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 + public let name: Attribute + + private enum CodingKeys: String, CodingKey { + case firstName + case lastName + } + } + + public typealias Relationships = NoRelationships + } + + typealias CustomAttributeEntity = BasicEntity + + enum CustomKeysEntityDescription: EntityDescription { + public static var type: String { return "test1" } + + public struct Attributes: JSONAPI.Attributes { + public let firstNameSilly: Attribute + public let lastNameSilly: Attribute + + enum CodingKeys: String, CodingKey { + case firstNameSilly = "firstName" + case lastNameSilly = "lastName" + } + } + + public typealias Relationships = NoRelationships + } + + typealias CustomKeysEntity = BasicEntity +} + +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)! diff --git a/Tests/JSONAPITests/JSONAPITestLib/EntityCheckTests.swift b/Tests/JSONAPITests/JSONAPITestLib/EntityCheckTests.swift index 0d2b8bc..5796669 100644 --- a/Tests/JSONAPITests/JSONAPITestLib/EntityCheckTests.swift +++ b/Tests/JSONAPITests/JSONAPITestLib/EntityCheckTests.swift @@ -61,7 +61,7 @@ extension EntityCheckTests { enum EnumAttributesDescription: EntityDescription { public static var type: String { return "hello" } - public enum Attributes: Codable, Equatable { + public enum Attributes: JSONAPI.Attributes { case hello public init(from decoder: Decoder) throws { @@ -82,7 +82,7 @@ extension EntityCheckTests { public typealias Attributes = NoAttributes - public enum Relationships: Codable, Equatable { + public enum Relationships: JSONAPI.Relationships { case hello public init(from decoder: Decoder) throws { diff --git a/Tests/JSONAPITests/XCTestManifests.swift b/Tests/JSONAPITests/XCTestManifests.swift index 378936e..33fef35 100644 --- a/Tests/JSONAPITests/XCTestManifests.swift +++ b/Tests/JSONAPITests/XCTestManifests.swift @@ -73,6 +73,15 @@ extension ComputedPropertiesTests { ] } +extension CustomAttributesTests { + static let __allTests = [ + ("test_customDecode", test_customDecode), + ("test_customEncode", test_customEncode), + ("test_customKeysDecode", test_customKeysDecode), + ("test_customKeysEncode", test_customKeysEncode), + ] +} + extension DocumentTests { static let __allTests = [ ("test_errorDocumentFailsWithNoAPIDescription", test_errorDocumentFailsWithNoAPIDescription), @@ -399,6 +408,7 @@ public func __allTests() -> [XCTestCaseEntry] { testCase(Attribute_FunctorTests.__allTests), testCase(Attribute_LiteralTests.__allTests), testCase(ComputedPropertiesTests.__allTests), + testCase(CustomAttributesTests.__allTests), testCase(DocumentTests.__allTests), testCase(EntityCheckTests.__allTests), testCase(EntityTests.__allTests),