Perfect — REST сервер на Swift
Большинству iOS разработчиков рано или поздно становится тесно в мире iOS SDK. И причина, отнюдь, не в том, что у IOS недостаточно возможностей для серьезной разработки, а в том, что большинство современных серьезных приложений имеет клиент -серверную архитектуру, но разработчикам iOS оказывается доступен только Клиентский мир. Серверная разработка отдана на откуп серверным оленям программистам, которые, весьма ревностно относятся к требованиям мобильных разработчиков что-то изменить в API. Не добавляет оптимизма тот факт, что для реализации простейшего API приходится изучать другой язык программирования, со своими парадигмами и нюансами. Даже для того чтоб обкатать какую-нибудь пилотную идею приходится либо привлекать людей со стороны, либо погружаться в мир чужеродных грез (PHP, Pyton, Ruby, C#). Все ли так плохо в стане Objectove-C / Swift? Оказалось, что совсем нет. Немного погуглив на предмет серверной разработки я наткнулся на довольно любопытное начинание, претендующее на то, чтоб стать реальным кросс-платформенным решением — т. е. работающим одинаково хорошо как в среде OSX, так и *nix систем (про Windows не говорю, там С# вряд ли кто подвинет — слишком много вкусностей).
Perfect — как заявляют создатели проекта — Идеальный веб-сервер и инструментарий для разработчиков, использующих Swift язык программирования для создания приложений и других служб REST. Понятно, что «Идеальный» — это не более чем игра слов, но вместе с тем, после знакомства с предлагаемым решеним начинаешь склоняться к тому, что толика правды в этом утверждении есть.
В «прессе» пробегали статьи о том, что на подходе новый язык программирования, который может стать промышленным стандартом с легкой подачи Apple. Язык, который базируется на продвигаемом в массы Swift. Как правило, статьи об этом вызывали больше вопросов, и еще больше раздражения у тех, кому надоело все переучивать (Swift сам по себе довольно быстро меняется). Однако, углубившись в изучение вопроса, становится понятным, что все намного лучше чем, кажется.
Perfect — это не новый язык, серверной разработки. Perfect это серверное окружение, которое позволяет создавать REST API сервисы используя исключительно Swift последней реализации (на момент написания статьи Swift 2.2) Там нет ничего, выходящего за рамки того, что приходится делать ежедневно клиентским разработчикам.
Что будем делать: Создадим страницу-визитку (заглушку), для демонстрации ее при обращении к серверу. Продемонстрируем возможности легкого создания REST API сервисов, которые будут отвечать на GET/POST запросы. Продемонстрируем механизм динамического формирования статических страниц сайта. Причем, делать будем все это на Swift.
Любители Mono могут заявить, что давно уже существует инструмент кроссплатформенной серверной разработки, и с этим трудно не согласится, если бы одно, существенное «НО» — для того кто хорошо знаком с серверной частью C#/.net программирование в Mono превращается в боль и слезы, вызывая отвращение.
Евангелисты HATEOAS и HAL могут пройти стороной. Здесь будет упоминание REST в том виде, к которому привыкли мобильные разработчики. Чистоту такого REST можно ставить под сомнение, но от этого он не перестанет им быть. Нет нужды устраивать холивары по поводу, может ли африканский слон считаться слоном, или слоном можно считать только индийских слонов.
Итак, отправным пунктом путешествия станет создание соответствующего окружения. К сожалению, путь который предстоит пройти не столь уж очевиден, и сопряжен с некоторым количеством весьма странных манипуляций — от создания Workspace до изменения схем в Xcode. Пошаговое руководство продемонстрировано в видео подготовленного авторами проекта. Описывать каждый шаг в статье — это скорее тема для хаба «переводы». Мне бы хотелось сосредоточится на практическом применении возможностей Perfect, которые отсутствуют в роликах, или поданы там в чудовищном загадочном виде. К слову сказать, некоторые ролики опубликованные авторами скорее вредны, чем полезны.
Для начала разберемся в понятиях. Perfect состоит из двух частей: Библиотеки сервера (PerfectLib), и запускаемого приложения с минималистическим интерфейсом (Perfect Server). Оба приложения имеют открытый код, и теоретически, Вы сами можете из изменить / допилить под свои нужны. Однако, я строго не рекомендую Вам это делать. Лично у меня постоянно возникают поползновения что-то улучшить. Но следует учитывать, что Perfect не адаптирован для использования совместно с Swift 3. А создатели языка заявляют, что Swift 3 не будет иметь поддержки «сверху в низ», а это значит, что после выхода 3-й версии языка Perfect гарантировано будет обновлен, и Вам придется полностью избавится от уже внесенных изменений, чтоб апнутся на новую версию Swift.
Если Вы еще не дошли до этапа «Hello Perfect!» — самое время это сделать, Скачать необходимое окружение можно здесь. (Часть ссылок на сайте проекта — битые)
Далее, создадим файлы index.html и template.html, и затем добавим их в наш рабочий проект. После добавления зайдем в Build Phases и добавим шаг «New Copy Files Phase»
В конечном итоге окно должно будет выглядеть так:
My company name.
My name
is a link to another nifty site
This is a Header
This is a Medium Header
Send me mail at
mail@gmail.com.
This is a new paragraph!
This is a new paragraph!
This is a new sentence without a paragraph break, in bold italics.
{{TITLE}}
{{TITLE}}
{{BODY}}
{{FOOTER}}
По большей части все эти действия рассмотрены в видеоролике со страницы проекта: www.youtube.com/watch? v=J441eJ40PH4 Однако, рассмотренный случай позволяет либо хостить Web страницы, либо использовать REST API. Мы постараемся объединить обе потребности в одну возможность.
Полностью замените код PerfectServerModuleInit приведенным ниже кодом:
public func PerfectServerModuleInit()
{
Routing.Handler.registerGlobally()
// Root index.html page
Routing.Routes["*"] = { (_:WebResponse) in StaticFileHandler() }
// Request for static pages
Routing.Routes["GET", ["/index", "/list"]] = { (_:WebResponse) in return StaticPageHandler(staticPage: "index.html") }
// REST
Routing.Routes["GET", ["/hello"]] = { (_:WebResponse) in return HelloHandler() }
Routing.Routes["GET", ["/help"]] = { (_:WebResponse) in return HelpHandler() }
Routing.Routes["GET", ["/cars", "/car"]] = { (_:WebResponse) in return CarsJson() }
Routing.Routes["POST", ["/list"]] = { (_:WebResponse) in return CarsJson() }
}
Вызов метода PerfectServerModuleInit присутствует в проекте сервера (в моем случае MySwiftServer), но не привязан к какому либо классу. Я вынес его в отдельный .swift файл.
PerfectServerModuleInit — это точка входа в наш Web сервис. Он подобен функции main. Метод Вызывается со стороны библиотеки сервера. Позже я поясню что здесь происходит.
Теперь необходимо добавить еще несколько классов.
import Foundation
import PerfectLib
class HelloHandler:RequestHandler
{
func handleRequest(request: WebRequest, response: WebResponse)
{
response.appendBodyString("Hello World!\n")
response.appendBodyString("Hello Perfect!\n")
response.appendBodyString("Hello Swift Server!\n")
response.requestCompletedCallback()
}
}
Класс HelloHandler не делает ничего полезного, и используется, в основном для проверки того, что сервер запущен и доступен. Вы видите, что ответ сервера сводится к добавлению строки в выходной буфер, и обратный вызов клиента (браузера или клиентского приложения).
import Foundation
import PerfectLib
class StaticPageHandler:RequestHandler
{
var staticPage = "index.html"
internal init(staticPage:String) {
self.staticPage = staticPage
}
func handleRequest(request: WebRequest, response: WebResponse)
{
let file = ContentPage().page(request.documentRoot, pageFile: self.staticPage)
response.appendBodyString(file)
response.requestCompletedCallback()
}
}
Класс StaticPageHandler позволяет хостить статические страницы с указанными именами. «По-умолчанию» будет использована index.html, но, в принципе, это может быть любая другая страница добавленная в проект.
import Foundation
import PerfectLib
class HelpHandler:RequestHandler
{
func handleRequest(request: WebRequest, response: WebResponse)
{
let list = Routing.Routes.description.stringByReplacingString("+h", withString: "")
let html = ContentPage(title:"HELP", body:list).page(request.documentRoot)
response.appendBodyString("\(html)")
response.requestCompletedCallback()
}
}
Класс HelpHandler позволяет получить список команд, обрабатываемых сервером. Некоторые другие серверные окружения (к примеру, MS Framework 4.5.1) позволяют получить автодокументируемое REST API сервера. Это очень удобно для разработчиков мобильных приложений — не приходится дергать разработчиков сервера на предмет обслуживания / добавления команд.
import Foundation
import PerfectLib
class CarsJson:RequestHandler
{
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()
}
}
Класс CarsJson демонстрирует работу GET/POST запросов со сложной структурой возвращаемых данных. Возвращаемые данные представлены объектом AnyObject.
import Foundation
public class ContentPage:NSObject
{
private var title = ""
private var body = ""
private var footer = ""
public init(title:String="", body:String="", footer:String="Copyright (C) 2016 _MY_COMPANY_NAME_. All rights reserved.")
{
self.title = title
self.body = body
self.footer = footer
}
func page(webRoot:String, pageFile:String="template.html") -> String
{
let template = self.loadIndexHtml(webRoot, pageFile:pageFile)
let htmlBody = self.body.stringByReplacingString("\n", withString: "
")
var page = template
page = page.stringByReplacingString("{{TITLE}}", withString: self.title)
page = page.stringByReplacingString("{{BODY}}", withString: htmlBody)
page = page.stringByReplacingString("{{FOOTER}}", withString: self.footer)
return page
}
private func loadIndexHtml(webRoot:String, pageFile:String) -> String {
let fileManager = NSFileManager.defaultManager()
let directory = fileManager.currentDirectoryPath
let path = directory + "/\(webRoot)/\(pageFile)"
do {
return try String(contentsOfFile: path)
} catch {
print("File didn't create")
}
return "404 NOT FOUND"
}
}
Сервисный класс, возвращающий статическую страницу и делающий замену в соответствующих полях. По-умолчанию, используется шаблон template.html, но в принципе, может быть использован любой другой шаблон добавленный в проект.
import Foundation
import PerfectLib
public class RESTResponse
{
public var data:AnyObject = "" // Data from Server to Client
public var status = "" // HTTP status or other code of operation.
public var message = "" // This message Client can show in the Alert View
public var errors = [String]() // All real errors for Console
public init(data:AnyObject="", status:String="200", message:String="", errors:[String]=[])
{
self.data = data
self.status = status
self.message = message
self.errors = errors
}
public var array: [UInt8] {
let result = ["data":self.data, "status":self.status, "message":self.message, "errors":self.errors]
return serialization(result)
}
private func serialization(object:AnyObject) -> [UInt8]
{
do {
let jsonData = try NSJSONSerialization.dataWithJSONObject(object, options: NSJSONWritingOptions.PrettyPrinted)
let count = jsonData.length / sizeof(UInt8)
var jsonArray = [UInt8](count: count, repeatedValue: 0)
jsonData.getBytes(&jsonArray, length:count * sizeof(UInt8))
return jsonArray
} catch let error as NSError {
print(error)
}
return [UInt8]()
}
}
Класс RESTResponse ключевой класс использования REST API и, наиболее спорный.
Возвращаемый объект данных имеет следующую JSON структуру обертки: {
«data»:{…. },
«errors»:[… ],
"message":"",
"status":200
}
1) Некоторые разработчики категорически неприемлют такой формат. Но каких-либо убедительных аргументов против такого формата возвращаемых данных — я не встречал. Зато, есть определенные преимущества в типизированном формате. Поле «status» по исторической традиции имеет значение »200» в случае успешного выполнения операции. Но, непосредственного отношения к HTTP ответам не имеет. Их всегда можно вычитать из заголовков запроса, и там же они передаются на сторону клиента самим окружением сервера. В поле «status» можно передать значение имеющее смысловую нагрузку в рамках бизнес-логики web-сервиса, а не HTTP слоя. Поле «message» содержит строку, которую нужно продемонстрировать пользователю для уведомления о каких-то действиях на стороне сервера. К примеру, информацию об истечении сессии, особенно тогда, когда с точки зрения клиентского приложения все идет благополучно. Поле «errors»:[] представляет собой массив срок, который уведомляет клиент о каких-либо нештатных ситуациях. Эту информацию полезно сохранять и обрабатывать на стороне клиента или отправлять на специализированный log сервер, для дальнейшей обработки. Ну и наконец поле «data»:{…. } содержит в себе сложную структуру данных — именно то, что ожидается быть полученным на стороне клиента. Основное преимущество такого подхода в том, что если ответ сервера не удовлетворяет заданной схеме — он может быть с чистой совестью отвергнут клиентом. О том как организовать это можно почитать здесь: habrahabr.ru/post/283012 Те из разработчиков, кто является противником описанного подхода могут легко модифицировать свойство «array» так, чтоб возвращались сырые данные, без описанной обертки.
2) Я был несколько ошарашен тем подходом, которые предлагали использовать разработчики Perfect для возврата JSON объекта. В предложенном туториале, предполагалось формирование каждого объекта JSON путем последовательного построения дерева через перечисление пар «ключ/значение» вида [JSONKey: JSONValue] (что эквивалентно [String: AnyObject]). При этом предполагается использовать следующий код:
public var json: String
{
let result = ["data":self.data, "status":self.status, "message":self.message, "errors":self.errors]
let jsonEncoder = JSONEncoder ()
do {
let respString = try jsonEncoder.encode (result)
return respString
} catch let error as NSError {
print (error)
}
return »
}
……
response.appendBodyString (self.json)
response.requestCompletedCallback ()
Однако, этот код абсолютно неработоспособен даже с теми данными, которые сейчас содержаться в классе CarsJson. Кроме того, если следовать логики туториала создателей Perfect, на каждый возвращаемый объект придется писать свой класс для кодирования сложных структур. В предлагаемом мной варианте, вполне очевидно, можно несколькими строками кода сериализировать объект любом степени вложенности. В этом будет не сложно убедится после запуска сервиса.
Запускаем!
Если все сделано правильно, то мы получим следующую страницу в браузере:
Поздороваемся с сервером: введем в строку браузера /hello
Запросим сервер информацию о выполняемых командах:
Обратите внимание, что все команды разбиты на категории — GET, POST. Если Вы будете использовать другие предикаты — то они тоже появятся в этом списке.
Вместе с тем, команда /list одновременно присутствует в обоих списках. Именно она у нас добавлена PerfectServerModuleInit в команду GET и POST, но (!) имеет разные обработчики!
Если обратится к /list из командной строки браузера, будет выведена индексная страница.
Но если использовать специальное приложение (например, бесплатный Postman), то в теле Post запроса будем иметь ожидаемый JSON:
Обратите внимание, что JSON полностью соответствует той структуре, которая была сформирована в классе CarsJson.
Но что будет, если из списка GET удалить команду /listRouting.Routes["GET", ["/index", "/list"]] = { (_:WebResponse) in return StaticPageHandler(staticPage: "index.html") }
Получаем:
Таким нехитрым способом, подставляя стартовую страницу, или любую страницу заглушки, можно легко замаскировать использование любой REST команды как правило, если это не сервис с открытым API, в конце разработки команды вида /help удаляют или блокируют. Но можно воспользоваться этой возможностью по-другому: вывести для GET запроса информацию с деталями по команде, заставив работать обработчик статических страниц.
Совершенно ожидаемо, что команды /car и /cars вернут такую же структуру, но уже в виде веб-страницы.
При определенной сноровке создание REST API будет не сложнее создания ViewController c необходимой навигацией.
Любители экстрима могут попробовать запустить все это на Linux.