diff --git a/README.md b/README.md index 4d2ae87..6002dfa 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,7 @@ See the JSON API Spec here: https://jsonapi.org/format/ - [Custom Attribute or Relationship Key Mapping](#custom-attribute-or-relationship-key-mapping) - [Custom Attribute Encode/Decode](#custom-attribute-encodedecode) - [Meta-Attributes](#meta-attributes) + - [Meta-Relationships](#meta-relationships) - [Example](#example) - [Preamble (Setup shared by server and client)](#preamble-setup-shared-by-server-and-client) - [Server Pseudo-example](#server-pseudo-example) @@ -574,6 +575,39 @@ 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. +### Meta-Relationships +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-relationships are just the thing to make your entities more natural to work with. + +Similarly to Meta-Attributes, Meta-Relationships allow you to represent non-compliant relationships as computed relationship properties. In the following example, a relationship is created from some attributes on the JSON model. + +```swift +enum UserDescription: EntityDescription { + public static var jsonType: String { return "users" } + + struct Attributes: JSONAPI.Attributes { + let friend_id: Attribute + } + + struct Relationships: JSONAPI.Relationships { + public var friend: (User) -> User.Identifier { + return { user in + return User.Identifier(rawValue: user[\.friend_id]) + } + } + } +} + +typealias User = JSONAPI.Entity +``` + +Given a value `user` of the above entity type, you can access the `friend` relationship just like you would any other: + +```swift +let friendId = user ~> \.friend +``` + +This works because `friend` is defined in the form: `var {name}: ({Entity}) -> {Identifier}` where `{Entity}` is the `JSONAPI.Entity` described by the `EntityDescription` containing the meta-relationship. + ## 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 22f16fe..077c7e1 100644 --- a/Sources/JSONAPI/Resource/Entity.swift +++ b/Sources/JSONAPI/Resource/Entity.swift @@ -522,6 +522,23 @@ public extension EntityProxy { } } +// MARK: Meta-Relationship Access +public extension EntityProxy { + /// Access to an Id of a `ToOneRelationship`. + /// This allows you to write `entity ~> \.other` instead + /// of `entity.relationships.other.id`. + public static func ~>(entity: Self, path: KeyPath Identifier>) -> Identifier { + return entity.relationships[keyPath: path](entity) + } + + /// Access to all Ids of a `ToManyRelationship`. + /// This allows you to write `entity ~> \.others` instead + /// of `entity.relationships.others.ids`. + public static func ~>(entity: Self, path: KeyPath [Identifier]>) -> [Identifier] { + return entity.relationships[keyPath: path](entity) + } +} + infix operator ~> // MARK: - Codable diff --git a/Tests/JSONAPITests/Entity/EntityTests.swift b/Tests/JSONAPITests/Entity/EntityTests.swift index ec41b31..6142247 100644 --- a/Tests/JSONAPITests/Entity/EntityTests.swift +++ b/Tests/JSONAPITests/Entity/EntityTests.swift @@ -612,7 +612,7 @@ extension EntityTests { // MARK: With a Meta Attribute extension EntityTests { - func test_MetaEntityAccessWorks() { + func test_MetaEntityAttributeAccessWorks() { let entity1 = TestEntityWithMetaAttribute(id: "even", attributes: .init(), relationships: .none, @@ -629,6 +629,20 @@ extension EntityTests { } } +// MARK: With a Meta Relationship + +extension EntityTests { + func test_MetaEntityRelationshipAccessWorks() { + let entity1 = TestEntityWithMetaRelationship(id: "even", + attributes: .none, + relationships: .init(), + meta: .none, + links: .none) + + XCTAssertEqual(entity1 ~> \.metaRelationship, "hello") + } +} + // MARK: - Test Types extension EntityTests { @@ -826,6 +840,22 @@ extension EntityTests { typealias TestEntityWithMetaAttribute = BasicEntity + enum TestEntityWithMetaRelationshipDescription: EntityDescription { + public static var jsonType: String { return "meta_relationship_entity" } + + typealias Attributes = NoAttributes + + struct Relationships: JSONAPI.Relationships { + var metaRelationship: (TestEntityWithMetaRelationship) -> TestEntity1.Identifier { + return { entity in + return TestEntity1.Identifier(rawValue: "hello") + } + } + } + } + + typealias TestEntityWithMetaRelationship = 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 12b18ca..d225cb5 100644 --- a/Tests/JSONAPITests/XCTestManifests.swift +++ b/Tests/JSONAPITests/XCTestManifests.swift @@ -228,7 +228,8 @@ extension EntityTests { ("test_IntOver10_success", test_IntOver10_success), ("test_IntToString", test_IntToString), ("test_IntToString_encode", test_IntToString_encode), - ("test_MetaEntityAccessWorks", test_MetaEntityAccessWorks), + ("test_MetaEntityAttributeAccessWorks", test_MetaEntityAttributeAccessWorks), + ("test_MetaEntityRelationshipAccessWorks", test_MetaEntityRelationshipAccessWorks), ("test_NonNullOptionalNullableAttribute", test_NonNullOptionalNullableAttribute), ("test_NonNullOptionalNullableAttribute_encode", test_NonNullOptionalNullableAttribute_encode), ("test_nullableRelationshipIsNull", test_nullableRelationshipIsNull),