Линзы в http4k

Линзы в 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 Один Обязательный

Указание объекта #

Для каждого целевого элемента предоставляется объект-строитель, настроенный на взаимодействие с данным целевым элементом


Настройка преобразования типа данных #

Данный этап можно пропустить, если необходима null-строка, однако лучше всегда явно указывать тип для преобразования

http4k предлагает поддерживает преобразование данных. Данные приходят от пользователя в строковом формате и их далее необходимо преобразовать в корректный внутренний формат

Рассмотрим список преобразований типов для объекта-запроса Query

  • enum() — преобразование в перечисление по имени элемента
  • dateTime() — преобразование переданных данных в тип LocalDate
  • int() — преобразование в целочисленный вариант
  • 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

Перехват исключения #

Выброс исключения при использовании линз должен быть обработан:

Если пользователь самостоятельно может ввести некорректные данные, то необходимо обеспечить обработку данных на стороне сервера


Поведение разных терминаторов #

Терминатор Данные есть Данных нет Некорректные данные
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)

© A. M. Васильев, 2024, CC BY-SA 4.0, andrey@crafted.su