📜 ⬆️ ⬇️

Autodocumenting Perfect Server

image
Last time we said that Perfect does not have autodocumentation of the implemented API out of the box. It is possible that in the next implementation, developers will fix this annoying omission. But nothing prevents us from taking care of this on our own. Fortunately, you need to add quite a bit of code.

We already have some stub that allows you to see the commands supported by the server. Let's try to expand the possibilities of this approach.

Step 1 : Run the previously created Perfect server and enter the / cars command to get JSON. This JSON is copied to jsonschema.net/# and we form the scheme from it, which we add as the cars.json file to the project. Do not forget to go to Xcode -> Project -> Build phase and add the created file to the “Copy Files” list just as we did with index.html
cars.json
<code> { "$schema": "http://json-schema.org/draft-04/schema#", "type": "object", "properties": { "address": { "type": "object", "properties": { "streetAddress": { "type": "string" }, "city": { "type": "string" } }, "required": [ "streetAddress", "city" ] }, "phoneNumber": { "type": "array", "items": { "type": "object", "properties": { "location": { "type": "string" }, "code": { "type": "integer" } }, "required": [ "location", "code" ] } } }, "required": [ "address", "phoneNumber" ] } </code> 


There is no great need for this, but it is always better to give the opportunity to get a JSON response scheme. Client application developers will be grateful to you.

Step 2 : Add the IRestHelp Interface
IRestHelp.swift
 <code> import Foundation protocol IRestHelp { var details:String {get} var params :String {get} var schema :String {get} } </code> 


')
Step 3 : Add the RestApi class
RestApi.swift
 <code> import PerfectLib class RestApi { var prefix:String? var commands:[String]? = nil var handler:RequestHandler? init(prefix:String?=nil, commands:[String]? = nil, handler:RequestHandler?=nil) { self.prefix = prefix self.commands = commands self.handler = handler } } </code> 


For what it is needed - it will become clear further.

Step 4 : Add the RestApiReg class
RestApiReg.swift
 <code> import Foundation import PerfectLib class RestApiReg { typealias APIList = [RestApi] // MARK: - Public Properties private var commandList = APIList() // MARK: - Private Properties private var globalRegistered = false // MARK: - Singletone Implementation private init() { } private class var sharedInstance: RestApiReg { struct Static { static var instance: RestApiReg? static var token: dispatch_once_t = 0 } dispatch_once(&Static.token) { Static.instance = RestApiReg() } return Static.instance! } // MARK: - Methods of class class func registration(list:APIList) { self.sharedInstance.commandList = list self.sharedInstance.linkAll() } class func add(command:RestApi) { self.sharedInstance.commandList += [command] self.sharedInstance.add(command) } class var list: APIList { return self.sharedInstance.commandList } // MARK: - Private methods private func linkAll() { Routing.Handler.registerGlobally() self.globalRegistered = true for api in self.commandList { self.add(api) } } private func add(api:RestApi) { if !self.globalRegistered { Routing.Handler.registerGlobally() } if let handler = api.handler { let prefix = api.prefix == nil ? "*" : api.prefix! if let commands = api.commands { Routing.Routes[prefix, commands] = { (_:WebResponse) in handler } } else { Routing.Routes[prefix] = { (_:WebResponse) in handler } } } } } </code> 


I could not come up with a better name for this class. The class mediates the registration of new server APIs.

Step 5 : Replace the HelpHandler class with the following code:
HelpHandler.swift
 <code> import Foundation import PerfectLib class HelpHandler:RequestHandler, IRestHelp { var details = "Show server comands list" var params = "" var schema = "" func handleRequest(request: WebRequest, response: WebResponse) { let list = self.createList() let html = ContentPage(title:"HELP", body:list).page(request.documentRoot) response.appendBodyString("\(html)") response.requestCompletedCallback() } private func createList() -> String { let list = RestApiReg.list var code = "" let allPrefixes = list.map { (api) -> String in api.prefix != nil ? api.prefix! : "/*" } let groups = Set<String>(allPrefixes).sort() for group in groups { let commandsApi = self.commandsByGroup(group, list:list) code += self.titleOfGroup(group) code += self.tableWithCommnads(commandsApi) } return code } private func commandsByGroup(group:String, list:RestApiReg.APIList) -> [String:RestApi] { var dict = [String:RestApi]() let commandsOfGroup = list.filter({ (api) -> Bool in api.prefix == group }) for api in commandsOfGroup { if let commands = api.commands { for cmd in commands { dict[cmd] = api } } else { dict[""] = api } } return dict } private func titleOfGroup(group:String) -> String { return " <B>\(group):</B> " } private func tableWithCommnads(commands:[String:RestApi]) -> String { let sortedList = commands.keys.sort() var table = "" table += "<table border = \"1px\" width=\"100%\">" for name in sortedList { let cmd = commands[name]! table += "<tr>" table += "<td width=\"15%\"><a href=\"\(name)\">\(name)</a></td>" if let help = cmd.handler as? IRestHelp { table += "<td>\(help.details)</td>" table += "<td>\(help.params)</td>" table += help.schema.characters.count > 0 ? "<td><a href=\"\(help.schema)\">/\(help.schema)</a></td>" : "<td></td>" } else { table += "<td></td>" table += "<td></td>" table += "<td></td>" } table += "</tr>" } table += "</table>" return table } } </code> 



Step 6 : Add the implementation of the IRestHelp protocol to the handler of each command that should be autodocumented. This step is optional. Those commands that do not support the protocol will have empty values ​​in the appropriate fields. For example, the handler of the / list command (CarsJson class) looks like this:
CarsJson.swift
 <code> import Foundation import PerfectLib class CarsJson:RequestHandler, IRestHelp { var details = "Show complexly JSON object" var params = "{}" var schema = "cars.json" func handleRequest(request: WebRequest, response: WebResponse) { let car1:[JSONKey: AnyObject] = ["Wheel":4, "Color":"Black"] let car2:[JSONKey: AnyObject] = ["Wheel":3, "Color":["mixColor":0xf2f2f2]] let cars = [car1, car2] let restResponse = RESTResponse(data:cars) response.appendBodyBytes(restResponse.array) response.requestCompletedCallback() } } </code> 



Step 7 : Replace the PerfectServerModuleInit () method with new code:
PerfectServerModuleInit ()
 <code> public func PerfectServerModuleInit() { RestApiReg.add(RestApi(handler: StaticFileHandler())) RestApiReg.add(RestApi(prefix: "GET", commands: ["/dynamic"], handler: StaticPageHandler(staticPage: "index.mustache"))) RestApiReg.add(RestApi(prefix: "GET", commands: ["/index", "/list"], handler: StaticPageHandler(staticPage: "index.html"))) RestApiReg.add(RestApi(prefix: "GET", commands: ["/hello"], handler: HelloHandler())) RestApiReg.add(RestApi(prefix: "GET", commands: ["/help"], handler: HelpHandler())) RestApiReg.add(RestApi(prefix: "GET", commands: ["/cars", "/car"], handler: CarsJson())) RestApiReg.add(RestApi(prefix: "POST", commands: ["/list"], handler: CarsJson())) } </code> 



Run!

The original page remained the same.
We try to enter / help in the browser command line:
image
We see that all the teams are arranged in a table in alphabetical order and got hyperlinks. After logging in to the help page, it is no longer necessary to enter each of the commands in the browser command line to execute it. And in the extreme right column there is a link to the schema to validate this command.
In the future, we ourselves can use the validation scheme to verify the correctness of the response we create, before it leaves the client application. And the client application, potentially, can load validation schemes directly from the server. With validation, this results in a double profit.

The table, of course, clumsy. Using CSS can significantly improve its aesthetic appearance. But for work, as a rule, this is enough.
Initially, there was a desire to display, on request / help, an XML file with a schema that would line the data into a similar table. However, improving the look and feel of HTML is much more exciting than having fun with all sorts of XML mappings.

PS As it became known, Perfect developers are all working towards getting rid of NextStep (Objective-C) heavy legacy in order to enable the server to run on * nix system, and therefore, some of the usual ways of working in the NS namespace are now considered not kosher.

Source: https://habr.com/ru/post/283432/


All Articles