dry-initializer 0.11.0 → 1.1.0

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