dry-initializer 0.0.1

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 (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