Auto Documentation Perfect Server

  • Tutorial
image
Last time, we said that Perfect does not have auto-documentation of an 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 capabilities 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 form a diagram 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 in the same way as we did with index.html
cars.json

{
  "$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"
  ]
}


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

Step 2 : Add IRestHelp Interface
IRestHelp.swift

import Foundation
protocol IRestHelp
{
    var details:String  {get}
    var params :String  {get}
    var schema :String  {get}
}



Step 3 : Add the RestApi Class
RestApi.swift

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
    }
}


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

Step 4 : Add the RestApiReg Class
RestApiReg.swift

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 }
            }
        }
    }
}


I was unable to 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

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(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 "
\(group):
"
    }
    private func tableWithCommnads(commands:[String:RestApi]) -> String {
        let sortedList = commands.keys.sort()
        var table = ""
        table += ""
        for name in sortedList
        {
            let cmd = commands[name]!
            table += ""
            table += ""
            if let help = cmd.handler as? IRestHelp
            {
                table += ""
                table += ""
                table += help.schema.characters.count > 0 ? "" : ""
            } else
            {
                table += ""
                table += ""
                table += ""
            }
             table += ""
        }
        table += "
\(name)\(help.details)\(help.params)/\(help.schema)
" return table } }



Step 6 : Add the implementation of the IRestHelp protocol to the handler of each command that should have auto-documentation. This step is optional. Those teams that will not support the protocol will have empty values ​​in the appropriate fields. For example, the / list command handler (CarsJson class) looks like this for me:
CarsJson.swift

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()
    }
}



Step 7 : Replace the PerfectServerModuleInit () method with the new code:
PerfectServerModuleInit ()

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()))
}



Launch!

The original page has remained the same.
We try to enter / help on the browser command line:
image
We see that all the commands are arranged in a table in alphabetical order and have got hyperlinks. After entering the help page, there is no need to enter each of the commands into the command line of the browser to execute it. And in the rightmost column there is a link to the circuit for the validation of this command.
In the future, we ourselves can use the validation scheme to verify the correctness of the response we create, before it goes to the client application. And the client application could potentially download validation schemes directly from the server. With validation, a double profit is thus obtained.

The table is, 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, at the request / help, an XML file with a schema that would line up the data in a similar table. However, improving the look of HTML is far more fun than having fun with all kinds of XML mappings.

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

Also popular now: