-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
8 changed files
with
537 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,86 @@ | ||
import SwiftyJSON | ||
|
||
public struct User { | ||
public let id: String? | ||
public let name: String | ||
public let email: String | ||
|
||
public init(id: String?, name: String, email: String) { | ||
self.id = id | ||
self.name = name | ||
self.email = email | ||
} | ||
|
||
public init(json: JSON) throws { | ||
// Required properties | ||
guard json["name"].exists() else { | ||
throw ModelError.requiredPropertyMissing(name: "name") | ||
} | ||
guard let name = json["name"].string else { | ||
throw ModelError.propertyTypeMismatch(name: "name", type: "string", value: json["name"].description, valueType: String(describing: json["name"].type)) | ||
} | ||
self.name = name | ||
guard json["email"].exists() else { | ||
throw ModelError.requiredPropertyMissing(name: "email") | ||
} | ||
guard let email = json["email"].string else { | ||
throw ModelError.propertyTypeMismatch(name: "email", type: "string", value: json["email"].description, valueType: String(describing: json["email"].type)) | ||
} | ||
self.email = email | ||
|
||
// Optional properties | ||
if json["id"].exists() && | ||
json["id"].type != .string { | ||
throw ModelError.propertyTypeMismatch(name: "id", type: "string", value: json["id"].description, valueType: String(describing: json["id"].type)) | ||
} | ||
self.id = json["id"].string | ||
|
||
// Check for extraneous properties | ||
if let jsonProperties = json.dictionary?.keys { | ||
let properties: [String] = ["id", "name", "email"] | ||
for jsonPropertyName in jsonProperties { | ||
if !properties.contains(where: { $0 == jsonPropertyName }) { | ||
throw ModelError.extraneousProperty(name: jsonPropertyName) | ||
} | ||
} | ||
} | ||
} | ||
|
||
public func settingID(_ newId: String?) -> User { | ||
return User(id: newId, name: name, email: email) | ||
} | ||
|
||
public func updatingWith(json: JSON) throws -> User { | ||
if json["id"].exists() && | ||
json["id"].type != .string { | ||
throw ModelError.propertyTypeMismatch(name: "id", type: "string", value: json["id"].description, valueType: String(describing: json["id"].type)) | ||
} | ||
let id = json["id"].string ?? self.id | ||
|
||
if json["name"].exists() && | ||
json["name"].type != .string { | ||
throw ModelError.propertyTypeMismatch(name: "name", type: "string", value: json["name"].description, valueType: String(describing: json["name"].type)) | ||
} | ||
let name = json["name"].string ?? self.name | ||
|
||
if json["email"].exists() && | ||
json["email"].type != .string { | ||
throw ModelError.propertyTypeMismatch(name: "email", type: "string", value: json["email"].description, valueType: String(describing: json["email"].type)) | ||
} | ||
let email = json["email"].string ?? self.email | ||
|
||
return User(id: id, name: name, email: email) | ||
} | ||
|
||
public func toJSON() -> JSON { | ||
var result = JSON([ | ||
"name": JSON(name), | ||
"email": JSON(email), | ||
]) | ||
if let id = id { | ||
result["id"] = JSON(id) | ||
} | ||
|
||
return result | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
public protocol UserAdapter { | ||
func findAll(onCompletion: @escaping ([User], Error?) -> Void) | ||
func create(_ model: User, onCompletion: @escaping (User?, Error?) -> Void) | ||
func deleteAll(onCompletion: @escaping (Error?) -> Void) | ||
|
||
func findOne(_ maybeID: String?, onCompletion: @escaping (User?, Error?) -> Void) | ||
func update(_ maybeID: String?, with model: User, onCompletion: @escaping (User?, Error?) -> Void) | ||
func delete(_ maybeID: String?, onCompletion: @escaping (User?, Error?) -> Void) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
import Foundation | ||
|
||
public class UserMemoryAdapter: UserAdapter { | ||
var items: [String:User] = [:] | ||
|
||
public func findAll(onCompletion: @escaping ([User], Error?) -> Void) { | ||
onCompletion(items.map { $1 }, nil) | ||
} | ||
|
||
public func create(_ model: User, onCompletion: @escaping (User?, Error?) -> Void) { | ||
let id = model.id ?? UUID().uuidString | ||
// TODO: Don't overwrite if id already exists | ||
let storedModel = model.settingID(id) | ||
items[id] = storedModel | ||
onCompletion(storedModel, nil) | ||
} | ||
|
||
public func deleteAll(onCompletion: @escaping (Error?) -> Void) { | ||
items.removeAll() | ||
onCompletion(nil) | ||
} | ||
|
||
public func findOne(_ maybeID: String?, onCompletion: @escaping (User?, Error?) -> Void) { | ||
guard let id = maybeID else { | ||
return onCompletion(nil, AdapterError.invalidId(maybeID)) | ||
} | ||
guard let retrievedModel = items[id] else { | ||
return onCompletion(nil, AdapterError.notFound(id)) | ||
} | ||
onCompletion(retrievedModel, nil) | ||
} | ||
|
||
public func update(_ maybeID: String?, with model: User, onCompletion: @escaping (User?, Error?) -> Void) { | ||
delete(maybeID) { _, error in | ||
if let error = error { | ||
onCompletion(nil, error) | ||
} else { | ||
// NOTE: delete() guarantees maybeID non-nil if error is nil | ||
let id = maybeID! | ||
let model = (model.id == nil) ? model.settingID(id) : model | ||
self.create(model) { storedModel, error in | ||
onCompletion(storedModel, error) | ||
} | ||
} | ||
} | ||
} | ||
|
||
public func delete(_ maybeID: String?, onCompletion: @escaping (User?, Error?) -> Void) { | ||
guard let id = maybeID else { | ||
return onCompletion(nil, AdapterError.invalidId(maybeID)) | ||
} | ||
guard let removedModel = items.removeValue(forKey: id) else { | ||
return onCompletion(nil, AdapterError.notFound(id)) | ||
} | ||
onCompletion(removedModel, nil) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,221 @@ | ||
import Kitura | ||
import LoggerAPI | ||
import SwiftyJSON | ||
|
||
public class UserResource { | ||
private let adapter: UserAdapter | ||
private let path = "/api/Users" | ||
private let pathWithID = "/api/Users/:id" | ||
|
||
init(factory: AdapterFactory) throws { | ||
adapter = try factory.getUserAdapter() | ||
} | ||
|
||
func setupRoutes(router: Router) { | ||
router.all("/*", middleware: BodyParser()) | ||
|
||
router.get(path, handler: handleIndex) | ||
router.post(path, handler: handleCreate) | ||
router.delete(path, handler: handleDeleteAll) | ||
|
||
router.get(pathWithID, handler: handleRead) | ||
router.put(pathWithID, handler: handleReplace) | ||
router.patch(pathWithID, handler: handleUpdate) | ||
router.delete(pathWithID, handler: handleDelete) | ||
} | ||
|
||
private func handleIndex(request: RouterRequest, response: RouterResponse, next: @escaping () -> Void) { | ||
Log.debug("GET \(path)") | ||
// TODO: offset and limit | ||
adapter.findAll() { models, error in | ||
if let _ = error { | ||
// TODO: Send error object? | ||
Log.error("InternalServerError during handleIndex: \(error)") | ||
response.status(.internalServerError) | ||
} else { | ||
response.send(json: JSON(models.map { $0.toJSON() })) | ||
} | ||
next() | ||
} | ||
} | ||
|
||
private func handleCreate(request: RouterRequest, response: RouterResponse, next: @escaping () -> Void) { | ||
Log.debug("POST \(path)") | ||
guard let contentType = request.headers["Content-Type"], | ||
contentType.hasPrefix("application/json") else { | ||
response.status(.unsupportedMediaType) | ||
response.send(json: JSON([ "error": "Request Content-Type must be application/json" ])) | ||
return next() | ||
} | ||
guard case .json(let json)? = request.body else { | ||
response.status(.badRequest) | ||
response.send(json: JSON([ "error": "Request body could not be parsed as JSON" ])) | ||
return next() | ||
} | ||
do { | ||
let model = try User(json: json) | ||
adapter.create(model) { storedModel, error in | ||
if let _ = error { | ||
// TODO: Handle model errors (eg id conflict) | ||
Log.error("InternalServerError during handleCreate: \(error)") | ||
response.status(.internalServerError) | ||
} else { | ||
response.send(json: storedModel!.toJSON()) | ||
} | ||
next() | ||
} | ||
} catch let error as ModelError { | ||
response.status(.unprocessableEntity) | ||
response.send(json: JSON([ "error": error.defaultMessage() ])) | ||
next() | ||
} catch { | ||
Log.error("InternalServerError during handleCreate: \(error)") | ||
response.status(.internalServerError) | ||
next() | ||
} | ||
} | ||
|
||
private func handleDeleteAll(request: RouterRequest, response: RouterResponse, next: @escaping () -> Void) { | ||
Log.debug("DELETE \(path)") | ||
adapter.deleteAll() { error in | ||
if let _ = error { | ||
response.status(.internalServerError) | ||
} else { | ||
let result = JSON([]) | ||
response.send(json: result) | ||
} | ||
next() | ||
} | ||
} | ||
|
||
private func handleRead(request: RouterRequest, response: RouterResponse, next: @escaping () -> Void) { | ||
Log.debug("GET \(pathWithID)") | ||
adapter.findOne(request.parameters["id"]) { model, error in | ||
if let error = error { | ||
switch error { | ||
case AdapterError.notFound: | ||
response.status(.notFound) | ||
default: | ||
response.status(.internalServerError) | ||
} | ||
} else { | ||
response.send(json: model!.toJSON()) | ||
} | ||
next() | ||
} | ||
} | ||
|
||
private func handleReplace(request: RouterRequest, response: RouterResponse, next: @escaping () -> Void) { | ||
Log.debug("PUT \(pathWithID)") | ||
guard let contentType = request.headers["Content-Type"], | ||
contentType.hasPrefix("application/json") else { | ||
response.status(.unsupportedMediaType) | ||
response.send(json: JSON([ "error": "Request Content-Type must be application/json" ])) | ||
return next() | ||
} | ||
guard case .json(let json)? = request.body else { | ||
response.status(.badRequest) | ||
response.send(json: JSON([ "error": "Request body could not be parsed as JSON" ])) | ||
return next() | ||
} | ||
do { | ||
let model = try User(json: json) | ||
adapter.update(request.parameters["id"], with: model) { storedModel, error in | ||
if let error = error { | ||
switch error { | ||
case AdapterError.notFound: | ||
response.status(.notFound) | ||
case AdapterError.idConflict(let id): | ||
response.status(.conflict) | ||
response.send(json: JSON([ "error": "Cannot update id to a value that already exists (\(id))" ])) | ||
default: | ||
Log.error("InternalServerError during handleCreate: \(error)") | ||
response.status(.internalServerError) | ||
} | ||
} else { | ||
response.send(json: storedModel!.toJSON()) | ||
} | ||
next() | ||
} | ||
} catch let error as ModelError { | ||
response.status(.unprocessableEntity) | ||
response.send(json: JSON([ "error": error.defaultMessage() ])) | ||
next() | ||
} catch { | ||
Log.error("InternalServerError during handleReplace: \(error)") | ||
response.status(.internalServerError) | ||
next() | ||
} | ||
} | ||
|
||
private func handleUpdate(request: RouterRequest, response: RouterResponse, next: @escaping () -> Void) { | ||
Log.debug("PATCH \(pathWithID)") | ||
guard let contentType = request.headers["Content-Type"], | ||
contentType.hasPrefix("application/json") else { | ||
response.status(.unsupportedMediaType) | ||
response.send(json: JSON([ "error": "Request Content-Type must be application/json" ])) | ||
return next() | ||
} | ||
guard case .json(let json)? = request.body else { | ||
response.status(.badRequest) | ||
response.send(json: JSON([ "error": "Request body could not be parsed as JSON" ])) | ||
return next() | ||
} | ||
adapter.findOne(request.parameters["id"]) { model, error in | ||
if let error = error { | ||
switch error { | ||
case AdapterError.notFound: | ||
response.status(.notFound) | ||
default: | ||
response.status(.internalServerError) | ||
} | ||
return next() | ||
} | ||
do { | ||
let updatedModel = try model!.updatingWith(json: json) | ||
self.adapter.update(request.parameters["id"], with: updatedModel) { storedModel, error in | ||
if let error = error { | ||
switch error { | ||
case AdapterError.notFound: | ||
response.status(.notFound) | ||
case AdapterError.idConflict(let id): | ||
response.status(.conflict) | ||
response.send(json: JSON([ "error": "Cannot update id to a value that already exists (\(id))" ])) | ||
default: | ||
Log.error("InternalServerError during handleUpdate: \(error)") | ||
response.status(.internalServerError) | ||
} | ||
} else { | ||
response.send(json: storedModel!.toJSON()) | ||
} | ||
next() | ||
} | ||
} catch let error as ModelError { | ||
response.status(.unprocessableEntity) | ||
response.send(json: JSON([ "error": error.defaultMessage() ])) | ||
next() | ||
} catch { | ||
Log.error("InternalServerError during handleUpdate: \(error)") | ||
response.status(.internalServerError) | ||
next() | ||
} | ||
} | ||
} | ||
|
||
private func handleDelete(request: RouterRequest, response: RouterResponse, next: @escaping () -> Void) { | ||
Log.debug("DELETE \(pathWithID)") | ||
adapter.delete(request.parameters["id"]) { model, error in | ||
if let error = error { | ||
switch error { | ||
case AdapterError.notFound: | ||
response.send(json: JSON([ "count": 0 ])) | ||
default: | ||
response.status(.internalServerError) | ||
} | ||
} else { | ||
response.send(json: JSON([ "count": 1] )) | ||
} | ||
next() | ||
} | ||
} | ||
} |
Oops, something went wrong.