anyway_config 2.1.0 → 2.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|