anyway_config 2.1.0 → 2.2.3

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.
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