
В прошлый раз мы говорили, что 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 в командной строке браузера:

Мы видим, что все команды выстроились в виде таб��ицы в алфавитном порядке и обзавелись гиперссылками. После входа на страницу помощи, уже нет необходимости вводить каждую из команд в командную строку браузера для ее выполнения. А в крайней правой колонке имеется ссылка на схему, для выполнения валидации этой команды.
В дальнейшем, мы сами можем использовать схему валидации для проверки правильности создаваемого нами ответа, до того, как он уйдет клиентскому приложению. И клиентское приложение, потенциально, может загружать схемы валидации прямо с сервера. C валидацией, таким образом, получается двойной профит.
Таблица, конечно, корявая. Использование CSS может существенно улучшить её эстетический вид. Но для работы, как правило, этого достаточно.
Первоначально было желание отобразить по запросу /help XML файл со схемой, которая выстроила бы данные в виде аналогичной таблицы. Однако, улучшать внешний вид HTML куда более увлекательное занятие, чем развлекаться с всевозможными отображениями XML.
P.S. Как стало известно, разработчики Perfect во всю трудятся направлении избавления от тяжелого наследия NextStep (Objective-С) с тем, чтоб дать возможность запускать сервер на * nix системе, и поэтому, некоторые привычные способы работы в пространстве имен NS сейчас считаются не кошерными.
