dry-initializer 0.11.0 → 1.1.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 (46) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/.rubocop.yml +4 -61
  4. data/.travis.yml +18 -11
  5. data/CHANGELOG.md +108 -43
  6. data/Gemfile +1 -0
  7. data/Rakefile +6 -0
  8. data/benchmarks/options.rb +4 -4
  9. data/benchmarks/params.rb +1 -1
  10. data/benchmarks/profiler.rb +28 -0
  11. data/benchmarks/with_types.rb +5 -9
  12. data/benchmarks/with_types_and_defaults.rb +2 -4
  13. data/benchmarks/without_options.rb +3 -3
  14. data/dry-initializer.gemspec +1 -1
  15. data/lib/dry/initializer/attribute.rb +92 -0
  16. data/lib/dry/initializer/builder.rb +76 -72
  17. data/lib/dry/initializer/exceptions/default_value_error.rb +8 -0
  18. data/lib/dry/initializer/exceptions/params_order_error.rb +8 -0
  19. data/lib/dry/initializer/exceptions/type_constraint_error.rb +7 -0
  20. data/lib/dry/initializer/option.rb +62 -0
  21. data/lib/dry/initializer/param.rb +54 -0
  22. data/lib/dry/initializer.rb +77 -13
  23. data/spec/enhancement_spec.rb +18 -0
  24. data/spec/missed_default_spec.rb +2 -2
  25. data/spec/optional_spec.rb +16 -6
  26. data/spec/options_var_spec.rb +39 -0
  27. data/spec/repetitive_definitions_spec.rb +38 -18
  28. data/spec/several_assignments_spec.rb +41 -0
  29. data/spec/type_constraint_spec.rb +6 -5
  30. metadata +15 -20
  31. data/lib/dry/initializer/errors/default_value_error.rb +0 -6
  32. data/lib/dry/initializer/errors/order_error.rb +0 -7
  33. data/lib/dry/initializer/errors/plugin_error.rb +0 -6
  34. data/lib/dry/initializer/errors/redefinition_error.rb +0 -5
  35. data/lib/dry/initializer/errors/type_constraint_error.rb +0 -5
  36. data/lib/dry/initializer/errors.rb +0 -10
  37. data/lib/dry/initializer/mixin.rb +0 -77
  38. data/lib/dry/initializer/plugins/base.rb +0 -47
  39. data/lib/dry/initializer/plugins/default_proc.rb +0 -28
  40. data/lib/dry/initializer/plugins/signature.rb +0 -28
  41. data/lib/dry/initializer/plugins/type_constraint.rb +0 -21
  42. data/lib/dry/initializer/plugins/variable_setter.rb +0 -30
  43. data/lib/dry/initializer/plugins.rb +0 -10
  44. data/lib/dry/initializer/signature.rb +0 -61
  45. data/spec/plugin_registry_spec.rb +0 -45
  46. data/spec/renaming_options_spec.rb +0 -20
data/Gemfile CHANGED
@@ -21,6 +21,7 @@ group :benchmarks do
21
21
  gem "value_struct"
22
22
  gem "values"
23
23
  gem "virtus"
24
+ gem "ruby-prof"
24
25
  end
25
26
 
26
27
  group :development, :test do
data/Rakefile CHANGED
@@ -45,3 +45,9 @@ namespace :benchmark do
45
45
  system "ruby benchmarks/options.rb"
46
46
  end
47
47
  end
48
+
49
+ desc "Runs profiler"
50
+ task :profile do
51
+ system "ruby benchmarks/profiler.rb && " \
52
+ "dot -Tpng ./tmp/profile.dot > ./tmp/profile.png"
53
+ end
@@ -18,15 +18,15 @@ end
18
18
  class TypesTest
19
19
  extend Dry::Initializer::Mixin
20
20
 
21
- param :foo, type: String
22
- option :bar, type: String
21
+ param :foo, proc(&:to_s)
22
+ option :bar, proc(&:to_s)
23
23
  end
24
24
 
25
25
  class DefaultsAndTypesTest
26
26
  extend Dry::Initializer::Mixin
27
27
 
28
- param :foo, type: String, default: proc { "FOO" }
29
- option :bar, type: String, default: proc { "BAR" }
28
+ param :foo, proc(&:to_s), default: proc { "FOO" }
29
+ option :bar, proc(&:to_s), default: proc { "BAR" }
30
30
  end
31
31
 
32
32
  puts "Benchmark for various options"
data/benchmarks/params.rb CHANGED
@@ -13,7 +13,7 @@ StructTest = Struct.new(:foo, :bar)
13
13
 
14
14
  require "dry-initializer"
15
15
  class DryTest
16
- extend Dry::Initializer::Mixin
16
+ extend Dry::Initializer
17
17
 
18
18
  param :foo
19
19
  param :bar
@@ -0,0 +1,28 @@
1
+ require "dry-initializer"
2
+ require "ruby-prof"
3
+ require "fileutils"
4
+
5
+ class User
6
+ extend Dry::Initializer
7
+
8
+ param :first_name, proc(&:to_s), default: proc { "Unknown" }
9
+ param :second_name, proc(&:to_s), default: proc { "Unknown" }
10
+ option :email, proc(&:to_s), optional: true
11
+ option :phone, proc(&:to_s), optional: true
12
+ end
13
+
14
+ result = RubyProf.profile do
15
+ 1_000.times { User.new :Andy, email: :"andy@example.com" }
16
+ end
17
+
18
+ FileUtils.mkdir_p "./tmp"
19
+
20
+ FileUtils.touch "./tmp/profile.dot"
21
+ File.open("./tmp/profile.dot", "w+") do |output|
22
+ RubyProf::DotPrinter.new(result).print(output, min_percent: 0)
23
+ end
24
+
25
+ FileUtils.touch "./tmp/profile.html"
26
+ File.open("./tmp/profile.html", "w+") do |output|
27
+ RubyProf::CallStackPrinter.new(result).print(output, min_percent: 0)
28
+ end
@@ -3,22 +3,18 @@ Bundler.require(:benchmarks)
3
3
  class PlainRubyTest
4
4
  attr_reader :foo, :bar
5
5
 
6
- def initialize(foo:, bar:)
7
- @foo = foo
8
- @bar = bar
9
- fail TypeError unless String === @foo
10
- fail TypeError unless String === @bar
6
+ def initialize(options)
7
+ @foo = options[:foo].to_s
8
+ @bar = options[:bar].to_s
11
9
  end
12
10
  end
13
11
 
14
12
  require "dry-initializer"
15
- require "dry/initializer/types"
16
13
  class DryTest
17
14
  extend Dry::Initializer::Mixin
18
- extend Dry::Initializer::Types
19
15
 
20
- option :foo, type: String
21
- option :bar, type: String
16
+ option :foo, &(:to_s)
17
+ option :bar, &(:to_s)
22
18
  end
23
19
 
24
20
  require "virtus"
@@ -12,13 +12,11 @@ class PlainRubyTest
12
12
  end
13
13
 
14
14
  require "dry-initializer"
15
- require "dry/initializer/types"
16
15
  class DryTest
17
16
  extend Dry::Initializer::Mixin
18
- extend Dry::Initializer::Types
19
17
 
20
- option :foo, type: String, default: proc { "FOO" }
21
- option :bar, type: String, default: proc { "BAR" }
18
+ option :foo, proc(&:to_s), default: proc { "FOO" }
19
+ option :bar, proc(&:to_s), default: proc { "BAR" }
22
20
  end
23
21
 
24
22
  require "virtus"
@@ -3,9 +3,9 @@ Bundler.require(:benchmarks)
3
3
  class PlainRubyTest
4
4
  attr_reader :foo, :bar
5
5
 
6
- def initialize(foo:, bar:)
7
- @foo = foo
8
- @bar = bar
6
+ def initialize(options = {})
7
+ @foo = options[:foo]
8
+ @bar = options[:bar]
9
9
  end
10
10
  end
11
11
 
@@ -1,6 +1,6 @@
1
1
  Gem::Specification.new do |gem|
2
2
  gem.name = "dry-initializer"
3
- gem.version = "0.11.0"
3
+ gem.version = "1.1.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"
@@ -0,0 +1,92 @@
1
+ module Dry::Initializer
2
+ # Contains definitions for a single attribute, and builds its parts of mixin
3
+ class Attribute
4
+ class << self
5
+ # Collection of additional dispatchers for method options
6
+ #
7
+ # @example Enhance the gem by adding :coercer alias for type
8
+ # Dry::Initializer::Attribute.dispatchers << -> (string: nil, **op) do
9
+ # op[:type] = proc(&:to_s) if string
10
+ # op
11
+ # end
12
+ #
13
+ # class User
14
+ # extend Dry::Initializer
15
+ # param :name, string: true # same as `type: proc(&:to_s)`
16
+ # end
17
+ #
18
+ def dispatchers
19
+ @@dispatchers ||= []
20
+ end
21
+
22
+ def new(source, coercer = nil, **options)
23
+ options[:source] = source
24
+ options[:target] = options.delete(:as) || source
25
+ options[:type] ||= coercer
26
+ params = dispatchers.inject(options) { |h, m| m.call(h) }
27
+
28
+ super(params)
29
+ end
30
+
31
+ def param(*args)
32
+ Param.new(*args)
33
+ end
34
+
35
+ def option(*args)
36
+ Option.new(*args)
37
+ end
38
+ end
39
+
40
+ attr_reader :source, :target, :coercer, :default, :optional, :reader
41
+
42
+ def initialize(options)
43
+ @source = options[:source]
44
+ @target = options[:target]
45
+ @coercer = options[:type]
46
+ @default = options[:default]
47
+ @optional = !!(options[:optional] || @default)
48
+ @reader = options.fetch(:reader, :public)
49
+ validate
50
+ end
51
+
52
+ def ==(other)
53
+ source == other.source
54
+ end
55
+
56
+ # definition for the getter method
57
+ def getter
58
+ return unless reader
59
+ command = %w(private protected).include?(reader.to_s) ? reader : :public
60
+
61
+ <<-RUBY.gsub(/^ *\|/, "")
62
+ |def #{target}
63
+ | @#{target} unless @#{target} == Dry::Initializer::UNDEFINED
64
+ |end
65
+ |#{command} :#{target}
66
+ RUBY
67
+ end
68
+
69
+ private
70
+
71
+ def validate
72
+ validate_target
73
+ validate_default
74
+ validate_coercer
75
+ end
76
+
77
+ def validate_target
78
+ return if target =~ /\A\w+\Z/
79
+ fail ArgumentError.new("Invalid name '#{target}' for the target variable")
80
+ end
81
+
82
+ def validate_default
83
+ return if default.nil? || default.is_a?(Proc)
84
+ fail DefaultValueError.new(source, default)
85
+ end
86
+
87
+ def validate_coercer
88
+ return if coercer.nil? || coercer.respond_to?(:call)
89
+ fail TypeConstraintError.new(source, coercer)
90
+ end
91
+ end
92
+ end
@@ -1,100 +1,104 @@
1
- require "set"
2
1
  module Dry::Initializer
3
- # Rebuilds the initializer every time a new argument defined
4
- #
5
- # @api private
6
- #
7
2
  class Builder
8
- include Plugins
3
+ def param(*args)
4
+ @params = insert(@params, Attribute.param(*args))
5
+ validate_collections
6
+ end
7
+
8
+ def option(*args)
9
+ @options = insert(@options, Attribute.option(*args))
10
+ validate_collections
11
+ end
12
+
13
+ def call(mixin)
14
+ defaults = send(:defaults)
15
+ coercers = send(:coercers)
16
+ mixin.send(:define_method, :__defaults__) { defaults }
17
+ mixin.send(:define_method, :__coercers__) { coercers }
18
+ mixin.class_eval(code, __FILE__, __LINE__ + 1)
19
+ end
20
+
21
+ private
9
22
 
10
23
  def initialize
11
- @signature = Signature.new
12
- @plugins = Set.new [VariableSetter, TypeConstraint, DefaultProc]
13
- @parts = []
24
+ @params = []
25
+ @options = []
14
26
  end
15
27
 
16
- # Register new plugin to be applied as a chunk of code, or a proc
17
- # to be evaluated in the instance's scope
18
- #
19
- # @param [Dry::Initializer::Plugin]
20
- #
21
- # @return [Dry::Initializer::Builder]
22
- #
23
- def register(plugin)
24
- plugins = @plugins + [plugin]
25
- copy { @plugins = plugins }
28
+ def insert(collection, new_item)
29
+ index = collection.index(new_item) || collection.count
30
+ collection.dup.tap { |list| list[index] = new_item }
26
31
  end
27
32
 
28
- # Defines new agrument and reloads mixin definitions
29
- #
30
- # @param [#to_sym] name
31
- # @param [Hash<Symbol, Object>] settings
32
- #
33
- # @return [Dry::Initializer::Builder]
34
- #
35
- def define(name, settings)
36
- signature = @signature.add(name, settings)
37
- parts = @parts + @plugins.map { |p| p.call(name, settings) }.compact
38
-
39
- copy do
40
- @signature = signature
41
- @parts = parts
42
- end
33
+ def code
34
+ <<-RUBY.gsub(/^ +\|/, "")
35
+ |def __initialize__(#{initializer_signatures})
36
+ | @__options__ = __options__
37
+ |#{initializer_presetters}
38
+ |#{initializer_setters}
39
+ |end
40
+ |private :__initialize__
41
+ |private :__defaults__
42
+ |private :__coercers__
43
+ |
44
+ |#{getters}
45
+ RUBY
43
46
  end
44
47
 
45
- # Redeclares initializer and readers in the mixin module
46
- #
47
- # @param [Module] mixin
48
- #
49
- def call(mixin)
50
- define_readers(mixin)
51
- reload_initializer(mixin)
52
- reload_callback(mixin)
53
- mixin
48
+ def attributes
49
+ @params + @options
54
50
  end
55
51
 
56
- private
52
+ def duplications
53
+ attributes.group_by(&:target)
54
+ .reject { |_, val| val.count == 1 }
55
+ .keys
56
+ end
57
57
 
58
- def copy(&block)
59
- dup.tap { |instance| instance.instance_eval(&block) }
58
+ def initializer_signatures
59
+ sig = @params.map(&:initializer_signature).compact.uniq
60
+ sig << (sig.any? && @options.any? ? "**__options__" : "__options__ = {}")
61
+ sig.join(", ")
60
62
  end
61
63
 
62
- def define_readers(mixin)
63
- define_reader(
64
- mixin,
65
- :attr_reader,
66
- ->(item) { item.settings[:reader] != false }
67
- )
68
- define_reader mixin, :private
69
- define_reader mixin, :protected
64
+ def initializer_presetters
65
+ dups = duplications
66
+ attributes
67
+ .map { |a| " #{a.presetter}" if dups.include? a.target }
68
+ .compact.uniq.join("\n")
70
69
  end
71
70
 
72
- def define_reader(mixin, method, filter_lambda = nil)
73
- filter_lambda ||= ->(item) { item.settings[:reader] == method }
74
- readers = @signature.select(&filter_lambda).map(&:rename)
75
- mixin.send method, *readers if readers.any?
71
+ def initializer_setters
72
+ dups = duplications
73
+ attributes.map do |a|
74
+ dups.include?(a.target) ? " #{a.safe_setter}" : " #{a.fast_setter}"
75
+ end.compact.uniq.join("\n")
76
76
  end
77
77
 
78
- def reload_initializer(mixin)
79
- mixin.class_eval <<-RUBY, __FILE__, __LINE__ + 1
80
- def __initialize__(#{@signature.call})
81
- @__options__ = __options__
82
- #{@parts.select { |part| String === part }.join("\n")}
83
- __after_initialize__
84
- end
85
- RUBY
78
+ def getters
79
+ attributes.map(&:getter).compact.uniq.join("\n")
80
+ end
81
+
82
+ def defaults
83
+ attributes.map(&:default_hash).reduce({}, :merge)
84
+ end
86
85
 
87
- mixin.send :private, :__initialize__
86
+ def coercers
87
+ attributes.map(&:coercer_hash).reduce({}, :merge)
88
88
  end
89
89
 
90
- def reload_callback(mixin)
91
- blocks = @parts.select { |part| Proc === part }
90
+ def validate_collections
91
+ optional_param = nil
92
92
 
93
- mixin.send :define_method, :__after_initialize__ do
94
- blocks.each { |block| instance_eval(&block) }
93
+ @params.each do |param|
94
+ if param.optional
95
+ optional_param = param.source if param.optional
96
+ elsif optional_param
97
+ fail ParamsOrderError.new(param.source, optional_param)
98
+ end
95
99
  end
96
100
 
97
- mixin.send :private, :__after_initialize__
101
+ self
98
102
  end
99
103
  end
100
104
  end
@@ -0,0 +1,8 @@
1
+ module Dry::Initializer
2
+ class DefaultValueError < TypeError
3
+ def initialize(name, value)
4
+ super "Cannot set #{value.inspect} directly as a default value" \
5
+ " of the argument '#{name}'. Wrap it to either proc or lambda."
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,8 @@
1
+ module Dry::Initializer
2
+ class ParamsOrderError < SyntaxError
3
+ def initialize(required, optional)
4
+ super "Optional param '#{optional}'" \
5
+ " should not preceed required '#{required}'"
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,7 @@
1
+ module Dry::Initializer
2
+ class TypeConstraintError < TypeError
3
+ def initialize(name, type)
4
+ super "#{type} constraint for argument '#{name}' doesn't respond to #call"
5
+ end
6
+ end
7
+ end