tl;dr: Here is an API cloned from elm/json
and NoRedInk/elm-json-decode-pipeline
that allows you to do:
type alias Email =
{ address : String
, id : Id
, tags : List Tag
}
type alias Tag =
{ id : Id
, name : String
}
decodeTag : IndexedDecoder Tag
decodeTag =
Decode.succeed Tag
|> Decode.index
|> Decode.required "name" Decode.string
decodeTags : IndexedDecoder (List Tag)
decodeTags =
Decode.list decodeTag
decodeEmail : IndexedDecoder Email
decodeEmail =
Decode.succeed Email
|> Decode.required "address" Decode.string
|> Decode.index
|> Decode.required "tags" decodeTags
decodeEmails : IndexedDecoder (List Email)
decodeEmails =
Decode.list decodeEmail
json : String
json =
"""
[
{ "address": "foo@mail.com",
"tags": [
{"name": "tag1"},
{"name": "tag2"}
]
},
{ "address": "bar@mail.com"
, "tags": [
{"name": "tag3"}
]
}
]
"""
main =
case Decode.decodeString decodeEmails json 0 of
Ok ( emails, id ) ->
viewEmails emails
Err err ->
text (Decode.errorToString err)
to produce:
foo@mail.com[2]
tag1[0]
tag2[1]
bar@mail.com[4]
tag3[3]
Basically, you have an additional Decode.index
function that you can place in the pipeline where the generated id
is needed.
Ellie all-in-one dirty example: https://ellie-app.com/43dX7qnC6Csa1
Details:
I think this is an interesting problem that could deserve an official package, and your last approach seems close to mine.
Here is a start using a random positive id with Random.int
and an API close to NoRedInk/elm-json-decode-pipeline
:
Given the following (incomplete but working) Indexed.Decode
module:
module Indexed.Decode exposing
( Id
, IndexedDecoder
, decodeString
, decoder
, errorToString
, index
, list
, map
, map2
, required
, string
, succeed
)
import Json.Decode as Decode exposing (Decoder)
import Json.Decode.Pipeline as Pipeline
import Random exposing (Generator, Seed)
import Random.Extra as Random
type alias Id =
Int
type alias Error =
Decode.Error
type alias IndexedDecoder a =
Decoder (Generator a)
custom : IndexedDecoder a -> IndexedDecoder (a -> b) -> IndexedDecoder b
custom =
Decode.map2 (Random.map2 (|>))
decoder : Decoder a -> IndexedDecoder a
decoder dec =
Decode.map Random.constant dec
decodeString : IndexedDecoder a -> String -> Seed -> Result Decode.Error ( a, Seed )
decodeString dec str seed =
Decode.decodeString (step dec seed) str
errorToString : Error -> String
errorToString =
Decode.errorToString
field : String -> IndexedDecoder a -> IndexedDecoder a
field key dec =
Decode.field key dec
index : IndexedDecoder (Id -> a) -> IndexedDecoder a
index dec =
let
genId =
Random.int 0 Random.maxInt
in
Decode.map (\gen -> Random.map2 (|>) genId gen) dec
list : IndexedDecoder a -> IndexedDecoder (List a)
list dec =
Decode.map Random.combine (Decode.list dec)
map : (a -> value) -> IndexedDecoder a -> IndexedDecoder value
map f dec =
Decode.map (Random.map f) dec
map2 : (a -> b -> value) -> IndexedDecoder a -> IndexedDecoder b -> IndexedDecoder value
map2 f dec1 dec2 =
Decode.map2 (Random.map2 f) dec1 dec2
required : String -> IndexedDecoder a -> IndexedDecoder (a -> b) -> IndexedDecoder b
required key valDecoder dec =
custom (field key valDecoder) dec
step : IndexedDecoder a -> Seed -> Decoder ( a, Seed )
step dec seed =
Decode.map (\gen -> Random.step gen seed) dec
string : IndexedDecoder String
string =
decoder Decode.string
succeed : a -> IndexedDecoder a
succeed a =
Decode.succeed (Random.constant a)
You can use it like the Json pipeline API with the addition of the index
function where an index is needed:
module Main exposing (main)
import Html exposing (Html, div, span, text)
import Html.Attributes exposing (style)
import Indexed.Decode as Decode exposing (Id, IndexedDecoder)
import Json.Encode as Encode
import Random
type alias Email =
{ address : String
, id : Id
, tags : List Tag
}
type alias Tag =
{ id : Id
, name : String
}
viewEmails : List Email -> Html msg
viewEmails emails =
div [] (List.map viewEmail emails)
viewEmail : Email -> Html msg
viewEmail email =
div []
[ text email.address
, viewId email.id
, viewTags email.tags
]
viewTags : List Tag -> Html msg
viewTags tags =
div [ style "margin-left" "8px" ]
(List.map viewTag tags)
viewTag : Tag -> Html msg
viewTag tag =
div []
[ text tag.name
, viewId tag.id
]
viewId : Id -> Html msg
viewId id =
text ("[" ++ String.fromInt id ++ "]")
json : String
json =
"""
[
{ "address": "foo@mail.com",
"tags": [
{"name": "tag1"},
{"name": "tag2"}
]
},
{ "address": "bar@mail.com"
, "tags": [
{"name": "tag3"}
]
}
]
"""
decodeTag : IndexedDecoder Tag
decodeTag =
Decode.succeed Tag
|> Decode.index
|> Decode.required "name" Decode.string
decodeTags : IndexedDecoder (List Tag)
decodeTags =
Decode.list decodeTag
decodeEmail : IndexedDecoder Email
decodeEmail =
Decode.succeed Email
|> Decode.required "address" Decode.string
|> Decode.index
|> Decode.required "tags" decodeTags
decodeEmails : IndexedDecoder (List Email)
decodeEmails =
Decode.list decodeEmail
main =
case Decode.decodeString decodeEmails json (Random.initialSeed 0) of
Ok ( emails, seed ) ->
viewEmails emails
Err err ->
text (Decode.errorToString err)
This produces the following output:
foo@mail.com[884571026]
tag1[1348153153]
tag2[1613638464]
bar@mail.com[1382093769]
tag3[1707621207]
It is then possible to have an incremental id instead of a random one by replacing the Random
API by a custom Indexed
one. Let’s add the Indexed
module exposing the Indexer
type:
module Indexed exposing
( Id
, Indexer
, combine
, constant
, id
, map
, map2
, step
)
type alias Id =
Int
type Indexer a
= Indexer (Id -> ( a, Id ))
combine : List (Indexer a) -> Indexer (List a)
combine indexers =
case indexers of
[] ->
constant []
g :: gs ->
map2 (::) g (combine gs)
constant : a -> Indexer a
constant value =
Indexer (\i -> ( value, i ))
id : Indexer Id
id =
Indexer (\i -> ( i, i + 1 ))
map : (a -> b) -> Indexer a -> Indexer b
map func (Indexer indexA) =
Indexer
(\id0 ->
let
( a, id1 ) =
indexA id0
in
( func a, id1 )
)
map2 : (a -> b -> c) -> Indexer a -> Indexer b -> Indexer c
map2 func (Indexer indexA) (Indexer indexB) =
Indexer
(\id0 ->
let
( a, id1 ) =
indexA id0
( b, id2 ) =
indexB id1
in
( func a b, id2 )
)
step : Indexer a -> Id -> ( a, Id )
step (Indexer generator) i =
generator i
then add a Indexed.IncDecode.elm
module almost identical to our previous Indexed.Decode
(that we could rename to Indexed.RandomDecode
or something), but using Indexed
instead of Random
:
module Indexed.IncDecode exposing
( Id
, IndexedDecoder
, decodeString
, decoder
, errorToString
, index
, list
, map
, map2
, required
, string
, succeed
)
import Indexed exposing (Indexer)
import Json.Decode as Decode exposing (Decoder)
import Json.Decode.Pipeline as Pipeline
type alias Id =
Indexed.Id
type alias Error =
Decode.Error
type alias IndexedDecoder a =
Decoder (Indexer a)
custom : IndexedDecoder a -> IndexedDecoder (a -> b) -> IndexedDecoder b
custom =
Decode.map2 (Indexed.map2 (|>))
decoder : Decoder a -> IndexedDecoder a
decoder dec =
Decode.map Indexed.constant dec
decodeString : IndexedDecoder a -> String -> Id -> Result Decode.Error ( a, Id )
decodeString dec str seed =
Decode.decodeString (step dec seed) str
errorToString : Error -> String
errorToString =
Decode.errorToString
field : String -> IndexedDecoder a -> IndexedDecoder a
field key dec =
Decode.field key dec
index : IndexedDecoder (Id -> a) -> IndexedDecoder a
index dec =
Decode.map (\gen -> Indexed.map2 (|>) Indexed.id gen) dec
list : IndexedDecoder a -> IndexedDecoder (List a)
list dec =
Decode.map Indexed.combine (Decode.list dec)
map : (a -> value) -> IndexedDecoder a -> IndexedDecoder value
map f dec =
Decode.map (Indexed.map f) dec
map2 : (a -> b -> value) -> IndexedDecoder a -> IndexedDecoder b -> IndexedDecoder value
map2 f dec1 dec2 =
Decode.map2 (Indexed.map2 f) dec1 dec2
required : String -> IndexedDecoder a -> IndexedDecoder (a -> b) -> IndexedDecoder b
required key valDecoder dec =
custom (field key valDecoder) dec
step : IndexedDecoder a -> Id -> Decoder ( a, Id )
step dec seed =
Decode.map (\gen -> Indexed.step gen seed) dec
string : IndexedDecoder String
string =
decoder Decode.string
succeed : a -> IndexedDecoder a
succeed a =
Decode.succeed (Indexed.constant a)
Our Main
is identical except the imports and the main
function:
module Main exposing (main)
import Html exposing (Html, div, span, text)
import Html.Attributes exposing (style)
import Indexed.IncDecode as Decode exposing (Id, IndexedDecoder)
import Json.Encode as Encode
import Random
type alias Email =
{ address : String
, id : Id
, tags : List Tag
}
type alias Tag =
{ id : Id
, name : String
}
viewEmails : List Email -> Html msg
viewEmails emails =
div [] (List.map viewEmail emails)
viewEmail : Email -> Html msg
viewEmail email =
div []
[ text email.address
, viewId email.id
, viewTags email.tags
]
viewTags : List Tag -> Html msg
viewTags tags =
div [ style "margin-left" "8px" ]
(List.map viewTag tags)
viewTag : Tag -> Html msg
viewTag tag =
div []
[ text tag.name
, viewId tag.id
]
viewId : Id -> Html msg
viewId id =
text ("[" ++ String.fromInt id ++ "]")
json : String
json =
"""
[
{ "address": "foo@mail.com",
"tags": [
{"name": "tag1"},
{"name": "tag2"}
]
},
{ "address": "bar@mail.com"
, "tags": [
{"name": "tag3"}
]
}
]
"""
decodeTag : IndexedDecoder Tag
decodeTag =
Decode.succeed Tag
|> Decode.index
|> Decode.required "name" Decode.string
decodeTags : IndexedDecoder (List Tag)
decodeTags =
Decode.list decodeTag
decodeEmail : IndexedDecoder Email
decodeEmail =
Decode.succeed Email
|> Decode.required "address" Decode.string
|> Decode.index
|> Decode.required "tags" decodeTags
decodeEmails : IndexedDecoder (List Email)
decodeEmails =
Decode.list decodeEmail
main =
case Decode.decodeString decodeEmails json 0 of
Ok ( emails, id ) ->
viewEmails emails
Err err ->
text (Decode.errorToString err)
and we get:
foo@mail.com[2]
tag1[0]
tag2[1]
bar@mail.com[4]
tag3[3]
Edit:
Here is a very dirty all-in-one ellie example to play with it: https://ellie-app.com/43dX7qnC6Csa1.
But this should really be properly splitted in several modules as lots of function names are identical.