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.
- {settingspec-0.6.0 → settingspec-0.8.0}/Cargo.lock +1 -1
- {settingspec-0.6.0 → settingspec-0.8.0}/Cargo.toml +1 -1
- {settingspec-0.6.0 → settingspec-0.8.0}/PKG-INFO +52 -28
- {settingspec-0.6.0 → settingspec-0.8.0}/README.md +51 -27
- {settingspec-0.6.0 → settingspec-0.8.0}/src/cli.rs +30 -8
- {settingspec-0.6.0 → settingspec-0.8.0}/src/error.rs +20 -13
- settingspec-0.8.0/src/generator/c.rs +211 -0
- settingspec-0.8.0/src/generator/cpp.rs +219 -0
- settingspec-0.8.0/src/generator/elm.rs +125 -0
- settingspec-0.8.0/src/generator/go.rs +205 -0
- settingspec-0.8.0/src/generator/haskell.rs +250 -0
- settingspec-0.8.0/src/generator/java.rs +203 -0
- {settingspec-0.6.0 → settingspec-0.8.0}/src/generator/mod.rs +33 -0
- settingspec-0.8.0/src/generator/ruby.rs +134 -0
- settingspec-0.8.0/src/generator/rust.rs +178 -0
- settingspec-0.8.0/src/generator/scala.rs +177 -0
- settingspec-0.8.0/src/generator/terraform.rs +120 -0
- settingspec-0.8.0/src/generator/zig.rs +167 -0
- {settingspec-0.6.0 → settingspec-0.8.0}/src/model.rs +11 -7
- settingspec-0.8.0/src/parser.rs +421 -0
- {settingspec-0.6.0 → settingspec-0.8.0}/src/profile.rs +2 -2
- {settingspec-0.6.0 → settingspec-0.8.0}/src/resolver.rs +1 -13
- settingspec-0.6.0/src/parser.rs +0 -341
- {settingspec-0.6.0 → settingspec-0.8.0}/LICENSE-APACHE +0 -0
- {settingspec-0.6.0 → settingspec-0.8.0}/LICENSE-MIT +0 -0
- {settingspec-0.6.0 → settingspec-0.8.0}/pyproject.toml +0 -0
- {settingspec-0.6.0 → settingspec-0.8.0}/src/envfile.rs +0 -0
- {settingspec-0.6.0 → settingspec-0.8.0}/src/filter.rs +0 -0
- {settingspec-0.6.0 → settingspec-0.8.0}/src/generator/common.rs +0 -0
- {settingspec-0.6.0 → settingspec-0.8.0}/src/generator/env.rs +0 -0
- {settingspec-0.6.0 → settingspec-0.8.0}/src/generator/javascript.rs +0 -0
- {settingspec-0.6.0 → settingspec-0.8.0}/src/generator/json.rs +0 -0
- {settingspec-0.6.0 → settingspec-0.8.0}/src/generator/lua.rs +0 -0
- {settingspec-0.6.0 → settingspec-0.8.0}/src/generator/python.rs +0 -0
- {settingspec-0.6.0 → settingspec-0.8.0}/src/generator/toml.rs +0 -0
- {settingspec-0.6.0 → settingspec-0.8.0}/src/generator/tree.rs +0 -0
- {settingspec-0.6.0 → settingspec-0.8.0}/src/generator/typescript.rs +0 -0
- {settingspec-0.6.0 → settingspec-0.8.0}/src/generator/yaml.rs +0 -0
- {settingspec-0.6.0 → settingspec-0.8.0}/src/lib.rs +0 -0
- {settingspec-0.6.0 → settingspec-0.8.0}/src/main.rs +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: settingspec
|
|
3
|
-
Version: 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. [
|
|
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
|
-
|
|
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]
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
|
82
|
-
# export.file = {
|
|
83
|
-
# "settings.
|
|
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.
|
|
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. [
|
|
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
|
-
|
|
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]
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
|
65
|
-
# export.file = {
|
|
66
|
-
# "settings.
|
|
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.
|
|
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::
|
|
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
|
|
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 '
|
|
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
|
|
87
|
+
Self::InvalidTagsValue(k) => write!(
|
|
84
88
|
f,
|
|
85
|
-
"Directive 'tags'
|
|
86
|
-
k
|
|
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'
|
|
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
|
+
}
|