disposita 0.1.0 → 0.2.0
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +32 -4
- data/README.md +169 -215
- data/lib/disposita/configuration.rb +18 -9
- data/lib/disposita/formats/yaml.rb +7 -0
- data/lib/disposita/internal/deep_merge.rb +3 -3
- data/lib/disposita/internal/schema_builder.rb +17 -12
- data/lib/disposita/paths.rb +8 -3
- data/lib/disposita/schema.rb +69 -104
- data/lib/disposita/source.rb +10 -5
- data/lib/disposita/sources/environment.rb +8 -5
- data/lib/disposita/sources/file.rb +13 -8
- data/lib/disposita/sources/{hash.rb → memory.rb} +2 -2
- data/lib/disposita/types.rb +7 -4
- data/lib/disposita/version.rb +1 -1
- data/lib/disposita.rb +17 -11
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b3e6ae8c89a6d847ab58eb8f855b790a96bf7ad92b358d996341611500971175
|
|
4
|
+
data.tar.gz: 845e9049c14ad16a4b90998e1770249846df04266b9b1c032bfa4c6ce31d3c2b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5b6db017fb12915f96cbbd2b64d77b9f3de06786d188991114588e81fde0a83ab9f271fecc737ac5a95d5070487c10a0374597ed5ebc7b28957c3577125847e5
|
|
7
|
+
data.tar.gz: 85c76cc35117c1fa0451c0b1e7cebb00445956f4d5caa004c7fc14d5c964b9dbcbb1107e362e7ba0403e4a5307c2e989c0f61f840906a2e5c2a019e466e184a8
|
data/CHANGELOG.md
CHANGED
|
@@ -4,17 +4,45 @@ All notable changes to Disposita will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project follows Semantic Versioning.
|
|
6
6
|
|
|
7
|
-
## [
|
|
7
|
+
## [0.2.0] - 2026-09-09
|
|
8
8
|
|
|
9
|
-
###
|
|
9
|
+
### Breaking
|
|
10
10
|
|
|
11
|
-
-
|
|
12
|
-
|
|
11
|
+
- Setting and namespace names must be non-empty String or Symbol path segments without dots.
|
|
12
|
+
|
|
13
|
+
- Renamed `Disposita.define` to `Disposita.define_schema`, without compatibility aliases.
|
|
14
|
+
- Removed `Schema#load`; `Schema#resolve(sources: [])` is the only resolution API and accepts no positional arguments.
|
|
15
|
+
- Sources are now listed from highest to lowest precedence: the first source wins, recursively within namespaces; arrays are selected whole. Defaults remain the final fallback.
|
|
16
|
+
- Removed special `env`, `env_prefix`, and runtime `overrides` handling from Schema. Environment and runtime values are explicit Sources.
|
|
17
|
+
- Renamed `Sources::Hash` to `Sources::Memory`, including its require path, without an alias.
|
|
18
|
+
- Removed public `Schema#settings` and `Schema#setting`. Use `describe` and `each_setting` metadata instead.
|
|
19
|
+
- Renamed `Source#allows_secrets?` to `allows_secret_persistence?` to distinguish writing policy from reading secrets. File's explicit `allow_secrets:` opt-in remains.
|
|
20
|
+
- Made YAML serialization and platform detection helpers private; excluded internal classes, constructors and dot-access mechanisms from public YARD documentation.
|
|
13
21
|
|
|
14
22
|
### Added
|
|
15
23
|
|
|
16
24
|
- Complete namespace/helper documentation and regression coverage for source contracts, safe YAML and failed atomic writes.
|
|
17
25
|
|
|
26
|
+
- Public `Schema#each_setting` introspection for custom Sources, with deeply frozen metadata and redacted secret defaults.
|
|
27
|
+
- Regression coverage for first-source-wins nested merges, source fallback, provenance, API boundaries and explicit ENV resolution.
|
|
28
|
+
|
|
29
|
+
### Changed
|
|
30
|
+
|
|
31
|
+
- Rewrote README and YARD examples for the explicit source-based 0.2.0 API.
|
|
32
|
+
- Provenance and `explain` now report the first source that supplies a value. `explain` keeps its selected-value format.
|
|
33
|
+
- A default may satisfy `required: true`; `required: true, optional: true` remains invalid.
|
|
34
|
+
- Resolution copies selected values before freezing, preserving caller-owned source data.
|
|
35
|
+
|
|
36
|
+
### Fixed
|
|
37
|
+
|
|
38
|
+
- Copy and deeply freeze schema defaults at declaration time so later mutation of caller-owned containers or strings cannot change the schema.
|
|
39
|
+
|
|
40
|
+
- Reject traversal in user configuration application names and handle filesystem-root project paths.
|
|
41
|
+
- Reject unknown empty mappings instead of silently ignoring them.
|
|
42
|
+
- Redact secret values from coercion errors.
|
|
43
|
+
- Load YARD correctly and keep Markdown out of Ruby source parsing; validate documentation in CI.
|
|
44
|
+
- Redact secret defaults in schema metadata and preserve caller-owned path arrays.
|
|
45
|
+
|
|
18
46
|
## [0.1.0] - 2026-09-04
|
|
19
47
|
|
|
20
48
|
### Added
|
data/README.md
CHANGED
|
@@ -1,322 +1,276 @@
|
|
|
1
1
|
# Disposita
|
|
2
2
|
|
|
3
|
-
Declarative, typed
|
|
4
|
-
|
|
5
|
-
Disposita provides the mechanics of configuration while leaving ownership and meaning with the consumer. It does not know what `:ssh`, `production` or a timeout mean to your application; it only knows how those values are declared, loaded, coerced, validated, layered and persisted.
|
|
6
|
-
|
|
7
|
-
## Why Disposita?
|
|
8
|
-
|
|
9
|
-
Configuration tends to grow into repeated infrastructure: parsers, defaults, environment variables, per-user paths, project overrides, validation, persistence and diagnostics. Disposita centralizes that infrastructure without making a toolkit or framework the owner of every consumer's configuration.
|
|
10
|
-
|
|
11
|
-
## Installation
|
|
12
|
-
|
|
13
|
-
Add to your Gemfile:
|
|
14
|
-
|
|
15
|
-
```ruby
|
|
16
|
-
gem "disposita"
|
|
17
|
-
```
|
|
18
|
-
|
|
19
|
-
Then run `bundle install`.
|
|
3
|
+
Declarative, typed configuration for Ruby applications and gems. Define the settings your application owns, choose explicit sources, and resolve an immutable configuration.
|
|
20
4
|
|
|
21
5
|
## Define a schema
|
|
22
6
|
|
|
23
7
|
```ruby
|
|
24
8
|
require "disposita"
|
|
25
9
|
|
|
26
|
-
|
|
27
|
-
namespace :
|
|
28
|
-
setting :
|
|
29
|
-
|
|
30
|
-
default: "origin",
|
|
31
|
-
description: "Default Git remote"
|
|
32
|
-
|
|
33
|
-
setting :transport,
|
|
34
|
-
type: Disposita::Types.enum(:ssh, :https),
|
|
35
|
-
default: :ssh,
|
|
36
|
-
env: "SCM_GIT_TRANSPORT"
|
|
37
|
-
|
|
38
|
-
setting :timeout,
|
|
39
|
-
type: Integer,
|
|
40
|
-
default: 30
|
|
41
|
-
|
|
42
|
-
setting :token,
|
|
43
|
-
type: String,
|
|
44
|
-
secret: true,
|
|
45
|
-
optional: true
|
|
10
|
+
AppSchema = Disposita.define_schema(:app, version: 1) do
|
|
11
|
+
namespace :server do
|
|
12
|
+
setting :host, type: String, default: "localhost"
|
|
13
|
+
setting :port, type: Integer, default: 3000
|
|
46
14
|
end
|
|
47
15
|
end
|
|
48
16
|
```
|
|
49
17
|
|
|
50
|
-
`Disposita.
|
|
18
|
+
`Disposita.define_schema` returns a `Disposita::Schema`. Requiring the gem and defining a schema do not read configuration files or ENV, create directories, or register global configuration state.
|
|
51
19
|
|
|
52
|
-
|
|
20
|
+
## Resolve defaults
|
|
53
21
|
|
|
54
|
-
|
|
55
|
-
|
|
22
|
+
```ruby
|
|
23
|
+
config = AppSchema.resolve
|
|
56
24
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
| `default:` | Absent | Value used when no source provides one. An explicit `nil` still counts as a default and must satisfy the declared type. |
|
|
61
|
-
| `required:` | `false` | Reject resolution if the setting is absent. Cannot be combined with a default or `optional: true`. |
|
|
62
|
-
| `optional:` | `false` | Explicitly documents that absence is allowed; settings are already optional unless required. |
|
|
63
|
-
| `env:` | `nil` | Explicit variable name, taking precedence over generated names. |
|
|
64
|
-
| `secret:` | `false` | Redact diagnostics and deny ordinary file persistence. |
|
|
65
|
-
| `description:` | `nil` | Consumer-facing text returned by schema introspection. |
|
|
66
|
-
| `coerce:` | `true` | Convert raw input before checking semantic validation. Set false for strict values. |
|
|
25
|
+
config.server.host # => "localhost"
|
|
26
|
+
config.server.port # => 3000
|
|
27
|
+
```
|
|
67
28
|
|
|
68
|
-
|
|
29
|
+
No external source participates unless supplied explicitly. Declaring a setting's `env:` name does not enable ENV reads by itself.
|
|
69
30
|
|
|
70
|
-
## Resolve
|
|
31
|
+
## Resolve multiple sources
|
|
32
|
+
|
|
33
|
+
Building on `AppSchema` above:
|
|
71
34
|
|
|
72
35
|
```ruby
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
36
|
+
config = AppSchema.resolve(
|
|
37
|
+
sources: [
|
|
38
|
+
Disposita::Sources::Environment.new(prefix: "APP", name: :environment),
|
|
39
|
+
Disposita::Sources::File.new(".app.yml", name: :project)
|
|
40
|
+
]
|
|
76
41
|
)
|
|
42
|
+
```
|
|
77
43
|
|
|
78
|
-
|
|
79
|
-
".scm.yml",
|
|
80
|
-
name: :project
|
|
81
|
-
)
|
|
44
|
+
Sources are ordered from highest to lowest precedence. For each setting, Disposita uses the first source that provides a value, falling back through the remaining sources and finally to the schema default.
|
|
82
45
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
46
|
+
For example, with `APP_SERVER_PORT=5000` and this `.app.yml`:
|
|
47
|
+
|
|
48
|
+
```yaml
|
|
49
|
+
server:
|
|
50
|
+
host: example.com
|
|
51
|
+
port: 4000
|
|
89
52
|
```
|
|
90
53
|
|
|
91
|
-
|
|
54
|
+
The result is `config.server.port == 5000` and `config.server.host == "example.com"`. ENV supplies the port, the project supplies the host, and defaults fill any remaining settings.
|
|
92
55
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
56
|
+
Namespaces merge recursively. Scalars and arrays are selected whole from the first source providing them; arrays are never concatenated. An explicit `false`, `nil`, or empty array is a supplied value, not absence, and must satisfy the schema's type. An invalid winning value raises an error instead of falling back to another source. Every source is checked for unknown settings and unsupported schema versions, even if its values are shadowed.
|
|
57
|
+
|
|
58
|
+
## Installation
|
|
96
59
|
|
|
97
|
-
|
|
60
|
+
Disposita 0.2.0 requires Ruby 3.2 or newer. After the release is published, use:
|
|
98
61
|
|
|
99
62
|
```ruby
|
|
100
|
-
|
|
101
|
-
config.git.transport # => :ssh
|
|
102
|
-
config.git.timeout # => 10
|
|
63
|
+
gem "disposita", "~> 0.2.0"
|
|
103
64
|
```
|
|
104
65
|
|
|
105
|
-
|
|
66
|
+
Then run `bundle install`. Breaking changes from the previous release are recorded in [CHANGELOG.md](CHANGELOG.md).
|
|
106
67
|
|
|
107
|
-
##
|
|
108
|
-
|
|
109
|
-
Disposita performs conservative coercion when a setting allows it (the default):
|
|
68
|
+
## Public model
|
|
110
69
|
|
|
111
70
|
```text
|
|
112
|
-
|
|
113
|
-
"1.5" -> Float
|
|
114
|
-
"ssh" -> Symbol / enum value
|
|
115
|
-
"false" -> Boolean
|
|
71
|
+
define_schema → Schema → Sources → resolve → Configuration
|
|
116
72
|
```
|
|
117
73
|
|
|
118
|
-
|
|
74
|
+
- `Disposita.define_schema` describes which settings exist.
|
|
75
|
+
- `Source#read(schema)` obtains partial raw data.
|
|
76
|
+
- `Schema#resolve(sources: [])` applies precedence, recursive merge, defaults, coercion and validation, and records provenance.
|
|
77
|
+
- `Configuration` is the final immutable result.
|
|
78
|
+
- `Schema#write(source, data)` persists explicitly to one selected source.
|
|
119
79
|
|
|
120
|
-
|
|
121
|
-
setting :strict_port, type: Integer, coerce: false
|
|
122
|
-
```
|
|
123
|
-
|
|
124
|
-
## Validation
|
|
125
|
-
|
|
126
|
-
A setting can add consumer-owned semantic validation:
|
|
80
|
+
## Namespaces, types and validation
|
|
127
81
|
|
|
128
82
|
```ruby
|
|
129
|
-
|
|
130
|
-
|
|
83
|
+
ServiceSchema = Disposita.define_schema(:service) do
|
|
84
|
+
namespace :git do
|
|
85
|
+
setting :transport, type: Disposita::Types.enum(:ssh, :https), default: :ssh
|
|
86
|
+
setting :mirrors, type: Disposita::Types.array(String), default: []
|
|
87
|
+
end
|
|
88
|
+
setting :enabled, type: Disposita::Types.boolean, default: true
|
|
89
|
+
setting :ratio, type: Float, default: 1.25
|
|
90
|
+
setting :mode, type: Symbol, default: :development
|
|
91
|
+
setting :timeout, type: Integer, required: true, default: 30 do |value|
|
|
92
|
+
value.positive?
|
|
93
|
+
end
|
|
131
94
|
end
|
|
132
95
|
```
|
|
133
96
|
|
|
134
|
-
Disposita
|
|
97
|
+
Namespaces can nest. Each setting or namespace name must be a non-empty String or Symbol without `.`; dots separate path segments, so use a namespace to declare `server.port`. No additional naming convention is imposed. Use Ruby `String`, `Integer`, `Float`, and `Symbol`, plus `Types.boolean`, `Types.enum(...)` and `Types.array(member_type)`. Use these factories instead of instantiating their implementation classes. Disposita remains a small configuration library with no Typio or Rails dependency.
|
|
135
98
|
|
|
136
|
-
|
|
99
|
+
Coercion follows the declared type: `"5432"` becomes integer `5432`, `"1.25"` becomes float `1.25`, `"true"`/`"false"` become booleans, and `"ssh"` becomes `:ssh` for Symbol or an appropriate enum. Strings remain strings when `type: String` is declared. Array members use their declared member type; ENV strings are not implicitly split into arrays. Sources return raw data and do not infer types.
|
|
137
100
|
|
|
138
|
-
|
|
101
|
+
| Setting option | Default | Meaning |
|
|
102
|
+
| --- | --- | --- |
|
|
103
|
+
| `type:` | Required | Ruby class or configuration type. Custom type objects may implement `valid?` and optionally `coerce`; otherwise validation uses `===`. |
|
|
104
|
+
| `default:` | Absent | Final fallback, coerced and validated like source data. Explicit `nil` counts as present and must satisfy the type. |
|
|
105
|
+
| `required:` | `false` | A value must exist after all sources and defaults resolve. May be satisfied by a default. |
|
|
106
|
+
| `optional:` | `false` | Documents that absence is allowed. Cannot be true together with `required: true`. |
|
|
107
|
+
| `env:` | `nil` | Explicit environment variable name, used only by an Environment source. |
|
|
108
|
+
| `secret:` | `false` | Redact diagnostics and apply persistence policy. |
|
|
109
|
+
| `description:` | `nil` | Text included in public metadata. |
|
|
110
|
+
| `coerce:` | `true` | Set false to require already typed values. |
|
|
139
111
|
|
|
140
|
-
|
|
141
|
-
setting :transport, type: Symbol, env: "SCM_GIT_TRANSPORT"
|
|
142
|
-
```
|
|
112
|
+
Defaults are copied and deeply frozen when the setting is declared. Mutating the original arrays, hashes or strings afterward does not change the schema. Metadata returned by `describe` is also deeply frozen and cannot modify the stored default.
|
|
143
113
|
|
|
144
|
-
|
|
114
|
+
`required: true` means the setting must have a value in the final Configuration; it does not require the consumer to supply it explicitly. For example, `required: true, default: 30` resolves to `30` without an external source. Combining `required: true` with `optional: true` remains invalid.
|
|
145
115
|
|
|
146
|
-
|
|
147
|
-
Disposita::Sources::Environment.new(prefix: "SCM")
|
|
148
|
-
# git.transport -> SCM_GIT_TRANSPORT
|
|
149
|
-
```
|
|
116
|
+
Settings without `required: true` may be absent. Missing values are omitted from `to_h`. A validator block runs after coercion and must return a truthy value. Required absence raises `MissingSettingError`; coercion and custom validation failures raise `CoercionError` and `ValidationError`. Unknown keys raise `UnknownSettingError`, including unknown empty mappings. Typos are never silently ignored.
|
|
150
117
|
|
|
151
|
-
##
|
|
118
|
+
## Environment source
|
|
152
119
|
|
|
153
|
-
|
|
120
|
+
```ruby
|
|
121
|
+
AuthSchema = Disposita.define_schema(:auth) do
|
|
122
|
+
setting :token, type: String, secret: true, env: "MY_SPECIAL_TOKEN"
|
|
123
|
+
setting :timeout, type: Integer, default: 30
|
|
124
|
+
end
|
|
154
125
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
126
|
+
environment = Disposita::Sources::Environment.new(
|
|
127
|
+
env: { "MY_SPECIAL_TOKEN" => "example-token", "APP_TIMEOUT" => "10" },
|
|
128
|
+
prefix: "APP",
|
|
129
|
+
name: :environment
|
|
130
|
+
)
|
|
131
|
+
config = AuthSchema.resolve(sources: [environment])
|
|
159
132
|
```
|
|
160
133
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
## Explicit writes
|
|
134
|
+
`env:` on the source defaults to `ENV` because creating that source is an explicit choice. `prefix:` derives uppercase names from the full dotted path: `server.port` becomes `APP_SERVER_PORT`. A setting's explicit name replaces the derived name; if the explicit variable is absent, the derived name is not used. Without a prefix, only explicitly named variables are read. Unrelated environment variables are ignored.
|
|
164
135
|
|
|
165
|
-
|
|
136
|
+
## Memory source and runtime values
|
|
166
137
|
|
|
167
138
|
```ruby
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
139
|
+
runtime = Disposita::Sources::Memory.new({ server: { port: 9292 } }, name: :runtime)
|
|
140
|
+
environment = Disposita::Sources::Environment.new(prefix: "APP")
|
|
141
|
+
project = Disposita::Sources::File.new(".app.yml", name: :project)
|
|
142
|
+
global = Disposita::Sources::File.new(
|
|
143
|
+
File.join(Disposita::Paths.user_config("app"), "config.yml"), name: :global
|
|
144
|
+
)
|
|
145
|
+
config = AppSchema.resolve(sources: [runtime, environment, project, global])
|
|
171
146
|
```
|
|
172
147
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
Defaults are not written automatically: consumers persist only the overrides they choose.
|
|
176
|
-
|
|
177
|
-
## Secrets
|
|
148
|
+
The priority here is runtime, environment, project, global, then defaults. Memory is a read-only source for tests, embedding and programmatic values, with default name `:memory`. It normalizes mapping keys without coercing values. There is no special runtime argument on Schema.
|
|
178
149
|
|
|
179
|
-
|
|
150
|
+
## Configuration, provenance and explain
|
|
180
151
|
|
|
181
152
|
```ruby
|
|
182
|
-
|
|
153
|
+
config = AppSchema.resolve(sources: [
|
|
154
|
+
Disposita::Sources::Memory.new({ server: { port: "5000" } }, name: :runtime)
|
|
155
|
+
])
|
|
156
|
+
config.server.port # => 5000
|
|
157
|
+
config[:server][:host] # => "localhost"
|
|
158
|
+
config.source_of("server.port") # => :runtime
|
|
159
|
+
config.source_of([:server, :host]) # => :default
|
|
160
|
+
config.explain("server.port")
|
|
161
|
+
# => { path: "server.port", value: 5000, source: :runtime }
|
|
162
|
+
copy = config.to_h
|
|
183
163
|
```
|
|
184
164
|
|
|
185
|
-
|
|
165
|
+
Resolved values are recursively frozen; `to_h` returns a detached mutable copy. Resolution does not freeze caller-owned source values. `source_of` returns nil when no value was supplied. `explain` reports the selected value and source, not the full candidate chain. For an absent optional setting it reports nil value/source; for an unknown setting it raises `KeyError`. `inspect` and `explain` redact secrets; ordinary access and `to_h` return actual values.
|
|
186
166
|
|
|
187
|
-
|
|
188
|
-
config.git.token
|
|
189
|
-
```
|
|
167
|
+
## File source and safe YAML
|
|
190
168
|
|
|
191
|
-
|
|
169
|
+
Missing files are empty sources. Files use a codec with `load(String)` and `dump(Hash)` methods, supplied through `format:`; the bundled codec is `Disposita::Formats::YAML`.
|
|
192
170
|
|
|
193
|
-
|
|
194
|
-
config.inspect
|
|
195
|
-
# => #<Disposita::Configuration git=#<... token=[REDACTED]>>
|
|
196
|
-
```
|
|
171
|
+
YAML loading uses Psych safe APIs with Ruby objects, arbitrary classes, Symbol tags and aliases disabled. Runtime Symbols are dumped as plain strings and recovered through schema coercion. No JSON or TOML codecs are included.
|
|
197
172
|
|
|
198
|
-
|
|
173
|
+
YAML interprets some unquoted values as special types before schema coercion. For example, `release_date: 2026-09-09` is interpreted as a Date and rejected by the safe loader, even if the setting declares `type: String`. Quote such values to keep them as strings:
|
|
199
174
|
|
|
200
|
-
```
|
|
201
|
-
|
|
202
|
-
"~/.config/scm/private.yml",
|
|
203
|
-
name: :private,
|
|
204
|
-
allow_secrets: true
|
|
205
|
-
)
|
|
175
|
+
```yaml
|
|
176
|
+
release_date: "2026-09-09"
|
|
206
177
|
```
|
|
207
178
|
|
|
208
|
-
|
|
179
|
+
Date is not a permitted class in Disposita 0.2.0.
|
|
209
180
|
|
|
210
|
-
##
|
|
181
|
+
## Explicit persistence
|
|
211
182
|
|
|
212
|
-
|
|
183
|
+
Reading may combine many sources. Writing always targets exactly one source.
|
|
213
184
|
|
|
214
185
|
```ruby
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
config.explain("git.transport")
|
|
219
|
-
# => { path: "git.transport", value: :https, source: :project }
|
|
186
|
+
project = Disposita::Sources::File.new(".app.yml", name: :project)
|
|
187
|
+
AppSchema.write(project, server: { port: 9292 })
|
|
220
188
|
```
|
|
221
189
|
|
|
222
|
-
|
|
190
|
+
`write` validates only the explicitly supplied partial values and adds the schema version. It replaces the target's contents with that payload. It never automatically persists ENV, runtime, defaults, other sources or a resolved configuration; there is no `config.save!`.
|
|
223
191
|
|
|
224
|
-
|
|
192
|
+
File writes create a temporary file in the destination directory, write, flush, fsync and rename it into place. Temporary files are cleaned up on failure. Files have restrictive permissions (`0600` on supported platforms). Failed replacement leaves the existing file intact. Parent directories are created only during explicit writes.
|
|
225
193
|
|
|
226
|
-
|
|
194
|
+
## Secrets
|
|
227
195
|
|
|
228
|
-
|
|
229
|
-
- macOS: `~/Library/Application Support/<app>`
|
|
230
|
-
- Windows: `%APPDATA%\<app>` (falling back to `%LOCALAPPDATA%`)
|
|
196
|
+
`secret: true != encryption`. It means diagnostic redaction, protection against accidental disclosure and an explicit persistence policy. Disposita is not a secret manager.
|
|
231
197
|
|
|
232
|
-
|
|
198
|
+
Every source may read secret values. `Source#allows_secret_persistence?` describes permission to **write** them, and defaults to false. File rejects secret writes unless explicitly opted in:
|
|
233
199
|
|
|
234
200
|
```ruby
|
|
235
|
-
Disposita::
|
|
201
|
+
private_store = Disposita::Sources::File.new(
|
|
202
|
+
"~/.config/app/private.yml", name: :private, allow_secrets: true
|
|
203
|
+
)
|
|
204
|
+
AuthSchema.write(private_store, token: "example-token")
|
|
236
205
|
```
|
|
237
206
|
|
|
238
|
-
|
|
207
|
+
Secret defaults are redacted in metadata; secret values are redacted in `inspect`, `explain` and coercion errors. Direct access and exports contain real secrets, so they are not logging APIs.
|
|
239
208
|
|
|
240
|
-
##
|
|
209
|
+
## Paths
|
|
241
210
|
|
|
242
|
-
|
|
243
|
-
SCMConfig.describe("git.transport")
|
|
244
|
-
# => {
|
|
245
|
-
# path: "git.transport",
|
|
246
|
-
# type: "enum(:ssh, :https)",
|
|
247
|
-
# default: :ssh,
|
|
248
|
-
# has_default: true,
|
|
249
|
-
# required: false,
|
|
250
|
-
# secret: false,
|
|
251
|
-
# env: "SCM_GIT_TRANSPORT",
|
|
252
|
-
# description: nil
|
|
253
|
-
# }
|
|
254
|
-
```
|
|
211
|
+
`Paths.user_config("app")` follows Linux/XDG (`$XDG_CONFIG_HOME` or `~/.config`), macOS Application Support, and Windows APPDATA with LOCALAPPDATA fallback. The application name must be a single directory name without traversal or path separators. Optional `env:` and `host_os:` arguments allow deterministic path selection.
|
|
255
212
|
|
|
256
|
-
|
|
213
|
+
`Paths.project(root, relative)` resolves a consumer-selected project path and rejects traversal outside the root. These helpers compute paths without creating directories. The consumer chooses the layout; no `.disposita` or other project directory is imposed.
|
|
257
214
|
|
|
258
|
-
|
|
215
|
+
## Introspection and custom Sources
|
|
259
216
|
|
|
260
|
-
|
|
217
|
+
`Schema#describe(path)` returns a deeply frozen metadata hash, or nil for an unknown path. `Schema#each_setting` yields the same hashes in declaration order and returns an Enumerator without a block.
|
|
261
218
|
|
|
262
|
-
|
|
219
|
+
```ruby
|
|
220
|
+
AppSchema.describe("server.port")
|
|
221
|
+
# => { path: "server.port", type: "Integer", default: 3000,
|
|
222
|
+
# has_default: true, required: false, secret: false, env: nil, description: nil }
|
|
223
|
+
AppSchema.each_setting.map { |metadata| metadata[:path] }
|
|
224
|
+
# => ["server.host", "server.port"]
|
|
225
|
+
```
|
|
263
226
|
|
|
264
|
-
|
|
227
|
+
Secret defaults appear as `[REDACTED]`. Metadata never exposes internal definition or type adapter objects.
|
|
265
228
|
|
|
266
|
-
|
|
229
|
+
A custom Source can work entirely through public metadata:
|
|
267
230
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
-
|
|
273
|
-
|
|
274
|
-
- secret-aware diagnostics
|
|
231
|
+
```ruby
|
|
232
|
+
class DatabaseSource < Disposita::Source
|
|
233
|
+
def initialize(rows, name: :database)
|
|
234
|
+
super(name: name)
|
|
235
|
+
@rows = rows # An application-owned mapping of dotted paths to raw values.
|
|
236
|
+
end
|
|
275
237
|
|
|
276
|
-
|
|
238
|
+
def read(schema)
|
|
239
|
+
schema.each_setting.each_with_object({}) do |metadata, data|
|
|
240
|
+
path = metadata.fetch(:path)
|
|
241
|
+
next unless @rows.key?(path)
|
|
277
242
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
243
|
+
segments = path.split(".").map(&:to_sym)
|
|
244
|
+
parent = segments[0...-1].reduce(data) { |node, key| node[key] ||= {} }
|
|
245
|
+
parent[segments.last] = @rows.fetch(path)
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
end
|
|
284
249
|
|
|
285
|
-
|
|
250
|
+
config = AppSchema.resolve(sources: [DatabaseSource.new({ "server.port" => "6000" })])
|
|
251
|
+
config.server.port # => 6000
|
|
252
|
+
```
|
|
286
253
|
|
|
287
|
-
|
|
254
|
+
Custom Sources implement `read(schema)` and inherit `name`, read-only `writable?`, and denied `allows_secret_persistence?`. Writable adapters implement `writable?`, `write(schema, data)` and enforce their persistence policy. Schema handles validation; sources own storage mechanics.
|
|
288
255
|
|
|
289
|
-
|
|
256
|
+
## Schema versions
|
|
290
257
|
|
|
291
|
-
|
|
258
|
+
`Disposita.define_schema(:app, version: 1)` uses a consumer-owned version independent of `Disposita::VERSION` (`0.2.0`). Explicit writes include `version:`. Resolution rejects persisted versions newer than the schema and malformed version values. Complex migrations are not included.
|
|
292
259
|
|
|
293
|
-
|
|
260
|
+
## Development and API documentation
|
|
261
|
+
|
|
262
|
+
```sh
|
|
294
263
|
bundle install
|
|
295
|
-
bundle exec rspec
|
|
296
|
-
bundle exec rubocop
|
|
297
|
-
bundle exec rake
|
|
298
264
|
COVERAGE=true bundle exec rspec
|
|
265
|
+
bundle exec rubocop
|
|
299
266
|
bundle exec rake yard
|
|
267
|
+
bundle exec rake build
|
|
300
268
|
```
|
|
301
269
|
|
|
302
|
-
|
|
270
|
+
`bundle exec rake` runs RuboCop, specs and YARD. Coverage thresholds remain line >=95% and branch >=90%. CI tests Ruby 3.2, 3.3, 3.4 and 4.0. YARD uses README as the homepage, writes documentation to `doc/`, and treats warnings as failures.
|
|
271
|
+
|
|
272
|
+
Public contracts are `Disposita.define_schema`, Schema, Configuration, Source and its three built-in implementations, the Types factories, Paths and the YAML codec. `Internal::*`, namespace implementation nodes, constructors for resolved objects, and dot-access machinery are implementation details excluded from public documentation.
|
|
303
273
|
|
|
304
274
|
## License
|
|
305
275
|
|
|
306
276
|
MIT.
|
|
307
|
-
|
|
308
|
-
## API documentation
|
|
309
|
-
|
|
310
|
-
Disposita's public API is documented with YARD comments that explain not only
|
|
311
|
-
method signatures, but also ownership, precedence, persistence safety and common
|
|
312
|
-
usage patterns. Generate the local documentation with:
|
|
313
|
-
|
|
314
|
-
```sh
|
|
315
|
-
bundle exec rake yard
|
|
316
|
-
```
|
|
317
|
-
|
|
318
|
-
The generated site is written to `doc/`. CI generates it with warnings treated as failures. Internal implementation objects are
|
|
319
|
-
marked with `@api private`; applications should build against the documented
|
|
320
|
-
public objects such as `Disposita`, `Disposita::Schema`,
|
|
321
|
-
`Disposita::Configuration`, `Disposita::Source`, `Disposita::Sources::*`,
|
|
322
|
-
`Disposita::Types` and `Disposita::Paths`.
|