Rerouted (renamed, verified). The GitHub user or repository was renamed. The tag is still there, at the new name. The Lamdera package server serves a copy recovered from https://github.com/alch-emi/elm-kdl.
What we checked: The repo moved to github.com/alch-emi/elm-kdl. Rebuilt with git archive from commit 526e0e2058e6 of https://github.com/alch-emi/elm-kdl (zip prefix Alch-Emi-elm-kdl-e89d755/, America/Los_Angeles times): the zip is byte-identical to the one package.elm-lang.org recorded, SHA-1 99c0c384852903c9bf261ca18292969f62f83dc9.
The Lamdera compiler uses this copy and prints a one-line notice saying so. elm itself fails to download this version.
Details · package.elm-lang.org's zip: https://github.com/Alch-Emi/elm-kdl/zipball/1.0.0/
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 "the data wasn't a number!"
|> 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 "needs 3 args" (int "expected an int"))
|> andApply (argument "needs 3 args" (string "expected a string"))
|> andApply (argument "needs 3 args" (int "expected an int"))
|> andRequire (noOtherArguments "too many arguments!")
|> andRequire (noOtherProperties "shouldnt be any properties")
|> andRequire (noChildren "shouldnt be any children!")
|> singleNodeDocument "no node provided!" "only expected one node!" "unrecognized node!" "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 give its first argument as an 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 also give its first argument as an 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 also give its first argument as an 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 return its first argument as an 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 return its first argument as an 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 its first argument as an error when applied to anything other then #null.
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 "count needs 1 argument" (
int "argument must be an integer"
|> guardV "can't have a negative count!" (\x -> x >= 0)
)
|> andRequire (noOtherArguments "too many arguments on count!")
|> singleNodeDocument "missing node!" "extra node!" "unrecognized node!" "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 "date takes one argument"
(
oneOfV "dates must be either strings or ints!"
[ string () |> map Datestring
, int () |> map UnixTime
]
)
|> andRequire (noOtherArguments "too many arguments!")
|> singleNodeDocument "missing node" "too many nodes" "unrecognized node" "date"
(parse "date 1733882937" |> Result.map (decode decodeDate)) == Ok (Ok (UnixTime 1733882937))oneOfV takes two arguments. The first is an error to return if all other
errors fail. The second is a list of decoders that should be tried. Note that
all errors from the internal parsers are discarded if those parsers fail.
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, the provided error will be returned instead.
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 "two args required!" (string "first argument should be a string"))
|> andApply (argument "two args required!" (int "second argument should be an int"))
|> andRequire (noOtherArguments "only 2-tuples are supported")
|> singleNodeDocument "missing node" "too many nodes" "wrong node name" "tuple"
-- Decodes `tuple 1 hi` but not `tuple hi 1`
tuple2 = succeed pair
|> andApply (argument "two args required!" (int "first argument should be an int"))
|> andApply (argument "two args required!" (string "second argument should be a string"))
|> andRequire (noOtherArguments "only 2-tuples are supported")
|> singleNodeDocument "missing node" "too many nodes" "wrong node name" "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.
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. The ifAbsent argument is returned as an error if the property is missing from the node. 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 (noOtherNodes <error>)
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 "your favorite things must be strings!")
|> guardN "You need to least at least three of your favorite things" (\l -> length l >= 3)
|> singleNodeDocument "missing node!" "extra node!" "unrecognized node!" "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 "no name provided" "employees can only have one name" "name" (argument "name takes one argument" (string "name must be a string")))
|> andApply (optionalEntry "employees can only have one birthday" "birthday" (argument "birthday takes one argument" (int "birthday must be a unix timestamp")))
|> andApply (entries "project" (argument "projects takes one argument" (string "projects must be strings")))
|> andRequire (noOtherEntries "unrecognized property")This 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
Takes four 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
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.
The first argument is the error to produce if any nodes are present at all.
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 "missing version" (string "version should be a string")
|> andRequire (noOtherArguments "a dependency should only have one version")
|> andRequire (noOtherProperties "dependency properties aren't supported")
|> andRequire (noChildren "dependencies can't have children")
decodeDependencies : DocumentDecoder String (Dict String String)
decodeDependencies =
dictDocument "dependencies can only be listed once" decodeDependencyVersion
|> children
|> andRequire (noOtherArguments "specify dependencies in the child block")
|> andRequire (noOtherProperties "specify dependencies in the child block")
|> singleNodeDocument "missing dependency block" "multiple dependency blocks" "unrecognized node" "dependencies"Notice that this will return "dependencies can only be listed once" 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.
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 first two arguments are errors to return if the node is absent, or if there is more than one node present. Then the third and final argument is a function which decides what NodeDecoder to use for a given node name.
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, the the ifUnrecognized 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.
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.