ymtszw / elm-xml-decode / Xml.Decode

XML decoder module sharing the spirit of Json.Decode.

Using XmlParser as its parser component.

Basic Example

Examples in this package are doc-tested.

exampleDecoder : Decoder ( String, List Int )
exampleDecoder =
    map2 Tuple.pair
        (path [ "string", "value" ] (single string))
        (path [ "int", "values" ] (list int))

run exampleDecoder
    """
    <root>
        <string>
            <value>SomeString</value>
        </string>
        <int>
            <values>1</values>
            <values>2</values>
        </int>
    </root>
    """
--> Ok ( "SomeString", [ 1, 2 ] )

Types


type Decoder a

A function that knows how to decode an XML node into Elm value.


type ListDecoder a

A function that knows how to decode list of XML nodes into Elm value.

They are constructed by functions such as single or list, then used in conjunction with path.

See path for examples.


type Error
    = Path (List String) Error
    | OneOf (List Error)
    | Failure String XmlParser.Node

Represents error on decode execution.

Decode Executor

run : Decoder a -> String -> Result String a

Synonym of decodeString.

decodeString : Decoder a -> String -> Result String a

Parses an XML string and decodes into Elm value.

Parsing will be done by XmlParser.parse

This function returns error in String. Should you need to separately handle Parser.Error and Xml.Decode.Error, explicitly use XmlParser.parse and decodeXml.

decodeXml : Decoder a -> XmlParser.Xml -> Result Error a

Decodes an XmlParser.Xml value into other type of Elm value.

It discards Document Type Definitoin (DTD) and Processing Instruction in XML, only cares about root XML node.

import XmlParser exposing (Xml, Node(..))

exampleDecoder : Decoder ( String, List Int )
exampleDecoder =
    map2 Tuple.pair
        (path [ "string", "value" ] (single string))
        (path [ "int", "values" ] (list int))

decodeXml exampleDecoder <|
    Xml [] Nothing <|
        Element "root" []
            [ Element "string" []
                [ Element "value" [] [ Text "SomeString" ]
                ]
            , Element "int" []
                [ Element "values" [] [ Text "1" ]
                , Element "values" [] [ Text "2" ]
                ]
            ]
--> Ok ( "SomeString", [ 1, 2 ] )

Decoders

string : Decoder String

Decodes an XmlParser.Node into String.

If you want to extract values from node attribute, use stringAttr and variants.

run string "<root>string</root>"
--> Ok "string"

run string "<root></root>"
--> Ok ""

run string "<root><nested>string</nested></root>"
--> Err "Node: <root><nested>string</nested></root>\nThe node is not a simple text node."

int : Decoder Basics.Int

Similar to string, but also tries to convert String to Int.

run int "<root>1</root>"
--> Ok 1

run int "<root>value</root>"
--> Err "Node: <root>value</root>\ncould not convert string 'value' to an Int"

float : Decoder Basics.Float

Decodes to Float.

run float "<root>1.0</root>"
--> Ok 1.0

run float "<root>value</root>"
--> Err "Node: <root>value</root>\ncould not convert string 'value' to a Float"

bool : Decoder Basics.Bool

Decodes to Bool.

In Xml Schema Definition (XSD), valid lexical representation of boolean values are,

We follow this specification, case-sensitively.

run bool "<root>true</root>"
--> Ok True

run bool "<root>value</root>"
--> Err "Node: <root>value</root>\nNot a valid boolean value."

node : Decoder XmlParser.Node

Decodes an XmlParser.Node into itself.

See XmlParser to further decompose the node.

import XmlParser exposing (Node(..))

run (path ["path", "to", "target"] (list node)) """<root><path><to><target><child></child><child></child></target><target></target></to></path></root>"""
--> Ok [Element "target" [] [Element "child" [] [],Element "child" [] []],Element "target" [] []]

run (path ["path", "to", "target"] (single node)) """<root><path><to><target><child></child><child></child></target><target></target></to></path></root>"""
--> Err ("Path: /path/to/target\nNode: <to><target><child></child><child></child></target><target></target></to>\nMultiple nodes found.")

Attribute Decoders

stringAttr : String -> Decoder String

Decodes an attribute value of XmlParser.Node into String.

Fails if the node does not have specified attribute.

run (stringAttr "attr") "<root attr='value'></root>"
--> Ok "value"

run (stringAttr "attr") "<root></root>"
--> Err "Node: <root></root>\nAttribute 'attr' not found."

intAttr : String -> Decoder Basics.Int

Decodes an attribute value into Int.

run (intAttr "attr") "<root attr='1'></root>"
--> Ok 1

run (intAttr "attr") "<root attr='value'></root>"
--> Err "Node: <root attr=\"value\"></root>\ncould not convert string 'value' to an Int"

floatAttr : String -> Decoder Basics.Float

Decodes an attribute value into Float.

run (floatAttr "attr") "<root attr='1.5'></root>"
--> Ok 1.5

run (floatAttr "attr") "<root attr='value'></root>"
--> Err "Node: <root attr=\"value\"></root>\ncould not convert string 'value' to a Float"

boolAttr : String -> Decoder Basics.Bool

Decodes an attribute value into Bool.

run (boolAttr "attr") "<root attr='true'></root>"
--> Ok True

run (boolAttr "attr") "<root attr='value'></root>"
--> Err "Node: <root attr=\"value\"></root>\nNot a valid boolean value."

List Decoders

single : Decoder a -> ListDecoder a

Composes ListDecoder that results in a singular value.

It fails if:

Examples:

run (path [ "tag" ] (single string)) "<root><tag>string</tag></root>"
--> Ok "string"

run (path [ "tag" ] (single string)) "<root></root>"
--> Err "Path: /tag\nNode: <root></root>\nNode not found."

run (path [ "tag" ] (single string)) "<root><tag>string1</tag><tag>string2</tag></root>"
--> Err "Path: /tag\nNode: <root><tag>string1</tag><tag>string2</tag></root>\nMultiple nodes found."

list : Decoder a -> ListDecoder (List a)

Composes ListDecoder that results in a list of values.

This ListDecoder fails if any incoming items cannot be decoded.

run (path [ "tag" ] (list string)) "<root><tag>string1</tag><tag>string2</tag></root>"
--> Ok [ "string1", "string2" ]

run (path [ "tag" ] (list int)) "<root><tag>1</tag><tag>nonInt</tag></root>"
--> Err "Path: /tag\nNode: <tag>nonInt</tag>\ncould not convert string 'nonInt' to an Int"

leakyList : Decoder a -> ListDecoder (List a)

Variation of list, which ignores items that cannot be decoded.

run (path [ "tag" ] (leakyList int)) "<root><tag>1</tag><tag>nonInt</tag></root>"
--> Ok [ 1 ]

index : Basics.Int -> Decoder a -> ListDecoder a

Similar to Json.Decode.index, decode from list of nodes at some index.

Useful for "narrowing down" your search paths.

Fails if there are not enough nodes.

run (path [ "tag" ] (index 1 int)) "<root><tag>0</tag><tag>1</tag></root>"
--> Ok 1

Decoder Utilities

mapN series are backed by Result.mapN series, thus it only supports up to map5.

succeed : a -> Decoder a

Decoder that always succeed with the given value.

fail : String -> Decoder a

Decoder that always fail with the given message.

oneOf : List (Decoder a) -> Decoder a

Try a list of decoders.

Fails if all given decoders failed, or no decoders are given.

run (oneOf [ int, succeed 0 ]) "<root>nonInt</root>"
--> Ok 0

run (oneOf [ int ]) "<root>nonInt</root>"
--> Err "All decoders failed:\n 1) Node: <root>nonInt</root>\n    could not convert string 'nonInt' to an Int"

andThen : (a -> Decoder b) -> Decoder a -> Decoder b

Generates a decoder that depends on previous value.

with : Decoder a -> (a -> Decoder b) -> Decoder b

Flipped andThen.

Why do we want this? It allows us to write decoders in Cotinuation-Passing Style (CPS)!

run
    (
        with (path ["string"] (single string)) <| \s ->
        with (path ["int"] (single int)) <| \i ->
        succeed ( s, i )
    )
    "<root><string>string</string><int>1</int></root>"
--> Ok ("string", 1)

This style is being discussed in Elm community, and sometimes it makes sense to adopt one, especially when you are trying to decode complex data.

https://discourse.elm-lang.org/t/experimental-json-decoding-api/2121

This style is not yet formatted nicely by elm-format as of writing this, but the proposal is already filed!

https://github.com/avh4/elm-format/issues/568

map : (a -> value) -> Decoder a -> Decoder value

Transform a decoder.

run (map String.length string) "<root>string</root>"
--> Ok 6

map2 : (a -> b -> value) -> Decoder a -> Decoder b -> Decoder value

Generates a decoder that combines results from two decoders.

It can be used for generating a decoder for a data type that takes two inputs. Also this is used as a building block of decoder composition helpers.

run (map2 Tuple.pair string string) "<root>string</root>"
--> Ok ( "string", "string" )

map3 : (a -> b -> c -> value) -> Decoder a -> Decoder b -> Decoder c -> Decoder value

map4 : (a -> b -> c -> d -> value) -> Decoder a -> Decoder b -> Decoder c -> Decoder d -> Decoder value

map5 : (a -> b -> c -> d -> e -> value) -> Decoder a -> Decoder b -> Decoder c -> Decoder d -> Decoder e -> Decoder value

andMap : Decoder a -> Decoder (a -> b) -> Decoder b

Equivalent to Json.Decode.Extra.andMap, allows writing XML decoders in sequential style.

run
    (succeed Tuple.pair
        |> andMap (path ["string"] (single string))
        |> andMap (path ["int"] (single int))
    )
    "<root><string>string</string><int>1</int></root>"
--> Ok ("string", 1)

withDefault : a -> Decoder a -> Decoder a

Generates a decoder that results in the default value on failure.

run (withDefault 0 int) "<root>1</root>"
--> Ok 1

run (withDefault 0 int) "<root>Non Int value</root>"
--> Ok 0

maybe : Decoder a -> Decoder (Maybe a)

Generates a decoder that results in a Maybe value.

If the given decoder resulted in Err, it succeeds with Nothing. Otherwise (in cases of Ok,) it succeeds with Just value.

run (maybe int) "<root>1</root>"
--> Ok (Just 1)

run (maybe int) "<root>Non Int value</root>"
--> Ok Nothing

lazy : (() -> Decoder a) -> Decoder a

Generates a lazy decoder.

Similar to Json.Decode.lazy.

someRecordDecoder : Decoder SomeRecord
someRecordDecoder =
    map2 SomeRecord
        (path [ "path", "to", "string", "value" ] (single string))
        (path [ "path", "to", "list", "of", "someRecord" ] (list (lazy (\_ -> someRecordDecoder))))

With this, you can avoid "bad-recursion" error on compilation which happens when you define nested part of the above decoder as (list someRecordDecoder)

Node Locater

path : List String -> ListDecoder a -> Decoder a

Generates a Decoder that applies a ListDecoder at specified XML path.

Typical usage:

someRecordDecoder : Decoder SomeRecord
someRecordDecoder =
    map2 SomeRecord
        (path [ "string", "value" ] (single string))
        (path [ "int", "values" ] (list int))

Due to the nature of XML, you cannot distinguish a particular tag or tags hold whether "singular value" or "list of values", from the structure of XML document itself. This is opposite to JSON, where there can be only one field of a paticular key in a level, and its quantization is obvious from the shape of the value.

For this reason, this function always produce list of XmlParser.Nodes. Then they must be decoded using special decoder parts: ListDecoder. In the above example, single and list are ListDecoder generators.

It collects ALL nodes matching the path in breadth-first manner. If you need to collect nodes only from a specific intermediate node that fulfills some conditions, (e.g. path [ "a", "b" ], but only in 3rd "<a>...</a>") you need to split the path and explicitly define a Decoder that narrows down the path. Be careful not to accidentally include nodes from not-targeted ancestors.

Note that in the path, you must "start" at the root scope. For instance, to work with an XML document like:

<Root>
    <Path>
        <Target>
            Value
        </Target>
    </Path>
</Root>

You should specify:

path [ "Path", "Target" ] (single string)

Basic usages:

run (path [ "tag", "nested" ] (single string)) "<root><tag><nested>string1</nested></tag></root>"
--> Ok "string1"

run (path [ "tag", "nested" ] (list string)) "<root><tag><nested>string1</nested><nested>string2</nested></tag></root>"
--> Ok [ "string1", "string2" ]

run (path [ "tag", "nested" ] (single (stringAttr "value"))) "<root><tag><nested value='attr1'></nested></tag></root>"
--> Ok "attr1"

Decoders will report errors with path at which error happened as well as nearest node:

run (path [ "tag", "nested" ] (single int)) "<root><tag><nested>string1</nested></tag></root>"
--> Err "Path: /tag/nested\nNode: <nested>string1</nested>\ncould not convert string 'string1' to an Int"

Pipeline APIs

Allow writing Decoders in Pipeline-style, just like Json.Decode.Pipeline.

requiredPath : List String -> ListDecoder a -> Decoder (a -> b) -> Decoder b

Decodes value at required XML path.

pipelineDecoder : Decoder ( String, List Int )
pipelineDecoder =
    succeed Tuple.pair
        |> requiredPath [ "path", "to", "string", "value" ] (single string)
        |> requiredPath [ "path", "to", "int", "values" ] (list int)

run pipelineDecoder
    """
    <root>
        <path>
            <to>
                <string>
                    <value>SomeString</value>
                </string>
                <int>
                    <values>1</values>
                    <values>2</values>
                </int>
            </to>
        </path>
    </root>
    """
--> Ok ( "SomeString", [ 1, 2 ] )

optionalPath : List String -> ListDecoder a -> a -> Decoder (a -> b) -> Decoder b

Tries to decode value at optional XML path. Uses default value if the node is missing.

decoderWithDefault : Decoder String
decoderWithDefault =
    succeed identity
        |> optionalPath [ "optional", "path" ] (single string) "default"

run decoderWithDefault "<root><optional><path>string</path></optional></root>"
--> Ok "string"

run decoderWithDefault "<root></root>"
--> Ok "default"

possiblePath : List String -> ListDecoder a -> Decoder (Maybe a -> b) -> Decoder b

Decodes value at possible XML path into Maybe value.

maybeDecoder : Decoder (Maybe String)
maybeDecoder =
    succeed identity
        |> possiblePath [ "possible", "path" ] (single string)

run maybeDecoder "<root><possible><path>string</path></possible></root>"
--> Ok (Just "string")

run maybeDecoder "<root></root>"
--> Ok Nothing

If you want to apply default value when the node is missing, use optionalPath.

Error Utility

errorToString : Error -> String

Convert Error to a formatted string.