feat: add Config module for YAML config loading
This commit is contained in:
@@ -46,6 +46,7 @@ dependencies:
|
|||||||
- time
|
- time
|
||||||
- wai
|
- wai
|
||||||
- warp
|
- warp
|
||||||
|
- yaml
|
||||||
|
|
||||||
library:
|
library:
|
||||||
source-dirs: src
|
source-dirs: src
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ library
|
|||||||
exposed-modules:
|
exposed-modules:
|
||||||
Data.CookLang
|
Data.CookLang
|
||||||
Roux
|
Roux
|
||||||
|
Roux.Config
|
||||||
Roux.CooklangPrint
|
Roux.CooklangPrint
|
||||||
Roux.Html
|
Roux.Html
|
||||||
Roux.NYTimes
|
Roux.NYTimes
|
||||||
@@ -58,6 +59,7 @@ library
|
|||||||
, time
|
, time
|
||||||
, wai
|
, wai
|
||||||
, warp
|
, warp
|
||||||
|
, yaml
|
||||||
default-language: Haskell2010
|
default-language: Haskell2010
|
||||||
|
|
||||||
executable check-recipes
|
executable check-recipes
|
||||||
@@ -95,6 +97,7 @@ executable check-recipes
|
|||||||
, time
|
, time
|
||||||
, wai
|
, wai
|
||||||
, warp
|
, warp
|
||||||
|
, yaml
|
||||||
default-language: Haskell2010
|
default-language: Haskell2010
|
||||||
|
|
||||||
executable roux-server
|
executable roux-server
|
||||||
@@ -133,6 +136,7 @@ executable roux-server
|
|||||||
, unix
|
, unix
|
||||||
, wai
|
, wai
|
||||||
, warp
|
, warp
|
||||||
|
, yaml
|
||||||
default-language: Haskell2010
|
default-language: Haskell2010
|
||||||
|
|
||||||
test-suite roux-server-test
|
test-suite roux-server-test
|
||||||
@@ -179,4 +183,5 @@ test-suite roux-server-test
|
|||||||
, time
|
, time
|
||||||
, wai
|
, wai
|
||||||
, warp
|
, warp
|
||||||
|
, yaml
|
||||||
default-language: Haskell2010
|
default-language: Haskell2010
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
{-# LANGUAGE DataKinds #-}
|
||||||
|
{-# LANGUAGE OverloadedStrings #-}
|
||||||
|
|
||||||
|
{- | Configuration for Roux, loaded from a YAML config file.
|
||||||
|
|
||||||
|
Default location: ./roux-config.yaml in the working directory.
|
||||||
|
Overridable via --config-file CLI flag.
|
||||||
|
-}
|
||||||
|
module Roux.Config (
|
||||||
|
RouxConfig (..),
|
||||||
|
AnthropicConfig (..),
|
||||||
|
loadConfig,
|
||||||
|
) where
|
||||||
|
|
||||||
|
import Data.Text (Text)
|
||||||
|
import qualified Data.Yaml as Y
|
||||||
|
import qualified Fleece.Aeson as Fleece
|
||||||
|
import qualified Fleece.Aeson.Decoder as FleeceD
|
||||||
|
import Fleece.Core (constructor, object, optionalNullable, required, text, (#+))
|
||||||
|
import qualified Fleece.Core as FC
|
||||||
|
import System.Directory (doesFileExist)
|
||||||
|
|
||||||
|
-- | Top-level config.
|
||||||
|
data RouxConfig = RouxConfig
|
||||||
|
{ rcAnthropic :: Maybe AnthropicConfig
|
||||||
|
}
|
||||||
|
deriving stock (Eq, Show)
|
||||||
|
|
||||||
|
-- | Anthropic API configuration.
|
||||||
|
data AnthropicConfig = AnthropicConfig
|
||||||
|
{ acApiKey :: !Text
|
||||||
|
, acBaseUrl :: !(Maybe Text) -- default: https://api.anthropic.com
|
||||||
|
, acModel :: !(Maybe Text) -- default: claude-sonnet-4-20250514
|
||||||
|
}
|
||||||
|
deriving stock (Eq, Show)
|
||||||
|
|
||||||
|
-- | Fleece schema for AnthropicConfig.
|
||||||
|
anthropicConfigSchema :: FC.Schema Fleece.Decoder AnthropicConfig
|
||||||
|
anthropicConfigSchema =
|
||||||
|
object $
|
||||||
|
constructor AnthropicConfig
|
||||||
|
#+ required "api_key" acApiKey text
|
||||||
|
#+ optionalNullable FC.EmitNull "base_url" acBaseUrl text
|
||||||
|
#+ optionalNullable FC.EmitNull "model" acModel text
|
||||||
|
|
||||||
|
-- | Fleece schema for RouxConfig.
|
||||||
|
rouxConfigSchema :: FC.Schema Fleece.Decoder RouxConfig
|
||||||
|
rouxConfigSchema =
|
||||||
|
object $
|
||||||
|
constructor RouxConfig
|
||||||
|
#+ optionalNullable FC.EmitNull "anthropic" rcAnthropic anthropicConfigSchema
|
||||||
|
|
||||||
|
{- | Load config from a YAML file. Returns config with nothing set if the file
|
||||||
|
doesn't exist (LLM features will be unavailable but the server still runs).
|
||||||
|
Exits with an error if the file exists but is malformed.
|
||||||
|
-}
|
||||||
|
loadConfig :: FilePath -> IO RouxConfig
|
||||||
|
loadConfig path = do
|
||||||
|
exists <- doesFileExist path
|
||||||
|
if not exists
|
||||||
|
then pure (RouxConfig Nothing)
|
||||||
|
else do
|
||||||
|
result <- Y.decodeFileEither path
|
||||||
|
case result of
|
||||||
|
Left parseErr ->
|
||||||
|
fail $ "Failed to parse " <> path <> ": " <> show parseErr
|
||||||
|
Right val ->
|
||||||
|
case FleeceD.fromValue rouxConfigSchema val of
|
||||||
|
Right cfg -> pure cfg
|
||||||
|
Left decodeErr ->
|
||||||
|
fail $
|
||||||
|
"Invalid config format in " <> path <> ": " <> show decodeErr
|
||||||
Reference in New Issue
Block a user