Elm. Удобный и неловкий. Json.Encoder и Json.Decoder

Продолжим говорить о Elm 0.18.

Elm. Удобный и неловкий
Elm. Удобный и неловкий. Композиция

В этой статье рассмотрим вопросы энкодеров/декодеров.

Декодеры/энкодеры используются для:

  1. преобразование ответов от сторонних ресурсов (Http, WebSocket и прочее);
  2. взаимодействия через порты. Подробнее про порты и нативный код расскажу в следующих статьях.

Как было описано ранее, 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 типов или дискриминаторы типов

Предположим, у нас есть каталог товаров. И каждый товар может иметь произвольное количество атрибутов, каждый из которых имеет тип один из множества:

  1. целое число;
  2. строка;
  3. перечислимое. Предполагает выбор одного из допустимых значений.

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/

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *