anyway_app_config 0.3.1
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 +7 -0
- data/CODE_OF_CONDUCT.md +10 -0
- data/LICENSE.txt +21 -0
- data/README.md +505 -0
- data/Rakefile +12 -0
- data/lib/anyway_app_config/config.rb +160 -0
- data/lib/anyway_app_config/loaders/env_yaml.rb +27 -0
- data/lib/anyway_app_config/loaders/flat_yaml.rb +17 -0
- data/lib/anyway_app_config/singleton.rb +43 -0
- data/lib/anyway_app_config/version.rb +5 -0
- data/lib/anyway_app_config.rb +28 -0
- metadata +86 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 617b75e0653ccf7f6cd5990ed5484810d48d08e799a1d8e6b526208499fbbe14
|
|
4
|
+
data.tar.gz: c9a057c7b0fa2f0e1ae3fa34cdfbdd0fd573113ee8002731756952815a6c2f7a
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: b5ed356d38908f1a8db01006504b87eb74d731b0c7dc71dc7a6df9d006ad511ca795f001daac49184272959e5dd021831a544dd86b8426eafadd6fbf265d748c
|
|
7
|
+
data.tar.gz: '058d004053ece9aeb7600162b68d5a595641b6d052c3bb3726f65451e182bd1cdedd6e36b2a88e4145d26a0f54dd5929fabadd189b1e253b5cc1f673a7b9abfd'
|
data/CODE_OF_CONDUCT.md
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Code of Conduct
|
|
2
|
+
|
|
3
|
+
"anyway_app_config" follows [The Ruby Community Conduct Guideline](https://www.ruby-lang.org/en/conduct) in all "collaborative space", which is defined as community communications channels (such as mailing lists, submitted patches, commit comments, etc.):
|
|
4
|
+
|
|
5
|
+
* Participants will be tolerant of opposing views.
|
|
6
|
+
* Participants must ensure that their language and actions are free of personal attacks and disparaging personal remarks.
|
|
7
|
+
* When interpreting the words and actions of others, participants should always assume good intentions.
|
|
8
|
+
* Behaviour which can be reasonably considered harassment will not be tolerated.
|
|
9
|
+
|
|
10
|
+
If you have any concerns about behaviour within this project, please contact us at ["senid231@gmail.com"](mailto:"senid231@gmail.com").
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Denis Talakevich
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,505 @@
|
|
|
1
|
+
# AnywayAppConfig
|
|
2
|
+
|
|
3
|
+
Schema-driven application config built on top of [`anyway_config`][anyway_config].
|
|
4
|
+
|
|
5
|
+
`anyway_config` does the heavy lifting of loading values from YAML and ENV.
|
|
6
|
+
`anyway_app_config` adds a small DSL on top for describing app config with
|
|
7
|
+
typed attributes, defaults, required fields, and **nested objects** (single
|
|
8
|
+
or array). Configs can be used as plain instances or as a singleton.
|
|
9
|
+
|
|
10
|
+
## Installation
|
|
11
|
+
|
|
12
|
+
Add to your Gemfile:
|
|
13
|
+
|
|
14
|
+
```ruby
|
|
15
|
+
gem "anyway_app_config"
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Then run `bundle install`.
|
|
19
|
+
|
|
20
|
+
## Defining a config
|
|
21
|
+
|
|
22
|
+
Inherit from `AnywayAppConfig::Config` and describe attributes with the
|
|
23
|
+
`attribute` DSL:
|
|
24
|
+
|
|
25
|
+
```ruby
|
|
26
|
+
require "anyway_app_config"
|
|
27
|
+
|
|
28
|
+
class AppConfig < AnywayAppConfig::Config
|
|
29
|
+
config_name "app_config"
|
|
30
|
+
env_prefix "APP"
|
|
31
|
+
|
|
32
|
+
attribute :deploy_env, type: :string, required: true
|
|
33
|
+
attribute :version, type: :string, default: "unknown"
|
|
34
|
+
attribute :commit_sha, type: :string, default: "000000"
|
|
35
|
+
|
|
36
|
+
attribute :sentry, required: true do
|
|
37
|
+
attribute :dsn, type: :string, default: ""
|
|
38
|
+
attribute :environment, type: :string, required: true
|
|
39
|
+
attribute :server_name, type: :string, required: true
|
|
40
|
+
attribute :tags, type: :hash, default: {}
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
attribute :prometheus, required: true do
|
|
44
|
+
attribute :enabled, type: :boolean, default: false
|
|
45
|
+
attribute :host, type: :string, default: "localhost"
|
|
46
|
+
attribute :port, type: :integer, default: 9394
|
|
47
|
+
attribute :default_labels, type: :hash, default: {}
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
`attribute` accepts:
|
|
53
|
+
|
|
54
|
+
| option | meaning |
|
|
55
|
+
| ---------- | ------------------------------------------------------------ |
|
|
56
|
+
| `type:` | type id from `anyway_config`'s registry (`:string`, `:integer`, `:float`, `:boolean`, `:date`, `:datetime`, `:uri`, `:hash`, …), or any object responding to `#call(value)` |
|
|
57
|
+
| `array:` | when `true`, value is an array of `type` (or nested objects) |
|
|
58
|
+
| `default:` | default value (defaults to `nil`, or `[]` when `array: true`) |
|
|
59
|
+
| `required:`| validate that the attribute is present and not empty |
|
|
60
|
+
| block | defines a nested config object (see below) |
|
|
61
|
+
|
|
62
|
+
### Nested attributes
|
|
63
|
+
|
|
64
|
+
Pass a block to define a nested config. The DSL builds a child config class
|
|
65
|
+
that inherits from `AnywayAppConfig::Config`, exposes the same DSL, and is
|
|
66
|
+
exposed as a constant (e.g. `AppConfig::SentryCfg`).
|
|
67
|
+
|
|
68
|
+
Combine with `array: true` to get a list of nested objects:
|
|
69
|
+
|
|
70
|
+
```ruby
|
|
71
|
+
class AppConfig < AnywayAppConfig::Config
|
|
72
|
+
config_name "app_config"
|
|
73
|
+
|
|
74
|
+
attribute :servers, array: true do
|
|
75
|
+
attribute :host, type: :string, required: true
|
|
76
|
+
attribute :port, type: :integer, default: 80
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### The `:hash` type
|
|
82
|
+
|
|
83
|
+
`AnywayAppConfig::Config` registers a `:hash` type on a per-class type
|
|
84
|
+
registry. It accepts any `Hash` value as-is and raises `ArgumentError` for
|
|
85
|
+
non-hash values. Anyway's global `TypeRegistry.default` is **not** mutated.
|
|
86
|
+
|
|
87
|
+
## Loading config
|
|
88
|
+
|
|
89
|
+
```ruby
|
|
90
|
+
config = AppConfig.load! # all sources merged, contained values frozen
|
|
91
|
+
config.sentry.environment
|
|
92
|
+
config.servers.first.host
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
`load!` returns a new instance every call (no caching). Sources are loaded
|
|
96
|
+
through `anyway_config` (YAML + ENV by default).
|
|
97
|
+
|
|
98
|
+
### Freezing
|
|
99
|
+
|
|
100
|
+
On `load!` (and `deep_freeze_values!`) the config freezes all of its contained
|
|
101
|
+
values — Arrays, Hashes, and scalars — so the loaded data is effectively
|
|
102
|
+
immutable. The `Config` instance itself and any nested `Config` objects are
|
|
103
|
+
**not** frozen, so RSpec stubs keep working:
|
|
104
|
+
|
|
105
|
+
```ruby
|
|
106
|
+
allow(AppConfig.sentry).to receive(:dsn).and_return("stubbed")
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
If a specific value class can't be frozen (e.g. it holds mutable state like a
|
|
110
|
+
cache client or logger), add it to `skip_freeze_classes` to exclude it from
|
|
111
|
+
the freeze walk (matched via `is_a?`, inherited by subclasses):
|
|
112
|
+
|
|
113
|
+
```ruby
|
|
114
|
+
class AppConfig < AnywayAppConfig::Config
|
|
115
|
+
self.skip_freeze_classes = [Logger, SomeCacheClient]
|
|
116
|
+
# ...
|
|
117
|
+
end
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Explicit config path
|
|
121
|
+
|
|
122
|
+
By default, `anyway_config` looks for `config/<config_name>.yml` (or whatever
|
|
123
|
+
`Anyway::Settings.default_config_path` resolves to). To point at a specific
|
|
124
|
+
YAML file, pass `config_path:` to `new`/`load!`:
|
|
125
|
+
|
|
126
|
+
```ruby
|
|
127
|
+
AppConfig.load!(config_path: "/etc/myapp/app_config.yml")
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
`config_path:` also forwards through `Singleton#load!`.
|
|
131
|
+
|
|
132
|
+
For a per-class default, set `explicit_config_path` on the class (inherited
|
|
133
|
+
by subclasses, overridden by a per-call `config_path:`). It accepts a `String`
|
|
134
|
+
or `Pathname` directly:
|
|
135
|
+
|
|
136
|
+
```ruby
|
|
137
|
+
class AppConfig < AnywayAppConfig::Config
|
|
138
|
+
self.explicit_config_path = Rails.root.join('config', 'app_config.yml')
|
|
139
|
+
# ...
|
|
140
|
+
end
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Or a `Proc` for cases where the path depends on runtime state (called every
|
|
144
|
+
time a new instance is built):
|
|
145
|
+
|
|
146
|
+
```ruby
|
|
147
|
+
class AppConfig < AnywayAppConfig::Config
|
|
148
|
+
self.explicit_config_path = -> { "/etc/myapp/#{ENV.fetch('APP_ENV')}.yml" }
|
|
149
|
+
# ...
|
|
150
|
+
end
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Anyway's other built-in mechanisms (`<CONFIG_NAME>_CONF` env var,
|
|
154
|
+
`Anyway::Settings.default_config_path` lambda) still work — `config_path:` and
|
|
155
|
+
`explicit_config_path` just give you a class-scoped, code-driven option.
|
|
156
|
+
|
|
157
|
+
### Choosing a YAML loader
|
|
158
|
+
|
|
159
|
+
By default the `:yml` loader decides whether your YAML is environment-keyed
|
|
160
|
+
(`development:` / `production:` sections) or flat by inspecting the file
|
|
161
|
+
content and global state. To make that explicit, the gem registers two extra
|
|
162
|
+
loaders you can select via `configuration_sources`:
|
|
163
|
+
|
|
164
|
+
- `:flat_yml` — always reads top-level keys, ignores environment sections.
|
|
165
|
+
- `:env_yml` — always reads the section matching
|
|
166
|
+
`Anyway::Settings.current_environment`; raises if it is not set.
|
|
167
|
+
|
|
168
|
+
```ruby
|
|
169
|
+
class Credentials < AnywayAppConfig::Config
|
|
170
|
+
config_name 'credentials'
|
|
171
|
+
self.configuration_sources = [:flat_yml, :env] # flat YAML + ENV overrides
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
class AppConfig < AnywayAppConfig::Config
|
|
175
|
+
config_name 'app_config'
|
|
176
|
+
self.configuration_sources = [:env_yml, :env] # env-keyed YAML + ENV overrides
|
|
177
|
+
end
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Pick one YAML loader per class — `:yml`, `:flat_yml`, and `:env_yml` all read
|
|
181
|
+
the same file, so listing more than one just loads it repeatedly. The default
|
|
182
|
+
`configuration_sources` is untouched, so classes that don't opt in keep using
|
|
183
|
+
`:yml`.
|
|
184
|
+
|
|
185
|
+
### Singleton mode
|
|
186
|
+
|
|
187
|
+
Include `AnywayAppConfig::Singleton` to get a class-level singleton with
|
|
188
|
+
class-level access to all instance methods:
|
|
189
|
+
|
|
190
|
+
```ruby
|
|
191
|
+
class AppConfig < AnywayAppConfig::Config
|
|
192
|
+
include AnywayAppConfig::Singleton
|
|
193
|
+
# ...
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
AppConfig.load! # values frozen, instance cached on the class
|
|
197
|
+
AppConfig.deploy_env # delegates to instance
|
|
198
|
+
AppConfig.sentry.environment # delegates to instance
|
|
199
|
+
AppConfig.instance # the cached instance
|
|
200
|
+
AppConfig.loaded? # true / false
|
|
201
|
+
|
|
202
|
+
AppConfig.load! # raises AnywayAppConfig::AlreadyLoadedError
|
|
203
|
+
AppConfig.foo # raises AnywayAppConfig::NotLoadedError if not loaded
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
The singleton is intentionally strict — there is no `reload!`. To re-read
|
|
207
|
+
config, restart the process.
|
|
208
|
+
|
|
209
|
+
> Note: class-level delegation goes through `method_missing`, so attribute
|
|
210
|
+
> names that clash with existing `Class` methods (`name`, `class`, `send`, …)
|
|
211
|
+
> are not delegated — pick non-clashing names.
|
|
212
|
+
|
|
213
|
+
### YAML and ENV
|
|
214
|
+
|
|
215
|
+
Loading is provided by `anyway_config`. A typical `config/app_config.yml`:
|
|
216
|
+
|
|
217
|
+
```yaml
|
|
218
|
+
development: &dev
|
|
219
|
+
deploy_env: "development"
|
|
220
|
+
|
|
221
|
+
sentry:
|
|
222
|
+
dsn: ""
|
|
223
|
+
environment: "development"
|
|
224
|
+
server_name: "denis-t.localhost"
|
|
225
|
+
tags:
|
|
226
|
+
custom: "tag"
|
|
227
|
+
|
|
228
|
+
prometheus:
|
|
229
|
+
enabled: false
|
|
230
|
+
host: "localhost"
|
|
231
|
+
port: 9394
|
|
232
|
+
|
|
233
|
+
test:
|
|
234
|
+
<<: *dev
|
|
235
|
+
deploy_env: "test"
|
|
236
|
+
|
|
237
|
+
production:
|
|
238
|
+
<<: *dev
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
ENV vars use the prefix declared via `env_prefix`, e.g. `APP_DEPLOY_ENV`,
|
|
242
|
+
`APP_SENTRY__ENVIRONMENT`. See the [anyway_config docs][anyway_config] for
|
|
243
|
+
the full source list and naming rules.
|
|
244
|
+
|
|
245
|
+
## Rails
|
|
246
|
+
|
|
247
|
+
There is no Railtie — Rails already exposes `Rails.configuration` as the
|
|
248
|
+
canonical place to hang application-wide settings, so wiring through it is
|
|
249
|
+
three lines. The pattern:
|
|
250
|
+
|
|
251
|
+
**1. Define the config class in `config/app_config.rb`:**
|
|
252
|
+
|
|
253
|
+
```ruby
|
|
254
|
+
require "anyway_app_config"
|
|
255
|
+
|
|
256
|
+
class AppConfig < AnywayAppConfig::Config
|
|
257
|
+
config_name "app_config"
|
|
258
|
+
env_prefix "APP"
|
|
259
|
+
|
|
260
|
+
attribute :deploy_env, type: :string, required: true
|
|
261
|
+
attribute :version, type: :string, default: "unknown"
|
|
262
|
+
|
|
263
|
+
attribute :sentry, required: true do
|
|
264
|
+
attribute :dsn, type: :string, default: ""
|
|
265
|
+
attribute :environment, type: :string, required: true
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
**2. (Optional) Add `config/app_config.yml`:**
|
|
271
|
+
|
|
272
|
+
```yaml
|
|
273
|
+
development:
|
|
274
|
+
deploy_env: "development"
|
|
275
|
+
sentry:
|
|
276
|
+
environment: "development"
|
|
277
|
+
|
|
278
|
+
production:
|
|
279
|
+
deploy_env: "production"
|
|
280
|
+
sentry:
|
|
281
|
+
environment: "production"
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
Any value can be overridden via ENV using the `env_prefix` (`APP` above).
|
|
285
|
+
Examples:
|
|
286
|
+
|
|
287
|
+
- `APP_DEPLOY_ENV=staging` → `AppConfig#deploy_env`
|
|
288
|
+
- `APP_SENTRY__DSN=https://...` → `AppConfig#sentry.dsn` (double underscore for nesting)
|
|
289
|
+
- `APP_VERSION=1.2.3` → `AppConfig#version`
|
|
290
|
+
|
|
291
|
+
ENV wins over YAML. See the [anyway_config docs][anyway_config] for the full
|
|
292
|
+
source list and naming rules.
|
|
293
|
+
|
|
294
|
+
**3. Load and assign in `config/application.rb`:**
|
|
295
|
+
|
|
296
|
+
```ruby
|
|
297
|
+
require_relative "app_config"
|
|
298
|
+
|
|
299
|
+
module MyApp
|
|
300
|
+
class Application < Rails::Application
|
|
301
|
+
config.app_config = AppConfig.load!
|
|
302
|
+
# ...
|
|
303
|
+
end
|
|
304
|
+
end
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
**4. Use it anywhere via `Rails.configuration`:**
|
|
308
|
+
|
|
309
|
+
```ruby
|
|
310
|
+
Rails.configuration.app_config.deploy_env
|
|
311
|
+
Rails.configuration.app_config.sentry.dsn
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
`AppConfig.load!` returns an instance with frozen values, so
|
|
315
|
+
`Rails.configuration.app_config` is safe to read from any thread. If you want class-level access
|
|
316
|
+
(`AppConfig.deploy_env`) instead, use [Singleton mode](#singleton-mode) and
|
|
317
|
+
just call `AppConfig.load!` in `config/application.rb` without assigning it
|
|
318
|
+
to `config.app_config`.
|
|
319
|
+
|
|
320
|
+
### Replacing `Rails.application.credentials` (unencrypted)
|
|
321
|
+
|
|
322
|
+
Rails 7.2+ encrypts `config/credentials.yml.enc` by default. If your secrets
|
|
323
|
+
are already injected by your deploy pipeline (Helm, Kubernetes secrets, CI/CD
|
|
324
|
+
vault, ENV) you don't need encryption-at-rest in the repo — and the encrypted
|
|
325
|
+
credentials flow becomes pure overhead. `anyway_app_config` is a drop-in
|
|
326
|
+
replacement: typed, required-checked, value-frozen, and ENV-overridable.
|
|
327
|
+
|
|
328
|
+
**1. Define the credentials class in `config/credentials.rb`:**
|
|
329
|
+
|
|
330
|
+
```ruby
|
|
331
|
+
require "anyway_app_config"
|
|
332
|
+
|
|
333
|
+
class Credentials < AnywayAppConfig::Config
|
|
334
|
+
config_name "credentials"
|
|
335
|
+
env_prefix "" # no prefix — read raw ENV like SECRET_KEY_BASE, DATABASE_PASSWORD
|
|
336
|
+
self.configuration_sources = [:yml, :env] # see note below
|
|
337
|
+
|
|
338
|
+
attribute :secret_key_base, type: :string, required: true
|
|
339
|
+
attribute :database_password, type: :string, required: true
|
|
340
|
+
attribute :aws_access_key_id, type: :string, default: ""
|
|
341
|
+
attribute :aws_secret_access_key, type: :string, default: ""
|
|
342
|
+
end
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
> **Pin `configuration_sources` explicitly.** Under Rails, `anyway_config`
|
|
346
|
+
> registers a `:credentials` loader that reads from
|
|
347
|
+
> `Rails.application.credentials` — exactly the thing you're trying to
|
|
348
|
+
> replace. If you don't override `configuration_sources`, your shiny new
|
|
349
|
+
> `Credentials` class will silently merge values back in from the encrypted
|
|
350
|
+
> credentials file (or fail to boot if `RAILS_MASTER_KEY` is missing).
|
|
351
|
+
> Setting `self.configuration_sources = [:yml, :env]` keeps loading limited
|
|
352
|
+
> to YAML + ENV and removes the dependency on the Rails credentials loader.
|
|
353
|
+
|
|
354
|
+
An empty `env_prefix` matches ENV var names directly against attribute names
|
|
355
|
+
(uppercased), so `SECRET_KEY_BASE` populates `:secret_key_base`. Useful for
|
|
356
|
+
secrets that have well-known unprefixed names — `SECRET_KEY_BASE`,
|
|
357
|
+
`DATABASE_URL`, `RAILS_MASTER_KEY`. Other ENV vars are simply ignored unless
|
|
358
|
+
they match a declared attribute. **Caveat:** pick attribute names carefully
|
|
359
|
+
— if you declare `attribute :path` or `:home` with empty prefix, you'll pick
|
|
360
|
+
up `PATH`/`HOME` from the shell. When in doubt, keep a prefix.
|
|
361
|
+
|
|
362
|
+
**2. Add `config/credentials.yml`** (gitignore it, or commit only the dev/test
|
|
363
|
+
branches and inject production values via ENV):
|
|
364
|
+
|
|
365
|
+
```yaml
|
|
366
|
+
development:
|
|
367
|
+
secret_key_base: "dev-only-not-a-real-secret"
|
|
368
|
+
database_password: "postgres"
|
|
369
|
+
|
|
370
|
+
test:
|
|
371
|
+
secret_key_base: "test-only-not-a-real-secret"
|
|
372
|
+
database_password: "postgres"
|
|
373
|
+
|
|
374
|
+
production:
|
|
375
|
+
# Leave empty here and inject via ENV (SECRET_KEY_BASE=..., etc) at deploy time.
|
|
376
|
+
secret_key_base: ""
|
|
377
|
+
database_password: ""
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
**3. Wire it in `config/application.rb`:**
|
|
381
|
+
|
|
382
|
+
```ruby
|
|
383
|
+
require_relative "credentials"
|
|
384
|
+
|
|
385
|
+
module MyApp
|
|
386
|
+
class Application < Rails::Application
|
|
387
|
+
config.load_defaults 8.1
|
|
388
|
+
|
|
389
|
+
Rails.application.credentials = Credentials.load!
|
|
390
|
+
# Rails 7.2+ generates a dynamic secret_key_base in dev/test when one
|
|
391
|
+
# is not set. Pin it from credentials so it stays stable across boots.
|
|
392
|
+
# See Rails::Application::Configuration#generate_local_secret?
|
|
393
|
+
config.secret_key_base = Rails.application.credentials.secret_key_base
|
|
394
|
+
end
|
|
395
|
+
end
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
Assigning directly inside the class body works because `Rails.application` is
|
|
399
|
+
already available by the time `config/application.rb` is evaluated. If you
|
|
400
|
+
need the assignment deferred (e.g. credentials depend on something set up by
|
|
401
|
+
an initializer or another `before_configuration` hook), wrap it in
|
|
402
|
+
`config.before_configuration { ... }` instead.
|
|
403
|
+
|
|
404
|
+
**4. Use it like the standard credentials object:**
|
|
405
|
+
|
|
406
|
+
```ruby
|
|
407
|
+
Rails.application.credentials.secret_key_base
|
|
408
|
+
Rails.application.credentials.database_password
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
ENV overrides use raw names because of the empty `env_prefix`:
|
|
412
|
+
`SECRET_KEY_BASE=...`, `DATABASE_PASSWORD=...`.
|
|
413
|
+
|
|
414
|
+
> **Trade-off:** you lose encryption-at-rest. Only do this if `credentials.yml`
|
|
415
|
+
> is gitignored or contains placeholders only, with real values injected via
|
|
416
|
+
> ENV at deploy time. The win is that secrets become typed, required-checked,
|
|
417
|
+
> and centrally declared — instead of an opaque `EncryptedConfiguration` blob
|
|
418
|
+
> that silently returns `nil` for typos.
|
|
419
|
+
|
|
420
|
+
### Inline form: `AnywayAppConfig.build`
|
|
421
|
+
|
|
422
|
+
For small Rails apps where dedicated `config/app_config.rb` and
|
|
423
|
+
`config/credentials.rb` files feel like overkill, declare configs inline in
|
|
424
|
+
`config/application.rb` with `AnywayAppConfig.build`:
|
|
425
|
+
|
|
426
|
+
```ruby
|
|
427
|
+
module MyApp
|
|
428
|
+
class Application < Rails::Application
|
|
429
|
+
Rails.application.credentials = AnywayAppConfig.build(load: true) do
|
|
430
|
+
config_name "credentials"
|
|
431
|
+
env_prefix "" # no prefix — read raw ENV like SECRET_KEY_BASE
|
|
432
|
+
|
|
433
|
+
attribute :secret_key_base, type: :string, required: true
|
|
434
|
+
attribute :database_password, type: :string, required: true
|
|
435
|
+
attribute :aws_access_key_id, type: :string, default: ""
|
|
436
|
+
attribute :aws_secret_access_key, type: :string, default: ""
|
|
437
|
+
end
|
|
438
|
+
config.secret_key_base = Rails.application.credentials.secret_key_base
|
|
439
|
+
|
|
440
|
+
config.app_config = AnywayAppConfig.build(load: true) do
|
|
441
|
+
config_name "app_config"
|
|
442
|
+
env_prefix "APP"
|
|
443
|
+
|
|
444
|
+
attribute :deploy_env, type: :string, required: true
|
|
445
|
+
attribute :version, type: :string, default: "unknown"
|
|
446
|
+
|
|
447
|
+
attribute :sentry, required: true do
|
|
448
|
+
attribute :dsn, type: :string, default: ""
|
|
449
|
+
attribute :environment, type: :string, required: true
|
|
450
|
+
end
|
|
451
|
+
end
|
|
452
|
+
end
|
|
453
|
+
end
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
`AnywayAppConfig.build(&block)` returns an anonymous `AnywayAppConfig::Config`
|
|
457
|
+
subclass; `build(load: true, &block)` calls `load!` and returns the loaded
|
|
458
|
+
instance (with frozen values). The block is `class_eval`'d on the new subclass, so `config_name`,
|
|
459
|
+
`env_prefix`, and `attribute` are available exactly as in a named class.
|
|
460
|
+
|
|
461
|
+
`config_name` is **mandatory** — anonymous classes have no name, so
|
|
462
|
+
`anyway_config` can't infer it. Otherwise YAML loading and ENV nesting work
|
|
463
|
+
identically (so `config/credentials.yml`, `config/app_config.yml`, and
|
|
464
|
+
`APP_SENTRY__DSN` all behave the same as the file-based form).
|
|
465
|
+
|
|
466
|
+
#### Disabling ENV loading entirely
|
|
467
|
+
|
|
468
|
+
If you want a config to read **only** from YAML and ignore the environment
|
|
469
|
+
(e.g. for credentials that must never leak in via stray ENV vars, or to make
|
|
470
|
+
behavior deterministic in tests), restrict `configuration_sources` on the
|
|
471
|
+
class to the YAML loader:
|
|
472
|
+
|
|
473
|
+
```ruby
|
|
474
|
+
class Credentials < AnywayAppConfig::Config
|
|
475
|
+
config_name "credentials"
|
|
476
|
+
self.configuration_sources = [:yml] # YAML only — ignore ENV, ignore Rails secrets/credentials loaders
|
|
477
|
+
|
|
478
|
+
attribute :secret_key_base, type: :string, required: true
|
|
479
|
+
attribute :database_password, type: :string, required: true
|
|
480
|
+
end
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
`configuration_sources` is an array of loader IDs registered on
|
|
484
|
+
`Anyway.loaders` (`:yml`, `:env`, `:credentials`, etc — Rails adds a few).
|
|
485
|
+
Only listed loaders run for this class. With just `[:yml]`, ENV variables
|
|
486
|
+
have no effect on the values, regardless of `env_prefix`.
|
|
487
|
+
|
|
488
|
+
## Development
|
|
489
|
+
|
|
490
|
+
```
|
|
491
|
+
bundle install
|
|
492
|
+
bundle exec rspec
|
|
493
|
+
bundle exec rubocop
|
|
494
|
+
```
|
|
495
|
+
|
|
496
|
+
## Contributing
|
|
497
|
+
|
|
498
|
+
Bug reports and pull requests are welcome on GitHub at
|
|
499
|
+
<https://github.com/senid231/anyway_app_config>.
|
|
500
|
+
|
|
501
|
+
## License
|
|
502
|
+
|
|
503
|
+
MIT. See [LICENSE.txt](LICENSE.txt).
|
|
504
|
+
|
|
505
|
+
[anyway_config]: https://github.com/palkan/anyway_config
|
data/Rakefile
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'anyway_config'
|
|
4
|
+
require 'active_support/core_ext/class/attribute'
|
|
5
|
+
require 'active_support/core_ext/hash/keys'
|
|
6
|
+
require 'active_support/core_ext/string/inflections'
|
|
7
|
+
|
|
8
|
+
module AnywayAppConfig
|
|
9
|
+
class NotLoadedError < StandardError; end
|
|
10
|
+
|
|
11
|
+
class Config < ::Anyway::Config
|
|
12
|
+
BaseNestedCfgClass = Class.new(self)
|
|
13
|
+
|
|
14
|
+
class_attribute :nested_config_class, instance_accessor: false, default: BaseNestedCfgClass
|
|
15
|
+
class_attribute :skip_freeze_classes, instance_accessor: false, default: [].freeze
|
|
16
|
+
class_attribute :explicit_config_path, instance_accessor: false
|
|
17
|
+
|
|
18
|
+
class << self
|
|
19
|
+
def attribute(name, type: nil, array: false, default: nil, required: false, &block)
|
|
20
|
+
if block
|
|
21
|
+
raise ArgumentError, "nested attribute #{name} does not support type" unless type.nil?
|
|
22
|
+
raise ArgumentError, "nested attribute #{name} does not support default" unless default.nil?
|
|
23
|
+
|
|
24
|
+
attr_nested(name, array: array, required: required, &block)
|
|
25
|
+
else
|
|
26
|
+
attr_value(name, type: type, array: array, default: default, required: required)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def attr_value(name, type:, array: false, default: nil, required: false)
|
|
31
|
+
default_val = default.nil? && array ? [] : default
|
|
32
|
+
attr_config(name => default_val)
|
|
33
|
+
coerce_types(name => { type: type, array: array })
|
|
34
|
+
self.required(name) if required
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def attr_nested(name, array: false, required: false, &block)
|
|
38
|
+
nested_klass = Class.new(nested_config_class)
|
|
39
|
+
const_set("#{name.to_s.classify}Cfg", nested_klass)
|
|
40
|
+
# nested_klass.config_name represents path to config struct in main config file
|
|
41
|
+
if self < nested_config_class
|
|
42
|
+
# nested inside another nested config
|
|
43
|
+
nested_klass.config_name :"#{config_name}.#{name}"
|
|
44
|
+
else
|
|
45
|
+
# nested in main config
|
|
46
|
+
nested_klass.config_name name.to_sym
|
|
47
|
+
end
|
|
48
|
+
nested_klass.configuration_sources = []
|
|
49
|
+
nested_klass.class_eval(&block) if block
|
|
50
|
+
|
|
51
|
+
if array
|
|
52
|
+
attr_config(name => [])
|
|
53
|
+
caster = ->(v) { nested_klass.new(v) }
|
|
54
|
+
coerce_types(name => { type: caster, array: true })
|
|
55
|
+
else
|
|
56
|
+
attr_config(name => {})
|
|
57
|
+
coerce_types(name => { config: nested_klass })
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
self.required(name) if required
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def load!(*, **)
|
|
64
|
+
new(*, **).tap(&:deep_freeze_values!)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def type_registry
|
|
68
|
+
return @type_registry if instance_variable_defined?(:@type_registry)
|
|
69
|
+
|
|
70
|
+
@type_registry =
|
|
71
|
+
if superclass < AnywayAppConfig::Config
|
|
72
|
+
superclass.type_registry.dup
|
|
73
|
+
else
|
|
74
|
+
::Anyway::TypeRegistry.default.dup.tap do |r|
|
|
75
|
+
r.accept(:hash) do |v|
|
|
76
|
+
raise ArgumentError, "expected Hash, got #{v.class}" unless v.is_a?(::Hash)
|
|
77
|
+
|
|
78
|
+
v
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def type_caster
|
|
85
|
+
@type_caster ||=
|
|
86
|
+
if coercion_mapping.empty?
|
|
87
|
+
fallback_type_caster
|
|
88
|
+
else
|
|
89
|
+
::Anyway::TypeCaster.new(
|
|
90
|
+
coercion_mapping,
|
|
91
|
+
registry: type_registry,
|
|
92
|
+
fallback: fallback_type_caster
|
|
93
|
+
)
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def initialize(overrides = nil, config_path: nil, **kwargs)
|
|
99
|
+
@explicit_config_path = calc_explicit_config_path(config_path)
|
|
100
|
+
|
|
101
|
+
if overrides.nil? && !kwargs.empty?
|
|
102
|
+
overrides = kwargs
|
|
103
|
+
elsif !kwargs.empty?
|
|
104
|
+
raise ArgumentError, "unknown keywords: #{kwargs.keys.join(', ')}"
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
super(overrides)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def deep_freeze_values!
|
|
111
|
+
deep_freeze_value(values)
|
|
112
|
+
self
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
alias deep_freeze! deep_freeze_values!
|
|
116
|
+
|
|
117
|
+
def load(overrides = nil)
|
|
118
|
+
overrides = overrides.deep_stringify_keys if overrides.is_a?(::Hash)
|
|
119
|
+
super
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def resolve_config_path(name, env_prefix)
|
|
123
|
+
@explicit_config_path || super
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
private
|
|
127
|
+
|
|
128
|
+
def calc_explicit_config_path(config_path)
|
|
129
|
+
return config_path.to_s unless config_path.nil?
|
|
130
|
+
|
|
131
|
+
class_default = self.class.explicit_config_path
|
|
132
|
+
return if class_default.nil?
|
|
133
|
+
|
|
134
|
+
class_default = class_default.call if class_default.is_a?(Proc)
|
|
135
|
+
class_default&.to_s
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def deep_freeze_value(val)
|
|
139
|
+
return val if skip_freeze?(val)
|
|
140
|
+
|
|
141
|
+
case val
|
|
142
|
+
when AnywayAppConfig::Config
|
|
143
|
+
val.deep_freeze_values!
|
|
144
|
+
when Array
|
|
145
|
+
val.each { |item| deep_freeze_value(item) }
|
|
146
|
+
val.freeze
|
|
147
|
+
when Hash
|
|
148
|
+
val.each_value { |item| deep_freeze_value(item) }
|
|
149
|
+
val.freeze
|
|
150
|
+
else
|
|
151
|
+
val.freeze if val.respond_to?(:freeze) && !val.frozen?
|
|
152
|
+
end
|
|
153
|
+
val
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def skip_freeze?(val)
|
|
157
|
+
self.class.skip_freeze_classes.any? { |klass| val.is_a?(klass) }
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'anyway/loaders/yaml'
|
|
4
|
+
|
|
5
|
+
module AnywayAppConfig
|
|
6
|
+
module Loaders
|
|
7
|
+
# YAML loader that always reads the section matching
|
|
8
|
+
# `Anyway::Settings.current_environment`, regardless of file content or
|
|
9
|
+
# global detection. Raises if the current environment is not set.
|
|
10
|
+
class EnvYAML < ::Anyway::Loaders::YAML
|
|
11
|
+
def call(**)
|
|
12
|
+
if ::Anyway::Settings.current_environment.nil?
|
|
13
|
+
raise ArgumentError,
|
|
14
|
+
'Anyway::Settings.current_environment must be set to use the :env_yml loader'
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
super
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def environmental?(_parsed_yml)
|
|
23
|
+
true
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'anyway/loaders/yaml'
|
|
4
|
+
|
|
5
|
+
module AnywayAppConfig
|
|
6
|
+
module Loaders
|
|
7
|
+
# YAML loader that always treats the file as a flat key/value document,
|
|
8
|
+
# ignoring environment sections regardless of file content or global state.
|
|
9
|
+
class FlatYAML < ::Anyway::Loaders::YAML
|
|
10
|
+
private
|
|
11
|
+
|
|
12
|
+
def environmental?(_parsed_yml)
|
|
13
|
+
false
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AnywayAppConfig
|
|
4
|
+
class AlreadyLoadedError < StandardError; end
|
|
5
|
+
|
|
6
|
+
module Singleton
|
|
7
|
+
def self.included(base)
|
|
8
|
+
base.extend(ClassMethods)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
module ClassMethods
|
|
12
|
+
def load!(*, **)
|
|
13
|
+
if instance_variable_defined?(:@instance) && @instance
|
|
14
|
+
raise AlreadyLoadedError, "#{name || self} is already loaded"
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
@instance = new(*, **).tap(&:deep_freeze_values!)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def loaded?
|
|
21
|
+
instance_variable_defined?(:@instance) && !@instance.nil?
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def instance
|
|
25
|
+
return @instance if instance_variable_defined?(:@instance) && @instance
|
|
26
|
+
|
|
27
|
+
raise NotLoadedError, "#{name || self} is not loaded; call #{name || self}.load! first"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def respond_to_missing?(method_name, include_private = false)
|
|
31
|
+
(loaded? && @instance.respond_to?(method_name, include_private)) || super
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def method_missing(method_name, ...)
|
|
35
|
+
if instance.respond_to?(method_name)
|
|
36
|
+
instance.public_send(method_name, ...)
|
|
37
|
+
else
|
|
38
|
+
super
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'anyway_app_config/version'
|
|
4
|
+
require_relative 'anyway_app_config/config'
|
|
5
|
+
require_relative 'anyway_app_config/singleton'
|
|
6
|
+
require_relative 'anyway_app_config/loaders/flat_yaml'
|
|
7
|
+
require_relative 'anyway_app_config/loaders/env_yaml'
|
|
8
|
+
|
|
9
|
+
Anyway.loaders.append(:flat_yml, AnywayAppConfig::Loaders::FlatYAML)
|
|
10
|
+
Anyway.loaders.append(:env_yml, AnywayAppConfig::Loaders::EnvYAML)
|
|
11
|
+
|
|
12
|
+
module AnywayAppConfig
|
|
13
|
+
class Error < StandardError; end
|
|
14
|
+
|
|
15
|
+
# Builds an anonymous AnywayAppConfig::Config subclass from a block,
|
|
16
|
+
# for use without a separate file. The block is class_eval'd on the new
|
|
17
|
+
# subclass, so `config_name`, `env_prefix`, and `attribute` are available.
|
|
18
|
+
#
|
|
19
|
+
# Returns the class by default; with `load: true` returns a frozen instance.
|
|
20
|
+
# Anonymous classes have no name, so `config_name` is mandatory in the block.
|
|
21
|
+
def self.build(load: false, &block)
|
|
22
|
+
raise ArgumentError, 'AnywayAppConfig.build requires a block' unless block
|
|
23
|
+
|
|
24
|
+
klass = Class.new(Config)
|
|
25
|
+
klass.class_eval(&block)
|
|
26
|
+
load ? klass.load! : klass
|
|
27
|
+
end
|
|
28
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: anyway_app_config
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.3.1
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Denis Talakevich
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: activesupport
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '6.1'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '6.1'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: anyway_config
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '2.0'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '2.0'
|
|
40
|
+
description: |
|
|
41
|
+
AnywayAppConfig adds a small DSL on top of anyway_config for describing
|
|
42
|
+
application configs with typed attributes, defaults, required fields and
|
|
43
|
+
nested objects (single or array). Configs can be used as plain instances
|
|
44
|
+
or as singletons, and load values from YAML and ENV via anyway_config.
|
|
45
|
+
email:
|
|
46
|
+
- senid231@gmail.com
|
|
47
|
+
executables: []
|
|
48
|
+
extensions: []
|
|
49
|
+
extra_rdoc_files: []
|
|
50
|
+
files:
|
|
51
|
+
- CODE_OF_CONDUCT.md
|
|
52
|
+
- LICENSE.txt
|
|
53
|
+
- README.md
|
|
54
|
+
- Rakefile
|
|
55
|
+
- lib/anyway_app_config.rb
|
|
56
|
+
- lib/anyway_app_config/config.rb
|
|
57
|
+
- lib/anyway_app_config/loaders/env_yaml.rb
|
|
58
|
+
- lib/anyway_app_config/loaders/flat_yaml.rb
|
|
59
|
+
- lib/anyway_app_config/singleton.rb
|
|
60
|
+
- lib/anyway_app_config/version.rb
|
|
61
|
+
homepage: https://github.com/senid231/anyway_app_config
|
|
62
|
+
licenses:
|
|
63
|
+
- MIT
|
|
64
|
+
metadata:
|
|
65
|
+
homepage_uri: https://github.com/senid231/anyway_app_config
|
|
66
|
+
source_code_uri: https://github.com/senid231/anyway_app_config
|
|
67
|
+
changelog_uri: https://github.com/senid231/anyway_app_config/blob/master/CHANGELOG.md
|
|
68
|
+
rubygems_mfa_required: 'true'
|
|
69
|
+
rdoc_options: []
|
|
70
|
+
require_paths:
|
|
71
|
+
- lib
|
|
72
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
73
|
+
requirements:
|
|
74
|
+
- - ">="
|
|
75
|
+
- !ruby/object:Gem::Version
|
|
76
|
+
version: 3.2.0
|
|
77
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - ">="
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: '0'
|
|
82
|
+
requirements: []
|
|
83
|
+
rubygems_version: 3.6.9
|
|
84
|
+
specification_version: 4
|
|
85
|
+
summary: Schema-driven application config built on top of anyway_config.
|
|
86
|
+
test_files: []
|