From 9c7f29e6f99c5df1ed9bfd384c24c724a4b933b6 Mon Sep 17 00:00:00 2001 From: Ryan Graham Date: Thu, 20 Apr 2017 12:43:05 -0700 Subject: [PATCH] yo swiftserver:model User --- .../Sources/Generated/AdapterFactory.swift | 3 + .../Sources/Generated/CRUDResources.swift | 2 + swift-blog/Sources/Generated/User.swift | 86 +++++++ .../Sources/Generated/UserAdapter.swift | 9 + .../Sources/Generated/UserMemoryAdapter.swift | 57 +++++ .../Sources/Generated/UserResource.swift | 221 ++++++++++++++++++ swift-blog/definitions/swift-blog.yaml | 142 ++++++++++- swift-blog/models/User.json | 19 ++ 8 files changed, 537 insertions(+), 2 deletions(-) create mode 100644 swift-blog/Sources/Generated/User.swift create mode 100644 swift-blog/Sources/Generated/UserAdapter.swift create mode 100644 swift-blog/Sources/Generated/UserMemoryAdapter.swift create mode 100644 swift-blog/Sources/Generated/UserResource.swift create mode 100644 swift-blog/models/User.json diff --git a/swift-blog/Sources/Generated/AdapterFactory.swift b/swift-blog/Sources/Generated/AdapterFactory.swift index 1c2106c..a05b354 100644 --- a/swift-blog/Sources/Generated/AdapterFactory.swift +++ b/swift-blog/Sources/Generated/AdapterFactory.swift @@ -9,5 +9,8 @@ public class AdapterFactory { self.manager = manager } + public func getUserAdapter() throws -> UserAdapter { + return UserMemoryAdapter() + } } diff --git a/swift-blog/Sources/Generated/CRUDResources.swift b/swift-blog/Sources/Generated/CRUDResources.swift index 9427a29..198595a 100644 --- a/swift-blog/Sources/Generated/CRUDResources.swift +++ b/swift-blog/Sources/Generated/CRUDResources.swift @@ -2,4 +2,6 @@ import Kitura import Configuration public func initializeCRUDResources(manager: ConfigurationManager, router: Router) throws { + let factory = AdapterFactory(manager: manager) + try UserResource(factory: factory).setupRoutes(router: router) } diff --git a/swift-blog/Sources/Generated/User.swift b/swift-blog/Sources/Generated/User.swift new file mode 100644 index 0000000..f845b4d --- /dev/null +++ b/swift-blog/Sources/Generated/User.swift @@ -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 + } +} diff --git a/swift-blog/Sources/Generated/UserAdapter.swift b/swift-blog/Sources/Generated/UserAdapter.swift new file mode 100644 index 0000000..c4263d9 --- /dev/null +++ b/swift-blog/Sources/Generated/UserAdapter.swift @@ -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) +} diff --git a/swift-blog/Sources/Generated/UserMemoryAdapter.swift b/swift-blog/Sources/Generated/UserMemoryAdapter.swift new file mode 100644 index 0000000..abed365 --- /dev/null +++ b/swift-blog/Sources/Generated/UserMemoryAdapter.swift @@ -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) + } +} diff --git a/swift-blog/Sources/Generated/UserResource.swift b/swift-blog/Sources/Generated/UserResource.swift new file mode 100644 index 0000000..2a90d6f --- /dev/null +++ b/swift-blog/Sources/Generated/UserResource.swift @@ -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() + } + } +} diff --git a/swift-blog/definitions/swift-blog.yaml b/swift-blog/definitions/swift-blog.yaml index 5017f0d..a5381c5 100644 --- a/swift-blog/definitions/swift-blog.yaml +++ b/swift-blog/definitions/swift-blog.yaml @@ -9,5 +9,143 @@ consumes: - application/json produces: - application/json -paths: {} -definitions: {} +paths: + '/Users/{id}': + get: + tags: + - User + summary: 'Find a model instance by {{id}}' + operationId: User.findOne + parameters: + - name: id + in: path + description: Model id + required: true + type: string + format: JSON + responses: + '200': + description: Request was successful + schema: + $ref: '#/definitions/User' + deprecated: false + put: + tags: + - User + summary: Put attributes for a model instance and persist it + operationId: User.replace + parameters: + - name: data + in: body + description: An object of model property name/value pairs + required: false + schema: + $ref: '#/definitions/User' + - name: id + in: path + description: Model id + required: true + type: string + format: JSON + responses: + '200': + description: Request was successful + schema: + $ref: '#/definitions/User' + deprecated: false + patch: + tags: + - User + summary: Patch attributes for a model instance and persist it + operationId: User.update + parameters: + - name: data + in: body + description: An object of model property name/value pairs + required: false + schema: + $ref: '#/definitions/User' + - name: id + in: path + description: Model id + required: true + type: string + format: JSON + responses: + '200': + description: Request was successful + schema: + $ref: '#/definitions/User' + deprecated: false + delete: + tags: + - User + summary: 'Delete a model instance by {{id}}' + operationId: User.delete + parameters: + - name: id + in: path + description: Model id + required: true + type: string + format: JSON + responses: + '200': + description: Request was successful + schema: + type: object + deprecated: false + /Users: + post: + tags: + - User + summary: Create a new instance of the model and persist it + operationId: User.create + parameters: + - name: data + in: body + description: Model instance data + required: false + schema: + $ref: '#/definitions/User' + responses: + '200': + description: Request was successful + schema: + $ref: '#/definitions/User' + deprecated: false + get: + tags: + - User + summary: Find all instances of the model + operationId: User.findAll + responses: + '200': + description: Request was successful + schema: + type: array + items: + $ref: '#/definitions/User' + deprecated: false + delete: + tags: + - User + summary: Delete all instances of the model + operationId: User.deleteAll + responses: + '200': + description: Request was successful + deprecated: false +definitions: + User: + properties: + id: + type: string + name: + type: string + email: + type: string + additionalProperties: false + required: + - name + - email diff --git a/swift-blog/models/User.json b/swift-blog/models/User.json new file mode 100644 index 0000000..50a998d --- /dev/null +++ b/swift-blog/models/User.json @@ -0,0 +1,19 @@ +{ + "name": "User", + "plural": "Users", + "classname": "User", + "properties": { + "id": { + "type": "string", + "id": true + }, + "name": { + "type": "string", + "required": true + }, + "email": { + "type": "string", + "required": true + } + } +}