Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/jsonapi links #68

Merged
merged 7 commits into from
Jun 26, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion Kakapo.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,13 @@

/* Begin PBXBuildFile section */
5B16AF891A00E1CEB873C5F8 /* Pods_Kakapo_iOSTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1250C40FD2117CD20E6AE31F /* Pods_Kakapo_iOSTests.framework */; };
8BE0FC801D16BD9A00FE706A /* JSONAPILinksTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BE0FC7E1D16BD8300FE706A /* JSONAPILinksTests.swift */; };
8BE0FC811D16BD9B00FE706A /* JSONAPILinksTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BE0FC7E1D16BD8300FE706A /* JSONAPILinksTests.swift */; };
8BE0FC821D16BD9C00FE706A /* JSONAPILinksTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BE0FC7E1D16BD8300FE706A /* JSONAPILinksTests.swift */; };
8BE0FC841D172EC900FE706A /* JSONAPILinks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BE0FC831D172EC900FE706A /* JSONAPILinks.swift */; };
8BE0FC851D172EC900FE706A /* JSONAPILinks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BE0FC831D172EC900FE706A /* JSONAPILinks.swift */; };
8BE0FC861D172EC900FE706A /* JSONAPILinks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BE0FC831D172EC900FE706A /* JSONAPILinks.swift */; };
8BE0FC871D172EC900FE706A /* JSONAPILinks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BE0FC831D172EC900FE706A /* JSONAPILinks.swift */; };
AC9577F342F6730725CC9D72 /* Pods_Kakapo_tvOSTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C67882333142806D5CF9A918 /* Pods_Kakapo_tvOSTests.framework */; };
D5363674ABF8735A0A1368BD /* Pods_Kakapo_macOSTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = ABE4F11D5A141DB87CD81023 /* Pods_Kakapo_macOSTests.framework */; };
DE76E1311D0DC62B009721A4 /* Kakapo.h in Headers */ = {isa = PBXBuildFile; fileRef = DE76E0FA1D0DC37E009721A4 /* Kakapo.h */; settings = {ATTRIBUTES = (Public, ); }; };
Expand Down Expand Up @@ -103,6 +110,8 @@
/* Begin PBXFileReference section */
1250C40FD2117CD20E6AE31F /* Pods_Kakapo_iOSTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Kakapo_iOSTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
822EBBB73B6B4117D229CDF1 /* Pods-Kakapo tvOSTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Kakapo tvOSTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Kakapo tvOSTests/Pods-Kakapo tvOSTests.debug.xcconfig"; sourceTree = "<group>"; };
8BE0FC7E1D16BD8300FE706A /* JSONAPILinksTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JSONAPILinksTests.swift; sourceTree = "<group>"; };
8BE0FC831D172EC900FE706A /* JSONAPILinks.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JSONAPILinks.swift; sourceTree = "<group>"; };
ABE4F11D5A141DB87CD81023 /* Pods_Kakapo_macOSTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Kakapo_macOSTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
C67882333142806D5CF9A918 /* Pods_Kakapo_tvOSTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Kakapo_tvOSTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
C9E9EAD776E83C0F5785A0DB /* Pods-Kakapo macOSTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Kakapo macOSTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Kakapo macOSTests/Pods-Kakapo macOSTests.debug.xcconfig"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -261,21 +270,23 @@
DE76E0FD1D0DC38B009721A4 /* Tests */ = {
isa = PBXGroup;
children = (
DE76E0FE1D0DC38B009721A4 /* Info.plist */,
8BE0FC7E1D16BD8300FE706A /* JSONAPILinksTests.swift */,
DE76E1B11D0DC857009721A4 /* JSONAPITests.swift */,
DE76E1B21D0DC857009721A4 /* KakapoDBTests.swift */,
DE76E1B31D0DC857009721A4 /* PropertyPolicyTests.swift */,
DE76E1B41D0DC857009721A4 /* RouterTests.swift */,
DE76E1B51D0DC857009721A4 /* SerializerTests.swift */,
DE76E1B61D0DC857009721A4 /* URLDecomposerTests.swift */,
DE76E1B71D0DC857009721A4 /* XCTestCase+CustomAssertions.swift */,
DE76E0FE1D0DC38B009721A4 /* Info.plist */,
);
path = Tests;
sourceTree = "<group>";
};
DE76E1001D0DC395009721A4 /* Source */ = {
isa = PBXGroup;
children = (
8BE0FC831D172EC900FE706A /* JSONAPILinks.swift */,
DE76E1011D0DC395009721A4 /* JSONAPISerializer.swift */,
DE76E1021D0DC395009721A4 /* KakapoDB.swift */,
DE76E1031D0DC395009721A4 /* KakapoServer.swift */,
Expand Down Expand Up @@ -717,6 +728,7 @@
DE76E19A1D0DC755009721A4 /* PropertyPolicy.swift in Sources */,
DE76E19D1D0DC755009721A4 /* URLDecomposer.swift in Sources */,
DE76E1971D0DC755009721A4 /* KakapoDB.swift in Sources */,
8BE0FC851D172EC900FE706A /* JSONAPILinks.swift in Sources */,
DE76E19C1D0DC755009721A4 /* Serializer.swift in Sources */,
DE76E1961D0DC755009721A4 /* JSONAPISerializer.swift in Sources */,
DE76E19B1D0DC755009721A4 /* Router.swift in Sources */,
Expand All @@ -733,6 +745,7 @@
DE76E1921D0DC755009721A4 /* PropertyPolicy.swift in Sources */,
DE76E1951D0DC755009721A4 /* URLDecomposer.swift in Sources */,
DE76E18F1D0DC755009721A4 /* KakapoDB.swift in Sources */,
8BE0FC861D172EC900FE706A /* JSONAPILinks.swift in Sources */,
DE76E1941D0DC755009721A4 /* Serializer.swift in Sources */,
DE76E18E1D0DC755009721A4 /* JSONAPISerializer.swift in Sources */,
DE76E1931D0DC755009721A4 /* Router.swift in Sources */,
Expand All @@ -746,6 +759,7 @@
buildActionMask = 2147483647;
files = (
DE76E1BC1D0DC857009721A4 /* JSONAPITests.swift in Sources */,
8BE0FC811D16BD9B00FE706A /* JSONAPILinksTests.swift in Sources */,
DE76E1CB1D0DC857009721A4 /* URLDecomposerTests.swift in Sources */,
DE76E1BF1D0DC857009721A4 /* KakapoDBTests.swift in Sources */,
DE76E1C81D0DC857009721A4 /* SerializerTests.swift in Sources */,
Expand All @@ -763,6 +777,7 @@
DE76E18A1D0DC754009721A4 /* PropertyPolicy.swift in Sources */,
DE76E18D1D0DC754009721A4 /* URLDecomposer.swift in Sources */,
DE76E1871D0DC754009721A4 /* KakapoDB.swift in Sources */,
8BE0FC871D172EC900FE706A /* JSONAPILinks.swift in Sources */,
DE76E18C1D0DC754009721A4 /* Serializer.swift in Sources */,
DE76E1861D0DC754009721A4 /* JSONAPISerializer.swift in Sources */,
DE76E18B1D0DC754009721A4 /* Router.swift in Sources */,
Expand All @@ -776,6 +791,7 @@
buildActionMask = 2147483647;
files = (
DE76E1BD1D0DC857009721A4 /* JSONAPITests.swift in Sources */,
8BE0FC821D16BD9C00FE706A /* JSONAPILinksTests.swift in Sources */,
DE76E1CC1D0DC857009721A4 /* URLDecomposerTests.swift in Sources */,
DE76E1C01D0DC857009721A4 /* KakapoDBTests.swift in Sources */,
DE76E1C91D0DC857009721A4 /* SerializerTests.swift in Sources */,
Expand All @@ -793,6 +809,7 @@
DE76E1A21D0DC756009721A4 /* PropertyPolicy.swift in Sources */,
DE76E1A51D0DC756009721A4 /* URLDecomposer.swift in Sources */,
DE76E19F1D0DC756009721A4 /* KakapoDB.swift in Sources */,
8BE0FC841D172EC900FE706A /* JSONAPILinks.swift in Sources */,
DE76E1A41D0DC756009721A4 /* Serializer.swift in Sources */,
DE76E19E1D0DC756009721A4 /* JSONAPISerializer.swift in Sources */,
DE76E1A31D0DC756009721A4 /* Router.swift in Sources */,
Expand All @@ -806,6 +823,7 @@
buildActionMask = 2147483647;
files = (
DE76E1BB1D0DC857009721A4 /* JSONAPITests.swift in Sources */,
8BE0FC801D16BD9A00FE706A /* JSONAPILinksTests.swift in Sources */,
DE76E1CA1D0DC857009721A4 /* URLDecomposerTests.swift in Sources */,
DE76E1BE1D0DC857009721A4 /* KakapoDBTests.swift in Sources */,
DE76E1C71D0DC857009721A4 /* SerializerTests.swift in Sources */,
Expand Down
106 changes: 106 additions & 0 deletions Source/JSONAPILinks.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
//
// JSONAPILinks.swift
// Kakapo
//
// Created by Joan Romano on 19/06/16.
// Copyright © 2016 devlucky. All rights reserved.
//

import Foundation

/**
* An enum representing JSON API Link types.

* A link MUST be represented as either:

- A string containing the link’s URL.
- An object which can contain the following members:
- `href`: a string containing the link’s URL.
- `meta`: a meta object containing non-standard meta-information about the link.

* [See the JSON API documentation on links](http://jsonapi.org/format/#document-links)
*/
public enum JSONAPILink: CustomSerializable {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Documentation missing

case Simple(value: String)
case Object(href: String, meta: Serializable)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't be easier a plain strict with optional meta? You wouldn't need a CustomSerializable then

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wait, I suggested an enumeration for links that were constrained to specific keys (Was it pagination? Where is gone?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I don't understand what you mean, Which constraints?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Which links where the one that were mutually exclusive?
In this case you have two cases: one with href and the other with href and meta so an enumeration doesn't make sense

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Basically you have two types:

  • A plain string containing the url
  • A link object which can contain href and meta


public func customSerialize() -> AnyObject? {
switch self {
case let Object(href, meta):
var serializedObject: [String: AnyObject] = ["href" : href]
serializedObject["meta"] = meta.serialize()

return serializedObject
case let Simple(value):
return value
}
}
}

/**
* A protocol representing a JSON API entity with links. This will handle all different `links` combinations:

- Inside `data` field
- Inside `included` field
- Inside `relationships` field

* For the first two options, an entity must use the `links` property. In order to add links in the `relationships` field,
an entity must use `relationshipsLinks` property and return them for every relationship. For example, given the representation:

```swift
struct Cat: JSONAPIEntity, JSONAPILinkedEntity {
let id: String
let name: String
let links: [String : JSONAPILink]?
}

struct Dog: JSONAPIEntity, JSONAPILinkedEntity {
let id: String
let name: String
let links: [String : JSONAPILink]?
}

struct User: JSONAPIEntity, JSONAPILinkedEntity {
let id: String
let name: String
let cats: [Cat]
let links: [String : JSONAPILink]?
let relationshipsLinks: [String : [String : JSONAPILink]]?
}
```

Appart from their own entity links, and in order to provide extra links for relationships, `User` must specify them for each relationship key:

```swift
let cats = [Cat(id: "33", name: "Stancho", links: nil),
Cat(id: "44", name: "Hez", links: ["test": JSONAPILink.Simple(value: "hello"),
"another": JSONAPILink.Object(href: "http://example.com/articles/1/comments", meta: Meta())])]

let dog = Dog(id: "22", name: "Joan", links: nil)

let user = User(id: "11", name: "Alex", dog: dog, cats: cats,
links: ["one": JSONAPILink.Simple(value: "hello"),
"two": JSONAPILink.Object(href: "hello", meta: Meta())],
relationshipsLinks: ["cats": ["prev": JSONAPILink.Simple(value: "hello"),
"next": JSONAPILink.Simple(value: "world"),
"first": JSONAPILink.Simple(value: "yeah"),
"last": JSONAPILink.Simple(value: "text")],
"dog": ["testDog": JSONAPILink.Simple(value: "hello"),
"anotherDog": JSONAPILink.Object(href: "http://example.com/articles/1/comments", meta: Meta())]
]
)
```

* In order to represent the top level links of the root object, check out `JSONAPISerializer` initialization methods.

* [See the JSON API documentation on links](http://jsonapi.org/format/#document-links)
*/
public protocol JSONAPILinkedEntity {
var links: [String : JSONAPILink]? { get }
var relationshipsLinks: [String : [String : JSONAPILink]]? { get }
}

extension JSONAPILinkedEntity {
public var links: [String : JSONAPILink]? { return nil }
public var relationshipsLinks: [String : [String : JSONAPILink]]? { return nil }
}
40 changes: 32 additions & 8 deletions Source/JSONAPISerializer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -72,14 +72,22 @@ public protocol JSONAPIEntity: CustomSerializable, JSONAPISerializable {
*/
public struct JSONAPISerializer<T: JSONAPIEntity>: Serializable {

// Top level `data` member: the document’s “primary data”
private let data: AnyObject

// Top level `included` member: an array of resource objects that are related to the primary data and/or each other (“included resources”).
private let included: [AnyObject]?

// Top level `links` member: a links object related to the primary data.
private let links: AnyObject?

private typealias JSONAPIConvertible = protocol<JSONAPISerializable, Serializable>
private typealias JSONAPISerializerInit = (data: AnyObject, links: AnyObject?, included: [AnyObject]?)

private static func commonInit(object: JSONAPIConvertible, includeChildren: Bool) -> (data: AnyObject, included: [AnyObject]?) {
private static func commonInit(object: JSONAPIConvertible, topLevelLinks: [String: JSONAPILink]?, includeChildren: Bool) -> JSONAPISerializerInit {
return (
data: object.serialize()!, // can't fail, JSONAPIEntity must always be serializable
links: topLevelLinks.serialize(),
included: object.includedRelationships(includeChildren)?.unifiedIncludedRelationships()
)
}
Expand All @@ -88,24 +96,26 @@ public struct JSONAPISerializer<T: JSONAPIEntity>: Serializable {
Initialize a serializer with a single `JSONAPIEntity`

- parameter object: A `JSONAPIEntities`
- parameter topLevelLinks: A top `JSONAPILink` optional object
- parameter includeChildren: when true it wll include relationships of relationships, false by default.

- returns: A serializable object that serializes a `JSONAPIEntity` conforming to JSON API
*/
public init(_ object: T, includeChildren: Bool = false) {
(data, included) = JSONAPISerializer.commonInit(object, includeChildren: includeChildren)
public init(_ object: T, topLevelLinks: [String: JSONAPILink]? = nil, includeChildren: Bool = false) {
(data, links, included) = JSONAPISerializer.commonInit(object, topLevelLinks: topLevelLinks, includeChildren: includeChildren)
}

/**
Initialize a serializer with an array of `JSONAPIEntity`

- parameter objects: An array of `JSONAPIEntity`
- parameter topLevelLinks: A top `JSONAPILink` optional object
- parameter includeChildren: when true it wll include relationships of relationships, false by default.

- returns: A serializable object that serializes an array of `JSONAPIEntity` conforming to JSON API
*/
public init(_ objects: [T], includeChildren: Bool = false) {
(data, included) = JSONAPISerializer.commonInit(objects, includeChildren: includeChildren)
public init(_ objects: [T], topLevelLinks: [String: JSONAPILink]? = nil, includeChildren: Bool = false) {
(data, links, included) = JSONAPISerializer.commonInit(objects, topLevelLinks: topLevelLinks, includeChildren: includeChildren)
}
}

Expand Down Expand Up @@ -240,16 +250,30 @@ public extension JSONAPIEntity {
var attributes = [String: AnyObject]()
var relationships = [String: AnyObject]()

if let linkedEntity = self as? JSONAPILinkedEntity,
entityLinks = linkedEntity.links where entityLinks.count > 0 {
data["links"] = entityLinks.serialize()
}

let excludedKeys: Set<String> = ["id", "links", "topLinks"]

for child in mirror.children {
if let label = child.label {
if let value = child.value as? JSONAPISerializable, let data = value.data(includeRelationships: false, includeAttributes: false) {
if includeRelationships {
relationships[label] = ["data": data]
var relationship: [String: AnyObject] = ["data" : data]

if let linkedEntity = self as? JSONAPILinkedEntity,
relationshipsLinks = linkedEntity.relationshipsLinks?[label] where relationshipsLinks.count > 0 {
relationship["links"] = relationshipsLinks.serialize()
}

relationships[label] = relationship
}
} else if includeAttributes {
} else if includeAttributes && !excludedKeys.contains(label) {
if let value = child.value as? Serializable {
attributes[label] = value.serialize()
} else if label != "id" {
} else {
assert(child.value is AnyObject)
attributes[label] = child.value as? AnyObject
}
Expand Down
Loading