Васильев Андрей Михайлович, 2022
Версии презентации
Вход — запрос от клиента, клиентом может выступать:
Выход — ответ сервера
Пользователь должен всегда получать корректный ответ, даже если составил некорректный запрос
Для каждого случая необходимо подготовить разумную стратегию обработки ошибок
fun interface Filter : (HttpHandler) -> HttpHandler
Фильтры позволяют выполнить действия, независимые от конкретного запроса:
Фильтр принимает в качестве аргумента HttpHandler и должен вернуть HttpHandler
val handler = { _: Request -> Response(OK) }
val myFilter = Filter {
next: HttpHandler -> {
request: Request ->
val start = System.currentTimeMillis()
val response = next(request)
val latency = System.currentTimeMillis() - start
println("I took $latency ms")
response
}
}
val latencyAndBasicAuth: Filter = ServerFilters.BasicAuth(
"my realm", "user", "password")
.then(myFilter)
val app: HttpHandler = latencyAndBasicAuth.then(handler)
Пакет org.http4k.filter содержит описание фильтров, поставляемых с помощью http4k
Подход к реализации:
Принципиальный подход к реализации
val errorFilter: Filter = { next: HttpHandler ->
{ request: Request ->
val response = next(request)
if (response.successfull) {
response
} else {
renderer(ErrorMessage(request))
}
}
}
val system = errorFilter.then(app)
Для всех данных, которые передаёт пользователь необходимо выполнить две базовые проверки:
Подходы к обработке данной ситуации:
Рассмотрим порядок обработки маршрута с переменной внутри. Стратегия решения проблемы — отображение сообщения, что искомый элемент не был найден
fun handler(renderer: TemplateRenderer): HttpHandler = { request ->
val paramemter: String? = request.path("parameter")
if (parameter == null) {
return@handler Response(UNSATISFIABLE_PARAMETERS).body(
renderer(EmptyParameterRequestFoundViewModel())
)
}
val entity: Entity? = getEntityByData(data)
if (entity == null) {
return@handler Response(NOT_FOUND).body(
renderer(EntityNotFoundViewModel())
)
}
Response(OK).body(renderer(ShowEntityViewModel(entity)))
}
При работе со сложными вложенными структурами приходится решать вопрос по получению и изменению свойств вложенных элементов
val structure = listOf(
mapOf(
"lang" to "kotlin",
"name" to "Веб-программирование",
)
)
Для редактирования структуры необходимо сначала получить доступ к элементу массива, а затем уже к свойству словаря
Для сильно вложенной структуры данных цепочка может быть гораздо длиннее. Это несёт следующие проблемы:
Линза — специальный объект, который позволяет осуществлять операции чтения и записи для конкретного вложенного свойства
Предположим, что мы создали линзу для получения переменной из пути:
val parameterLens = Path.of("parameter")
Для получения параметра:
val parameter:String = parameterLens(request)
Потенциально для установки значения можно было бы написать:
val newRequest = parameterLens("newData", request)
Но смысла в операции записи на запросе нет
Объект | Начальный тип данных | Применимо для объектов | Несколько | Ограничитель требований |
---|---|---|---|---|
Параметры запроса | String | Request | Один или много | Обязательный или необязательный |
Заголовок | String | Request / Response | Один или много | Обязательный или необязательный |
Переменная пути | String | Request | Один | Обязательный |
Поле формы | String | WebForm | Один или много | Обязательный или необязательный |
Тело | String | Request / Response | Один | Обязательный |
При создании линзы необходимо указать:
Данный этап можно пропустить, если необходима строка, однако лучше всегда явно указывать тип для преобразования
http4k предлагает большой набор функций расширения, с помощью которых можно настраивать преобразование данных. Рассмотрим список для типа Query
val request = Request(GET, Uri.of("http://server.ru"))
val requestWithQuery = request.query("data", "42")
val queryDataLens = Query.int().defaulted("data", 15)
val data = queryDataLens(requestWithQuery) // 42
Уровни необходимости параметра описываются в рамках класса LensSpec
Ключевое отличие — поведение при отсутствии целевого значения
Предыдущий рассмотренный сценарий обработки форм не удовлетворяет требованиям по обработке данных:
sequenceDiagram autonumber actor Пользователь participant Браузер participant Сервер Пользователь ->> Браузер: Нажимает на ссылку к странице формы Браузер ->> Сервер: Отправляет GET-запрос по адресу документа Сервер ->> Браузер: HTML-документ, содержащий форму Браузер ->> Пользователь: Показывает документ с формой Пользователь ->> Браузер: Заполняет интерактивные элементы на форме Пользователь ->> Браузер: Нажимает на кнопку отправки запроса Браузер ->> Сервер: POST-запрос с содержимым формы alt Форма не содержит ошибок Сервер ->> Браузер: Ответ со статусом 302, FOUND else Сервер ->> Браузер: HTML-документ, содержащий форму с данными пользователя и сообщениями об ошибках, 3 end
HTML форма представляет собой набор полей, которые пользователь заполняет
В рамках http4k используются следующие типы данных:
val ageField = FormField.int().required("age")
val nameField = FormField.map(::Name, Name::value).optional("name")
val feedbackFormBody = Body.webForm(Validator.Feedback, nameField, ageField).toLens()
val invalidRequest = Request(GET, "/")
.with(Header.CONTENT_TYPE of ContentType.APPLICATION_FORM_URLENCODED)
val invalidForm = feedbackFormBody(invalidRequest)
println(invalidForm.errors)
val validForm = strictFormBody(validRequest)
val age = ageField(validForm)
Удобно использовать объекты класса WebForm в обоих случаях
data class WebForm constructor(
val fields: Map<String, List<String>> = emptyMap(),
val errors: List<Failure> = emptyList()
)
Для проверки наличия данных в форме можно использовать следующие конструкции:
{% if model.form.errors is not empty %}
Присутствуют ошибки
{% endif %}
{% if model.form.fields contains "field" %}
Поле присутствует: {{ model.form.fields["field"] | first }}
{% endif %}
Для уменьшения количества повторений внутри шаблонов Pebble можно воспользоваться макросами, которые можно несколько раз вызывать внутри шаблона
{% macro input(type="text", name, placeholder="", form) %}
<input
type="{{ type }}"
name="{{ name }}"
placeholder="{{ placeholder }}"
{% if form.fields contains name %}
value="{{ form.fields[name] | first }}"
{% endif %}
>
{% endmacro %}
{{
input(name="count", type="number", placeholder="Число работников",
form=model.form)
}}
Согласно протоколу HTTP серверу желательно сообщать клиенту тип документа с помощью MIME типов. Также поступает и клиента
Для указания типа документа используется заголовок Content-Type
В http4k для решения этой задачи можно либо воспользоваться фильтрами:
val SetHtmlContentType = Filter { next ->
{ next(it).with(CONTENT_TYPE of TEXT_HTML) }
}
val app = routes(
return "/create" bind POST to SetHtmlContentType.then someHandler
)
Либо использовать линзы:
val renderer = PebbleTemplates().HotReload("src/main/resources")
val htmlView = Body.viewModel(renderer, TEXT_HTML).toLens()
val handler: HttpHandler {
Response(OK).with(htmlView of SomeViewModel(42))
}