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