diff options
author | stuebinm | 2024-06-07 22:36:40 +0200 |
---|---|---|
committer | stuebinm | 2024-06-07 22:36:40 +0200 |
commit | c1cf159fa67a107a395166c199c401aac0918c31 (patch) | |
tree | 968941b8bd717df0f17f24595be9327267ec3042 | |
parent | 17631c7294ee21a48f78ce4e51c827da93b501fa (diff) |
the Config interface should be Applicative, not Monad
this allows for safer alternative uses for it, such as running it on an
empty list of config sources to just get the list of keys it uses.
-rw-r--r-- | src/Conftrack.hs | 123 | ||||
-rw-r--r-- | test/Main.hs | 19 |
2 files changed, 94 insertions, 48 deletions
diff --git a/src/Conftrack.hs b/src/Conftrack.hs index 0d3ffdd..b15fa02 100644 --- a/src/Conftrack.hs +++ b/src/Conftrack.hs @@ -6,6 +6,7 @@ {-# LANGUAGE TypeFamilies #-} {-# LANGUAGE AllowAmbiguousTypes #-} {-# LANGUAGE LambdaCase #-} +{-# LANGUAGE RecordWildCards #-} module Conftrack ( Config(..) @@ -19,78 +20,112 @@ module Conftrack , ConfigError(..) , Key(..) , Value(..) + , configKeysOf ) where import Conftrack.Value (ConfigError(..), ConfigValue(..), Key (..), Origin(..), Value(..), KeyPart, prefixedWith) import Conftrack.Source (SomeSource (..), ConfigSource (..)) import Prelude hiding (unzip) -import Control.Monad.State (get, StateT (..), MonadState (..), modify) +import Control.Monad.State (StateT (..)) import Data.Functor ((<&>)) -import Control.Monad.Reader (MonadIO (liftIO)) import Data.List.NonEmpty (NonEmpty, unzip) import qualified Data.List.NonEmpty as NonEmpty -import Control.Monad (forM, foldM) +import Control.Monad (forM, (>=>)) import Data.Either (isRight) -import Control.Monad.Trans.Except (ExceptT(..), runExceptT, throwE) -import Control.Monad.Trans (lift) import Data.Text (Text) import qualified Data.Text as T -import Data.Maybe (isJust) +import Data.Maybe (isJust, mapMaybe) class Config a where - readConfig :: FetchMonad a + readConfig :: Fetch a -type FetchMonad = StateT (NonEmpty SomeSource, [KeyPart], [Origin], [Warning]) (ExceptT [ConfigError] IO) +data FetcherState = FetcherState + { fetcherSources :: [SomeSource] + , fetcherPrefix :: [KeyPart] + , fetcherOrigins :: [Origin] + , fetcherWarnings :: [Warning] + , fetcherErrors :: [ConfigError] + } + +newtype Fetch a = Fetch (FetcherState -> IO (a, FetcherState)) + deriving (Functor) newtype Warning = Warning Text deriving Show -runFetchConfig :: Config a => NonEmpty SomeSource -> IO (Either [ConfigError] (a, [Origin], [Warning])) +instance Applicative Fetch where + pure a = Fetch (\s -> pure (a, s)) + + liftA2 f (Fetch m) (Fetch n) = Fetch $ \s -> do + (a, s2) <- m s + (b, s3) <- n s2 + pure (f a b, s3) + + +runFetchConfig :: forall a. Config a => NonEmpty SomeSource -> IO (Either [ConfigError] (a, [Origin], [Warning])) runFetchConfig sources = do - results <- runExceptT $ runStateT readConfig (sources, [], [], []) - case results of - Left a -> pure $ Left a - Right (result, (sources, _prefix, origins, warnings)) -> do - unusedWarnings <- collectUnused sources - pure $ Right (result, origins, unusedWarnings <> warnings) + let (Fetch m) = readConfig @a + + (result, FetcherState sources2 _ origins warnings errors) <- m (FetcherState (NonEmpty.toList sources) [] [] [] []) + unusedWarnings <- collectUnused sources2 + if null errors + then pure $ Right (result, origins, unusedWarnings <> warnings) + else pure $ Left errors + +configKeysOf :: forall a. Config a => IO [Key] +configKeysOf = do + let (Fetch m) = readConfig @a + (_, FetcherState _ _ origins warnings errors) <- m (FetcherState [] [] [] [] []) -readOptionalValue :: ConfigValue a => Key -> FetchMonad (Maybe a) -readOptionalValue bareKey = do - (sources, prefix, origins, warnings) <- get + let keys = mapMaybe (\case {(NotPresent key) -> Just key; _ -> Nothing }) errors + pure keys - let key = bareKey `prefixedWith` prefix - stuff <- liftIO $ firstMatchInSources key (NonEmpty.toList sources) <&> NonEmpty.fromList - let (maybeValues, states) = unzip stuff +readOptionalValue :: ConfigValue a => Key -> Fetch (Maybe a) +readOptionalValue bareKey = Fetch $ \s1@FetcherState{..} -> do + + let key = bareKey `prefixedWith` fetcherPrefix + + stuff <- firstMatchInSources key fetcherSources + + let (maybeValues, sources) = unzip stuff let values = maybeValues <&> \case Right (val, text) -> fromConfig val <&> (\val -> (val, Origin key text)) Left e -> Left e - val <- case fmap (\(Right a) -> a) $ NonEmpty.filter isRight values of + val <- case fmap (\(Right a) -> a) $ filter isRight values of [] -> pure (Nothing, Origin key "default value") (value, origin):_ -> pure (Just value, origin) - put (states, prefix, snd val : origins, warnings) - pure (fst val) + pure (fst val, s1 { fetcherSources = sources, fetcherOrigins = snd val : fetcherOrigins }) -readRequiredValue :: ConfigValue a => Key -> FetchMonad a -readRequiredValue key = - readOptionalValue key >>= \case - Nothing -> lift $ throwE [NotPresent key] - Just a -> pure a -readValue :: ConfigValue a => a -> Key -> FetchMonad a +readRequiredValue :: ConfigValue a => Key -> Fetch a +readRequiredValue key = + let + Fetch m = readOptionalValue key + in + Fetch (m >=> (\(a, s) -> case a of + Nothing -> + let + dummy = error "A nonexisting config value was evaluated. This is a bug." + in + pure (dummy, s { fetcherErrors = NotPresent (key `prefixedWith` fetcherPrefix s) : fetcherErrors s }) + Just v -> pure (v, s))) + +readValue :: forall a. ConfigValue a => a -> Key -> Fetch a readValue defaultValue key = - readOptionalValue key >>= \case - Just a -> pure a - Nothing -> do - modify (\(states, prefix, origins, warnings) -> - (states, prefix, Origin key "default value" : origins, warnings)) - pure defaultValue + let + Fetch m = readOptionalValue @a key + in + Fetch (m >=> (\(a, s) -> case a of + Just val -> pure (val, s) + Nothing -> do + pure (defaultValue, s { fetcherOrigins = Origin (key `prefixedWith` fetcherPrefix s) "default value" : fetcherOrigins s }))) firstMatchInSources :: Key -> [SomeSource] -> IO [(Either ConfigError (Value, Text), SomeSource)] firstMatchInSources _ [] = pure [] @@ -104,19 +139,17 @@ firstMatchInSources key (SomeSource (source, sourceState):sources) = do Right _ -> pure $ (eitherValue, SomeSource (source, newState)) : fmap (Left Shadowed ,) sources +readNested :: forall a. Config a => Key -> Fetch a +readNested (Key prefix') = Fetch $ \s1 -> do + let (Fetch nested) = readConfig @a + (config, s2) <- nested (s1 { fetcherPrefix = fetcherPrefix s1 <> NonEmpty.toList prefix' }) + pure (config, s2 { fetcherPrefix = fetcherPrefix s1 }) -readNested :: forall a. Config a => Key -> FetchMonad a -readNested (Key prefix') = do - prefix <- state (\(sources, prefix, origins, warnings) -> - (prefix, (sources, prefix <> NonEmpty.toList prefix', origins, warnings))) - config <- readConfig - modify (\(sources, _, origins, warnings) -> (sources, prefix, origins, warnings)) - pure config -collectUnused :: NonEmpty SomeSource -> IO [Warning] +collectUnused :: [SomeSource] -> IO [Warning] collectUnused sources = do forM sources (\(SomeSource (source, sourceState)) -> runStateT (leftovers source) sourceState <&> fst) <&> fmap (\(Just a) -> Warning $ "Unused Keys " <> T.pack (show a)) . filter (\(Just a) -> not (null a)) - . NonEmpty.filter isJust + . filter isJust diff --git a/test/Main.hs b/test/Main.hs index 5c01f64..1af3d9a 100644 --- a/test/Main.hs +++ b/test/Main.hs @@ -2,6 +2,7 @@ {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE DeriveAnyClass #-} {-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE ApplicativeDo #-} module Main (main) where import Conftrack @@ -15,6 +16,7 @@ import Test.QuickCheck.Monadic import Test.QuickCheck.Instances () import System.Exit (exitFailure, exitSuccess) import qualified Data.Text.Encoding as BS +import Data.List ((\\)) data TestFlat = TestType { testFoo :: Text, testBar :: Integer } @@ -35,9 +37,10 @@ instance Config TestFlat where <*> readRequiredValue (Key ["bar"]) instance Config TestNested where - readConfig = TestNested - <$> readRequiredValue (Key ["foo"]) - <*> readNested (Key ["nested"]) + readConfig = do + a <- readRequiredValue (Key ["foo"]) + b <- readNested (Key ["nested"]) + pure (TestNested a b) testTypeToTrivial :: TestFlat -> SomeSource testTypeToTrivial (TestType foo bar) = mkTrivialSource @@ -81,6 +84,16 @@ prop_aeson_flat = roundtripVia testTypeToJson prop_aeson_nested :: TestNested -> Property prop_aeson_nested = roundtripVia nestedToJson +prop_flat_keys :: Property +prop_flat_keys = monadicIO $ do + keys <- run $ configKeysOf @TestFlat + assert (null (keys \\ [ Key ["foo"], Key ["bar"] ])) + +prop_nested_keys :: Property +prop_nested_keys = monadicIO $ do + keys <- run $ configKeysOf @TestNested + assert (null (keys \\ [ Key ["foo"], Key ["nested", "bar"], Key ["nested", "foo"] ])) + -- see quickcheck docs for why this return is here return [] runTests = $quickCheckAll |