Skip to content

Commit

Permalink
Add more routes and convenient functions
Browse files Browse the repository at this point in the history
Added getRelatedResource for routes to get relationship resources
  • Loading branch information
Koray Koska committed May 8, 2017
1 parent 8c34074 commit 9da7023
Show file tree
Hide file tree
Showing 8 changed files with 191 additions and 54 deletions.
14 changes: 14 additions & 0 deletions Sources/VaporJsonApi/Json/JsonApiDocument.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,22 @@ import Vapor

public class JsonApiDocument: JSONRepresentable {

public fileprivate(set) var nilData: Bool = false
public let data: JsonApiData?
public let errors: [JsonApiErrorObject]?
public let meta: JsonApiMeta?

/**
* Nil data initializer
*
*/
public init(meta: JsonApiMeta? = nil) {
self.nilData = true
self.data = nil
self.meta = meta
self.errors = nil
}

public init(data: JsonApiData, meta: JsonApiMeta? = nil) {
self.data = data
self.meta = meta
Expand All @@ -39,6 +51,8 @@ public class JsonApiDocument: JSONRepresentable {
jsonErrors.append(try error.makeJSON())
}
json["errors"] = JSON(jsonErrors)
} else if nilData {
json["data"] = JSON(Node(nilLiteral: ()))
} else {

// This should really *never* happens because of the unambiguous initializers but this
Expand Down
53 changes: 30 additions & 23 deletions Sources/VaporJsonApi/Json/JsonApiResourceObject.swift
Original file line number Diff line number Diff line change
Expand Up @@ -157,13 +157,13 @@ public class JsonApiRelationshipObject: JSONRepresentable {

public class JsonApiLinksObject: JSONRepresentable {

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

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

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

Expand All @@ -172,30 +172,37 @@ public class JsonApiLinksObject: JSONRepresentable {
}

public func makeJSON() throws -> JSON {
let selfJson: JSON
if let selfMeta = selfMeta {
selfJson = try JSON(node: [
"href": try selfLink.makeFoundationURL().absoluteString,
"meta": selfMeta.makeJSON()
])
} else {
selfJson = try JSON(Node(selfLink.makeFoundationURL().absoluteString))
var json = JSON([:])

if let selfLink = selfLink {
let selfJson: JSON
if let selfMeta = selfMeta {
selfJson = try JSON(node: [
"href": try selfLink.makeFoundationURL().absoluteString,
"meta": selfMeta.makeJSON()
])
} else {
selfJson = try JSON(Node(selfLink.makeFoundationURL().absoluteString))
}

json["self"] = selfJson
}

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

json["related"] = relatedJson
}

return try JSON(node: [
"self": selfJson,
"related": relatedJson
])
return json
}
}

Expand Down
13 changes: 11 additions & 2 deletions Sources/VaporJsonApi/Resources/JsonApiChildrenModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,21 @@ public struct JsonApiChildrenModel<T: JsonApiResourceModel> {
}

public let getter: JsonApiChildrenGetter
public let adder: JsonApiChildrenAdder
public let adder: JsonApiChildrenAdder?
public let replacer: JsonApiChildrenReplacer?

public init(getter: @escaping JsonApiChildrenGetter, adder: @escaping JsonApiChildrenAdder, replacer: JsonApiChildrenReplacer? = nil) {
public init(getter: @escaping JsonApiChildrenGetter, adder: JsonApiChildrenAdder? = nil, replacer: JsonApiChildrenReplacer? = nil) {
self.getter = getter
self.adder = adder
self.replacer = replacer
}

public init(parent: JsonApiResourceModel, foreignKey: String?, adder: JsonApiChildrenAdder? = nil, replacer: JsonApiChildrenReplacer? = nil) {
getter = {
return parent.children(foreignKey, T.self)
}

self.adder = adder
self.replacer = replacer
}
}
12 changes: 4 additions & 8 deletions Sources/VaporJsonApi/Resources/JsonApiParentModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,22 +19,18 @@ public struct JsonApiParentModel<T: JsonApiResourceModel> {
}

public let getter: JsonApiParentGetter
public let setter: JsonApiParentSetter
public let setter: JsonApiParentSetter?

public init(getter: @escaping JsonApiParentGetter, setter: @escaping JsonApiParentSetter) {
public init(getter: @escaping JsonApiParentGetter, setter: JsonApiParentSetter? = nil) {
self.getter = getter
self.setter = setter
}

public init<C: JsonApiResourceModel>(child: C, parentId: Node, foreignKey: String?) {
public init(child: JsonApiResourceModel, parentId: Node, foreignKey: String? = nil, setter: JsonApiParentSetter? = nil) {
getter = {
return try child.parent(parentId, foreignKey, T.self)
}

setter = { parent in
let p = Parent<T>(child: child, parentId: parentId, foreignKey: foreignKey)
var par = try p.get()
try par?.save()
}
self.setter = setter
}
}
109 changes: 92 additions & 17 deletions Sources/VaporJsonApi/Resources/JsonApiResourceController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import Vapor
import HTTP
import Fluent
import URI

public protocol JsonApiResourceController {
associatedtype Resource: JsonApiResourceModel
Expand All @@ -33,25 +34,14 @@ public extension JsonApiResourceController {

let query = req.jsonApiQuery()

let pageCount = query["page"]?["size"]?.string?.int ?? JsonApiConfig.defaultPageSize
if pageCount > JsonApiConfig.maximumPageSize {
throw JsonApiInvalidPageValueError(page: "page[size]", value: query["page"]?["size"]?.string ?? "*Nothing*")
}
let pageNumber = query["page"]?["number"]?.string?.int ?? 1
if pageNumber < 1 {
throw JsonApiInvalidPageValueError(page: "page[number]", value: query["page"]?["number"]?.string ?? "*Nothing*")
}
let resources = try Resource.query().limit(pageCount, withOffset: (pageNumber * pageCount) - pageCount).all()

var resourceObjects = [JsonApiResourceObject]()
for r in resources {
resourceObjects.append(try r.makeResourceObject(resourceModel: r, baseUrl: req.uri))
}
let page = try pageForQuery(query: query)
let pageNumber = page.pageNumber
let pageCount = page.pageCount

let data = JsonApiData(resourceObjects: resourceObjects)
let document = JsonApiDocument(data: data)
let resources = try Resource.query().limit(pageCount, withOffset: (pageNumber * pageCount) - pageCount).all()
let jsonDocument = try document(forResources: resources, baseUrl: req.uri)

return JsonApiResponse(status: .ok, document: document)
return JsonApiResponse(status: .ok, document: jsonDocument)
}

/**
Expand Down Expand Up @@ -82,6 +72,65 @@ public extension JsonApiResourceController {
return JsonApiResponse(status: .ok, document: document)
}

/**
* The `getRelatedResource` method is responsible for get requests to a relationship of a specific resource.
*
* Example: `/articles/5/author` for the author resource of the article with id `5`.
*
* - parameter req: The `Request` which fired this method.
* - parameter id: The id represented as a String which is the first route parameter for this request.
* - parameter relationshipType: The relationshipType represented as a String which is the relationship name as defined in the JsonApiResourceModel.
*/
func getRelatedResource(_ req: Request, _ id: String, _ relationshipType: String) throws -> ResponseRepresentable {

guard req.fulfillsJsonApiAcceptResponsibilities() else {
throw JsonApiNotAcceptableError(mediaType: req.acceptHeaderValue() ?? "*No Accept header*")
}

guard let resource = try Resource.find(id) else {
throw JsonApiRecordNotFoundError(id: id)
}

let query = req.jsonApiQuery()

if let parentModel = try resource.parentRelationships()[relationshipType] {
if let parent = try parentModel.getter().get() {
let resourceObject = try parent.makeResourceObject(resourceModel: parent, baseUrl: req.uri)
let data = JsonApiData(resourceObject: resourceObject)
let document = JsonApiDocument(data: data)

return JsonApiResponse(status: .ok, document: document)
} else {
let document = JsonApiDocument()
return JsonApiResponse(status: .ok, document: document)
}
} else if let childrenCollection = try resource.childrenRelationships()[relationshipType] {
let children = try childrenCollection.getter()

let page = try pageForQuery(query: query)
let pageNumber = page.pageNumber
let pageCount = page.pageCount

let resources = try children.limit(pageCount, withOffset: (pageNumber * pageCount) - pageCount).all()
let jsonDocument = try document(forResources: resources, baseUrl: req.uri)

return JsonApiResponse(status: .ok, document: jsonDocument)
} else if let siblingsCollection = try resource.siblingsRelationships()[relationshipType] {
let siblings = try siblingsCollection.getter()

let page = try pageForQuery(query: query)
let pageNumber = page.pageNumber
let pageCount = page.pageCount

let resources = try siblings.limit(pageCount, withOffset: (pageNumber * pageCount) - pageCount).all()
let jsonDocument = try document(forResources: resources, baseUrl: req.uri)

return JsonApiResponse(status: .ok, document: jsonDocument)
}

throw JsonApiRelationshipNotFoundError(relationship: relationshipType)
}

/**
* The `postResource` method is responsible for post requests to a specific resource.
*
Expand Down Expand Up @@ -120,3 +169,29 @@ public extension JsonApiResourceController {
return JsonApiResponse(status: .created, document: document)
}
}

fileprivate extension JsonApiResourceController {

fileprivate func pageForQuery(query: JSON) throws -> (pageCount: Int, pageNumber: Int) {
let pageCount = query["page"]?["size"]?.string?.int ?? JsonApiConfig.defaultPageSize
if pageCount > JsonApiConfig.maximumPageSize {
throw JsonApiInvalidPageValueError(page: "page[size]", value: query["page"]?["size"]?.string ?? "*Nothing*")
}
let pageNumber = query["page"]?["number"]?.string?.int ?? 1
if pageNumber < 1 {
throw JsonApiInvalidPageValueError(page: "page[number]", value: query["page"]?["number"]?.string ?? "*Nothing*")
}

return (pageCount: pageCount, pageNumber: pageNumber)
}

fileprivate func document(forResources resources: [JsonApiResourceModel], baseUrl: URI) throws -> JsonApiDocument {
var resourceObjects = [JsonApiResourceObject]()
for r in resources {
resourceObjects.append(try r.makeResourceObject(resourceModel: r, baseUrl: baseUrl))
}

let data = JsonApiData(resourceObjects: resourceObjects)
return JsonApiDocument(data: data)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public protocol JsonApiResourceRepresentable {

typealias JsonApiChildrenRelationships = [String: JsonApiChildrenModel<JsonApiResourceModel>]

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

static var resourceType: JsonApiResourceType { get }

Expand Down Expand Up @@ -100,8 +100,11 @@ public extension JsonApiResourceRepresentable {
relationshipObjects.append(try makeSiblingsRelationshipObject(name: s.key, type: s.value.type, getter: s.value.getter, baseUrl: baseUrl, resourcePath: resourcePath))
}

let selfLink = URI(scheme: baseUrl.scheme, host: baseUrl.host, port: baseUrl.port, path: "\(resourcePath)")
let links = JsonApiLinksObject(selfLink: selfLink)

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

public func makeResourceIdentifierObject(resourceModel: JsonApiResourceModel, meta: JsonApiMeta?) throws -> JsonApiResourceIdentifierObject {
Expand Down
26 changes: 24 additions & 2 deletions Sources/VaporJsonApi/Resources/JsonApiSiblingsModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,34 @@ public struct JsonApiSiblingsModel<T: JsonApiResourceModel> {
}

public let getter: JsonApiSiblingsGetter
public let adder: JsonApiSiblingsAdder
public let adder: JsonApiSiblingsAdder?
public let replacer: JsonApiSiblingsReplacer?

public init(getter: @escaping JsonApiSiblingsGetter, adder: @escaping JsonApiSiblingsAdder, replacer: JsonApiSiblingsReplacer? = nil) {
public init(getter: @escaping JsonApiSiblingsGetter, adder: JsonApiSiblingsAdder? = nil, replacer: JsonApiSiblingsReplacer? = nil) {
self.getter = getter
self.adder = adder
self.replacer = replacer
}

public init<S: JsonApiResourceModel>(toSibling: S, localKey: String? = nil, foreignKey: String? = nil) {
getter = {
return try toSibling.siblings(localKey, foreignKey)
}

// TODO: Don't allow duplicate linkage
self.adder = { siblings in
for s in siblings {
var p = Pivot<S, T>(toSibling, s)
try p.save()
}
}
self.replacer = { siblings in
let oldSiblings: Siblings<T> = try toSibling.siblings(localKey, foreignKey)
try oldSiblings.delete()
for s in siblings {
var p = Pivot<S, T>(toSibling, s)
try p.save()
}
}
}
}
11 changes: 11 additions & 0 deletions Sources/VaporJsonApi/Response/JsonApiError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,17 @@ public class JsonApiRecordNotFoundError: JsonApiGeneralError {
}
}

public class JsonApiRelationshipNotFoundError: JsonApiGeneralError {

public init(title: String, detail: String) {
super.init(status: Status.notFound, code: String(Status.notFound.statusCode), title: title, detail: detail)
}

public convenience init(relationship: String) {
self.init(title: "Relationship not found", detail: "The relationship identified by \(relationship) could not be found.")
}
}

public class JsonApiUnsupportedMediaTypeError: JsonApiGeneralError {

public init(title: String, detail: String) {
Expand Down

0 comments on commit 9da7023

Please sign in to comment.