Skip to content

Commit 141d12d

Browse files
committed
feat(_)!: new syntax: <key>._.<env>.<directive> = <value>
This breaking change adds a separator between the key section and profile/tags declaration section. Also, tags declaration syntax is now: `<key>.tags = [...]` Reasons: - Better visiblity - easy to recognise the key part. - Only `_` is now a reserved keyword for the key segment. - Only `default` and `tags` and reserved keywords for the profile options. - Tags are now simplified - detached from profiles.
1 parent 0ea3b5d commit 141d12d

19 files changed

Lines changed: 817 additions & 705 deletions

‎README.md‎

Lines changed: 22 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -41,14 +41,14 @@ profile.options = [ # Optional: Enables strict checking of per-
4141
]
4242
profile.default = "dev" # Optional: Default profile when the switch is not set
4343

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
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
4747

4848
# 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"
49+
key2._.dev.val = "dev2" # When SETTINGSPEC_PROFILE=dev, key2=dev2
50+
key2._.stage.val = "stage2"
51+
key2._.prod.val = "prod2"
5252
```
5353

5454
Overrides are intentional and kept in plain sight (see `key1`).
@@ -61,10 +61,10 @@ The command-line tool `settingspec` can export the final settings into well-know
6161

6262
```toml
6363
[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" = {
64+
export.file = true # Default: Export all settings into settings.toml
65+
# export.file = { # Optional: Fine-grained control over what to export
66+
# "settings.py" = true, # Get auto-completion of settings in your editor/IDE for free
67+
# "settings.yaml" = { # Select which keys to export
6868
# key1 = true,
6969
# group1 = true,
7070
# group2.subgroup = true,
@@ -80,11 +80,11 @@ export.env = "PREFIX_" # Optional: Export as environment variables prefixed wit
8080
export.stdin = "toml" # Optional: Pass settings to program stdin in specified format
8181

8282
[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"]
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._.tags = ["tag1", "tag2"] # Syntax: `<key>._.tags = [...]`
8888
```
8989

9090
Now export the final settings as declared.
@@ -108,7 +108,7 @@ settingspec run -- [your program]...
108108
If no export option is specified, the default behavior is to export all settings into `settings.toml`.
109109

110110
> [!NOTE]
111-
> If the target programming language supports null values, you can declare it as `key.default.null = true` in the settings section.
111+
> If the target programming language supports null values, you can declare it as `key._.default.null = true` in the settings section.
112112
113113
### Easy secrets
114114

@@ -139,12 +139,12 @@ settingspec.toml
139139
profile.options = ["dev", "stage", "prod"]
140140

141141
[settings]
142-
secret1.default.env = "SECRET1" # Load value from $SECRET1
142+
secret1._.default.env = "SECRET1" # Load value from $SECRET1
143143

144144
# 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
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
148148
```
149149

150150
With these set up, run:
@@ -169,7 +169,7 @@ envfile.prod = "-" # Read from stdin, encrypted or not
169169
# decryption.key.path = "~/.ssh/" # Default
170170

171171
[settings]
172-
secret1.default.env = "SECRET1" # Load value from $SECRET1
172+
secret1._.default.env = "SECRET1" # Load value from $SECRET1
173173
```
174174

175175
#### Cottage or agebox
@@ -186,7 +186,7 @@ decryption.key.env = "COTTAGE_IDENTITY"
186186
# decryption.key.env = "AGEBOX_PRIVATE_KEYS"
187187

188188
[settings]
189-
secret.default.env = "SECRET"
189+
secret._.default.env = "SECRET"
190190
```
191191

192192
## Learn more

‎SPECIFICATION.md‎

Lines changed: 80 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# SettingSpec Specification
22

3-
**Version:** 0.6
3+
**Version:** 0.7
44
**Status:** Released
55
**Authors:** Arijit Basu and SettingSpec Contributors
66
**Repository:** <https://github.com/sayanarijit/settingspec>
@@ -43,7 +43,10 @@ The words **MUST**, **MUST NOT**, **REQUIRED**, **SHALL**, **SHALL NOT**, **SHOU
4343
- **Profile:** A named environment or operating mode, such as `dev`, `stage`, or `prod`.
4444
- **Default profile:** The `default` profile. It provides values that apply to all profiles unless a profile overrides them.
4545
- **Setting key:** A setting name that can contain dot-separated parts, such as `database.host`.
46-
- **Directive:** The final part of a setting declaration. Supported directives are `val`, `env`, `null`, and `tags`.
46+
- **Separator:** The reserved `_` component that separates the setting key from the rest of a declaration.
47+
- **Profile declaration:** A declaration of the form `{key}._.{profile}.{directive}`.
48+
- **Directive:** The final part of a profile declaration. Supported directives are `val`, `env`, and `null`.
49+
- **Tags declaration:** A declaration of the form `{key}._.tags`. It attaches tags to a setting regardless of profile.
4750
- **Active profile:** The profile selected for the current run.
4851
- **Export target:** A file or standard output where resolved settings are written.
4952

@@ -66,17 +69,19 @@ The file has two top-level sections:
6669

6770
### 2.2 Setting names
6871

69-
SettingSpec uses dot-separated keys to represent settings, profiles, and directives.
72+
SettingSpec uses dot-separated keys to represent settings, profiles, and directives. The reserved `_` component separates the setting key from the rest of the declaration.
7073

7174
```toml
7275
[settings]
73-
key1.default.val = "val1"
76+
key1._.default.val = "val1"
77+
key1._.tags = ["tag1"]
7478
```
7579

76-
The general form is:
80+
The general forms are:
7781

7882
```text
79-
{key}.{profile}.{directive} = {value}
83+
{key}._.{profile}.{directive} = {value}
84+
{key}._.tags = [{tag}, ...]
8085
```
8186

8287
---
@@ -132,8 +137,8 @@ When `profile.options` is set:
132137
1. The active profile MUST be in the list.
133138
2. Every profile used in `[settings]` MUST be in the list or be `default`.
134139
3. `default` MUST NOT be included in `profile.options`.
135-
4. Every setting MUST either have a `default` declaration or have a declaration for every profile in `profile.options`.
136-
5. Profile names MUST NOT be `val`, `env`, `null`, or `tags`.
140+
4. Every setting MUST either have a `default` profile declaration or have a declaration for every profile in `profile.options`. A tags declaration alone does not count as a profile declaration.
141+
5. Profile names MUST NOT be `default` or `tags`.
137142

138143
If any of these rules fail, configuration loading MUST stop with an error.
139144

@@ -346,53 +351,71 @@ The `[settings]` section defines setting values, environment variables, tags, an
346351

347352
### 4.1 Declaration format
348353

349-
Each setting uses this form:
354+
Each setting is defined by one or more declarations. There are two kinds.
355+
356+
A **profile declaration** provides a value for a profile:
350357

351358
```text
352-
{key}.{profile}.{directive} = {value}
359+
{key}._.{profile}.{directive} = {value}
360+
```
361+
362+
A **tags declaration** attaches tags to a setting:
363+
364+
```text
365+
{key}._.tags = [{tag}, ...]
353366
```
354367

355368
For example:
356369

357370
```toml
358-
app.name.default.val = "MyApp"
359-
app.port.dev.val = 8080
360-
app.port.prod.val = 80
371+
app.name._.default.val = "MyApp"
372+
app.port._.dev.val = 8080
373+
app.port._.prod.val = 80
374+
app.port._.tags = ["network"]
361375
```
362376

363-
The three parts are:
377+
The parts are:
364378

365-
- `{key}` — the setting name, such as `database.host`.
379+
- `{key}` — the setting name, such as `database.host`. It can contain dot-separated parts.
380+
- `_` — the reserved separator between the key and the rest of the declaration.
366381
- `{profile}` — a profile from `profile.options`, or `default`.
367-
- `{directive}` — `val`, `env`, `null`, or `tags`.
382+
- `{directive}` — `val`, `env`, or `null`.
368383
- `{value}` — the value for that directive.
369384

370-
SettingSpec reads a dotted declaration by position:
385+
#### Parsing declarations
386+
387+
SettingSpec reads a dotted declaration by splitting it at the `_` component:
388+
389+
1. Everything before the `_` component is the setting key. It MUST NOT be empty.
390+
2. Everything after the `_` component MUST be one of:
391+
- the single component `tags`, which makes it a tags declaration, or
392+
- exactly two components, `{profile}.{directive}`, which makes it a profile declaration.
393+
3. A declaration with no `_` component, more than one `_` component, or any other shape after the `_` component is malformed.
371394

372-
1. The last part is the directive.
373-
2. The part before it is the profile.
374-
3. Everything before those two parts is the setting key.
395+
Because the separator is explicit, setting keys MAY contain parts named `val`, `env`, `null`, or `tags`. For example, `group1.env._.default.val` declares the setting `group1.env`.
375396

376397
### 4.1.1 Reserved names
377398

378-
The names `val`, `env`, `null`, and `tags` are reserved.
399+
The name `_` is reserved as the separator.
379400

380-
They MUST NOT be used:
401+
- It MUST NOT be used as any part of a setting key.
381402

382-
- In any part of a setting key.
383-
- As a profile name.
403+
The names `default` and `tags` are reserved in the profile position.
404+
405+
- `default` is the default profile. It MUST NOT be listed in `profile.options`.
406+
- `tags` marks a tags declaration. It MUST NOT be used as a profile name.
384407

385408
For example, these are invalid:
386409

387410
```toml
388-
env.default.val = "x"
389-
group1.tags.prod.val = "y"
390-
val.default.env = "Z"
411+
_.default.val = "x" # empty key
412+
key1.default.val = "x" # missing `_` separator
413+
key1._.default._.val = "y" # more than one `_` component
414+
key1._.tags.val = "z" # `tags` used as a profile
415+
key1._.default = "w" # missing directive
391416
```
392417

393-
This rule prevents dotted names from being interpreted incorrectly.
394-
395-
SettingSpec MUST reject these conflicts during configuration validation and report the offending name.
418+
SettingSpec MUST reject these during configuration validation and report the offending declaration.
396419

397420
### 4.2 Directives
398421

@@ -413,17 +436,17 @@ Supported values include:
413436
Example:
414437

415438
```toml
416-
app.name.default.val = "MyApp"
417-
app.port.dev.val = 8080
418-
app.port.prod.val = 80
439+
app.name._.default.val = "MyApp"
440+
app.port._.dev.val = 8080
441+
app.port._.prod.val = 80
419442
```
420443

421444
#### `env`
422445

423446
Gets the value from an environment variable.
424447

425448
```toml
426-
database.password.default.env = "DB_PASSWORD"
449+
database.password._.default.env = "DB_PASSWORD"
427450
```
428451

429452
SettingSpec reads `DB_PASSWORD` when the setting is resolved. This includes variables loaded from `spec.envfile`.
@@ -433,9 +456,9 @@ SettingSpec reads `DB_PASSWORD` when the setting is resolved. This includes vari
433456
A setting can provide an environment variable plus a fallback value:
434457

435458
```toml
436-
secret2.default.val = "defaultvalue"
437-
secret2.default.env = "SECRET2"
438-
secret2.prod.env = "PRODSECRET"
459+
secret2._.default.val = "defaultvalue"
460+
secret2._.default.env = "SECRET2"
461+
secret2._.prod.env = "PRODSECRET"
439462
```
440463

441464
The rules are:
@@ -449,8 +472,8 @@ The rules are:
449472
Marks a setting as unset for a profile.
450473

451474
```toml
452-
debug_banner.default.val = "My App BETA 0.0.1"
453-
debug_banner.prod.null = true
475+
debug_banner._.default.val = "My App BETA 0.0.1"
476+
debug_banner._.prod.null = true
454477
```
455478

456479
When exported:
@@ -460,14 +483,21 @@ When exported:
460483
- `null` can only be `true`.
461484
- `null` cannot be used together with `val` for the same profile.
462485

463-
#### `tags`
486+
### 4.3 Tags
464487

465-
Adds metadata tags to a setting.
488+
Tags add metadata to a setting. They are declared after the key, not per profile, so they apply to the setting in every profile.
466489

467490
```toml
468-
api_key.default.tags = ["sensitive", "auth"]
491+
api_key._.default.env = "API_KEY"
492+
api_key._.tags = ["sensitive", "auth"]
469493
```
470494

495+
The rules are:
496+
497+
1. The value MUST be an array of strings.
498+
2. A setting MAY have at most one tags declaration.
499+
3. A tags declaration does not define a value. A setting that has only a tags declaration and no profile declarations is treated as having no declaration for resolution purposes (see section 5.2).
500+
471501
Tags are not exported as setting values. They can be used when selecting settings for an export, for example with `#auth`.
472502

473503
---
@@ -518,7 +548,7 @@ flowchart TD
518548

519549
For the same rules in numbered form:
520550

521-
1. If `K` has a declaration for `P`:
551+
1. If `K` has a profile declaration for `P`:
522552
- If its `env` variable exists, use that value.
523553
- Otherwise, use its `val` or `null` value if one exists.
524554
2. If there is no declaration for `P`, check `default`:
@@ -528,7 +558,9 @@ For the same rules in numbered form:
528558
- With `profile.options`, this is a profile-coverage error.
529559
- Without `profile.options`, the key is omitted.
530560

531-
The flow above assumes that the setting key, profile, and directive have already been identified correctly. Reserved names such as `val`, `env`, `null`, and `tags` must be rejected during validation.
561+
Tags declarations are not profile declarations and play no part in this flow.
562+
563+
The flow above assumes that each declaration has already been parsed into its key, profile, and directive. Malformed declarations and reserved names must be rejected during validation.
532564

533565
Environment files and their decryption are completed before this process starts, so their variables are available during resolution.
534566

@@ -540,9 +572,9 @@ It checks that:
540572

541573
1. Every profile declared in settings belongs to the options declared in spec.
542574
2. Every setting has complete profile coverage when strict profile validation is enabled.
543-
3. Every declaration uses valid directives.
544-
4. No setting key or profile name uses a reserved directive name.
545-
5. Envionment values for the active profile are present if required.
575+
3. Every declaration is well-formed and uses a valid directive (`val`, `env`, or `null`) or is a valid tags declaration.
576+
4. No setting key contains the reserved `_` component, and no profile name is `default` or `tags` in `profile.options`.
577+
5. Environment values for the active profile are present if required.
546578

547579
---
548580

@@ -740,7 +772,7 @@ settingspec check [OPTIONS]
740772
It checks:
741773

742774
- TOML syntax.
743-
- Reserved keywords in setting names and profiles.
775+
- Malformed declarations, the reserved `_` separator in setting keys, and reserved profile names (`default`, `tags`).
744776
- Profile completeness.
745777
- Required environment variables for active profile.
746778
- Decryption identities for encrypted environment files used by the active profile.

0 commit comments

Comments
 (0)