cfg-0.0.2.0: src/Cfg/Parser/Config.hs
-- |
-- Module : Cfg.Parser.Config
-- Copyright : © Jonathan Lorimer, 2023
-- License : MIT
-- Maintainer : jonathanlorimer@pm.me
-- Stability : stable
--
-- @since 0.0.2.0
--
-- This module contains the generic machinery for building parsers from the
-- structure of a type. The majority of the work here is threading a
-- 'Cfg.Parser.Value.ValueParser' through the 'KeyTree.KeyTree' structure until
-- a 'Pure' value is hit, and then dispatching the correct parser.
module Cfg.Parser.Config
( -- * Default Parser Function
defaultParseConfig
-- * Generic Machinery
, GConfigParser (..)
)
where
import Cfg.Options
import Cfg.Parser (ConfigParseError (..), ConfigParser (..))
import Data.Kind (Type)
import Data.Map qualified as M
import Data.Text (Text)
import Data.Text qualified as T
import GHC.Generics
import KeyTree
-- | This function is the workhorse of the generic machinery, however the user
-- should never have to invoke it directly. Instead, one of the newtypes from
-- 'Cfg.Deriving.Config' should call into this function in the definition of a
-- 'Cfg.Parser.ConfigParser' instance. The deriving via type should pull out
-- the 'ConfigOptions' from type level information.
--
-- @since 0.0.1.0
defaultParseConfig
:: forall a
. (Generic a, GConfigParser (Rep a))
=> ConfigOptions
-> KeyTree Text Text
-> Either ConfigParseError a
defaultParseConfig opts tree = fmap to $ gParseConfig opts tree
-- | This class is the generic version of 'ConfigParser'. It recurses on the
-- generic structure of a type, building up a return type for the parser.
--
-- @since 0.0.2.0
class GConfigParser (f :: Type -> Type) where
gParseConfig :: ConfigOptions -> KeyTree Text Text -> Either ConfigParseError (f p)
-- | This is the \"base case\", since "GHC.Generics" don't recurse the generic
-- represetation multiple levels, @a@ is just a plain type. Therefore we call
-- 'parseConfig' on it. @a@ may be another nested record, in which case
-- 'gParseConfig' will probably get called again, but for the generic
-- representation of a sub-tree. Or it will find the default instance for
-- 'ConfigParser' (indicating that we have reached a leaf) and dispatch to a
-- value parser through 'parseConfig'.
--
-- @since 0.0.2.0
instance (ConfigParser a) => GConfigParser (K1 R a) where
gParseConfig _ kt = K1 <$> parseConfig kt
-- | This is the type constructor case, if we are dealing with a
-- 'Cfg.Deriving.Config.ConfigRoot' instance, then we have lookup the \"root
-- key\", but in all other cases we just keep recursing.
--
-- @since 0.0.2.0
instance (GConfigParser f, Datatype d) => GConfigParser (M1 D d f) where
gParseConfig opts t@(Free keyForest) =
case opts of
Root (RootOptions{rootOptionsRootKey = TypeName modifier}) ->
let
key = modifier . T.pack $ datatypeName @d undefined
in
case M.lookup key keyForest of
Just subTree -> M1 <$> gParseConfig opts subTree
Nothing -> Left $ MissingKey key t
_ -> M1 <$> gParseConfig opts t
gParseConfig opts (Pure value) = Left $ ExpectedKeyFoundValue key value
where
key = keyModifier opts . T.pack $ datatypeName @d undefined
-- | This is the data constructor case, if we are dealing with a
-- 'Cfg.Deriving.Config.ConfigRoot' instance, then we have lookup the \"root
-- key\", but in all other cases we just keep recursing.
--
-- @since 0.0.2.0
instance (Constructor c, GConfigParser f) => GConfigParser (M1 C c f) where
gParseConfig opts t@(Free keyForest) =
case opts of
Root (RootOptions{rootOptionsRootKey = ConstructorName modifier}) ->
let
key = modifier . T.pack $ conName @c undefined
in
case M.lookup key keyForest of
Just subTree -> M1 <$> gParseConfig opts subTree
Nothing -> Left $ MissingKey key t
_ -> M1 <$> gParseConfig opts t
gParseConfig opts (Pure value) = Left $ ExpectedKeyFoundValue key value
where
key = keyModifier opts . T.pack $ conName @c undefined
-- | This is the most important case, we need to look up the subconfig by key
-- (just the record field with all key modifiers applied), and then recursively
-- parse the sub tree.
--
-- @since 0.0.2.0
instance (Selector s, GConfigParser f) => GConfigParser (M1 S s f) where
gParseConfig opts (Pure value) = Left $ ExpectedKeyFoundValue key value
where
key = keyModifier opts . T.pack $ selName @s undefined
gParseConfig opts t@(Free keyForest) =
case M.lookup selectorName keyForest of
Nothing -> Left $ MissingKey selectorName t
Just subTree -> M1 <$> gParseConfig opts subTree
where
selectorName = keyModifier opts . T.pack $ selName @s undefined
-- | This is the product case, we just distribute the parsers over the
-- different product fields.
--
-- Notably, there is no sum type case. We could potentially add that in the future,
-- allowing users to specify different cases of configuration. But right now
-- that seems like it would be more confusing than helpful, so we just give a
-- type error by eliding the instance.
--
-- @since 0.0.2.0
instance (GConfigParser a, GConfigParser b) => GConfigParser (a :*: b) where
gParseConfig opts xs = do
a <- gParseConfig opts xs
b <- gParseConfig opts xs
pure $ a :*: b