Retain some structural information while encoding to JSON.
Examples assume the following imports:
import Json.Encode
For readability, an alias for Json.Encode.Value
which is also aliased as
Json.Decode.Value
Something which has been encoded. The type parameter t
can realistically
be one of three things:
Value
which represents opaque encoded values. This included primitives or
any other value that was made opaque using force
or value
.Seq
which represents a sequence of things. I.e. a List
or a JS array.Obj
which represents an object made up of key-value pairs.An opaque sequence of things.
An opaque object of key-value pairs.
Wrap up encoding to turn an Encoded
thing into a Value
.
The equivalent of Json.Encode.string
toValue <| string "hello"
--> Json.Encode.string "hello"
The equivalent of Json.Encode.int
toValue <| int 5
--> Json.Encode.int 5
The equivalent of Json.Encode.float
toValue <| float 4.5
--> Json.Encode.float 4.5
The equivalent of Json.Encode.null
toValue null
--> Json.Encode.null
Wrap a value in Encoded
.
Json.Encode.string "hello world"
|> value
|> toValue
--> Json.Encode.string "hello world"
Force any Encoded x
into an Encoded Value
listOf string [ "hello", "world" ] -- Encoded Seq
|> force -- Encoded Value
|> toValue -- Value
--> Json.Encode.list
[ Json.Encode.string "hello", Json.Encode.string "world" ]
The type of thing having a force
is useful for.
maybe string Nothing
--> null
maybe string (Just "foo")
--> string "foo"
Combine a List
of encoded values into a single Seq
uence.
Note that making a sequence involving both Encoded Value
and Encoded Seq
will involve "downcasting" the Encoded Seq
to an Encoded Value
using force
.
toValue <| list [ string "hello", string "world" ]
--> Json.Encode.list
[ Json.Encode.string "hello", Json.Encode.string "world" ]
toValue <| list [ string "hello", null, int 5 ]
--> Json.Encode.list
[ Json.Encode.string "hello", Json.Encode.null, Json.Encode.int 5 ]
toValue <| list [ force emptySeq, force emptyObj, null ]
--> Json.Encode.list
[ Json.Encode.list []
, Json.Encode.object []
, Json.Encode.null
]
An even more constrained way of creating a sequence, yet often useful.
toValue <| listOf string [ "hello", "world" ]
--> Json.Encode.list
[ Json.Encode.string "hello", Json.Encode.string "world" ]
toValue <| listOf (always null) [ 1, 2, 3 ]
--> Json.Encode.list
[ Json.Encode.null, Json.Encode.null, Json.Encode.null ]
listOf identity [ null ]
--> list [ null ]
An empty sequence. Useful for consing thing onto.
emptySeq
--> list []
cons (string "hello") <| cons (int 5) emptySeq
--> list [ string "hello", int 5 ]
Prepend a single value to a sequence.
listOf string [ "a", "b" ]
|> cons (int 0)
|> cons null
--> list
[ null
, int 0
, string "a"
, string "b"
]
Concatenate two sequences.
left : Encoded Seq
left =
listOf string [ "a", "b", "c" ]
right : Encoded Seq
right =
listOf int [ 1, 2, 3 ]
left |> concatSeq right
--> list
[ string "a"
, string "b"
, string "c"
, int 1
, int 2
, int 3
]
Like sequence
creates an Encoded Seq
, this creates an Encoded Obj
.
toValue <| object [ ( "hello", string "world" ), ( "nope", null ) ]
--> Json.Encode.object
[ ( "hello", Json.Encode.string "world" )
, ( "nope", Json.Encode.null )
]
Create an object from a list of key-value pairs where the values all happen to be the same type.
Since this results in an Encoded Obj
, you can still add information of a
different type afterwards.
objectOf string [ ( "foo", "bar" ), ( "hello", "world" ) ]
--> object [ ( "foo", string "bar" ), ( "hello", string "world" ) ]
Create an empty JSON object, so you can add things to it as you see fit
emptyObj
--> object []
emptyObj
|> addField "hello" (string "world")
--> object [ ( "hello", string "world" ) ]
Add a single field to an encoded object.
objectOf string [ ( "hello", "world" ) ]
|> addField "foo" (string "bar")
--> object
[ ( "foo", string "bar" )
, ( "hello", string "world" )
]
Concatenates two objects. Order doesn't really matter, but is preserved.
left : Encoded Obj
left =
objectOf string [ ( "hello", "world" ) ]
right : Encoded Obj
right =
objectOf int [ ( "foo", 5 ) ]
left |> concatObj right
--> object
[ ( "hello", string "world" )
, ( "foo", int 5 )
]
module Json.Encode.Exploration
exposing
( Encoded
, Obj
, Seq
, Value
, addField
, concatObj
, concatSeq
, cons
, emptyObj
, emptySeq
, float
, force
, int
, list
, listOf
, maybe
, null
, object
, objectOf
, string
, toValue
, value
)
{-| Retain some structural information while encoding to JSON.
Examples assume the following imports:
import Json.Encode
# Building blocks
@docs Value, Encoded, Seq, Obj, toValue
# Primitives
@docs string, int, float, null, value, force, maybe
# Sequences
@docs list, listOf, emptySeq, cons, concatSeq
# Objects
@docs object, objectOf, emptyObj, addField, concatObj
-}
import Json.Encode as Encode
{-| For readability, an alias for `Json.Encode.Value` which is also aliased as
`Json.Decode.Value`
-}
type alias Value =
Encode.Value
{-| Something which has been encoded. The type parameter `t` can realistically
be one of three things:
- `Value` which represents opaque encoded values. This included primitives or
any other value that was made opaque using `force` or `value`.
- `Seq` which represents a sequence of things. I.e. a `List` or a JS array.
- `Obj` which represents an object made up of key-value pairs.
-}
type Encoded t
= Encoded (t -> Value) t
{-| An opaque sequence of things.
-}
type Seq
= Seq (List Value)
{-| An opaque object of key-value pairs.
-}
type Obj
= Obj (List ( String, Value ))
{-| Wrap up encoding to turn an `Encoded` thing into a `Value`.
-}
toValue : Encoded a -> Value
toValue (Encoded encoder value) =
encoder value
-- Primitives
{-| The equivalent of `Json.Encode.int`
toValue <| int 5
--> Json.Encode.int 5
-}
int : Int -> Encoded Value
int =
value << Encode.int
{-| The equivalent of `Json.Encode.float`
toValue <| float 4.5
--> Json.Encode.float 4.5
-}
float : Float -> Encoded Value
float =
value << Encode.float
{-| The equivalent of `Json.Encode.string`
toValue <| string "hello"
--> Json.Encode.string "hello"
-}
string : String -> Encoded Value
string =
value << Encode.string
{-| The equivalent of `Json.Encode.null`
toValue null
--> Json.Encode.null
-}
null : Encoded Value
null =
value Encode.null
{-| Wrap a value in `Encoded`.
Json.Encode.string "hello world"
|> value
|> toValue
--> Json.Encode.string "hello world"
-}
value : Value -> Encoded Value
value =
Encoded identity
{-| Force any `Encoded x` into an `Encoded Value`
listOf string [ "hello", "world" ] -- Encoded Seq
|> force -- Encoded Value
|> toValue -- Value
--> Json.Encode.list
[ Json.Encode.string "hello", Json.Encode.string "world" ]
-}
force : Encoded a -> Encoded Value
force =
toValue >> value
{-| The type of thing having a `force` is useful for.
maybe string Nothing
--> null
maybe string (Just "foo")
--> string "foo"
-}
maybe : (a -> Encoded b) -> Maybe a -> Encoded Value
maybe encoder =
Maybe.map (force << encoder) >> Maybe.withDefault null
-- Structural
{-| An empty sequence. Useful for consing thing onto.
emptySeq
--> list []
cons (string "hello") <| cons (int 5) emptySeq
--> list [ string "hello", int 5 ]
-}
emptySeq : Encoded Seq
emptySeq =
seq []
{-| Combine a `List` of encoded values into a single `Seq`uence.
Note that making a sequence involving both `Encoded Value` and `Encoded Seq`
will involve "downcasting" the `Encoded Seq` to an `Encoded Value` using `force`.
toValue <| list [ string "hello", string "world" ]
--> Json.Encode.list
[ Json.Encode.string "hello", Json.Encode.string "world" ]
toValue <| list [ string "hello", null, int 5 ]
--> Json.Encode.list
[ Json.Encode.string "hello", Json.Encode.null, Json.Encode.int 5 ]
toValue <| list [ force emptySeq, force emptyObj, null ]
--> Json.Encode.list
[ Json.Encode.list []
, Json.Encode.object []
, Json.Encode.null
]
-}
list : List (Encoded a) -> Encoded Seq
list =
listOf identity
{-| An even more constrained way of creating a sequence, yet often useful.
toValue <| listOf string [ "hello", "world" ]
--> Json.Encode.list
[ Json.Encode.string "hello", Json.Encode.string "world" ]
toValue <| listOf (always null) [ 1, 2, 3 ]
--> Json.Encode.list
[ Json.Encode.null, Json.Encode.null, Json.Encode.null ]
listOf identity [ null ]
--> list [ null ]
-}
listOf : (a -> Encoded b) -> List a -> Encoded Seq
listOf encoder =
List.map (encoder >> toValue) >> seq
{-| Create an empty JSON object, so you can add things to it as you see fit
emptyObj
--> object []
emptyObj
|> addField "hello" (string "world")
--> object [ ( "hello", string "world" ) ]
-}
emptyObj : Encoded Obj
emptyObj =
obj []
{-| Like `sequence` creates an `Encoded Seq`, this creates an `Encoded Obj`.
toValue <| object [ ( "hello", string "world" ), ( "nope", null ) ]
--> Json.Encode.object
[ ( "hello", Json.Encode.string "world" )
, ( "nope", Json.Encode.null )
]
-}
object : List ( String, Encoded a ) -> Encoded Obj
object =
objectOf identity
{-| Create an object from a list of key-value pairs where the values all happen
to be the same type.
Since this results in an `Encoded Obj`, you can still add information of a
different type afterwards.
objectOf string [ ( "foo", "bar" ), ( "hello", "world" ) ]
--> object [ ( "foo", string "bar" ), ( "hello", string "world" ) ]
-}
objectOf : (a -> Encoded b) -> List ( String, a ) -> Encoded Obj
objectOf encoder =
List.map (Tuple.mapSecond <| encoder >> toValue) >> obj
-- Structural manipulation
{-| Concatenate two sequences.
left : Encoded Seq
left =
listOf string [ "a", "b", "c" ]
right : Encoded Seq
right =
listOf int [ 1, 2, 3 ]
left |> concatSeq right
--> list
[ string "a"
, string "b"
, string "c"
, int 1
, int 2
, int 3
]
-}
concatSeq : Encoded Seq -> Encoded Seq -> Encoded Seq
concatSeq (Encoded _ (Seq right)) (Encoded _ (Seq left)) =
seq <| left ++ right
{-| Prepend a single value to a sequence.
listOf string [ "a", "b" ]
|> cons (int 0)
|> cons null
--> list
[ null
, int 0
, string "a"
, string "b"
]
-}
cons : Encoded a -> Encoded Seq -> Encoded Seq
cons (Encoded valCoder x) (Encoded _ (Seq xs)) =
seq <| valCoder x :: xs
{-| Concatenates two objects. Order doesn't really matter, but is preserved.
left : Encoded Obj
left =
objectOf string [ ( "hello", "world" ) ]
right : Encoded Obj
right =
objectOf int [ ( "foo", 5 ) ]
left |> concatObj right
--> object
[ ( "hello", string "world" )
, ( "foo", int 5 )
]
-}
concatObj : Encoded Obj -> Encoded Obj -> Encoded Obj
concatObj (Encoded _ (Obj right)) (Encoded _ (Obj left)) =
obj <| left ++ right
{-| Add a single field to an encoded object.
objectOf string [ ( "hello", "world" ) ]
|> addField "foo" (string "bar")
--> object
[ ( "foo", string "bar" )
, ( "hello", string "world" )
]
-}
addField : String -> Encoded a -> Encoded Obj -> Encoded Obj
addField key (Encoded valCoder val) (Encoded _ (Obj kvPairs)) =
obj <| ( key, valCoder val ) :: kvPairs
-- Helpers
seq : List Value -> Encoded Seq
seq =
Encoded seqToValue << Seq
obj : List ( String, Value ) -> Encoded Obj
obj =
Encoded objToValue << Obj
seqToValue : Seq -> Value
seqToValue (Seq values) =
Encode.list values
objToValue : Obj -> Value
objToValue (Obj kvPairs) =
Encode.object kvPairs