anyway_config 2.1.0 → 2.2.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 +34 -0
- data/README.md +162 -4
- data/lib/.rbnext/1995.next/anyway/config.rb +48 -1
- data/lib/.rbnext/1995.next/anyway/dynamic_config.rb +6 -2
- data/lib/.rbnext/1995.next/anyway/tracing.rb +2 -2
- data/lib/.rbnext/2.7/anyway/auto_cast.rb +39 -19
- data/lib/.rbnext/2.7/anyway/config.rb +48 -1
- data/lib/.rbnext/2.7/anyway/rbs.rb +92 -0
- data/lib/.rbnext/2.7/anyway/tracing.rb +5 -7
- data/lib/.rbnext/2.7/anyway/type_casting.rb +130 -0
- data/lib/.rbnext/3.0/anyway/auto_cast.rb +53 -0
- data/lib/.rbnext/3.0/anyway/config.rb +48 -1
- data/lib/.rbnext/3.0/anyway/tracing.rb +5 -7
- data/lib/anyway/auto_cast.rb +39 -19
- data/lib/anyway/config.rb +48 -1
- data/lib/anyway/dynamic_config.rb +6 -2
- data/lib/anyway/ext/deep_dup.rb +6 -0
- data/lib/anyway/ext/hash.rb +10 -0
- data/lib/anyway/loaders/env.rb +3 -1
- data/lib/anyway/loaders/yaml.rb +6 -2
- data/lib/anyway/option_parser_builder.rb +1 -3
- data/lib/anyway/optparse_config.rb +5 -7
- data/lib/anyway/rails/loaders/credentials.rb +2 -2
- data/lib/anyway/rails/loaders/secrets.rb +5 -7
- data/lib/anyway/rails/settings.rb +3 -2
- data/lib/anyway/rbs.rb +92 -0
- data/lib/anyway/tracing.rb +2 -2
- data/lib/anyway/type_casting.rb +121 -0
- data/lib/anyway/version.rb +1 -1
- data/lib/anyway_config.rb +2 -0
- data/sig/anyway_config.rbs +123 -0
- metadata +28 -9
- 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:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 87d32ae676d03c8a9fd3a0b22efe3321d76661f226438f52fd313f4d73b1876d
|
4
|
+
data.tar.gz: 82f2ab5d663b03622f650fbcf8e37216a71d81fdfd9270d54f539fd6253ada9d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9f7952609c7964cc9866fe530a9a519cf981f0551f9979c72f38b2acd31e59edd98d6234f095da99419acd462bcfc7c6b7d5016c41ded3bb0206d7f1e03550b3
|
7
|
+
data.tar.gz: 1559b8e140b7df15164134233fd6c65e11d9283f94c9495a0d92f6afdcc69271cec68a7bc6f5052b8ec06dab35e33f1cadd32f4f044701156bfcc0ca4676ba41
|
data/CHANGELOG.md
CHANGED
@@ -2,6 +2,40 @@
|
|
2
2
|
|
3
3
|
## master
|
4
4
|
|
5
|
+
## 2.2.0 ⛓
|
6
|
+
|
7
|
+
- Add RBS signatures and generator. ([@palkan][])
|
8
|
+
|
9
|
+
Anyway Config now ships with the basic RBS support. To use config types with Steep, add `library "anyway_config"` to your Steepfile.
|
10
|
+
|
11
|
+
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.
|
12
|
+
|
13
|
+
- Add type coercion support. ([@palkan][])
|
14
|
+
|
15
|
+
Example:
|
16
|
+
|
17
|
+
```ruby
|
18
|
+
class CoolConfig < Anyway::Config
|
19
|
+
attr_config :port, :user
|
20
|
+
|
21
|
+
coerce_types port: :string, user: {dob: :date}
|
22
|
+
end
|
23
|
+
|
24
|
+
ENV["COOL_USER__DOB"] = "1989-07-01"
|
25
|
+
|
26
|
+
config = CoolConfig.new({port: 8080})
|
27
|
+
config.port == "8080" #=> true
|
28
|
+
config.user["dob"] == Date.new(1989, 7, 1) #=> true
|
29
|
+
```
|
30
|
+
|
31
|
+
You can also add `.disable_auto_cast!` to your config class to disable automatic conversion.
|
32
|
+
|
33
|
+
**Warning** Now values from all sources are coerced (e.g., YAML files). That could lead to a different behaviour.
|
34
|
+
|
35
|
+
- Do not dup modules/classes passed as configuration values. ([@palkan][])
|
36
|
+
|
37
|
+
- Handle loading empty YAML config files. ([@micahlee][])
|
38
|
+
|
5
39
|
## 2.1.0 (2020-12-29)
|
6
40
|
|
7
41
|
- Drop deprecated `attr_config` instance variables support.
|
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
|
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
|
-
|
337
|
+
Rails environment is used as the namespace (required); supports `ERB`:
|
335
338
|
|
336
339
|
```yml
|
337
340
|
test:
|
@@ -451,6 +454,22 @@ config.anyway_config.autoload_static_config_path = "path/to/configs"
|
|
451
454
|
**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
455
|
Or you can store everything in `app/configs` by setting `config.anyway_config.autoload_static_config_path = "app/configs"`.
|
453
456
|
|
457
|
+
**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:
|
458
|
+
|
459
|
+
```ruby
|
460
|
+
# config/application.rb
|
461
|
+
|
462
|
+
# ...
|
463
|
+
|
464
|
+
require_relative "initializers/inflections"
|
465
|
+
|
466
|
+
module SomeApp
|
467
|
+
class Application < Rails::Application
|
468
|
+
# ...
|
469
|
+
end
|
470
|
+
end
|
471
|
+
```
|
472
|
+
|
454
473
|
### Generators
|
455
474
|
|
456
475
|
Anyway Config provides Rails generators to create new config classes:
|
@@ -525,13 +544,15 @@ Environmental variables for your config should start with your config name, uppe
|
|
525
544
|
|
526
545
|
For example, if your config name is "mycoolgem", then the env var "MYCOOLGEM_PASSWORD" is used as `config.password`.
|
527
546
|
|
528
|
-
|
547
|
+
By default, environment variables are automatically type cast\*:
|
529
548
|
|
530
549
|
- `"True"`, `"t"` and `"yes"` to `true`;
|
531
550
|
- `"False"`, `"f"` and `"no"` to `false`;
|
532
551
|
- `"nil"` and `"null"` to `nil` (do you really need it?);
|
533
552
|
- `"123"` to 123 and `"3.14"` to 3.14.
|
534
553
|
|
554
|
+
\* See below for coercion customization.
|
555
|
+
|
535
556
|
*Anyway Config* supports nested (_hashed_) env variables—just separate keys with double-underscore.
|
536
557
|
|
537
558
|
For example, "MYCOOLGEM_OPTIONS__VERBOSE" is parsed as `config.options["verbose"]`.
|
@@ -549,6 +570,87 @@ If you want to provide a text-like env variable which contains commas then wrap
|
|
549
570
|
MYCOOLGEM = "Nif-Nif, Naf-Naf and Nouf-Nouf"
|
550
571
|
```
|
551
572
|
|
573
|
+
## Type coercion
|
574
|
+
|
575
|
+
> 🆕 v2.2.0
|
576
|
+
|
577
|
+
You can define custom type coercion rules to convert string data to config values. To do that, use `.coerce_types` method:
|
578
|
+
|
579
|
+
```ruby
|
580
|
+
class CoolConfig < Anyway::Config
|
581
|
+
config_name :cool
|
582
|
+
attr_config port: 8080,
|
583
|
+
host: "localhost",
|
584
|
+
user: {name: "admin", password: "admin"}
|
585
|
+
|
586
|
+
coerce_types port: :string, user: {dob: :date}
|
587
|
+
end
|
588
|
+
|
589
|
+
ENV["COOL_USER__DOB"] = "1989-07-01"
|
590
|
+
|
591
|
+
config = CoolConfig.new
|
592
|
+
config.port == "8080" # Even though we defined the default value as int, it's converted into a string
|
593
|
+
config.user["dob"] == Date.new(1989, 7, 1) #=> true
|
594
|
+
```
|
595
|
+
|
596
|
+
Type coercion is especially useful to deal with array values:
|
597
|
+
|
598
|
+
```ruby
|
599
|
+
# To define an array type, provide a hash with two keys:
|
600
|
+
# - type — elements type
|
601
|
+
# - array: true — mark the parameter as array
|
602
|
+
coerce_types list: {type: :string, array: true}
|
603
|
+
```
|
604
|
+
|
605
|
+
It's also could be useful to explicitly define non-array types (to avoid confusion):
|
606
|
+
|
607
|
+
```ruby
|
608
|
+
coerce_types non_list: :string
|
609
|
+
```
|
610
|
+
|
611
|
+
Finally, it's possible to disable auto-casting for a particular config completely:
|
612
|
+
|
613
|
+
```ruby
|
614
|
+
class CoolConfig < Anyway::Config
|
615
|
+
attr_config port: 8080,
|
616
|
+
host: "localhost",
|
617
|
+
user: {name: "admin", password: "admin"}
|
618
|
+
|
619
|
+
disable_auto_cast!
|
620
|
+
end
|
621
|
+
|
622
|
+
ENV["COOL_PORT"] = "443"
|
623
|
+
|
624
|
+
CoolConfig.new.port == "443" #=> true
|
625
|
+
```
|
626
|
+
|
627
|
+
**IMPORTANT**: Values provided explicitly (via attribute writers) are not coerced. Coercion is only happening during the load phase.
|
628
|
+
|
629
|
+
The following types are supported out-of-the-box: `:string`, `:integer`, `:float`, `:date`, `:datetime`, `:uri`, `:boolean`.
|
630
|
+
|
631
|
+
You can use custom deserializers by passing a callable object instead of a type name:
|
632
|
+
|
633
|
+
```ruby
|
634
|
+
COLOR_TO_HEX = lambda do |raw|
|
635
|
+
case raw
|
636
|
+
when "red"
|
637
|
+
"#ff0000"
|
638
|
+
when "green"
|
639
|
+
"#00ff00"
|
640
|
+
when "blue"
|
641
|
+
"#0000ff"
|
642
|
+
end
|
643
|
+
end
|
644
|
+
|
645
|
+
class CoolConfig < Anyway::Config
|
646
|
+
attr_config :color
|
647
|
+
|
648
|
+
coerce_types color: COLOR_TO_HEX
|
649
|
+
end
|
650
|
+
|
651
|
+
CoolConfig.new({color: "red"}).color #=> "#ff0000"
|
652
|
+
```
|
653
|
+
|
552
654
|
## Local files
|
553
655
|
|
554
656
|
It's useful to have a personal, user-specific configuration in development, which extends the project-wide one.
|
@@ -610,7 +712,7 @@ In order to support [source tracing](#tracing), you need to wrap the resulting H
|
|
610
712
|
|
611
713
|
```ruby
|
612
714
|
def call(name:, **_opts)
|
613
|
-
trace!(
|
715
|
+
trace!(:chamber) do
|
614
716
|
Chamber.env.to_h[name] || {}
|
615
717
|
end
|
616
718
|
end
|
@@ -790,6 +892,62 @@ describe_options(
|
|
790
892
|
)
|
791
893
|
```
|
792
894
|
|
895
|
+
## RBS support
|
896
|
+
|
897
|
+
Anyway Config comes with Ruby type signatures (RBS).
|
898
|
+
|
899
|
+
To use them with Steep, add `library "anyway_config"` to your Steepfile.
|
900
|
+
|
901
|
+
We also provide an API to generate a type signature for your config class:
|
902
|
+
|
903
|
+
```ruby
|
904
|
+
class MyGem::Config < Anyway::Config
|
905
|
+
attr_config :host, port: 8080, tags: [], debug: false
|
906
|
+
|
907
|
+
coerce_types host: :string, port: :integer,
|
908
|
+
tags: {type: :string, array: true}
|
909
|
+
|
910
|
+
required :host
|
911
|
+
end
|
912
|
+
```
|
913
|
+
|
914
|
+
Then calling `MyGem::Config.to_rbs` will return the following signature:
|
915
|
+
|
916
|
+
```rbs
|
917
|
+
module MyGem
|
918
|
+
interface _Config
|
919
|
+
def host: () -> String
|
920
|
+
def host=: (String) -> void
|
921
|
+
def port: () -> String?
|
922
|
+
def port=: (String) -> void
|
923
|
+
def tags: () -> Array[String]?
|
924
|
+
def tags=: (Array[String]) -> void
|
925
|
+
def debug: () -> bool
|
926
|
+
def debug?: () -> bool
|
927
|
+
def debug=: (bool) -> void
|
928
|
+
end
|
929
|
+
|
930
|
+
class Config < Anyway::Config
|
931
|
+
include _Config
|
932
|
+
end
|
933
|
+
end
|
934
|
+
```
|
935
|
+
|
936
|
+
### Handling `on_load`
|
937
|
+
|
938
|
+
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:
|
939
|
+
|
940
|
+
```ruby
|
941
|
+
class MyConfig < Anyway::Config
|
942
|
+
on_load do
|
943
|
+
# @type self : MyConfig
|
944
|
+
raise_validation_error("host is invalid") if host.start_with?("localhost")
|
945
|
+
end
|
946
|
+
end
|
947
|
+
```
|
948
|
+
|
949
|
+
Yeah, a lot of annotations 😞 Welcome to the type-safe world!
|
950
|
+
|
793
951
|
## Contributing
|
794
952
|
|
795
953
|
Bug reports and pull requests are welcome on GitHub at [https://github.com/palkan/anyway_config](https://github.com/palkan/anyway_config).
|
@@ -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() = {}
|
198
199
|
|
200
|
+
def coerce_types(mapping)
|
201
|
+
coercion_mapping.deep_merge!(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
|
@@ -12,11 +12,15 @@ module Anyway
|
|
12
12
|
# my_config = Anyway::Config.for(:my_app)
|
13
13
|
# # will load data from config/my_app.yml, secrets.my_app, ENV["MY_APP_*"]
|
14
14
|
#
|
15
|
-
def for(name, **options)
|
15
|
+
def for(name, auto_cast: true, **options)
|
16
16
|
config = allocate
|
17
17
|
options[:env_prefix] ||= name.to_s.upcase
|
18
18
|
options[:config_path] ||= config.resolve_config_path(name, options[:env_prefix])
|
19
|
-
|
19
|
+
|
20
|
+
raw_config = config.load_from_sources(new_empty_config, name: name, **options)
|
21
|
+
return raw_config unless auto_cast
|
22
|
+
|
23
|
+
AutoCast.call(raw_config)
|
20
24
|
end
|
21
25
|
end
|
22
26
|
|
@@ -34,11 +34,11 @@ module Anyway
|
|
34
34
|
|
35
35
|
def record_value(val, *path, **opts)
|
36
36
|
key = path.pop
|
37
|
-
if val.is_a?(Hash)
|
37
|
+
trace = if val.is_a?(Hash)
|
38
38
|
Trace.new.tap { _1.merge_values(val, **opts) }
|
39
39
|
else
|
40
40
|
Trace.new(:value, val, **opts)
|
41
|
-
end
|
41
|
+
end
|
42
42
|
|
43
43
|
target_trace = path.empty? ? self : value.dig(*path)
|
44
44
|
target_trace.value[key.to_s] = trace
|
@@ -7,27 +7,47 @@ module Anyway
|
|
7
7
|
# and doesn't start/end with quote
|
8
8
|
ARRAY_RXP = /\A[^'"].*\s*,\s*.*[^'"]\z/
|
9
9
|
|
10
|
-
|
11
|
-
|
10
|
+
class << self
|
11
|
+
def call(val)
|
12
|
+
return val unless val.is_a?(::Hash) || val.is_a?(::String)
|
12
13
|
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
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
|
+
coercion_mapping.deep_merge!(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
|
@@ -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
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
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
|