Skip to content

Commit

Permalink
Implement first version of JsonApiResourceModel and get collection re…
Browse files Browse the repository at this point in the history
…quest
  • Loading branch information
Ybrin committed May 6, 2017
1 parent 0fbdded commit 33416de
Show file tree
Hide file tree
Showing 5 changed files with 300 additions and 27 deletions.
15 changes: 8 additions & 7 deletions Sources/VaporJsonApi/Json/JsonApiResourceObject.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
//

import Vapor
import URI

public class JsonApiResourceObject: JSONRepresentable {

Expand Down Expand Up @@ -156,13 +157,13 @@ public class JsonApiRelationshipObject: JSONRepresentable {

public class JsonApiLinksObject: JSONRepresentable {

public let selfLink: String
public let selfLink: URI
public let selfMeta: JsonApiMeta?

public let relatedLink: String
public let relatedLink: URI
public let relatedMeta: JsonApiMeta?

public init(selfLink: String, selfMeta: JsonApiMeta? = nil, relatedLink: String, relatedMeta: JsonApiMeta? = nil) {
public init(selfLink: URI, selfMeta: JsonApiMeta? = nil, relatedLink: URI, relatedMeta: JsonApiMeta? = nil) {
self.selfLink = selfLink
self.selfMeta = selfMeta

Expand All @@ -174,21 +175,21 @@ public class JsonApiLinksObject: JSONRepresentable {
let selfJson: JSON
if let selfMeta = selfMeta {
selfJson = try JSON(node: [
"href": Node(selfLink),
"href": try selfLink.makeFoundationURL().absoluteString,
"meta": selfMeta.makeJSON()
])
} else {
selfJson = JSON(Node(selfLink))
selfJson = try JSON(Node(selfLink.makeFoundationURL().absoluteString))
}

let relatedJson: JSON
if let relatedMeta = relatedMeta {
relatedJson = try JSON(node: [
"href": Node(relatedLink),
"href": try relatedLink.makeFoundationURL().absoluteString,
"meta": relatedMeta.makeJSON()
])
} else {
relatedJson = JSON(Node(relatedLink))
relatedJson = try JSON(Node(relatedLink.makeFoundationURL().absoluteString))
}

return try JSON(node: [
Expand Down
10 changes: 9 additions & 1 deletion Sources/VaporJsonApi/Resources/JsonApiResourceController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,14 @@ public extension JsonApiResourceController {
}
let resources = try Resource.query().limit(pageCount, withOffset: (pageNumber * pageCount) - pageCount).all()

return "Hello"
var resourceObjects = [JsonApiResourceObject]()
for r in resources {
resourceObjects.append(try r.makeResourceObject(resourceModel: r, baseUrl: req.uri))
}

let data = JsonApiData(resourceObjects: resourceObjects)
let document = JsonApiDocument(data: data)

return JsonApiResponse(status: .ok, document: document)
}
}
55 changes: 36 additions & 19 deletions Sources/VaporJsonApi/Resources/JsonApiResourceModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,32 +8,49 @@

import Vapor
import HTTP
import Fluent

public protocol JsonApiResourceModel: Model {
public class JsonApiResourceModel: Model, JsonApiResourceRepresentable {

typealias JsonApiAttributes = [String: JSON?]
typealias JsonApiRelationships = [String: JsonApiResourceModel.Type]
// MARK: - JsonApiResourceRepresentable stubs

var resourceType: JsonApiResourceType { get }
public static var resourceType: JsonApiResourceType {
return ""
}

func attributes() throws -> JsonApiAttributes
public func attributes() throws -> JsonApiAttributes {
throw JsonApiInternalServerError(title: "Internal Server Error", detail: "Subclasses of 'JsonApiResourceModel' must implement attributes()")
}

func relationships() throws -> JsonApiRelationships
}
public func parentRelationships() throws -> JsonApiParentRelationships {
throw JsonApiInternalServerError(title: "Internal Server Error", detail: "Subclasses of 'JsonApiResourceModel' must implement parentRelationships()")
}

public func childrenRelationships() throws -> JsonApiChildrenRelationships {
throw JsonApiInternalServerError(title: "Internal Server Error", detail: "Subclasses of 'JsonApiResourceModel' must implement childrenRelationships()")
}

public func siblingsRelationships() throws -> JsonApiSiblingsRelationships {
throw JsonApiInternalServerError(title: "Internal Server Error", detail: "Subclasses of 'JsonApiResourceModel' must implement siblingsRelationships()")
}

// MARK: - Model stubs

public extension JsonApiResourceModel {
public var id: Node?

public required init(node: Node, in context: Context) throws {
throw JsonApiInternalServerError(title: "Internal Server Error", detail: "Subclasses of 'Model' must implement init(node:in context)")
}

public func makeResourceObject() throws -> JsonApiResourceObject {
guard let id = self.id?.string else {
throw JsonApiInternalServerError(title: "Internal Server Error", detail: "A fetched model does not seem to have a valid id.")
}
let attributes = JsonApiAttributesObject(attributes: try JSON(node: self.attributes()))
/*
let relationships = JsonApiRelationshipsObject(relationshipObjects: [])
let resourceObject = JsonApiResourceObject(id: id, type: resourceType, attributes: attributes, relationships: relationships, links: links, meta: meta)
*/
public func makeNode(context: Context) throws -> Node {
throw JsonApiInternalServerError(title: "Internal Server Error", detail: "Subclasses of 'Model' must implement makeNode()")
}

public static func prepare(_ database: Database) throws {
throw JsonApiInternalServerError(title: "Internal Server Error", detail: "Subclasses of 'Model' must implement prepare(_:)")
}

// TODO: Finish implementing makeResourceObject
throw JsonApiInternalServerError()
public static func revert(_ database: Database) throws {
throw JsonApiInternalServerError(title: "Internal Server Error", detail: "Subclasses of 'Model' must implement revert(_:)")
}
}
224 changes: 224 additions & 0 deletions Sources/VaporJsonApi/Resources/JsonApiResourceRepresentable.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
//
// JsonApiResourceRepresentable.swift
// VaporJsonApi
//
// Created by Koray Koska on 06/05/2017.
//
//

import Vapor
import Fluent
import URI

public protocol JsonApiResourceRepresentable {

typealias JsonApiAttributes = [String: (getter: () throws -> NodeRepresentable?, setter: (_ value: NodeRepresentable) throws -> ())]

typealias JsonApiParentRelationships = [String: (type: JsonApiResourceModel.Type, getter: () throws -> Parent<JsonApiResourceModel>)]

typealias JsonApiChildrenRelationships = [String: (type: JsonApiResourceModel.Type, getter: () throws -> Children<JsonApiResourceModel>)]

typealias JsonApiSiblingsRelationships = [String: (type: JsonApiResourceModel.Type, getter: () throws -> Siblings<JsonApiResourceModel>)]

static var resourceType: JsonApiResourceType { get }

func attributes() throws -> JsonApiAttributes

/**
* Returns all `Parent` relationships defined as a key-value list where `key` represents
* the relationship `name` and value represents a tuple with the `type` of the
* relationship and a getter which returns the `Parent` relationship.
*
* See the following link for more information about Relations in Vapor:
* [Vapor Relations](https://vapor.github.io/documentation/fluent/relation.html)
*
* - returns: A dictionary which defines all `Parent` relationships for this ResourceModel.
*/
func parentRelationships() throws -> JsonApiParentRelationships

/**
* Returns all `Children` relationships defined as a key-value list where `key` represents
* the relationship `name` and value represents a tuple with the `type` of the
* relationship and a getter which returns the `Children` relationship.
*
* See the following link for more information about Relations in Vapor:
* [Vapor Relations](https://vapor.github.io/documentation/fluent/relation.html)
*
* - returns: A dictionary which defines all `Children` relationships for this ResourceModel.
*/
func childrenRelationships() throws -> JsonApiChildrenRelationships

/**
* Returns all `Siblings` relationships defined as a key-value list where `key` represents
* the relationship `name` and value represents a tuple with the `type` of the
* relationship an a getter which returns the `Siblings` relationship.
*
* See the following link for more information about Relations in Vapor:
* [Vapor Relations](https://vapor.github.io/documentation/fluent/relation.html)
*
* - returns: A dictionary which defines all `Siblings` relationships for this ResourceModel.
*/
func siblingsRelationships() throws -> JsonApiSiblingsRelationships
}

public extension JsonApiResourceRepresentable {

/**
* Returns this ResourceModel represented as a JsonApiResourceObject.
* If `baseUrl` is not nil, a links object will be generated where appropriate.
*
* - parameter baseUrl: The `baseUrl` for which links should be generated. Must have the following format: _scheme_://_host_:_port_ (Any `path`, `query` and `fragment` elements will be ignored.)
*/
public func makeResourceObject(resourceModel: JsonApiResourceModel, baseUrl: URI) throws -> JsonApiResourceObject {
guard let id = resourceModel.id?.string ?? resourceModel.id?.int?.string else {
throw JsonApiInternalServerError(title: "Internal Server Error", detail: "A fetched model does not seem to have a valid id.")
}
let type = type(of: self).resourceType

var attr = try JSON(node: [:])
for s in try attributes() {
if let node = try s.value.getter()?.makeNode() {
attr[s.key] = JSON(node)
}
}

let attributesObject = JsonApiAttributesObject(attributes: attr)

var relationshipObjects = [JsonApiRelationshipObject]()

let resourcePath = "/\(type.parse())/\(id)"

for p in try parentRelationships() {
relationshipObjects.append(try makeParentRelationshipObject(name: p.key, type: p.value.type, getter: p.value.getter, baseUrl: baseUrl, resourcePath: resourcePath))
}

for c in try childrenRelationships() {
relationshipObjects.append(try makeChildrenRelationshipObject(name: c.key, type: c.value.type, getter: c.value.getter, baseUrl: baseUrl, resourcePath: resourcePath))
}

for s in try siblingsRelationships() {
relationshipObjects.append(try makeSiblingsRelationshipObject(name: s.key, type: s.value.type, getter: s.value.getter, baseUrl: baseUrl, resourcePath: resourcePath))
}

let relationshipsObject = JsonApiRelationshipsObject(relationshipObjects: relationshipObjects)
return JsonApiResourceObject(id: id, type: type, attributes: attributesObject, relationships: relationshipsObject)
}

public func makeResourceIdentifierObject(resourceModel: JsonApiResourceModel, meta: JsonApiMeta?) throws -> JsonApiResourceIdentifierObject {
guard let id = resourceModel.id?.string ?? resourceModel.id?.int?.string else {
throw JsonApiInternalServerError(title: "Internal Server Error", detail: "A fetched model does not seem to have a valid id.")
}
return JsonApiResourceIdentifierObject(id: id, type: type(of: self).resourceType, meta: meta)
}

public func makeParentRelationshipObject (
name: String,
type: JsonApiResourceModel.Type,
getter: (() throws -> Parent<JsonApiResourceModel>)? = nil,
baseUrl: URI,
resourcePath: String,
meta: JsonApiMeta? = nil,
data: Bool = false
) throws -> JsonApiRelationshipObject {
let links = self.relationshipLinks(name: name, baseUrl: baseUrl, resourcePath: resourcePath)

// TDOD: Resource linkage with "nil" as data: Must also be handled
var resourceLinkage: JsonApiResourceLinkage? = nil
if data {
if let parent = try getter?().get() {
guard let parentId = parent.id?.string ?? parent.id?.int?.string else {
throw JsonApiInternalServerError(title: "Internal Server Error", detail: "A fetched model does not seem to have a valid id.")
}

resourceLinkage = JsonApiResourceLinkage(resourceIdentifierObject: JsonApiResourceIdentifierObject(id: parentId, type: type.resourceType))
}
}

return JsonApiRelationshipObject(name: name, links: links, data: resourceLinkage, meta: meta)
}

public func makeChildrenRelationshipObject (
name: String,
type: JsonApiResourceModel.Type,
getter: (() throws -> Children<JsonApiResourceModel>)? = nil,
baseUrl: URI,
resourcePath: String,
meta: JsonApiMeta? = nil,
data: Bool = false
) throws -> JsonApiRelationshipObject {
let links = self.relationshipLinks(name: name, baseUrl: baseUrl, resourcePath: resourcePath)

// TODO: Pagination
var resourceLinkage: JsonApiResourceLinkage? = nil
if data {
if let children = try getter?().all() {
var resourceIdentifierObjects = [JsonApiResourceIdentifierObject]()
for c in children {
guard let id = c.id?.string ?? c.id?.int?.string else {
throw JsonApiInternalServerError(title: "Internal Server Error", detail: "A fetched model does not seem to have a valid id.")
}
resourceIdentifierObjects.append(JsonApiResourceIdentifierObject(id: id, type: type.resourceType))
}

resourceLinkage = JsonApiResourceLinkage(resourceIdentifierObjects: resourceIdentifierObjects)
}
}

return JsonApiRelationshipObject(name: name, links: links, data: resourceLinkage, meta: meta)
}

public func makeSiblingsRelationshipObject (
name: String,
type: JsonApiResourceModel.Type,
getter: (() throws -> Siblings<JsonApiResourceModel>)? = nil,
baseUrl: URI,
resourcePath: String,
meta: JsonApiMeta? = nil,
data: Bool = false
) throws -> JsonApiRelationshipObject {
let links = self.relationshipLinks(name: name, baseUrl: baseUrl, resourcePath: resourcePath)

// TODO: Pagination
var resourceLinkage: JsonApiResourceLinkage? = nil
if data {
if let siblings = try getter?().all() {
var resourceIdentifierObjects = [JsonApiResourceIdentifierObject]()
for s in siblings {
guard let id = s.id?.string ?? s.id?.int?.string else {
throw JsonApiInternalServerError(title: "Internal Server Error", detail: "A fetched model does not seem to have a valid id.")
}
resourceIdentifierObjects.append(JsonApiResourceIdentifierObject(id: id, type: type.resourceType))
}

resourceLinkage = JsonApiResourceLinkage(resourceIdentifierObjects: resourceIdentifierObjects)
}
}

return JsonApiRelationshipObject(name: name, links: links, data: resourceLinkage, meta: meta)
}
}

fileprivate extension Int {

fileprivate var string: String {
return String(self)
}
}

fileprivate extension JsonApiResourceRepresentable {

fileprivate func relationshipLinks(name: String, baseUrl: URI, resourcePath: String) -> JsonApiLinksObject {
let selfUrl = self.relationshipSelfUrl(name: name, baseUrl: baseUrl, resourcePath: resourcePath)
let relatedUrl = self.relationshipRelatedUrl(name: name, baseUrl: baseUrl, resourcePath: resourcePath)

return JsonApiLinksObject(selfLink: selfUrl, relatedLink: relatedUrl)
}

fileprivate func relationshipSelfUrl(name: String, baseUrl: URI, resourcePath: String) -> URI {
return URI(scheme: baseUrl.scheme, host: baseUrl.host, port: baseUrl.port, path: "\(resourcePath)/relationships/\(name)")
}

fileprivate func relationshipRelatedUrl(name: String, baseUrl: URI, resourcePath: String) -> URI {
return URI(scheme: baseUrl.scheme, host: baseUrl.host, port: baseUrl.port, path: "\(resourcePath)/\(name)")
}
}
23 changes: 23 additions & 0 deletions Sources/VaporJsonApi/Toolbox/URIExtensions.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
//
// URIExtensions.swift
// VaporJsonApi
//
// Created by Koray Koska on 03/05/2017.
//
//

import URI

extension URI {

func baseUrl() -> String {
var portString = ""
if let port = port, let defaultPort = defaultPort, port != defaultPort {
portString = ":\(String(port))"
} else if let port = port {
portString = ":\(String(port))"
}

return "\(scheme)://\(host)\(portString)"
}
}

0 comments on commit 33416de

Please sign in to comment.