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