anyway_config 2.3.0 → 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 +4 -4
- data/CHANGELOG.md +58 -0
- data/README.md +60 -1
- data/lib/.rbnext/2.6/anyway/ejson_parser.rb +40 -0
- data/lib/.rbnext/2.7/anyway/config.rb +21 -30
- data/lib/.rbnext/2.7/anyway/loaders/yaml.rb +2 -2
- data/lib/.rbnext/2.7/anyway/rbs.rb +1 -1
- data/lib/.rbnext/2.7/anyway/settings.rb +14 -0
- data/lib/.rbnext/2.7/anyway/tracing.rb +10 -4
- data/lib/.rbnext/2.7/anyway/type_casting.rb +14 -1
- data/lib/.rbnext/3.0/anyway/config.rb +21 -30
- data/lib/.rbnext/3.0/anyway/loaders.rb +2 -0
- data/lib/.rbnext/3.0/anyway/tracing.rb +10 -4
- data/lib/.rbnext/3.1/anyway/config.rb +21 -30
- data/lib/.rbnext/3.1/anyway/env.rb +22 -5
- data/lib/.rbnext/3.1/anyway/tracing.rb +8 -2
- data/lib/anyway/config.rb +21 -30
- data/lib/anyway/ejson_parser.rb +40 -0
- data/lib/anyway/env.rb +22 -5
- data/lib/anyway/ext/flatten_names.rb +37 -0
- data/lib/anyway/ext/hash.rb +8 -1
- data/lib/anyway/ext/string_constantize.rb +24 -0
- data/lib/anyway/loaders/doppler.rb +63 -0
- data/lib/anyway/loaders/ejson.rb +85 -0
- data/lib/anyway/loaders/yaml.rb +2 -2
- data/lib/anyway/loaders.rb +2 -0
- data/lib/anyway/rails/settings.rb +2 -0
- data/lib/anyway/settings.rb +14 -0
- data/lib/anyway/tracing.rb +8 -2
- data/lib/anyway/type_casting.rb +11 -1
- data/lib/anyway/utils/which.rb +18 -0
- data/lib/anyway/version.rb +1 -1
- data/lib/anyway_config.rb +7 -0
- data/sig/anyway_config.rbs +8 -3
- data/sig/manifest.yml +7 -0
- metadata +38 -15
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 44a860f50ae339b170da1a4ea99a7c6125b8ddcf2e25269c7341150bb8b8cf1f
|
4
|
+
data.tar.gz: f39c3ff413a1f334ad85a8f3eb0dfabca16dfc0c18c7f8719236607a777f947d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6b903f239c176e8c031fdea2d81f9d6d46d0657c2b5525f7e36fed495ab2410dc017bbe676fdc8c633d6b303c619e5caeb109d8c172d172720bfb3417746c612
|
7
|
+
data.tar.gz: 7bbb6db11016ce9c9473009c49aa676e3121ab921a2493ceb6d8e0e270cf75369935678bb1a2e5f69ebe387be1d5a44d159df819dcfab4a591aa67bab2e757fa
|
data/CHANGELOG.md
CHANGED
@@ -2,6 +2,61 @@
|
|
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
|
+
|
50
|
+
## 2.3.1 (2023-01-17)
|
51
|
+
|
52
|
+
- [Fixes [#110](https://github.com/palkan/anyway_config/issues/110)] Fix setting up autoloader for the same folder. ([@palkan][])
|
53
|
+
|
54
|
+
- RBS: Now `.on_load` automatically pass block context type (instance), so no need to add annotations! ([@palkan][])
|
55
|
+
|
56
|
+
Steep 1.2+ is required. Read more about the [feature](https://hackmd.io/xLrYaqUtQ1GhgTHODkYypw?view).
|
57
|
+
|
58
|
+
- Added `manifest.yml` for RBS. ([@palkan][])
|
59
|
+
|
5
60
|
## 2.3.0 (2022-03-11)
|
6
61
|
|
7
62
|
- Add ability to load configurations under specific environments in pure Ruby apps. ([@fargelus][]).
|
@@ -459,3 +514,6 @@ No we're dependency-free!
|
|
459
514
|
[@progapandist]: https://github.com/progapandist
|
460
515
|
[@skryukov]: https://github.com/skryukov
|
461
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 [
|
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
|
-
|
136
|
-
required_attributes.push(*names)
|
137
|
-
end
|
135
|
+
return unless Settings.matching_env?(env)
|
138
136
|
|
139
|
-
|
140
|
-
|
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,
|
257
|
+
__trace__&.record_value(val, "#{name}", **Tracing.current_trace_source)
|
272
258
|
values[:#{name}] = val
|
273
259
|
end
|
274
260
|
|
@@ -297,7 +283,7 @@ module Anyway # :nodoc:
|
|
297
283
|
# - SomeModule::Config => "some_module"
|
298
284
|
# - SomeConfig => "some"
|
299
285
|
unless name =~ /^(\w+)(::)?Config$/
|
300
|
-
raise "Couldn't infer config name, please, specify it explicitly" \
|
286
|
+
raise "Couldn't infer config name, please, specify it explicitly " \
|
301
287
|
"via `config_name :my_config`"
|
302
288
|
end
|
303
289
|
|
@@ -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
|
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(", ")}"
|
@@ -53,13 +53,13 @@ module Anyway
|
|
53
53
|
# the interface when no config file is present.
|
54
54
|
begin
|
55
55
|
if defined?(ERB)
|
56
|
-
::YAML.load(ERB.new(File.read(path)).result, aliases: true) || {}
|
56
|
+
::YAML.load(ERB.new(File.read(path)).result, aliases: true) || {}
|
57
57
|
else
|
58
58
|
::YAML.load_file(path, aliases: true) || {}
|
59
59
|
end
|
60
60
|
rescue ArgumentError
|
61
61
|
if defined?(ERB)
|
62
|
-
::YAML.load(ERB.new(File.read(path)).result) || {}
|
62
|
+
::YAML.load(ERB.new(File.read(path)).result) || {}
|
63
63
|
else
|
64
64
|
::YAML.load_file(path) || {}
|
65
65
|
end
|
@@ -44,7 +44,7 @@ module Anyway
|
|
44
44
|
TYPE_TO_CLASS.fetch(type) { defaults[param] ? "Symbol" : "untyped" }
|
45
45
|
when (Array === __m__)
|
46
46
|
"Array[untyped]"
|
47
|
-
when ((__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?))))
|
47
|
+
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?)))))
|
48
48
|
"Array[#{TYPE_TO_CLASS.fetch(type, "untyped")}]"
|
49
49
|
when (Hash === __m__)
|
50
50
|
"Hash[string,untyped]"
|
@@ -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,12 +19,12 @@ 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__)
|
26
26
|
value.dig(*__rest__, &__block__)
|
27
|
-
end
|
27
|
+
end; respond_to?(:ruby2_keywords, true) && (ruby2_keywords :dig)
|
28
28
|
|
29
29
|
def record_value(val, *path, **opts)
|
30
30
|
key = path.pop
|
@@ -35,7 +35,7 @@ module Anyway
|
|
35
35
|
end
|
36
36
|
|
37
37
|
target_trace = path.empty? ? self : value.dig(*path)
|
38
|
-
target_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?
|
@@ -70,7 +76,7 @@ module Anyway
|
|
70
76
|
def keep_if(*__rest__, &__block__)
|
71
77
|
raise ArgumentError, "You can only filter :trace type, and this is :#{type}" unless trace?
|
72
78
|
value.keep_if(*__rest__, &__block__)
|
73
|
-
end
|
79
|
+
end; respond_to?(:ruby2_keywords, true) && (ruby2_keywords :keep_if)
|
74
80
|
|
75
81
|
def clear() ; value.clear; end
|
76
82
|
|
@@ -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 ((__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
|
-
|
136
|
-
required_attributes.push(*names)
|
137
|
-
end
|
135
|
+
return unless Settings.matching_env?(env)
|
138
136
|
|
139
|
-
|
140
|
-
|
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,
|
257
|
+
__trace__&.record_value(val, "#{name}", **Tracing.current_trace_source)
|
272
258
|
values[:#{name}] = val
|
273
259
|
end
|
274
260
|
|
@@ -297,7 +283,7 @@ module Anyway # :nodoc:
|
|
297
283
|
# - SomeModule::Config => "some_module"
|
298
284
|
# - SomeConfig => "some"
|
299
285
|
unless name =~ /^(\w+)(::)?Config$/
|
300
|
-
raise "Couldn't infer config name, please, specify it explicitly" \
|
286
|
+
raise "Couldn't infer config name, please, specify it explicitly " \
|
301
287
|
"via `config_name :my_config`"
|
302
288
|
end
|
303
289
|
|
@@ -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
|
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(", ")}"
|
@@ -19,12 +19,12 @@ 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(...)
|
26
26
|
value.dig(...)
|
27
|
-
end
|
27
|
+
end; respond_to?(:ruby2_keywords, true) && (ruby2_keywords :dig)
|
28
28
|
|
29
29
|
def record_value(val, *path, **opts)
|
30
30
|
key = path.pop
|
@@ -35,7 +35,7 @@ module Anyway
|
|
35
35
|
end
|
36
36
|
|
37
37
|
target_trace = path.empty? ? self : value.dig(*path)
|
38
|
-
target_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?
|
@@ -70,7 +76,7 @@ module Anyway
|
|
70
76
|
def keep_if(...)
|
71
77
|
raise ArgumentError, "You can only filter :trace type, and this is :#{type}" unless trace?
|
72
78
|
value.keep_if(...)
|
73
|
-
end
|
79
|
+
end; respond_to?(:ruby2_keywords, true) && (ruby2_keywords :keep_if)
|
74
80
|
|
75
81
|
def clear() ; value.clear; end
|
76
82
|
|