Продолжим говорить о Elm 0.18.
Elm. Удобный и неловкий
Elm. Удобный и неловкий. Композиция
В этой статье рассмотрим вопросы энкодеров/декодеров.
Декодеры/энкодеры используются для:
- преобразование ответов от сторонних ресурсов (Http, WebSocket и прочее);
- взаимодействия через порты. Подробнее про порты и нативный код расскажу в следующих статьях.
Как было описано ранее, Elm требует от нас обязательного преобразования внешних данных во внутренние типы приложения. За данный процесс отвечает модуль Json.Decode. Обратный процесс — Json.Encode.
Тип определяющий правила декодирования — Json.Decode.Decoder a. Данный тип параметризуется пользовательским типом и определяет каким образом из JSON объекта получить пользовательский тип a.
Для энкодера определен только тип результата — Json.Encode.Value.
Рассмотрим примеры для типа UserData.
type alias User = { id: Int , name: String , email: String }
Декодер для получения данных от пользователя:
decodeUserData : Json.Decode.Decoder UserData decodeUserData = Json.Decode.map3 UserData (Json.Decode.field “id” Json.Decode.int) (Json.Decode.field “name” Json.Decode.string) (Json.Decode.field “email” Json.Decode.string) encodeUserData : UserData -> Json.Encode.Value encodeUserData userData = Json.Encode.object [ ( “id”, Json.Encode.int userData.id) , ( “name”, Json.Encode.string userData.name) , ( “email”, Json.Encode.string userData.email) ]
Функция Json.Decode.map3 принимает конструктора типа UserData. Далее передаются три декодера типа в соответствии с порядок их объявления в пользовательском типе UserData.
Функция decodeUserData может быть использована совместно с функциями Json.Decode.decodeString или Json.Decode.decodeValue. Пример использования из предыдущих статей.
Функция encodeUserData производит кодирование пользовательского типа в тип Json.Encode.Value, который может быть отправлен наружу. По простому, Json.Encode.Value соответствует JSON объекту.
Простые варианты описаны в документации, их можно изучить без особых трудностей. Давайте рассмотрим жизненные случаи, которые требуют некоторой ловкости пальцев.
Декодеры Union типов или дискриминаторы типов
Предположим, у нас есть каталог товаров. И каждый товар может иметь произвольное количество атрибутов, каждый из которых имеет тип один из множества:
- целое число;
- строка;
- перечислимое. Предполагает выбор одного из допустимых значений.
JSON объект допустим следующего вида:
{ “id”: 1, “name”: “Product name”, “price”: 1000, “attributes”: [ { “id”: 1, “name”: “Length”, “unit”: “meters”, “value”: 100 }, { “id”: 1, “name”: “Color”, “unit”: “”, “value”: { “id”: 1, “label”: “red” } },... ] }
Остальные возможные типы не будем рассматривать, работа с ними аналогична. Тогда пользовательский тип товар имел бы следующее описание:
type alias Product = { id: Int , name: String , price: Int , attributes: Attributes } type alias Attributes = List Attribute type alias Attribute = { id: Int , name: String , unit: String , value: AttributeValue } type AttributeValue = IntValue Int | StringValue String | EnumValue Enum type alias Enum = { id: Int , label: String }
Слегка обсудим описанные типы. Есть товар (Product), который содержит список атрибутов/характеристик (Attributes). Каждый атрибут (Attribute) содержит идентификатор, наименование, размерность и значение. Значение атрибута описано как union type, по одному элементу для каждого типа значения характеристики. Тип Enum описывает одно значение из допустимого множества и содержит: идентификатор и человеко читаемое значение.
Описание декодера, префикс Json.Decode опустим для краткости:
decodeProduct : Decoder Product decodeProduct = map4 Product (field “id” int) (field “name” string) (field “price” int) (field “attributes” decodeAttributes) decodeAttributes : Decoder Attributes decodeAttributes = list decodeAttribute decodeAttribute : Decoder Attribute decodeAttribute = map4 Attribute (field “id” int) (field “name” string) (field “unit” string) (field “value” decodeAttributeValue) decodeAttributeValue : Decoder AttributeValue decodeAttributeValue = oneOf [ map IntValue int , map StringValue string , map EnumValue decodeEnumValue ] decodeEnumValue : Decoder Enum decodeEnumValue = map2 Enum (field “id” int) (field “label” string)
Весь трюк содержится в функции decodeAttributeValue. При помощи функции Json.Decode.oneOf перебираются все допустимые декодеры для значения атрибута. В случае успешной распаковки одним из декодоров, значение тегируется соответствующим тегом из типа AttributeValue.
Кодирование типа Product, может быть выполнено при помощи функции Json.Encode.object, в которую будут переданы закодированные атрибуты типа. Стоит уделить внимание кодированию типа AttributeValue. В соответствии с описанным ранее JSON объектом, энкодер может быть описан как, префикс Json.Encode опустим для краткости:
encodeAttributeValue : AttributeValue -> Value encodeAttributeValue attributeValue = case attributeValue of IntValue value -> int value StringValue value -> string value EnumValue value -> object [ (“id”, int value.id) , (“id”, string value.label) ]
Как видно, сопоставляем варианты типа и используем соответствующие энкодеры.
Изменим описание атрибутов и определим их с использование дискриминатора типа. JSON объект атрибута, в этом случае, имел бы следующий вид:
{ “id”: 1, “name”: “Attribute name”, “type”: “int”, “value_int”: 1, “value_string”: null, “value_enum_id”: null, “value_enum_label”: null }
В данном случае дискриминатор типа хранится в поле type и определяет в каком поле хранится значение. Такая структура описания вероятно не самая удобная, но часто встречающаяся. Стоит ли менять описание типа для данного JSON объекта, наверное не стоит, лучше держать типы в удобной форме для внутреннего использования. В этом случае описание декодера может иметь следующий вид:
decodeAttribute2 : Decoder Attribute decodeAttribute2 = field "type" string |> andThen decodeAttributeValueType |> andThen (\attributeValue -> map4 Attribute (field "id" int) (field "name" string) (field "unit" string) (succeed attributeValue) ) decodeAttributeValueType : String -> Decoder AttributeValue decodeAttributeValueType valueType = case valueType of "int" -> field "value_int" int |> Json.Decode.map IntValue "string" -> field "value_string" string |> Json.Decode.map StringValue "enum" -> map2 Enum (field "value_enum_id" int) (field "value_enum_label" string) |> Json.Decode.map EnumValue _ -> Json.Decode.fail "Unknown attribute type"
В функции decodeAttribute2 сначала декодируем дискриминатор, в случае успеха — декодируем значение атрибута. Далее декодируем оставшиеся поля типа Attribute, а в качестве значения поля value указываем ранее полученное значение.
Частичное обновление типа
Встречаются случаи, когда API возвращает не весь объект, а лишь его часть. Например, при регистрации просмотра или смене статуса объекта. В этом случае в сообщении удобнее сразу получать обновленный объект, а все манипуляции скрыть за декодером.
Для примера возьмем тот же товар, но добавим в него поле статус и будем обрабатывать запрос на закрытие товара.
type alias Product = { id: Int , name: String , price: Int , attributes: Attributes , status: Int } decodeUpdateStatus : Product -> Decoder Product decodeUpdateStatus product = field “status” int |> andThen (\newStatus -> succeed { product | status = newStatus} )
Или можно использовать функцию Json.Decode.map.
decodeUpdateStatus : Product -> Decoder Product decodeUpdateStatus product = field “status” int |> map (\newStatus -> { product | status = newStatus} )
Дата и время
Будем использовать функцию Date.fromString, которая реализована при помощи конструктора типа Date.
decodeDateFromString : Decoder Date.Date decodeDateFromString = string |> andThen (\stringDate -> case Date.fromString stringDate of Ok date -> Json.Decode.succeed date Err reason -> Json.Decode.fail reason )
Если в качестве представления даты/времени используется Timestamp, то декодер в общем виде можно описать как:
decodeDateFromTimestamp : Decoder Date.Date decodeDateFromTimestamp = oneOf [ int |> Json.Decode.map toFloat , float ] |> Json.Decode.map Date.fromTime
ссылка на оригинал статьи https://habr.com/post/424437/