RESTful APIs¶
Разработка RESTful APIs с использованием Xitrum.
import xitrum.Action
import xitrum.annotation.GET
@GET("articles")
class ArticlesIndex extends Action {
def execute() {...}
}
@GET("articles/:id")
class ArticlesShow extends Action {
def execute() {...}
}
Подобным образом описываются POST, PUT, PATCH, DELETE, и OPTIONS запросы. Xitrum автоматически обрабатывает HEAD запросы как GET с пустым ответом.
Для HTTP клиентов не поддерживающих PUT и DELETE (например, обычные браузеры), используется метод POST c параметрами _method=put
или _method=delete
внутри тела запроса.
При старте веб приложения, Xitrum сканирует аннотации, создает таблицу маршрутизации и печатает ее в лог. Из лога понятно какое API приложение поддерживает на данный момент:
[INFO] Routes:
GET /articles quickstart.action.ArticlesIndex
GET /articles/:id quickstart.action.ArticlesShow
Маршруты (routes) автоматически строятся в духе JAX-RS и Rails. Нет необходимости объявлять все маршруты в одном месте. Допускается включать одно приложение в другое. Например, движок блога можно упаковать в JAR файл и подключить его в другое приложение, после этого у приложения появятся все возможности блога. Маршрутизация осуществляется в два направления, можно генерировать URL по контроллеру (обратная маршрутизация). Автоматическое документирование ваших маршрутов можно выполнить используя Swagger Doc.
Кэш маршрутов¶
Для более быстро скорости запуска, маршруты кэшируются в файл routes.cache
.
В режиме разработчика, этот файл не используется. В случае изменения зависимостей
содержащих маршруты, необходимо удалить routes.cache
. Этот файл не должен попасть
в ваши систему контроля версий.
Очередность маршрутов¶
Возможно вам потребуется организовать маршруты в определенном порядке.
/articles/:id --> ArticlesShow
/articles/new --> ArticlesNew
В данном случае необходимо что бы второй маршрут был проверен первым.
Для этих целей нужно использовать аннотацию First
:
import xitrum.annotation.{GET, First}
@GET("articles/:id")
class ArticlesShow extends Action {
def execute() {...}
}
@First // This route has higher priority than "ArticlesShow" above
@GET("articles/new")
class ArticlesNew extends Action {
def execute() {...}
}
Last
работает помещает маршрут на обработку последним.
Несколько маршрутов для одного контроллера¶
@GET("image", "image/:format")
class Image extends Action {
def execute() {
val format = paramo("format").getOrElse("png")
// ...
}
}
Точка в маршруте¶
@GET("articles/:id", "articles/:id.:format")
class ArticlesShow extends Action {
def execute() {
val id = param[Int]("id")
val format = paramo("format").getOrElse("html")
// ...
}
}
Регулярные выражения в маршруте¶
Регулярные выражения могут быть использованы для задания ограничений в маршруте:
GET("articles/:id<[0-9]+>")
Обработка не стандартных маршрутов¶
Использование символа /
не допускается в именах параметров. Если есть необходимость в его
использовании вы можете определить маршрут следующим образом:
GET("service/:id/proxy/:*")
Например, данный маршрут будет обрабатывать запросы:
/service/123/proxy/http://foo.com/bar
Извлечение значение из части :*
:
val url = param("*") // Будет "http://foo.com/bar"
Ссылка на контроллер¶
Xitrum пытается быть достаточно безопасным. Не пишите ссылки самостоятельно (в явном виде). Используйте генератор ссылок:
<a href={url[ArticlesShow]("id" -> myArticle.id)}>{myArticle.title}</a>
Редирект на контроллер¶
Читайте подробнее про редирект.
import xitrum.Action
import xitrum.annotation.{GET, POST}
@GET("login")
class LoginInput extends Action {
def execute() {...}
}
@POST("login")
class DoLogin extends Action {
def execute() {
...
// After login success
redirectTo[AdminIndex]()
}
}
GET("admin")
class AdminIndex extends Action {
def execute() {
...
// Check if the user has not logged in, redirect him to the login page
redirectTo[LoginInput]()
}
}
Допускается делать редирект на тот же самый контроллер с помощью метода redirecToThis()
.
Форвардинг (перенаправление) на контроллер¶
Используйте forwardTo[AnotherAction]()
. redirectTo
заставляет браузер делать новый запрос, в то
время как forwardTo
работает в рамках одного запроса.
Определение Ajax запроса¶
Используйте isAjax
.
// В контроллере
val msg = "A message"
if (isAjax)
jsRender("alert(" + jsEscape(msg) + ")")
else
respondText(msg)
Anti-CSRF¶
Для запросов отличных от GET Xitrum автоматически защищает приложение от Cross-site request forgery атаки.
Включите в шаблон antiCsrfMeta
:
import xitrum.Action
import xitrum.view.DocType
trait AppAction extends Action {
override def layout = DocType.html5(
<html>
<head>
{antiCsrfMeta}
{xitrumCss}
{jsDefaults}
<title>Welcome to Xitrum</title>
</head>
<body>
{renderedView}
{jsForView}
</body>
</html>
)
}
Тогда секция <head>
будет включать в себя csrf-token:
<!DOCTYPE html>
<html>
<head>
...
<meta name="csrf-token" content="5402330e-9916-40d8-a3f4-16b271d583be" />
...
</head>
...
</html>
Этот токен будет автоматически включен во все Ajax запросы jQuery как заголовок
X-CSRF-Token
если вы подключите xitrum.js. xitrum.js подключается вызовом jsDefaults
. Если вы не хотите
использовать jsDefaults
, вы можете подключить xitrum.js следующим образом (или посылать токен самостоятельно):
<script type="text/javascript" src={url[xitrum.js]}></script>
antiCsrfInput и antiCsrfToken¶
Xitrum использует CSRF токен из заголовка запроса с именем X-CSRF-Token
. Если заголовок
не установлен, Xitrum берет значение из параметра csrf-token
переданного в теле запроса
(не из URL).
Если вы вручную создаете формы, и не используйте мета тэг и xitrum.js как сказано выше,
то вам нужно использовать методы контроллера antiCsrfInput
или antiCsrfToken
:
form(method="post" action={url[AdminAddGroup]})
!= antiCsrfInput
form(method="post" action={url[AdminAddGroup]})
input(type="hidden" name="csrf-token" value={antiCsrfToken})
SkipCsrfCheck¶
Для некоторые API не требуется защита от CSRF атак, в этом случае проще всего пропустить эту проверку. Для этого дополнительно наследуйте свой контроллер от трейта xitrum.SkipCsrfCheck:
import xitrum.{Action, SkipCsrfCheck}
import xitrum.annotation.POST
trait Api extends Action with SkipCsrfCheck
@POST("api/positions")
class LogPositionAPI extends Api {
def execute() {...}
}
@POST("api/todos")
class CreateTodoAPI extends Api {
def execute() {...}
}
Управление маршрутами¶
Xitrum автоматически собирает маршруты при запуске. Для управления этими маршрутами используйте xitrum.Config.routes.
Например:
import xitrum.{Config, Server}
object Boot {
def main(args: Array[String]) {
// Вы можете поправить маршруты до запуска сервера
val routes = Config.routes
// Удаление маршрутов относящихся к конкретному классу
routes.removeByClass[MyClass]()
if (demoVersion) {
// Удаление маршрутов начинающихся с префикса
routes.removeByPrefix("premium/features")
// Допустимый вариант
routes.removeByPrefix("/premium/features")
}
...
Server.start()
}
}
Получение полных (сырых) данных запроса¶
Обычно когда mime тип запроса не соответствует application/x-www-form-urlencoded
,
предполагается что содержимое запроса будет обработано в ручном режиме.
Получение тела запроса в виде строки:
val body = requestContentString
JSON:
val myJValue = requestContentJValue // => JSON4S (http://json4s.org) JValue
val myMap = xitrum.util.SeriDeseri.fromJValue[Map[String, Int]](myJValue)
Если вам нужно получить полный доступ к запросу, используйте request.getContent. Он возвращает ByteBuf.
Документирование API¶
Из коробки вы можете документировать API и использованием Swagger.
Добавьте аннотацию @Swagger
к контроллеру который нужно задокументировать
Xitrum генерирует /xitrum/swagger.json.
Этот файл может быть использован в Swagger UI
для генерации интерактивной документации.
Xitrum включает Swagger UI, по пути /xitrum/swagger-ui
,
например http://localhost:8000/xitrum/swagger-ui.
Рассмотрим пример:
import xitrum.{Action, SkipCsrfCheck}
import xitrum.annotation.{GET, Swagger}
@Swagger(
Swagger.Tags("image", "APIs to create images"),
Swagger.Description("Dimensions should not be bigger than 2000 x 2000"),
Swagger.OptStringQuery("text", "Text to render on the image, default: Placeholder"),
Swagger.Produces("image/png"),
Swagger.Response(200, "PNG image"),
Swagger.Response(400, "Width or height is invalid or too big")
)
trait ImageApi extends Action with SkipCsrfCheck {
lazy val text = paramo("text").getOrElse("Placeholder")
}
@GET("image/:width/:height")
@Swagger( // <-- Inherits other info from ImageApi
Swagger.Summary("Generate rectangle image"),
Swagger.IntPath("width"),
Swagger.IntPath("height")
)
class RectImageApi extends Api {
def execute {
val width = param[Int]("width")
val height = param[Int]("height")
// ...
}
}
@GET("image/:width")
@Swagger( // <-- Inherits other info from ImageApi
Swagger.Summary("Generate square image"),
Swagger.IntPath("width")
)
class SquareImageApi extends Api {
def execute {
val width = param[Int]("width")
// ...
}
}
JSON для Swagger
будет генерироваться при доступе /xitrum/swagger
.
Swagger UI использует эту информацию для генерации интерактивной документации к API.
Возможные параметры на подобии Swagger.IntPath определяются шаблоном:
<Тип переменной><Тип параметра>
(обязательный параметр)Opt<Тип переменной><Тип параметра>
(опциональный параметр)
Типы переменных: Byte, Int, Int32, Int64, Long, Number, Float, Double, String, Boolean, Date, DateTime
Типы параметров: Path, Query, Body, Header, Form
Подробнее о типах переменных и типах параметров.