settingspec 0.6.0__tar.gz → 0.8.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. {settingspec-0.6.0 → settingspec-0.8.0}/Cargo.lock +1 -1
  2. {settingspec-0.6.0 → settingspec-0.8.0}/Cargo.toml +1 -1
  3. {settingspec-0.6.0 → settingspec-0.8.0}/PKG-INFO +52 -28
  4. {settingspec-0.6.0 → settingspec-0.8.0}/README.md +51 -27
  5. {settingspec-0.6.0 → settingspec-0.8.0}/src/cli.rs +30 -8
  6. {settingspec-0.6.0 → settingspec-0.8.0}/src/error.rs +20 -13
  7. settingspec-0.8.0/src/generator/c.rs +211 -0
  8. settingspec-0.8.0/src/generator/cpp.rs +219 -0
  9. settingspec-0.8.0/src/generator/elm.rs +125 -0
  10. settingspec-0.8.0/src/generator/go.rs +205 -0
  11. settingspec-0.8.0/src/generator/haskell.rs +250 -0
  12. settingspec-0.8.0/src/generator/java.rs +203 -0
  13. {settingspec-0.6.0 → settingspec-0.8.0}/src/generator/mod.rs +33 -0
  14. settingspec-0.8.0/src/generator/ruby.rs +134 -0
  15. settingspec-0.8.0/src/generator/rust.rs +178 -0
  16. settingspec-0.8.0/src/generator/scala.rs +177 -0
  17. settingspec-0.8.0/src/generator/terraform.rs +120 -0
  18. settingspec-0.8.0/src/generator/zig.rs +167 -0
  19. {settingspec-0.6.0 → settingspec-0.8.0}/src/model.rs +11 -7
  20. settingspec-0.8.0/src/parser.rs +421 -0
  21. {settingspec-0.6.0 → settingspec-0.8.0}/src/profile.rs +2 -2
  22. {settingspec-0.6.0 → settingspec-0.8.0}/src/resolver.rs +1 -13
  23. settingspec-0.6.0/src/parser.rs +0 -341
  24. {settingspec-0.6.0 → settingspec-0.8.0}/LICENSE-APACHE +0 -0
  25. {settingspec-0.6.0 → settingspec-0.8.0}/LICENSE-MIT +0 -0
  26. {settingspec-0.6.0 → settingspec-0.8.0}/pyproject.toml +0 -0
  27. {settingspec-0.6.0 → settingspec-0.8.0}/src/envfile.rs +0 -0
  28. {settingspec-0.6.0 → settingspec-0.8.0}/src/filter.rs +0 -0
  29. {settingspec-0.6.0 → settingspec-0.8.0}/src/generator/common.rs +0 -0
  30. {settingspec-0.6.0 → settingspec-0.8.0}/src/generator/env.rs +0 -0
  31. {settingspec-0.6.0 → settingspec-0.8.0}/src/generator/javascript.rs +0 -0
  32. {settingspec-0.6.0 → settingspec-0.8.0}/src/generator/json.rs +0 -0
  33. {settingspec-0.6.0 → settingspec-0.8.0}/src/generator/lua.rs +0 -0
  34. {settingspec-0.6.0 → settingspec-0.8.0}/src/generator/python.rs +0 -0
  35. {settingspec-0.6.0 → settingspec-0.8.0}/src/generator/toml.rs +0 -0
  36. {settingspec-0.6.0 → settingspec-0.8.0}/src/generator/tree.rs +0 -0
  37. {settingspec-0.6.0 → settingspec-0.8.0}/src/generator/typescript.rs +0 -0
  38. {settingspec-0.6.0 → settingspec-0.8.0}/src/generator/yaml.rs +0 -0
  39. {settingspec-0.6.0 → settingspec-0.8.0}/src/lib.rs +0 -0
  40. {settingspec-0.6.0 → settingspec-0.8.0}/src/main.rs +0 -0
@@ -1823,7 +1823,7 @@ dependencies = [
1823
1823
 
1824
1824
  [[package]]
1825
1825
  name = "settingspec"
1826
- version = "0.6.0"
1826
+ version = "0.8.0"
1827
1827
  dependencies = [
1828
1828
  "age",
1829
1829
  "anyhow",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "settingspec"
3
- version = "0.6.0"
3
+ version = "0.8.0"
4
4
  edition = "2024"
5
5
  description = "One file for all environments, all languages, all submodules."
6
6
  readme = "README.md"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: settingspec
3
- Version: 0.6.0
3
+ Version: 0.8.0
4
4
  Classifier: Programming Language :: Rust
5
5
  Classifier: Programming Language :: Python :: Implementation :: CPython
6
6
  Classifier: Programming Language :: Python :: Implementation :: PyPy
@@ -30,7 +30,8 @@ Project-URL: Source Code, https://github.com/sayanarijit/settingspec
30
30
 
31
31
  # SettingSpec
32
32
 
33
- 1. [Concepts](#concepts)
33
+ 1. [Install](#install)
34
+ 2. [Concepts](#concepts)
34
35
  1. [Single file convenience](#single-file-convenience)
35
36
  2. [Declarative profiles as environments](#declarative-profiles-as-environments)
36
37
  3. [Language independent](#language-independent)
@@ -38,7 +39,29 @@ Project-URL: Source Code, https://github.com/sayanarijit/settingspec
38
39
  1. [SecretSpec](#secretspec)
39
40
  2. [Dotenv](#dotenv)
40
41
  3. [Cottage or agebox](#cottage-or-agebox)
41
- 2. [Learn more](#learn-more)
42
+ 3. [Learn more](#learn-more)
43
+
44
+ ## Install
45
+
46
+ ```bash
47
+ # rust: cargo-binstall/cargo
48
+ cargo binstall --locked settingspec
49
+ cargo install --locked settingspec
50
+
51
+ # python: pip/uv/uvx
52
+ pip install settingspec
53
+ uv pip install settingspec
54
+ uvx settingspec --version
55
+
56
+ # node: yarn/pnpm/npx
57
+ yarn global add @sayanarijit/settingspec
58
+ pnpm add -g @sayanarijit/settingspec
59
+ npx @sayanarijit/settingspec --version
60
+
61
+ # container: docker/podman
62
+ docker run --rm -v $PWD:/app ghcr.io/sayanarijit/settingspec --version
63
+ podman run --rm -v $PWD:/app ghcr.io/sayanarijit/settingspec --version
64
+ ```
42
65
 
43
66
  ## Concepts
44
67
 
@@ -58,30 +81,31 @@ profile.options = [ # Optional: Enables strict checking of per-
58
81
  ]
59
82
  profile.default = "dev" # Optional: Default profile when the switch is not set
60
83
 
61
- [settings] # Declare the settings here with syntax: `<key>.<profile>.<directive> = <value>`
62
- key1.default.val = "val1" # Default value for all profiles
63
- key1.prod.val = "prod1" # Override the default value when SETTINGSPEC_PROFILE=prod
84
+ [settings] # Declare the settings here with syntax: `<key>._.<profile>.<directive> = <value>`
85
+ key1._.default.val = "val1" # Default value for all profiles
86
+ key1._.prod.val = "prod1" # Override the default value when SETTINGSPEC_PROFILE=prod
64
87
 
65
- # Strictly define different values for different profiles
66
- key2.dev.val = "dev2" # When SETTINGSPEC_PROFILE=dev, key2=dev2
67
- key2.stage.val = "stage2"
68
- key2.prod.val = "prod2"
88
+ key2._ = { # Strictly define different values for different profiles
89
+ dev.val = "dev2",
90
+ stage.val = "stage2",
91
+ prod.val = "prod2",
92
+ }
69
93
  ```
70
94
 
71
95
  Overrides are intentional and kept in plain sight (see `key1`).
72
96
 
73
- By not defining a default value, you can ensure that you never miss adding a value for a specific profile (e.g. this file will refuse to load if `key2.prod` declaration is missing).
97
+ By not defining a default value, you can ensure that you never miss adding a value for a specific profile (e.g. this file will refuse to load if `key2._.prod` declaration is missing).
74
98
 
75
99
  ### Language independent
76
100
 
77
- The command-line tool `settingspec` can export the final settings into well-known formats such as `toml`, `json`, `yaml`, etc., or hard-coded modules such as `.py`, `.js`, `.lua`, etc., write them to disk, print, or pipe them via stdin or even export as environment variables, allowing you to use a single `settingspec.toml` without worrying about the target language.
101
+ The command-line tool `settingspec` can export the final settings into well-known formats such as `toml`, `json`, `yaml`, `tf`, etc., or hard-coded modules such as `.py`, `.js`, `.ts`, `.lua`, `.rs`, `.go`, `.zig`, `.c`, `.cpp`, `.java`, `.elm`, `.rb`, `.scala`, `.hs`, etc., write them to disk, print, or pipe them via stdin or even export as environment variables, allowing you to use a single `settingspec.toml` without worrying about the target language.
78
102
 
79
103
  ```toml
80
104
  [spec]
81
- export.file = true # Default: Export all settings into settings.toml
82
- # export.file = { # Optional: Fine-grained control over what to export
83
- # "settings.json" = true,
84
- # "settings.yaml" = {
105
+ export.file = true # Default: Export all settings into settings.toml
106
+ # export.file = { # Optional: Fine-grained control over what to export
107
+ # "settings.py" = true, # Get auto-completion of settings in your editor/IDE for free
108
+ # "settings.yaml" = { # Select which keys to export
85
109
  # key1 = true,
86
110
  # group1 = true,
87
111
  # group2.subgroup = true,
@@ -97,11 +121,11 @@ export.env = "PREFIX_" # Optional: Export as environment variables prefixed wit
97
121
  export.stdin = "toml" # Optional: Pass settings to program stdin in specified format
98
122
 
99
123
  [settings]
100
- key1.default.val = "val1"
101
- group1.key1.default.val = "group1val1"
102
- group2.subgroup.key1.default.val = "group2val1"
103
- group3.subgroup.key1.default.val = "group3val1"
104
- group3.subgroup.key1.default.tags = ["tag1", "tag2"]
124
+ key1._.default.val = "val1"
125
+ group1.key1._.default.val = "group1val1"
126
+ group2.subgroup.key1._.default.val = "group2val1"
127
+ group3.subgroup.key1._.default.val = "group3val1"
128
+ group3.subgroup.key1._.tags = ["tag1", "tag2"] # Syntax: `<key>._.tags = [...]`
105
129
  ```
106
130
 
107
131
  Now export the final settings as declared.
@@ -125,7 +149,7 @@ settingspec run -- [your program]...
125
149
  If no export option is specified, the default behavior is to export all settings into `settings.toml`.
126
150
 
127
151
  > [!NOTE]
128
- > If the target programming language supports null values, you can declare it as `key.default.null = true` in the settings section.
152
+ > If the target programming language supports null values, you can declare it as `key._.default.null = true` in the settings section.
129
153
 
130
154
  ### Easy secrets
131
155
 
@@ -156,12 +180,12 @@ settingspec.toml
156
180
  profile.options = ["dev", "stage", "prod"]
157
181
 
158
182
  [settings]
159
- secret1.default.env = "SECRET1" # Load value from $SECRET1
183
+ secret1._.default.env = "SECRET1" # Load value from $SECRET1
160
184
 
161
185
  # Override the default value from environment variables
162
- secret2.default.val = "defaultvalue" # Default value if $SECRET2 is not set
163
- secret2.default.env = "SECRET2" # Load value from $SECRET2 if set
164
- secret2.prod.env = "PRODSECRET" # Load value from $PRODSECRET if SETTINGSPEC_PROFILE=prod
186
+ secret2._.default.val = "defaultvalue" # Default value if $SECRET2 is not set
187
+ secret2._.default.env = "SECRET2" # Load value from $SECRET2 if set
188
+ secret2._.prod.env = "PRODSECRET" # Load value from $PRODSECRET if SETTINGSPEC_PROFILE=prod
165
189
  ```
166
190
 
167
191
  With these set up, run:
@@ -186,7 +210,7 @@ envfile.prod = "-" # Read from stdin, encrypted or not
186
210
  # decryption.key.path = "~/.ssh/" # Default
187
211
 
188
212
  [settings]
189
- secret1.default.env = "SECRET1" # Load value from $SECRET1
213
+ secret1._.default.env = "SECRET1" # Load value from $SECRET1
190
214
  ```
191
215
 
192
216
  #### Cottage or agebox
@@ -203,7 +227,7 @@ decryption.key.env = "COTTAGE_IDENTITY"
203
227
  # decryption.key.env = "AGEBOX_PRIVATE_KEYS"
204
228
 
205
229
  [settings]
206
- secret.default.env = "SECRET"
230
+ secret._.default.env = "SECRET"
207
231
  ```
208
232
 
209
233
  ## Learn more
@@ -13,7 +13,8 @@
13
13
 
14
14
  # SettingSpec
15
15
 
16
- 1. [Concepts](#concepts)
16
+ 1. [Install](#install)
17
+ 2. [Concepts](#concepts)
17
18
  1. [Single file convenience](#single-file-convenience)
18
19
  2. [Declarative profiles as environments](#declarative-profiles-as-environments)
19
20
  3. [Language independent](#language-independent)
@@ -21,7 +22,29 @@
21
22
  1. [SecretSpec](#secretspec)
22
23
  2. [Dotenv](#dotenv)
23
24
  3. [Cottage or agebox](#cottage-or-agebox)
24
- 2. [Learn more](#learn-more)
25
+ 3. [Learn more](#learn-more)
26
+
27
+ ## Install
28
+
29
+ ```bash
30
+ # rust: cargo-binstall/cargo
31
+ cargo binstall --locked settingspec
32
+ cargo install --locked settingspec
33
+
34
+ # python: pip/uv/uvx
35
+ pip install settingspec
36
+ uv pip install settingspec
37
+ uvx settingspec --version
38
+
39
+ # node: yarn/pnpm/npx
40
+ yarn global add @sayanarijit/settingspec
41
+ pnpm add -g @sayanarijit/settingspec
42
+ npx @sayanarijit/settingspec --version
43
+
44
+ # container: docker/podman
45
+ docker run --rm -v $PWD:/app ghcr.io/sayanarijit/settingspec --version
46
+ podman run --rm -v $PWD:/app ghcr.io/sayanarijit/settingspec --version
47
+ ```
25
48
 
26
49
  ## Concepts
27
50
 
@@ -41,30 +64,31 @@ profile.options = [ # Optional: Enables strict checking of per-
41
64
  ]
42
65
  profile.default = "dev" # Optional: Default profile when the switch is not set
43
66
 
44
- [settings] # Declare the settings here with syntax: `<key>.<profile>.<directive> = <value>`
45
- key1.default.val = "val1" # Default value for all profiles
46
- key1.prod.val = "prod1" # Override the default value when SETTINGSPEC_PROFILE=prod
67
+ [settings] # Declare the settings here with syntax: `<key>._.<profile>.<directive> = <value>`
68
+ key1._.default.val = "val1" # Default value for all profiles
69
+ key1._.prod.val = "prod1" # Override the default value when SETTINGSPEC_PROFILE=prod
47
70
 
48
- # Strictly define different values for different profiles
49
- key2.dev.val = "dev2" # When SETTINGSPEC_PROFILE=dev, key2=dev2
50
- key2.stage.val = "stage2"
51
- key2.prod.val = "prod2"
71
+ key2._ = { # Strictly define different values for different profiles
72
+ dev.val = "dev2",
73
+ stage.val = "stage2",
74
+ prod.val = "prod2",
75
+ }
52
76
  ```
53
77
 
54
78
  Overrides are intentional and kept in plain sight (see `key1`).
55
79
 
56
- By not defining a default value, you can ensure that you never miss adding a value for a specific profile (e.g. this file will refuse to load if `key2.prod` declaration is missing).
80
+ By not defining a default value, you can ensure that you never miss adding a value for a specific profile (e.g. this file will refuse to load if `key2._.prod` declaration is missing).
57
81
 
58
82
  ### Language independent
59
83
 
60
- The command-line tool `settingspec` can export the final settings into well-known formats such as `toml`, `json`, `yaml`, etc., or hard-coded modules such as `.py`, `.js`, `.lua`, etc., write them to disk, print, or pipe them via stdin or even export as environment variables, allowing you to use a single `settingspec.toml` without worrying about the target language.
84
+ The command-line tool `settingspec` can export the final settings into well-known formats such as `toml`, `json`, `yaml`, `tf`, etc., or hard-coded modules such as `.py`, `.js`, `.ts`, `.lua`, `.rs`, `.go`, `.zig`, `.c`, `.cpp`, `.java`, `.elm`, `.rb`, `.scala`, `.hs`, etc., write them to disk, print, or pipe them via stdin or even export as environment variables, allowing you to use a single `settingspec.toml` without worrying about the target language.
61
85
 
62
86
  ```toml
63
87
  [spec]
64
- export.file = true # Default: Export all settings into settings.toml
65
- # export.file = { # Optional: Fine-grained control over what to export
66
- # "settings.json" = true,
67
- # "settings.yaml" = {
88
+ export.file = true # Default: Export all settings into settings.toml
89
+ # export.file = { # Optional: Fine-grained control over what to export
90
+ # "settings.py" = true, # Get auto-completion of settings in your editor/IDE for free
91
+ # "settings.yaml" = { # Select which keys to export
68
92
  # key1 = true,
69
93
  # group1 = true,
70
94
  # group2.subgroup = true,
@@ -80,11 +104,11 @@ export.env = "PREFIX_" # Optional: Export as environment variables prefixed wit
80
104
  export.stdin = "toml" # Optional: Pass settings to program stdin in specified format
81
105
 
82
106
  [settings]
83
- key1.default.val = "val1"
84
- group1.key1.default.val = "group1val1"
85
- group2.subgroup.key1.default.val = "group2val1"
86
- group3.subgroup.key1.default.val = "group3val1"
87
- group3.subgroup.key1.default.tags = ["tag1", "tag2"]
107
+ key1._.default.val = "val1"
108
+ group1.key1._.default.val = "group1val1"
109
+ group2.subgroup.key1._.default.val = "group2val1"
110
+ group3.subgroup.key1._.default.val = "group3val1"
111
+ group3.subgroup.key1._.tags = ["tag1", "tag2"] # Syntax: `<key>._.tags = [...]`
88
112
  ```
89
113
 
90
114
  Now export the final settings as declared.
@@ -108,7 +132,7 @@ settingspec run -- [your program]...
108
132
  If no export option is specified, the default behavior is to export all settings into `settings.toml`.
109
133
 
110
134
  > [!NOTE]
111
- > If the target programming language supports null values, you can declare it as `key.default.null = true` in the settings section.
135
+ > If the target programming language supports null values, you can declare it as `key._.default.null = true` in the settings section.
112
136
 
113
137
  ### Easy secrets
114
138
 
@@ -139,12 +163,12 @@ settingspec.toml
139
163
  profile.options = ["dev", "stage", "prod"]
140
164
 
141
165
  [settings]
142
- secret1.default.env = "SECRET1" # Load value from $SECRET1
166
+ secret1._.default.env = "SECRET1" # Load value from $SECRET1
143
167
 
144
168
  # Override the default value from environment variables
145
- secret2.default.val = "defaultvalue" # Default value if $SECRET2 is not set
146
- secret2.default.env = "SECRET2" # Load value from $SECRET2 if set
147
- secret2.prod.env = "PRODSECRET" # Load value from $PRODSECRET if SETTINGSPEC_PROFILE=prod
169
+ secret2._.default.val = "defaultvalue" # Default value if $SECRET2 is not set
170
+ secret2._.default.env = "SECRET2" # Load value from $SECRET2 if set
171
+ secret2._.prod.env = "PRODSECRET" # Load value from $PRODSECRET if SETTINGSPEC_PROFILE=prod
148
172
  ```
149
173
 
150
174
  With these set up, run:
@@ -169,7 +193,7 @@ envfile.prod = "-" # Read from stdin, encrypted or not
169
193
  # decryption.key.path = "~/.ssh/" # Default
170
194
 
171
195
  [settings]
172
- secret1.default.env = "SECRET1" # Load value from $SECRET1
196
+ secret1._.default.env = "SECRET1" # Load value from $SECRET1
173
197
  ```
174
198
 
175
199
  #### Cottage or agebox
@@ -186,7 +210,7 @@ decryption.key.env = "COTTAGE_IDENTITY"
186
210
  # decryption.key.env = "AGEBOX_PRIVATE_KEYS"
187
211
 
188
212
  [settings]
189
- secret.default.env = "SECRET"
213
+ secret._.default.env = "SECRET"
190
214
  ```
191
215
 
192
216
  ## Learn more
@@ -6,7 +6,8 @@ use crate::model::*;
6
6
  use crate::parser::parse_config_str;
7
7
  use crate::profile::resolve_active_profile;
8
8
  use crate::resolver::resolve_settings;
9
- use clap::{Parser, Subcommand};
9
+ use clap::builder::styling::*;
10
+ use clap::{CommandFactory, Parser, Subcommand};
10
11
  use std::collections::{HashMap, HashSet};
11
12
  use std::io::Write;
12
13
  use std::path::{Path, PathBuf};
@@ -31,12 +32,14 @@ impl Drop for CleanupGuard {
31
32
  }
32
33
  }
33
34
 
35
+ const STYLES: Styles = Styles::styled()
36
+ .header(AnsiColor::Green.on_default().bold())
37
+ .usage(AnsiColor::Green.on_default().bold())
38
+ .literal(AnsiColor::Cyan.on_default().bold())
39
+ .placeholder(AnsiColor::Cyan.on_default().dimmed());
40
+
34
41
  #[derive(Parser, Debug)]
35
- #[command(
36
- name = "settingspec",
37
- version,
38
- about = "One file for all environments, all languages, all submodules."
39
- )]
42
+ #[command(author, version, about, styles = STYLES, long_about = None, arg_required_else_help = true)]
40
43
  pub struct Cli {
41
44
  #[command(subcommand)]
42
45
  pub command: Commands,
@@ -60,6 +63,16 @@ pub enum Commands {
60
63
 
61
64
  /// Deletes all generated export files
62
65
  Clean,
66
+
67
+ #[cfg(feature = "autocomplete")]
68
+ /// Generate shell completions.
69
+ /// Example: `eval "$(settingspec autocomplete bash)"` to load completions for bash.
70
+ #[command(name = "autocomplete")]
71
+ AutoComplete {
72
+ /// The shell to generate completions for.
73
+ #[arg(value_enum)]
74
+ shell: clap_complete::Shell,
75
+ },
63
76
  }
64
77
 
65
78
  pub const INIT_CONFIG: &str = r#"[spec]
@@ -67,8 +80,8 @@ profile.options = ["local", "prod"]
67
80
  profile.default = "local" # Switch via $SETTINGSPEC_PROFILE
68
81
 
69
82
  [settings]
70
- key.local.val = "local"
71
- key.prod.env = "KEY" # Load from $KEY when active profile is "prod"
83
+ key._.local.val = "local"
84
+ key._.prod.env = "KEY" # Load from $KEY when active profile is "prod"
72
85
  "#;
73
86
 
74
87
  pub fn init_config() -> Result<()> {
@@ -173,9 +186,18 @@ pub fn run_cli() -> Result<()> {
173
186
  }
174
187
  Commands::Watch => run_watch(),
175
188
  Commands::Clean => clean_exported_files(),
189
+ Commands::AutoComplete { shell } => generate_autocompletions(shell),
176
190
  }
177
191
  }
178
192
 
193
+ fn generate_autocompletions(shell: clap_complete::Shell) -> Result<()> {
194
+ let mut cmd = Cli::command();
195
+ let mut out = std::io::stdout();
196
+ let bin_name = cmd.get_name().to_string();
197
+ clap_complete::generate(shell, &mut cmd, bin_name, &mut out);
198
+ Ok(())
199
+ }
200
+
179
201
  fn run_watch() -> Result<()> {
180
202
  let ctx = load_and_resolve()?;
181
203
  let config_path = ctx.project_root.join("settingspec.toml");
@@ -5,8 +5,9 @@ pub enum SettingSpecError {
5
5
  ConfigNotFound,
6
6
  InvalidToml(String),
7
7
  MissingSettingsSection,
8
- ReservedProfileInOptions,
8
+ ReservedProfileInOptions(String),
9
9
  ReservedKeywordConflict(String),
10
+ InvalidDeclaration(String, String),
10
11
  UnknownProfileInSettings(String),
11
12
  ActiveProfileNotInOptions(String),
12
13
  NoProfileSelected,
@@ -14,7 +15,8 @@ pub enum SettingSpecError {
14
15
  InvalidDirective(String, String, String), // setting key, profile, directive
15
16
  NullCoexistsWithVal(String, String), // setting key, profile
16
17
  InvalidNullValue(String, String), // setting key, profile
17
- InvalidTagsValue(String, String), // setting key, profile
18
+ InvalidTagsValue(String), // setting key
19
+ DuplicateTags(String), // setting key
18
20
  InvalidEnvValue(String, String), // setting key, profile
19
21
  MissingRequiredEnvVar(String, String), // setting key, env var name
20
22
  RequiredValueMissing(String), // setting key
@@ -37,15 +39,17 @@ impl fmt::Display for SettingSpecError {
37
39
  f,
38
40
  "Missing required '[settings]' section in settingspec.toml"
39
41
  ),
40
- Self::ReservedProfileInOptions => write!(
42
+ Self::ReservedProfileInOptions(name) => write!(
41
43
  f,
42
- "Reserved profile identifier 'default' MUST NOT be in spec.profile.options"
43
- ),
44
- Self::ReservedKeywordConflict(seg) => write!(
45
- f,
46
- "Reserved directive keyword '{}' cannot be used as a setting key segment or profile name",
47
- seg
44
+ "Reserved profile identifier '{}' MUST NOT be in spec.profile.options",
45
+ name
48
46
  ),
47
+ Self::ReservedKeywordConflict(seg) => {
48
+ write!(f, "Reserved profile name '{}' cannot be used", seg)
49
+ }
50
+ Self::InvalidDeclaration(decl, reason) => {
51
+ write!(f, "Invalid setting declaration '{}': {}", decl, reason)
52
+ }
49
53
  Self::UnknownProfileInSettings(p) => write!(
50
54
  f,
51
55
  "Unknown profile '{}' used in [settings] (not in spec.profile.options)",
@@ -80,14 +84,17 @@ impl fmt::Display for SettingSpecError {
80
84
  "Directive 'null' must be true for setting '{}' in profile '{}'",
81
85
  k, p
82
86
  ),
83
- Self::InvalidTagsValue(k, p) => write!(
87
+ Self::InvalidTagsValue(k) => write!(
84
88
  f,
85
- "Directive 'tags' must be an array of strings for setting '{}' in profile '{}'",
86
- k, p
89
+ "Directive 'tags' for setting '{}': expected a sequence of strings",
90
+ k
87
91
  ),
92
+ Self::DuplicateTags(k) => {
93
+ write!(f, "Setting '{}' has more than one tags declaration", k)
94
+ }
88
95
  Self::InvalidEnvValue(k, p) => write!(
89
96
  f,
90
- "Directive 'env' must be a string for setting '{}' in profile '{}'",
97
+ "Directive 'env' for setting '{}' in profile '{}': expected a string",
91
98
  k, p
92
99
  ),
93
100
  Self::MissingRequiredEnvVar(k, env) => write!(
@@ -0,0 +1,211 @@
1
+ use super::common::{format_scalar_literal, is_valid_identifier};
2
+ use super::tree::{ConfigNode, build_config_tree};
3
+ use crate::error::Result;
4
+ use crate::model::ResolvedSetting;
5
+ use indexmap::IndexMap;
6
+ use toml::Value as TomlValue;
7
+
8
+ pub fn generate_c(settings: &[ResolvedSetting]) -> Result<String> {
9
+ let tree = build_config_tree(settings, false);
10
+ let mut out = String::from(
11
+ "/* Generated by SettingSpec */\n\n#ifndef SETTINGS_H\n#define SETTINGS_H\n\n#include <stdbool.h>\n#include <stddef.h>\n\n",
12
+ );
13
+
14
+ let mut struct_defs = String::new();
15
+ let root_type = generate_c_structs(&tree, "Settings", &mut struct_defs);
16
+
17
+ out.push_str(&struct_defs);
18
+
19
+ let mut init_vals = String::new();
20
+ format_c_init(&tree, 1, &mut init_vals);
21
+
22
+ out.push_str(&format!(
23
+ "static const {} settings = {{\n{}}};\n\n#endif /* SETTINGS_H */\n",
24
+ root_type, init_vals
25
+ ));
26
+
27
+ Ok(out)
28
+ }
29
+
30
+ fn generate_c_structs(
31
+ map: &IndexMap<String, ConfigNode>,
32
+ struct_name: &str,
33
+ out: &mut String,
34
+ ) -> String {
35
+ let mut fields = Vec::new();
36
+
37
+ for (k, node) in map {
38
+ let field_name = sanitize_c_identifier(k);
39
+ match node {
40
+ ConfigNode::Null => {
41
+ fields.push(format!(" const void* {};", field_name));
42
+ }
43
+ ConfigNode::Value(v) => match v {
44
+ TomlValue::String(_) | TomlValue::Datetime(_) => {
45
+ fields.push(format!(" const char* {};", field_name));
46
+ }
47
+ TomlValue::Integer(_) => {
48
+ fields.push(format!(" long long {};", field_name));
49
+ }
50
+ TomlValue::Float(_) => {
51
+ fields.push(format!(" double {};", field_name));
52
+ }
53
+ TomlValue::Boolean(_) => {
54
+ fields.push(format!(" bool {};", field_name));
55
+ }
56
+ TomlValue::Array(arr) => {
57
+ let (type_str, len) = c_array_type(arr);
58
+ fields.push(format!(" {} {}[{}];", type_str, field_name, len.max(1)));
59
+ fields.push(format!(" size_t {}_len;", field_name));
60
+ }
61
+ TomlValue::Table(tbl) => {
62
+ let sub_name = format!("{}{}", struct_name, capitalize(k));
63
+ let mut sub_map = IndexMap::new();
64
+ for (sub_k, sub_v) in tbl {
65
+ sub_map.insert(sub_k.clone(), ConfigNode::Value(sub_v.clone()));
66
+ }
67
+ let sub_type = generate_c_structs(&sub_map, &sub_name, out);
68
+ fields.push(format!(" {} {};", sub_type, field_name));
69
+ }
70
+ },
71
+ ConfigNode::Map(sub_map) => {
72
+ let sub_name = format!("{}{}", struct_name, capitalize(k));
73
+ let sub_type = generate_c_structs(sub_map, &sub_name, out);
74
+ fields.push(format!(" {} {};", sub_type, field_name));
75
+ }
76
+ }
77
+ }
78
+
79
+ let struct_def = format!(
80
+ "typedef struct {{\n{}\n}} {};\n\n",
81
+ fields.join("\n"),
82
+ struct_name
83
+ );
84
+ out.push_str(&struct_def);
85
+ struct_name.to_string()
86
+ }
87
+
88
+ fn format_c_init(map: &IndexMap<String, ConfigNode>, indent_level: usize, out: &mut String) {
89
+ let indent = " ".repeat(indent_level);
90
+
91
+ for (k, node) in map {
92
+ let field_name = sanitize_c_identifier(k);
93
+ match node {
94
+ ConfigNode::Null => {
95
+ out.push_str(&format!("{}.{} = NULL,\n", indent, field_name));
96
+ }
97
+ ConfigNode::Value(v) => match v {
98
+ TomlValue::Boolean(b) => {
99
+ out.push_str(&format!("{}.{} = {},\n", indent, field_name, b));
100
+ }
101
+ TomlValue::Array(arr) => {
102
+ let items: Vec<String> = arr
103
+ .iter()
104
+ .map(|item| match item {
105
+ TomlValue::Boolean(b) => b.to_string(),
106
+ _ => format_scalar_literal(item).unwrap_or_else(|| "\"\"".to_string()),
107
+ })
108
+ .collect();
109
+ out.push_str(&format!(
110
+ "{}.{} = {{ {} }},\n{}.{}_len = {},\n",
111
+ indent,
112
+ field_name,
113
+ items.join(", "),
114
+ indent,
115
+ field_name,
116
+ arr.len()
117
+ ));
118
+ }
119
+ TomlValue::Table(tbl) => {
120
+ out.push_str(&format!("{}.{} = {{\n", indent, field_name));
121
+ let mut sub_map = IndexMap::new();
122
+ for (sub_k, sub_v) in tbl {
123
+ sub_map.insert(sub_k.clone(), ConfigNode::Value(sub_v.clone()));
124
+ }
125
+ format_c_init(&sub_map, indent_level + 1, out);
126
+ out.push_str(&format!("{}}},\n", indent));
127
+ }
128
+ _ => {
129
+ if let Some(lit) = format_scalar_literal(v) {
130
+ out.push_str(&format!("{}.{} = {},\n", indent, field_name, lit));
131
+ }
132
+ }
133
+ },
134
+ ConfigNode::Map(sub_map) => {
135
+ out.push_str(&format!("{}.{} = {{\n", indent, field_name));
136
+ format_c_init(sub_map, indent_level + 1, out);
137
+ out.push_str(&format!("{}}},\n", indent));
138
+ }
139
+ }
140
+ }
141
+ }
142
+
143
+ fn c_array_type(arr: &[TomlValue]) -> (&'static str, usize) {
144
+ if arr.is_empty() {
145
+ return ("const char*", 0);
146
+ }
147
+ let elem_type = match &arr[0] {
148
+ TomlValue::Integer(_) => "long long",
149
+ TomlValue::Float(_) => "double",
150
+ TomlValue::Boolean(_) => "bool",
151
+ _ => "const char*",
152
+ };
153
+ (elem_type, arr.len())
154
+ }
155
+
156
+ fn sanitize_c_identifier(name: &str) -> String {
157
+ let is_keyword = matches!(
158
+ name,
159
+ "auto"
160
+ | "break"
161
+ | "case"
162
+ | "char"
163
+ | "const"
164
+ | "continue"
165
+ | "default"
166
+ | "do"
167
+ | "double"
168
+ | "else"
169
+ | "enum"
170
+ | "extern"
171
+ | "float"
172
+ | "for"
173
+ | "goto"
174
+ | "if"
175
+ | "inline"
176
+ | "int"
177
+ | "long"
178
+ | "register"
179
+ | "restrict"
180
+ | "return"
181
+ | "short"
182
+ | "signed"
183
+ | "sizeof"
184
+ | "static"
185
+ | "struct"
186
+ | "switch"
187
+ | "typedef"
188
+ | "union"
189
+ | "unsigned"
190
+ | "void"
191
+ | "volatile"
192
+ | "while"
193
+ );
194
+
195
+ if is_keyword || !is_valid_identifier(name) {
196
+ format!(
197
+ "_{}",
198
+ name.replace(|c: char| !c.is_ascii_alphanumeric(), "_")
199
+ )
200
+ } else {
201
+ name.to_string()
202
+ }
203
+ }
204
+
205
+ fn capitalize(s: &str) -> String {
206
+ let mut c = s.chars();
207
+ match c.next() {
208
+ None => String::new(),
209
+ Some(f) => f.to_uppercase().collect::<String>() + c.as_str(),
210
+ }
211
+ }