kdl-hs-0.1.0: src/Data/KDL/Decoder/Monad.hs
{-# LANGUAGE LambdaCase #-}
{-|
This module defines the Monad interface for decoding a KDL document. Intended to
be imported from "Data.KDL" as:
> import Data.KDL qualified as KDL
For most use-cases, this Monad interface is sufficient. You may wish to use
"Data.KDL.Decoder.Arrow" if you would like to statically analyze a decoder's
schema, e.g. to generate documentation.
= Quickstart
See "Data.KDL"
-}
module Data.KDL.Decoder.Monad (
-- * Decoding entrypoint
decodeWith,
decodeFileWith,
decodeDocWith,
-- * Decoder
Decoder,
module Data.KDL.Decoder.DecodeM,
fail,
withDecoder,
debug,
-- * Decode type classes
-- $decodeTypeClassesDoc
noSchema,
Arrow.DecodeNode (..),
Arrow.DecodeValue (..),
-- * Document
DocumentDecoder,
document,
-- * NodeList
NodeListDecoder,
node,
remainingNodes,
argAt,
argsAt,
dashChildrenAt,
dashNodesAt,
-- ** Explicitly specify decoders
nodeWith,
remainingNodesWith,
dashChildrenAtWith,
dashNodesAtWith,
argAtWith,
argsAtWith,
-- ** Explicitly specify decoders and type annotations
nodeWith',
remainingNodesWith',
dashChildrenAtWith',
argAtWith',
argsAtWith',
-- * Node
NodeDecoder,
arg,
prop,
remainingProps,
children,
-- ** Explicitly specify decoders
argWith,
propWith,
remainingPropsWith,
-- ** Explicitly specify decoders and type annotations
argWith',
propWith',
remainingPropsWith',
-- * Value
ValueDecoder,
any,
text,
number,
bool,
null,
-- * Combinators
Arrow.oneOf,
Arrow.many,
Arrow.optional,
Arrow.option,
Arrow.some,
) where
import Control.Applicative (Alternative)
import Control.Arrow qualified as Arrow
import Data.Coerce (coerce)
import Data.KDL.Decoder.Arrow qualified as Arrow
import Data.KDL.Decoder.DecodeM
import Data.KDL.Decoder.Schema (
Schema (..),
)
import Data.KDL.Types (
Document,
Node,
NodeList,
Value,
)
import Data.Map (Map)
import Data.Scientific (Scientific)
import Data.Text (Text)
import Data.Typeable (Typeable)
import Prelude hiding (any, fail, null)
-- | Decode the given KDL configuration with the given decoder.
decodeWith :: forall a. DocumentDecoder a -> Text -> Either DecodeError a
decodeWith = coerce (Arrow.decodeWith @a)
-- | Read KDL configuration from the given file path and decode it with the given decoder.
decodeFileWith :: forall a. DocumentDecoder a -> FilePath -> IO (Either DecodeError a)
decodeFileWith = coerce (Arrow.decodeFileWith @a)
-- | Decode an already-parsed 'Document' with the given decoder.
decodeDocWith :: forall a. DocumentDecoder a -> Document -> Either DecodeError a
decodeDocWith = coerce (Arrow.decodeDocWith @a)
{----- Decoder -----}
-- | @Decoder o a@ represents an action that decodes a KDL object of type @o@
-- and returns a value of type @a@.
--
-- Exactly the same as the Arrow 'Data.KDL.Decoder.Arrow.Decoder', except
-- provides a Monad instance that's useful for do-notation. The only downside of
-- using this over the Arrow 'Data.KDL.Decoder.Arrow.Decoder' is you don't get a
-- statically derived schema of the entire document. If you want that, you must
-- use either arrows notation with the Arrow 'Data.KDL.Decoder.Arrow.Decoder' or
-- use do-notation with @-XApplicativeDo@.
newtype Decoder o a = Decoder (Arrow.Decoder o () a)
deriving
( Functor
, Applicative
, Alternative
)
instance Monad (Decoder o) where
Decoder (Arrow.Decoder _ run1) >>= k =
Decoder . Arrow.Decoder SchemaUnknown $ \a -> do
x <- run1 a
let Decoder (Arrow.Decoder _ run2) = k x
run2 a
-- $decodeTypeClassesDoc
-- To avoid code duplication, the same 'DecodeNode' and 'DecodeValue' type classes
-- are used for both the Arrow and Monad decoders. However, the type classes are
-- implemented with the Arrow decoder, as the Monad decoder is lossy regarding the
-- schema. If you're implementing instances that may be used with the Arrow
-- decoder, you should probably implement the decoder with
-- the "Data.KDL.Decoder.Arrow" API.
--
-- Otherwise, use the normal Monad decoder API and use 'noSchema' at the very end,
-- which indicates that this instance doesn't provide any schema information.
--
-- @
-- instance KDL.DecodeNode Person where
-- nodeDecoder = KDL.noSchema $ do
-- name <- KDL.arg
-- age <- KDL.prop "age"
-- pure Person{..}
--
-- instance KDL.DecodeValue MyVal where
-- valueDecoder =
-- KDL.noSchema . KDL.oneOf $
-- [ MyText \<$> KDL.text
-- , KDL.withDecoder KDL.number $ \x -> do
-- if x == 0
-- then pure MyZero
-- else KDL.failM "integer value must be zero"
-- ]
-- @
-- | Drop the Monad Decoder back down to an Arrow Decoder.
noSchema :: Decoder o a -> Arrow.Decoder o () a
noSchema (Decoder decoder) = decoder
-- | Unconditionally fail the decoder.
--
-- === __Example__
--
-- @
-- decoder = do
-- x <- KDL.arg
-- when (x > 100) $ do
-- KDL.fail $ "argument is too large: " <> (Text.pack . show) x
-- pure x
-- @
fail :: forall a o. Text -> Decoder o a
fail msg = coerce (Arrow.arr (\() -> msg) Arrow.>>> Arrow.fail @a)
-- | Run actions within a t'Decoder'. Useful for adding post-processing logic.
--
-- === __Example__
--
-- @
-- decoder = KDL.withDecoder KDL.number $ \\x -> do
-- when (x > 100)
-- KDL.failM $ "argument is too large: " <> (Text.pack . show) x
-- pure $ MyVal x
-- @
withDecoder :: forall o a b. Decoder o a -> (a -> DecodeM b) -> Decoder o b
withDecoder = coerce (Arrow.withDecoder @o @() @a @b)
-- | Debug the current state of the object being decoded.
--
-- === __Example__
--
-- @
-- decoder = do
-- KDL.debug -- Node{entries = [Entry{}, Entry{}]}
-- x <- KDL.arg
-- KDL.debug -- Node{entries = [Entry{}]}
-- y <- KDL.arg
-- KDL.debug -- Node{entries = []}
-- pure (x, y)
-- @
debug :: forall o. (Show o) => Decoder o ()
debug = coerce (Arrow.debug @o @())
{----- Decoding Document -----}
newtype DocumentDecoder a = DocumentDecoder (NodeListDecoder a)
-- | Finalize a 'NodeListDecoder' as a 'DocumentDecoder' to use with 'decodeWith'.
--
-- Ensures that all nodes have been decoded (e.g. error if the user specified
-- unrecognized nodes, or misspelled a node name). To allow unrecognized nodes,
-- use @remainingNodes \@Node@ and ignore the result.
document :: forall a. NodeListDecoder a -> DocumentDecoder a
document = coerce (Arrow.document @a)
{----- Decoding NodeList -----}
type NodeListDecoder = Decoder NodeList
-- | Decode a node with the given name and decoder.
--
-- == __Example__
--
-- @
-- instance KDL.DecodeNode Person where
-- nodeTypeAnns _ = ["Person"]
-- nodeDecoder = KDL.noSchema $ do
-- name <- KDL.arg
-- pure Person{..}
--
-- let
-- config =
-- """
-- person "Alice"
-- person "Bob"
-- person "Charlie"
-- (Person)person "Danielle"
-- (Dog)person "Fido"
-- """
-- decoder = KDL.document $ do
-- many $ KDL.node "person"
-- KDL.decodeWith decoder config == Right ["Alice", "Bob", "Charlie", "Danielle"]
-- @
node :: forall a. (Arrow.DecodeNode a) => Text -> NodeListDecoder a
node = coerce (Arrow.node @a)
-- | Same as 'node', except explicitly specify the 'NodeDecoder' instead of using 'Arrow.DecodeNode'.
--
-- == __Example__
--
-- @
-- let
-- config =
-- """
-- person "Alice"
-- person "Bob"
-- person "Charlie"
-- (Person)person "Danielle"
-- (Dog)person "Fido"
-- """
-- decoder = KDL.document $ do
-- many . KDL.nodeWith "person" ["Person"] $ KDL.arg
-- KDL.decodeWith decoder config == Right ["Alice", "Bob", "Charlie", "Danielle"]
-- @
nodeWith :: forall a. (Typeable a) => Text -> NodeDecoder a -> NodeListDecoder a
nodeWith = coerce (Arrow.nodeWith @() @a)
-- | Same as 'nodeWith', except allow specifying type annotations.
nodeWith' :: forall a. (Typeable a) => Text -> [Text] -> NodeDecoder a -> NodeListDecoder a
nodeWith' = coerce (Arrow.nodeWith' @() @a)
-- | Decode all remaining nodes with the given decoder.
--
-- == __Example__
--
-- @
-- instance KDL.DecodeNode MyArg where
-- nodeDecoder = KDL.noSchema $ do
-- name <- KDL.arg
-- pure MyArg{..}
--
-- let
-- config =
-- """
-- build "pkg1"
-- build "pkg2"
-- lint "pkg1"
-- """
-- decoder = KDL.document $ do
-- KDL.remainingNodes
-- KDL.decodeWith decoder config == Right (Map.fromList [("build", [MyArg "pkg1", MyArg "pkg2"]), ("lint", [MyArg "pkg1"])])
-- @
remainingNodes :: forall a. (Arrow.DecodeNode a) => NodeListDecoder (Map Text [a])
remainingNodes = coerce (Arrow.remainingNodes @a)
-- | Same as 'remainingNodes', except explicitly specify the 'NodeDecoder' instead of using 'Arrow.DecodeNode'
--
-- == __Example__
--
-- @
-- let
-- config =
-- """
-- build "pkg1"
-- build "pkg2"
-- lint "pkg1"
-- """
-- decoder = KDL.document $ do
-- KDL.remainingNodesWith [] KDL.arg
-- KDL.decodeWith decoder config == Right (Map.fromList [("build", ["pkg1", "pkg2"]), ("lint", ["pkg1"])])
-- @
remainingNodesWith :: forall a. (Typeable a) => NodeDecoder a -> NodeListDecoder (Map Text [a])
remainingNodesWith = coerce (Arrow.remainingNodesWith @() @a)
-- | Same as 'remainingNodesWith', except allow specifying type annotations.
remainingNodesWith' :: forall a. (Typeable a) => [Text] -> NodeDecoder a -> NodeListDecoder (Map Text [a])
remainingNodesWith' = coerce (Arrow.remainingNodesWith' @() @a)
-- | A helper to decode the first argument of the first node with the given name.
-- A utility for nodes that are acting like a key-value store.
--
-- == __Example__
--
-- @
-- let
-- config =
-- """
-- verbose #true
-- """
-- decoder = KDL.document $ do
-- KDL.argAt "verbose"
-- KDL.decodeWith decoder config == Right True
-- @
argAt :: forall a. (Arrow.DecodeValue a) => Text -> NodeListDecoder a
argAt = coerce (Arrow.argAt @a)
-- | Same as 'argAt', except explicitly specify the 'ValueDecoder' instead of using 'Arrow.DecodeValue'
--
-- == __Example__
--
-- @
-- let
-- config =
-- """
-- verbose #true
-- """
-- decoder = KDL.document $ do
-- KDL.argAtWith "verbose" [] KDL.bool
-- KDL.decodeWith decoder config == Right True
-- @
argAtWith :: forall a. (Typeable a) => Text -> ValueDecoder a -> NodeListDecoder a
argAtWith = coerce (Arrow.argAtWith @() @a)
-- | Same as 'argAtWith', except allow specifying type annotations.
argAtWith' :: forall a. (Typeable a) => Text -> [Text] -> ValueDecoder a -> NodeListDecoder a
argAtWith' = coerce (Arrow.argAtWith' @() @a)
-- | A helper to decode all the arguments of the first node with the given name.
-- A utility for nodes that are acting like a key-value store with a list of values.
--
-- == __Example__
--
-- @
-- let
-- config =
-- """
-- email "a@example.com" "b@example.com"
-- """
-- decoder = KDL.document $ do
-- KDL.argsAt "email"
-- KDL.decodeWith decoder config == Right ["a@example.com", "b@example.com"]
-- @
argsAt :: forall a. (Arrow.DecodeValue a) => Text -> NodeListDecoder [a]
argsAt = coerce (Arrow.argsAt @a)
-- | Same as 'argsAt', except explicitly specify the 'ValueDecoder' instead of using 'Arrow.DecodeValue'
--
-- == __Example__
--
-- @
-- let
-- config =
-- """
-- email "a@example.com" "b@example.com"
-- """
-- decoder = KDL.document $ do
-- KDL.argsAtWith "email" [] KDL.text
-- KDL.decodeWith decoder config == Right ["a@example.com", "b@example.com"]
-- @
argsAtWith :: forall a. (Typeable a) => Text -> ValueDecoder a -> NodeListDecoder [a]
argsAtWith = coerce (Arrow.argsAtWith @() @a)
-- | Same as 'argsAtWith', except allow specifying type annotations.
argsAtWith' :: forall a. (Typeable a) => Text -> [Text] -> ValueDecoder a -> NodeListDecoder [a]
argsAtWith' = coerce (Arrow.argsAtWith' @() @a)
-- | A helper for decoding child values in a list following the KDL convention of being named "-".
--
-- == __Example__
--
-- @
-- let
-- config =
-- """
-- attendees {
-- - "Alice"
-- - "Bob"
-- }
-- """
-- decoder = KDL.document $ do
-- KDL.dashChildrenAt "attendees"
-- KDL.decodeWith decoder config == Right ["Alice", "Bob"]
-- @
dashChildrenAt :: forall a. (Arrow.DecodeValue a) => Text -> NodeListDecoder [a]
dashChildrenAt = coerce (Arrow.dashChildrenAt @a)
-- | Same as 'dashChildrenAt', except explicitly specify the 'ValueDecoder' instead of using 'Arrow.DecodeValue'
--
-- == __Example__
--
-- @
-- let
-- config =
-- """
-- attendees {
-- - "Alice"
-- - "Bob"
-- }
-- """
-- decoder = KDL.document $ do
-- KDL.dashChildrenAtWith "attendees" [] KDL.text
-- KDL.decodeWith decoder config == Right ["Alice", "Bob"]
-- @
dashChildrenAtWith :: forall a. (Typeable a) => Text -> ValueDecoder a -> NodeListDecoder [a]
dashChildrenAtWith = coerce (Arrow.dashChildrenAtWith @() @a)
-- | Same as 'dashChildrenAtWith', except allow specifying type annotations.
dashChildrenAtWith' :: forall a. (Typeable a) => Text -> [Text] -> ValueDecoder a -> NodeListDecoder [a]
dashChildrenAtWith' = coerce (Arrow.dashChildrenAtWith' @() @a)
-- | A helper for decoding child values in a list following the KDL convention of being named "-".
--
-- == __Example__
--
-- @
-- instance KDL.DecodeNode Attendee where
-- nodeDecoder = KDL.noSchema $ do
-- name <- KDL.arg
-- pure Attendee{..}
--
-- let
-- config =
-- """
-- attendees {
-- - "Alice"
-- - "Bob"
-- }
-- """
-- decoder = KDL.document $ do
-- KDL.dashNodesAt "attendees"
-- KDL.decodeWith decoder config == Right [Attendee "Alice", Attendee "Bob"]
-- @
dashNodesAt :: forall a. (Arrow.DecodeNode a) => Text -> NodeListDecoder [a]
dashNodesAt = coerce (Arrow.dashNodesAt @a)
-- | Same as 'dashChildrenAt', except explicitly specify the 'NodeDecoder' instead of using 'Arrow.DecodeNode'
--
-- == __Example__
--
-- @
-- let
-- config =
-- """
-- attendees {
-- - "Alice"
-- - "Bob"
-- }
-- """
-- decoder = KDL.document $ do
-- KDL.dashNodesAtWith "attendees" [] KDL.arg
-- KDL.decodeWith decoder config == Right ["Alice", "Bob"]
-- @
dashNodesAtWith :: forall a. (Typeable a) => Text -> NodeDecoder a -> NodeListDecoder [a]
dashNodesAtWith = coerce (Arrow.dashNodesAtWith @() @a)
{----- Decoding Node -----}
type NodeDecoder a = Decoder Node a
-- | Decode an argument in the node.
--
-- === __Example__
--
-- @
-- let
-- config =
-- """
-- person \"Alice" 1 2 3
-- """
-- decoder = KDL.document $ do
-- KDL.nodeWith "person" decodePerson
-- decodePerson = do
-- name <- KDL.arg
-- vals <- KDL.many KDL.arg
-- pure (name, vals)
-- KDL.decodeWith decoder config == Right (\"Alice", [1, 2, 3])
-- @
arg :: forall a. (Arrow.DecodeValue a) => NodeDecoder a
arg = coerce (Arrow.arg @a)
-- | Same as 'arg', except explicitly specify the 'ValueDecoder' instead of using 'DecodeValue'
--
-- === __Example__
--
-- @
-- let
-- config =
-- """
-- person \"Alice" 1 2 3
-- """
-- decoder = KDL.document $ do
-- KDL.nodeWith "person" $ decodePerson
-- decodePerson = do
-- name \<- KDL.argWith $ Text.toUpper \<$> KDL.text
-- vals \<- KDL.many $ KDL.argWith $ show \<$> KDL.valueDecoder @Int
-- pure (name, vals)
-- KDL.decodeWith decoder config == Right (\"ALICE", ["1", "2", "3"])
-- @
argWith :: forall a. (Typeable a) => ValueDecoder a -> NodeDecoder a
argWith = coerce (Arrow.argWith @() @a)
-- | Same as 'argWith', except allow specifying type annotations.
argWith' :: forall a. (Typeable a) => [Text] -> ValueDecoder a -> NodeDecoder a
argWith' = coerce (Arrow.argWith' @() @a)
-- | Decode the property with the given name in the node.
--
-- If the property appears multiple times, the last value is returned, as
-- defined in the spec.
--
-- === __Example__
--
-- @
-- let
-- config =
-- """
-- my-node a=1 b=2 a=3
-- """
-- decoder = KDL.document $ do
-- KDL.nodeWith "my-node" $ KDL.prop @Int "a"
-- KDL.decodeWith decoder config == Right 3
-- @
prop :: forall a. (Arrow.DecodeValue a) => Text -> NodeDecoder a
prop = coerce (Arrow.prop @a)
-- | Same as 'prop', except explicitly specify the 'ValueDecoder' instead of using 'DecodeValue'
--
-- === __Example__
--
-- @
-- let
-- config =
-- """
-- my-node a=1 b=2 a=3
-- """
-- decoder = KDL.document $ do
-- KDL.nodeWith "my-node" $ do
-- KDL.propWith "a" $ show \<$> KDL.number
-- KDL.decodeWith decoder config == Right "3.0"
-- @
propWith :: forall a. (Typeable a) => Text -> ValueDecoder a -> NodeDecoder a
propWith = coerce (Arrow.propWith @() @a)
-- | Same as 'propWith', except allow specifying type annotations.
propWith' :: forall a. (Typeable a) => Text -> [Text] -> ValueDecoder a -> NodeDecoder a
propWith' = coerce (Arrow.propWith' @() @a)
-- | Decode all remaining props
--
-- === __Example__
--
-- @
-- let
-- config =
-- """
-- my-node a=1 b=2 a=3
-- """
-- decoder = KDL.document $ do
-- KDL.nodeWith "my-node" $ KDL.remainingProps @Int
-- KDL.decodeWith decoder config == (Right . Map.fromList) [("a", 3), ("b", 2)]
-- @
remainingProps :: forall a. (Arrow.DecodeValue a) => NodeDecoder (Map Text a)
remainingProps = coerce (Arrow.remainingProps @a)
-- | Same as 'remainingProps', except explicitly specify the 'ValueDecoder' instead of using 'DecodeValue'
--
-- === __Example__
--
-- @
-- let
-- config =
-- """
-- my-node a=1 b=2 a=3
-- """
-- decoder = KDL.document $ do
-- KDL.nodeWith "my-node" $ do
-- KDL.remainingPropsWith $ show \<$> KDL.number
-- KDL.decodeWith decoder config == (Right . Map.fromList) [("a", "3.0"), ("b", "2.0")]
-- @
remainingPropsWith :: forall a. (Typeable a) => ValueDecoder a -> NodeDecoder (Map Text a)
remainingPropsWith = coerce (Arrow.remainingPropsWith @() @a)
-- | Same as 'remainingPropsWith', except allow specifying type annotations.
remainingPropsWith' :: forall a. (Typeable a) => [Text] -> ValueDecoder a -> NodeDecoder (Map Text a)
remainingPropsWith' = coerce (Arrow.remainingPropsWith' @() @a)
-- | Decode the children of the node.
--
-- === __Example__
--
-- @
-- let
-- config =
-- """
-- person \"Alice" {
-- email "alice@example.com"
-- }
-- """
-- decoder = KDL.document $ do
-- KDL.nodeWith "person" $ do
-- name <- KDL.arg
-- email <- KDL.children $ KDL.argAt "email"
-- pure Person{..}
-- KDL.decodeWith decoder config == Right Person{name = \"Alice", email = "alice\@example.com"}
-- @
children :: forall a. NodeListDecoder a -> NodeDecoder a
children = coerce (Arrow.children @() @a)
{----- Decoding Value -----}
type ValueDecoder a = Decoder Value a
-- | Decode any value, without any possibility of failure.
any :: ValueDecoder Value
any = coerce (Arrow.any @())
-- | Decode a KDL text value.
text :: ValueDecoder Text
text = coerce (Arrow.text @())
-- | Decode a KDL number value.
number :: ValueDecoder Scientific
number = coerce (Arrow.number @())
-- | Decode a KDL bool value.
bool :: ValueDecoder Bool
bool = coerce (Arrow.bool @())
-- | Decode a KDL null value.
null :: ValueDecoder ()
null = coerce (Arrow.null @())