Линзы в http4k #
Васильев Андрей Михайлович, 2024
Версии презентации
Линзы http4k #
В рамках библиотеки http4k линзы применяются для решения различных задач по взаимодействию со структурами данных:
- Для чтения данных из объектов запроса
- Для записи данный в объекты ответа
- Для взаимодействия (чтения и записи) данных во специальных хранилищах
Линзы библиотеки http4k определены в пакете org.http4k.lens
В рамках пакета определены два основных интерфейса:
- Lens — односторонняя
линза, позволяющая извлекать данные из сущности
- Для работы с переменной пути доступна PathLens
- BiDiLens —
двусторонняя линза, позволяющая читывать и модифицировать данные
- Для работы с переменной пути доступна BiDiPathLens
Для удобства построения линз применяются отдельные классы-строители
Создание линзы #
Для создания линзы необходимо указать следующие параметры:
- Объект, для которого формируется линза (вызвать базовый строитель)
- Настройки линзы, обычно состоит из определения типа данных для преобразования (указать параметры будущей линзы через строителя)
- Терминатор, т.е. параметры обязательности и идентификатор (создать линзу на основе параметров строителя)
Терминатор создаёт объект линзы из соответствующего объекта-строителя
Классы-линзы описаны в пакете org.http4k.lens
- строители описываются с помощью классов-спецификаций (интерфейс LensSpec),
- сами линзы описываются с помощью классов (интерфейс Lens)
Поддерживается два типа линз:
- Однонаправленные линзы, считывающие и преобразующие данные из источника
- Двунаправленные линзы, считывающие и записывающие данные в целевой объект
Для стандартных классов http4k в основном предлагаются двунаправленные линзы
Пример линзы http4k #
Создадим линзу для получения непустой строковой переменной из пути:
val parameterLens = Path.nonBlankString().of("parameter")
Для получения параметра необходимо применить лизу к источнику данных, к запросу:
val parameter: String = parameterLens(request)
- Линза для работы с путями является однонаправленной, поддерживает только считывание данных из пути
- Линза описана как
nonBlankString
, т.е. обязательно вернёт String - В случае ошибки, если в переменной пути нет данных, линза выбросит исключение
Линзы http4k #
Объект | Внутренний тип данных | Применимо для объектов | Количество | Необходимый |
---|---|---|---|---|
Параметры запроса | String | Request | Один или много | Обязательный или необязательный |
Заголовок | String | Request / Response | Один или много | Обязательный или необязательный |
Переменная пути | String | Request | Один | Обязательный |
Поле формы | String | WebForm | Один или много | Обязательный или необязательный |
Тело | String | Request / Response | Один | Обязательный |
Указание объекта #
Для каждого целевого элемента предоставляется объект-строитель, настроенный на взаимодействие с данным целевым элементом
- Параметры запроса: org.http4k.lens.Query
- Заголовок: org.http4k.lens.Header
- Переменные пути: org.http4k.lens.Path
- Поле формы: org.http4k.lens.FormField
- Тело: org.http4k.lens.Body
Настройка преобразования типа данных #
Данный этап можно пропустить, если необходима null-строка, однако лучше всегда явно указывать тип для преобразования
http4k предлагает поддерживает преобразование данных. Данные приходят от пользователя в строковом формате и их далее необходимо преобразовать в корректный внутренний формат
Рассмотрим список преобразований типов для объекта-запроса Query
enum()
— преобразование в перечисление по имени элементаdateTime()
— преобразование переданных данных в тип LocalDateint()
— преобразование в целочисленный вариантnonEmptyString()
— проверка строки на существованиеnonBlankString()
— проверка строки на наличие печатных символовuuid()
— преобразование в тип данных UUID
Данные могут быть преобразованы в любой формат с помощью метода map()
Терминатор #
Терминатор определяет уровнень необходимости параметра. Терминаторы описаны в интерфейсах LensSpec и BiDiLensSpec
- defaulted() — нужно указать название элемента, значение по умолчанию и описание
- optional() — параметр является необязательным, необходимо указать название
- required() — параметр является обязательным, необходимо указать название
Ключевое отличие — поведение при отсутствии целевого значения
- defaulted() возвращает значение по умолчанию
- optional() возвращает null-тип, но выбрасывает исключение, если параметра нет
- required() выбрасывает исключение LensFailure
Терминатор создаёт на основе спецификации (строителя, LensSpec) нужную линзу
Для Path-спецификаций доступен только терминатор of()
, т.к. переменная
обязательно будет присутствовать
Линзы для параметров запроса #
Рассмотрим пример линзы, которая извлекает целое число из параметров запроса
val queryDataLens = Query.int().defaulted("data", 15)
- Целью выступает класс
Query
- Далее указываем преобразование параметров из строки в целое число, метод
int()
- Затем указываем, что значением по умолчанию является число
15
При наличии данных будет выполнено их преобразование и получение
val request = Request(GET, Uri.of("http://example.ru"))
val requestWithQuery = request.query("data", "42")
val data = queryDataLens(requestWithQuery) // 42
При отсутствии данных линза вернёт значение по умолчанию
val request = Request(GET, Uri.of("http://example.ru"))
val data = queryDataLens(request) // 15
Данные в некорректном формате #
При отсутствии данных будет выброшено исключение
val request = Request(GET, Uri.of("http://example.ru"))
val requestWithQuery = request.query("data", "abc")
queryDataLens(requestWithQuery)
org.http4k.lens.LensFailure: query 'data' must be integer
Перехват исключения #
Выброс исключения при использовании линз должен быть обработан:
- Локальная обработка исключения
- Обработка исключения на уровне всего приложения с помощью фильтра, http4k предлагает https://www.http4k.org/api/org.http4k.filter/-server-filters/-catch-lens-failure.html
Если пользователь самостоятельно может ввести некорректные данные, то необходимо обеспечить обработку данных на стороне сервера
Поведение разных терминаторов #
Терминатор | Данные есть | Данных нет | Некорректные данные |
---|---|---|---|
defaulted | Данные | Значение по умолчанию | Исключение |
optional | Данные | null | Исключение |
required | Данные | Исключение | Исключение |
Каждый тип терминатора продуцирует линзу, способную выбросить исключение в одной или нескольких ситуациях
Проверка данных на стороне клиента (в браузере) #
- Можно предположить, что клиент формирует корректные данные
- В полях ввода на стороне браузера поддерживается проверка данных по типу
- Поддерживается проверка необходимости указания данных
Для обработки ситуации выхода за указанные границы достаточно воспользоваться обработкой на уровне всего приложения
Описание линз для формы #
HTML-форма представляет собой набор полей, которые пользователь заполняет
В рамках http4k используются следующие типы данных:
- FormField — описание требований к конкретному полю формы в форме линзы
- Body.webForm — описание спецификации линзы для всей формы
- WebForm — структура для хранения результатов проверки формы
Порядок использования элементов следующий:
- Для каждого поля ввода необходимо оформить требования с использованием FormField
- Необходимо объединить поля ввода в общую линзу для работы с формой
- Выполнить обработку запроса и проверить результаты обработки
Пример формы #
Форма для отправки обратной связи
<form method="POST">
<div class="mb-3">
<label for="age" class="form-label">Возраст</label>
<input type="number" class="form-control" id="age" name="age" required>
</div>
<div class="mb-3">
<label for="name" class="form-label">Имя</label>
<input type="password" class="form-control" id="password"
name="password" required>
</div>
<div class="mb-3 form-check">
<label for="feedback" class="form-label">Комментарии</label>
<textarea id="feedback" name="feedback" rows="10"></textarea>
</div>
<button type="submit" class="btn btn-primary">Отправить</button>
</form>
Обработка формы #
val ageField = FormField.int().required("age")
val nameField = FormField.nonEmptyString().required("name")
val feedbackField = FormField.nonEmptyString().optional("feedback")
val formLens = Body.webForm(Validator.Feedback,
ageField, nameField, feedbackField).toLens()
Обработка данных с формы
val form = formLens(request)
if (form.errors.isEmpty()) {
val age = ageField(form)
val feedback = feedbackField(form)
}
Можно использовать в тестах:
// Отправка пустой формы
val invalidRequest = Request(GET, "/")
.with(Header.CONTENT_TYPE of ContentType.APPLICATION_FORM_URLENCODED)
val invalidForm = formLens(invalidRequest)
println(invalidForm.errors)
// Отправка заполненной формы
val webForm = WebForm().with(ageField of 55, nameField of "Rita"))
val validRequest = Request(GET, "/").with(formLens of webForm)
val validForm = formLens(validRequest)
val age = ageField(validForm)