ДемоКод

Когда мы имеем дело с JSON, всегда есть риск сломать его, особенно если он редактируется людьми.

С другой стороны, Elm со своими кодировщиками/декодерами очень строг к этому.

Давайте посмотрим в этом очень простом руководстве, как Elm может поддерживать создание редактора JSON.

Сначала мы определяем, какую структуру данных мы хотим хранить в формате JSON.

type alias Data =
    { text : String
    , logo : Logo
    , toggle : Bool
    }

text и toggle являются прямым представлением типа JSON. logo вместо этого является типом Elm, определенным как

type Logo
    = Elm
    | Strawberry
    | Watermelon

Теперь нам нужно кодировать, один для перехода из JSON в Elm и один для перехода из Elm в JSON.

jsonEncoder : Data -> Json.Encode.Value
jsonEncoder data =
    Json.Encode.object
        [ ( "text", Json.Encode.string <| data.text )
        , ( "logo", Json.Encode.string <| toString data.logo )
        , ( "toggle", Json.Encode.bool <| data.toggle )
        ]
jsonDecoder : Json.Decode.Decoder Data
jsonDecoder =
    Json.Decode.Pipeline.decode Data
        |> Json.Decode.Pipeline.required "text" Json.Decode.string
        |> Json.Decode.Pipeline.required "logo" (Json.Decode.string 
            |> Json.Decode.andThen logoDecoder)
        |> Json.Decode.Pipeline.required "toggle" Json.Decode.bool

Декодер для логотипа более сложен, потому что его нужно перевести в шрифт Elm.

В энкодере это преобразование делается просто с помощью toString.

Для декодера мы используем Json.Decode.andThen, которые создают декодеры, зависящие от предыдущих результатов. Таким образом, в этом случае он позволяет сначала декодировать значение как строку. В случае успеха результирующая строка декодируется с помощью logoDecoder.

Это logoDecoder:

logoDecoder : String -> Json.Decode.Decoder Logo
logoDecoder logoString =
    case logoString of
        "Elm" ->
            Json.Decode.succeed Elm
        "Strawberry" ->
            Json.Decode.succeed Strawberry
        "Watermelon" ->
            Json.Decode.succeed Watermelon
        _ ->
            Json.Decode.fail <| "I don't know a logo named " ++ 
                logoString

Затем нам нужно преобразовать строку в JSON и JSON в строку, что-то похожее на JSON.stringify() и JSON.parse() в Javascript.

Из данных в строку функция проста:

dataToString : Data -> String
dataToString data =
    Json.Encode.encode 4 <| jsonEncoder data

4 — это уровень отступа в возвращаемой строке.

Обратный путь немного сложнее. Сделаем это в два шага.

Сначала идем от String к Result:

stringToResult : String -> Result String Data
stringToResult string =
    Json.Decode.decodeString jsonDecoder string

Затем мы переходим от String к Data, но в этот момент нам также требуется последняя версия Data, чтобы в случае сбоя декодирования мы возвращали самые последние действительные данные. Таким образом, данные всегда будут в правильном формате.

stringToData : String -> Data -> ( Maybe String, Data )
stringToData string oldData =
    case stringToResult string of
        Ok newData ->
            ( Nothing, newData )
        Err err ->
            ( Just err, oldData )

Мы также возвращаем описание ошибки, если при декодировании произошла ошибка.

Чтобы увидеть поведение этих декодеров/кодировщиков в действии, достаточно создать текстовое поле с текстом JSON и попробовать отредактировать: http://guupa.com/elm-unbreakable-json/

Если наше нажатие клавиши приводит JSON в невозможное состояние, приложение восстановит предыдущую версию. (Кроме того, курсор будет нажат в конце текста, это известная проблема, которая на данный момент не имеет простого решения.)

Спасибо за чтение.