anyway_config 2.1.0 → 2.2.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +52 -0
  3. data/README.md +189 -4
  4. data/lib/.rbnext/2.7/anyway/auto_cast.rb +41 -21
  5. data/lib/.rbnext/2.7/anyway/config.rb +48 -1
  6. data/lib/.rbnext/2.7/anyway/rails/loaders/yaml.rb +8 -2
  7. data/lib/.rbnext/2.7/anyway/rbs.rb +92 -0
  8. data/lib/.rbnext/2.7/anyway/tracing.rb +5 -7
  9. data/lib/.rbnext/2.7/anyway/type_casting.rb +143 -0
  10. data/lib/.rbnext/3.0/anyway/auto_cast.rb +53 -0
  11. data/lib/.rbnext/3.0/anyway/config.rb +48 -1
  12. data/lib/.rbnext/3.0/anyway/tracing.rb +5 -7
  13. data/lib/.rbnext/{1995.next → 3.1}/anyway/config.rb +48 -1
  14. data/lib/.rbnext/{1995.next → 3.1}/anyway/dynamic_config.rb +6 -2
  15. data/lib/.rbnext/{1995.next → 3.1}/anyway/env.rb +0 -0
  16. data/lib/.rbnext/{1995.next → 3.1}/anyway/loaders/base.rb +0 -0
  17. data/lib/.rbnext/{1995.next → 3.1}/anyway/tracing.rb +2 -2
  18. data/lib/anyway/auto_cast.rb +41 -21
  19. data/lib/anyway/config.rb +48 -1
  20. data/lib/anyway/dynamic_config.rb +6 -2
  21. data/lib/anyway/ext/deep_dup.rb +6 -0
  22. data/lib/anyway/loaders/env.rb +3 -1
  23. data/lib/anyway/loaders/yaml.rb +16 -4
  24. data/lib/anyway/option_parser_builder.rb +1 -3
  25. data/lib/anyway/optparse_config.rb +5 -7
  26. data/lib/anyway/rails/loaders/credentials.rb +2 -2
  27. data/lib/anyway/rails/loaders/secrets.rb +5 -7
  28. data/lib/anyway/rails/loaders/yaml.rb +8 -2
  29. data/lib/anyway/rails/settings.rb +8 -2
  30. data/lib/anyway/rbs.rb +92 -0
  31. data/lib/anyway/tracing.rb +3 -3
  32. data/lib/anyway/type_casting.rb +134 -0
  33. data/lib/anyway/version.rb +1 -1
  34. data/lib/anyway_config.rb +2 -0
  35. data/lib/generators/anyway/install/templates/application_config.rb.tt +1 -1
  36. data/sig/anyway_config.rbs +129 -0
  37. metadata +21 -16
  38. data/lib/.rbnext/2.7/anyway/option_parser_builder.rb +0 -31
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ae67246988f82692f84ae07b01642fbca38c9f1593b0e4292b335e08654bbf7e
4
- data.tar.gz: 045f36927093f3e7929889918dff12d63c61ef58ffcb5fc5793f017dd2da1c5e
3
+ metadata.gz: 0e4b1e98155e34f0220746d9230e433072143638e838f8975023c79cf75a9fe1
4
+ data.tar.gz: e5d8b4f78fe7f32fc2dd3068f90897a28f57a292596346f5bb368a36094cea36
5
5
  SHA512:
6
- metadata.gz: de0f8281daad0d2da4ad557f7099602a46d7fa4476f0a3adfb5b7f14c3abcd44fd020a89fd661a4c9ebeb9b5e70550d37f1b16d01bf68d535ab63877b9ae1661
7
- data.tar.gz: 04de1b39c49b4dc39faa12ee199143ca64b3722cbc547e8c41b41bc3e7e365848bae2f06dfc9f99db5693782df4239cd5bbdca99cf2e58e50f38147b2b5dc95e
6
+ metadata.gz: 849c9c7fb4bc1101444dfec952a84ac3b1f605f3415d65d31ea727f176d9281cddc90e8b56ff73a3b25431126d61640931cfbc2621dc607493014e9a97d4ee3a
7
+ data.tar.gz: 840073b4420e4c1a792db2571b29eb7605cbc7066833bc4afe91defd94ba08153a4f32036a6cd09c6cf8f1da718f7f11d41bb46c8b9ea1ab0a0bfd2a81ab0a20
data/CHANGELOG.md CHANGED
@@ -2,6 +2,57 @@
2
2
 
3
3
  ## master
4
4
 
5
+ ## 2.2.3 (2022-01-21)
6
+
7
+ - Fix Ruby 3.1 compatibility. ([@palkan][])
8
+
9
+ - Add ability to set default key for environmental YAML files. ([@skryukov])
10
+
11
+ Define a key for environmental yaml files to read default values from with `config.anyway_config.default_environmental_key = "default"`.
12
+ This way Anyway Config will try to read settings under the `"default"` key and then merge environmental settings into them.
13
+
14
+ ## 2.2.2 (2020-10-26)
15
+
16
+ - Fixed regression introduced by the `#deep_merge!` refinement.
17
+
18
+ ## 2.2.1 (2020-09-28)
19
+
20
+ - Minor fixes to the prev release.
21
+
22
+ ## 2.2.0 ⛓ (2020-09-28)
23
+
24
+ - Add RBS signatures and generator. ([@palkan][])
25
+
26
+ Anyway Config now ships with the basic RBS support. To use config types with Steep, add `library "anyway_config"` to your Steepfile.
27
+
28
+ We also provide an API to generate a signature for you config class: `MyConfig.to_rbs`. You can use this method to generate a scaffold for your config class.
29
+
30
+ - Add type coercion support. ([@palkan][])
31
+
32
+ Example:
33
+
34
+ ```ruby
35
+ class CoolConfig < Anyway::Config
36
+ attr_config :port, :user
37
+
38
+ coerce_types port: :string, user: {dob: :date}
39
+ end
40
+
41
+ ENV["COOL_USER__DOB"] = "1989-07-01"
42
+
43
+ config = CoolConfig.new({port: 8080})
44
+ config.port == "8080" #=> true
45
+ config.user["dob"] == Date.new(1989, 7, 1) #=> true
46
+ ```
47
+
48
+ You can also add `.disable_auto_cast!` to your config class to disable automatic conversion.
49
+
50
+ **Warning** Now values from all sources are coerced (e.g., YAML files). That could lead to a different behaviour.
51
+
52
+ - Do not dup modules/classes passed as configuration values. ([@palkan][])
53
+
54
+ - Handle loading empty YAML config files. ([@micahlee][])
55
+
5
56
  ## 2.1.0 (2020-12-29)
6
57
 
7
58
  - Drop deprecated `attr_config` instance variables support.
@@ -397,3 +448,4 @@ No we're dependency-free!
397
448
  [@jastkand]: https://github.com/jastkand
398
449
  [@envek]: https://github.com/Envek
399
450
  [@progapandist]: https://github.com/progapandist
451
+ [@skryukov]: https://github.com/skryukov
data/README.md CHANGED
@@ -42,12 +42,14 @@ For version 1.x see the [1-4-stable branch](https://github.com/palkan/anyway_con
42
42
  - [Generators](#generators)
43
43
  - [Using with Ruby applications](#using-with-ruby)
44
44
  - [Environment variables](#environment-variables)
45
+ - [Type coercion](#type-coercion)
45
46
  - [Local configuration](#local-files)
46
47
  - [Data loaders](#data-loaders)
47
48
  - [Source tracing](#tracing)
48
49
  - [Pattern matching](#pattern-matching)
49
50
  - [Test helpers](#test-helpers)
50
51
  - [OptionParser integration](#optionparser-integration)
52
+ - [RBS support](#rbs-support)
51
53
 
52
54
  ## Main concepts
53
55
 
@@ -94,7 +96,7 @@ Or adding to your project:
94
96
 
95
97
  ```ruby
96
98
  # Gemfile
97
- gem "anyway_config", "~> 2.0.0"
99
+ gem "anyway_config", "~> 2.0"
98
100
  ```
99
101
 
100
102
  ### Supported Ruby versions
@@ -269,6 +271,7 @@ This feature is similar to `Rails.application.config_for` but more powerful:
269
271
  | Load data from environment | ❌ | ✅ |
270
272
  | Load data from [custom sources](#data-loaders) | ❌ | ✅ |
271
273
  | Local config files | ❌ | ✅ |
274
+ | Type coercion | ❌ | ✅ |
272
275
  | [Source tracing](#tracing) | ❌ | ✅ |
273
276
  | Return Hash with indifferent access | ❌ | ✅ |
274
277
  | Support ERB\* within `config/app.yml` | ✅ | ✅ |
@@ -331,7 +334,7 @@ Your config is filled up with values from the following sources (ordered by prio
331
334
 
332
335
  1) **YAML configuration files**: `RAILS_ROOT/config/my_cool_gem.yml`.
333
336
 
334
- Recognizes Rails environment, supports `ERB`:
337
+ Rails environment is used as the namespace (required); supports `ERB`:
335
338
 
336
339
  ```yml
337
340
  test:
@@ -370,6 +373,26 @@ staging:
370
373
  port: 3002 # This value will not be loaded at all
371
374
  ```
372
375
 
376
+ 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:
377
+
378
+ ```ruby
379
+ config.anyway_config.default_environmental_key = "default"
380
+ ```
381
+
382
+ After that, Anyway Config will start reading settings under the `"default"` key and then merge environmental settings into them.
383
+
384
+ ```yml
385
+ default:
386
+ server: # This values will be loaded in all environments by default
387
+ host: localhost
388
+ port: 3002
389
+
390
+ staging:
391
+ server:
392
+ host: staging.example.com # This value will override the defaults when Rails.env.staging? is true
393
+ # port will be set to the value from the defaults — 3002
394
+ ```
395
+
373
396
  You can specify the lookup path for YAML files in one of the following ways:
374
397
 
375
398
  - By setting `config.anyway_config.default_config_path` to a target directory path:
@@ -451,6 +474,22 @@ config.anyway_config.autoload_static_config_path = "path/to/configs"
451
474
  **NOTE:** Configs loaded from the `autoload_static_config_path` are **not reloaded in development**. We call them _static_. So, it makes sense to keep only configs necessary for initialization in this folder. Other configs, _dynamic_, could be stored in `app/configs`.
452
475
  Or you can store everything in `app/configs` by setting `config.anyway_config.autoload_static_config_path = "app/configs"`.
453
476
 
477
+ **NOTE 2**: Since _static_ configs are loaded before initializers, it's not possible to use custom inflection Rules (usually defined in `config/initializers/inflections.rb`) to resolve constant names from files. If you rely on custom inflection rules (see, for example, [#81](https://github.com/palkan/anyway_config/issues/81)), we recommend configuration Rails inflector before initialization as well:
478
+
479
+ ```ruby
480
+ # config/application.rb
481
+
482
+ # ...
483
+
484
+ require_relative "initializers/inflections"
485
+
486
+ module SomeApp
487
+ class Application < Rails::Application
488
+ # ...
489
+ end
490
+ end
491
+ ```
492
+
454
493
  ### Generators
455
494
 
456
495
  Anyway Config provides Rails generators to create new config classes:
@@ -525,13 +564,15 @@ Environmental variables for your config should start with your config name, uppe
525
564
 
526
565
  For example, if your config name is "mycoolgem", then the env var "MYCOOLGEM_PASSWORD" is used as `config.password`.
527
566
 
528
- Environment variables are automatically type cast:
567
+ By default, environment variables are automatically type cast\*:
529
568
 
530
569
  - `"True"`, `"t"` and `"yes"` to `true`;
531
570
  - `"False"`, `"f"` and `"no"` to `false`;
532
571
  - `"nil"` and `"null"` to `nil` (do you really need it?);
533
572
  - `"123"` to 123 and `"3.14"` to 3.14.
534
573
 
574
+ \* See below for coercion customization.
575
+
535
576
  *Anyway Config* supports nested (_hashed_) env variables—just separate keys with double-underscore.
536
577
 
537
578
  For example, "MYCOOLGEM_OPTIONS__VERBOSE" is parsed as `config.options["verbose"]`.
@@ -549,6 +590,94 @@ If you want to provide a text-like env variable which contains commas then wrap
549
590
  MYCOOLGEM = "Nif-Nif, Naf-Naf and Nouf-Nouf"
550
591
  ```
551
592
 
593
+ ## Type coercion
594
+
595
+ > 🆕 v2.2.0
596
+
597
+ You can define custom type coercion rules to convert string data to config values. To do that, use `.coerce_types` method:
598
+
599
+ ```ruby
600
+ class CoolConfig < Anyway::Config
601
+ config_name :cool
602
+ attr_config port: 8080,
603
+ host: "localhost",
604
+ user: {name: "admin", password: "admin"}
605
+
606
+ coerce_types port: :string, user: {dob: :date}
607
+ end
608
+
609
+ ENV["COOL_USER__DOB"] = "1989-07-01"
610
+
611
+ config = CoolConfig.new
612
+ config.port == "8080" # Even though we defined the default value as int, it's converted into a string
613
+ config.user["dob"] == Date.new(1989, 7, 1) #=> true
614
+ ```
615
+
616
+ Type coercion is especially useful to deal with array values:
617
+
618
+ ```ruby
619
+ # To define an array type, provide a hash with two keys:
620
+ # - type — elements type
621
+ # - array: true — mark the parameter as array
622
+ coerce_types list: {type: :string, array: true}
623
+ ```
624
+
625
+ You can use `type: nil` in case you don't want to coerce values, just convert a value into an array:
626
+
627
+ ```ruby
628
+ # From AnyCable config (sentinels could be represented via strings or hashes)
629
+ coerce_types redis_sentinels: {type: nil, array: true}
630
+ ```
631
+
632
+ It's also could be useful to explicitly define non-array types (to avoid confusion):
633
+
634
+ ```ruby
635
+ coerce_types non_list: :string
636
+ ```
637
+
638
+ Finally, it's possible to disable auto-casting for a particular config completely:
639
+
640
+ ```ruby
641
+ class CoolConfig < Anyway::Config
642
+ attr_config port: 8080,
643
+ host: "localhost",
644
+ user: {name: "admin", password: "admin"}
645
+
646
+ disable_auto_cast!
647
+ end
648
+
649
+ ENV["COOL_PORT"] = "443"
650
+
651
+ CoolConfig.new.port == "443" #=> true
652
+ ```
653
+
654
+ **IMPORTANT**: Values provided explicitly (via attribute writers) are not coerced. Coercion is only happening during the load phase.
655
+
656
+ The following types are supported out-of-the-box: `:string`, `:integer`, `:float`, `:date`, `:datetime`, `:uri`, `:boolean`.
657
+
658
+ You can use custom deserializers by passing a callable object instead of a type name:
659
+
660
+ ```ruby
661
+ COLOR_TO_HEX = lambda do |raw|
662
+ case raw
663
+ when "red"
664
+ "#ff0000"
665
+ when "green"
666
+ "#00ff00"
667
+ when "blue"
668
+ "#0000ff"
669
+ end
670
+ end
671
+
672
+ class CoolConfig < Anyway::Config
673
+ attr_config :color
674
+
675
+ coerce_types color: COLOR_TO_HEX
676
+ end
677
+
678
+ CoolConfig.new({color: "red"}).color #=> "#ff0000"
679
+ ```
680
+
552
681
  ## Local files
553
682
 
554
683
  It's useful to have a personal, user-specific configuration in development, which extends the project-wide one.
@@ -610,7 +739,7 @@ In order to support [source tracing](#tracing), you need to wrap the resulting H
610
739
 
611
740
  ```ruby
612
741
  def call(name:, **_opts)
613
- trace!(source: :chamber) do
742
+ trace!(:chamber) do
614
743
  Chamber.env.to_h[name] || {}
615
744
  end
616
745
  end
@@ -790,6 +919,62 @@ describe_options(
790
919
  )
791
920
  ```
792
921
 
922
+ ## RBS support
923
+
924
+ Anyway Config comes with Ruby type signatures (RBS).
925
+
926
+ To use them with Steep, add `library "anyway_config"` to your Steepfile.
927
+
928
+ We also provide an API to generate a type signature for your config class:
929
+
930
+ ```ruby
931
+ class MyGem::Config < Anyway::Config
932
+ attr_config :host, port: 8080, tags: [], debug: false
933
+
934
+ coerce_types host: :string, port: :integer,
935
+ tags: {type: :string, array: true}
936
+
937
+ required :host
938
+ end
939
+ ```
940
+
941
+ Then calling `MyGem::Config.to_rbs` will return the following signature:
942
+
943
+ ```rbs
944
+ module MyGem
945
+ interface _Config
946
+ def host: () -> String
947
+ def host=: (String) -> void
948
+ def port: () -> String?
949
+ def port=: (String) -> void
950
+ def tags: () -> Array[String]?
951
+ def tags=: (Array[String]) -> void
952
+ def debug: () -> bool
953
+ def debug?: () -> bool
954
+ def debug=: (bool) -> void
955
+ end
956
+
957
+ class Config < Anyway::Config
958
+ include _Config
959
+ end
960
+ end
961
+ ```
962
+
963
+ ### Handling `on_load`
964
+
965
+ When we use `on_load` callback with a block, we switch the context (via `instance_eval`), and we need to provide type hints for the type checker. Here is an example:
966
+
967
+ ```ruby
968
+ class MyConfig < Anyway::Config
969
+ on_load do
970
+ # @type self : MyConfig
971
+ raise_validation_error("host is invalid") if host.start_with?("localhost")
972
+ end
973
+ end
974
+ ```
975
+
976
+ Yeah, a lot of annotations 😞 Welcome to the type-safe world!
977
+
793
978
  ## Contributing
794
979
 
795
980
  Bug reports and pull requests are welcome on GitHub at [https://github.com/palkan/anyway_config](https://github.com/palkan/anyway_config).
@@ -4,30 +4,50 @@ 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
- def self.call(val)
11
- return val unless String === val
10
+ class << self
11
+ def call(val)
12
+ return val unless val.is_a?(::Hash) || val.is_a?(::String)
12
13
 
13
- case val
14
- when ARRAY_RXP
15
- val.split(/\s*,\s*/).map { |_1| call(_1) }
16
- when /\A(true|t|yes|y)\z/i
17
- true
18
- when /\A(false|f|no|n)\z/i
19
- false
20
- when /\A(nil|null)\z/i
21
- nil
22
- when /\A\d+\z/
23
- val.to_i
24
- when /\A\d*\.\d+\z/
25
- val.to_f
26
- when /\A['"].*['"]\z/
27
- val.gsub(/(\A['"]|['"]\z)/, "")
28
- else
29
- val
14
+ case val
15
+ when Hash
16
+ val.transform_values { |_1| call(_1) }
17
+ when ARRAY_RXP
18
+ val.split(/\s*,\s*/).map { |_1| call(_1) }
19
+ when /\A(true|t|yes|y)\z/i
20
+ true
21
+ when /\A(false|f|no|n)\z/i
22
+ false
23
+ when /\A(nil|null)\z/i
24
+ nil
25
+ when /\A\d+\z/
26
+ val.to_i
27
+ when /\A\d*\.\d+\z/
28
+ val.to_f
29
+ when /\A['"].*['"]\z/
30
+ val.gsub(/(\A['"]|['"]\z)/, "")
31
+ else
32
+ val
33
+ end
34
+ end
35
+
36
+ def cast_hash(obj)
37
+ obj.transform_values do |val|
38
+ val.is_a?(::Hash) ? cast_hash(val) : call(val)
39
+ end
40
+ end
41
+
42
+ def coerce(_key, val)
43
+ call(val)
30
44
  end
31
45
  end
32
46
  end
47
+
48
+ module NoCast
49
+ def self.call(val) ; val; end
50
+
51
+ def self.coerce(_key, val) ; val; end
52
+ end
33
53
  end
@@ -44,6 +44,7 @@ module Anyway # :nodoc:
44
44
  to_h
45
45
  to_source_trace
46
46
  write_config_attr
47
+ __type_caster__
47
48
  ].freeze
48
49
 
49
50
  class Error < StandardError; end
@@ -196,6 +197,47 @@ module Anyway # :nodoc:
196
197
 
197
198
  def new_empty_config() ; {}; end
198
199
 
200
+ def coerce_types(mapping)
201
+ Utils.deep_merge!(coercion_mapping, mapping)
202
+ end
203
+
204
+ def coercion_mapping
205
+ return @coercion_mapping if instance_variable_defined?(:@coercion_mapping)
206
+
207
+ @coercion_mapping = if superclass < Anyway::Config
208
+ superclass.coercion_mapping.deep_dup
209
+ else
210
+ {}
211
+ end
212
+ end
213
+
214
+ def type_caster(val = nil)
215
+ return @type_caster unless val.nil?
216
+
217
+ @type_caster ||=
218
+ if coercion_mapping.empty?
219
+ fallback_type_caster
220
+ else
221
+ ::Anyway::TypeCaster.new(coercion_mapping, fallback: fallback_type_caster)
222
+ end
223
+ end
224
+
225
+ def fallback_type_caster(val = nil)
226
+ return (@fallback_type_caster = val) unless val.nil?
227
+
228
+ return @fallback_type_caster if instance_variable_defined?(:@fallback_type_caster)
229
+
230
+ @fallback_type_caster = if superclass < Anyway::Config
231
+ superclass.fallback_type_caster.deep_dup
232
+ else
233
+ ::Anyway::AutoCast
234
+ end
235
+ end
236
+
237
+ def disable_auto_cast!
238
+ @fallback_type_caster = ::Anyway::NoCast
239
+ end
240
+
199
241
  private
200
242
 
201
243
  def define_config_accessor(*names)
@@ -373,7 +415,7 @@ module Anyway # :nodoc:
373
415
  values[name].nil? || (values[name].is_a?(String) && values[name].empty?)
374
416
  end.then do |missing|
375
417
  next if missing.empty?
376
- raise_validation_error "The following config parameters are missing or empty: #{missing.join(", ")}"
418
+ raise_validation_error "The following config parameters for `#{self.class.name}(config_name: #{self.class.config_name})` are missing or empty: #{missing.join(", ")}"
377
419
  end
378
420
  end
379
421
 
@@ -381,11 +423,16 @@ module Anyway # :nodoc:
381
423
  key = key.to_sym
382
424
  return unless self.class.config_attributes.include?(key)
383
425
 
426
+ val = __type_caster__.coerce(key, val)
384
427
  public_send(:"#{key}=", val)
385
428
  end
386
429
 
387
430
  def raise_validation_error(msg)
388
431
  raise ValidationError, msg
389
432
  end
433
+
434
+ def __type_caster__
435
+ self.class.type_caster
436
+ end
390
437
  end
391
438
  end
@@ -8,7 +8,11 @@ module Anyway
8
8
  parsed_yml = super
9
9
  return parsed_yml unless environmental?(parsed_yml)
10
10
 
11
- super[::Rails.env] || {}
11
+ env_config = parsed_yml[::Rails.env] || {}
12
+ return env_config if Anyway::Settings.default_environmental_key.blank?
13
+
14
+ default_config = parsed_yml[Anyway::Settings.default_environmental_key] || {}
15
+ Utils.deep_merge!(default_config, env_config)
12
16
  end
13
17
 
14
18
  private
@@ -18,7 +22,9 @@ module Anyway
18
22
  # likely
19
23
  return true if parsed_yml.key?(::Rails.env)
20
24
  # less likely
21
- ::Rails.application.config.anyway_config.known_environments.any? { |_1| parsed_yml.key?(_1) }
25
+ return true if ::Rails.application.config.anyway_config.known_environments.any? { |_1| parsed_yml.key?(_1) }
26
+ # strange, but still possible
27
+ Anyway::Settings.default_environmental_key.present? && parsed_yml.key?(Anyway::Settings.default_environmental_key)
22
28
  end
23
29
 
24
30
  def relative_config_path(path)
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+ using RubyNext;
3
+ module Anyway
4
+ module RBSGenerator
5
+ TYPE_TO_CLASS = {
6
+ string: "String",
7
+ integer: "Integer",
8
+ float: "Float",
9
+ date: "Date",
10
+ datetime: "DateTime",
11
+ uri: "URI",
12
+ boolean: "bool"
13
+ }.freeze
14
+
15
+ # Generate RBS signature from a config class
16
+ def to_rbs
17
+ *namespace, class_name = name.split("::")
18
+
19
+ buf = []
20
+ indent = 0
21
+ interface_name = "_Config"
22
+
23
+ if namespace.empty?
24
+ interface_name = "_#{class_name}"
25
+ else
26
+ buf << "module #{namespace.join("::")}"
27
+ indent += 1
28
+ end
29
+
30
+ # Using interface emulates a module we include to provide getters and setters
31
+ # (thus making `super` possible)
32
+ buf << "#{" " * indent}interface #{interface_name}"
33
+ indent += 1
34
+
35
+ # Generating setters and getters for config attributes
36
+ config_attributes.each do |param|
37
+ type = coercion_mapping[param] || defaults[param.to_s]
38
+
39
+ type =
40
+ case; when ((__m__ = type)) && false
41
+ when (NilClass === __m__)
42
+ "untyped"
43
+ when (Symbol === __m__)
44
+ TYPE_TO_CLASS.fetch(type) { defaults[param] ? "Symbol" : "untyped" }
45
+ when (Array === __m__)
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?))))
48
+ "Array[#{TYPE_TO_CLASS.fetch(type, "untyped")}]"
49
+ when (Hash === __m__)
50
+ "Hash[string,untyped]"
51
+ when ((TrueClass === __m__) || (FalseClass === __m__))
52
+ "bool"
53
+ else
54
+ type.class.to_s
55
+ end
56
+
57
+ getter_type = type
58
+ getter_type = "#{type}?" unless required_attributes.include?(param)
59
+
60
+ buf << "#{" " * indent}def #{param}: () -> #{getter_type}"
61
+ buf << "#{" " * indent}def #{param}=: (#{type}) -> void"
62
+
63
+ if type == "bool" || type == "bool?"
64
+ buf << "#{" " * indent}def #{param}?: () -> #{getter_type}"
65
+ end
66
+ end
67
+
68
+ indent -= 1
69
+ buf << "#{" " * indent}end"
70
+
71
+ buf << ""
72
+
73
+ buf << "#{" " * indent}class #{class_name} < #{superclass.name}"
74
+ indent += 1
75
+
76
+ buf << "#{" " * indent}include #{interface_name}"
77
+
78
+ indent -= 1
79
+ buf << "#{" " * indent}end"
80
+
81
+ unless namespace.empty?
82
+ buf << "end"
83
+ end
84
+
85
+ buf << ""
86
+
87
+ buf.join("\n")
88
+ end
89
+ end
90
+
91
+ Config.extend RBSGenerator
92
+ end
@@ -34,13 +34,11 @@ module Anyway
34
34
 
35
35
  def record_value(val, *path, **opts)
36
36
  key = path.pop
37
- (__m__ = if val.is_a?(Hash)
38
- Trace.new.tap { |_1|
39
- _1.merge_values(val, **opts)
40
- }
41
- else
42
- Trace.new(:value, val, **opts)
43
- end) && (((trace = __m__) || true) || Kernel.raise(NoMatchingPatternError, __m__.inspect))
37
+ trace = if val.is_a?(Hash)
38
+ Trace.new.tap { |_1| _1.merge_values(val, **opts) }
39
+ else
40
+ Trace.new(:value, val, **opts)
41
+ end
44
42
 
45
43
  target_trace = path.empty? ? self : value.dig(*path)
46
44
  target_trace.value[key.to_s] = trace