Васильев Андрей Михайлович, 2023
Версии презентации
При работе со сложными вложенными структурами приходится решать вопрос по получению и изменению свойств вложенных элементов
val structure = listOf(
mapOf(
"lang" to "kotlin",
"name" to "Веб-программирование",
)
)
Для редактирования структуры необходимо сначала получить доступ к элементу массива, а затем уже к свойству словаря
Для сильно вложенной структуры данных цепочка может быть гораздо длиннее. Это несёт следующие проблемы:
Линза — специальный объект, который позволяет осуществлять операции чтения и записи для конкретного вложенного свойства
Предположим, что мы создали линзу для получения строковой переменной из пути:
val parameterLens = Path.nonEmptyString().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
enum()
— преобразование в перечислениеdateTime()
— преобразование переданных данных в тип LocalDateint()
— преобразование в целочисленный вариантnonEmptyString()
— проверка данных на пустотуuuid()
— преобразование в тип данных UUIDЛинзы могут быть преобразованы в любой формат с помощью метода map()
Уровни необходимости параметра описываются в рамках класса LensSpec
Ключевое отличие — поведение при отсутствии целевого значения
Терминатор создаёт на основе спецификации (строителя, LensSpec) нужную линзу
Рассмотрим пример линзы, которая извлекает целое число из параметров запроса
val queryDataLens = Query.int().defaulted("data", 15)
Query
int()
1
val request = Request(GET, Uri.of("http://server.ru"))
val requestWithQuery = request.query("data", "42")
val data = queryDataLens(requestWithQuery) // 42
Линза, к сожалению, выбросит исключение, если параметра нет в запросе:
val request = Request(GET, Uri.of("http://server.ru"))
val data = queryDataLens(request) // Будет выброшено исключение
К сожалению базовый интерфейс линз не позволяет обработать некорректные значения, а также частично заполненные данные. Для решения подобных проблем можно воспользоваться следующими методами-обёртками.
import org.http4k.lens.Lens
import org.http4k.lens.LensFailure
fun <IN : Any, OUT>lensOrNull(lens: Lens<IN, OUT?>, value: IN): OUT? =
try {
lens.invoke(value)
} catch (_: LensFailure) {
null
}
fun <IN : Any, OUT>lensOrDefault(lens: Lens<IN, OUT?>, value: IN, default: OUT): OUT =
try {
lens.invoke(value) ?: default
} catch (_: LensFailure) {
default
}
Вариант вызова данных функций-обёрток:
lensOrNull(fromLens, request)
Для данных от пользователя к веб-приложению используются HTML-формы
Формы являются частью языка HTML, для их описания используются следующие элементы:
CSS-фреймворк Zurb поддерживает стилизацию форм
<form method="POST">
<input type="text" name="start">
<textarea name="description">
<input type="submit" value="Отправить">
</form>
От клиента к серверу все данные передаются в виде строк, причём нет возможности для любого поля гарантировать, что оно будет заполнено
Рассмотрим обработку данных от формы на низком уровне
val parameters: Map<String, List<String?>> = request.formAsMap()
val firstValue: String? = parameters.findSingle("start")
formAsMap
возвращает Map<String, List<String?>>
findSingle
HTML-формы зачастую направлены на изменение данных на стороне сервера:
Для этих случаев используется не-GET-запрос. На такие запросы HTTP-сервер должен вернуть Redirect-ответ с указанием, куда следует сделать GET-запрос
val strings = mutableListOf<String>()
val formHandler: HttpHandler = { request ->
val form = request.form()
val newString = form.findSingle("text").orEmpty()
strings.add(newString)
Response(FOUND).header("Location", "/strings")
}
Предыдущий рассмотренный сценарий обработки форм не удовлетворяет требованиям по обработке данных:
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 webForm = WebForm().with(ageField of 55, nameField of Name("rita"))
val validRequest = Request(GET, "/").with(feedbackFormBody of webForm)
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" %}
Данные поля field: {{ model.form.fields["field"] | first }}
{% endif %}
Для уменьшения количества повторений внутри шаблонов Pebble можно воспользоваться макросами, которые можно несколько раз вызывать внутри шаблона
{% macro input(type="text", name, placeholder="", form) %}
<input
type="{{ type }}"
name="{{ name }}"
placeholder="{{ placeholder }}"
value="{{ form.fields[name] | first }}"
>
{% 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))
}