Add Yore.Config

This commit is contained in:
Paul Brinkmeier
2025-08-26 21:54:26 +02:00
parent c1e79230ba
commit c65d366611
3 changed files with 103 additions and 1 deletions
+92
View File
@@ -0,0 +1,92 @@
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DefaultSignatures #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE UndecidableInstances #-}
module Yore.Config (Config (..), load) where
import Control.Monad.IO.Class (liftIO)
import Control.Monad.Trans.Except (ExceptT (..), except, runExceptT)
import Data.Char (isLower, isUpper, toUpper)
import Data.Kind (Type)
import Data.Proxy (Proxy (..))
import Data.Typeable (Typeable, typeRep)
import GHC.Generics (C, D, Generic, K1 (..), M1 (..), Meta (..), Rec0, Rep, S, to, (:*:) (..))
import GHC.TypeLits (KnownSymbol, symbolVal)
import System.Environment (lookupEnv)
import Text.Printf (printf)
import Text.Read (readEither)
data Config = Config
{ port :: Maybe Int
, downloadDir :: FilePath
}
deriving (Generic, Show)
instance FromEnv Config where
prefix _ = "YORE_"
class FromEnv c where
prefix :: Proxy c -> String
prefix _ = ""
load :: IO (Either String c)
default load :: (Generic c, FromEnv' (Rep c)) => IO (Either String c)
load = runExceptT $ to <$> load' (prefix @c Proxy)
class FromEnv' (f :: Type -> Type) where
load' :: String -> ExceptT String IO (f p)
instance (FromEnv' f) => FromEnv' (M1 D c f) where
load' p = M1 <$> load' p
instance (FromEnv' f) => FromEnv' (M1 C c f) where
load' p = M1 <$> load' p
instance (FromEnv' f, FromEnv' g) => FromEnv' (f :*: g) where
load' p = (:*:) <$> load' p <*> load' p
instance (KnownSymbol nameSym, FromEnvVar t) => FromEnv' (M1 S ('MetaSel ('Just nameSym) nu ns dl) (Rec0 t)) where
load' p = M1 . K1 <$> fromEnvVar (p ++ toEnvName (symbolVal @nameSym Proxy))
class FromEnvVar t where
fromEnvVar :: String -> ExceptT String IO t
instance {-# OVERLAPPABLE #-} (ReadEnvVar t, Typeable t) => FromEnvVar t where
fromEnvVar key = do
value <-
ExceptT $
maybe (Left $ printf "env var %s is not set, expected %s" key (show $ typeRep $ Proxy @t)) Right <$> lookupEnv key
let contextualize =
printf "env var %s is malformed: could not read %s into %s: %s" key value (show $ typeRep $ Proxy @t)
except $ either (Left . contextualize) Right $ readEnvVar value
instance {-# OVERLAPPABLE #-} (ReadEnvVar t, Typeable t) => FromEnvVar (Maybe t) where
fromEnvVar key = do
mbValue <- liftIO $ lookupEnv key
case mbValue of
Nothing -> pure Nothing
Just value -> do
let contextualize =
printf
"optional env var %s is malformed (provide valid content or unset it): could not read %s into %s: %s"
key
value
(show $ typeRep $ Proxy @t)
except $ either (Left . contextualize) (Right . Just) $ readEnvVar value
class ReadEnvVar t where
readEnvVar :: String -> Either String t
instance ReadEnvVar Int where
readEnvVar = readEither
instance ReadEnvVar String where
readEnvVar = Right
toEnvName :: String -> String
toEnvName "" = ""
toEnvName (c0 : c1 : cs)
| isUpper c0 && isLower c1 = '_' : c0 : toEnvName (c1 : cs)
toEnvName (c0 : cs) = toUpper c0 : toEnvName cs