dry-initializer 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.codeclimate.yml +15 -0
- data/.gitignore +9 -0
- data/.rspec +3 -0
- data/.rubocop.yml +91 -0
- data/.travis.yml +16 -0
- data/CHANGELOG.md +3 -0
- data/Gemfile +25 -0
- data/Guardfile +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +444 -0
- data/Rakefile +47 -0
- data/benchmarks/options.rb +54 -0
- data/benchmarks/params.rb +73 -0
- data/benchmarks/params_vs_options.rb +35 -0
- data/benchmarks/several_defaults.rb +82 -0
- data/benchmarks/with_defaults.rb +55 -0
- data/benchmarks/with_types.rb +62 -0
- data/benchmarks/with_types_and_defaults.rb +48 -0
- data/benchmarks/without_options.rb +52 -0
- data/dry-initializer.gemspec +19 -0
- data/lib/dry-initializer.rb +1 -0
- data/lib/dry/initializer.rb +53 -0
- data/lib/dry/initializer/argument.rb +96 -0
- data/lib/dry/initializer/arguments.rb +85 -0
- data/lib/dry/initializer/builder.rb +33 -0
- data/lib/dry/initializer/errors.rb +13 -0
- data/lib/dry/initializer/errors/existing_argument_error.rb +5 -0
- data/lib/dry/initializer/errors/invalid_default_value_error.rb +6 -0
- data/lib/dry/initializer/errors/invalid_type_error.rb +6 -0
- data/lib/dry/initializer/errors/key_error.rb +5 -0
- data/lib/dry/initializer/errors/missed_default_value_error.rb +5 -0
- data/lib/dry/initializer/errors/type_error.rb +5 -0
- data/spec/dry/base_spec.rb +21 -0
- data/spec/dry/default_nil_spec.rb +17 -0
- data/spec/dry/default_values_spec.rb +39 -0
- data/spec/dry/dry_type_spec.rb +25 -0
- data/spec/dry/invalid_default_spec.rb +13 -0
- data/spec/dry/invalid_type_spec.rb +13 -0
- data/spec/dry/missed_default_spec.rb +14 -0
- data/spec/dry/poro_type_spec.rb +33 -0
- data/spec/dry/proc_type_spec.rb +25 -0
- data/spec/dry/reader_spec.rb +22 -0
- data/spec/dry/repetitive_definitions_spec.rb +49 -0
- data/spec/dry/subclassing_spec.rb +24 -0
- data/spec/spec_helper.rb +15 -0
- 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,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
|