figment-0.2.0.0: src/Figment/Partial.hs
{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DefaultSignatures #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE UndecidableInstances #-}
-- | Higher-kinded partial configurations.
--
-- Write a configuration record once, parameterised by a functor:
--
-- > data AppF f = App
-- > { host :: Field f Text
-- > , port :: Field f Int
-- > , db :: DbF f -- nested partial record
-- > } deriving stock Generic
-- > instance Partial AppF
-- > type App = AppF Identity -- fields are plain: host :: Text
--
-- @AppF Maybe@ is the /partial/ form: every field optional. Defaults
-- are written as a partial value, merged under whatever the providers
-- supplied, and 'complete' turns the result into the total @AppF
-- Identity@, reporting /every/ missing field (with the env var, CLI
-- flag, or file key that would have set it) rather than the first.
--
-- The compile-time guarantee: 'Figment.extractTotal' takes a total
-- @AppF Identity@ as its defaults, so a missing-field error is
-- impossible by construction; only type errors remain.
module Figment.Partial
( Field
, Partial (..)
, GPartial
) where
import Control.Applicative ((<|>))
import Data.Functor.Identity (Identity (..))
import Data.Kind (Type)
import Data.List.NonEmpty (NonEmpty (..))
import qualified Data.List.NonEmpty as NE
import qualified Data.Map.Strict as Map
import Data.Text (Text)
import qualified Data.Text as Text
import GHC.Generics
import GHC.TypeLits (ErrorMessage (..), TypeError)
import Figment.Decode
import Figment.Error
import Figment.Path
import Figment.Value
-- | @Field Identity a = a@, so the total record has ordinary fields;
-- @Field Maybe a = Maybe a@ for the partial record.
type family Field (f :: Type -> Type) (a :: Type) :: Type where
Field Identity a = a
Field f a = f a
-- | Records whose fields are 'Field's or nested 'Partial' records.
-- Every method has a generic default: @instance Partial AppF@ suffices.
class Partial (c :: (Type -> Type) -> Type) where
-- | Decode whatever is present; absent fields are 'Nothing'. Type
-- errors in present fields are reported (all of them).
partialDecoder :: Options -> Decoder (c Maybe)
default partialDecoder
:: (Generic (c Maybe), GPartial (Rep (c Maybe)) (Rep (c Identity)))
=> Options -> Decoder (c Maybe)
partialDecoder o = to <$> mkDecoder (\c v -> case valueNode v of
Dict m -> recordCheck o (gpnames @(Rep (c Maybe)) @(Rep (c Identity)) o) c m
(gpfields @(Rep (c Maybe)) @(Rep (c Identity)) o c m)
n -> Failure (single (atValue c v (InvalidType (describe n) "a dictionary"))))
-- | Right-biased combination: fields set on the right win.
overlay :: c Maybe -> c Maybe -> c Maybe
default overlay
:: (Generic (c Maybe), GPartial (Rep (c Maybe)) (Rep (c Identity)))
=> c Maybe -> c Maybe -> c Maybe
overlay a b = to (goverlay @(Rep (c Maybe)) @(Rep (c Identity)) (from a) (from b))
-- | The partial value with nothing set.
emptyPartial :: c Maybe
default emptyPartial
:: (Generic (c Maybe), GPartial (Rep (c Maybe)) (Rep (c Identity)))
=> c Maybe
emptyPartial = to (gempty @(Rep (c Maybe)) @(Rep (c Identity)))
-- | The total value, or the (relative) paths of every required field
-- that is still missing. Fields whose type has a 'missingValue'
-- (e.g. 'Maybe') are never missing.
complete :: Options -> c Maybe -> Either (NonEmpty Path) (c Identity)
default complete
:: (Generic (c Maybe), Generic (c Identity), GPartial (Rep (c Maybe)) (Rep (c Identity)))
=> Options -> c Maybe -> Either (NonEmpty Path) (c Identity)
complete o p = case gcomplete @(Rep (c Maybe)) @(Rep (c Identity)) o root (from p) of
Left (x : xs) -> Left (x :| xs)
Left [] -> Left (root :| [])
Right r -> Right (to r)
-- | Embed a total value (every field set).
total :: c Identity -> c Maybe
default total
:: (Generic (c Maybe), Generic (c Identity), GPartial (Rep (c Maybe)) (Rep (c Identity)))
=> c Identity -> c Maybe
total = to . gtotal @(Rep (c Maybe)) @(Rep (c Identity)) . from
-- | Accumulating completion.
type Completion a = Either [Path] a
both :: Completion (a -> b) -> Completion a -> Completion b
both (Left a) (Left b) = Left (a ++ b)
both (Left a) (Right _) = Left a
both (Right _) (Left b) = Left b
both (Right f) (Right x) = Right (f x)
-- | Generic machinery behind 'Partial's defaults.
class GPartial (pm :: Type -> Type) (pi :: Type -> Type) where
gpfields :: Options -> DecodeContext -> Dict -> Result (pm x)
gpnames :: Options -> [Text]
goverlay :: pm x -> pm x -> pm x
gempty :: pm x
gcomplete :: Options -> Path -> pm x -> Completion (pi x)
gtotal :: pi x -> pm x
instance GPartial pm pi => GPartial (D1 d pm) (D1 d pi) where
gpfields o c m = M1 <$> gpfields @pm @pi o c m
gpnames o = gpnames @pm @pi o
goverlay (M1 a) (M1 b) = M1 (goverlay @pm @pi a b)
gempty = M1 (gempty @pm @pi)
gcomplete o p (M1 a) = M1 <$> gcomplete @pm @pi o p a
gtotal (M1 a) = M1 (gtotal @pm @pi a)
instance GPartial pm pi => GPartial (C1 c pm) (C1 c pi) where
gpfields o c m = M1 <$> gpfields @pm @pi o c m
gpnames o = gpnames @pm @pi o
goverlay (M1 a) (M1 b) = M1 (goverlay @pm @pi a b)
gempty = M1 (gempty @pm @pi)
gcomplete o p (M1 a) = M1 <$> gcomplete @pm @pi o p a
gtotal (M1 a) = M1 (gtotal @pm @pi a)
instance (GPartial am ai, GPartial bm bi) => GPartial (am :*: bm) (ai :*: bi) where
gpfields o c m = (:*:) <$> gpfields @am @ai o c m <*> gpfields @bm @bi o c m
gpnames o = gpnames @am @ai o ++ gpnames @bm @bi o
goverlay (a :*: b) (a' :*: b') = goverlay @am @ai a a' :*: goverlay @bm @bi b b'
gempty = gempty @am @ai :*: gempty @bm @bi
gcomplete o p (a :*: b) = fmap (:*:) (gcomplete @am @ai o p a) `both` gcomplete @bm @bi o p b
gtotal (a :*: b) = gtotal @am @ai a :*: gtotal @bm @bi b
instance GPartial U1 U1 where
gpfields _ _ _ = Success U1
gpnames _ = []
goverlay _ _ = U1
gempty = U1
gcomplete _ _ _ = Right U1
gtotal _ = U1
-- | A leaf field: @Field f a@.
instance (Selector s, FromConfig a) => GPartial (S1 s (Rec0 (Maybe a))) (S1 s (Rec0 a)) where
gpfields o c m =
let name = selKey @s o
in case Map.lookup name m of
Just v -> M1 . K1 . Just <$> runIn c {ctxPath = ctxPath c /. name} v
Nothing -> Success (M1 (K1 Nothing))
gpnames o = [selKey @s o]
goverlay (M1 (K1 a)) (M1 (K1 b)) = M1 (K1 (b <|> a))
gempty = M1 (K1 Nothing)
gcomplete o p (M1 (K1 m)) = case m <|> missingValue @a of
Just a -> Right (M1 (K1 a))
Nothing -> Left [p /. selKey @s o]
gtotal (M1 (K1 a)) = M1 (K1 (Just a))
-- | A nested partial record.
instance (Selector s, Partial b) => GPartial (S1 s (Rec0 (b Maybe))) (S1 s (Rec0 (b Identity))) where
gpfields o c m =
let name = Text.pack (fieldLabelModifier o (selName (SP :: SP s U1 ())))
in case Map.lookup name m of
Just v -> M1 . K1 <$> runDecoderIn (partialDecoder @b o) c {ctxPath = ctxPath c /. name} v
Nothing -> Success (M1 (K1 (emptyPartial @b)))
gpnames o = [Text.pack (fieldLabelModifier o (selName (SP :: SP s U1 ())))]
goverlay (M1 (K1 a)) (M1 (K1 b)) = M1 (K1 (overlay @b a b))
gempty = M1 (K1 (emptyPartial @b))
gcomplete o p (M1 (K1 m)) =
let here = p /. Text.pack (fieldLabelModifier o (selName (SP :: SP s U1 ())))
in case complete @b o m of
Right r -> Right (M1 (K1 r))
Left ps -> Left (map (here <>) (NE.toList ps))
gtotal (M1 (K1 a)) = M1 (K1 (total @b a))
instance
TypeError ('Text "figment: a Partial configuration must be a record with a single constructor")
=> GPartial (f :+: g) (f' :+: g')
where
gpfields _ _ _ = Failure (single (configError (Message "unreachable")))
gpnames _ = []
goverlay a _ = a
gempty = gempty @(f :+: g) @(f' :+: g')
gcomplete _ _ _ = Left []
gtotal _ = gempty @(f :+: g) @(f' :+: g')
-- helpers ----------------------------------------------------------------
data SP (s :: Meta) (f :: Type -> Type) a = SP
selKey :: forall (s :: Meta). Selector s => Options -> Text
selKey o = Text.pack (fieldLabelModifier o (selName (SP :: SP s U1 ())))
-- | Run 'fromConfig' at an explicit context.
runIn :: FromConfig a => DecodeContext -> Value -> Result a
runIn = decodeWith fromConfig
runDecoderIn :: Decoder a -> DecodeContext -> Value -> Result a
runDecoderIn = decodeWith
-- | Unknown-field check, accumulated together with the field results.
recordCheck :: Options -> [Text] -> DecodeContext -> Dict -> Result a -> Result a
recordCheck o known c m fields =
let unknown =
[ single (atValue c {ctxPath = ctxPath c /. key} x (UnknownField key (suggest key known)))
| rejectUnknownFields o, (key, x) <- Map.toList m, key `notElem` known ]
in case unknown of
[] -> fields
(e : es) -> Failure (foldl (<>) e es) <* fields
atValue :: DecodeContext -> Value -> ErrorKind -> ConfigError
atValue c x kind =
let t = valueTag x
in ConfigError kind (ctxPath c) (tagProfile t) (tagMeta t) (tagPos t) []