anyway_config 2.8.1 → 2.9.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 +4 -4
- data/CHANGELOG.md +22 -0
- data/README.md +61 -2
- data/lib/.rbnext/2.7/anyway/loaders/yaml.rb +2 -0
- data/lib/.rbnext/2.7/anyway/settings.rb +12 -0
- data/lib/.rbnext/3.0/anyway/config.rb +51 -3
- data/lib/.rbnext/3.0/anyway/loaders/base.rb +5 -1
- data/lib/.rbnext/3.0/anyway/loaders/ejson.rb +94 -0
- data/lib/.rbnext/3.0/anyway/loaders/yaml.rb +2 -0
- data/lib/.rbnext/3.0/anyway/rails/loaders/credentials.rb +64 -0
- data/lib/.rbnext/3.0/anyway/rails/loaders/secrets.rb +42 -0
- data/lib/.rbnext/3.1/anyway/config.rb +51 -3
- data/lib/.rbnext/3.1/anyway/loaders/base.rb +5 -1
- data/lib/.rbnext/3.2/anyway/config.rb +51 -3
- data/lib/.rbnext/3.2/anyway/loaders/base.rb +5 -1
- data/lib/.rbnext/3.4/anyway/config.rb +51 -3
- data/lib/anyway/config.rb +51 -3
- data/lib/anyway/loaders/base.rb +5 -1
- data/lib/anyway/loaders/ejson.rb +6 -1
- data/lib/anyway/loaders/yaml.rb +2 -0
- data/lib/anyway/rails/loaders/credentials.rb +2 -0
- data/lib/anyway/rails/loaders/secrets.rb +2 -0
- data/lib/anyway/rails.rb +1 -1
- data/lib/anyway/settings.rb +12 -0
- data/lib/anyway/version.rb +1 -1
- data/sig/anyway_config.rbs +12 -1
- data/skills/anyway-config/SKILL.md +223 -0
- data/skills/anyway-config/references/docs.md +30 -0
- data/skills/anyway-config/references/organizing.md +341 -0
- data/skills/anyway-config/references/testing.md +139 -0
- metadata +11 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5b4393c06a6078956ade1727f37879c8a38003806b59c8c5a7e9c330708a42fd
|
|
4
|
+
data.tar.gz: 98e3e7d9c0a50e2a18f98a917818ab4fa088314471102d6d6f9e67c08b059670
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c9a771b5fd116be880f4aa3271a5389c6023500452eb8ea98ff8cefc5452ad0f24e3eab97bc02d39daab5231a070e4441efed4a285331d64f7f329073bfbab38
|
|
7
|
+
data.tar.gz: 8af2c0fdc71913929d78ef07f84db3b440cfa0a324834958f3bce0b77dd90e5a9c6995a61c3ba5637008171f6e02d7c7a429f46470b2d5a15984bcc524698737
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,27 @@
|
|
|
2
2
|
|
|
3
3
|
## master
|
|
4
4
|
|
|
5
|
+
## 2.9.0 (2026-10-21)
|
|
6
|
+
|
|
7
|
+
- Add an agent skill (`skills/anyway-config`) with guidance on organizing configuration in Rails apps. ([@palkan][])
|
|
8
|
+
|
|
9
|
+
The skill follows the [skills.sh](https://www.skills.sh) layout and installs automatically with [Rails Hyperdrive](https://github.com/rails-hyperdrive/rails-hyperdrive).
|
|
10
|
+
|
|
11
|
+
## 2.9.0 (2026-10-20)
|
|
12
|
+
|
|
13
|
+
- Add a new `enforce_config_schema` to warn or raise on unknown attributes. ([@palkan][])
|
|
14
|
+
|
|
15
|
+
When the schema is enforced, the config attributes defined in the key-based sources (YAML, credentials, etc.) must correspond to `attr_config` names in the corresponding class. Providing an unknown attribute (e.g., a typo) would raise or print a warning depending on the parameter value:
|
|
16
|
+
|
|
17
|
+
```ruby
|
|
18
|
+
Anyway::Settings.enforce_config_schema = true # or :warn
|
|
19
|
+
|
|
20
|
+
# in Rails
|
|
21
|
+
config.anyway_config.enforce_config_schema = true
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
- EJSON loader now ignores underscore-prefixed (unencrypted) keys. ([@palkan][])
|
|
25
|
+
|
|
5
26
|
## 2.8.1 (2026-07-22)
|
|
6
27
|
|
|
7
28
|
- Fix default loaders order to keep ENV the last one (=higher precedence). ([@palkan][])
|
|
@@ -9,6 +30,7 @@
|
|
|
9
30
|
- Guard against a missing `PATH` environment variable in `Anyway::Utils.which`. ([@SAY-5][])
|
|
10
31
|
|
|
11
32
|
- Do not require optional loader dependencies to speed up loading. ([@ardecvz][])
|
|
33
|
+
- Use `to_s` on values that support it during validation for required attributes. This provides more consistent behaviour for the `:uri` coercer, which will otherwise produce a `#<URI::Generic >` rather than a validation failure. ([@phil-janeapp][])
|
|
12
34
|
|
|
13
35
|
## 2.8.0 (2026-06-24)
|
|
14
36
|
|
data/README.md
CHANGED
|
@@ -22,8 +22,9 @@ For application developers, Anyway Config could be useful to:
|
|
|
22
22
|
**NOTE:** this readme shows documentation for 2.x version.
|
|
23
23
|
For version 1.x see the [1-4-stable branch](https://github.com/palkan/anyway_config/tree/1-4-stable).
|
|
24
24
|
|
|
25
|
-
<
|
|
26
|
-
|
|
25
|
+
<br/>
|
|
26
|
+
|
|
27
|
+
<img src="https://cdn.evilmartians.com/badges/logo-no-label.svg" alt="Evil Martians logo" width="22" height="16" /> <b>Anyway Config</b> is built by <b><a href="https://evilmartians.com/">Evil Martians</a></b>, an American design and engineering consultancy for <b>developer tools, AI, and cybersecurity startups</b>.
|
|
27
28
|
|
|
28
29
|
## Links
|
|
29
30
|
|
|
@@ -33,10 +34,12 @@ For version 1.x see the [1-4-stable branch](https://github.com/palkan/anyway_con
|
|
|
33
34
|
|
|
34
35
|
- [Main concepts](#main-concepts)
|
|
35
36
|
- [Installation](#installation)
|
|
37
|
+
- [Adding agent skills](#adding-agent-skills)
|
|
36
38
|
- [Usage](#usage)
|
|
37
39
|
- [Configuration classes](#configuration-classes)
|
|
38
40
|
- [Dynamic configuration](#dynamic-configuration)
|
|
39
41
|
- [Validation & Callbacks](#validation-and-callbacks)
|
|
42
|
+
- [Enforcing config schema](#enforcing-config-schema)
|
|
40
43
|
- [Using with Rails applications](#using-with-rails)
|
|
41
44
|
- [Data population](#data-population)
|
|
42
45
|
- [Organizing configs](#organizing-configs)
|
|
@@ -103,6 +106,18 @@ Or adding to your project:
|
|
|
103
106
|
gem "anyway_config", "~> 2.0"
|
|
104
107
|
```
|
|
105
108
|
|
|
109
|
+
### Adding agent skills
|
|
110
|
+
|
|
111
|
+
The gem ships a skill for coding agents in `skills/anyway-config`. It guides an agent when it adds or changes configuration in a Rails app: where to put a config class, which source to use for a value, how to replace direct `ENV` reads, and how to test configs. It follows the [skills.sh](https://www.skills.sh) layout, so it works with any agent that reads `SKILL.md`.
|
|
112
|
+
|
|
113
|
+
With [Rails Hyperdrive](https://github.com/rails-hyperdrive/rails-hyperdrive), the skill installs automatically: run `bin/rails hyperdrive:init` the first time, then `bin/rails hyperdrive:sync`.
|
|
114
|
+
|
|
115
|
+
Without it, install the skill from the repository with the [skills.sh](https://www.skills.sh) CLI, or copy `skills/anyway-config` into your agent's skills directory:
|
|
116
|
+
|
|
117
|
+
```sh
|
|
118
|
+
npx skills add palkan/anyway_config
|
|
119
|
+
```
|
|
120
|
+
|
|
106
121
|
### Supported Ruby versions
|
|
107
122
|
|
|
108
123
|
- Ruby (MRI) >= 2.7.0
|
|
@@ -345,6 +360,45 @@ class MyConfig < Anyway::Config
|
|
|
345
360
|
end
|
|
346
361
|
```
|
|
347
362
|
|
|
363
|
+
### Enforcing config schema
|
|
364
|
+
|
|
365
|
+
By default, Anyway Config ignores keys that don't match any of the config attributes. Thus, a typo in a YAML file (e.g., `hots` instead of `host`) goes unnoticed. You can make Anyway Config check for unknown attributes via the `enforce_config_schema` setting:
|
|
366
|
+
|
|
367
|
+
```ruby
|
|
368
|
+
# Raise Anyway::Config::UnknownAttributesError
|
|
369
|
+
Anyway::Settings.enforce_config_schema = true
|
|
370
|
+
|
|
371
|
+
# Print a warning instead
|
|
372
|
+
Anyway::Settings.enforce_config_schema = :warn
|
|
373
|
+
|
|
374
|
+
# Or in Rails
|
|
375
|
+
config.anyway_config.enforce_config_schema = true
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
The schema is only enforced for key-based (_schemaful_) sources: YAML files, Rails credentials and secrets, and EJSON. Environment-based sources (ENV, Doppler) are never checked, since env prefixes can easily collide with unrelated variables (e.g., `AWS_PROFILE` for the `aws` config).
|
|
379
|
+
|
|
380
|
+
Only top-level keys are checked; nested hashes are not validated.
|
|
381
|
+
|
|
382
|
+
#### Allowing unknown attributes for some configs
|
|
383
|
+
|
|
384
|
+
You can use the `rescue_unknown_attributes_with` configuration parameter to ignore specific attributes. It's called for every unknown attribute with the config object and the attribute name; return a truthy value to skip (allow) it:
|
|
385
|
+
|
|
386
|
+
```ruby
|
|
387
|
+
Anyway::Settings.rescue_unknown_attributes_with = ->(config, key) do
|
|
388
|
+
config.config_name == "app"
|
|
389
|
+
end
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
This is particularly useful when you have multiple config classes associated with the same configurable entity (say, a service provider).
|
|
393
|
+
|
|
394
|
+
#### Unknown explicit values
|
|
395
|
+
|
|
396
|
+
Unknown attributes passed explicitly (`MyConfig.new(hots: "localhost")`) will raise disregards the `enforce_config_schema` setting in the future. Currently, they follow the `enforce_config_schema` configuration but we recommend to opt-in for the stricter future behaviour:
|
|
397
|
+
|
|
398
|
+
```ruby
|
|
399
|
+
Anyway::Settings.future.use :reject_unknown_overrides
|
|
400
|
+
```
|
|
401
|
+
|
|
348
402
|
## Using with Rails
|
|
349
403
|
|
|
350
404
|
**NOTE:** version 2.x supports Rails >= 6.0; for older versions use version 1.x of the gem.
|
|
@@ -825,6 +879,8 @@ class MyConfig < Anyway::Config
|
|
|
825
879
|
end
|
|
826
880
|
```
|
|
827
881
|
|
|
882
|
+
Underscore-prefixed keys (which are not encrypted by EJSON, e.g., `_public_key`) are ignored.
|
|
883
|
+
|
|
828
884
|
**NOTE:** You can opt-out from EJSON loader by specifying the `ANYWAY_CONFIG_DISABLE_EJSON=true` env var (in case you have the `ejson` executable in your PATH, but don't want to use it with Anyway Config).
|
|
829
885
|
|
|
830
886
|
### Custom loaders
|
|
@@ -865,6 +921,9 @@ class ChamberConfigLoader < Base
|
|
|
865
921
|
warn "Couldn't decrypt Chamber settings: #{e.message}"
|
|
866
922
|
{}
|
|
867
923
|
end
|
|
924
|
+
|
|
925
|
+
# Report unknown keys if enforce_config_schema is enabled
|
|
926
|
+
def schemaful? = true
|
|
868
927
|
end
|
|
869
928
|
|
|
870
929
|
# Don't forget to register it
|
|
@@ -38,6 +38,7 @@ module Anyway
|
|
|
38
38
|
end
|
|
39
39
|
|
|
40
40
|
setting :unwrap_known_environments, true
|
|
41
|
+
setting :reject_unknown_overrides, true
|
|
41
42
|
|
|
42
43
|
private
|
|
43
44
|
|
|
@@ -55,6 +56,14 @@ module Anyway
|
|
|
55
56
|
# Suppress required validations for CI/CD pipelines
|
|
56
57
|
attr_accessor :suppress_required_validations
|
|
57
58
|
|
|
59
|
+
# Whether to check for unknown attributes in schemaful (key-based) sources:
|
|
60
|
+
# false — ignore unknown attributes, true — raise, :warn — print a warning
|
|
61
|
+
attr_accessor :enforce_config_schema
|
|
62
|
+
|
|
63
|
+
# A callable (config, key) -> bool to ignore specific unknown attributes
|
|
64
|
+
# (e.g., when the same source is shared by multiple configs)
|
|
65
|
+
attr_accessor :rescue_unknown_attributes_with
|
|
66
|
+
|
|
58
67
|
# A proc returning a path to YML config file given the config name
|
|
59
68
|
attr_reader :default_config_path
|
|
60
69
|
|
|
@@ -113,5 +122,8 @@ module Anyway
|
|
|
113
122
|
|
|
114
123
|
# By default, use ANYWAY_SUPPRESS_VALIDATIONS
|
|
115
124
|
self.suppress_required_validations = %w[1 t true y yes].include?(ENV["ANYWAY_SUPPRESS_VALIDATIONS"])
|
|
125
|
+
|
|
126
|
+
# Unknown attributes are ignored by default
|
|
127
|
+
self.enforce_config_schema = false
|
|
116
128
|
end
|
|
117
129
|
end
|
|
@@ -33,6 +33,7 @@ module Anyway # :nodoc:
|
|
|
33
33
|
deconstruct_keys
|
|
34
34
|
dig
|
|
35
35
|
dup
|
|
36
|
+
format_unknown_attribute
|
|
36
37
|
initialize
|
|
37
38
|
load
|
|
38
39
|
load_from_sources
|
|
@@ -52,6 +53,8 @@ module Anyway # :nodoc:
|
|
|
52
53
|
|
|
53
54
|
class ValidationError < Error; end
|
|
54
55
|
|
|
56
|
+
class UnknownAttributesError < ValidationError; end
|
|
57
|
+
|
|
55
58
|
include OptparseConfig
|
|
56
59
|
include DynamicConfig
|
|
57
60
|
|
|
@@ -374,6 +377,8 @@ module Anyway # :nodoc:
|
|
|
374
377
|
)
|
|
375
378
|
|
|
376
379
|
if overrides
|
|
380
|
+
reject_unknown_config_attributes!(overrides, source: :overrides, strict: Settings.future.reject_unknown_overrides == true)
|
|
381
|
+
|
|
377
382
|
Tracing.trace!(:load) { overrides }
|
|
378
383
|
|
|
379
384
|
Utils.deep_merge!(base_config, overrides)
|
|
@@ -400,10 +405,16 @@ module Anyway # :nodoc:
|
|
|
400
405
|
def load_from_sources(base_config, **opts)
|
|
401
406
|
source_filter = self.class.configuration_sources
|
|
402
407
|
|
|
403
|
-
Anyway.loaders.each do |(id,
|
|
408
|
+
Anyway.loaders.each do |(id, loader_or_proc)|
|
|
404
409
|
next if source_filter && !source_filter.include?(id)
|
|
405
|
-
|
|
410
|
+
|
|
411
|
+
loader = (loader_or_proc.is_a?(Class) && loader_or_proc <= Loaders::Base) ? loader_or_proc.new(**opts) : loader_or_proc
|
|
412
|
+
loader_data = loader.call(**opts)
|
|
413
|
+
reject_unknown_config_attributes!(loader_data, source: id) if loader.is_a?(Loaders::Base) && loader.schemaful?
|
|
414
|
+
|
|
415
|
+
Utils.deep_merge!(base_config, loader_data)
|
|
406
416
|
end
|
|
417
|
+
|
|
407
418
|
base_config
|
|
408
419
|
end
|
|
409
420
|
|
|
@@ -460,13 +471,50 @@ module Anyway # :nodoc:
|
|
|
460
471
|
|
|
461
472
|
self.class.required_attributes.select do |name|
|
|
462
473
|
val = values.dig(*name.to_s.split(".").map(&:to_sym))
|
|
463
|
-
val.nil? || (val.
|
|
474
|
+
val.nil? || (val.respond_to?(:to_s) && val.to_s.empty?)
|
|
464
475
|
end.then do |missing|
|
|
465
476
|
next if missing.empty?
|
|
466
477
|
raise_validation_error "The following config parameters for `#{self.class.name}(config_name: #{self.class.config_name})` are missing or empty: #{missing.join(", ")}"
|
|
467
478
|
end
|
|
468
479
|
end
|
|
469
480
|
|
|
481
|
+
def reject_unknown_config_attributes!(data, source:, strict: false)
|
|
482
|
+
return if data.nil? || data.empty?
|
|
483
|
+
|
|
484
|
+
mode = strict || Settings.enforce_config_schema
|
|
485
|
+
return unless mode
|
|
486
|
+
|
|
487
|
+
known = self.class.config_attributes.map(&:to_s)
|
|
488
|
+
unknown = data.keys.map(&:to_s) - known
|
|
489
|
+
|
|
490
|
+
unless strict
|
|
491
|
+
rescuer = Settings.rescue_unknown_attributes_with
|
|
492
|
+
unknown.reject! { |it| rescuer.call(self, it) } if rescuer
|
|
493
|
+
end
|
|
494
|
+
|
|
495
|
+
return if unknown.empty?
|
|
496
|
+
|
|
497
|
+
msg = "Unknown config attributes for `#{self.class.name || "<anonymous>"}(config_name: #{config_name})` " \
|
|
498
|
+
"provided by #{source}: #{unknown.map { |it| format_unknown_attribute(it, known) }.join(", ")}"
|
|
499
|
+
|
|
500
|
+
if strict
|
|
501
|
+
raise ArgumentError, msg
|
|
502
|
+
elsif mode == :warn
|
|
503
|
+
Kernel.warn msg
|
|
504
|
+
else
|
|
505
|
+
raise UnknownAttributesError, msg
|
|
506
|
+
end
|
|
507
|
+
end
|
|
508
|
+
|
|
509
|
+
def format_unknown_attribute(key, known)
|
|
510
|
+
return key unless defined?(::DidYouMean::SpellChecker)
|
|
511
|
+
|
|
512
|
+
suggestions = ::DidYouMean::SpellChecker.new(dictionary: known).correct(key)
|
|
513
|
+
return key if suggestions.empty?
|
|
514
|
+
|
|
515
|
+
"#{key} (did you mean: #{suggestions.join(", ")}?)"
|
|
516
|
+
end
|
|
517
|
+
|
|
470
518
|
def write_config_attr(key, val)
|
|
471
519
|
key = key.to_sym
|
|
472
520
|
return unless self.class.config_attributes.include?(key)
|
|
@@ -11,11 +11,15 @@ module Anyway
|
|
|
11
11
|
end
|
|
12
12
|
end
|
|
13
13
|
|
|
14
|
-
def initialize(local:)
|
|
14
|
+
def initialize(local: Anyway::Settings.use_local_files, **__kwrest__)
|
|
15
15
|
@local = local
|
|
16
16
|
end
|
|
17
17
|
|
|
18
18
|
def use_local?() ; @local == true; end
|
|
19
|
+
|
|
20
|
+
# Whether this loader supports strict schema (so we can warn or raise on
|
|
21
|
+
# unknown attributes)
|
|
22
|
+
def schemaful?() ; false; end
|
|
19
23
|
end
|
|
20
24
|
end
|
|
21
25
|
end
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "anyway/ejson_parser"
|
|
4
|
+
|
|
5
|
+
module Anyway
|
|
6
|
+
module Loaders
|
|
7
|
+
class EJSON < Base
|
|
8
|
+
class << self
|
|
9
|
+
attr_accessor :bin_path
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
self.bin_path = "ejson"
|
|
13
|
+
|
|
14
|
+
def call(name:, ejson_namespace: name, ejson_parser: Anyway::EJSONParser.new(EJSON.bin_path), **_options)
|
|
15
|
+
configs = []
|
|
16
|
+
|
|
17
|
+
rel_config_paths.each do |rel_config_path|
|
|
18
|
+
secrets_hash, rel_path =
|
|
19
|
+
extract_hash_from_rel_config_path(
|
|
20
|
+
ejson_parser: ejson_parser,
|
|
21
|
+
rel_config_path: rel_config_path
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
next unless secrets_hash
|
|
25
|
+
|
|
26
|
+
config_hash = if ejson_namespace
|
|
27
|
+
secrets_hash[ejson_namespace]
|
|
28
|
+
else
|
|
29
|
+
secrets_hash
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
next unless config_hash.is_a?(Hash)
|
|
33
|
+
|
|
34
|
+
# Underscore-prefixed keys are not encrypted (e.g., _public_key)
|
|
35
|
+
config_hash = config_hash.reject { |key, _| key.start_with?("_") }
|
|
36
|
+
|
|
37
|
+
configs <<
|
|
38
|
+
trace!(:ejson, path: rel_path) do
|
|
39
|
+
config_hash
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
return {} if configs.empty?
|
|
44
|
+
|
|
45
|
+
configs.inject do |result_config, next_config|
|
|
46
|
+
Utils.deep_merge!(result_config, next_config)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def schemaful?() ; true; end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
def rel_config_paths
|
|
55
|
+
chain = [environmental_rel_config_path]
|
|
56
|
+
|
|
57
|
+
chain << "secrets.local.ejson" if use_local?
|
|
58
|
+
|
|
59
|
+
chain
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def environmental_rel_config_path
|
|
63
|
+
if Settings.current_environment
|
|
64
|
+
# if environment file is absent, then take data from the default one
|
|
65
|
+
[
|
|
66
|
+
"#{Settings.current_environment}/secrets.ejson",
|
|
67
|
+
default_rel_config_path
|
|
68
|
+
]
|
|
69
|
+
else
|
|
70
|
+
default_rel_config_path
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def default_rel_config_path
|
|
75
|
+
"secrets.ejson"
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def extract_hash_from_rel_config_path(ejson_parser:, rel_config_path:)
|
|
79
|
+
rel_config_path = [rel_config_path] unless rel_config_path.is_a?(Array)
|
|
80
|
+
|
|
81
|
+
rel_config_path.each do |rel_conf_path|
|
|
82
|
+
rel_path = "config/#{rel_conf_path}"
|
|
83
|
+
abs_path = "#{Settings.app_root}/#{rel_path}"
|
|
84
|
+
|
|
85
|
+
result = ejson_parser.call(abs_path)
|
|
86
|
+
|
|
87
|
+
return [result, rel_path] if result
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
nil
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Anyway
|
|
4
|
+
module Rails
|
|
5
|
+
module Loaders
|
|
6
|
+
class Credentials < Anyway::Loaders::Base
|
|
7
|
+
LOCAL_CONTENT_PATH = "config/credentials/local.yml.enc"
|
|
8
|
+
|
|
9
|
+
def call(name:, **_options)
|
|
10
|
+
return {} unless ::Rails.application.respond_to?(:credentials)
|
|
11
|
+
|
|
12
|
+
# do not load from credentials if we're in the context
|
|
13
|
+
# of the `credentials:edit` command
|
|
14
|
+
return {} if defined?(::Rails::Command::CredentialsCommand)
|
|
15
|
+
|
|
16
|
+
# Create a new hash cause credentials are mutable!
|
|
17
|
+
config = {}
|
|
18
|
+
|
|
19
|
+
trace!(
|
|
20
|
+
:credentials,
|
|
21
|
+
store: credentials_path
|
|
22
|
+
) do
|
|
23
|
+
::Rails.application.credentials.config[name.to_sym]
|
|
24
|
+
end.then do |creds|
|
|
25
|
+
Utils.deep_merge!(config, creds) if creds
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
if use_local?
|
|
29
|
+
trace!(:credentials, store: LOCAL_CONTENT_PATH) do
|
|
30
|
+
local_credentials(name)
|
|
31
|
+
end.then { |creds| Utils.deep_merge!(config, creds) if creds }
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
config
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def schemaful?() ; true; end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def local_credentials(name)
|
|
42
|
+
local_creds_path = ::Rails.root.join(LOCAL_CONTENT_PATH).to_s
|
|
43
|
+
|
|
44
|
+
return unless File.file?(local_creds_path)
|
|
45
|
+
|
|
46
|
+
creds = ::Rails.application.encrypted(
|
|
47
|
+
local_creds_path,
|
|
48
|
+
key_path: ::Rails.root.join("config/credentials/local.key")
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
creds.config[name.to_sym]
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def credentials_path
|
|
55
|
+
if ::Rails.application.config.respond_to?(:credentials)
|
|
56
|
+
::Rails.root.join(::Rails.application.config.credentials.content_path).relative_path_from(::Rails.root).to_s
|
|
57
|
+
else
|
|
58
|
+
"config/credentials.yml.enc"
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Anyway
|
|
4
|
+
module Rails
|
|
5
|
+
module Loaders
|
|
6
|
+
class Secrets < Anyway::Loaders::Base
|
|
7
|
+
def call(name:, **_options)
|
|
8
|
+
return {} unless ::Rails.application.respond_to?(:secrets)
|
|
9
|
+
|
|
10
|
+
# Create a new hash cause secrets are mutable!
|
|
11
|
+
config = {}
|
|
12
|
+
|
|
13
|
+
trace!(:secrets) do
|
|
14
|
+
secrets.public_send(name)
|
|
15
|
+
end.then do |secrets|
|
|
16
|
+
Utils.deep_merge!(config, secrets) if secrets
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
config
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def schemaful?() ; true; end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def secrets
|
|
27
|
+
@secrets ||= ::Rails.application.secrets.tap do |_|
|
|
28
|
+
# Reset secrets state if the app hasn't been initialized
|
|
29
|
+
# See https://github.com/palkan/anyway_config/issues/14
|
|
30
|
+
next if ::Rails.application.initialized?
|
|
31
|
+
|
|
32
|
+
# Address unable to load application: NameError: instance variable @secrets
|
|
33
|
+
# not defined
|
|
34
|
+
next unless ::Rails.application.instance_variable_defined?(:@secrets)
|
|
35
|
+
|
|
36
|
+
::Rails.application.remove_instance_variable(:@secrets)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -33,6 +33,7 @@ module Anyway # :nodoc:
|
|
|
33
33
|
deconstruct_keys
|
|
34
34
|
dig
|
|
35
35
|
dup
|
|
36
|
+
format_unknown_attribute
|
|
36
37
|
initialize
|
|
37
38
|
load
|
|
38
39
|
load_from_sources
|
|
@@ -52,6 +53,8 @@ module Anyway # :nodoc:
|
|
|
52
53
|
|
|
53
54
|
class ValidationError < Error; end
|
|
54
55
|
|
|
56
|
+
class UnknownAttributesError < ValidationError; end
|
|
57
|
+
|
|
55
58
|
include OptparseConfig
|
|
56
59
|
include DynamicConfig
|
|
57
60
|
|
|
@@ -374,6 +377,8 @@ module Anyway # :nodoc:
|
|
|
374
377
|
)
|
|
375
378
|
|
|
376
379
|
if overrides
|
|
380
|
+
reject_unknown_config_attributes!(overrides, source: :overrides, strict: Settings.future.reject_unknown_overrides == true)
|
|
381
|
+
|
|
377
382
|
Tracing.trace!(:load) { overrides }
|
|
378
383
|
|
|
379
384
|
Utils.deep_merge!(base_config, overrides)
|
|
@@ -400,10 +405,16 @@ module Anyway # :nodoc:
|
|
|
400
405
|
def load_from_sources(base_config, **opts)
|
|
401
406
|
source_filter = self.class.configuration_sources
|
|
402
407
|
|
|
403
|
-
Anyway.loaders.each do |(id,
|
|
408
|
+
Anyway.loaders.each do |(id, loader_or_proc)|
|
|
404
409
|
next if source_filter && !source_filter.include?(id)
|
|
405
|
-
|
|
410
|
+
|
|
411
|
+
loader = (loader_or_proc.is_a?(Class) && loader_or_proc <= Loaders::Base) ? loader_or_proc.new(**opts) : loader_or_proc
|
|
412
|
+
loader_data = loader.call(**opts)
|
|
413
|
+
reject_unknown_config_attributes!(loader_data, source: id) if loader.is_a?(Loaders::Base) && loader.schemaful?
|
|
414
|
+
|
|
415
|
+
Utils.deep_merge!(base_config, loader_data)
|
|
406
416
|
end
|
|
417
|
+
|
|
407
418
|
base_config
|
|
408
419
|
end
|
|
409
420
|
|
|
@@ -460,13 +471,50 @@ module Anyway # :nodoc:
|
|
|
460
471
|
|
|
461
472
|
self.class.required_attributes.select do |name|
|
|
462
473
|
val = values.dig(*name.to_s.split(".").map(&:to_sym))
|
|
463
|
-
val.nil? || (val.
|
|
474
|
+
val.nil? || (val.respond_to?(:to_s) && val.to_s.empty?)
|
|
464
475
|
end.then do |missing|
|
|
465
476
|
next if missing.empty?
|
|
466
477
|
raise_validation_error "The following config parameters for `#{self.class.name}(config_name: #{self.class.config_name})` are missing or empty: #{missing.join(", ")}"
|
|
467
478
|
end
|
|
468
479
|
end
|
|
469
480
|
|
|
481
|
+
def reject_unknown_config_attributes!(data, source:, strict: false)
|
|
482
|
+
return if data.nil? || data.empty?
|
|
483
|
+
|
|
484
|
+
mode = strict || Settings.enforce_config_schema
|
|
485
|
+
return unless mode
|
|
486
|
+
|
|
487
|
+
known = self.class.config_attributes.map(&:to_s)
|
|
488
|
+
unknown = data.keys.map(&:to_s) - known
|
|
489
|
+
|
|
490
|
+
unless strict
|
|
491
|
+
rescuer = Settings.rescue_unknown_attributes_with
|
|
492
|
+
unknown.reject! { |it| rescuer.call(self, it) } if rescuer
|
|
493
|
+
end
|
|
494
|
+
|
|
495
|
+
return if unknown.empty?
|
|
496
|
+
|
|
497
|
+
msg = "Unknown config attributes for `#{self.class.name || "<anonymous>"}(config_name: #{config_name})` " \
|
|
498
|
+
"provided by #{source}: #{unknown.map { |it| format_unknown_attribute(it, known) }.join(", ")}"
|
|
499
|
+
|
|
500
|
+
if strict
|
|
501
|
+
raise ArgumentError, msg
|
|
502
|
+
elsif mode == :warn
|
|
503
|
+
Kernel.warn msg
|
|
504
|
+
else
|
|
505
|
+
raise UnknownAttributesError, msg
|
|
506
|
+
end
|
|
507
|
+
end
|
|
508
|
+
|
|
509
|
+
def format_unknown_attribute(key, known)
|
|
510
|
+
return key unless defined?(::DidYouMean::SpellChecker)
|
|
511
|
+
|
|
512
|
+
suggestions = ::DidYouMean::SpellChecker.new(dictionary: known).correct(key)
|
|
513
|
+
return key if suggestions.empty?
|
|
514
|
+
|
|
515
|
+
"#{key} (did you mean: #{suggestions.join(", ")}?)"
|
|
516
|
+
end
|
|
517
|
+
|
|
470
518
|
def write_config_attr(key, val)
|
|
471
519
|
key = key.to_sym
|
|
472
520
|
return unless self.class.config_attributes.include?(key)
|
|
@@ -11,11 +11,15 @@ module Anyway
|
|
|
11
11
|
end
|
|
12
12
|
end
|
|
13
13
|
|
|
14
|
-
def initialize(local:)
|
|
14
|
+
def initialize(local: Anyway::Settings.use_local_files, **__kwrest__)
|
|
15
15
|
@local = local
|
|
16
16
|
end
|
|
17
17
|
|
|
18
18
|
def use_local?() = @local == true
|
|
19
|
+
|
|
20
|
+
# Whether this loader supports strict schema (so we can warn or raise on
|
|
21
|
+
# unknown attributes)
|
|
22
|
+
def schemaful?() = false
|
|
19
23
|
end
|
|
20
24
|
end
|
|
21
25
|
end
|