Skip to content

Commit

Permalink
Merge pull request #70 from mattpolzin/feature/swift-identifiable
Browse files Browse the repository at this point in the history
Add Swift Identifiable conformance
  • Loading branch information
mattpolzin authored May 29, 2020
2 parents 1e2a87a + 754255b commit 8aa20f3
Show file tree
Hide file tree
Showing 15 changed files with 129 additions and 90 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ typealias UnidentifiedJSONEntity<Description: ResourceObjectDescription> = JSONA
// Create relationship typealiases because we do not expect
// JSON:API Relationships for this particular API to have
// Metadata or Links associated with them.
typealias ToOneRelationship<Entity: Identifiable> = JSONAPI.ToOneRelationship<Entity, NoMetadata, NoLinks>
typealias ToOneRelationship<Entity: JSONAPIIdentifiable> = JSONAPI.ToOneRelationship<Entity, NoMetadata, NoLinks>
typealias ToManyRelationship<Entity: Relatable> = JSONAPI.ToManyRelationship<Entity, NoMetadata, NoLinks>

// Create a typealias for a Document because we do not expect
Expand Down Expand Up @@ -86,7 +86,7 @@ typealias SingleArticleDocument = Document<SingleResourceBody<Article>, NoInclud
func articleDocument(includeAuthor: Bool) -> Either<SingleArticleDocument, SingleArticleDocumentWithIncludes> {
// Let's pretend all of this is coming from a database:

let authorId = Author.Identifier(rawValue: "1234")
let authorId = Author.ID(rawValue: "1234")

let article = Article(id: .init(rawValue: "5678"),
attributes: .init(title: .init(value: "JSON:API in Swift"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -129,9 +129,9 @@ enum ArticleDocumentError: String, JSONAPIError, Codable {
typealias SingleArticleDocument = JSONAPI.Document<SingleResourceBody<Article>, DocumentMetadata, SingleArticleDocumentLinks, Include1<Author>, APIDescription<APIDescriptionMetadata>, ArticleDocumentError>

// MARK: - Instantiations
let authorId1 = Author.Identifier()
let authorId2 = Author.Identifier()
let authorId3 = Author.Identifier()
let authorId1 = Author.ID()
let authorId2 = Author.ID()
let authorId3 = Author.ID()

let now = Date()
let tomorrow = Calendar.current.date(byAdding: .day, value: 1, to: now)!
Expand All @@ -155,7 +155,7 @@ let author1Links = EntityLinks(selfLink: .init(url: URL(string: "https://article
meta: .init(expiry: tomorrow)))
let author1 = Author(id: authorId1,
attributes: .init(name: .init(value: "James Kinney")),
relationships: .init(articles: .init(ids: [article.id, Article.Identifier(), Article.Identifier()],
relationships: .init(articles: .init(ids: [article.id, Article.ID(), Article.ID()],
meta: .init(pagination: .init(total: 3,
limit: 50,
offset: 0)),
Expand All @@ -167,7 +167,7 @@ let author2Links = EntityLinks(selfLink: .init(url: URL(string: "https://article
meta: .init(expiry: tomorrow)))
let author2 = Author(id: authorId2,
attributes: .init(name: .init(value: "James Kinney")),
relationships: .init(articles: .init(ids: [article.id, Article.Identifier()],
relationships: .init(articles: .init(ids: [article.id, Article.ID()],
meta: .init(pagination: .init(total: 2,
limit: 50,
offset: 0)),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ print("Received dog with owner: \(dog3 ~> \.owner)")

// give the dog an owner
let changedDog3 = dog3.replacingRelationships { _ in
return .init(owner: .init(id: Id(rawValue: "1")))
return .init(owner: .init(id: ID(rawValue: "1")))
}

// create a document to be used as a request body for a PATCH request
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,18 +20,18 @@ 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.primaryResource?.value
let dogOwner: Person.Identifier? = dogFromData.flatMap { $0 ~> \.owner }
let dogOwner: Person.ID? = dogFromData.flatMap { $0 ~> \.owner }


// MARK: - 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, BasicJSONAPIError<String>>
let altDogResponse = try! JSONDecoder().decode(AltSingleDogDocument.self, from: singleDogData)
let altDogFromData = altDogResponse.body.primaryResource?.value
let altDogHuman: Person.Identifier? = altDogFromData.flatMap { $0 ~> \.human }
let altDogHuman: Person.ID? = altDogFromData.flatMap { $0 ~> \.human }


// MARK: - Create a request or response with multiple people and dogs and houses included
let personIds = [Person.Identifier(), Person.Identifier()]
let personIds = [Person.ID(), Person.ID()]
let dogs = try! [Dog(name: "Buddy", owner: personIds[0]), Dog(name: "Joy", owner: personIds[0]), Dog(name: "Travis", owner: personIds[1])]
let houses = [House(attributes: .none, relationships: .none, meta: .none, links: .none), House(attributes: .none, relationships: .none, meta: .none, links: .none)]
let people = try! [Person(id: personIds[0], name: ["Gary", "Doe"], favoriteColor: "Orange-Red", friends: [], dogs: [dogs[0], dogs[1]], home: houses[0]), Person(id: personIds[1], name: ["Elise", "Joy"], favoriteColor: "Red", friends: [], dogs: [dogs[2]], home: houses[1])]
Expand Down
8 changes: 4 additions & 4 deletions JSONAPI.playground/Sources/Entities.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ extension String: CreatableRawIdType {

// MARK: - typealiases for convenience
public typealias ExampleEntity<Description: ResourceObjectDescription> = ResourceObject<Description, NoMetadata, NoLinks, String>
public typealias ToOne<E: Identifiable> = ToOneRelationship<E, NoMetadata, NoLinks>
public typealias ToOne<E: JSONAPIIdentifiable> = ToOneRelationship<E, NoMetadata, NoLinks>
public typealias ToMany<E: Relatable> = ToManyRelationship<E, NoMetadata, NoLinks>

// MARK: - A few resource objects (entities)
Expand Down Expand Up @@ -63,8 +63,8 @@ public enum PersonDescription: ResourceObjectDescription {
public typealias Person = ExampleEntity<PersonDescription>

public extension ResourceObject where Description == PersonDescription, MetaType == NoMetadata, LinksType == NoLinks, EntityRawIdType == String {
init(id: Person.Id? = nil,name: [String], favoriteColor: String, friends: [Person], dogs: [Dog], home: House) throws {
self = Person(id: id ?? Person.Id(), attributes: .init(name: .init(value: name), favoriteColor: .init(value: favoriteColor)), relationships: .init(friends: .init(resourceObjects: friends), dogs: .init(resourceObjects: dogs), home: .init(resourceObject: home)), meta: .none, links: .none)
init(id: Person.ID? = nil,name: [String], favoriteColor: String, friends: [Person], dogs: [Dog], home: House) throws {
self = Person(id: id ?? Person.ID(), attributes: .init(name: .init(value: name), favoriteColor: .init(value: favoriteColor)), relationships: .init(friends: .init(resourceObjects: friends), dogs: .init(resourceObjects: dogs), home: .init(resourceObject: home)), meta: .none, links: .none)
}
}

Expand Down Expand Up @@ -147,7 +147,7 @@ public extension ResourceObject where Description == DogDescription, MetaType ==
self = Dog(attributes: .init(name: .init(value: name)), relationships: DogDescription.Relationships(owner: .init(resourceObject: owner)), meta: .none, links: .none)
}

init(name: String, owner: Person.Id) throws {
init(name: String, owner: Person.ID) throws {
self = Dog(attributes: .init(name: .init(value: name)), relationships: .init(owner: .init(id: owner)), meta: .none, links: .none)
}
}
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ typealias UnidentifiedJSONEntity<Description: ResourceObjectDescription> = JSONA
// Create relationship typealiases because we do not expect
// JSON:API Relationships for this particular API to have
// Metadata or Links associated with them.
typealias ToOneRelationship<Entity: Identifiable> = JSONAPI.ToOneRelationship<Entity, NoMetadata, NoLinks>
typealias ToOneRelationship<Entity: JSONAPIIdentifiable> = JSONAPI.ToOneRelationship<Entity, NoMetadata, NoLinks>
typealias ToManyRelationship<Entity: Relatable> = JSONAPI.ToManyRelationship<Entity, NoMetadata, NoLinks>

// Create a typealias for a Document because we do not expect
Expand Down Expand Up @@ -220,7 +220,7 @@ typealias SingleArticleDocument = Document<SingleResourceBody<Article>, NoInclud
func articleDocument(includeAuthor: Bool) -> Either<SingleArticleDocument, SingleArticleDocumentWithIncludes> {
// Let's pretend all of this is coming from a database:

let authorId = Author.Identifier(rawValue: "1234")
let authorId = Author.ID(rawValue: "1234")

let article = Article(id: .init(rawValue: "5678"),
attributes: .init(title: .init(value: "JSON:API in Swift"),
Expand Down
54 changes: 27 additions & 27 deletions Sources/JSONAPI/Resource/Relationship.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,46 +35,46 @@ public struct MetaRelationship<MetaType: JSONAPI.Meta, LinksType: JSONAPI.Links>
/// a JSON API "Resource Linkage."
/// See https://jsonapi.org/format/#document-resource-object-linkage
/// A convenient typealias might make your code much more legible: `One<ResourceObjectDescription>`
public struct ToOneRelationship<Identifiable: JSONAPI.Identifiable, MetaType: JSONAPI.Meta, LinksType: JSONAPI.Links>: RelationshipType, Equatable {
public struct ToOneRelationship<Identifiable: JSONAPI.JSONAPIIdentifiable, MetaType: JSONAPI.Meta, LinksType: JSONAPI.Links>: RelationshipType, Equatable {

public let id: Identifiable.Identifier
public let id: Identifiable.ID

public let meta: MetaType
public let links: LinksType

public init(id: Identifiable.Identifier, meta: MetaType, links: LinksType) {
public init(id: Identifiable.ID, meta: MetaType, links: LinksType) {
self.id = id
self.meta = meta
self.links = links
}
}

extension ToOneRelationship where MetaType == NoMetadata, LinksType == NoLinks {
public init(id: Identifiable.Identifier) {
public init(id: Identifiable.ID) {
self.init(id: id, meta: .none, links: .none)
}
}

extension ToOneRelationship {
public init<T: ResourceObjectType>(resourceObject: T, meta: MetaType, links: LinksType) where T.Id == Identifiable.Identifier {
public init<T: ResourceObjectType>(resourceObject: T, meta: MetaType, links: LinksType) where T.ID == Identifiable.ID {
self.init(id: resourceObject.id, meta: meta, links: links)
}
}

extension ToOneRelationship where MetaType == NoMetadata, LinksType == NoLinks {
public init<T: ResourceObjectType>(resourceObject: T) where T.Id == Identifiable.Identifier {
public init<T: ResourceObjectType>(resourceObject: T) where T.ID == Identifiable.ID {
self.init(id: resourceObject.id, meta: .none, links: .none)
}
}

extension ToOneRelationship where Identifiable: OptionalRelatable {
public init<T: ResourceObjectType>(resourceObject: T?, meta: MetaType, links: LinksType) where T.Id == Identifiable.Wrapped.Identifier {
public init<T: ResourceObjectType>(resourceObject: T?, meta: MetaType, links: LinksType) where T.ID == Identifiable.Wrapped.ID {
self.init(id: resourceObject?.id, meta: meta, links: links)
}
}

extension ToOneRelationship where Identifiable: OptionalRelatable, MetaType == NoMetadata, LinksType == NoLinks {
public init<T: ResourceObjectType>(resourceObject: T?) where T.Id == Identifiable.Wrapped.Identifier {
public init<T: ResourceObjectType>(resourceObject: T?) where T.ID == Identifiable.Wrapped.ID {
self.init(id: resourceObject?.id, meta: .none, links: .none)
}
}
Expand All @@ -85,24 +85,24 @@ extension ToOneRelationship where Identifiable: OptionalRelatable, MetaType == N
/// A convenient typealias might make your code much more legible: `Many<ResourceObjectDescription>`
public struct ToManyRelationship<Relatable: JSONAPI.Relatable, MetaType: JSONAPI.Meta, LinksType: JSONAPI.Links>: RelationshipType, Equatable {

public let ids: [Relatable.Identifier]
public let ids: [Relatable.ID]

public let meta: MetaType
public let links: LinksType

public init(ids: [Relatable.Identifier], meta: MetaType, links: LinksType) {
public init(ids: [Relatable.ID], meta: MetaType, links: LinksType) {
self.ids = ids
self.meta = meta
self.links = links
}

public init<T: JSONAPI.Identifiable>(pointers: [ToOneRelationship<T, NoMetadata, NoLinks>], meta: MetaType, links: LinksType) where T.Identifier == Relatable.Identifier {
public init<T: JSONAPI.JSONAPIIdentifiable>(pointers: [ToOneRelationship<T, NoMetadata, NoLinks>], meta: MetaType, links: LinksType) where T.ID == Relatable.ID {
ids = pointers.map(\.id)
self.meta = meta
self.links = links
}

public init<T: ResourceObjectType>(resourceObjects: [T], meta: MetaType, links: LinksType) where T.Id == Relatable.Identifier {
public init<T: ResourceObjectType>(resourceObjects: [T], meta: MetaType, links: LinksType) where T.ID == Relatable.ID {
self.init(ids: resourceObjects.map(\.id), meta: meta, links: links)
}

Expand All @@ -117,40 +117,40 @@ public struct ToManyRelationship<Relatable: JSONAPI.Relatable, MetaType: JSONAPI

extension ToManyRelationship where MetaType == NoMetadata, LinksType == NoLinks {

public init(ids: [Relatable.Identifier]) {
public init(ids: [Relatable.ID]) {
self.init(ids: ids, meta: .none, links: .none)
}

public init<T: JSONAPI.Identifiable>(pointers: [ToOneRelationship<T, NoMetadata, NoLinks>]) where T.Identifier == Relatable.Identifier {
public init<T: JSONAPI.JSONAPIIdentifiable>(pointers: [ToOneRelationship<T, NoMetadata, NoLinks>]) where T.ID == Relatable.ID {
self.init(pointers: pointers, meta: .none, links: .none)
}

public static var none: ToManyRelationship {
return .none(withMeta: .none, links: .none)
}

public init<T: ResourceObjectType>(resourceObjects: [T]) where T.Id == Relatable.Identifier {
public init<T: ResourceObjectType>(resourceObjects: [T]) where T.ID == Relatable.ID {
self.init(resourceObjects: resourceObjects, meta: .none, links: .none)
}
}

public protocol Identifiable: JSONTyped {
associatedtype Identifier: Equatable
public protocol JSONAPIIdentifiable: JSONTyped {
associatedtype ID: Equatable
}

/// The Relatable protocol describes anything that
/// has an IdType Identifier
public protocol Relatable: Identifiable where Identifier: JSONAPI.IdType {
public protocol Relatable: JSONAPIIdentifiable where ID: JSONAPI.IdType {
}

/// OptionalRelatable just describes an Optional
/// with a Reltable Wrapped type.
public protocol OptionalRelatable: Identifiable where Identifier == Wrapped.Identifier? {
public protocol OptionalRelatable: JSONAPIIdentifiable where ID == Wrapped.ID? {
associatedtype Wrapped: JSONAPI.Relatable
}

extension Optional: Identifiable, OptionalRelatable, JSONTyped where Wrapped: JSONAPI.Relatable {
public typealias Identifier = Wrapped.Identifier?
extension Optional: JSONAPIIdentifiable, OptionalRelatable, JSONTyped where Wrapped: JSONAPI.Relatable {
public typealias ID = Wrapped.ID?

public static var jsonType: String { return Wrapped.jsonType }
}
Expand Down Expand Up @@ -196,7 +196,7 @@ extension MetaRelationship: Codable {
}
}

extension ToOneRelationship: Codable where Identifiable.Identifier: OptionalId {
extension ToOneRelationship: Codable where Identifiable.ID: OptionalId {
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: ResourceLinkageCodingKeys.self)

Expand All @@ -219,7 +219,7 @@ extension ToOneRelationship: Codable where Identifiable.Identifier: OptionalId {
// type at which point we can store nil in `id`.
let anyNil: Any? = nil
if try container.decodeNil(forKey: .data) {
guard let val = anyNil as? Identifiable.Identifier else {
guard let val = anyNil as? Identifiable.ID else {
throw DecodingError.valueNotFound(
Self.self,
DecodingError.Context(
Expand Down Expand Up @@ -256,7 +256,7 @@ extension ToOneRelationship: Codable where Identifiable.Identifier: OptionalId {
)
}

id = Identifiable.Identifier(rawValue: try identifier.decode(Identifiable.Identifier.RawType.self, forKey: .id))
id = Identifiable.ID(rawValue: try identifier.decode(Identifiable.ID.RawType.self, forKey: .id))
}

public func encode(to encoder: Encoder) throws {
Expand All @@ -273,7 +273,7 @@ extension ToOneRelationship: Codable where Identifiable.Identifier: OptionalId {
// If id is nil, instead of {id: , type: } we will just
// encode `null`
let anyNil: Any? = nil
let nilId = anyNil as? Identifiable.Identifier
let nilId = anyNil as? Identifiable.ID
guard id != nilId else {
try container.encodeNil(forKey: .data)
return
Expand Down Expand Up @@ -314,7 +314,7 @@ extension ToManyRelationship: Codable {
path: context.codingPath)
}

var newIds = [Relatable.Identifier]()
var newIds = [Relatable.ID]()
while !identifiers.isAtEnd {
let identifier = try identifiers.nestedContainer(keyedBy: ResourceIdentifierCodingKeys.self)

Expand All @@ -324,7 +324,7 @@ extension ToManyRelationship: Codable {
throw JSONAPICodingError.typeMismatch(expected: Relatable.jsonType, found: type, path: decoder.codingPath)
}

newIds.append(Relatable.Identifier(rawValue: try identifier.decode(Relatable.Identifier.RawType.self, forKey: .id)))
newIds.append(Relatable.ID(rawValue: try identifier.decode(Relatable.ID.RawType.self, forKey: .id)))
}
ids = newIds
}
Expand Down
Loading

0 comments on commit 8aa20f3

Please sign in to comment.