anyway_config 2.3.1 → 2.4.0

Sign up to get free protection for your applications and to get access to all the features.
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) }