dry-initializer 2.5.0 → 3.0.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 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