dry-initializer 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. checksums.yaml +7 -0
  2. data/.codeclimate.yml +15 -0
  3. data/.gitignore +9 -0
  4. data/.rspec +3 -0
  5. data/.rubocop.yml +91 -0
  6. data/.travis.yml +16 -0
  7. data/CHANGELOG.md +3 -0
  8. data/Gemfile +25 -0
  9. data/Guardfile +5 -0
  10. data/LICENSE.txt +21 -0
  11. data/README.md +444 -0
  12. data/Rakefile +47 -0
  13. data/benchmarks/options.rb +54 -0
  14. data/benchmarks/params.rb +73 -0
  15. data/benchmarks/params_vs_options.rb +35 -0
  16. data/benchmarks/several_defaults.rb +82 -0
  17. data/benchmarks/with_defaults.rb +55 -0
  18. data/benchmarks/with_types.rb +62 -0
  19. data/benchmarks/with_types_and_defaults.rb +48 -0
  20. data/benchmarks/without_options.rb +52 -0
  21. data/dry-initializer.gemspec +19 -0
  22. data/lib/dry-initializer.rb +1 -0
  23. data/lib/dry/initializer.rb +53 -0
  24. data/lib/dry/initializer/argument.rb +96 -0
  25. data/lib/dry/initializer/arguments.rb +85 -0
  26. data/lib/dry/initializer/builder.rb +33 -0
  27. data/lib/dry/initializer/errors.rb +13 -0
  28. data/lib/dry/initializer/errors/existing_argument_error.rb +5 -0
  29. data/lib/dry/initializer/errors/invalid_default_value_error.rb +6 -0
  30. data/lib/dry/initializer/errors/invalid_type_error.rb +6 -0
  31. data/lib/dry/initializer/errors/key_error.rb +5 -0
  32. data/lib/dry/initializer/errors/missed_default_value_error.rb +5 -0
  33. data/lib/dry/initializer/errors/type_error.rb +5 -0
  34. data/spec/dry/base_spec.rb +21 -0
  35. data/spec/dry/default_nil_spec.rb +17 -0
  36. data/spec/dry/default_values_spec.rb +39 -0
  37. data/spec/dry/dry_type_spec.rb +25 -0
  38. data/spec/dry/invalid_default_spec.rb +13 -0
  39. data/spec/dry/invalid_type_spec.rb +13 -0
  40. data/spec/dry/missed_default_spec.rb +14 -0
  41. data/spec/dry/poro_type_spec.rb +33 -0
  42. data/spec/dry/proc_type_spec.rb +25 -0
  43. data/spec/dry/reader_spec.rb +22 -0
  44. data/spec/dry/repetitive_definitions_spec.rb +49 -0
  45. data/spec/dry/subclassing_spec.rb +24 -0
  46. data/spec/spec_helper.rb +15 -0
  47. metadata +149 -0
@@ -0,0 +1,52 @@
1
+ Bundler.require(:benchmarks)
2
+
3
+ class PlainRubyTest
4
+ attr_reader :foo, :bar
5
+
6
+ def initialize(foo:, bar:)
7
+ @foo = foo
8
+ @bar = bar
9
+ end
10
+ end
11
+
12
+ require "dry-initializer"
13
+ class DryTest
14
+ extend Dry::Initializer
15
+
16
+ option :foo
17
+ option :bar
18
+ end
19
+
20
+ require "anima"
21
+ class AnimaTest
22
+ include Anima.new(:foo, :bar)
23
+ end
24
+
25
+ require "kwattr"
26
+ class KwattrTest
27
+ kwattr :foo, :bar
28
+ end
29
+
30
+ puts "Benchmark for instantiation without options"
31
+
32
+ Benchmark.ips do |x|
33
+ x.config time: 15, warmup: 10
34
+
35
+ x.report("plain Ruby") do
36
+ PlainRubyTest.new foo: "FOO", bar: "BAR"
37
+ end
38
+
39
+ x.report("dry-initializer") do
40
+ DryTest.new foo: "FOO", bar: "BAR"
41
+ end
42
+
43
+ x.report("anima") do
44
+ AnimaTest.new foo: "FOO", bar: "BAR"
45
+ end
46
+
47
+ x.report("kwattr") do
48
+ KwattrTest.new foo: "FOO", bar: "BAR"
49
+ end
50
+
51
+ x.compare!
52
+ end
@@ -0,0 +1,19 @@
1
+ Gem::Specification.new do |gem|
2
+ gem.name = "dry-initializer"
3
+ gem.version = "0.0.1"
4
+ gem.author = ["Vladimir Kochnev (marshall-lee)", "Andrew Kozin (nepalez)"]
5
+ gem.email = ["hashtable@yandex.ru", "andrew.kozin@gmail.com"]
6
+ gem.homepage = "https://github.com/dryrb/dry-initializer"
7
+ gem.summary = "DSL for declaring params and options of the initializer"
8
+ gem.license = "MIT"
9
+
10
+ gem.files = `git ls-files`.split($INPUT_RECORD_SEPARATOR)
11
+ gem.test_files = gem.files.grep(/^spec/)
12
+ gem.extra_rdoc_files = Dir["README.md", "LICENSE", "CHANGELOG.md"]
13
+
14
+ gem.required_ruby_version = ">= 2.2"
15
+
16
+ gem.add_development_dependency "guard-rspec", "~> 4.0"
17
+ gem.add_development_dependency "rake", "~> 10.5"
18
+ gem.add_development_dependency "dry-types", "~> 0.5.1"
19
+ end
@@ -0,0 +1 @@
1
+ require_relative "dry/initializer"
@@ -0,0 +1,53 @@
1
+ module Dry
2
+ # Declares arguments of the initializer (params and options)
3
+ #
4
+ # @api public
5
+ #
6
+ module Initializer
7
+
8
+ require_relative "initializer/errors"
9
+ require_relative "initializer/argument"
10
+ require_relative "initializer/arguments"
11
+ require_relative "initializer/builder"
12
+
13
+ # Declares a plain argument
14
+ #
15
+ # @param [#to_sym] name
16
+ #
17
+ # @option options [Object] :default The default value
18
+ # @option options [#call] :type The type constraings via `dry-types`
19
+ # @option options [Boolean] :reader (true) Whether to define attr_reader
20
+ #
21
+ # @return [self] itself
22
+ #
23
+ def param(name, **options)
24
+ arguments_builder.define_initializer(name, option: false, **options)
25
+ self
26
+ end
27
+
28
+ # Declares a named argument
29
+ #
30
+ # @param (see #param)
31
+ # @option (see #param)
32
+ # @return (see #param)
33
+ #
34
+ def option(name, **options)
35
+ arguments_builder.define_initializer(name, option: true, **options)
36
+ self
37
+ end
38
+
39
+ private
40
+
41
+ def arguments_builder
42
+ @arguments_builder ||= begin
43
+ builder = Builder.new
44
+ include builder.mixin
45
+ builder
46
+ end
47
+ end
48
+
49
+ def inherited(klass)
50
+ klass.instance_variable_set(:@arguments_builder, arguments_builder)
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,96 @@
1
+ module Dry::Initializer
2
+ # A simple structure describes an argument (either param, or option)
3
+ #
4
+ # @api private
5
+ #
6
+ class Argument
7
+ include Errors
8
+
9
+ UNDEFINED = Object.new.freeze
10
+
11
+ # @!attribute [r] option
12
+ # @return [Boolean]
13
+ # Whether this is an option, or param of the initializer
14
+ attr_reader :option
15
+
16
+ # @!attribute [r] name
17
+ # @return [Symbol] the name of the argument
18
+ attr_reader :name
19
+
20
+ # @!attribute [r] default
21
+ # @return [Boolean] whether the argument has a default value
22
+ attr_reader :default
23
+
24
+ # @!attribute [r] default_value
25
+ # @return [Object] the default value of the argument
26
+ attr_reader :default_value
27
+
28
+ # @!attribute [r] type
29
+ # @return [Class, nil] a type constraint
30
+ attr_reader :type
31
+
32
+ # @!attribute [r] reader
33
+ # @return [Boolean] whether an attribute reader is defined for the argument
34
+ attr_reader :reader
35
+
36
+ def initialize(name, option:, reader: true, **options)
37
+ @name = name.to_sym
38
+ @option = option
39
+ @reader = reader
40
+ assign_default_value(options)
41
+ assign_type(options)
42
+ end
43
+
44
+ def ==(other)
45
+ other.name == name
46
+ end
47
+
48
+ def signature
49
+ case [option, default]
50
+ when [false, false] then name
51
+ when [false, true] then "#{name} = Dry::Initializer::Argument::UNDEFINED"
52
+ when [true, false] then "#{name}:"
53
+ else
54
+ "#{name}: Dry::Initializer::Argument::UNDEFINED"
55
+ end
56
+ end
57
+
58
+ def assignment
59
+ "@#{name} = #{name}"
60
+ end
61
+
62
+ def default_assignment
63
+ "@#{name} = instance_eval(&__arguments__[:#{name}].default_value)" \
64
+ " if #{name} == Dry::Initializer::Argument::UNDEFINED"
65
+ end
66
+
67
+ def type_constraint
68
+ "__arguments__[:#{name}].type.call(@#{name})"
69
+ end
70
+
71
+ private
72
+
73
+ def assign_default_value(options)
74
+ @default = options.key? :default
75
+ return unless @default
76
+
77
+ @default_value = options[:default]
78
+ return if Proc === @default_value
79
+
80
+ fail InvalidDefaultValueError.new(@default_value)
81
+ end
82
+
83
+ def assign_type(options)
84
+ return unless options.key? :type
85
+
86
+ type = options[:type]
87
+ type = plain_type_to_proc(type) if Module === type
88
+ fail InvalidTypeError.new(type) unless type.respond_to? :call
89
+ @type = type
90
+ end
91
+
92
+ def plain_type_to_proc(type)
93
+ proc { |value| fail TypeError.new(type, value) unless type === value }
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,85 @@
1
+ module Dry::Initializer
2
+ # Collection of definitions for arguments
3
+ #
4
+ # @api private
5
+ #
6
+ class Arguments
7
+ include Errors
8
+ include Enumerable
9
+
10
+ def initialize(**arguments)
11
+ @arguments = arguments
12
+ end
13
+
14
+ def add(name, options)
15
+ validate_uniqueness(name)
16
+ validate_presence_of_default(name, options)
17
+
18
+ new_argument = Argument.new(name, options)
19
+ self.class.new @arguments.merge(name.to_sym => new_argument)
20
+ end
21
+
22
+ def declaration
23
+ <<-RUBY
24
+ attr_reader #{select(&:reader).map { |arg| ":#{arg.name}" }.join(", ")}
25
+ define_method :initialize do |#{signature}|
26
+ #{assign_arguments}
27
+ #{take_declarations}
28
+ #{assign_defaults}
29
+ #{check_constraints}
30
+ end
31
+ RUBY
32
+ end
33
+
34
+ def [](name)
35
+ @arguments[name]
36
+ end
37
+
38
+ private
39
+
40
+ def each
41
+ @arguments.each { |_, argument| yield(argument) }
42
+ end
43
+
44
+ def params
45
+ reject(&:option)
46
+ end
47
+
48
+ def options
49
+ select(&:option)
50
+ end
51
+
52
+ def validate_uniqueness(name)
53
+ fail ExistingArgumentError.new(name) if self[name.to_sym]
54
+ end
55
+
56
+ def validate_presence_of_default(name, options)
57
+ return if options.key? :default
58
+ return if options[:option]
59
+ return unless params.any?(&:default)
60
+
61
+ fail MissedDefaultValueError.new(name)
62
+ end
63
+
64
+ def signature
65
+ (params + options).map(&:signature).join(", ")
66
+ end
67
+
68
+ def assign_arguments
69
+ map(&:assignment).join("\n")
70
+ end
71
+
72
+ def take_declarations
73
+ return unless any?(&:default) || any?(&:type)
74
+ "__arguments__ = self.class.send(:arguments_builder).arguments"
75
+ end
76
+
77
+ def assign_defaults
78
+ select(&:default).map(&:default_assignment).join("\n")
79
+ end
80
+
81
+ def check_constraints
82
+ select(&:type).map(&:type_constraint).join("\n")
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,33 @@
1
+ module Dry::Initializer
2
+ # Carries declarations for arguments along with a mixin module
3
+ #
4
+ # @api private
5
+ #
6
+ class Builder
7
+ def arguments
8
+ @arguments ||= Arguments.new
9
+ end
10
+
11
+ def mixin
12
+ @mixin ||= Module.new
13
+ end
14
+
15
+ def define_initializer(name, **options)
16
+ @arguments = arguments.add(name, **options)
17
+ mixin.instance_eval @arguments.declaration
18
+ end
19
+
20
+ def define_attributes_reader(name, keys)
21
+ symbol_keys = keys.map { |key| ":" << key.to_s }.join(", ")
22
+ key = '@#{key}'
23
+
24
+ mixin.class_eval <<-RUBY
25
+ def #{name}
26
+ [#{symbol_keys}].inject({}) do |hash, key|
27
+ hash.merge key => instance_variable_get(:"#{key}")
28
+ end
29
+ end
30
+ RUBY
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,13 @@
1
+ module Dry::Initializer
2
+ # Collection of gem-specific exceptions
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"
10
+ require_relative "errors/type_error"
11
+
12
+ end
13
+ end
@@ -0,0 +1,5 @@
1
+ class Dry::Initializer::Errors::ExistingArgumentError < SyntaxError
2
+ def initialize(name)
3
+ super "The argument '#{name}' is already defined."
4
+ end
5
+ end
@@ -0,0 +1,6 @@
1
+ class Dry::Initializer::Errors::InvalidDefaultValueError < SyntaxError
2
+ def initialize(value)
3
+ super "#{value.inspect} is set directly as a default value." \
4
+ " Wrap it to either proc or lambda."
5
+ end
6
+ end
@@ -0,0 +1,6 @@
1
+ class Dry::Initializer::Errors::InvalidTypeError < ::TypeError
2
+ def initialize(type)
3
+ super "#{type.inspect} doesn't describe a valid type." \
4
+ " Use either a module/class, or an object responding to #call."
5
+ end
6
+ end
@@ -0,0 +1,5 @@
1
+ class Dry::Initializer::Errors::KeyError < ::KeyError
2
+ def initialize(*keys)
3
+ super "Unrecognized keys: #{keys.join(", ")}."
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ class Dry::Initializer::Errors::MissedDefaultValueError < SyntaxError
2
+ def initialize(name)
3
+ super "You should set a default value for the '#{name}'."
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ class Dry::Initializer::Errors::TypeError < ::TypeError
2
+ def initialize(type, value)
3
+ super "#{value.inspect} mismatches the type #{type}."
4
+ end
5
+ end
@@ -0,0 +1,21 @@
1
+ describe "base example" do
2
+ before do
3
+ class Test::Foo
4
+ extend Dry::Initializer
5
+
6
+ param :foo
7
+ param :bar
8
+ option :baz
9
+ option :qux
10
+ end
11
+ end
12
+
13
+ it "instantiates attributes" do
14
+ subject = Test::Foo.new(1, 2, baz: 3, qux: 4)
15
+
16
+ expect(subject.foo).to eql 1
17
+ expect(subject.bar).to eql 2
18
+ expect(subject.baz).to eql 3
19
+ expect(subject.qux).to eql 4
20
+ end
21
+ end
@@ -0,0 +1,17 @@
1
+ describe "default nil" do
2
+ before do
3
+ class Test::Foo
4
+ extend Dry::Initializer
5
+
6
+ param :foo, default: proc { nil }
7
+ param :bar, default: proc { nil }
8
+ end
9
+ end
10
+
11
+ it "is assigned" do
12
+ subject = Test::Foo.new(1)
13
+
14
+ expect(subject.foo).to eql 1
15
+ expect(subject.bar).to be_nil
16
+ end
17
+ end