From 1d6e5d3810cc48644c9c5ae00a70f40def3a3c58 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Wed, 2 Jan 2019 22:49:38 -0800 Subject: [PATCH] Added Meta-Attribute support and documentation --- README.md | 46 +++++++++++++++++++++ Sources/JSONAPI/Resource/Entity.swift | 9 ++++ Tests/JSONAPITests/Entity/EntityTests.swift | 36 ++++++++++++++++ Tests/JSONAPITests/XCTestManifests.swift | 1 + 4 files changed, 92 insertions(+) diff --git a/README.md b/README.md index c83401f..bb0ec79 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,7 @@ See the JSON API Spec here: https://jsonapi.org/format/ - [`JSONAPI.RawIdType`](#jsonapirawidtype) - [Custom Attribute or Relationship Key Mapping](#custom-attribute-or-relationship-key-mapping) - [Custom Attribute Encode/Decode](#custom-attribute-encodedecode) + - [Meta-attributes](#meta-attributes) - [Example](#example) - [Preamble (Setup shared by server and client)](#preamble-setup-shared-by-server-and-client) - [Server Pseudo-example](#server-pseudo-example) @@ -526,6 +527,51 @@ extension EntityDescription1.Attributes { } ``` +### Meta-attributes +This advanced feature may not ever be useful, but if you find yourself in the situation of dealing with an API that does not 100% follow the **SPEC** then you might find meta-attributes are just the thing to make your entities more natural to work with. + +Suppose, for example, you are presented with the unfortunate situation where a piece of information you need is only available as part of the `Id` of an entity. Perhaps a user's `Id` is formatted "{integer}-{createdAt}" where "createdAt" is the unix timestamp when the user account was created. The following `UserDescription` will expose what you need as an attribute. Realistically, this code is still terrible for its error handling. Using a `Result` type and/or invariants would clean things up substantially. + +``` +enum UserDescription: EntityDescription { + public static var jsonType: String { return "users" } + + struct Attributes: JSONAPI.Attributes { + var createdAt: (User) -> Date { + return { user in + let components = user.id.rawValue.split(separator: "-") + + guard components.count == 2 else { + assertionFailure() + return Date() + } + + let timestamp = TimeInterval(components[1]) + + guard let date = timestamp.map(Date.init(timeIntervalSince1970:)) else { + assertionFailure() + return Date() + } + + return date + } + } + } + + typealias Relationships = NoRelationships +} + +typealias User = JSONAPI.Entity +``` + +Given a value `user` of the above entity type, you can access the `createdAt` attribute just like you would any other: + +``` +let createdAt = user[\.createdAt] +``` + +This works because `createdAt` is defined in the form: `var {name}: ({Entity}) -> {Value}` where `{Entity}` is the `JSONAPI.Entity` described by the `EntityDescription` containing the meta-attribute. + ## Example The following serves as a sort of pseudo-example. It skips server/client implementation details not related to JSON:API but still gives a more complete picture of what an implementation using this framework might look like. You can play with this example code in the Playground provided with this repo. diff --git a/Sources/JSONAPI/Resource/Entity.swift b/Sources/JSONAPI/Resource/Entity.swift index fedb19a..6d6da14 100644 --- a/Sources/JSONAPI/Resource/Entity.swift +++ b/Sources/JSONAPI/Resource/Entity.swift @@ -471,6 +471,15 @@ public extension EntityProxy { } } +// MARK: Meta-Attribute Access +public extension EntityProxy { + /// Access an attribute requiring a transformation on the RawValue _and_ + /// a secondary transformation on this entity (self). + subscript(_ path: KeyPath T>) -> T { + return attributes[keyPath: path](self) + } +} + // MARK: Relationship Access public extension EntityProxy { /// Access to an Id of a `ToOneRelationship`. diff --git a/Tests/JSONAPITests/Entity/EntityTests.swift b/Tests/JSONAPITests/Entity/EntityTests.swift index 68a3d1f..ec41b31 100644 --- a/Tests/JSONAPITests/Entity/EntityTests.swift +++ b/Tests/JSONAPITests/Entity/EntityTests.swift @@ -609,6 +609,26 @@ extension EntityTests { } } +// MARK: With a Meta Attribute + +extension EntityTests { + func test_MetaEntityAccessWorks() { + let entity1 = TestEntityWithMetaAttribute(id: "even", + attributes: .init(), + relationships: .none, + meta: .none, + links: .none) + let entity2 = TestEntityWithMetaAttribute(id: "odd", + attributes: .init(), + relationships: .none, + meta: .none, + links: .none) + + XCTAssertEqual(entity1[\.metaAttribute], true) + XCTAssertEqual(entity2[\.metaAttribute], false) + } +} + // MARK: - Test Types extension EntityTests { @@ -790,6 +810,22 @@ extension EntityTests { typealias UnidentifiedTestEntityWithMetaAndLinks = NewEntity + enum TestEntityWithMetaAttributeDescription: EntityDescription { + public static var jsonType: String { return "meta_attribute_entity" } + + struct Attributes: JSONAPI.Attributes { + var metaAttribute: (TestEntityWithMetaAttribute) -> Bool { + return { entity in + (entity.id.rawValue.count % 2) == 0 + } + } + } + + typealias Relationships = NoRelationships + } + + typealias TestEntityWithMetaAttribute = BasicEntity + enum IntToString: Transformer { public static func transform(_ from: Int) -> String { return String(from) diff --git a/Tests/JSONAPITests/XCTestManifests.swift b/Tests/JSONAPITests/XCTestManifests.swift index 9714887..12b18ca 100644 --- a/Tests/JSONAPITests/XCTestManifests.swift +++ b/Tests/JSONAPITests/XCTestManifests.swift @@ -228,6 +228,7 @@ extension EntityTests { ("test_IntOver10_success", test_IntOver10_success), ("test_IntToString", test_IntToString), ("test_IntToString_encode", test_IntToString_encode), + ("test_MetaEntityAccessWorks", test_MetaEntityAccessWorks), ("test_NonNullOptionalNullableAttribute", test_NonNullOptionalNullableAttribute), ("test_NonNullOptionalNullableAttribute_encode", test_NonNullOptionalNullableAttribute_encode), ("test_nullableRelationshipIsNull", test_nullableRelationshipIsNull),