anyway_config 2.3.1 → 2.4.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0a5f209fc2143bc50d22a63e991a97c5cc70f29cf9a04df1910831699e186042
4
- data.tar.gz: 43feb4ea48737e139022c848d6a4367fe6947ea24d3fc37c22292bd6b80c63c7
3
+ metadata.gz: dd660b7594fd85e041d4f00a4abbadef72085256e250df638cd239a3512db58c
4
+ data.tar.gz: 94fa4463eaf4bf2d64cddba05ba1fac18e635f62ff89620f8a460c9cf0162535
5
5
  SHA512:
6
- metadata.gz: a89d3edcace073fdb144022fbaf31626a4131d4beb7164bc2bb13dd9e78b9cfb8dba9c007c9ca0b423b06319c9bd197b64c82033087cbfc3fbd375a41a02baf4
7
- data.tar.gz: 035dd8e51f7a6e5fe02a03912bb147fb9de3e9a84525925a87e36451c97a71238639c2f0e60b94ddffddd203be50808ca11264be2dea9b43759676f9003cc0bd
6
+ metadata.gz: a1585d343611b50d1ca021dfbd2dfb49f92b8aad835c7f3c48aabeab4d5cc905877db4ef57a86e51b59c53f9b1192f69c7b58af4ec6c1b182447b146e19b4a3b
7
+ data.tar.gz: 983a3079303037e6cda97057ad51ef2479000baa1e975d481a7c2f4cd2510605fd36227d3692b1a82f7a85db333a7909e5610515286e1c10adc0aeb0e11f3006
data/CHANGELOG.md CHANGED
@@ -2,6 +2,57 @@
2
2
 
3
3
  ## master
4
4
 
5
+ ## 2.4.1 (2023-05-04)
6
+
7
+ - Add custom namespace support via `ejson_namespace` ([@bessey])
8
+
9
+ - Add arbitrary custom loader options support via `loader_options` ([@bessey])
10
+
11
+ ## 2.4.0 (2023-04-04)
12
+
13
+ - Added `Confi#as_env` to convert config into a ENV-like Hash. ([@tagirahmad][])
14
+
15
+ - Added experimental support for sub-configs via coercion. ([@palkan][])
16
+
17
+ ```ruby
18
+ class AnotherConfig < Anyway::Config
19
+ attr_config foo: "bar"
20
+ end
21
+
22
+ class MyConfig < Anyway::Config
23
+ attr_config :another_config
24
+
25
+ coerce_types another_config: "AnotherConfig"
26
+ end
27
+
28
+ MyConfig.new.another_config.foo #=> "bar"
29
+
30
+ ENV["MY_ANOTHER_CONFIG__FOO"] = "baz"
31
+ MyConfig.new.another_config.foo #=> "baz"
32
+ ```
33
+
34
+ - Define predicate methods when `:boolean` type is specified for the attribute. ([@palkan][])
35
+
36
+ - Add support for nested required config attributes. ([@palkan][])
37
+
38
+ The API is inspired by Rails permitted params:
39
+
40
+ ```ruby
41
+ class AppConfig < Anyway::Config
42
+ attr_config :assets_host, database: {host: nil, port: nil}
43
+
44
+ required :assets_host, database: [:host, :port]
45
+ end
46
+ ```
47
+
48
+ - Add support for using `env_prefix ""` to load from unprefixed env vars. ([@palkan][])
49
+
50
+ See [#118](https://github.com/palkan/anyway_config/issues/118).
51
+
52
+ - Added EJSON support. ([@inner-whisper])
53
+
54
+ - Add Doppler loader. ([@prog-supdex][]).
55
+
5
56
  ## 2.3.1 (2023-01-17)
6
57
 
7
58
  - [Fixes [#110](https://github.com/palkan/anyway_config/issues/110)] Fix setting up autoloader for the same folder. ([@palkan][])
@@ -469,3 +520,7 @@ No we're dependency-free!
469
520
  [@progapandist]: https://github.com/progapandist
470
521
  [@skryukov]: https://github.com/skryukov
471
522
  [@fargelus]: https://github.com/fargelus
523
+ [@prog-supdex]: https://github.com/prog-supdex
524
+ [@inner-whisper]: https://github.com/inner-whisper
525
+ [@tagirahmad]: https://github.com/tagirahmad
526
+ [@bessey]: https://github.com/bessey
data/README.md CHANGED
@@ -45,6 +45,9 @@ For version 1.x see the [1-4-stable branch](https://github.com/palkan/anyway_con
45
45
  - [Type coercion](#type-coercion)
46
46
  - [Local configuration](#local-files)
47
47
  - [Data loaders](#data-loaders)
48
+ - [Doppler integration](#doppler-integration)
49
+ - [EJSON support](#ejson-support)
50
+ - [Custom loaders](#custom-loaders)
48
51
  - [Source tracing](#tracing)
49
52
  - [Pattern matching](#pattern-matching)
50
53
  - [Test helpers](#test-helpers)
@@ -269,7 +272,7 @@ This feature is similar to `Rails.application.config_for` but more powerful:
269
272
  | Load data from `secrets` | ❌ | ✅ |
270
273
  | Load data from `credentials` | ❌ | ✅ |
271
274
  | Load data from environment | ❌ | ✅ |
272
- | Load data from [custom sources](#data-loaders) | ❌ | ✅ |
275
+ | Load data from [other sources](#data-loaders) | ❌ | ✅ |
273
276
  | Local config files | ❌ | ✅ |
274
277
  | Type coercion | ❌ | ✅ |
275
278
  | [Source tracing](#tracing) | ❌ | ✅ |
@@ -541,6 +544,8 @@ Would you like to generate a heroku.yml file? (Y/n) n
541
544
  You can also specify the `--app` option to put the newly created class into `app/configs` folder.
542
545
  Alternatively, you can call `rails g anyway:app_config name param1 param2 ...`.
543
546
 
547
+ **NOTE:** The generated `ApplicationConfig` class uses a singleton pattern along with `delegate_missing_to` to re-use the same instance across the application. However, the delegation can lead to unexpected behaviour and break Anyway Config internals if you have attributes named as `Anyway::Config` class methods. See [#120](https://github.com/palkan/anyway_config/issues/120).
548
+
544
549
  ## Using with Ruby
545
550
 
546
551
  The default data loading mechanism for non-Rails applications is the following (ordered by priority from low to high):
@@ -733,6 +738,74 @@ Don't forget to add `*.local.yml` (and `config/credentials/local.*`) to your `.g
733
738
 
734
739
  ## Data loaders
735
740
 
741
+ ### Doppler integration
742
+
743
+ Anyway Config can pull configuration data from [Doppler](https://www.doppler.com/). All you need is to specify the `DOPPLER_TOKEN` environment variable with the **service token**, associated with the specific content (read more about [service tokens](https://docs.doppler.com/docs/service-tokens)).
744
+
745
+ You can also configure Doppler loader manually if needed:
746
+
747
+ ```ruby
748
+ # Add loader
749
+ Anyway.loaders.append :Doppler, Anyway::Loaders::Doppler
750
+
751
+ # Configure API URL and token (defaults are shown)
752
+ Anyway::Loaders::Doppler.download_url = "https://api.doppler.com/v3/configs/config/secrets/download"
753
+ Anyway::Loaders::Doppler.token = ENV["DOPPLER_TOKEN"]
754
+ ```
755
+
756
+ **NOTE:** You can opt-out from Doppler loader by specifying the`ANYWAY_CONFIG_DISABLE_DOPPLER=true` env var (in case you have the `DOPPLER_TOKEN` env var, but don't want to use it with Anyway Config).
757
+
758
+ ### EJSON support
759
+
760
+ Anyway Config allows you to keep your configuration also in encrypted `.ejson` files. More information
761
+ about EJSON format you can read [here](https://github.com/Shopify/ejson).
762
+
763
+ Configuration will be loaded only if you have `ejson` executable in your PATH. Easiest way to do this - install `ejson` as a gem into project:
764
+
765
+ ```ruby
766
+ # Gemfile
767
+ gem "ejson"
768
+ ```
769
+
770
+ Loading order of configuration is next:
771
+
772
+ - `config/secrets.local.ejson` (see [Local files](#local-files) for more information)
773
+ - `config/<environment>/secrets.ejson` (if you have any multi-environment setup, e.g Rails environments)
774
+ - `config/secrets.ejson`
775
+
776
+ Example of `config/secrets.ejson` file content for your `MyConfig`:
777
+
778
+ ```json
779
+ {
780
+ "_public_key": "0843d33f0eee994adc66b939fe4ef569e4c97db84e238ff581934ee599e19d1a",
781
+ "my":
782
+ {
783
+ "_username": "root",
784
+ "password": "EJ[1:IC1d347GkxLXdZ0KrjGaY+ljlsK1BmK7CobFt6iOLgE=:Z55OYS1+On0xEBvxUaIOdv/mE2r6lp44:T7bE5hkAbazBnnH6M8bfVcv8TOQJAgUDQffEgw==]"
785
+ }
786
+ }
787
+ ```
788
+
789
+ To debug any problems with loading configurations from `.ejson` files you can directly call `ejson decrypt`:
790
+
791
+ ```sh
792
+ ejson decrypt config/secrets.ejson
793
+ ```
794
+
795
+ You can customize the JSON namespace under which a loader searches for configuration via `loader_options`:
796
+
797
+ ```ruby
798
+ class MyConfig < Anyway::Config
799
+ # To look under the key "foo" instead of the default key of "my"
800
+ loader_options ejson_namespace: "foo"
801
+
802
+ # Or to disable namespacing entirely, and instead search in the root object
803
+ loader_options ejson_namespace: false
804
+ end
805
+ ```
806
+
807
+ ### Custom loaders
808
+
736
809
  You can provide your own data loaders or change the existing ones using the Loaders API (which is very similar to Rack middleware builder):
737
810
 
738
811
  ```ruby
@@ -751,7 +824,8 @@ def call(
751
824
  name:, # config name
752
825
  env_prefix:, # prefix for env vars if any
753
826
  config_path:, # path to YML config
754
- local: # true|false, whether to load local configuration
827
+ local:, # true|false, whether to load local configuration
828
+ **options # custom options can be passed via Anyway::Config.loader_options example: "custom", option: "blah"
755
829
  )
756
830
  #=> must return Hash with configuration data
757
831
  end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "open3"
4
+ require "anyway/ext/hash"
5
+
6
+ using Anyway::Ext::Hash
7
+
8
+ module Anyway
9
+ class EJSONParser
10
+ attr_reader :bin_path
11
+
12
+ def initialize(bin_path = "ejson")
13
+ @bin_path = bin_path
14
+ end
15
+
16
+ def call(file_path)
17
+ return unless File.exist?(file_path)
18
+
19
+ raw_content = nil
20
+
21
+ stdout, stderr, status = Open3.capture3("#{bin_path} decrypt #{file_path}")
22
+
23
+ if status.success?
24
+ raw_content = JSON.parse(stdout.chomp)
25
+ else
26
+ Kernel.warn "Failed to decrypt #{file_path}: #{stderr}"
27
+ end
28
+
29
+ return unless raw_content
30
+
31
+ raw_content.deep_transform_keys do |key|
32
+ if key[0] == "_"
33
+ key[1..-1]
34
+ else
35
+ key
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -8,6 +8,7 @@ module Anyway # :nodoc:
8
8
  using Anyway::Ext::DeepDup
9
9
  using Anyway::Ext::DeepFreeze
10
10
  using Anyway::Ext::Hash
11
+ using Anyway::Ext::FlattenNames
11
12
 
12
13
  using(Module.new do
13
14
  refine Object do
@@ -26,6 +27,7 @@ module Anyway # :nodoc:
26
27
  RESERVED_NAMES = %i[
27
28
  config_name
28
29
  env_prefix
30
+ as_env
29
31
  values
30
32
  class
31
33
  clear
@@ -47,8 +49,6 @@ module Anyway # :nodoc:
47
49
  __type_caster__
48
50
  ].freeze
49
51
 
50
- ENV_OPTION_EXCLUDE_KEY = :except
51
-
52
52
  class Error < StandardError; end
53
53
 
54
54
  class ValidationError < Error; end
@@ -128,34 +128,14 @@ module Anyway # :nodoc:
128
128
  end
129
129
  end
130
130
 
131
- def required(*names, env: nil)
132
- unknown_names = names - config_attributes
131
+ def required(*names, env: nil, **nested)
132
+ unknown_names = names + nested.keys - config_attributes
133
133
  raise ArgumentError, "Unknown config param: #{unknown_names.join(",")}" if unknown_names.any?
134
134
 
135
- names = filter_by_env(names, env)
136
- required_attributes.push(*names)
137
- end
138
-
139
- def filter_by_env(names, env)
140
- return names if env.nil? || env.to_s == current_env
141
-
142
- filtered_names = if env.is_a?(Hash)
143
- names_with_exclude_env_option(names, env)
144
- elsif env.is_a?(Array)
145
- names if env.flat_map(&:to_s).include?(current_env)
146
- end
147
-
148
- filtered_names || []
149
- end
150
-
151
- def current_env
152
- Settings.current_environment.to_s
153
- end
135
+ return unless Settings.matching_env?(env)
154
136
 
155
- def names_with_exclude_env_option(names, env)
156
- envs = env[ENV_OPTION_EXCLUDE_KEY]
157
- excluded_envs = [envs].flat_map(&:to_s)
158
- names if excluded_envs.none?(current_env)
137
+ required_attributes.push(*names)
138
+ required_attributes.push(*nested.flatten_names)
159
139
  end
160
140
 
161
141
  def required_attributes
@@ -219,10 +199,28 @@ module Anyway # :nodoc:
219
199
  end
220
200
  end
221
201
 
202
+ def loader_options(val = nil)
203
+ return (@loader_options = val) unless val.nil?
204
+
205
+ return @loader_options if instance_variable_defined?(:@loader_options)
206
+
207
+ @loader_options = if superclass < Anyway::Config
208
+ superclass.loader_options
209
+ else
210
+ {}
211
+ end
212
+ end
213
+
222
214
  def new_empty_config() ; {}; end
223
215
 
224
216
  def coerce_types(mapping)
225
217
  Utils.deep_merge!(coercion_mapping, mapping)
218
+
219
+ mapping.each do |key, val|
220
+ next unless val == :boolean || (val.is_a?(::Hash) && val[:type] == :boolean)
221
+
222
+ alias_method :"#{key}?", :"#{key}"
223
+ end
226
224
  end
227
225
 
228
226
  def coercion_mapping
@@ -268,7 +266,7 @@ module Anyway # :nodoc:
268
266
  names.each do |name|
269
267
  accessors_module.module_eval <<~RUBY, __FILE__, __LINE__ + 1
270
268
  def #{name}=(val)
271
- __trace__&.record_value(val, \"#{name}\", **Tracing.current_trace_source)
269
+ __trace__&.record_value(val, "#{name}", **Tracing.current_trace_source)
272
270
  values[:#{name}] = val
273
271
  end
274
272
 
@@ -357,7 +355,13 @@ module Anyway # :nodoc:
357
355
 
358
356
  config_path = resolve_config_path(config_name, env_prefix)
359
357
 
360
- load_from_sources(base_config, name: config_name, env_prefix: env_prefix, config_path: config_path)
358
+ load_from_sources(
359
+ base_config,
360
+ name: config_name,
361
+ env_prefix: env_prefix,
362
+ config_path: config_path,
363
+ **self.class.loader_options
364
+ )
361
365
 
362
366
  if overrides
363
367
  Tracing.trace!(:load) { overrides }
@@ -430,13 +434,18 @@ module Anyway # :nodoc:
430
434
  end
431
435
  end
432
436
 
437
+ def as_env
438
+ Env.from_hash(to_h, prefix: env_prefix)
439
+ end
440
+
433
441
  private
434
442
 
435
443
  attr_reader :values, :__trace__
436
444
 
437
445
  def validate_required_attributes!
438
446
  self.class.required_attributes.select do |name|
439
- values[name].nil? || (values[name].is_a?(String) && values[name].empty?)
447
+ val = values.dig(*name.to_s.split(".").map(&:to_sym))
448
+ val.nil? || (val.is_a?(String) && val.empty?)
440
449
  end.then do |missing|
441
450
  next if missing.empty?
442
451
  raise_validation_error "The following config parameters for `#{self.class.name}(config_name: #{self.class.config_name})` are missing or empty: #{missing.join(", ")}"
@@ -80,6 +80,20 @@ module Anyway
80
80
  def default_environmental_key?
81
81
  !default_environmental_key.nil?
82
82
  end
83
+
84
+ def matching_env?(env)
85
+ return true if env.nil? || env.to_s == current_environment
86
+
87
+ if env.is_a?(::Hash)
88
+ envs = env[:except]
89
+ excluded_envs = [envs].flat_map(&:to_s)
90
+ excluded_envs.none?(current_environment)
91
+ elsif env.is_a?(::Array)
92
+ env.flat_map(&:to_s).include?(current_environment)
93
+ else
94
+ false
95
+ end
96
+ end
83
97
  end
84
98
 
85
99
  # By default, use local files only in development (that's the purpose if the local files)
@@ -19,7 +19,7 @@ module Anyway
19
19
  def initialize(type = :trace, value = UNDEF, **source)
20
20
  @type = type
21
21
  @source = source
22
- @value = value == UNDEF ? Hash.new { |h, k| h[k] = Trace.new(:trace) } : value
22
+ @value = (value == UNDEF) ? Hash.new { |h, k| h[k] = Trace.new(:trace) } : value
23
23
  end
24
24
 
25
25
  def dig(*__rest__, &__block__)
@@ -35,7 +35,7 @@ module Anyway
35
35
  end
36
36
 
37
37
  target_trace = path.empty? ? self : value.dig(*path)
38
- target_trace.value[key.to_s] = trace
38
+ target_trace.record_key(key.to_s, trace)
39
39
 
40
40
  val
41
41
  end
@@ -54,6 +54,12 @@ module Anyway
54
54
  hash
55
55
  end
56
56
 
57
+ def record_key(key, key_trace)
58
+ @value = Hash.new { |h, k| h[k] = Trace.new(:trace) } unless value.is_a?(::Hash)
59
+
60
+ value[key] = key_trace
61
+ end
62
+
57
63
  def merge!(another_trace)
58
64
  raise ArgumentError, "You can only merge into a :trace type, and this is :#{type}" unless trace?
59
65
  raise ArgumentError, "You can only merge a :trace type, but trying :#{type}" unless another_trace.trace?
@@ -90,6 +90,11 @@ module Anyway
90
90
  end
91
91
  end
92
92
 
93
+ unless "".respond_to?(:safe_constantize)
94
+ require "anyway/ext/string_constantize"
95
+ using Anyway::Ext::StringConstantize
96
+ end
97
+
93
98
  # TypeCaster is an object responsible for type-casting.
94
99
  # It uses a provided types registry and mapping, and also
95
100
  # accepts a fallback typecaster.
@@ -109,8 +114,16 @@ module Anyway
109
114
  return fallback.coerce(key, val) unless caster_config
110
115
 
111
116
  case; when ((__m__ = caster_config)) && false
112
- when (((array, type) = nil) || ((__m__.respond_to?(:deconstruct_keys) && (((__m_hash__src__ = __m__.deconstruct_keys(nil)) || true) && (Hash === __m_hash__src__ || Kernel.raise(TypeError, "#deconstruct_keys must return Hash"))) && (__m_hash__ = __m_hash__src__.dup)) && ((__m_hash__.key?(:array) && __m_hash__.key?(:type)) && (((array = __m_hash__.delete(:array)) || true) && (((type = __m_hash__.delete(:type)) || true) && __m_hash__.empty?)))))
117
+ when (((array, type) = nil) || ((Hash === __m__) && ((__m__.respond_to?(:deconstruct_keys) && (((__m_hash__src__ = __m__.deconstruct_keys(nil)) || true) && (Hash === __m_hash__src__ || Kernel.raise(TypeError, "#deconstruct_keys must return Hash"))) && (__m_hash__ = __m_hash__src__.dup)) && ((__m_hash__.key?(:array) && __m_hash__.key?(:type)) && (((array = __m_hash__.delete(:array)) || true) && (((type = __m_hash__.delete(:type)) || true) && __m_hash__.empty?))))))
113
118
  registry.deserialize(val, type, array: array)
119
+ when (((subconfig,) = nil) || ((Hash === __m__) && (__m__.respond_to?(:deconstruct_keys) && (((__m_hash__ = __m__.deconstruct_keys([:config])) || true) && (Hash === __m_hash__ || Kernel.raise(TypeError, "#deconstruct_keys must return Hash"))) && (__m_hash__.key?(:config) && ((subconfig = __m_hash__[:config]) || true)))))
120
+
121
+
122
+
123
+ subconfig = subconfig.safe_constantize if subconfig.is_a?(::String)
124
+ raise ArgumentError, "Config is not found: #{subconfig}" unless subconfig
125
+
126
+ subconfig.new(val)
114
127
  when (Hash === __m__)
115
128
 
116
129
 
@@ -8,6 +8,7 @@ module Anyway # :nodoc:
8
8
  using Anyway::Ext::DeepDup
9
9
  using Anyway::Ext::DeepFreeze
10
10
  using Anyway::Ext::Hash
11
+ using Anyway::Ext::FlattenNames
11
12
 
12
13
  using(Module.new do
13
14
  refine Object do
@@ -26,6 +27,7 @@ module Anyway # :nodoc:
26
27
  RESERVED_NAMES = %i[
27
28
  config_name
28
29
  env_prefix
30
+ as_env
29
31
  values
30
32
  class
31
33
  clear
@@ -47,8 +49,6 @@ module Anyway # :nodoc:
47
49
  __type_caster__
48
50
  ].freeze
49
51
 
50
- ENV_OPTION_EXCLUDE_KEY = :except
51
-
52
52
  class Error < StandardError; end
53
53
 
54
54
  class ValidationError < Error; end
@@ -128,34 +128,14 @@ module Anyway # :nodoc:
128
128
  end
129
129
  end
130
130
 
131
- def required(*names, env: nil)
132
- unknown_names = names - config_attributes
131
+ def required(*names, env: nil, **nested)
132
+ unknown_names = names + nested.keys - config_attributes
133
133
  raise ArgumentError, "Unknown config param: #{unknown_names.join(",")}" if unknown_names.any?
134
134
 
135
- names = filter_by_env(names, env)
136
- required_attributes.push(*names)
137
- end
138
-
139
- def filter_by_env(names, env)
140
- return names if env.nil? || env.to_s == current_env
141
-
142
- filtered_names = if env.is_a?(Hash)
143
- names_with_exclude_env_option(names, env)
144
- elsif env.is_a?(Array)
145
- names if env.flat_map(&:to_s).include?(current_env)
146
- end
147
-
148
- filtered_names || []
149
- end
150
-
151
- def current_env
152
- Settings.current_environment.to_s
153
- end
135
+ return unless Settings.matching_env?(env)
154
136
 
155
- def names_with_exclude_env_option(names, env)
156
- envs = env[ENV_OPTION_EXCLUDE_KEY]
157
- excluded_envs = [envs].flat_map(&:to_s)
158
- names if excluded_envs.none?(current_env)
137
+ required_attributes.push(*names)
138
+ required_attributes.push(*nested.flatten_names)
159
139
  end
160
140
 
161
141
  def required_attributes
@@ -219,10 +199,28 @@ module Anyway # :nodoc:
219
199
  end
220
200
  end
221
201
 
202
+ def loader_options(val = nil)
203
+ return (@loader_options = val) unless val.nil?
204
+
205
+ return @loader_options if instance_variable_defined?(:@loader_options)
206
+
207
+ @loader_options = if superclass < Anyway::Config
208
+ superclass.loader_options
209
+ else
210
+ {}
211
+ end
212
+ end
213
+
222
214
  def new_empty_config() ; {}; end
223
215
 
224
216
  def coerce_types(mapping)
225
217
  Utils.deep_merge!(coercion_mapping, mapping)
218
+
219
+ mapping.each do |key, val|
220
+ next unless val == :boolean || (val.is_a?(::Hash) && val[:type] == :boolean)
221
+
222
+ alias_method :"#{key}?", :"#{key}"
223
+ end
226
224
  end
227
225
 
228
226
  def coercion_mapping
@@ -268,7 +266,7 @@ module Anyway # :nodoc:
268
266
  names.each do |name|
269
267
  accessors_module.module_eval <<~RUBY, __FILE__, __LINE__ + 1
270
268
  def #{name}=(val)
271
- __trace__&.record_value(val, \"#{name}\", **Tracing.current_trace_source)
269
+ __trace__&.record_value(val, "#{name}", **Tracing.current_trace_source)
272
270
  values[:#{name}] = val
273
271
  end
274
272
 
@@ -357,7 +355,13 @@ module Anyway # :nodoc:
357
355
 
358
356
  config_path = resolve_config_path(config_name, env_prefix)
359
357
 
360
- load_from_sources(base_config, name: config_name, env_prefix: env_prefix, config_path: config_path)
358
+ load_from_sources(
359
+ base_config,
360
+ name: config_name,
361
+ env_prefix: env_prefix,
362
+ config_path: config_path,
363
+ **self.class.loader_options
364
+ )
361
365
 
362
366
  if overrides
363
367
  Tracing.trace!(:load) { overrides }
@@ -430,13 +434,18 @@ module Anyway # :nodoc:
430
434
  end
431
435
  end
432
436
 
437
+ def as_env
438
+ Env.from_hash(to_h, prefix: env_prefix)
439
+ end
440
+
433
441
  private
434
442
 
435
443
  attr_reader :values, :__trace__
436
444
 
437
445
  def validate_required_attributes!
438
446
  self.class.required_attributes.select do |name|
439
- values[name].nil? || (values[name].is_a?(String) && values[name].empty?)
447
+ val = values.dig(*name.to_s.split(".").map(&:to_sym))
448
+ val.nil? || (val.is_a?(String) && val.empty?)
440
449
  end.then do |missing|
441
450
  next if missing.empty?
442
451
  raise_validation_error "The following config parameters for `#{self.class.name}(config_name: #{self.class.config_name})` are missing or empty: #{missing.join(", ")}"
@@ -75,3 +75,5 @@ end
75
75
  require "anyway/loaders/base"
76
76
  require "anyway/loaders/yaml"
77
77
  require "anyway/loaders/env"
78
+ require "anyway/loaders/doppler"
79
+ require "anyway/loaders/ejson"
@@ -19,7 +19,7 @@ module Anyway
19
19
  def initialize(type = :trace, value = UNDEF, **source)
20
20
  @type = type
21
21
  @source = source
22
- @value = value == UNDEF ? Hash.new { |h, k| h[k] = Trace.new(:trace) } : value
22
+ @value = (value == UNDEF) ? Hash.new { |h, k| h[k] = Trace.new(:trace) } : value
23
23
  end
24
24
 
25
25
  def dig(...)
@@ -35,7 +35,7 @@ module Anyway
35
35
  end
36
36
 
37
37
  target_trace = path.empty? ? self : value.dig(*path)
38
- target_trace.value[key.to_s] = trace
38
+ target_trace.record_key(key.to_s, trace)
39
39
 
40
40
  val
41
41
  end
@@ -54,6 +54,12 @@ module Anyway
54
54
  hash
55
55
  end
56
56
 
57
+ def record_key(key, key_trace)
58
+ @value = Hash.new { |h, k| h[k] = Trace.new(:trace) } unless value.is_a?(::Hash)
59
+
60
+ value[key] = key_trace
61
+ end
62
+
57
63
  def merge!(another_trace)
58
64
  raise ArgumentError, "You can only merge into a :trace type, and this is :#{type}" unless trace?
59
65
  raise ArgumentError, "You can only merge a :trace type, but trying :#{type}" unless another_trace.trace?