Продолжим говорить о 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
Автор: vturchaninov