packages feed

seihou-core-0.4.0.0: src/Seihou/Effect/ConfigReaderInterp.hs

module Seihou.Effect.ConfigReaderInterp
  ( runConfigReader,
  )
where

import Data.Map.Strict qualified as Map
import Data.Text qualified as T
import Seihou.Core.Types (ConfigError (..))
import Seihou.Dhall.Config (evalConfigFileIfExists)
import Seihou.Effect.ConfigReader (ConfigReader (..))
import Seihou.Prelude
import System.Directory (XdgDirectory (..), getCurrentDirectory, getXdgDirectory)

-- | Real IO interpreter for the ConfigReader effect.
--
-- Resolves config file paths using standard locations:
--
--   * Global: @~\/.config\/seihou\/config.dhall@
--   * Local: @.seihou\/config.dhall@ (relative to current directory)
--   * Namespace: @~\/.config\/seihou\/namespaces\/\<ns\>\/config.dhall@
--   * Context: @~\/.config\/seihou\/contexts\/\<ctx\>\/config.dhall@
--
-- Missing files are silently treated as empty maps. Invalid Dhall
-- is reported as @Left (ConfigParseError ...)@.
runConfigReader :: (IOE :> es) => Eff (ConfigReader : es) a -> Eff es a
runConfigReader = interpret $ \_ -> \case
  ReadGlobalConfig -> liftIO $ do
    base <- getXdgDirectory XdgConfig "seihou"
    let path = base </> "config.dhall"
    first (ConfigParseError path) <$> evalConfigFileIfExists path
  ReadLocalConfig -> liftIO $ do
    cwd <- getCurrentDirectory
    let path = cwd </> ".seihou" </> "config.dhall"
    first (ConfigParseError path) <$> evalConfigFileIfExists path
  ReadNamespaceConfig ns -> liftIO $ loadNamespacedConfig "namespaces" ns "namespace must not contain '..' or '/'"
  ReadContextConfig ctx -> liftIO $ loadNamespacedConfig "contexts" ctx "context name must not contain '..' or '/'"

-- | Load config from a named subdirectory under XDG config, with path-traversal validation.
loadNamespacedConfig :: String -> Text -> Text -> IO (Either ConfigError (Map Text Text))
loadNamespacedConfig subdir name errMsg
  | T.null name = pure (Right Map.empty)
  | hasPathTraversal name = pure (Left (InvalidNamespace name errMsg))
  | otherwise = do
      base <- getXdgDirectory XdgConfig "seihou"
      let path = base </> subdir </> T.unpack name </> "config.dhall"
      first (ConfigParseError path) <$> evalConfigFileIfExists path
  where
    hasPathTraversal t = ".." `T.isInfixOf` t || "/" `T.isInfixOf` t