dry-initializer 2.5.0 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 861264b64b414e7ff11f68071a530e3208093092
4
- data.tar.gz: f3426ffd2670dbe47c276d67fd972e86fdeb9677
2
+ SHA256:
3
+ metadata.gz: d65124ac530b85023e9152387677a694f2cab063b484ba48133ed8006fc79c2a
4
+ data.tar.gz: e35526cc7df9ad5271485742f75991e397c06c04af8029003ebdd567974f763c
5
5
  SHA512:
6
- metadata.gz: 3205f5f09066d20a3f05255a71ae8451ec6c00b0c6701bdf2fb4d8a854a61d49d5b1eea952d4cd08d05e3165e8748d73dd1e530a39a12d85bb90e5514bf6c37c
7
- data.tar.gz: 05205f55be677d470f553ec0e9fcf8d357ffc3ba245c9195b144c24cac478dd3aa2e1e8d6bcbbd6fc1b6b04d92d8e3e8345762bc76838651dab26d90632a31d2
6
+ metadata.gz: ad262ac79a0191fdaee557a4d1f1b48fbb3c3dc57af0cd639077277dffe52fcecb979db69af0b5f00c803006ceab893d7a057ae085006fc343ef742e2b6f0a2a
7
+ data.tar.gz: 7b79bd6d05a8a9a7bb239b35cd12629dbd8eda35721916d06b7d04dac7639d361f04958e3a3ace67f5267163c27ae983ad4cb63bbf003efc96cdcf25d9028099
@@ -10,13 +10,17 @@ AllCops:
10
10
  Bundler/DuplicatedGem:
11
11
  Enabled: false
12
12
 
13
+ Naming/FileName:
14
+ Exclude:
15
+ - lib/dry-initializer.rb
16
+
13
17
  Style/CaseEquality:
14
18
  Enabled: false
15
19
 
16
- Style/ClassVars:
20
+ Style/ClassAndModuleChildren:
17
21
  Enabled: false
18
22
 
19
- Style/ClassAndModuleChildren:
23
+ Style/ClassVars:
20
24
  Enabled: false
21
25
 
22
26
  Style/Documentation:
@@ -25,10 +29,6 @@ Style/Documentation:
25
29
  Style/DoubleNegation:
26
30
  Enabled: false
27
31
 
28
- Style/FileName:
29
- Exclude:
30
- - lib/dry-initializer.rb
31
-
32
32
  Style/Lambda:
33
33
  Exclude:
34
34
  - spec/**/*.rb
@@ -36,7 +36,7 @@ Style/Lambda:
36
36
  Style/LambdaCall:
37
37
  Enabled: false
38
38
 
39
- Style/RescueModified:
39
+ Style/RescueModifier:
40
40
  Exclude:
41
41
  - spec/**/*.rb
42
42
 
@@ -1,16 +1,19 @@
1
1
  ---
2
2
  language: ruby
3
- sudo: false
4
3
  cache: bundler
5
4
  bundler_args: --without benchmarks tools
6
5
  script:
7
6
  - bundle exec rake spec
8
7
  rvm:
9
- - 2.3.0
10
- - 2.4.0
8
+ - 2.3.8
9
+ - 2.4.6
10
+ - 2.5.5
11
+ - 2.6.2
12
+ - jruby-9.2.7.0
11
13
  - jruby-9000
12
14
  - rbx-3
13
15
  - ruby-head
16
+ - truffleruby
14
17
  env:
15
18
  global:
16
19
  - JRUBY_OPTS='--dev -J-Xmx1024M'
@@ -19,6 +22,7 @@ matrix:
19
22
  - rvm: rbx-3
20
23
  - rvm: ruby-head
21
24
  - rvm: jruby-head
25
+ - rvm: truffleruby
22
26
  include:
23
27
  - rvm: jruby-head
24
- before_install: gem install bundler --no-ri --no-rdoc
28
+ before_install: gem install bundler --no-document
@@ -5,6 +5,89 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](http://keepachangelog.com/)
6
6
  and this project adheres to [Semantic Versioning](http://semver.org/).
7
7
 
8
+ ## [3.0.0] [2019-04-14]
9
+
10
+ ### Added
11
+
12
+ - Support of wrapped types/coercers (nepalez)
13
+
14
+ ```ruby
15
+ class Test
16
+ # Wrap type to the array
17
+ param :foo, [proc(&:to_s)]
18
+ end
19
+
20
+ # And the value will be wrapped as well
21
+ test = Test.new(42)
22
+ test.foo # => ["42"]
23
+ ```
24
+
25
+ - It works with several layers of nesting (nepalez)
26
+
27
+ ```ruby
28
+ class Test
29
+ # Wrap type to the array
30
+ param :foo, [[proc(&:to_s)]]
31
+ end
32
+
33
+ # And the value will be wrapped as well
34
+ test = Test.new(42)
35
+ test.foo # => [["42"]]
36
+ ```
37
+
38
+ - Support of nested types/coercers (nepalez)
39
+
40
+ ```ruby
41
+ class Test
42
+ param :foo do
43
+ option :bar do
44
+ option :baz, proc(&:to_s)
45
+ end
46
+ end
47
+ end
48
+
49
+ test = Test.new(bar: { "baz" => 42 })
50
+ test.foo.bar.baz # => "42"
51
+ ```
52
+
53
+ - Wrapped/nested combinations are supported as well (nepalez)
54
+
55
+ ```ruby
56
+ class Test
57
+ param :foo, [] do
58
+ option :bar, proc(&:to_s)
59
+ end
60
+ end
61
+
62
+ test = Test.new(bar: 42)
63
+ test.foo.first.bar # => "42"
64
+ ```
65
+
66
+ ## [2.7.0] Unreleazed
67
+
68
+ ### Fixed
69
+
70
+ - Roll back master to the state of [2.5.0].
71
+
72
+ Somehow distinction between `@default_null` and `@null` variables
73
+ in the `Dry::Initializer::Builders` broken the `rom` library.
74
+
75
+ The version [2.6.0] has been yanked on rubygems, so the master
76
+ was rolled back to the previous state until the reason for
77
+ the incompatibility become clear (bjeanes, nepalez)
78
+
79
+ ## [2.6.0] [2018-09-09] (YANKED)
80
+
81
+ ## [2.5.0] [2018-08-17]
82
+
83
+ ### Fixed
84
+
85
+ - `nil` coercion (belousovAV)
86
+
87
+ When default value is `nil` instead of `Dry::Initializer::UNDEFINED`,
88
+ the coercion should be applied to any value, including `nil`, because
89
+ we cannot distinct "undefined" `nil` from the "assigned" `nil` value.
90
+
8
91
  ## [2.4.0] [2018-02-01]
9
92
 
10
93
  ### Added
@@ -367,18 +450,20 @@ and to @gzigzigzeo for persuading me to do this refactoring.
367
450
  ### Added
368
451
  - enhancement via `Dry::Initializer::Attribute.dispatchers` registry (nepalez)
369
452
 
370
- # Register dispatcher for `:string` option
371
- Dry::Initializer::Attribute.dispatchers << ->(string: nil, **op) do
372
- string ? op.merge(type: proc(&:to_s)) : op
373
- end
453
+ ```ruby
454
+ # Register dispatcher for `:string` option
455
+ Dry::Initializer::Attribute.dispatchers << ->(string: nil, **op) do
456
+ string ? op.merge(type: proc(&:to_s)) : op
457
+ end
374
458
 
375
- # Now you can use the `:string` key for `param` and `option`
376
- class User
377
- extend Dry::Initializer
378
- param :name, string: true
379
- end
459
+ # Now you can use the `:string` key for `param` and `option`
460
+ class User
461
+ extend Dry::Initializer
462
+ param :name, string: true
463
+ end
380
464
 
381
- User.new(:Andy).name # => "Andy"
465
+ User.new(:Andy).name # => "Andy"
466
+ ```
382
467
 
383
468
  ### Changed
384
469
  - optimize assignments for performance (nepalez)
@@ -757,4 +842,7 @@ First public release
757
842
  [2.1.0]: https://github.com/dry-rb/dry-initializer/compare/v2.0.0...v2.1.0
758
843
  [2.2.0]: https://github.com/dry-rb/dry-initializer/compare/v2.1.0...v2.2.0
759
844
  [2.3.0]: https://github.com/dry-rb/dry-initializer/compare/v2.2.0...v2.3.0
760
- [2.4.0]: https://github.com/dry-rb/dry-initializer/compare/v2.3.0...v2.4.0
845
+ [2.4.0]: https://github.com/dry-rb/dry-initializer/compare/v2.3.0...v2.4.0
846
+ [2.6.0]: https://github.com/dry-rb/dry-initializer/compare/v2.4.0...v2.5.0
847
+ [2.6.0]: https://github.com/dry-rb/dry-initializer/compare/v2.5.0...v2.6.0
848
+ [3.0.0]: https://github.com/dry-rb/dry-initializer/compare/v2.5.0...v3.0.0
data/Gemfile CHANGED
@@ -17,7 +17,7 @@ group :benchmarks do
17
17
  gem "concord"
18
18
  gem "fast_attributes"
19
19
  gem "kwattr"
20
- gem "ruby-prof"
20
+ gem "ruby-prof", platform: :mri
21
21
  gem "value_struct"
22
22
  gem "values"
23
23
  gem "virtus"
data/README.md CHANGED
@@ -2,7 +2,6 @@
2
2
 
3
3
  [![Gem Version](https://badge.fury.io/rb/dry-initializer.svg)][gem]
4
4
  [![Build Status](https://travis-ci.org/dry-rb/dry-initializer.svg?branch=master)][travis]
5
- [![Dependency Status](https://gemnasium.com/dry-rb/dry-initializer.svg)][gemnasium]
6
5
  [![Code Climate](https://codeclimate.com/github/dry-rb/dry-initializer/badges/gpa.svg)][codeclimate]
7
6
  [![Test Coverage](https://codeclimate.com/github/dry-rb/dry-initializer/badges/coverage.svg)][coveralls]
8
7
  [![Inline docs](http://inch-ci.org/github/dry-rb/dry-initializer.svg?branch=master)][inchpages]
@@ -1,9 +1,9 @@
1
1
  Gem::Specification.new do |gem|
2
2
  gem.name = "dry-initializer"
3
- gem.version = "2.5.0"
3
+ gem.version = "3.0.0"
4
4
  gem.author = ["Vladimir Kochnev (marshall-lee)", "Andrew Kozin (nepalez)"]
5
5
  gem.email = "andrew.kozin@gmail.com"
6
- gem.homepage = "https://github.com/dryrb/dry-initializer"
6
+ gem.homepage = "https://github.com/dry-rb/dry-initializer"
7
7
  gem.summary = "DSL for declaring params and options of the initializer"
8
8
  gem.license = "MIT"
9
9
 
@@ -6,9 +6,7 @@ module Dry
6
6
  # DSL for declaring params and options of class initializers
7
7
  #
8
8
  module Initializer
9
- # Singleton for unassigned values
10
- UNDEFINED = Object.new.freeze
11
-
9
+ require_relative "initializer/undefined"
12
10
  require_relative "initializer/dsl"
13
11
  require_relative "initializer/definition"
14
12
  require_relative "initializer/builders"
@@ -32,18 +30,20 @@ module Dry
32
30
  # @option opts [Boolean] :optional
33
31
  # @option opts [Symbol] :as
34
32
  # @option opts [true, false, :protected, :public, :private] :reader
33
+ # @yield block with nested definition
35
34
  # @return [self] itself
36
- def param(name, type = nil, **opts)
37
- dry_initializer.param(name, type, Dispatchers[opts])
35
+ def param(name, type = nil, **opts, &block)
36
+ dry_initializer.param(name, type, **opts, &block)
38
37
  self
39
38
  end
40
39
 
41
40
  # Adds or redefines an option of [#dry_initializer]
42
41
  # @param (see #param)
43
42
  # @option (see #param)
43
+ # @yield (see #param)
44
44
  # @return (see #param)
45
- def option(name, type = nil, **opts)
46
- dry_initializer.option(name, type, Dispatchers[opts])
45
+ def option(name, type = nil, **opts, &block)
46
+ dry_initializer.option(name, type, **opts, &block)
47
47
  self
48
48
  end
49
49
 
@@ -55,5 +55,7 @@ module Dry
55
55
  klass.send(:instance_variable_set, :@dry_initializer, config)
56
56
  dry_initializer.children << config
57
57
  end
58
+
59
+ require_relative "initializer/struct"
58
60
  end
59
61
  end
@@ -57,8 +57,8 @@ module Dry::Initializer
57
57
  # @option opts [Symbol] :as
58
58
  # @option opts [true, false, :protected, :public, :private] :reader
59
59
  # @return [self] itself
60
- def param(name, type = nil, **opts)
61
- add_definition(false, name, type, opts)
60
+ def param(name, type = nil, **opts, &block)
61
+ add_definition(false, name, type, block, opts)
62
62
  end
63
63
 
64
64
  # Adds or redefines an option of [#dry_initializer]
@@ -67,8 +67,8 @@ module Dry::Initializer
67
67
  # @option (see #param)
68
68
  # @return (see #param)
69
69
  #
70
- def option(name, type = nil, **opts)
71
- add_definition(true, name, type, opts)
70
+ def option(name, type = nil, **opts, &block)
71
+ add_definition(true, name, type, block, opts)
72
72
  end
73
73
 
74
74
  # The hash of public attributes for an instance of the [#extended_class]
@@ -133,13 +133,25 @@ module Dry::Initializer
133
133
  finalize
134
134
  end
135
135
 
136
- def add_definition(option, name, type, opts)
137
- definition = Definition.new(option, null, name, type, Dispatchers[opts])
136
+ # rubocop: disable Metrics/MethodLength
137
+ def add_definition(option, name, type, block, **opts)
138
+ opts = {
139
+ parent: extended_class,
140
+ option: option,
141
+ null: null,
142
+ source: name,
143
+ type: type,
144
+ block: block,
145
+ **opts,
146
+ }
147
+
148
+ options = Dispatchers.call(opts)
149
+ definition = Definition.new(options)
138
150
  definitions[definition.source] = definition
139
151
  finalize
140
-
141
152
  mixin.class_eval definition.code
142
153
  end
154
+ # rubocop: enable Metrics/MethodLength
143
155
 
144
156
  def final_definitions
145
157
  parent_definitions = Hash(parent&.definitions&.dup)
@@ -49,68 +49,17 @@ module Dry::Initializer
49
49
 
50
50
  private
51
51
 
52
- def initialize(option, null, source, coercer = nil, **options)
53
- @option = !!option
54
- @null = null
55
- @source = source.to_sym
56
- @target = check_target options.fetch(:as, source).to_sym
57
- @ivar = :"@#{target}"
58
- @type = check_type(coercer || options[:type])
59
- @reader = prepare_reader options.fetch(:reader, true)
60
- @default = check_default options[:default]
61
- @optional = options.fetch(:optional, @default)
62
- @desc = options[:desc]&.to_s&.capitalize
52
+ def initialize(**options)
53
+ @option = options[:option]
54
+ @null = options[:null]
55
+ @source = options[:source]
56
+ @target = options[:target]
57
+ @ivar = "@#{@target}"
58
+ @type = options[:type]
59
+ @reader = options[:reader]
60
+ @default = options[:default]
61
+ @optional = options[:optional]
62
+ @desc = options[:desc]
63
63
  end
64
-
65
- def check_source(value)
66
- if RESERVED.include? value
67
- raise ArgumentError, "Name #{value} is reserved by dry-initializer gem"
68
- end
69
-
70
- unless option || value[ATTRIBUTE]
71
- raise ArgumentError, "Invalid parameter name :'#{value}'"
72
- end
73
-
74
- value
75
- end
76
-
77
- def check_target(value)
78
- return value if value[ATTRIBUTE]
79
- raise ArgumentError, "Invalid variable name :'#{value}'"
80
- end
81
-
82
- def check_type(value)
83
- return if value.nil?
84
- arity = value.arity if value.is_a? Proc
85
- arity ||= value.method(:call).arity if value.respond_to? :call
86
- return value if [1, 2].include? arity.to_i.abs
87
- raise TypeError,
88
- "type of #{inspect} should respond to #call with 1..2 arguments"
89
- end
90
-
91
- def check_default(value)
92
- return if value.nil?
93
- return value if value.is_a?(Proc) && value.arity < 1
94
- raise TypeError,
95
- "default value of #{inspect} should be a proc without params"
96
- end
97
-
98
- def prepare_reader(value)
99
- case value.to_s
100
- when "", "false" then false
101
- when "private" then :private
102
- when "protected" then :protected
103
- else :public
104
- end
105
- end
106
-
107
- ATTRIBUTE = /\A\w+\z/
108
- RESERVED = %i[
109
- __dry_initializer_options__
110
- __dry_initializer_config__
111
- __dry_initializer_value__
112
- __dry_initializer_definition__
113
- __dry_initializer_initializer__
114
- ].freeze
115
64
  end
116
65
  end
@@ -1,44 +1,112 @@
1
- module Dry::Initializer
2
- #
3
- # @private
4
- #
5
- # Dispatchers allow adding syntax sugar to `.param` and `.option` methods.
1
+ #
2
+ # The module is responsible for __normalizing__ arguments
3
+ # of `.param` and `.option`.
4
+ #
5
+ # What the module does is convert the source list of arguments
6
+ # into the standard set of options:
7
+ # - `:option` -- whether an argument is an option (or param)
8
+ # - `:source` -- the name of source option
9
+ # - `:target` -- the target name of the reader
10
+ # - `:reader` -- if the reader's privacy (:public, :protected, :private, nil)
11
+ # - `:ivar` -- the target nane of the variable
12
+ # - `:type` -- the callable coercer of the source value
13
+ # - `:optional` -- if the argument is optional
14
+ # - `:default` -- the proc returning the default value of the source value
15
+ # - `:null` -- the value to be set to unassigned optional argument
16
+ #
17
+ # It is this set is used to build [Dry::Initializer::Definition].
18
+ #
19
+ # @example
20
+ # # from `option :foo, [], as: :bar, optional: :true
21
+ # input = { name: :foo, as: :bar, type: [], optional: true }
22
+ #
23
+ # Dry::Initializer::Dispatcher.call(input)
24
+ # # => {
25
+ # # source: "foo",
26
+ # # target: "bar",
27
+ # # reader: :public,
28
+ # # ivar: "@bar",
29
+ # # type: ->(v) { Array(v) } }, # simplified for brevity
30
+ # # optional: true,
31
+ # # default: -> { Dry::Initializer::UNDEFINED },
32
+ # # }
33
+ #
34
+ # # Settings
35
+ #
36
+ # The module uses global setting `null` to define what value
37
+ # should be set to variables that kept unassigned. By default it
38
+ # uses `Dry::Initializer::UNDEFINED`
39
+ #
40
+ # # Syntax Extensions
41
+ #
42
+ # The module supports syntax extensions. You can add any number
43
+ # of custom dispatchers __on top__ of the stack of default dispatchers.
44
+ # Every dispatcher should be a callable object that takes
45
+ # the source set of options and converts it to another set of options.
46
+ #
47
+ # @example Add special dispatcher
48
+ #
49
+ # # Define a dispatcher for key :integer
50
+ # dispatcher = proc do |integer: false, **opts|
51
+ # opts.merge(type: proc(&:to_i)) if integer
52
+ # end
53
+ #
54
+ # # Register a dispatcher
55
+ # Dry::Initializer::Dispatchers << dispatcher
56
+ #
57
+ # # Now you can use option `integer: true` instead of `type: proc(&:to_i)`
58
+ # class Foo
59
+ # extend Dry::Initializer
60
+ # param :id, integer: true
61
+ # end
62
+ #
63
+ module Dry::Initializer::Dispatchers
64
+ extend self
65
+
66
+ # @!attribute [rw] null Defines a value to be set to unassigned attributes
67
+ # @return [Object]
68
+ attr_accessor :null
69
+
6
70
  #
7
- # Every dispatcher should convert the source hash of options into
8
- # the resulting hash so that you can send additional keys to the helpers.
71
+ # Registers a new dispatcher
9
72
  #
10
- # @example Add special dispatcher
73
+ # @param [#call] dispatcher
74
+ # @return [self] itself
11
75
  #
12
- # # Define a dispatcher for key :integer
13
- # dispatcher = proc do |opts|
14
- # opts.merge(type: proc(&:to_i)) if opts[:integer]
15
- # end
76
+ def <<(dispatcher)
77
+ @pipeline = [dispatcher] + pipeline
78
+ self
79
+ end
80
+
16
81
  #
17
- # # Register a dispatcher
18
- # Dry::Initializer::Dispatchers << dispatcher
82
+ # Normalizes the source set of options
19
83
  #
20
- # # Now you can use option `integer: true` instead of `type: proc(&:to_i)`
21
- # class Foo
22
- # extend Dry::Initializer
23
- # param :id, integer: true
24
- # end
84
+ # @param [Hash<Symbol, Object>] options
85
+ # @return [Hash<Symbol, Objct>] normalized set of options
25
86
  #
26
- module Dispatchers
27
- class << self
28
- def <<(item)
29
- list << item
30
- self
31
- end
87
+ def call(**options)
88
+ options = { null: null }.merge(options)
89
+ pipeline.reduce(options) { |opts, dispatcher| dispatcher.call(opts) }
90
+ end
32
91
 
33
- def [](options)
34
- list.inject(options) { |opts, item| item.call(opts) }
35
- end
92
+ private
36
93
 
37
- private
94
+ require_relative "dispatchers/build_nested_type"
95
+ require_relative "dispatchers/check_type"
96
+ require_relative "dispatchers/prepare_default"
97
+ require_relative "dispatchers/prepare_ivar"
98
+ require_relative "dispatchers/prepare_optional"
99
+ require_relative "dispatchers/prepare_reader"
100
+ require_relative "dispatchers/prepare_source"
101
+ require_relative "dispatchers/prepare_target"
102
+ require_relative "dispatchers/unwrap_type"
103
+ require_relative "dispatchers/wrap_type"
38
104
 
39
- def list
40
- @list ||= []
41
- end
42
- end
105
+ def pipeline
106
+ @pipeline ||= [
107
+ PrepareSource, PrepareTarget, PrepareIvar, PrepareReader,
108
+ PrepareDefault, PrepareOptional,
109
+ UnwrapType, CheckType, BuildNestedType, WrapType
110
+ ]
43
111
  end
44
112
  end
@@ -0,0 +1,58 @@
1
+ #
2
+ # Prepare nested data type from a block
3
+ #
4
+ # @example
5
+ # option :foo do
6
+ # option :bar
7
+ # option :qux
8
+ # end
9
+ #
10
+ module Dry::Initializer::Dispatchers::BuildNestedType
11
+ extend self
12
+
13
+ # rubocop: disable Metrics/ParameterLists
14
+ def call(parent:, source:, target:, type: nil, block: nil, **options)
15
+ check_certainty!(source, type, block)
16
+ check_name!(target)
17
+ type ||= build_nested_type(parent, target, block)
18
+ { parent: parent, source: source, target: target, type: type, **options }
19
+ end
20
+ # rubocop: enable Metrics/ParameterLists
21
+
22
+ private
23
+
24
+ def check_certainty!(source, type, block)
25
+ return unless type
26
+ return unless block
27
+
28
+ raise ArgumentError, <<~MESSAGE
29
+ You should define coercer of values of argument '#{source}'
30
+ either though the parameter/option, or via nested block, but not the both.
31
+ MESSAGE
32
+ end
33
+
34
+ def check_name!(name)
35
+ return unless name[/^_|__|_$/]
36
+
37
+ raise ArgumentError, <<~MESSAGE
38
+ The name of the argument '#{name}' cannot be used for nested struct.
39
+ A proper name can use underscores _ to divide alphanumeric parts only.
40
+ MESSAGE
41
+ end
42
+
43
+ def build_nested_type(parent, name, block)
44
+ return unless block
45
+
46
+ klass_name = full_name(parent, name)
47
+ build_struct(klass_name, block)
48
+ end
49
+
50
+ def full_name(parent, name)
51
+ "::#{parent.name}::#{name.to_s.split("_").compact.map(&:capitalize).join}"
52
+ end
53
+
54
+ def build_struct(klass_name, block)
55
+ eval "class #{klass_name} < Dry::Initializer::Struct; end"
56
+ const_get(klass_name).tap { |klass| klass.class_eval(&block) }
57
+ end
58
+ end
@@ -0,0 +1,43 @@
1
+ #
2
+ # Checks whether an unwrapped type is valid
3
+ #
4
+ module Dry::Initializer::Dispatchers::CheckType
5
+ extend self
6
+
7
+ def call(source:, type: nil, wrap: 0, **options)
8
+ check_if_callable! source, type
9
+ check_arity! source, type, wrap
10
+
11
+ { source: source, type: type, wrap: wrap, **options }
12
+ end
13
+
14
+ private
15
+
16
+ def check_if_callable!(source, type)
17
+ return if type.nil?
18
+ return if type.respond_to?(:call)
19
+
20
+ raise ArgumentError,
21
+ "The type of the argument '#{source}' should be callable"
22
+ end
23
+
24
+ def check_arity!(_source, type, wrap)
25
+ return if type.nil?
26
+ return if wrap.zero?
27
+ return if type.method(:call).arity.abs == 1
28
+
29
+ raise ArgumentError, <<~MESSAGE
30
+ The dry_intitializer supports wrapped types with one argument only.
31
+ You cannot use array types with element coercers having several arguments.
32
+
33
+ For example, this definitions are correct:
34
+ option :foo, [proc(&:to_s)]
35
+ option :bar, type: [[]]
36
+ option :baz, ->(a, b) { [a, b] }
37
+
38
+ While this is not:
39
+ option :foo, [->(a, b) { [a, b] }]
40
+ MESSAGE
41
+ end
42
+ # rubocop: enable Metrics/MethodLength
43
+ end
@@ -0,0 +1,40 @@
1
+ #
2
+ # Prepares the `:default` option
3
+ #
4
+ # It must respond to `.call` without arguments
5
+ #
6
+ module Dry::Initializer::Dispatchers::PrepareDefault
7
+ extend self
8
+
9
+ def call(default: nil, optional: nil, **options)
10
+ default = callable! default
11
+ check_arity! default
12
+
13
+ { default: default, optional: (optional | default), **options }
14
+ end
15
+
16
+ private
17
+
18
+ def callable!(default)
19
+ return unless default
20
+ return default if default.respond_to?(:call)
21
+ return callable(default.to_proc) if default.respond_to?(:to_proc)
22
+
23
+ invalid!(default)
24
+ end
25
+
26
+ def check_arity!(default)
27
+ return unless default
28
+
29
+ arity = default.method(:call).arity.to_i
30
+ return unless arity.positive?
31
+
32
+ invalid!(default)
33
+ end
34
+
35
+ def invalid!(default)
36
+ raise TypeError, "The #{default.inspect} should be" \
37
+ " either convertable to proc with no arguments," \
38
+ " or respond to #call without arguments."
39
+ end
40
+ end
@@ -0,0 +1,12 @@
1
+ #
2
+ # Prepares the variable name of a parameter or an option.
3
+ #
4
+ module Dry::Initializer::Dispatchers::PrepareIvar
5
+ module_function
6
+
7
+ def call(target:, **options)
8
+ ivar = "@#{target}".delete("?").to_sym
9
+
10
+ { target: target, ivar: ivar, **options }
11
+ end
12
+ end
@@ -0,0 +1,13 @@
1
+ #
2
+ # Defines whether an argument is optional
3
+ #
4
+ module Dry::Initializer::Dispatchers::PrepareOptional
5
+ module_function
6
+
7
+ def call(optional: nil, default: nil, required: nil, **options)
8
+ optional ||= default
9
+ optional &&= !required
10
+
11
+ { optional: !!optional, default: default, **options }
12
+ end
13
+ end
@@ -0,0 +1,30 @@
1
+ #
2
+ # Checks the reader privacy
3
+ #
4
+ module Dry::Initializer::Dispatchers::PrepareReader
5
+ extend self
6
+
7
+ def call(target: nil, reader: :public, **options)
8
+ reader = case reader.to_s
9
+ when "false", "" then nil
10
+ when "true" then :public
11
+ when "public", "private", "protected" then reader.to_sym
12
+ else invalid_reader!(target, reader)
13
+ end
14
+
15
+ { target: target, reader: reader, **options }
16
+ end
17
+
18
+ private
19
+
20
+ def invalid_reader!(target, _reader)
21
+ raise ArgumentError, <<~MESSAGE
22
+ Invalid setting for the ##{target} reader's privacy.
23
+ Use the one of the following values for the `:reader` option:
24
+ - 'public' (true) for the public reader (default)
25
+ - 'private' for the private reader
26
+ - 'protected' for the protected reader
27
+ - nil (false) if no reader should be defined
28
+ MESSAGE
29
+ end
30
+ end
@@ -0,0 +1,28 @@
1
+ #
2
+ # The dispatcher verifies a correctness of the source name
3
+ # of param or option, taken as a `:source` option.
4
+ #
5
+ # We allow any stringified name for the source.
6
+ # For example, this syntax is correct because we accept any key
7
+ # in the original hash of arguments, but give them proper names:
8
+ #
9
+ # ```ruby
10
+ # class Foo
11
+ # extend Dry::Initializer
12
+ #
13
+ # option "", as: :first
14
+ # option 1, as: :second
15
+ # end
16
+ #
17
+ # foo = Foo.new("": 42, 1: 666)
18
+ # foo.first # => 42
19
+ # foo.second # => 666
20
+ # ```
21
+ #
22
+ module Dry::Initializer::Dispatchers::PrepareSource
23
+ module_function
24
+
25
+ def call(source:, **options)
26
+ { source: source.to_s.to_sym, **options }
27
+ end
28
+ end
@@ -0,0 +1,44 @@
1
+ #
2
+ # Prepares the target name of a parameter or an option.
3
+ #
4
+ # Unlike source, the target must satisfy requirements for Ruby variable names.
5
+ # It also shouldn't be in conflict with names used by the gem.
6
+ #
7
+ module Dry::Initializer::Dispatchers::PrepareTarget
8
+ extend self
9
+
10
+ # List of variable names reserved by the gem
11
+ RESERVED = %i[
12
+ __dry_initializer_options__
13
+ __dry_initializer_config__
14
+ __dry_initializer_value__
15
+ __dry_initializer_definition__
16
+ __dry_initializer_initializer__
17
+ ].freeze
18
+
19
+ def call(source:, target: nil, as: nil, **options)
20
+ target ||= as || source
21
+ target = target.to_s.to_sym.downcase
22
+
23
+ check_ruby_name!(target)
24
+ check_reserved_names!(target)
25
+
26
+ { source: source, target: target, **options }
27
+ end
28
+
29
+ private
30
+
31
+ def check_ruby_name!(target)
32
+ return if target[/\A[[:alpha:]_][[:alnum:]_]*\??\z/u]
33
+
34
+ raise ArgumentError,
35
+ "The name `#{target}` is not allowed for Ruby methods"
36
+ end
37
+
38
+ def check_reserved_names!(target)
39
+ return unless RESERVED.include?(target)
40
+
41
+ raise ArgumentError,
42
+ "The method name `#{target}` is reserved by the dry-initializer gem"
43
+ end
44
+ end
@@ -0,0 +1,22 @@
1
+ #
2
+ # Looks at the `:type` option and counts how many nested arrays
3
+ # it contains around either nil or a callable value.
4
+ #
5
+ # The counted number is preserved in the `:wrap` virtual option
6
+ # used by the [WrapType] dispatcher.
7
+ #
8
+ module Dry::Initializer::Dispatchers::UnwrapType
9
+ extend self
10
+
11
+ def call(type: nil, wrap: 0, **options)
12
+ type, wrap = unwrap(type, 0)
13
+
14
+ { type: type, wrap: wrap, **options }
15
+ end
16
+
17
+ private
18
+
19
+ def unwrap(type, count)
20
+ type.is_a?(Array) ? unwrap(type.first, count + 1) : [type, count]
21
+ end
22
+ end
@@ -0,0 +1,27 @@
1
+ #
2
+ # Takes `:type` and `:wrap` to construct the final value coercer
3
+ #
4
+ module Dry::Initializer::Dispatchers::WrapType
5
+ extend self
6
+
7
+ def call(type: nil, wrap: 0, **options)
8
+ { type: wrapped_type(type, wrap), **options }
9
+ end
10
+
11
+ private
12
+
13
+ def wrapped_type(type, count)
14
+ return type if count.zero?
15
+
16
+ ->(value) { wrap_value(value, count, type) }
17
+ end
18
+
19
+ def wrap_value(value, count, type)
20
+ if count.zero?
21
+ type ? type.call(value) : value
22
+ else
23
+ return [wrap_value(value, count - 1, type)] unless value.is_a?(Array)
24
+ value.map { |item| wrap_value(item, count - 1, type) }
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,40 @@
1
+ #
2
+ # The nested structure that takes nested hashes with indifferent access
3
+ #
4
+ class Dry::Initializer::Struct
5
+ extend Dry::Initializer
6
+
7
+ class << self
8
+ undef_method :param
9
+
10
+ def new(options)
11
+ super Hash(options).transform_keys(&:to_sym)
12
+ end
13
+ alias call new
14
+ end
15
+
16
+ #
17
+ # Represents event data as a nested hash with deeply stringified keys
18
+ # @return [Hash<String, ...>]
19
+ #
20
+ def to_h
21
+ self
22
+ .class
23
+ .dry_initializer
24
+ .attributes(self)
25
+ .transform_values { |v| __hashify(v) }
26
+ .stringify_keys
27
+ end
28
+
29
+ private
30
+
31
+ def __hashify(value)
32
+ case value
33
+ when Hash
34
+ value.each_with_object({}) { |(k, v), obj| obj[k.to_s] = __hashify(v) }
35
+ when Array then value.map { |v| __hashify(v) }
36
+ when Dry::Initializer::Struct then value.to_h
37
+ else value
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,2 @@
1
+ module Dry::Initializer::UNDEFINED
2
+ end
@@ -22,7 +22,9 @@ describe "definition" do
22
22
  [definition.source, definition.options]
23
23
  end
24
24
 
25
- expect(params).to eq [[:foo, { as: :foo, reader: :public }]]
25
+ expect(params).to eq [
26
+ [:foo, { as: :foo, reader: :public, optional: false }]
27
+ ]
26
28
  end
27
29
 
28
30
  it "preservers definition options" do
@@ -30,7 +32,9 @@ describe "definition" do
30
32
  [definition.source, definition.options]
31
33
  end
32
34
 
33
- expect(options).to eq [[:bar, { as: :bar, reader: :public }]]
35
+ expect(options).to eq [
36
+ [:bar, { as: :bar, reader: :public, optional: false }]
37
+ ]
34
38
  end
35
39
  end
36
40
 
@@ -0,0 +1,32 @@
1
+ require "dry-types"
2
+
3
+ describe "list type argument" do
4
+ before do
5
+ class Test::Foo
6
+ extend Dry::Initializer
7
+ param :foo, [proc(&:to_s)]
8
+ option :bar, [Dry::Types["strict.string"]]
9
+ option :baz, []
10
+ end
11
+ end
12
+
13
+ context "with single items" do
14
+ subject { Test::Foo.new(1, bar: "2", baz: { qux: :QUX }) }
15
+
16
+ it "coerces and wraps them to arrays" do
17
+ expect(subject.foo).to eq %w[1]
18
+ expect(subject.bar).to eq %w[2]
19
+ expect(subject.baz).to eq [{ qux: :QUX }]
20
+ end
21
+ end
22
+
23
+ context "with arrays" do
24
+ subject { Test::Foo.new([1], bar: %w[2], baz: [{ qux: :QUX }]) }
25
+
26
+ it "coerces elements" do
27
+ expect(subject.foo).to eq %w[1]
28
+ expect(subject.bar).to eq %w[2]
29
+ expect(subject.baz).to eq [{ qux: :QUX }]
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,44 @@
1
+ describe "nested type argument" do
2
+ subject { Test::Xyz.new("bar" => { "baz" => 42 }) }
3
+
4
+ context "with nested definition only" do
5
+ before do
6
+ class Test::Xyz
7
+ extend Dry::Initializer
8
+
9
+ param :foo, as: :x do
10
+ option :bar, as: :y do
11
+ option :baz, proc(&:to_s), as: :z
12
+ option :qux, as: :w, optional: true
13
+ end
14
+ end
15
+ end
16
+ end
17
+
18
+ it "builds the type" do
19
+ expect(subject.x.y.z).to eq "42"
20
+ end
21
+ end
22
+
23
+ context "with nested and wrapped definitions" do
24
+ before do
25
+ class Test::Xyz
26
+ extend Dry::Initializer
27
+
28
+ param :foo, [], as: :x do
29
+ option :bar, as: :y do
30
+ option :baz, proc(&:to_s), as: :z
31
+ option :qux, as: :w, optional: true
32
+ end
33
+ end
34
+ end
35
+ end
36
+
37
+ it "builds the type" do
38
+ x = subject.x
39
+ expect(x).to be_instance_of Array
40
+
41
+ expect(x.first.y.z).to eq "42"
42
+ end
43
+ end
44
+ end
@@ -1,7 +1,8 @@
1
1
  require "dry/initializer"
2
+
2
3
  begin
3
4
  require "pry"
4
- rescue
5
+ rescue LoadError
5
6
  nil
6
7
  end
7
8
 
@@ -42,7 +42,7 @@ describe "type constraint" do
42
42
  context "in case of mismatch" do
43
43
  subject { Test::Foo.new 1 }
44
44
 
45
- it "raises TypeError" do
45
+ it "raises ArgumentError" do
46
46
  expect { subject }.to raise_error TypeError, /1/
47
47
  end
48
48
  end
@@ -66,13 +66,13 @@ describe "type constraint" do
66
66
  end
67
67
 
68
68
  context "by invalid constraint" do
69
- it "raises TypeError" do
69
+ it "raises ArgumentError" do
70
70
  expect do
71
71
  class Test::Foo
72
72
  extend Dry::Initializer
73
73
  param :foo, type: String
74
74
  end
75
- end.to raise_error(TypeError)
75
+ end.to raise_error(ArgumentError)
76
76
  end
77
77
  end
78
78
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dry-initializer
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.5.0
4
+ version: 3.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Vladimir Kochnev (marshall-lee)
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2018-08-16 00:00:00.000000000 Z
12
+ date: 2019-04-14 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rspec
@@ -103,10 +103,22 @@ files:
103
103
  - lib/dry/initializer/config.rb
104
104
  - lib/dry/initializer/definition.rb
105
105
  - lib/dry/initializer/dispatchers.rb
106
+ - lib/dry/initializer/dispatchers/build_nested_type.rb
107
+ - lib/dry/initializer/dispatchers/check_type.rb
108
+ - lib/dry/initializer/dispatchers/prepare_default.rb
109
+ - lib/dry/initializer/dispatchers/prepare_ivar.rb
110
+ - lib/dry/initializer/dispatchers/prepare_optional.rb
111
+ - lib/dry/initializer/dispatchers/prepare_reader.rb
112
+ - lib/dry/initializer/dispatchers/prepare_source.rb
113
+ - lib/dry/initializer/dispatchers/prepare_target.rb
114
+ - lib/dry/initializer/dispatchers/unwrap_type.rb
115
+ - lib/dry/initializer/dispatchers/wrap_type.rb
106
116
  - lib/dry/initializer/dsl.rb
107
117
  - lib/dry/initializer/mixin.rb
108
118
  - lib/dry/initializer/mixin/local.rb
109
119
  - lib/dry/initializer/mixin/root.rb
120
+ - lib/dry/initializer/struct.rb
121
+ - lib/dry/initializer/undefined.rb
110
122
  - lib/tasks/benchmark.rake
111
123
  - lib/tasks/profile.rake
112
124
  - spec/attributes_spec.rb
@@ -116,7 +128,9 @@ files:
116
128
  - spec/default_values_spec.rb
117
129
  - spec/definition_spec.rb
118
130
  - spec/invalid_default_spec.rb
131
+ - spec/list_type_spec.rb
119
132
  - spec/missed_default_spec.rb
133
+ - spec/nested_type_spec.rb
120
134
  - spec/optional_spec.rb
121
135
  - spec/options_tolerance_spec.rb
122
136
  - spec/public_attributes_utility_spec.rb
@@ -128,7 +142,7 @@ files:
128
142
  - spec/type_argument_spec.rb
129
143
  - spec/type_constraint_spec.rb
130
144
  - spec/value_coercion_via_dry_types_spec.rb
131
- homepage: https://github.com/dryrb/dry-initializer
145
+ homepage: https://github.com/dry-rb/dry-initializer
132
146
  licenses:
133
147
  - MIT
134
148
  metadata: {}
@@ -147,8 +161,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
147
161
  - !ruby/object:Gem::Version
148
162
  version: '0'
149
163
  requirements: []
150
- rubyforge_project:
151
- rubygems_version: 2.6.14
164
+ rubygems_version: 3.0.3
152
165
  signing_key:
153
166
  specification_version: 4
154
167
  summary: DSL for declaring params and options of the initializer
@@ -160,7 +173,9 @@ test_files:
160
173
  - spec/default_values_spec.rb
161
174
  - spec/definition_spec.rb
162
175
  - spec/invalid_default_spec.rb
176
+ - spec/list_type_spec.rb
163
177
  - spec/missed_default_spec.rb
178
+ - spec/nested_type_spec.rb
164
179
  - spec/optional_spec.rb
165
180
  - spec/options_tolerance_spec.rb
166
181
  - spec/public_attributes_utility_spec.rb