anyway_config 2.2.1 → 2.3.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: a0f3278ebf47801b6fdd2836c3f9f63d9a8fc1fe324f04e30f74d2dca179f471
4
- data.tar.gz: 8be597f8b2c6065f0ea96e5c58ec54d6ef3532f00d7a8639a963e99b2bba65c9
3
+ metadata.gz: fe4f769be22bc9cb820fd1159b88a565b2fb71e13ad617d90aaafbcb1673f995
4
+ data.tar.gz: 175e468716722b43b6c7d163a97b6523801f6658118d1c56bb5a4a0255807ef9
5
5
  SHA512:
6
- metadata.gz: 8174b86b24e86de09a55e473bcec74fc37aeca1c38bb2a75e83e71dbeebf512b68fdb93ae23d247cb6a8ae6c5d1e2fbb86d659bf611f83ad219fccabad1ba7b6
7
- data.tar.gz: dcf73ae92079a78b8c518cc3059a85848dd4934633269d016b402114279a0303108ab7962477ea2172ff15a1abc9c6401cea9106a83ba9b4c46a02220264eb23
6
+ metadata.gz: 7c323f89197ca35405ca77ee1e58a8b3fbbf4b0d6b57e7ccce380311a13994d18121966428a2681cb55a5892c2eff52ec347a6e3568072ea8f85d1cf7f75aa80
7
+ data.tar.gz: 25ee0b877318a2335c2019a414684d5b15e3755ceff90e51de3a0b821ee48f86a119f9753eaddac5dfb4724e38876ec5f18b68b065db7aa4b23d20b837666929
data/CHANGELOG.md CHANGED
@@ -2,6 +2,28 @@
2
2
 
3
3
  ## master
4
4
 
5
+ ## 2.3.0 (2022-03-11)
6
+
7
+ - Add ability to load configurations under specific environments in pure Ruby apps. ([@fargelus][]).
8
+
9
+ Before loading environment configurations you need to specify variable `Anyway::Settings.current_environment`. In Rails apps this variable is same as `Rails.env` value.
10
+ After adding yaml loader will try to load params under this environment.
11
+
12
+ Also required env option was added to `Anyway::Config`.
13
+
14
+ ## 2.2.3 (2022-01-21)
15
+
16
+ - Fix Ruby 3.1 compatibility. ([@palkan][])
17
+
18
+ - Add ability to set default key for environmental YAML files. ([@skryukov])
19
+
20
+ Define a key for environmental yaml files to read default values from with `config.anyway_config.default_environmental_key = "default"`.
21
+ This way Anyway Config will try to read settings under the `"default"` key and then merge environmental settings into them.
22
+
23
+ ## 2.2.2 (2020-10-26)
24
+
25
+ - Fixed regression introduced by the `#deep_merge!` refinement.
26
+
5
27
  ## 2.2.1 (2020-09-28)
6
28
 
7
29
  - Minor fixes to the prev release.
@@ -435,3 +457,5 @@ No we're dependency-free!
435
457
  [@jastkand]: https://github.com/jastkand
436
458
  [@envek]: https://github.com/Envek
437
459
  [@progapandist]: https://github.com/progapandist
460
+ [@skryukov]: https://github.com/skryukov
461
+ [@fargelus]: https://github.com/fargelus
data/README.md CHANGED
@@ -297,6 +297,24 @@ end
297
297
  MyConfig.new(api_secret: "") #=> raises Anyway::Config::ValidationError
298
298
  ```
299
299
 
300
+ `Required` method supports additional `env` parameter which indicates necessity to run validations under specified
301
+ environments. `Env` parameter could be present in symbol, string, array or hash formats:
302
+
303
+ ```ruby
304
+ class EnvConfig < Anyway::Config
305
+ required :password, env: "production"
306
+ required :maps_api_key, env: :production
307
+ required :smtp_host, env: %i[production staging]
308
+ required :aws_bucket, env: %w[production staging]
309
+ required :anycable_rpc_host, env: {except: :development}
310
+ required :anycable_redis_url, env: {except: %i[development test]}
311
+ required :anycable_broadcast_adapter, env: {except: %w[development test]}
312
+ end
313
+ ```
314
+
315
+ If your current `Anyway::Settings.current_environment` is mismatch keys that specified
316
+ `Anyway::Config::ValidationError` error will be raised.
317
+
300
318
  If you need more complex validation or need to manipulate with config state right after it has been loaded, you can use _on load callbacks_ and `#raise_validation_error` method:
301
319
 
302
320
  ```ruby
@@ -373,6 +391,26 @@ staging:
373
391
  port: 3002 # This value will not be loaded at all
374
392
  ```
375
393
 
394
+ To provide default values you can use YAML anchors, but they do not deep-merge settings, so Anyway Config provides a way to define a special top-level key for default values like this:
395
+
396
+ ```ruby
397
+ config.anyway_config.default_environmental_key = "default"
398
+ ```
399
+
400
+ After that, Anyway Config will start reading settings under the `"default"` key and then merge environmental settings into them.
401
+
402
+ ```yml
403
+ default:
404
+ server: # This values will be loaded in all environments by default
405
+ host: localhost
406
+ port: 3002
407
+
408
+ staging:
409
+ server:
410
+ host: staging.example.com # This value will override the defaults when Rails.env.staging? is true
411
+ # port will be set to the value from the defaults — 3002
412
+ ```
413
+
376
414
  You can specify the lookup path for YAML files in one of the following ways:
377
415
 
378
416
  - By setting `config.anyway_config.default_config_path` to a target directory path:
@@ -509,7 +547,22 @@ The default data loading mechanism for non-Rails applications is the following (
509
547
 
510
548
  1) **YAML configuration files**: `./config/<config-name>.yml`.
511
549
 
512
- In pure Ruby apps, we do not know about _environments_ (`test`, `development`, `production`, etc.); thus, we assume that the YAML contains values for a single environment:
550
+ In pure Ruby apps, we also can load data under specific _environments_ (`test`, `development`, `production`, etc.).
551
+ If you want to enable this feature you must specify `Anyway::Settings.current_environment` variable for load config under specific environment.
552
+
553
+ ```ruby
554
+ Anyway::Settings.current_environment = "development"
555
+ ```
556
+
557
+ YAML files should be in this format:
558
+
559
+ ```yml
560
+ development:
561
+ host: localhost
562
+ port: 3000
563
+ ```
564
+
565
+ If `Anyway::Settings.current_environment` is missed we assume that the YAML contains values for a single environment:
513
566
 
514
567
  ```yml
515
568
  host: localhost
@@ -4,8 +4,8 @@ module Anyway
4
4
  module AutoCast
5
5
  # Regexp to detect array values
6
6
  # Array value is a values that contains at least one comma
7
- # and doesn't start/end with quote
8
- ARRAY_RXP = /\A[^'"].*\s*,\s*.*[^'"]\z/
7
+ # and doesn't start/end with quote or curly braces
8
+ ARRAY_RXP = /\A[^'"{].*\s*,\s*.*[^'"}]\z/
9
9
 
10
10
  class << self
11
11
  def call(val)
@@ -47,6 +47,8 @@ module Anyway # :nodoc:
47
47
  __type_caster__
48
48
  ].freeze
49
49
 
50
+ ENV_OPTION_EXCLUDE_KEY = :except
51
+
50
52
  class Error < StandardError; end
51
53
 
52
54
  class ValidationError < Error; end
@@ -126,14 +128,36 @@ module Anyway # :nodoc:
126
128
  end
127
129
  end
128
130
 
129
- def required(*names)
130
- unless (unknown_names = (names - config_attributes)).empty?
131
- raise ArgumentError, "Unknown config param: #{unknown_names.join(",")}"
132
- end
131
+ def required(*names, env: nil)
132
+ unknown_names = names - config_attributes
133
+ raise ArgumentError, "Unknown config param: #{unknown_names.join(",")}" if unknown_names.any?
133
134
 
135
+ names = filter_by_env(names, env)
134
136
  required_attributes.push(*names)
135
137
  end
136
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
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)
159
+ end
160
+
137
161
  def required_attributes
138
162
  return @required_attributes if instance_variable_defined?(:@required_attributes)
139
163
 
@@ -198,7 +222,7 @@ module Anyway # :nodoc:
198
222
  def new_empty_config() ; {}; end
199
223
 
200
224
  def coerce_types(mapping)
201
- coercion_mapping.deep_merge!(mapping)
225
+ Utils.deep_merge!(coercion_mapping, mapping)
202
226
  end
203
227
 
204
228
  def coercion_mapping
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pathname"
4
+ require "anyway/ext/hash"
5
+
6
+ using RubyNext
7
+ using Anyway::Ext::Hash
8
+
9
+ module Anyway
10
+ module Loaders
11
+ class YAML < Base
12
+ def call(config_path:, **_options)
13
+ rel_config_path = relative_config_path(config_path).to_s
14
+ base_config = trace!(:yml, path: rel_config_path) do
15
+ config = load_base_yml(config_path)
16
+ environmental?(config) ? config_with_env(config) : config
17
+ end
18
+
19
+ return base_config unless use_local?
20
+
21
+ local_path = local_config_path(config_path)
22
+ local_config = trace!(:yml, path: relative_config_path(local_path).to_s) { load_local_yml(local_path) }
23
+ Utils.deep_merge!(base_config, local_config)
24
+ end
25
+
26
+ private
27
+
28
+ def environmental?(parsed_yml)
29
+ # strange, but still possible
30
+ return true if Settings.default_environmental_key? && parsed_yml.key?(Settings.default_environmental_key)
31
+ # possible
32
+ return true if !Settings.future.unwrap_known_environments && Settings.current_environment
33
+ # for other environments
34
+ return true if Settings.known_environments&.any? { |_1| parsed_yml.key?(_1) }
35
+ # preferred
36
+ parsed_yml.key?(Settings.current_environment)
37
+ end
38
+
39
+ def config_with_env(config)
40
+ env_config = config[Settings.current_environment] || {}
41
+ return env_config unless Settings.default_environmental_key?
42
+
43
+ default_config = config[Settings.default_environmental_key] || {}
44
+ Utils.deep_merge!(default_config, env_config)
45
+ end
46
+
47
+ def parse_yml(path)
48
+ return {} unless File.file?(path)
49
+ require "yaml" unless defined?(::YAML)
50
+
51
+ # By default, YAML load will return `false` when the yaml document is
52
+ # empty. When this occurs, we return an empty hash instead, to match
53
+ # the interface when no config file is present.
54
+ begin
55
+ if defined?(ERB)
56
+ ::YAML.load(ERB.new(File.read(path)).result, aliases: true) || {} # rubocop:disable Security/YAMLLoad
57
+ else
58
+ ::YAML.load_file(path, aliases: true) || {}
59
+ end
60
+ rescue ArgumentError
61
+ if defined?(ERB)
62
+ ::YAML.load(ERB.new(File.read(path)).result) || {} # rubocop:disable Security/YAMLLoad
63
+ else
64
+ ::YAML.load_file(path) || {}
65
+ end
66
+ end
67
+ end
68
+
69
+ alias_method :load_base_yml, :parse_yml
70
+ alias_method :load_local_yml, :parse_yml
71
+
72
+ def local_config_path(path)
73
+ path.sub(/\.yml/, ".local.yml")
74
+ end
75
+
76
+ def relative_config_path(path)
77
+ Pathname.new(path).then do |path|
78
+ return path if path.relative?
79
+ path.relative_path_from(Settings.app_root)
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "pathname"
4
+
3
5
  module Anyway
4
6
  # Use Settings name to not confuse with Config.
5
7
  #
@@ -35,6 +37,8 @@ module Anyway
35
37
  names.each { |_1| store[_1] = self.class.settings[_1] }
36
38
  end
37
39
 
40
+ setting :unwrap_known_environments, true
41
+
38
42
  private
39
43
 
40
44
  attr_reader :store
@@ -43,7 +47,10 @@ module Anyway
43
47
  class << self
44
48
  # Define whether to load data from
45
49
  # *.yml.local (or credentials/local.yml.enc)
46
- attr_accessor :use_local_files
50
+ attr_accessor :use_local_files,
51
+ :current_environment,
52
+ :default_environmental_key,
53
+ :known_environments
47
54
 
48
55
  # A proc returning a path to YML config file given the config name
49
56
  attr_reader :default_config_path
@@ -65,6 +72,14 @@ module Anyway
65
72
  def future
66
73
  @future ||= Future.new
67
74
  end
75
+
76
+ def app_root
77
+ Pathname.new(Dir.pwd)
78
+ end
79
+
80
+ def default_environmental_key?
81
+ !default_environmental_key.nil?
82
+ end
68
83
  end
69
84
 
70
85
  # By default, use local files only in development (that's the purpose if the local files)
@@ -6,12 +6,6 @@ module Anyway
6
6
  using Anyway::Ext::DeepDup
7
7
 
8
8
  using(Module.new do
9
- refine Hash do
10
- def inspect
11
- "{#{map { |k, v| "#{k}: #{v.inspect}" }.join(", ")}}"
12
- end
13
- end
14
-
15
9
  refine Thread::Backtrace::Location do
16
10
  def path_lineno() ; "#{path}:#{lineno}"; end
17
11
  end
@@ -100,8 +94,15 @@ module Anyway
100
94
  q.group do
101
95
  q.text k
102
96
  q.text " =>"
103
- q.breakable " " unless v.trace?
104
- q.pp v
97
+ if v.trace?
98
+ q.text " { "
99
+ q.pp v
100
+ q.breakable " "
101
+ q.text "}"
102
+ else
103
+ q.breakable " "
104
+ q.pp v
105
+ end
105
106
  end
106
107
  end
107
108
  end
@@ -4,8 +4,8 @@ module Anyway
4
4
  module AutoCast
5
5
  # Regexp to detect array values
6
6
  # Array value is a values that contains at least one comma
7
- # and doesn't start/end with quote
8
- ARRAY_RXP = /\A[^'"].*\s*,\s*.*[^'"]\z/
7
+ # and doesn't start/end with quote or curly braces
8
+ ARRAY_RXP = /\A[^'"{].*\s*,\s*.*[^'"}]\z/
9
9
 
10
10
  class << self
11
11
  def call(val)
@@ -47,6 +47,8 @@ module Anyway # :nodoc:
47
47
  __type_caster__
48
48
  ].freeze
49
49
 
50
+ ENV_OPTION_EXCLUDE_KEY = :except
51
+
50
52
  class Error < StandardError; end
51
53
 
52
54
  class ValidationError < Error; end
@@ -126,14 +128,36 @@ module Anyway # :nodoc:
126
128
  end
127
129
  end
128
130
 
129
- def required(*names)
130
- unless (unknown_names = (names - config_attributes)).empty?
131
- raise ArgumentError, "Unknown config param: #{unknown_names.join(",")}"
132
- end
131
+ def required(*names, env: nil)
132
+ unknown_names = names - config_attributes
133
+ raise ArgumentError, "Unknown config param: #{unknown_names.join(",")}" if unknown_names.any?
133
134
 
135
+ names = filter_by_env(names, env)
134
136
  required_attributes.push(*names)
135
137
  end
136
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
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)
159
+ end
160
+
137
161
  def required_attributes
138
162
  return @required_attributes if instance_variable_defined?(:@required_attributes)
139
163
 
@@ -198,7 +222,7 @@ module Anyway # :nodoc:
198
222
  def new_empty_config() ; {}; end
199
223
 
200
224
  def coerce_types(mapping)
201
- coercion_mapping.deep_merge!(mapping)
225
+ Utils.deep_merge!(coercion_mapping, mapping)
202
226
  end
203
227
 
204
228
  def coercion_mapping
@@ -6,12 +6,6 @@ module Anyway
6
6
  using Anyway::Ext::DeepDup
7
7
 
8
8
  using(Module.new do
9
- refine Hash do
10
- def inspect
11
- "{#{map { |k, v| "#{k}: #{v.inspect}" }.join(", ")}}"
12
- end
13
- end
14
-
15
9
  refine Thread::Backtrace::Location do
16
10
  def path_lineno() ; "#{path}:#{lineno}"; end
17
11
  end
@@ -100,8 +94,15 @@ module Anyway
100
94
  q.group do
101
95
  q.text k
102
96
  q.text " =>"
103
- q.breakable " " unless v.trace?
104
- q.pp v
97
+ if v.trace?
98
+ q.text " { "
99
+ q.pp v
100
+ q.breakable " "
101
+ q.text "}"
102
+ else
103
+ q.breakable " "
104
+ q.pp v
105
+ end
105
106
  end
106
107
  end
107
108
  end
@@ -47,6 +47,8 @@ module Anyway # :nodoc:
47
47
  __type_caster__
48
48
  ].freeze
49
49
 
50
+ ENV_OPTION_EXCLUDE_KEY = :except
51
+
50
52
  class Error < StandardError; end
51
53
 
52
54
  class ValidationError < Error; end
@@ -126,14 +128,36 @@ module Anyway # :nodoc:
126
128
  end
127
129
  end
128
130
 
129
- def required(*names)
130
- unless (unknown_names = (names - config_attributes)).empty?
131
- raise ArgumentError, "Unknown config param: #{unknown_names.join(",")}"
132
- end
131
+ def required(*names, env: nil)
132
+ unknown_names = names - config_attributes
133
+ raise ArgumentError, "Unknown config param: #{unknown_names.join(",")}" if unknown_names.any?
133
134
 
135
+ names = filter_by_env(names, env)
134
136
  required_attributes.push(*names)
135
137
  end
136
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
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)
159
+ end
160
+
137
161
  def required_attributes
138
162
  return @required_attributes if instance_variable_defined?(:@required_attributes)
139
163
 
@@ -198,7 +222,7 @@ module Anyway # :nodoc:
198
222
  def new_empty_config() = {}
199
223
 
200
224
  def coerce_types(mapping)
201
- coercion_mapping.deep_merge!(mapping)
225
+ Utils.deep_merge!(coercion_mapping, mapping)
202
226
  end
203
227
 
204
228
  def coercion_mapping
File without changes
@@ -6,12 +6,6 @@ module Anyway
6
6
  using Anyway::Ext::DeepDup
7
7
 
8
8
  using(Module.new do
9
- refine Hash do
10
- def inspect
11
- "{#{map { |k, v| "#{k}: #{v.inspect}" }.join(", ")}}"
12
- end
13
- end
14
-
15
9
  refine Thread::Backtrace::Location do
16
10
  def path_lineno() = "#{path}:#{lineno}"
17
11
  end
@@ -100,8 +94,15 @@ module Anyway
100
94
  q.group do
101
95
  q.text k
102
96
  q.text " =>"
103
- q.breakable " " unless v.trace?
104
- q.pp v
97
+ if v.trace?
98
+ q.text " { "
99
+ q.pp v
100
+ q.breakable " "
101
+ q.text "}"
102
+ else
103
+ q.breakable " "
104
+ q.pp v
105
+ end
105
106
  end
106
107
  end
107
108
  end
@@ -4,8 +4,8 @@ module Anyway
4
4
  module AutoCast
5
5
  # Regexp to detect array values
6
6
  # Array value is a values that contains at least one comma
7
- # and doesn't start/end with quote
8
- ARRAY_RXP = /\A[^'"].*\s*,\s*.*[^'"]\z/
7
+ # and doesn't start/end with quote or curly braces
8
+ ARRAY_RXP = /\A[^'"{].*\s*,\s*.*[^'"}]\z/
9
9
 
10
10
  class << self
11
11
  def call(val)
data/lib/anyway/config.rb CHANGED
@@ -47,6 +47,8 @@ module Anyway # :nodoc:
47
47
  __type_caster__
48
48
  ].freeze
49
49
 
50
+ ENV_OPTION_EXCLUDE_KEY = :except
51
+
50
52
  class Error < StandardError; end
51
53
 
52
54
  class ValidationError < Error; end
@@ -126,14 +128,36 @@ module Anyway # :nodoc:
126
128
  end
127
129
  end
128
130
 
129
- def required(*names)
130
- unless (unknown_names = (names - config_attributes)).empty?
131
- raise ArgumentError, "Unknown config param: #{unknown_names.join(",")}"
132
- end
131
+ def required(*names, env: nil)
132
+ unknown_names = names - config_attributes
133
+ raise ArgumentError, "Unknown config param: #{unknown_names.join(",")}" if unknown_names.any?
133
134
 
135
+ names = filter_by_env(names, env)
134
136
  required_attributes.push(*names)
135
137
  end
136
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
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)
159
+ end
160
+
137
161
  def required_attributes
138
162
  return @required_attributes if instance_variable_defined?(:@required_attributes)
139
163
 
@@ -198,7 +222,7 @@ module Anyway # :nodoc:
198
222
  def new_empty_config() = {}
199
223
 
200
224
  def coerce_types(mapping)
201
- coercion_mapping.deep_merge!(mapping)
225
+ Utils.deep_merge!(coercion_mapping, mapping)
202
226
  end
203
227
 
204
228
  def coercion_mapping
@@ -26,16 +26,6 @@ module Anyway
26
26
  end
27
27
  hash[last_key] = val
28
28
  end
29
-
30
- def deep_merge!(other)
31
- other.each do |k, v|
32
- if key?(k) && self[k].is_a?(::Hash) && v.is_a?(::Hash)
33
- self[k].deep_merge!(v)
34
- else
35
- self[k] = v
36
- end
37
- end
38
- end
39
29
  end
40
30
 
41
31
  using self
@@ -10,18 +10,40 @@ module Anyway
10
10
  module Loaders
11
11
  class YAML < Base
12
12
  def call(config_path:, **_options)
13
- base_config = trace!(:yml, path: relative_config_path(config_path).to_s) { load_base_yml(config_path) }
13
+ rel_config_path = relative_config_path(config_path).to_s
14
+ base_config = trace!(:yml, path: rel_config_path) do
15
+ config = load_base_yml(config_path)
16
+ environmental?(config) ? config_with_env(config) : config
17
+ end
14
18
 
15
19
  return base_config unless use_local?
16
20
 
17
21
  local_path = local_config_path(config_path)
18
22
  local_config = trace!(:yml, path: relative_config_path(local_path).to_s) { load_local_yml(local_path) }
19
-
20
23
  Utils.deep_merge!(base_config, local_config)
21
24
  end
22
25
 
23
26
  private
24
27
 
28
+ def environmental?(parsed_yml)
29
+ # strange, but still possible
30
+ return true if Settings.default_environmental_key? && parsed_yml.key?(Settings.default_environmental_key)
31
+ # possible
32
+ return true if !Settings.future.unwrap_known_environments && Settings.current_environment
33
+ # for other environments
34
+ return true if Settings.known_environments&.any? { parsed_yml.key?(_1) }
35
+ # preferred
36
+ parsed_yml.key?(Settings.current_environment)
37
+ end
38
+
39
+ def config_with_env(config)
40
+ env_config = config[Settings.current_environment] || {}
41
+ return env_config unless Settings.default_environmental_key?
42
+
43
+ default_config = config[Settings.default_environmental_key] || {}
44
+ Utils.deep_merge!(default_config, env_config)
45
+ end
46
+
25
47
  def parse_yml(path)
26
48
  return {} unless File.file?(path)
27
49
  require "yaml" unless defined?(::YAML)
@@ -29,10 +51,18 @@ module Anyway
29
51
  # By default, YAML load will return `false` when the yaml document is
30
52
  # empty. When this occurs, we return an empty hash instead, to match
31
53
  # the interface when no config file is present.
32
- if defined?(ERB)
33
- ::YAML.load(ERB.new(File.read(path)).result) || {} # rubocop:disable Security/YAMLLoad
34
- else
35
- ::YAML.load_file(path) || {}
54
+ begin
55
+ if defined?(ERB)
56
+ ::YAML.load(ERB.new(File.read(path)).result, aliases: true) || {} # rubocop:disable Security/YAMLLoad
57
+ else
58
+ ::YAML.load_file(path, aliases: true) || {}
59
+ end
60
+ rescue ArgumentError
61
+ if defined?(ERB)
62
+ ::YAML.load(ERB.new(File.read(path)).result) || {} # rubocop:disable Security/YAMLLoad
63
+ else
64
+ ::YAML.load_file(path) || {}
65
+ end
36
66
  end
37
67
  end
38
68
 
@@ -46,7 +76,7 @@ module Anyway
46
76
  def relative_config_path(path)
47
77
  Pathname.new(path).then do |path|
48
78
  return path if path.relative?
49
- path.relative_path_from(Pathname.new(Dir.pwd))
79
+ path.relative_path_from(Settings.app_root)
50
80
  end
51
81
  end
52
82
  end
@@ -3,28 +3,7 @@
3
3
  module Anyway
4
4
  module Rails
5
5
  module Loaders
6
- class YAML < Anyway::Loaders::YAML
7
- def load_base_yml(*)
8
- parsed_yml = super
9
- return parsed_yml unless environmental?(parsed_yml)
10
-
11
- super[::Rails.env] || {}
12
- end
13
-
14
- private
15
-
16
- def environmental?(parsed_yml)
17
- return true unless Settings.future.unwrap_known_environments
18
- # likely
19
- return true if parsed_yml.key?(::Rails.env)
20
- # less likely
21
- ::Rails.application.config.anyway_config.known_environments.any? { parsed_yml.key?(_1) }
22
- end
23
-
24
- def relative_config_path(path)
25
- Pathname.new(path).relative_path_from(::Rails.root)
26
- end
27
- end
6
+ class YAML < Anyway::Loaders::YAML; end
28
7
  end
29
8
  end
30
9
  end
@@ -9,13 +9,8 @@ end
9
9
 
10
10
  module Anyway
11
11
  class Settings
12
- class Future
13
- setting :unwrap_known_environments, true
14
- end
15
-
16
12
  class << self
17
13
  attr_reader :autoload_static_config_path, :autoloader
18
- attr_accessor :known_environments
19
14
 
20
15
  if defined?(::Zeitwerk)
21
16
  def autoload_static_config_path=(val)
@@ -59,9 +54,19 @@ module Anyway
59
54
  :no_op
60
55
  end
61
56
  end
57
+
58
+ def current_environment
59
+ ::Rails.env.to_s
60
+ end
61
+
62
+ def app_root
63
+ ::Rails.root
64
+ end
62
65
  end
63
66
 
64
67
  self.default_config_path = ->(name) { ::Rails.root.join("config", "#{name}.yml") }
65
68
  self.known_environments = %w[test development production]
69
+ # Don't try read defaults when no key defined
70
+ self.default_environmental_key = nil
66
71
  end
67
72
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "pathname"
4
+
3
5
  module Anyway
4
6
  # Use Settings name to not confuse with Config.
5
7
  #
@@ -35,6 +37,8 @@ module Anyway
35
37
  names.each { store[_1] = self.class.settings[_1] }
36
38
  end
37
39
 
40
+ setting :unwrap_known_environments, true
41
+
38
42
  private
39
43
 
40
44
  attr_reader :store
@@ -43,7 +47,10 @@ module Anyway
43
47
  class << self
44
48
  # Define whether to load data from
45
49
  # *.yml.local (or credentials/local.yml.enc)
46
- attr_accessor :use_local_files
50
+ attr_accessor :use_local_files,
51
+ :current_environment,
52
+ :default_environmental_key,
53
+ :known_environments
47
54
 
48
55
  # A proc returning a path to YML config file given the config name
49
56
  attr_reader :default_config_path
@@ -65,6 +72,14 @@ module Anyway
65
72
  def future
66
73
  @future ||= Future.new
67
74
  end
75
+
76
+ def app_root
77
+ Pathname.new(Dir.pwd)
78
+ end
79
+
80
+ def default_environmental_key?
81
+ !default_environmental_key.nil?
82
+ end
68
83
  end
69
84
 
70
85
  # By default, use local files only in development (that's the purpose if the local files)
@@ -6,12 +6,6 @@ module Anyway
6
6
  using Anyway::Ext::DeepDup
7
7
 
8
8
  using(Module.new do
9
- refine Hash do
10
- def inspect
11
- "{#{map { |k, v| "#{k}: #{v.inspect}" }.join(", ")}}"
12
- end
13
- end
14
-
15
9
  refine Thread::Backtrace::Location do
16
10
  def path_lineno() = "#{path}:#{lineno}"
17
11
  end
@@ -86,7 +80,7 @@ module Anyway
86
80
  if trace?
87
81
  value.transform_values(&:to_h).tap { _1.default_proc = nil }
88
82
  else
89
- {value, source}
83
+ {value:, source:}
90
84
  end
91
85
  end
92
86
 
@@ -100,8 +94,15 @@ module Anyway
100
94
  q.group do
101
95
  q.text k
102
96
  q.text " =>"
103
- q.breakable " " unless v.trace?
104
- q.pp v
97
+ if v.trace?
98
+ q.text " { "
99
+ q.pp v
100
+ q.breakable " "
101
+ q.text "}"
102
+ else
103
+ q.breakable " "
104
+ q.pp v
105
+ end
105
106
  end
106
107
  end
107
108
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Anyway # :nodoc:
4
- VERSION = "2.2.1"
4
+ VERSION = "2.3.0"
5
5
  end
@@ -4,7 +4,7 @@
4
4
  class ApplicationConfig < Anyway::Config
5
5
  class << self
6
6
  # Make it possible to access a singleton config instance
7
- # via class methods (i.e., without explictly calling `instance`)
7
+ # via class methods (i.e., without explicitly calling `instance`)
8
8
  delegate_missing_to :instance
9
9
 
10
10
  private
@@ -3,8 +3,11 @@ module Anyway
3
3
  def self.loaders: -> Loaders::Registry
4
4
 
5
5
  class Settings
6
- def self.default_config_path=: (^(untyped) -> String val) -> ^(untyped) -> String?
6
+ def self.default_config_path=: (String | Pathname | ^(untyped) -> String val) -> void
7
7
  def self.future: -> Future
8
+ def self.current_environment: -> String?
9
+ def self.default_environmental_key: -> String?
10
+ def self.known_environments: -> Array[String]?
8
11
 
9
12
  class Future
10
13
  def self.setting: (untyped name, untyped default_value) -> untyped
@@ -19,14 +22,14 @@ module Anyway
19
22
  end
20
23
 
21
24
  def inspect: -> String
22
- def self.capture: ?{ -> Hash[untyped, untyped] } -> nil
25
+ def self.capture: ?{ -> Hash[untyped, untyped]? } -> Trace
23
26
  def self.trace_stack: -> Array[untyped]
24
27
  def self.current_trace: -> Trace?
25
28
  def self.source_stack: -> Array[untyped]
26
- def self.current_trace_source: -> {type: :accessor, called_from: untyped}
27
- def self.with_trace_source: (untyped src) -> untyped
28
- def trace!: (Symbol, *Array[String] paths, **untyped) ?{ -> Hash[untyped, untyped]} -> Hash[untyped, untyped]
29
- def self.trace!: (Symbol, *Array[String] paths, **untyped) ?{ -> Hash[untyped, untyped]} -> Hash[untyped, untyped]
29
+ def self.current_trace_source: -> ({type: Symbol} & Hash[Symbol, untyped])
30
+ def self.with_trace_source: (untyped src) { -> void } -> untyped
31
+ def trace!: [V] (Symbol, *String paths, **untyped) ?{ -> V} -> V
32
+ def self.trace!: [V] (Symbol, *String paths, **untyped) ?{ -> V} -> V
30
33
  end
31
34
 
32
35
  module RBSGenerator
@@ -56,6 +59,7 @@ module Anyway
56
59
  type hashType = Hash[Symbol, valueType | arrayType | hashType]
57
60
 
58
61
  type mappingType = valueType | arrayType | hashType
62
+ type envType = String | Symbol | Array[String | Symbol] | {except: String | Symbol | Array[String | Symbol]}
59
63
 
60
64
  class Config
61
65
  extend RBSGenerator
@@ -67,7 +71,7 @@ module Anyway
67
71
  def self.attr_config: (*Symbol args, **untyped) -> void
68
72
  def self.defaults: -> Hash[String, untyped]
69
73
  def self.config_attributes: -> Array[Symbol]?
70
- def self.required: (*Symbol names) -> void
74
+ def self.required: (*Symbol names, ?env: envType) -> void
71
75
  def self.required_attributes: -> Array[Symbol]
72
76
  def self.on_load: (*Symbol callbacks) ?{ () -> void } -> void
73
77
  def self.config_name: (?(Symbol | String) val) -> String?
@@ -80,9 +84,11 @@ module Anyway
80
84
  attr_reader env_prefix: String
81
85
 
82
86
  def initialize: (?Hash[Symbol | String, untyped] overrides) -> void
87
+ | (NilClass) -> void
83
88
  def reload: (?Hash[Symbol | String, untyped] overrides) -> Config
84
89
  def clear: -> void
85
90
  def load: (Hash[Symbol | String, untyped] overrides) -> Config
91
+ | (NilClass) -> Config
86
92
  def dig: (*(Symbol | String) keys) -> untyped
87
93
  def to_h: -> Hash[untyped, untyped]
88
94
  def dup: -> Config
@@ -117,12 +123,21 @@ module Anyway
117
123
  def use_local?: -> bool
118
124
  end
119
125
 
126
+ interface _Loader
127
+ def call: (**untyped) -> Hash[untyped, untyped]
128
+ end
129
+
120
130
  class Registry
121
- def prepend: (Symbol id, Base loader) -> void
122
- def append: (Symbol id, Base loader) -> void
123
- def insert_before: (Symbol another_id, Symbol id, Base loader) -> void
124
- def insert_after: (Symbol another_id, Symbol id, Base loader) -> void
125
- def override: (Symbol id, Base loader) -> void
131
+ def prepend: (Symbol id, _Loader loader) -> void
132
+ | (Symbol id) { (**untyped) -> Hash[untyped, untyped] } -> void
133
+ def append: (Symbol id, _Loader loader) -> void
134
+ | (Symbol id) { (**untyped) -> Hash[untyped, untyped] } -> void
135
+ def insert_before: (Symbol another_id, Symbol id, _Loader loader) -> void
136
+ | (Symbol another_id, Symbol id) { (**untyped) -> Hash[untyped, untyped] } -> void
137
+ def insert_after: (Symbol another_id, Symbol id, _Loader loader) -> void
138
+ | (Symbol another_id, Symbol id) { (**untyped) -> Hash[untyped, untyped] } -> void
139
+ def override: (Symbol id, _Loader loader) -> void
140
+ | (Symbol id) { (**untyped) -> Hash[untyped, untyped] } -> void
126
141
  def delete: (Symbol id) -> void
127
142
  end
128
143
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: anyway_config
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.2.1
4
+ version: 2.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Vladimir Dementyev
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-09-28 00:00:00.000000000 Z
11
+ date: 2022-03-11 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: ruby-next-core
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: 0.11.0
19
+ version: 0.14.0
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
- version: 0.11.0
26
+ version: 0.14.0
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: ammeter
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -86,28 +86,14 @@ dependencies:
86
86
  requirements:
87
87
  - - ">="
88
88
  - !ruby/object:Gem::Version
89
- version: '0.8'
89
+ version: 0.14.0
90
90
  type: :development
91
91
  prerelease: false
92
92
  version_requirements: !ruby/object:Gem::Requirement
93
93
  requirements:
94
94
  - - ">="
95
95
  - !ruby/object:Gem::Version
96
- version: '0.8'
97
- - !ruby/object:Gem::Dependency
98
- name: steep
99
- requirement: !ruby/object:Gem::Requirement
100
- requirements:
101
- - - ">="
102
- - !ruby/object:Gem::Version
103
- version: '0'
104
- type: :development
105
- prerelease: false
106
- version_requirements: !ruby/object:Gem::Requirement
107
- requirements:
108
- - - ">="
109
- - !ruby/object:Gem::Version
110
- version: '0'
96
+ version: 0.14.0
111
97
  description: "\n Configuration DSL for Ruby libraries and applications.\n Allows
112
98
  you to easily follow the twelve-factor application principles (https://12factor.net/config).\n
113
99
  \ "
@@ -120,14 +106,9 @@ files:
120
106
  - CHANGELOG.md
121
107
  - LICENSE.txt
122
108
  - README.md
123
- - lib/.rbnext/1995.next/anyway/config.rb
124
- - lib/.rbnext/1995.next/anyway/dynamic_config.rb
125
- - lib/.rbnext/1995.next/anyway/env.rb
126
- - lib/.rbnext/1995.next/anyway/loaders/base.rb
127
- - lib/.rbnext/1995.next/anyway/tracing.rb
128
109
  - lib/.rbnext/2.7/anyway/auto_cast.rb
129
110
  - lib/.rbnext/2.7/anyway/config.rb
130
- - lib/.rbnext/2.7/anyway/rails/loaders/yaml.rb
111
+ - lib/.rbnext/2.7/anyway/loaders/yaml.rb
131
112
  - lib/.rbnext/2.7/anyway/rbs.rb
132
113
  - lib/.rbnext/2.7/anyway/settings.rb
133
114
  - lib/.rbnext/2.7/anyway/tracing.rb
@@ -137,6 +118,11 @@ files:
137
118
  - lib/.rbnext/3.0/anyway/loaders.rb
138
119
  - lib/.rbnext/3.0/anyway/loaders/base.rb
139
120
  - lib/.rbnext/3.0/anyway/tracing.rb
121
+ - lib/.rbnext/3.1/anyway/config.rb
122
+ - lib/.rbnext/3.1/anyway/dynamic_config.rb
123
+ - lib/.rbnext/3.1/anyway/env.rb
124
+ - lib/.rbnext/3.1/anyway/loaders/base.rb
125
+ - lib/.rbnext/3.1/anyway/tracing.rb
140
126
  - lib/anyway.rb
141
127
  - lib/anyway/auto_cast.rb
142
128
  - lib/anyway/config.rb
@@ -1,30 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Anyway
4
- module Rails
5
- module Loaders
6
- class YAML < Anyway::Loaders::YAML
7
- def load_base_yml(*)
8
- parsed_yml = super
9
- return parsed_yml unless environmental?(parsed_yml)
10
-
11
- super[::Rails.env] || {}
12
- end
13
-
14
- private
15
-
16
- def environmental?(parsed_yml)
17
- return true unless Settings.future.unwrap_known_environments
18
- # likely
19
- return true if parsed_yml.key?(::Rails.env)
20
- # less likely
21
- ::Rails.application.config.anyway_config.known_environments.any? { |_1| parsed_yml.key?(_1) }
22
- end
23
-
24
- def relative_config_path(path)
25
- Pathname.new(path).relative_path_from(::Rails.root)
26
- end
27
- end
28
- end
29
- end
30
- end