Tools for building a translator from KDL to your Elm datastructure
This module follow's the same decoding philosophy as Elm uses for its JSON decoders. If you haven't read the JSON chapter of Elm's official guide, I recommend it! It's a 5-10 minute read, and it'll give you a great high-level overview that'll make working with this module a lot easier.
In short, this philosophy centers around Decoders, which are objects that convert data from KDL (or another format) into Elm data structures. At first, you start out with very simple decoders, like a decoder that simply converts a KDL string into an Elm string. What makes this technique powerful, though, is that decoders snap together like legos to construct bigger decoders. The functions that combine these decoders are known as combinators.
By piecing together these simple decoders with simple combinators, you can construct a decoder for pretty much any datastructure you can imagine, and adding validation and intuitive error messages is a snap!
One distinction between this library and Elm's JSON library is that JSON objects all belong to one "class", so Elm only needs one type of decoder. In KDL, values, properties, and nodes behave differently, so this library uses a different decoder type for each. Fear not though! A simple set of combinators is provided for each, and the documentation will make it clear which one you need.
These are the decoder types. They fit together much like you would
expect: DocumentDecoders are built out of NodeDecoder, and
NodeDecoders are built out of ValueDecoders and DocumentDecoders
(for decoding child nodes).
You'll notice that all of the decoders here are generic over two types.
t is the output type of the decoder-- The datastructure that
the decoder spits out.e type is your custom error type. For maximum flexibility, you can
choose whatever type you want here. The quickest and easiest way to get started
is to just use the unit type (aka the empty tuple) (). This will let you
build fast, but you won't get much information about what went wrong when a
decode fails. At some point, you'll probably want actual error messages, so you
may want to use a String or even a custom enum to represent possible errors.A decoder for simple KDL values, like strings, numbers, and booleans
A decoder for KDL nodes
A decoder for KDL documents, and equivalently, child blocks of KDL nodes
'cause they're both just lists of Nodes.
Combinators are what you use to combine decoders. The ones in this section are generic, so you can use them with any of the five decoders.
Alter the output type of an existing combinator
Sometimes a simple existing decoder isn't enough. Suppose you want to decode a percentage, but in KDL it's represented as an integer between 0 and 100, but in your datastructure, you want to represent it as a floating point between 0.0 and 1.0. You could represent that decoder like this:
-- the `float` decoder automatically converts integers to floats
decodePercent = float
|> map (\input -> input / 100.0)Note: This combinator is generic, and can be used with any of the three decoders in this module.
Require two things to be true of a value
For those familiar with the JSON decoding module, this function is included to
make you feel at home. But most of the time you'll probably want to use the
andApply function instead.
This function runs TWO decoders on one object. The decode succeeds only if both of the component decoders succeed. If that's the case, then the output of each constituent decoder is fed to the provided function.
Note: This combinator is generic, and can be used with any of the three decoders in this module.
Add another requirement to a decoder and combine their values through application
This can be very helpful when defining decoders for documents, properties, and arguments, especially when combined with succeed. This can be a little easier to see through example:
type MyWeirdDatatype = MyWeirdDatatype String Int
mkMyWeirdDatatype : Int -> String -> Int -> MyWeirdDatatype
mkMyWeirdDatatype x str y = MyWeirdDatatype str (x - y)
decodeMyWeirdData : DocumentDecoder String MyWeirdDatatype
decodeMyWeirdData =
succeed mkMyWeirdDatatype
|> andApply (argument int)
|> andApply (argument string)
|> andApply (argument int)
|> andRequire noOtherArguments
|> andRequire noOtherProperties
|> andRequire noChildren
|> singleNodeDocument "my-weird-data"
(parse "my-weird-data 5 meowy 2" |> Result.map (decode decodeMyWeirdData)) == Ok (Ok (MyWeirdDatatype "meowy" 3))Note: This combinator is generic, and can be used with any of the three decoders in this module, though most of the time you'll be using it with NodeDecoders.
A version of andApply that ignores the output of its first argument and while still adding the constraints
Note: This combinator is generic, and can be used with any of the three decoders in this module, though most of the time you'll be using it with NodeDecoders.
Run a decoder, then run a function on the result to produce a new decoder, which is also run.
Sometimes the behaviour of the decoder going forward is contingent on values already decoded. This is the function to turn to in that case. Analagous to Json.Decode.andThen.
Note: This combinator is generic, and can be used with any of the three decoders in this module.
A decoder that always succeeds and produces a value of your choice
Note: This combinator is generic, and can be used with any of the three decoders in this module.
Values are the most basic building block of KDL, and can be used to construct decoders for arguments and properties.
Decode any rational number
This will handle integers, numbers with decimals, and even numbers with exponents, like 120.9312e100, however, it can't decode special "numbers" like NaN (#nan) or infinity (#inf), and will return its first argument as an error if applied to any of these.
Of course, this will also produced a WrongType error when applied to non-numeric data, like strings or booleans.
Decode a number as a floating point
Unlike rational, this can decode values like NaN (#nan) and infinity (#inf), but may not necessarily represent a number with maximal position, and can introduce minor numeric errors, so isn't ideal for situations where absolute accuracy is important.
This will produced a WrongType error when applied to non-numeric data, like strings or booleans.
Decode a number as an integer
This will decode a value as an integer. However, it will fail (and return its first argument as an error) if used to decode non-integer data, or any numbers too large or to small to be represented by Elm's Int type.
Note that 1.1e1 and 1.0 are considered integers.
This will produced a WrongType error when applied to non-numeric data, like strings or booleans.
Decode a string
This will decode any quoted string, bare string, raw string, or multiline string of any variety into an Elm String
This will produced a WrongType error when applied to non-string data, like numbers or booleans.
Decode a Boolean
This will decode #true and #false into their Elm Bool equivalents
This will produce a WrongType error when applied to non-boolean data, like numbers or strings.
Require a value to be #null
This decoder only succeeds when applied to the KDL value #null. Its return value is always unit, aka the empty tuple, aka ().
This will return a WrongType error if anything other than null is decoded.
Require that a condition be true of the value returned by a ValueDecoder
If that condition isn't true, then an error of your choice is raised. Here's
a simple example. Say you want a node count that only accepts non-negative
integers as an argument. You could describe this node like this:
decodeCount =
argument (
int
|> guardV "can't have a negative count!" (\x -> x >= 0)
)
|> andRequire noOtherArguments
|> singleNodeDocument "count"See also: guardN, the NodeDecoder equivalent of this function
A decoder which always fails with an error of your choice rather than parsing a value
Try several different decoders to see if any work
Sometimes, you'll need to decode a value which could have one of several
possible types. For example, say you have a date node which can be followed
by either an integer (which should be interpretted as a UNIX timestamp) or
a string (which should be interpretted as a string). For example, say you have
a date node which can be followed by either an integer (which should be
interpretted as a UNIX timestamp) or a string (which should be interpretted
as a string). In this case, you can't just decode that value using
string or int — you'll need to try both to see which one
works. Here's how you can do that with oneOfV:
type MyDate
= Datestring String
| UnixTime Int
decodeDate : DocumentDecoder String MyDate
decodeDate =
argument
(
oneOfV
[ string |> map Datestring
, int |> map UnixTime
]
)
|> andRequire noOtherArguments
|> singleNodeDocument "date"
(parse "date 1733882937" |> Result.map (decode decodeDate)) == Ok (Ok (UnixTime 1733882937))oneOfV takes a list of decoders that should be tried.
If all provided decoders fail, a PossibilitiesExhausted error is produced
Arguments are values applied directly to nodes, like my-node 1 2 "hello" myprop=10. In this example, 1, 2, and "hello" are all arguments. myprop=10 is not an argument, but a property, which is the other way of associating values with a node.
The decoders in this section are concerned with making sure that nodes have the right number of arguments, in the right order (if there is one).
Add a required argument to a node
The argument takes the form of a ValueDecoder, which will be used to decode the argument itself. If the argument is absent, however, a TooFewArguments error will be returned
One thing to keep in mind is that arguments are order sensitive. Arguments will be expected to appear in the same order that they are applied to the NodeDecoder being built. That is:
-- Decodes `tuple hi 1` but not `tuple 1 hi`
tuple1 = succeed pair
|> andApply (argument string)
|> andApply (argument int)
|> andRequire noOtherArguments
|> singleNodeDocument "tuple"
-- Decodes `tuple 1 hi` but not `tuple hi 1`
tuple2 = succeed pair
|> andApply (argument int)
|> andApply (argument string)
|> andRequire noOtherArguments
|> singleNodeDocument "tuple"Similarly, if noOtherArguments or arguments is used, they should be come after all other arguments.
If you'd rather the argument be completely optional, see optionalArgument. If you would like to decode an arbitrary number of arguments all using the same decoder, see arguments.
This function mirrors the behavior of property, but for arguments rather than properties.
Add an optional argument to a node
If any (not-yet-decoded) argument is present, this decodes it with the provided decoder. Notice that the returned parser decodes to Maybe t rather than just t. This is because Nothing will be the result when the argument is absent.
Please read the documentation for argument for important details regarding ordering.
This mirrors the behavior of optionalProperty for arguments rather than properties.
Decodes all remaining arguments into a list
Requires that you provide a decoder which will be used on all arguments which have not yet been decoded. The results of each decode will be compiled into a list.
This mirrors the behavior of properties but for arguments rather than properties.
Automatically fails if there are any arguments which have not yet been decoded
By default, if there are trailing arguments which don't get decoded, they are simply ignored. For example, the pair parser from the argument documentation will parse tuple 1 hi, but also tuple 1 hi #nan etc etc. This behavior isn't always optimal, since it can lead to users not realizing they're misusing something. Applying this function causes your parser to error if there are any trailing arguments.
Also notice that noOtherArguments and arguments mutually make the other redundant. When noOtherArguments comes first, arguments is guaranteed to decode nothing (and produce the empty list). Meanwhile, if arguments comes first, noOtherArguments is guaranteed to succeed, as arguments is guaranteed to decode all the arguments if it succeeds.
This mirrors the behavior of noOtherProperties but for arguments rather than properties.
Produces a TooManyArguments error if additional arguments are present
Properties are key/value associations tied to nodes. For example in my-node hello=world favorite-number=1080, there's two properties: One called "hello" with the value "world", and one called "favorite-number" with the value 1080. Property keys are always strings, while property values can be any KDL value.
This section is dedicated to making sure that all the right properties are specified, and none of the wrong ones.
Require that a node have a certain property
This adds a requirement of a certain property to a node, which must be present on the node for the parse to succeed. If the property is missing from the node, a PropertyAbsent error will be returned. Otherwise, it's decoded using the decodePropertyValue node. propertyKey is the name of the property.
Note that this is order sensitive. When building a decoder, if you first decode a property, then use properties or noOtherProperties, this property will be "accounted for", and not count towards the properties decoded by either of those two functions. Inversely, if you run noOtherProperties before running property, the resulting decoder will always fail, either because the property was present, but got passed to noOtherProperties first, or because it was absent.
This function mirrors the behavior of argument but for properties rather than arguments.
Optionally decode a property
The resulting node decoder will produce Nothing when the property is absent, or Just when the property is present and can be decoded by the provided decoder.
See property for important notes regarding ordering
As with other node decoders, you'll probably want to chain this with other decoders using andThen and succeed.
This mirrors the behavior of optionalArgument for properties rather than arguments.
Decodes an arbitrary number of properties agnostic regardless of key
You provide a decoder for the value attached to each key, and the decoder decodes each value, and produces a Dict mapping each property name to the resulting value.
Pay attention to the ordering between this and property/optionalProperty. Putting either property or optionalProperty will exclude that property from the set of properties decoded by this function.
This is, in effect, mutually exclusive with noOtherProperties
This mirrors the behavior of arguments but for properties rather than arguments.
Automatically fail if any properties have yet to be decoded
By default, if there are properties which don't get decoded, they are simply ignored. This behavior isn't always optimal, since it can lead to users not realizing they're misusing something. Applying this function causes your parser to error if there are any arguments that are unaccounted for.
This mirrors the behavior of noOtherArguments but for properties rather than arguments.
These decoders help you decode the children of a node. If you don't specify either of these, then by default your decoder will simply ignore any children which are present. It's recommended that you do specify a behavior though, so that accidental misuse is met by a helpful error rather than silent acceptance.
Parse the children of a node
The children of a node is parsed as just another document. Any of the functions you use to create DocumentDecoders can be used to construct a document decoder which works for the children of a node.
By default, any child nodes are simply ignored without fanfare, but you can specify a specific expectation by either using this function or noChildren, which can ensure that the child nodes are what you expect.
See also: document
Assert that a node does not have any children
Instead of simply ignoring the children of a node, it's often preferable to ensure that the node definitely does not have children, and therefore is less likely to be being used incorrectly. This function concisely accomplishes that.
Equivalent to children noOtherEntries
See also: children, noOtherEntries
These decoders help you get a little bit more information about your nodes. Note that the Arguments, Properties, and Children all also contain NodeDecoders as well, but this section is dedicated to decoders for node metadata alone.
Decode a node, yeilding its name
It's still possible to decode other parts of the node using other node decoders. It's often helpful to use a pattern like the one demonstrated in the example of andApply. If you just need to check that the name is equal to a certain value, you might prefer assertNodeName.
Require that a condition be true of the value returned by a NodeDecoder
If that condition isn't true, then an error of your choice is raised. Here's one way you could use this to parse a list that needs at least three elments:
-- Decodes "favorite-things \"raindrops on roses\" \"whiskers on kittens\" \"bright copper kettles\""
-- But fails on "favorite-things \"pussy willows\" \"christmas\""
decodeFavoriteThings =
arguments string
|> guardN "You need to least at least three of your favorite things" (\l -> length l >= 3)
|> singleNodeDocument "favorite-things"See also: guardV, the ValueDecoder equivalent of this function
A decoder which always fails with an error of your choice rather than parsing a node
There are several strategies you can use to decode documents and child blocks (which are treated as equivalent by this decoder). This section is broken up by strategy.
One of the more common types of documents you'll see has a series of nodes
which each encode some property. For example, the block of child nodes for an
employee record might contain a name node, an optional birthday node, and a
several project nodes. Note that this does not fit neatly as a dict-like node
(from the next section) since there are a fixed number of keys, and each key has
a special meaning.
In parse this example, you could parse the employee's name with [entry]
(#entry), their birthday with optionalEntry, and their
project with entries. This method would be agnostic to the order
of the properties in the block, but would require exactly one name and no more
more one birthday.
type alias EmployeeData =
{ name : String
, birthday : Maybe Int
, projects : List String
}
decodeEmployeeData = succeed EmployeeData
|> andApply (entry "name" (argument string))
|> andApply (optionalEntry "birthday" (argument int))
|> andApply (entries "project" (argument string))
|> andRequire noOtherEntriesThis decoder could successfully decode this document:
name bob
birthday 944937799
project big-freaking-contraption
project evil-laser-beamOne thing to note about this parsing strategy is that while most of the
rest of the parsing strategies simply take arguments and return a complete
Documentdecoder, this parsing strategy looks a little
bit more like the parsing strategy for arguments and values, where smaller unit
decoders are combined with andApply to form a bigger decoder.
Decode one entry which should occur exactly once
Returns MissingNode when the entry is absent, and DuplicateEntry if the entry is present more than once
Takes two arguments:
Decode an entry which may occur any number of times (including zero)
Takes two arguments:
Decode an entry which can occur zero or one times
Produces a DuplicateNode error if the entry occurs more than once
Takes two arguments:
Only succeed when a document has no nodes left
Any nodes proccessed by entry, optionalEntry, and entries are omitted from the count.
If no nodes are present (or remain) at all, then a TooManyNodes error is produced.
This can also be used to decode an empty document.
A common use of documents is to represent dict-like structures. In these cases, the names of nodes are arbitrary strings which represent keys in a dictionary, and the bodies of those nodes are simple types corresponding to the values in that dictionary.
Crucially, the keys shouldn't be preset keywords, and the values should all be decoded in the same way.
Decode a dict-like document
For each node in the document, that node is treated like an entry in a
dictionary. The name of the node is seen as the key, and the value is what
that node decodes to. Consider this subsection of the cargo.kdl official
KDL example:
dependencies {
nom "6.0.1"
thiserror "1.0.22"
}We could parse this like so:
decodeDependencyVersion : NodeDecoder String String
decodeDependencyVersion =
argument string
|> andRequire noOtherArguments
|> andRequire noOtherProperties
|> andRequire noChildren
decodeDependencies : DocumentDecoder String (Dict String String)
decodeDependencies =
children (dictDocument decodeDependencyVersion)
|> andRequire noOtherArguments
|> andRequire noOtherProperties
|> singleNodeDocument "dependencies"Notice that this will return DuplicateNode if someone
tries to specify a dependency twice.
Decode a document as if it were a multidict (a dict that can have multiple values associated with a single key)
This behaves much like dictDocument, but rather than raise an error when there are multiple nodes with one key,
Sometimes your document only has one node, so you don't need much to parse it!
Decode a document with exactly one node with a fixed name
Best used when your document only has one node, and that node should always have the same name. If you have multiple types of nodes that could appear, you might prefer to use singleNodeDocumentF.
The first three arguments are error values to return if the document is empty (contains no nodes), if there is an extra node that's unaccounted for, and if the name of the node in the document isn't the expected one.
If more than one node is present, then a TooManyNodes error will be produced.
If No nodes are present, a MissingNode error will be produced. If the node
has the incorrect name, than an UnexpectedNode error will be produced.
The next two arguments specify the expected name of the node and the NodeDecoder that should be used for it.
Decode a document with exactly one node, but where there might be several possible types of nodes.
Rather than only parse a single type of node (with a fixed name), this function lets you conditionally accept or deny multiple types of nodes based on their name.
The argument is expected to be a function which decides what
NodeDecoder to use for a given node name. If a node
name is unrecognized/unsupported, then returning Nothing will produce a
UnexpectedNode error.
If the document being parsed is empty, a NoNodes error is produced. If there is more than one node, an UnexpectedNode error is produced.
Keep in mind that you can return failN to reject a node that has a name you don't support.
See also:
singleNodeDocument, a version of this function for a fixed node namedocumentF, a version of this which can accept multiple nodes, not just one.The most powerful document decoders are these, which preserve the order of the nodes which they parse rather than forming them into a more ergonomic datastructure. This means that you can arrange the nodes into your own order-aware datastructure, although it forces you to do a little bit more work.
Decode a docmunt with an arbitrary number of nodes
This is the most basic, unspecialized type of document decoder. It simply
decodes all of the nodes that appear in the document, in order, choosing a
NodeDecoder based on the provided lookup table.
For each node to be decoded, its name will be checked against the provided
Dict. If a decoder is found there, it will be returned. Otherwise, an
UnexpectedNode error is returned.
It is often helpful to set fallbackNodeDecoder to fail with some error.
For a more powerful version of this, see documentF.
Decode a document with an arbitrary number of nodes, and the power to choose which decoder to use based on an algorithm
Similar to document, documentF allows for parsing an arbitrary number of
nodes in a document, ultimately yeilding a list. But while document chose
how to decode a node based on a Dict lookup, and always failed when the node
was absent from the dict, documentF allows you to specify your own algorithm
for choosing a subsequent decoder.
Sometimes, users or programmers make mistakes, and produce a KDL document which is valid KDL, but still contains mistakes that make it un-decodeable. When this happens, it's important to provide helpful and specific error messages so that the problem can be quickly identified and resolved. This module contains some error handling tools which aim to make this proccess simpler.
An error which occurs during decoding
Contains information about exactly where the error occured through the context
stack, which describes the location in terms of semantics (see
ContextFrame), and the location attribute, which describes
the location within the original text document.
The kind attribute describes the error itself -- What exactly went wrong. See
ErrorKind for more details.
You'll notice that Error and ErrorKind are both parameterized
in terms of some type e. That is a custom error type that you can use to
pass more information about certain kinds of failures. In particular, when
guard methods like guardV or guardN fail, or
when you deliberately trigger a failure with the fail decoder,
you may use your own error type to describe why the failure occured. If
you don't use either of these decoders, feel free to set this type to
Never.
Describes what went wrong in a decoding error
See Error for more information about decoding errors in general.
There are several ways that a decode can fail, all of which are enumerated by this type:
#inf as a rational, or any other time that the
type you're trying to decode into can't handle whatever the actual KDL value is.
Takes two arguments, the first being the expected type, and the latter being the
actual type.guard methods or by the fail decoder. See the
docs for Error for more information.oneOf
decoder, there tend to be multiple possibilities for why that failure occured,
since if any of the branches had succeeded, the decode would have succeeded.
When this occurs, this error kind is returned, describing all of the errors
that occured for each branch of the oneOf. The order of errors in the list
is guaranteed to exactly match the order of cases in the oneOf that triggered
the error.Describes where in the document tree an error occured
WithinProperty and WithinValue of course describe the name of the property
the error occured in and the order of the value that the problem occured in,
respectively. WithinNode describes both the name of the node, and the order
the node appears within its parent document/child block.
"Deeper" parts of the structure will be located closer to the top/front of the context stack. By the nature of KDL.
Represents each of the possible types that built-in decoders are available for.
Used to describe the expected type in a WrongType error.
Renders a DecodeError to a string
Takes as an argument the renderer to use for custom errors. If your custom
error type is Never, you can pass the Basics.never function here. Custom
errors are rendered directly using this function, and the result is returned
unmodifier.
Note that while this function will try its best to provide friendly error messages, it may be better in some cases to disect the error yourself and produce error messages tailored to the datastructure that you're decoding.
In particular, the error messages for PossibilitiesExhausted will likely be
poor quality.
Use a decoder to decode a KDL document
At the end of the day, once you've built up your KDL decoder, this function is how you use it. Just provide your decoder and a
Documentto decode, and you'll either get back the data structure your decoder was designed for, or one of your error types if something went wrong.