Pull to refresh

Автодокументирование Perfect сервера

Reading time6 min
Views4.5K
image
В прошлый раз мы говорили, что Perfect не имеет автодокументирование реализуемого API из коробки. Возможно, что в следующей реализации разработчики пофиксят это досадное упущение. Но ничего не мешает нам позаботится об этом самостоятельно. Благо, необходимо добавить совсем не много кода.

У нас уже есть некоторая заглушка, которая позволяет посмотреть команды поддерживаемые сервером. Попробуем расширить возможности этого подхода.

Шаг 1: Запускаем ранее созданный Perfect сервер и вводим команду /cars чтоб получить JSON. Этот JSON копируем в jsonschema.net/# и формируем из него схему, которую добавляем как файл cars.json к проекту. Не забываем зайти в XCode -> Project -> Build phase и добавить созданный файл в список «Copy Files» так же, как мы делали это с 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>


Большой необходимости в этом нет, но, всегда лучше дать возможность получить схему JSON ответа. Разработчики клиентских приложений будут Вам благодарны.

Шаг 2: Добавляем интерфейс IRestHelp
IRestHelp.swift
<code>
import Foundation

protocol IRestHelp
{
    var details:String  {get}
    var params :String  {get}
    var schema :String  {get}
}
</code>



Шаг 3: Добавляем класс RestApi
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>


Для чего он нужен — станет понятно дальше.

Шаг 4: Добавляем класс RestApiReg
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>


Мне не удалось придумать более удачного названия для этого класса. Класс опосредует регистрацию новых API сервера.

Шаг 5: Заменяем класс HelpHandler следующим кодом:
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>



Шаг 6: Добавляем реализацию протокола IRestHelp в обработчик каждой команды, которая должна иметь автодокументирование. Этот шаг не обязательный. Те команды которые не будут поддерживать протокол будут иметь пустые значения в соотвествующих полях. К примеру, обработчик команды /list (класс CarsJson) выглядит у меня следующим образом:
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>



Шаг 7: Заменяем метод PerfectServerModuleInit() новым кодом:
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>



Запускаем!

Первоначальная страница осталась прежней.
Пробуем ввести /help в командной строке браузера:
image
Мы видим, что все команды выстроились в виде таблицы в алфавитном порядке и обзавелись гиперссылками. После входа на страницу помощи, уже нет необходимости вводить каждую из команд в командную строку браузера для ее выполнения. А в крайней правой колонке имеется ссылка на схему, для выполнения валидации этой команды.
В дальнейшем, мы сами можем использовать схему валидации для проверки правильности создаваемого нами ответа, до того, как он уйдет клиентскому приложению. И клиентское приложение, потенциально, может загружать схемы валидации прямо с сервера. C валидацией, таким образом, получается двойной профит.

Таблица, конечно, корявая. Использование CSS может существенно улучшить её эстетический вид. Но для работы, как правило, этого достаточно.
Первоначально было желание отобразить по запросу /help XML файл со схемой, которая выстроила бы данные в виде аналогичной таблицы. Однако, улучшать внешний вид HTML куда более увлекательное занятие, чем развлекаться с всевозможными отображениями XML.

P.S. Как стало известно, разработчики Perfect во всю трудятся направлении избавления от тяжелого наследия NextStep (Objective-С) с тем, чтоб дать возможность запускать сервер на * nix системе, и поэтому, некоторые привычные способы работы в пространстве имен NS сейчас считаются не кошерными.
Tags:
Hubs:
+6
Comments0

Articles