settingspec 0.5.1__tar.gz → 0.7.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.5.1 → settingspec-0.7.0}/Cargo.lock +1 -1
  2. {settingspec-0.5.1 → settingspec-0.7.0}/Cargo.toml +1 -1
  3. {settingspec-0.5.1 → settingspec-0.7.0}/PKG-INFO +29 -27
  4. {settingspec-0.5.1 → settingspec-0.7.0}/README.md +29 -27
  5. {settingspec-0.5.1 → settingspec-0.7.0}/src/cli.rs +17 -2
  6. {settingspec-0.5.1 → settingspec-0.7.0}/src/error.rs +20 -13
  7. settingspec-0.7.0/src/generator/c.rs +211 -0
  8. settingspec-0.7.0/src/generator/cpp.rs +219 -0
  9. settingspec-0.7.0/src/generator/elm.rs +125 -0
  10. settingspec-0.7.0/src/generator/go.rs +205 -0
  11. settingspec-0.7.0/src/generator/haskell.rs +250 -0
  12. settingspec-0.7.0/src/generator/java.rs +203 -0
  13. {settingspec-0.5.1 → settingspec-0.7.0}/src/generator/mod.rs +33 -0
  14. settingspec-0.7.0/src/generator/ruby.rs +134 -0
  15. settingspec-0.7.0/src/generator/rust.rs +178 -0
  16. settingspec-0.7.0/src/generator/scala.rs +177 -0
  17. settingspec-0.7.0/src/generator/terraform.rs +120 -0
  18. settingspec-0.7.0/src/generator/zig.rs +167 -0
  19. {settingspec-0.5.1 → settingspec-0.7.0}/src/model.rs +11 -7
  20. settingspec-0.7.0/src/parser.rs +421 -0
  21. {settingspec-0.5.1 → settingspec-0.7.0}/src/profile.rs +2 -2
  22. {settingspec-0.5.1 → settingspec-0.7.0}/src/resolver.rs +1 -13
  23. settingspec-0.5.1/src/parser.rs +0 -341
  24. {settingspec-0.5.1 → settingspec-0.7.0}/LICENSE-APACHE +0 -0
  25. {settingspec-0.5.1 → settingspec-0.7.0}/LICENSE-MIT +0 -0
  26. {settingspec-0.5.1 → settingspec-0.7.0}/pyproject.toml +0 -0
  27. {settingspec-0.5.1 → settingspec-0.7.0}/src/envfile.rs +0 -0
  28. {settingspec-0.5.1 → settingspec-0.7.0}/src/filter.rs +0 -0
  29. {settingspec-0.5.1 → settingspec-0.7.0}/src/generator/common.rs +0 -0
  30. {settingspec-0.5.1 → settingspec-0.7.0}/src/generator/env.rs +0 -0
  31. {settingspec-0.5.1 → settingspec-0.7.0}/src/generator/javascript.rs +0 -0
  32. {settingspec-0.5.1 → settingspec-0.7.0}/src/generator/json.rs +0 -0
  33. {settingspec-0.5.1 → settingspec-0.7.0}/src/generator/lua.rs +0 -0
  34. {settingspec-0.5.1 → settingspec-0.7.0}/src/generator/python.rs +0 -0
  35. {settingspec-0.5.1 → settingspec-0.7.0}/src/generator/toml.rs +0 -0
  36. {settingspec-0.5.1 → settingspec-0.7.0}/src/generator/tree.rs +0 -0
  37. {settingspec-0.5.1 → settingspec-0.7.0}/src/generator/typescript.rs +0 -0
  38. {settingspec-0.5.1 → settingspec-0.7.0}/src/generator/yaml.rs +0 -0
  39. {settingspec-0.5.1 → settingspec-0.7.0}/src/lib.rs +0 -0
  40. {settingspec-0.5.1 → settingspec-0.7.0}/src/main.rs +0 -0
@@ -1823,7 +1823,7 @@ dependencies = [
1823
1823
 
1824
1824
  [[package]]
1825
1825
  name = "settingspec"
1826
- version = "0.5.1"
1826
+ version = "0.7.0"
1827
1827
  dependencies = [
1828
1828
  "age",
1829
1829
  "anyhow",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "settingspec"
3
- version = "0.5.1"
3
+ version = "0.7.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.5.1
3
+ Version: 0.7.0
4
4
  Classifier: Programming Language :: Rust
5
5
  Classifier: Programming Language :: Python :: Implementation :: CPython
6
6
  Classifier: Programming Language :: Python :: Implementation :: PyPy
@@ -58,14 +58,15 @@ profile.options = [ # Optional: Enables strict checking of per-
58
58
  ]
59
59
  profile.default = "dev" # Optional: Default profile when the switch is not set
60
60
 
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
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
64
64
 
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"
65
+ key2._ = { # Strictly define different values for different profiles
66
+ dev.val = "dev2",
67
+ stage.val = "stage2",
68
+ prod.val = "prod2",
69
+ }
69
70
  ```
70
71
 
71
72
  Overrides are intentional and kept in plain sight (see `key1`).
@@ -74,14 +75,14 @@ By not defining a default value, you can ensure that you never miss adding a val
74
75
 
75
76
  ### Language independent
76
77
 
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.
78
+ 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
79
 
79
80
  ```toml
80
81
  [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" = {
82
+ export.file = true # Default: Export all settings into settings.toml
83
+ # export.file = { # Optional: Fine-grained control over what to export
84
+ # "settings.py" = true, # Get auto-completion of settings in your editor/IDE for free
85
+ # "settings.yaml" = { # Select which keys to export
85
86
  # key1 = true,
86
87
  # group1 = true,
87
88
  # group2.subgroup = true,
@@ -97,11 +98,11 @@ export.env = "PREFIX_" # Optional: Export as environment variables prefixed wit
97
98
  export.stdin = "toml" # Optional: Pass settings to program stdin in specified format
98
99
 
99
100
  [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"]
101
+ key1._.default.val = "val1"
102
+ group1.key1._.default.val = "group1val1"
103
+ group2.subgroup.key1._.default.val = "group2val1"
104
+ group3.subgroup.key1._.default.val = "group3val1"
105
+ group3.subgroup.key1._.tags = ["tag1", "tag2"] # Syntax: `<key>._.tags = [...]`
105
106
  ```
106
107
 
107
108
  Now export the final settings as declared.
@@ -125,7 +126,7 @@ settingspec run -- [your program]...
125
126
  If no export option is specified, the default behavior is to export all settings into `settings.toml`.
126
127
 
127
128
  > [!NOTE]
128
- > If the target programming language supports null values, you can declare it as `key.default.null = true` in the settings section.
129
+ > If the target programming language supports null values, you can declare it as `key._.default.null = true` in the settings section.
129
130
 
130
131
  ### Easy secrets
131
132
 
@@ -156,12 +157,12 @@ settingspec.toml
156
157
  profile.options = ["dev", "stage", "prod"]
157
158
 
158
159
  [settings]
159
- secret1.default.env = "SECRET1" # Load value from $SECRET1
160
+ secret1._.default.env = "SECRET1" # Load value from $SECRET1
160
161
 
161
162
  # 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
163
+ secret2._.default.val = "defaultvalue" # Default value if $SECRET2 is not set
164
+ secret2._.default.env = "SECRET2" # Load value from $SECRET2 if set
165
+ secret2._.prod.env = "PRODSECRET" # Load value from $PRODSECRET if SETTINGSPEC_PROFILE=prod
165
166
  ```
166
167
 
167
168
  With these set up, run:
@@ -186,7 +187,7 @@ envfile.prod = "-" # Read from stdin, encrypted or not
186
187
  # decryption.key.path = "~/.ssh/" # Default
187
188
 
188
189
  [settings]
189
- secret1.default.env = "SECRET1" # Load value from $SECRET1
190
+ secret1._.default.env = "SECRET1" # Load value from $SECRET1
190
191
  ```
191
192
 
192
193
  #### Cottage or agebox
@@ -196,13 +197,14 @@ Too simple? Use [Cottage](https://cottage-cli.pages.dev) or [agebox](https://git
196
197
  ```toml
197
198
  [spec]
198
199
  envfile.default = ".env.cott.age"
200
+ # envfile.default = ".env.agebox"
199
201
 
200
- # decryption.key.env = "AGEBOX_PRIVATE_KEYS"
201
- # decryption.key.env = "COTTAGE_IDENTITY"
202
202
  decryption.key.path = "~/.config/cottage/identity:~/.ssh"
203
+ decryption.key.env = "COTTAGE_IDENTITY"
204
+ # decryption.key.env = "AGEBOX_PRIVATE_KEYS"
203
205
 
204
206
  [settings]
205
- secret.default.env = "SECRET"
207
+ secret._.default.env = "SECRET"
206
208
  ```
207
209
 
208
210
  ## Learn more
@@ -41,14 +41,15 @@ profile.options = [ # Optional: Enables strict checking of per-
41
41
  ]
42
42
  profile.default = "dev" # Optional: Default profile when the switch is not set
43
43
 
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
47
-
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"
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
47
+
48
+ key2._ = { # Strictly define different values for different profiles
49
+ dev.val = "dev2",
50
+ stage.val = "stage2",
51
+ prod.val = "prod2",
52
+ }
52
53
  ```
53
54
 
54
55
  Overrides are intentional and kept in plain sight (see `key1`).
@@ -57,14 +58,14 @@ By not defining a default value, you can ensure that you never miss adding a val
57
58
 
58
59
  ### Language independent
59
60
 
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.
61
+ 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
62
 
62
63
  ```toml
63
64
  [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" = {
65
+ export.file = true # Default: Export all settings into settings.toml
66
+ # export.file = { # Optional: Fine-grained control over what to export
67
+ # "settings.py" = true, # Get auto-completion of settings in your editor/IDE for free
68
+ # "settings.yaml" = { # Select which keys to export
68
69
  # key1 = true,
69
70
  # group1 = true,
70
71
  # group2.subgroup = true,
@@ -80,11 +81,11 @@ export.env = "PREFIX_" # Optional: Export as environment variables prefixed wit
80
81
  export.stdin = "toml" # Optional: Pass settings to program stdin in specified format
81
82
 
82
83
  [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"]
84
+ key1._.default.val = "val1"
85
+ group1.key1._.default.val = "group1val1"
86
+ group2.subgroup.key1._.default.val = "group2val1"
87
+ group3.subgroup.key1._.default.val = "group3val1"
88
+ group3.subgroup.key1._.tags = ["tag1", "tag2"] # Syntax: `<key>._.tags = [...]`
88
89
  ```
89
90
 
90
91
  Now export the final settings as declared.
@@ -108,7 +109,7 @@ settingspec run -- [your program]...
108
109
  If no export option is specified, the default behavior is to export all settings into `settings.toml`.
109
110
 
110
111
  > [!NOTE]
111
- > If the target programming language supports null values, you can declare it as `key.default.null = true` in the settings section.
112
+ > If the target programming language supports null values, you can declare it as `key._.default.null = true` in the settings section.
112
113
 
113
114
  ### Easy secrets
114
115
 
@@ -139,12 +140,12 @@ settingspec.toml
139
140
  profile.options = ["dev", "stage", "prod"]
140
141
 
141
142
  [settings]
142
- secret1.default.env = "SECRET1" # Load value from $SECRET1
143
+ secret1._.default.env = "SECRET1" # Load value from $SECRET1
143
144
 
144
145
  # 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
146
+ secret2._.default.val = "defaultvalue" # Default value if $SECRET2 is not set
147
+ secret2._.default.env = "SECRET2" # Load value from $SECRET2 if set
148
+ secret2._.prod.env = "PRODSECRET" # Load value from $PRODSECRET if SETTINGSPEC_PROFILE=prod
148
149
  ```
149
150
 
150
151
  With these set up, run:
@@ -169,7 +170,7 @@ envfile.prod = "-" # Read from stdin, encrypted or not
169
170
  # decryption.key.path = "~/.ssh/" # Default
170
171
 
171
172
  [settings]
172
- secret1.default.env = "SECRET1" # Load value from $SECRET1
173
+ secret1._.default.env = "SECRET1" # Load value from $SECRET1
173
174
  ```
174
175
 
175
176
  #### Cottage or agebox
@@ -179,13 +180,14 @@ Too simple? Use [Cottage](https://cottage-cli.pages.dev) or [agebox](https://git
179
180
  ```toml
180
181
  [spec]
181
182
  envfile.default = ".env.cott.age"
183
+ # envfile.default = ".env.agebox"
182
184
 
183
- # decryption.key.env = "AGEBOX_PRIVATE_KEYS"
184
- # decryption.key.env = "COTTAGE_IDENTITY"
185
185
  decryption.key.path = "~/.config/cottage/identity:~/.ssh"
186
+ decryption.key.env = "COTTAGE_IDENTITY"
187
+ # decryption.key.env = "AGEBOX_PRIVATE_KEYS"
186
188
 
187
189
  [settings]
188
- secret.default.env = "SECRET"
190
+ secret._.default.env = "SECRET"
189
191
  ```
190
192
 
191
193
  ## Learn more
@@ -57,6 +57,9 @@ pub enum Commands {
57
57
  },
58
58
  /// Watches settingspec.toml and regenerates exports whenever it changes
59
59
  Watch,
60
+
61
+ /// Deletes all generated export files
62
+ Clean,
60
63
  }
61
64
 
62
65
  pub const INIT_CONFIG: &str = r#"[spec]
@@ -64,8 +67,8 @@ profile.options = ["local", "prod"]
64
67
  profile.default = "local" # Switch via $SETTINGSPEC_PROFILE
65
68
 
66
69
  [settings]
67
- key.local.val = "local"
68
- key.prod.env = "KEY" # Load from $KEY when active profile is "prod"
70
+ key._.local.val = "local"
71
+ key._.prod.env = "KEY" # Load from $KEY when active profile is "prod"
69
72
  "#;
70
73
 
71
74
  pub fn init_config() -> Result<()> {
@@ -169,6 +172,7 @@ pub fn run_cli() -> Result<()> {
169
172
  execute_run(&ctx, &command)
170
173
  }
171
174
  Commands::Watch => run_watch(),
175
+ Commands::Clean => clean_exported_files(),
172
176
  }
173
177
  }
174
178
 
@@ -278,6 +282,17 @@ fn perform_export(ctx: &ExecutionContext) -> Result<Vec<PathBuf>> {
278
282
  Ok(generated_files)
279
283
  }
280
284
 
285
+ fn clean_exported_files() -> Result<()> {
286
+ let ctx = load_and_resolve()?;
287
+
288
+ let _guard = CleanupGuard {
289
+ keep: false,
290
+ paths: ctx.doc.spec.export.file.keys().map(PathBuf::from).collect(),
291
+ };
292
+
293
+ Ok(())
294
+ }
295
+
281
296
  fn execute_run(ctx: &ExecutionContext, command_args: &[String]) -> Result<()> {
282
297
  if command_args.is_empty() {
283
298
  return Err(SettingSpecError::InvalidToml(
@@ -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
+ }