Add Starter.elm
This commit is contained in:
509
vendor/Json/Starter.elm
vendored
Normal file
509
vendor/Json/Starter.elm
vendored
Normal file
@@ -0,0 +1,509 @@
|
||||
module Json.Starter
|
||||
exposing
|
||||
( Field
|
||||
, Json
|
||||
, Outcome(..)
|
||||
, decodeAll
|
||||
, decodeObject
|
||||
, errorsToString
|
||||
, fromString
|
||||
, nullable
|
||||
, required
|
||||
, toDecoder
|
||||
)
|
||||
|
||||
{-| Decode JSON values into Elm values.
|
||||
|
||||
Let's say we have a `User` type, and some JSON that represents one:
|
||||
|
||||
type alias User =
|
||||
{ name : String
|
||||
, stars : Int
|
||||
, email : Maybe String
|
||||
, administrator : Bool
|
||||
}
|
||||
|
||||
userJSON : String
|
||||
userJSON =
|
||||
"""
|
||||
{
|
||||
"name": "Sam Sample",
|
||||
"num_stars": 5,
|
||||
"email": "sam@sample.com",
|
||||
"auth_token": "abc93fd3b",
|
||||
"is an admin": false,
|
||||
}
|
||||
"""
|
||||
|
||||
There are a few differences between the two.
|
||||
|
||||
- The JSON object has an `auth_token` field that we don't care about.
|
||||
- The `stars` field in the Elm record is called `num_stars` in the JSON object.
|
||||
- The `administrator` field in the Elm record is called `is an admin` in JSON.
|
||||
- The `email` field in the Elm record is a `Maybe String`, but JSON doesn't have `Maybe`.
|
||||
|
||||
We'll resolve all three of these differences in the course of decoding the JSON.
|
||||
Heres the function which will do this:
|
||||
|
||||
decodeUser : Json -> Result String User
|
||||
decodeUser json =
|
||||
case
|
||||
decodeObject json
|
||||
[ required [ "name" ]
|
||||
, nullable [ "email" ]
|
||||
, required [ "is an admin" ]
|
||||
, required [ "num_stars" ]
|
||||
]
|
||||
of
|
||||
[ String name, MaybeString email, Bool administrator, Number stars _ ] ->
|
||||
Ok
|
||||
{ name = name
|
||||
, email = email
|
||||
, administrator = administrator
|
||||
, stars = stars
|
||||
}
|
||||
|
||||
errors ->
|
||||
Err (Json.errorsToString errors)
|
||||
|
||||
This function takes a `Json` value (we'll see how to get one later) and returns
|
||||
`Ok` with the resulting `User` if decoding succeeded, and `Err` with an error
|
||||
message `String` if it failed. Here are some ways decoding could fail:
|
||||
|
||||
- The JSON was malformed
|
||||
- One of the expected fields (such as `email`) was missing
|
||||
- A value had an unexpected type (for example, `email` was a number)
|
||||
|
||||
The call to `decodeObject` specifies which fields we expect, and what their
|
||||
types should be.
|
||||
|
||||
decodeObject json
|
||||
[ required [ "name" ]
|
||||
, nullable [ "email" ]
|
||||
, required [ "is an admin" ]
|
||||
, required [ "num_stars" ]
|
||||
]
|
||||
|
||||
This says that we expect the `name`, `email`, `num_stars`, and `is an admin`
|
||||
fields to be present in the JSON. There may be additional fields present (such
|
||||
as `auth_token`) but we don't care about them.
|
||||
|
||||
The distinction between `required` and `nullable` is that `name`, `num_stars`,
|
||||
and `is an admin` must not be `null`, but we expect that `email` might be `null`.
|
||||
|
||||
Next we have a _case-expression_ with a branch that specifies the types of
|
||||
values we expect to find in this JSON.
|
||||
|
||||
[ String name, MaybeString email, Bool administrator, Number stars _] ->
|
||||
|
||||
The order we use for this list corresponds to the order we specified our
|
||||
`nullable` and `required` fields earlier. Because we had `required "name"`
|
||||
first in that list, we have `String name` first in this list, and so on.
|
||||
|
||||
Here's what these types do:
|
||||
|
||||
- `String name` confirms that `required "name"` found a string in the JSON, and assigns it to the `name` variable. That `name` variable's type is `String`.
|
||||
- `MaybeString email` confirms that `nullable "email"` found either a string or a `null` in the JSON. If it found `null`, it assigns `Nothing` to the `email : Maybe String` variable, and if it found a string, it assigns `Just` that string.
|
||||
- `Bool administrator` confirms that `required "is an admin"` found a boolean in the JSON, and assigns it to `administrator : Bool`.
|
||||
- `Number stars _` confirms that `required "num_stars"` found a number. Since JSON does not distinguish between integers and floats, `Number` presents both alternatives; use `Number intGoesHere _` to get the integer (with any decimals truncated) and `Nuumber _ floatGoesHere` to get the float. Since we wanted an integer, we used `Number stars _`. If `stars` were a `Float`, we would have used `Number _ stars` instead.
|
||||
|
||||
Now that we have the decoded values in Elm variables, we can use them to return
|
||||
a `User` record.
|
||||
|
||||
[ String name, MaybeString email, Bool administrator, Number stars _ ] ->
|
||||
Ok
|
||||
{ name = name
|
||||
, email = email
|
||||
, administrator = administrator
|
||||
, stars = stars
|
||||
}
|
||||
|
||||
If anything went wrong - for example, the JSON was malformed, or there was no
|
||||
`email` field, or `stars` was a string instead of a number - then the other
|
||||
branch of the _case-expression_ will get run:
|
||||
|
||||
errors ->
|
||||
Err (errorsToString errors)
|
||||
|
||||
The `errorsToString` function generates an error message string. This message
|
||||
is for the benefit of programmers, not users (who can't do much with a message
|
||||
like "this JSON was malformed") - so this string should be used for
|
||||
behind-the-scenes logging purposes, if at all.
|
||||
|
||||
|
||||
## Nested JSON
|
||||
|
||||
Sometimes we need to deal with JSON that has a nested structure. For example,
|
||||
what if our example JSON had `name` and `email` nested under a `user` field?
|
||||
|
||||
userJSON : String
|
||||
userJSON =
|
||||
"""
|
||||
{
|
||||
"user": {
|
||||
"name": "Sam Sample",
|
||||
"email": "sam@sample.com",
|
||||
},
|
||||
"num_stars": 5,
|
||||
"auth_token": "abc93fd3b",
|
||||
"is an admin": false,
|
||||
}
|
||||
"""
|
||||
|
||||
We can decode this by adding `"user"` before the `"name"` and `"email"` fields:
|
||||
|
||||
decodeObject json
|
||||
[ required [ "user", "name" ]
|
||||
, nullable [ "user", "email" ]
|
||||
, required [ "is an admin" ]
|
||||
, required [ "num_stars" ]
|
||||
]
|
||||
|
||||
This will use `user.name` for the first value in the list and `user.email` as
|
||||
the second value. We can add as many of these as we like; for example,
|
||||
`[ "user", "account", "email" ]` would decode from `user.account.email`.
|
||||
|
||||
We can also handle nested JSON by calling `decodeObject` multiple times.
|
||||
Let's say we had some JSON with a `users` field, which held a JSON array of
|
||||
objects that fit the pattern of the individual "user" JSON we decoded earlier.
|
||||
|
||||
We could write a function which decodes this JSON to a `List User` like so:
|
||||
|
||||
decodeUsers : Json -> Result String (List User)
|
||||
decodeUsers json =
|
||||
case decodeObject json [ required [ "users" ] ] of
|
||||
[ List usersJson ] ->
|
||||
decodeAll usersJson decodeUser
|
||||
|
||||
errors ->
|
||||
Err (errorsToString errors)
|
||||
|
||||
`decodeObject json [ required [ "users" ] ]` looks about the same as what we did
|
||||
before. The error branch looks identical to the one we wrote last time. The only
|
||||
difference is that it decodes to a `List` instead of a `String` or `Bool` like
|
||||
before:
|
||||
|
||||
[ List usersJson ] ->
|
||||
decodeAll usersJson decodeUser
|
||||
|
||||
This `usersJson` value has the type `List Json`, which means we can use a
|
||||
function like `decodeUser` to translate it from JSON into an Elm type.
|
||||
|
||||
The `decodeAll` function does exactly what we want here: it applies our
|
||||
`decodeUser` function to each of the `Json` values in `usersJson : List Json`.
|
||||
If all those decoding operations succeed, we get back an `Ok` with a `List User`
|
||||
inside. If any of them fail, we instead get back an `Err` with a `String` inside.
|
||||
|
||||
Finally, we can get `Json` values in one of two ways. One is directly from
|
||||
a string, using [`fromString : String -> Json`](#fromString). Another is
|
||||
by using [`toDecoder`](#toDecoder) which gives us a `Decoder` value that is
|
||||
commonly used by packages like [`elm-lang/http`](http://package.elm-lang.org/packages/elm-lang/http/latest).
|
||||
|
||||
|
||||
## Upgrading to Decoders
|
||||
|
||||
You now know how to turn raw JSON into validated and parsed Elm values.
|
||||
Congratulations! You're now ready to get started building Elm things that
|
||||
interact with JSON.
|
||||
|
||||
So why is this library called a Starter Kit, anyway?
|
||||
|
||||
The [Decoder](http://package.elm-lang.org/packages/elm-lang/core/5.1.1/Json-Decode)
|
||||
library is what most production applications end up using for their JSON needs.
|
||||
It's generally more flexible, composable, and concise than this Starter Kit,
|
||||
but it's not ideal for getting up and running because there are several more
|
||||
concepts to learn before you can start using it.
|
||||
|
||||
Fortunately, you don't need a JSON toolbox with all the trimmings to get up
|
||||
and running, and having a nice incremental progression is great for learning!
|
||||
(Not to worry - it's straightforward to upgrade to Decoders once you find
|
||||
yourself craving something more than what this library gives you.)
|
||||
|
||||
Until then, feel free to put it out of your mind and have fun building things!
|
||||
|
||||
|
||||
# JSON
|
||||
|
||||
@docs Json, fromString
|
||||
|
||||
|
||||
# Turning JSON into Result
|
||||
|
||||
@docs decodeObject, errorsToString
|
||||
@docs nullable, required
|
||||
@docs Outcome(..)
|
||||
@docs Field
|
||||
|
||||
|
||||
# Converting decoders
|
||||
|
||||
@docs toDecoder, decodeAll
|
||||
|
||||
-}
|
||||
|
||||
import Json.Decode as Decode exposing (Decoder)
|
||||
|
||||
|
||||
-- TYPES --
|
||||
|
||||
|
||||
{-| A JSON string, or a value from JavaScript (which has the same structure as
|
||||
JSON - for example, an event object).
|
||||
-}
|
||||
type Json
|
||||
= JsonString String
|
||||
| JsonValue Decode.Value
|
||||
|
||||
|
||||
{-| The outcome from decoding a [`Json`](#Json) value.
|
||||
|
||||
[`decodeObject`](#decodeObject) returns one of these.
|
||||
|
||||
-}
|
||||
type Outcome
|
||||
= String String
|
||||
| MaybeString (Maybe String)
|
||||
| Number Int Float
|
||||
| MaybeNumber (Maybe Int) (Maybe Float)
|
||||
| Bool Bool
|
||||
| MaybeBool (Maybe Bool)
|
||||
| Object Json
|
||||
| MaybeObject (Maybe Json)
|
||||
| List (List Json)
|
||||
| MaybeList (Maybe (List Json))
|
||||
| DecodingError String
|
||||
|
||||
|
||||
{-| A field in a [`Json`](#Json) object.
|
||||
-}
|
||||
type Field
|
||||
= Required (List String)
|
||||
| Optional (List String)
|
||||
|
||||
|
||||
|
||||
-- PUBLIC FUNCTIONS --
|
||||
|
||||
|
||||
{-| Convert a JSON string to a [`Json`](#Json) value.
|
||||
-}
|
||||
fromString : String -> Json
|
||||
fromString str =
|
||||
JsonString str
|
||||
|
||||
|
||||
{-| A required field in a [`Json`](#Json) object.
|
||||
-}
|
||||
required : List String -> Field
|
||||
required =
|
||||
Required
|
||||
|
||||
|
||||
{-| A field in a [`Json`](#Json) object that can potentially
|
||||
be `null`. This works like [`required`](#required), except
|
||||
the result will be a [`Maybe`](http://package.elm-lang.org/packages/elm-lang/core/5.1.1/Maybe).
|
||||
-}
|
||||
nullable : List String -> Field
|
||||
nullable =
|
||||
Optional
|
||||
|
||||
|
||||
{-| Decode some fields from an object. If the object has more fields than the
|
||||
requested ones, they will be ignored.
|
||||
-}
|
||||
decodeObject : Json -> List Field -> List Outcome
|
||||
decodeObject json fields =
|
||||
case List.foldr (decodeField json) (Ok []) fields of
|
||||
Ok outcomes ->
|
||||
outcomes
|
||||
|
||||
Err errors ->
|
||||
errors
|
||||
|
||||
|
||||
{-| Run a `(Json -> Result)` function on each `Json` value in a list. If they
|
||||
all return `Ok`, then return `Ok` with a list of those values. If any of them
|
||||
returns `Err`, return `Err`.
|
||||
|
||||
From the example in this module's introduction:
|
||||
|
||||
decodeUsers : Json -> Result String (List User)
|
||||
decodeUsers json =
|
||||
case decodeObject json [ required [ "users" ] ] of
|
||||
[ List usersJson ] ->
|
||||
decodeAll usersJson decodeUser
|
||||
|
||||
errors ->
|
||||
Err (errorsToString errors)
|
||||
|
||||
-}
|
||||
decodeAll : List Json -> (Json -> Result String a) -> Result String (List a)
|
||||
decodeAll values decodeFn =
|
||||
case List.foldr (decodeAllHelp decodeFn) (Ok []) values of
|
||||
Ok decodedValues ->
|
||||
Ok decodedValues
|
||||
|
||||
Err errors ->
|
||||
Err (String.join "\n\n" errors)
|
||||
|
||||
|
||||
decodeAllHelp :
|
||||
(Json -> Result String a)
|
||||
-> Json
|
||||
-> Result (List String) (List a)
|
||||
-> Result (List String) (List a)
|
||||
decodeAllHelp decodeFn json result =
|
||||
case ( result, decodeFn json ) of
|
||||
( Ok values, Ok value ) ->
|
||||
Ok (value :: values)
|
||||
|
||||
( (Err _) as outcomes, Ok _ ) ->
|
||||
outcomes
|
||||
|
||||
( Ok outcomes, Err err ) ->
|
||||
Err [ err ]
|
||||
|
||||
( Err errors, Err err ) ->
|
||||
Err (err :: errors)
|
||||
|
||||
|
||||
{-| Create a `Decoder` from a `(Json -> Result)` function.
|
||||
|
||||
One place this is useful is with the [`elm-lang/http`](http://package.elm-lang.org/packages/elm-lang/http)
|
||||
library, which works with `Decoder` values.
|
||||
|
||||
httpRequest : Http.Request User
|
||||
httpRequest =
|
||||
Http.get "http://example.com/user" (toDecoder decodeUser)
|
||||
|
||||
|
||||
-- See the decodeObject documentation for an example of
|
||||
-- how to implement a function like this:
|
||||
|
||||
decodeUser : Json -> Result String User
|
||||
|
||||
-}
|
||||
toDecoder : (Json -> Result String a) -> Decoder a
|
||||
toDecoder toResult =
|
||||
let
|
||||
resolve json =
|
||||
case toResult (JsonValue json) of
|
||||
Ok val ->
|
||||
Decode.succeed val
|
||||
|
||||
Err err ->
|
||||
Decode.fail err
|
||||
in
|
||||
Decode.value
|
||||
|> Decode.andThen resolve
|
||||
|
||||
|
||||
{-| Returns a string describing any errors that resulted
|
||||
from decoding a [`Json`](#Json) value.
|
||||
-}
|
||||
errorsToString : List Outcome -> String
|
||||
errorsToString errors =
|
||||
-- TODO handle the case where there are no DecodingError values in the list
|
||||
List.filterMap errorToMaybeString errors
|
||||
|> String.join "\n\n"
|
||||
|
||||
|
||||
|
||||
-- INTERNAL HELPERS --
|
||||
|
||||
|
||||
errorToMaybeString : Outcome -> Maybe String
|
||||
errorToMaybeString error =
|
||||
case error of
|
||||
DecodingError str ->
|
||||
Just str
|
||||
|
||||
_ ->
|
||||
Nothing
|
||||
|
||||
|
||||
decodeField :
|
||||
Json
|
||||
-> Field
|
||||
-> Result (List Outcome) (List Outcome)
|
||||
-> Result (List Outcome) (List Outcome)
|
||||
decodeField json field result =
|
||||
let
|
||||
decoder =
|
||||
case field of
|
||||
Required path ->
|
||||
Decode.at path outcomeDecoder
|
||||
|
||||
Optional path ->
|
||||
Decode.at path maybeOutcomeDecoder
|
||||
in
|
||||
case ( result, decodeFromDecoder decoder json ) of
|
||||
( Ok outcomes, Ok outcome ) ->
|
||||
Ok (outcome :: outcomes)
|
||||
|
||||
( (Err _) as outcomes, Ok _ ) ->
|
||||
outcomes
|
||||
|
||||
( Ok outcomes, Err err ) ->
|
||||
Err [ DecodingError err ]
|
||||
|
||||
( Err errors, Err err ) ->
|
||||
Err (DecodingError err :: errors)
|
||||
|
||||
|
||||
decodeFromDecoder : Decoder a -> Json -> Result String a
|
||||
decodeFromDecoder decoder json =
|
||||
case json of
|
||||
JsonString str ->
|
||||
Decode.decodeString decoder str
|
||||
|
||||
JsonValue val ->
|
||||
Decode.decodeValue decoder val
|
||||
|
||||
|
||||
outcomeDecoder : Decoder Outcome
|
||||
outcomeDecoder =
|
||||
Decode.oneOf
|
||||
[ Decode.map String Decode.string
|
||||
, Decode.map Bool Decode.bool
|
||||
, Decode.map numberFromTuple numberDecoder
|
||||
, Decode.map List (Decode.list jsonDecoder)
|
||||
, Decode.map Object jsonDecoder
|
||||
]
|
||||
|
||||
|
||||
numberFromTuple : ( Int, Float ) -> Outcome
|
||||
numberFromTuple ( int, float ) =
|
||||
Number int float
|
||||
|
||||
|
||||
maybeNumberFromTuple : Maybe ( Int, Float ) -> Outcome
|
||||
maybeNumberFromTuple tuple =
|
||||
case tuple of
|
||||
Just ( int, float ) ->
|
||||
MaybeNumber (Just int) (Just float)
|
||||
|
||||
Nothing ->
|
||||
MaybeNumber Nothing Nothing
|
||||
|
||||
|
||||
numberDecoder : Decoder ( Int, Float )
|
||||
numberDecoder =
|
||||
Decode.float
|
||||
|> Decode.map (\num -> ( truncate num, num ))
|
||||
|
||||
|
||||
maybeOutcomeDecoder : Decoder Outcome
|
||||
maybeOutcomeDecoder =
|
||||
Decode.oneOf
|
||||
[ Decode.map MaybeString (Decode.nullable Decode.string)
|
||||
, Decode.map MaybeBool (Decode.nullable Decode.bool)
|
||||
, Decode.map maybeNumberFromTuple (Decode.nullable numberDecoder)
|
||||
, Decode.map MaybeList (Decode.nullable (Decode.list jsonDecoder))
|
||||
, Decode.map MaybeObject (Decode.nullable jsonDecoder)
|
||||
]
|
||||
|
||||
|
||||
jsonDecoder : Decoder Json
|
||||
jsonDecoder =
|
||||
Decode.map JsonValue Decode.value
|
||||
Reference in New Issue
Block a user