Skip to content

Commit d9668dc

Browse files
committed
Refactor Config to use generic HKD utilities
Extract generic Higher-Kinded Data (HKD) functionality into a new TerranixCodegen.Config.Generic module that can be reused across the codebase. Key changes: - Add TerranixCodegen.Config.Generic module with generic HKD operations - Implement completeHKD for validation and Last -> Identity conversion - Implement readEnvHKD for reading environment variables generically - Use casing library for camelCase to SCREAMING_SNAKE_CASE conversion - Refactor Config module to use generic utilities (simpler implementation) - Update CLI.Commands to use ValidationError instead of ConfigError - Update tests to match new API (completeConfig takes list, new error type) Benefits: - Generic completion works for any HKD type with Generic instance - Generic environment reading automatically handles field name conversion - Reduced boilerplate in Config module - Type-safe with compile-time validation via type-level strings - All 158 tests passing The generic module uses GHC.Generics to traverse HKD structures and provides reusable functionality for any future HKD configuration types.
1 parent ad31fea commit d9668dc

13 files changed

Lines changed: 813 additions & 21 deletions

File tree

app/CLI/Commands.hs

Lines changed: 95 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,51 +4,127 @@ module CLI.Commands (
44
where
55

66
import CLI.Types
7-
import Control.Monad.Except (runExceptT)
87
import Control.Monad.Reader (runReaderT)
8+
import Control.Monad.Validate (runValidate)
99
import Data.Aeson (encode)
1010
import Data.Aeson.Encode.Pretty (encodePretty)
1111
import Data.ByteString.Lazy qualified as BL
12+
import Data.List.NonEmpty qualified as NE
1213
import Data.Maybe (fromMaybe)
14+
import Data.Monoid (Last (..))
1315
import Data.Text qualified as T
1416
import Prettyprinter (Doc, annotate, hardline, pipe, pretty, vcat, vsep, (<+>))
1517
import Prettyprinter.Render.Terminal (AnsiStyle, Color (..), bold, color, hPutDoc, putDoc)
1618
import System.Exit (exitFailure)
1719
import System.IO (hPutStrLn, stderr)
20+
import TerranixCodegen.Config (
21+
Config,
22+
PartialConfig,
23+
ValidationError (..),
24+
completeConfig,
25+
defaultConfig,
26+
emptyConfig,
27+
getOutputDirectory,
28+
mergeConfigs,
29+
readConfigFile,
30+
readEnvConfig,
31+
pattern PartialConfig,
32+
)
1833
import TerranixCodegen.FileOrganizer
1934
import TerranixCodegen.PrettyPrint
2035
import TerranixCodegen.ProviderSchema
2136
import TerranixCodegen.ProviderSpec
22-
import TerranixCodegen.TerraformGenerator (GeneratorConfig (..), TerraformError (..), defaultGeneratorConfig, extractSchemaFromProviders)
37+
import TerranixCodegen.TerraformGenerator (TerraformError (..), extractSchemaFromProviders, generatorConfigFromComplete)
38+
39+
{- | Load and merge configuration from multiple sources
40+
41+
Precedence (highest to lowest):
42+
1. CLI arguments
43+
2. Environment variables
44+
3. Config file
45+
4. Defaults
46+
-}
47+
loadCompleteConfig ::
48+
Maybe FilePath -> -- Config file path
49+
Maybe FilePath -> -- CLI terraform executable override
50+
Maybe FilePath -> -- CLI output directory override
51+
IO Config
52+
loadCompleteConfig configFilePath cliTfExe cliOutput = do
53+
-- Load config file if specified
54+
fileConfig <- case configFilePath of
55+
Nothing -> pure emptyConfig
56+
Just path -> do
57+
result <- readConfigFile path
58+
case result of
59+
Left err -> do
60+
hPutDoc stderr $
61+
vsep
62+
[ annotate (color Red <> bold) "Error:" <+> "Failed to read config file"
63+
, hardline
64+
, pretty err
65+
, mempty
66+
]
67+
exitFailure
68+
Right config -> pure config
69+
70+
-- Load environment variables
71+
envConfig <- readEnvConfig
72+
73+
-- Build CLI config from arguments
74+
let cliConfig = PartialConfig cliTfExe cliOutput
75+
76+
-- Merge all configs with proper precedence
77+
-- Later configs override earlier ones, with defaults providing base values
78+
case runValidate (completeConfig [defaultConfig, fileConfig, envConfig, cliConfig]) of
79+
Left errors -> do
80+
hPutDoc stderr $
81+
vsep
82+
[ annotate (color Red <> bold) "Error:" <+> "Missing required configuration fields"
83+
, hardline
84+
, vcat $ NE.toList $ fmap formatConfigError errors
85+
, hardline
86+
, "This should not happen if default config is properly merged."
87+
, mempty
88+
]
89+
exitFailure
90+
Right cfg -> pure cfg
91+
where
92+
formatConfigError :: ValidationError -> Doc AnsiStyle
93+
formatConfigError (MissingField _typeName field) =
94+
"" <+> annotate (color Yellow) (pretty field) <+> "is required but was not provided"
2395

2496
-- | Execute a command
2597
runCommand :: Command -> IO ()
2698
runCommand cmd = case cmd of
27-
Generate input output printSchema tfExe -> do
28-
schemas <- loadSchemas tfExe input
99+
Generate input output printSchema tfExe configFile -> do
100+
config <- loadCompleteConfig configFile tfExe (Just output)
101+
schemas <- loadSchemasWithConfig config input
29102
if printSchema
30103
then do
31104
putDoc $ prettyProviderSchemas schemas
32105
hPutStrLn stderr "Done"
33106
else do
34-
hPutStrLn stderr $ "Generating modules to: " <> output
35-
organizeFiles output schemas
107+
let outDir = getOutputDirectory config
108+
hPutStrLn stderr $ "Generating modules to: " <> outDir
109+
organizeFiles outDir schemas
36110
hPutStrLn stderr "✓ Module generation complete!"
37-
Show input tfExe -> do
38-
schemas <- loadSchemas tfExe input
111+
Show input tfExe configFile -> do
112+
config <- loadCompleteConfig configFile tfExe Nothing
113+
schemas <- loadSchemasWithConfig config input
39114
putDoc $ prettyProviderSchemas schemas
40-
ExtractSchema input prettyJson tfExe -> do
41-
schemas <- loadSchemas tfExe input
115+
ExtractSchema input prettyJson tfExe configFile -> do
116+
config <- loadCompleteConfig configFile tfExe Nothing
117+
schemas <- loadSchemasWithConfig config input
42118
let jsonOutput
43119
| prettyJson = encodePretty schemas
44120
| otherwise = encode schemas
45121
BL.putStr jsonOutput
46122

47-
-- | Load schemas from various input sources
48-
loadSchemas :: Maybe FilePath -> SchemaInput -> IO ProviderSchemas
49-
loadSchemas tfExe input = case input of
123+
-- | Load schemas from various input sources using complete configuration
124+
loadSchemasWithConfig :: Config -> SchemaInput -> IO ProviderSchemas
125+
loadSchemasWithConfig config input = case input of
50126
FromFile maybePath -> loadFromFile maybePath
51-
FromProviderSpecs specs -> loadFromProviderSpecs tfExe specs
127+
FromProviderSpecs specs -> loadFromProviderSpecs config specs
52128
FromProvidersFile path -> loadFromProvidersFile path
53129

54130
-- | Load schemas from a file or stdin
@@ -74,8 +150,8 @@ loadFromFile maybePath = do
74150
pure schemas
75151

76152
-- | Load schemas by generating minimal Terraform from provider specs
77-
loadFromProviderSpecs :: Maybe FilePath -> [T.Text] -> IO ProviderSchemas
78-
loadFromProviderSpecs tfExe specTexts = do
153+
loadFromProviderSpecs :: Config -> [T.Text] -> IO ProviderSchemas
154+
loadFromProviderSpecs completeConfig specTexts = do
79155
-- Parse provider specifications
80156
specs <- case mapM parseProviderSpecText specTexts of
81157
Left err -> do
@@ -97,11 +173,9 @@ loadFromProviderSpecs tfExe specTexts = do
97173
, mempty
98174
]
99175

100-
-- Extract schemas using Terraform with custom executable if provided
101-
let config = case tfExe of
102-
Nothing -> defaultGeneratorConfig
103-
Just exe -> defaultGeneratorConfig {terraformExecutable = exe}
104-
result <- runExceptT $ runReaderT (extractSchemaFromProviders specs) config
176+
-- Extract schemas using Terraform with the complete config
177+
let genConfig = generatorConfigFromComplete completeConfig
178+
result <- runExceptT $ runReaderT (extractSchemaFromProviders specs) genConfig
105179
case result of
106180
Left err -> do
107181
hPutDoc stderr $ formatTerraformError err

app/CLI/Parser.hs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,18 @@ import Options.Applicative
1010
import Paths_terranix_codegen (version)
1111
import Prettyprinter
1212

13+
-- | Parse config file option
14+
configFileParser :: Parser (Maybe FilePath)
15+
configFileParser =
16+
optional
17+
( strOption
18+
( long "config"
19+
<> short 'c'
20+
<> metavar "FILE"
21+
<> help "Configuration file path (JSON format)"
22+
)
23+
)
24+
1325
-- | Parse terraform executable option
1426
terraformExecutableParser :: Parser (Maybe FilePath)
1527
terraformExecutableParser =
@@ -76,13 +88,15 @@ generateCommand =
7688
<> help "Pretty-print the schema instead of generating modules"
7789
)
7890
<*> terraformExecutableParser
91+
<*> configFileParser
7992

8093
-- | Parser for the 'show' subcommand
8194
showCommand :: Parser Command
8295
showCommand =
8396
Show
8497
<$> schemaInputParser
8598
<*> terraformExecutableParser
99+
<*> configFileParser
86100

87101
-- | Parser for the 'schema' subcommand
88102
schemaCommand :: Parser Command
@@ -95,6 +109,7 @@ schemaCommand =
95109
<> help "Pretty-print JSON output (default: compact)"
96110
)
97111
<*> terraformExecutableParser
112+
<*> configFileParser
98113

99114
-- | Parser for all commands
100115
commandParser :: Parser Command

app/CLI/Types.hs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,17 @@ data Command
2323
, cmdOutput :: FilePath
2424
, cmdPrintSchema :: Bool
2525
, cmdTerraformExecutable :: Maybe FilePath
26+
, cmdConfigFile :: Maybe FilePath
2627
}
2728
| Show
2829
{ cmdSchemaInput :: SchemaInput
2930
, cmdTerraformExecutable :: Maybe FilePath
31+
, cmdConfigFile :: Maybe FilePath
3032
}
3133
| ExtractSchema
3234
{ cmdSchemaInput :: SchemaInput
3335
, cmdPrettyJson :: Bool
3436
, cmdTerraformExecutable :: Maybe FilePath
37+
, cmdConfigFile :: Maybe FilePath
3538
}
3639
deriving (Show)

examples/README.md

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
# Configuration Examples
2+
3+
This directory contains example configuration files for terranix-codegen.
4+
5+
## Configuration File Format
6+
7+
The configuration file uses JSON format and supports the following options:
8+
9+
### `terranix-codegen.json`
10+
11+
```json
12+
{
13+
"terraformExecutable": "tofu",
14+
"outputDirectory": "./terranix-modules"
15+
}
16+
```
17+
18+
## Configuration Options
19+
20+
### `terraformExecutable` (optional)
21+
22+
- **Type**: String (file path or command name)
23+
- **Default**: `"tofu"`
24+
- **Description**: The Terraform or OpenTofu executable to use for schema extraction.
25+
26+
**Examples**:
27+
28+
- `"tofu"` - Use OpenTofu (default)
29+
- `"terraform"` - Use Terraform
30+
- `"/usr/local/bin/terraform"` - Use specific path
31+
32+
### `outputDirectory` (optional)
33+
34+
- **Type**: String (directory path)
35+
- **Default**: `"./terranix-modules"`
36+
- **Description**: Directory where generated Nix modules will be written.
37+
38+
**Examples**:
39+
40+
- `"./terranix-modules"` - Relative to current directory (default)
41+
- `"/absolute/path/modules"` - Absolute path
42+
- `"./providers"` - Custom relative directory
43+
44+
## Configuration Precedence
45+
46+
Configuration values are merged from multiple sources with the following precedence (highest to lowest):
47+
48+
1. **CLI flags** - Command-line arguments (e.g., `--terraform-executable terraform`)
49+
1. **Environment variables**:
50+
- `TERRANIX_CODEGEN_TERRAFORM_EXECUTABLE`
51+
- `TERRANIX_CODEGEN_OUTPUT_DIRECTORY`
52+
1. **Config file** - Specified with `--config` flag
53+
1. **Defaults** - Built-in defaults
54+
55+
The configuration system uses higher-kinded data (HKD) with the `Last` monoid to merge
56+
configurations. Later values in the precedence chain override earlier ones. This is
57+
implemented using quantified constraints to allow generic merging of configuration values.
58+
59+
## Usage Examples
60+
61+
### Using a config file
62+
63+
```bash
64+
# Specify config file with --config flag
65+
terranix-codegen generate -p aws --config terranix-codegen.json
66+
67+
# Short form
68+
terranix-codegen generate -p aws -c terranix-codegen.json
69+
```
70+
71+
### Overriding config file values
72+
73+
```bash
74+
# Config file sets terraformExecutable to "tofu", but CLI overrides it
75+
terranix-codegen generate -p aws -c config.json -t terraform
76+
```
77+
78+
### Using environment variables
79+
80+
```bash
81+
# Set environment variables
82+
export TERRANIX_CODEGEN_TERRAFORM_EXECUTABLE=terraform
83+
export TERRANIX_CODEGEN_OUTPUT_DIRECTORY=/tmp/modules
84+
85+
# Run without config file - uses env vars
86+
terranix-codegen generate -p aws
87+
```
88+
89+
### Combining all sources
90+
91+
```bash
92+
# Environment variable
93+
export TERRANIX_CODEGEN_TERRAFORM_EXECUTABLE=tofu
94+
95+
# Config file has: {"outputDirectory": "./modules"}
96+
# CLI overrides output directory
97+
terranix-codegen generate -p aws -c config.json -o /custom/output
98+
99+
# Result: uses tofu (env), /custom/output (CLI)
100+
```
101+
102+
## Minimal Configurations
103+
104+
### Just override terraform executable
105+
106+
```json
107+
{
108+
"terraformExecutable": "terraform"
109+
}
110+
```
111+
112+
### Just override output directory
113+
114+
```json
115+
{
116+
"outputDirectory": "/var/lib/terranix-modules"
117+
}
118+
```
119+
120+
### Empty config (use all defaults)
121+
122+
```json
123+
{}
124+
```

examples/terranix-codegen.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"terraformExecutable": "tofu",
3+
"outputDirectory": "./terranix-modules"
4+
}

0 commit comments

Comments
 (0)