dry-initializer 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +13 -9
  3. data/CHANGELOG.md +72 -3
  4. data/LICENSE.txt +1 -1
  5. data/README.md +7 -279
  6. data/benchmarks/with_types.rb +2 -0
  7. data/benchmarks/with_types_and_defaults.rb +2 -0
  8. data/dry-initializer.gemspec +1 -1
  9. data/lib/dry/initializer.rb +5 -4
  10. data/lib/dry/initializer/builder.rb +66 -13
  11. data/lib/dry/initializer/errors.rb +5 -7
  12. data/lib/dry/initializer/errors/default_value_error.rb +6 -0
  13. data/lib/dry/initializer/errors/order_error.rb +7 -0
  14. data/lib/dry/initializer/errors/plugin_error.rb +6 -0
  15. data/lib/dry/initializer/errors/{existing_argument_error.rb → redefinition_error.rb} +1 -1
  16. data/lib/dry/initializer/errors/type_constraint_error.rb +6 -0
  17. data/lib/dry/initializer/errors/type_error.rb +4 -3
  18. data/lib/dry/initializer/mixin.rb +7 -7
  19. data/lib/dry/initializer/plugins.rb +10 -0
  20. data/lib/dry/initializer/plugins/base.rb +42 -0
  21. data/lib/dry/initializer/plugins/default_proc.rb +28 -0
  22. data/lib/dry/initializer/plugins/signature.rb +35 -0
  23. data/lib/dry/initializer/plugins/type_constraint.rb +58 -0
  24. data/lib/dry/initializer/plugins/variable_setter.rb +12 -0
  25. data/lib/dry/initializer/signature.rb +47 -0
  26. data/spec/dry/container_spec.rb +3 -3
  27. data/spec/dry/dry_type_constraint_spec.rb +30 -0
  28. data/spec/dry/invalid_default_spec.rb +1 -1
  29. data/spec/dry/{proc_type_spec.rb → object_type_constraint_spec.rb} +4 -4
  30. data/spec/dry/{poro_type_spec.rb → plain_type_constraint_spec.rb} +1 -1
  31. data/spec/dry/value_coercion_via_dry_types_spec.rb +21 -0
  32. metadata +29 -25
  33. data/lib/dry/initializer/argument.rb +0 -96
  34. data/lib/dry/initializer/arguments.rb +0 -85
  35. data/lib/dry/initializer/errors/invalid_default_value_error.rb +0 -6
  36. data/lib/dry/initializer/errors/invalid_type_error.rb +0 -6
  37. data/lib/dry/initializer/errors/key_error.rb +0 -5
  38. data/lib/dry/initializer/errors/missed_default_value_error.rb +0 -5
  39. data/spec/dry/dry_type_spec.rb +0 -25
  40. data/spec/dry/invalid_type_spec.rb +0 -13
@@ -12,8 +12,10 @@ class PlainRubyTest
12
12
  end
13
13
 
14
14
  require "dry-initializer"
15
+ require "dry/initializer/types"
15
16
  class DryTest
16
17
  extend Dry::Initializer::Mixin
18
+ extend Dry::Initializer::Types
17
19
 
18
20
  option :foo, type: String
19
21
  option :bar, type: String
@@ -12,8 +12,10 @@ class PlainRubyTest
12
12
  end
13
13
 
14
14
  require "dry-initializer"
15
+ require "dry/initializer/types"
15
16
  class DryTest
16
17
  extend Dry::Initializer::Mixin
18
+ extend Dry::Initializer::Types
17
19
 
18
20
  option :foo, type: String, default: proc { "FOO" }
19
21
  option :bar, type: String, default: proc { "BAR" }
@@ -1,6 +1,6 @@
1
1
  Gem::Specification.new do |gem|
2
2
  gem.name = "dry-initializer"
3
- gem.version = "0.1.1"
3
+ gem.version = "0.2.0"
4
4
  gem.author = ["Vladimir Kochnev (marshall-lee)", "Andrew Kozin (nepalez)"]
5
5
  gem.email = ["hashtable@yandex.ru", "andrew.kozin@gmail.com"]
6
6
  gem.homepage = "https://github.com/dryrb/dry-initializer"
@@ -4,16 +4,17 @@ module Dry
4
4
  # @api public
5
5
  #
6
6
  module Initializer
7
-
8
7
  require_relative "initializer/errors"
9
- require_relative "initializer/argument"
10
- require_relative "initializer/arguments"
8
+ require_relative "initializer/plugins"
9
+ require_relative "initializer/signature"
11
10
  require_relative "initializer/builder"
12
11
  require_relative "initializer/mixin"
13
12
 
13
+ UNDEFINED = Object.new.freeze
14
+
14
15
  def self.define(proc = nil, &block)
15
16
  Module.new do |container|
16
- container.extend Dry::Initializer::Mixin
17
+ container.extend Mixin
17
18
  container.instance_exec(&(proc || block))
18
19
  end
19
20
  end
@@ -1,33 +1,86 @@
1
1
  module Dry::Initializer
2
- # Carries declarations for arguments along with a mixin module
2
+ # Rebuilds the initializer every time a new argument defined
3
3
  #
4
4
  # @api private
5
5
  #
6
6
  class Builder
7
- def arguments
8
- @arguments ||= Arguments.new
7
+ include Plugins
8
+
9
+ def initialize
10
+ @signature = Signature.new
11
+ @plugins = Set.new [VariableSetter, TypeConstraint, DefaultProc]
12
+ @parts = []
13
+ end
14
+
15
+ # Register new plugin to be applied as a chunk of code, or a proc
16
+ # to be evaluated in the instance's scope
17
+ #
18
+ # @param [Dry::Initializer::Plugin]
19
+ #
20
+ def register(plugin)
21
+ @plugins << plugin
22
+ end
23
+
24
+ # Defines new agrument and rebuilds the initializer
25
+ #
26
+ # @param [#to_sym] name
27
+ # @param [Hash<Symbol, Object>] settings
28
+ #
29
+ # @return [self] itself
30
+ #
31
+ def define(name, settings)
32
+ update_signature(name, settings)
33
+ update_parts(name, settings)
34
+
35
+ define_reader(name, settings)
36
+ reload_initializer
37
+ reload_callback
38
+
39
+ self
9
40
  end
10
41
 
42
+ # The module with two methods: `#initialize` and `##__after_initialize__`
43
+ # to be mixed into the target class
44
+ #
45
+ # @return [Module]
46
+ #
11
47
  def mixin
12
48
  @mixin ||= Module.new
13
49
  end
14
50
 
15
- def define_initializer(name, **options)
16
- @arguments = arguments.add(name, **options)
17
- mixin.instance_eval @arguments.declaration
51
+ private
52
+
53
+ def update_signature(name, settings)
54
+ @signature.add(name, settings)
55
+ end
56
+
57
+ def update_parts(name, settings)
58
+ @parts += @plugins.map { |klass| klass.call(name, settings) }.compact
59
+ end
60
+
61
+ def define_reader(name, settings)
62
+ mixin.send :attr_reader, name unless settings[:reader] == false
18
63
  end
19
64
 
20
- def define_attributes_reader(name, keys)
21
- symbol_keys = keys.map { |key| ":" << key.to_s }.join(", ")
22
- key = '@#{key}'
65
+ def reload_initializer
66
+ strings = @parts.select { |part| String === part }
23
67
 
24
68
  mixin.class_eval <<-RUBY
25
- def #{name}
26
- [#{symbol_keys}].inject({}) do |hash, key|
27
- hash.merge key => instance_variable_get(:"#{key}")
28
- end
69
+ def initialize(#{@signature.call})
70
+ #{strings.join("\n")}
71
+ __after_initialize__
29
72
  end
30
73
  RUBY
31
74
  end
75
+
76
+ def reload_callback
77
+ blocks = @parts.select { |part| Proc === part }
78
+
79
+ mixin.send :define_method, :__after_initialize__ do
80
+ blocks.each { |block| instance_eval(&block) }
81
+ end
82
+
83
+ mixin.send :private, :__after_initialize__
84
+ end
32
85
  end
33
86
  end
@@ -1,13 +1,11 @@
1
1
  module Dry::Initializer
2
2
  # Collection of gem-specific exceptions
3
3
  module Errors
4
-
5
- require_relative "errors/existing_argument_error"
6
- require_relative "errors/invalid_default_value_error"
7
- require_relative "errors/invalid_type_error"
8
- require_relative "errors/key_error"
9
- require_relative "errors/missed_default_value_error"
4
+ require_relative "errors/default_value_error"
5
+ require_relative "errors/order_error"
6
+ require_relative "errors/plugin_error"
7
+ require_relative "errors/redefinition_error"
10
8
  require_relative "errors/type_error"
11
-
9
+ require_relative "errors/type_constraint_error"
12
10
  end
13
11
  end
@@ -0,0 +1,6 @@
1
+ class Dry::Initializer::Errors::DefaultValueError < TypeError
2
+ def initialize(name, value)
3
+ super "Cannot set #{value.inspect} directly as a default value" \
4
+ " of the argument '#{name}'. Wrap it to either proc or lambda."
5
+ end
6
+ end
@@ -0,0 +1,7 @@
1
+ class Dry::Initializer::Errors::OrderError < SyntaxError
2
+ def initialize(name)
3
+ super "Cannot define the required param '#{name}' after optional ones." \
4
+ " Either provide a default value for the '#{name}', or declare it" \
5
+ " before params with default values."
6
+ end
7
+ end
@@ -0,0 +1,6 @@
1
+ class Dry::Initializer::Errors::PluginError < TypeError
2
+ def initialize(plugin)
3
+ super "#{plugin} is not a valid plugin." \
4
+ " Use a subclass of Dry::Initialier::Plugins::Base."
5
+ end
6
+ end
@@ -1,4 +1,4 @@
1
- class Dry::Initializer::Errors::ExistingArgumentError < SyntaxError
1
+ class Dry::Initializer::Errors::RedefinitionError < SyntaxError
2
2
  def initialize(name)
3
3
  super "The argument '#{name}' is already defined."
4
4
  end
@@ -0,0 +1,6 @@
1
+ class Dry::Initializer::Errors::TypeConstraintError < TypeError
2
+ def initialize(name, type)
3
+ super "#{type} is inacceptable constraint for the argument '#{name}'." \
4
+ " Use either plain Ruby module/class, or dry-type."
5
+ end
6
+ end
@@ -1,5 +1,6 @@
1
- class Dry::Initializer::Errors::TypeError < ::TypeError
2
- def initialize(type, value)
3
- super "#{value.inspect} mismatches the type #{type}."
1
+ class Dry::Initializer::Errors::TypeError < TypeError
2
+ def initialize(name, type, value)
3
+ super "A value #{value.inspect} assigned to the argument '#{name}'" \
4
+ " mismatches type constraint: #{type}."
4
5
  end
5
6
  end
@@ -12,7 +12,7 @@ module Dry::Initializer
12
12
  # @return [self] itself
13
13
  #
14
14
  def param(name, **options)
15
- arguments_builder.define_initializer(name, option: false, **options)
15
+ initializer_builder.define(name, option: false, **options)
16
16
  self
17
17
  end
18
18
 
@@ -23,22 +23,22 @@ module Dry::Initializer
23
23
  # @return (see #param)
24
24
  #
25
25
  def option(name, **options)
26
- arguments_builder.define_initializer(name, option: true, **options)
26
+ initializer_builder.define(name, option: true, **options)
27
27
  self
28
28
  end
29
29
 
30
- private
31
-
32
- def arguments_builder
33
- @arguments_builder ||= begin
30
+ # @private
31
+ def initializer_builder
32
+ @initializer_builder ||= begin
34
33
  builder = Builder.new
35
34
  include builder.mixin
36
35
  builder
37
36
  end
38
37
  end
39
38
 
39
+ # @private
40
40
  def inherited(klass)
41
- klass.instance_variable_set(:@arguments_builder, arguments_builder)
41
+ klass.instance_variable_set(:@initializer_builder, initializer_builder)
42
42
  end
43
43
  end
44
44
  end
@@ -0,0 +1,10 @@
1
+ module Dry::Initializer
2
+ # Namespace for code plugins builders
3
+ module Plugins
4
+ require_relative "plugins/base"
5
+ require_relative "plugins/default_proc"
6
+ require_relative "plugins/signature"
7
+ require_relative "plugins/type_constraint"
8
+ require_relative "plugins/variable_setter"
9
+ end
10
+ end
@@ -0,0 +1,42 @@
1
+ module Dry::Initializer::Plugins
2
+ # Base class for plugins
3
+ #
4
+ # A plugin should has class method [.call] that takes argument name and
5
+ # settings and return a chunk of code for the #initialize method body.
6
+ #
7
+ class Base
8
+ include Dry::Initializer::Errors
9
+
10
+ # Builds the proc for the `__after_initializer__` callback
11
+ #
12
+ # @param [#to_s] name
13
+ # @param [Hash<Symbol, Object>] settings
14
+ #
15
+ # @return [String, Proc, nil]
16
+ #
17
+ def self.call(name, settings)
18
+ new(name, settings).call
19
+ end
20
+
21
+ # @private
22
+ attr_reader :name, :settings
23
+
24
+ # Initializes a builder with argument name and settings
25
+ # @param (see .call)
26
+ def initialize(name, settings)
27
+ @name = name
28
+ @settings = settings
29
+ end
30
+
31
+ # Checks equality to another instance by name
32
+ # @return [Boolean]
33
+ def ==(other)
34
+ other.instance_of?(self.class) && (other.name == name)
35
+ end
36
+
37
+ # Builds a chunk of code
38
+ # @return (see .call)
39
+ def call
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,28 @@
1
+ module Dry::Initializer::Plugins
2
+ # Builds a block to be evaluated by initializer (__after_initialize__)
3
+ # to assign a default value to the argument
4
+ class DefaultProc < Base
5
+ def call
6
+ return unless default
7
+
8
+ ivar = :"@#{name}"
9
+ default_proc = default
10
+
11
+ proc do
12
+ if instance_variable_get(ivar) == Dry::Initializer::UNDEFINED
13
+ instance_variable_set ivar, instance_eval(&default_proc)
14
+ end
15
+ end
16
+ end
17
+
18
+ private
19
+
20
+ def default
21
+ return unless settings.key? :default
22
+
23
+ @default ||= settings[:default].tap do |value|
24
+ fail DefaultValueError.new(name, value) unless Proc === value
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,35 @@
1
+ module Dry::Initializer::Plugins
2
+ # Plugin builds a chunk of code for the initializer's signature:
3
+ #
4
+ # @example
5
+ # Signature.call(:user, option: true)
6
+ # # => "user:"
7
+ #
8
+ # Signature.call(:user, default: -> { nil })
9
+ # # => "user = Dry::Initializer::UNDEFINED"
10
+ #
11
+ class Signature < Base
12
+ def param?
13
+ settings[:option] != true
14
+ end
15
+
16
+ def default?
17
+ settings.key? :default
18
+ end
19
+
20
+ def call
21
+ case [param?, default?]
22
+ when [true, false] then name.to_s
23
+ when [false, false] then "#{name}:"
24
+ when [true, true] then "#{name} = #{undefined}"
25
+ when [false, true] then "#{name}: #{undefined}"
26
+ end
27
+ end
28
+
29
+ private
30
+
31
+ def undefined
32
+ @undefined ||= "Dry::Initializer::UNDEFINED"
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,58 @@
1
+ module Dry::Initializer::Plugins
2
+ # Plugin builds either chunk of code for the #initializer,
3
+ # or a proc for the ##__after_initialize__ callback.
4
+ class TypeConstraint < Base
5
+ def call
6
+ return unless settings.key? :type
7
+ dry_type_constraint || module_type_constraint || object_type_constraint
8
+ end
9
+
10
+ private
11
+
12
+ def type
13
+ @type ||= settings[:type]
14
+ end
15
+
16
+ def dry_type?
17
+ type.class.ancestors.map(&:name).include? "Dry::Types::Builder"
18
+ end
19
+
20
+ def plain_type?
21
+ Module === type
22
+ end
23
+
24
+ def module_type_constraint
25
+ return unless plain_type?
26
+
27
+ "fail #{TypeError}.new(:#{name}, #{type}, @#{name})" \
28
+ " unless @#{name} == Dry::Initializer::UNDEFINED ||" \
29
+ " #{type} === @#{name}"
30
+ end
31
+
32
+ def dry_type_constraint
33
+ return unless dry_type?
34
+
35
+ ivar = :"@#{name}"
36
+ constraint = type
37
+
38
+ lambda do |*|
39
+ value = instance_variable_get(ivar)
40
+ return if value == Dry::Initializer::UNDEFINED
41
+
42
+ instance_variable_set ivar, constraint[value]
43
+ end
44
+ end
45
+
46
+ def object_type_constraint
47
+ ivar = :"@#{name}"
48
+ constraint = type
49
+
50
+ lambda do |*|
51
+ value = instance_variable_get(ivar)
52
+ return if value == Dry::Initializer::UNDEFINED
53
+
54
+ fail TypeError.new(ivar, constraint, value) unless constraint === value
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,12 @@
1
+ module Dry::Initializer::Plugins
2
+ # Plugin builds a code for variable setter:
3
+ #
4
+ # @example
5
+ # VariableSetter.call(:user, {}) # => "@user = user"
6
+ #
7
+ class VariableSetter < Base
8
+ def call
9
+ "@#{name} = #{name}"
10
+ end
11
+ end
12
+ end