anyway_config 2.3.1 → 2.4.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0a5f209fc2143bc50d22a63e991a97c5cc70f29cf9a04df1910831699e186042
4
- data.tar.gz: 43feb4ea48737e139022c848d6a4367fe6947ea24d3fc37c22292bd6b80c63c7
3
+ metadata.gz: 44a860f50ae339b170da1a4ea99a7c6125b8ddcf2e25269c7341150bb8b8cf1f
4
+ data.tar.gz: f39c3ff413a1f334ad85a8f3eb0dfabca16dfc0c18c7f8719236607a777f947d
5
5
  SHA512:
6
- metadata.gz: a89d3edcace073fdb144022fbaf31626a4131d4beb7164bc2bb13dd9e78b9cfb8dba9c007c9ca0b423b06319c9bd197b64c82033087cbfc3fbd375a41a02baf4
7
- data.tar.gz: 035dd8e51f7a6e5fe02a03912bb147fb9de3e9a84525925a87e36451c97a71238639c2f0e60b94ddffddd203be50808ca11264be2dea9b43759676f9003cc0bd
6
+ metadata.gz: 6b903f239c176e8c031fdea2d81f9d6d46d0657c2b5525f7e36fed495ab2410dc017bbe676fdc8c633d6b303c619e5caeb109d8c172d172720bfb3417746c612
7
+ data.tar.gz: 7bbb6db11016ce9c9473009c49aa676e3121ab921a2493ceb6d8e0e270cf75369935678bb1a2e5f69ebe387be1d5a44d159df819dcfab4a591aa67bab2e757fa
data/CHANGELOG.md CHANGED
@@ -2,6 +2,51 @@
2
2
 
3
3
  ## master
4
4
 
5
+ ## 2.4.0 (2023-04-04)
6
+
7
+ - Add `Confi#as_env` to convert config into a ENV-like HASH. ([@tagirahmad][])
8
+
9
+ - Added experimental support for sub-configs via coercion. ([@palkan][])
10
+
11
+ ```ruby
12
+ class AnotherConfig < Anyway::Config
13
+ attr_config foo: "bar"
14
+ end
15
+
16
+ class MyConfig < Anyway::Config
17
+ attr_config :another_config
18
+
19
+ coerce_types another_config: "AnotherConfig"
20
+ end
21
+
22
+ MyConfig.new.another_config.foo #=> "bar"
23
+
24
+ ENV["MY_ANOTHER_CONFIG__FOO"] = "baz"
25
+ MyConfig.new.another_config.foo #=> "baz"
26
+ ```
27
+
28
+ - Define predicate methods when `:boolean` type is specified for the attribute. ([@palkan][])
29
+
30
+ - Add support for nested required config attributes. ([@palkan][])
31
+
32
+ The API is inspired by Rails permitted params:
33
+
34
+ ```ruby
35
+ class AppConfig < Anyway::Config
36
+ attr_config :assets_host, database: {host: nil, port: nil}
37
+
38
+ required :assets_host, database: [:host, :port]
39
+ end
40
+ ```
41
+
42
+ - Add support for using `env_prefix ""` to load from unprefixed env vars. ([@palkan][])
43
+
44
+ See [#118](https://github.com/palkan/anyway_config/issues/118).
45
+
46
+ - Added EJSON support. ([@inner-whisper])
47
+
48
+ - Add Doppler loader. ([@prog-supdex][]).
49
+
5
50
  ## 2.3.1 (2023-01-17)
6
51
 
7
52
  - [Fixes [#110](https://github.com/palkan/anyway_config/issues/110)] Fix setting up autoloader for the same folder. ([@palkan][])
@@ -469,3 +514,6 @@ No we're dependency-free!
469
514
  [@progapandist]: https://github.com/progapandist
470
515
  [@skryukov]: https://github.com/skryukov
471
516
  [@fargelus]: https://github.com/fargelus
517
+ [@prog-supdex]: https://github.com/prog-supdex
518
+ [@inner-whisper]: https://github.com/inner-whisper
519
+ [@tagirahmad]: https://github.com/tagirahmad
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) | ❌ | ✅ |
@@ -733,6 +736,62 @@ Don't forget to add `*.local.yml` (and `config/credentials/local.*`) to your `.g
733
736
 
734
737
  ## Data loaders
735
738
 
739
+ ### Doppler integration
740
+
741
+ 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)).
742
+
743
+ You can also configure Doppler loader manually if needed:
744
+
745
+ ```ruby
746
+ # Add loader
747
+ Anyway.loaders.append :Doppler, Anyway::Loaders::Doppler
748
+
749
+ # Configure API URL and token (defaults are shown)
750
+ Anyway::Loaders::Doppler.download_url = "https://api.doppler.com/v3/configs/config/secrets/download"
751
+ Anyway::Loaders::Doppler.token = ENV["DOPPLER_TOKEN"]
752
+ ```
753
+
754
+ **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).
755
+
756
+ ### EJSON support
757
+
758
+ Anyway Config allows you to keep your configuration also in encrypted `.ejson` files. More information
759
+ about EJSON format you can read [here](https://github.com/Shopify/ejson).
760
+
761
+ 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:
762
+
763
+ ```ruby
764
+ # Gemfile
765
+ gem "ejson"
766
+ ```
767
+
768
+ Loading order of configuration is next:
769
+
770
+ - `config/secrets.local.ejson` (see [Local files](#local-files) for more information)
771
+ - `config/<environment>/secrets.ejson` (if you have any multi-environment setup, e.g Rails environments)
772
+ - `config/secrets.ejson`
773
+
774
+ Example of `config/secrets.ejson` file content for your `MyConfig`:
775
+
776
+ ```json
777
+ {
778
+ "_public_key": "0843d33f0eee994adc66b939fe4ef569e4c97db84e238ff581934ee599e19d1a",
779
+ "my":
780
+ {
781
+ "_username": "root",
782
+ "password": "EJ[1:IC1d347GkxLXdZ0KrjGaY+ljlsK1BmK7CobFt6iOLgE=:Z55OYS1+On0xEBvxUaIOdv/mE2r6lp44:T7bE5hkAbazBnnH6M8bfVcv8TOQJAgUDQffEgw==]"
783
+ }
784
+ }
785
+ ```
786
+
787
+ To debug any problems with loading configurations from `.ejson` files you can directly call `ejson decrypt`:
788
+
789
+ ```sh
790
+ ejson decrypt config/secrets.ejson
791
+ ```
792
+
793
+ ### Custom loaders
794
+
736
795
  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
796
 
738
797
  ```ruby
@@ -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
135
+ return unless Settings.matching_env?(env)
138
136
 
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
154
-
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
@@ -223,6 +203,12 @@ module Anyway # :nodoc:
223
203
 
224
204
  def coerce_types(mapping)
225
205
  Utils.deep_merge!(coercion_mapping, mapping)
206
+
207
+ mapping.each do |key, val|
208
+ next unless val == :boolean || (val.is_a?(::Hash) && val[:type] == :boolean)
209
+
210
+ alias_method :"#{key}?", :"#{key}"
211
+ end
226
212
  end
227
213
 
228
214
  def coercion_mapping
@@ -268,7 +254,7 @@ module Anyway # :nodoc:
268
254
  names.each do |name|
269
255
  accessors_module.module_eval <<~RUBY, __FILE__, __LINE__ + 1
270
256
  def #{name}=(val)
271
- __trace__&.record_value(val, \"#{name}\", **Tracing.current_trace_source)
257
+ __trace__&.record_value(val, "#{name}", **Tracing.current_trace_source)
272
258
  values[:#{name}] = val
273
259
  end
274
260
 
@@ -430,13 +416,18 @@ module Anyway # :nodoc:
430
416
  end
431
417
  end
432
418
 
419
+ def as_env
420
+ Env.from_hash(to_h, prefix: env_prefix)
421
+ end
422
+
433
423
  private
434
424
 
435
425
  attr_reader :values, :__trace__
436
426
 
437
427
  def validate_required_attributes!
438
428
  self.class.required_attributes.select do |name|
439
- values[name].nil? || (values[name].is_a?(String) && values[name].empty?)
429
+ val = values.dig(*name.to_s.split(".").map(&:to_sym))
430
+ val.nil? || (val.is_a?(String) && val.empty?)
440
431
  end.then do |missing|
441
432
  next if missing.empty?
442
433
  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
135
+ return unless Settings.matching_env?(env)
138
136
 
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
154
-
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
@@ -223,6 +203,12 @@ module Anyway # :nodoc:
223
203
 
224
204
  def coerce_types(mapping)
225
205
  Utils.deep_merge!(coercion_mapping, mapping)
206
+
207
+ mapping.each do |key, val|
208
+ next unless val == :boolean || (val.is_a?(::Hash) && val[:type] == :boolean)
209
+
210
+ alias_method :"#{key}?", :"#{key}"
211
+ end
226
212
  end
227
213
 
228
214
  def coercion_mapping
@@ -268,7 +254,7 @@ module Anyway # :nodoc:
268
254
  names.each do |name|
269
255
  accessors_module.module_eval <<~RUBY, __FILE__, __LINE__ + 1
270
256
  def #{name}=(val)
271
- __trace__&.record_value(val, \"#{name}\", **Tracing.current_trace_source)
257
+ __trace__&.record_value(val, "#{name}", **Tracing.current_trace_source)
272
258
  values[:#{name}] = val
273
259
  end
274
260
 
@@ -430,13 +416,18 @@ module Anyway # :nodoc:
430
416
  end
431
417
  end
432
418
 
419
+ def as_env
420
+ Env.from_hash(to_h, prefix: env_prefix)
421
+ end
422
+
433
423
  private
434
424
 
435
425
  attr_reader :values, :__trace__
436
426
 
437
427
  def validate_required_attributes!
438
428
  self.class.required_attributes.select do |name|
439
- values[name].nil? || (values[name].is_a?(String) && values[name].empty?)
429
+ val = values.dig(*name.to_s.split(".").map(&:to_sym))
430
+ val.nil? || (val.is_a?(String) && val.empty?)
440
431
  end.then do |missing|
441
432
  next if missing.empty?
442
433
  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?
@@ -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
135
+ return unless Settings.matching_env?(env)
138
136
 
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
154
-
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
@@ -223,6 +203,12 @@ module Anyway # :nodoc:
223
203
 
224
204
  def coerce_types(mapping)
225
205
  Utils.deep_merge!(coercion_mapping, mapping)
206
+
207
+ mapping.each do |key, val|
208
+ next unless val == :boolean || (val.is_a?(::Hash) && val[:type] == :boolean)
209
+
210
+ alias_method :"#{key}?", :"#{key}"
211
+ end
226
212
  end
227
213
 
228
214
  def coercion_mapping
@@ -268,7 +254,7 @@ module Anyway # :nodoc:
268
254
  names.each do |name|
269
255
  accessors_module.module_eval <<~RUBY, __FILE__, __LINE__ + 1
270
256
  def #{name}=(val)
271
- __trace__&.record_value(val, \"#{name}\", **Tracing.current_trace_source)
257
+ __trace__&.record_value(val, "#{name}", **Tracing.current_trace_source)
272
258
  values[:#{name}] = val
273
259
  end
274
260
 
@@ -430,13 +416,18 @@ module Anyway # :nodoc:
430
416
  end
431
417
  end
432
418
 
419
+ def as_env
420
+ Env.from_hash(to_h, prefix: env_prefix)
421
+ end
422
+
433
423
  private
434
424
 
435
425
  attr_reader :values, :__trace__
436
426
 
437
427
  def validate_required_attributes!
438
428
  self.class.required_attributes.select do |name|
439
- values[name].nil? || (values[name].is_a?(String) && values[name].empty?)
429
+ val = values.dig(*name.to_s.split(".").map(&:to_sym))
430
+ val.nil? || (val.is_a?(String) && val.empty?)
440
431
  end.then do |missing|
441
432
  next if missing.empty?
442
433
  raise_validation_error "The following config parameters for `#{self.class.name}(config_name: #{self.class.config_name})` are missing or empty: #{missing.join(", ")}"
@@ -8,14 +8,31 @@ module Anyway
8
8
  using Anyway::Ext::DeepDup
9
9
  using Anyway::Ext::Hash
10
10
 
11
+ class << self
12
+ def from_hash(hash, prefix: nil, memo: {})
13
+ hash.each do |key, value|
14
+ prefix_with_key = (prefix && !prefix.empty?) ? "#{prefix}_#{key.to_s.upcase}" : key.to_s.upcase
15
+
16
+ if value.is_a?(Hash)
17
+ from_hash(value, prefix: "#{prefix_with_key}_", memo: memo)
18
+ else
19
+ memo[prefix_with_key] = value.to_s
20
+ end
21
+ end
22
+
23
+ memo
24
+ end
25
+ end
26
+
11
27
  include Tracing
12
28
 
13
- attr_reader :data, :traces, :type_cast
29
+ attr_reader :data, :traces, :type_cast, :env_container
14
30
 
15
- def initialize(type_cast: AutoCast)
31
+ def initialize(type_cast: AutoCast, env_container: ENV)
16
32
  @type_cast = type_cast
17
33
  @data = {}
18
34
  @traces = {}
35
+ @env_container = env_container
19
36
  end
20
37
 
21
38
  def clear
@@ -42,11 +59,11 @@ module Anyway
42
59
  private
43
60
 
44
61
  def parse_env(prefix)
45
- match_prefix = "#{prefix}_"
46
- ENV.each_pair.with_object({}) do |(key, val), data|
62
+ match_prefix = prefix.empty? ? prefix : "#{prefix}_"
63
+ env_container.each_pair.with_object({}) do |(key, val), data|
47
64
  next unless key.start_with?(match_prefix)
48
65
 
49
- path = key.sub(/^#{prefix}_/, "").downcase
66
+ path = key.sub(/^#{match_prefix}/, "").downcase
50
67
 
51
68
  paths = path.split("__")
52
69
  trace!(:env, *paths, key: key) { data.bury(type_cast.call(val), *paths) }