dry-initializer 3.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (69) hide show
  1. checksums.yaml +7 -0
  2. data/.codeclimate.yml +23 -0
  3. data/.gitignore +10 -0
  4. data/.rspec +4 -0
  5. data/.rubocop.yml +51 -0
  6. data/.travis.yml +28 -0
  7. data/CHANGELOG.md +883 -0
  8. data/Gemfile +29 -0
  9. data/Guardfile +5 -0
  10. data/LICENSE.txt +21 -0
  11. data/README.md +90 -0
  12. data/Rakefile +8 -0
  13. data/benchmarks/compare_several_defaults.rb +82 -0
  14. data/benchmarks/plain_options.rb +63 -0
  15. data/benchmarks/plain_params.rb +84 -0
  16. data/benchmarks/with_coercion.rb +71 -0
  17. data/benchmarks/with_defaults.rb +66 -0
  18. data/benchmarks/with_defaults_and_coercion.rb +59 -0
  19. data/dry-initializer.gemspec +20 -0
  20. data/lib/dry-initializer.rb +1 -0
  21. data/lib/dry/initializer.rb +61 -0
  22. data/lib/dry/initializer/builders.rb +7 -0
  23. data/lib/dry/initializer/builders/attribute.rb +81 -0
  24. data/lib/dry/initializer/builders/initializer.rb +61 -0
  25. data/lib/dry/initializer/builders/reader.rb +50 -0
  26. data/lib/dry/initializer/builders/signature.rb +32 -0
  27. data/lib/dry/initializer/config.rb +184 -0
  28. data/lib/dry/initializer/definition.rb +65 -0
  29. data/lib/dry/initializer/dispatchers.rb +112 -0
  30. data/lib/dry/initializer/dispatchers/build_nested_type.rb +59 -0
  31. data/lib/dry/initializer/dispatchers/check_type.rb +43 -0
  32. data/lib/dry/initializer/dispatchers/prepare_default.rb +40 -0
  33. data/lib/dry/initializer/dispatchers/prepare_ivar.rb +12 -0
  34. data/lib/dry/initializer/dispatchers/prepare_optional.rb +13 -0
  35. data/lib/dry/initializer/dispatchers/prepare_reader.rb +30 -0
  36. data/lib/dry/initializer/dispatchers/prepare_source.rb +28 -0
  37. data/lib/dry/initializer/dispatchers/prepare_target.rb +44 -0
  38. data/lib/dry/initializer/dispatchers/unwrap_type.rb +22 -0
  39. data/lib/dry/initializer/dispatchers/wrap_type.rb +27 -0
  40. data/lib/dry/initializer/dsl.rb +43 -0
  41. data/lib/dry/initializer/mixin.rb +15 -0
  42. data/lib/dry/initializer/mixin/local.rb +19 -0
  43. data/lib/dry/initializer/mixin/root.rb +10 -0
  44. data/lib/dry/initializer/struct.rb +40 -0
  45. data/lib/dry/initializer/undefined.rb +2 -0
  46. data/lib/tasks/benchmark.rake +41 -0
  47. data/lib/tasks/profile.rake +78 -0
  48. data/spec/attributes_spec.rb +38 -0
  49. data/spec/coercion_of_nil_spec.rb +25 -0
  50. data/spec/custom_dispatchers_spec.rb +35 -0
  51. data/spec/custom_initializer_spec.rb +30 -0
  52. data/spec/default_values_spec.rb +83 -0
  53. data/spec/definition_spec.rb +111 -0
  54. data/spec/invalid_default_spec.rb +13 -0
  55. data/spec/list_type_spec.rb +32 -0
  56. data/spec/missed_default_spec.rb +14 -0
  57. data/spec/nested_type_spec.rb +44 -0
  58. data/spec/optional_spec.rb +71 -0
  59. data/spec/options_tolerance_spec.rb +11 -0
  60. data/spec/public_attributes_utility_spec.rb +22 -0
  61. data/spec/reader_spec.rb +87 -0
  62. data/spec/repetitive_definitions_spec.rb +69 -0
  63. data/spec/several_assignments_spec.rb +41 -0
  64. data/spec/spec_helper.rb +22 -0
  65. data/spec/subclassing_spec.rb +49 -0
  66. data/spec/type_argument_spec.rb +35 -0
  67. data/spec/type_constraint_spec.rb +78 -0
  68. data/spec/value_coercion_via_dry_types_spec.rb +29 -0
  69. metadata +189 -0
@@ -0,0 +1,40 @@
1
+ #
2
+ # Prepares the `:default` option
3
+ #
4
+ # It must respond to `.call` without arguments
5
+ #
6
+ module Dry::Initializer::Dispatchers::PrepareDefault
7
+ extend self
8
+
9
+ def call(default: nil, optional: nil, **options)
10
+ default = callable! default
11
+ check_arity! default
12
+
13
+ { default: default, optional: (optional | default), **options }
14
+ end
15
+
16
+ private
17
+
18
+ def callable!(default)
19
+ return unless default
20
+ return default if default.respond_to?(:call)
21
+ return callable(default.to_proc) if default.respond_to?(:to_proc)
22
+
23
+ invalid!(default)
24
+ end
25
+
26
+ def check_arity!(default)
27
+ return unless default
28
+
29
+ arity = default.method(:call).arity.to_i
30
+ return unless arity.positive?
31
+
32
+ invalid!(default)
33
+ end
34
+
35
+ def invalid!(default)
36
+ raise TypeError, "The #{default.inspect} should be" \
37
+ " either convertable to proc with no arguments," \
38
+ " or respond to #call without arguments."
39
+ end
40
+ end
@@ -0,0 +1,12 @@
1
+ #
2
+ # Prepares the variable name of a parameter or an option.
3
+ #
4
+ module Dry::Initializer::Dispatchers::PrepareIvar
5
+ module_function
6
+
7
+ def call(target:, **options)
8
+ ivar = "@#{target}".delete("?").to_sym
9
+
10
+ { target: target, ivar: ivar, **options }
11
+ end
12
+ end
@@ -0,0 +1,13 @@
1
+ #
2
+ # Defines whether an argument is optional
3
+ #
4
+ module Dry::Initializer::Dispatchers::PrepareOptional
5
+ module_function
6
+
7
+ def call(optional: nil, default: nil, required: nil, **options)
8
+ optional ||= default
9
+ optional &&= !required
10
+
11
+ { optional: !!optional, default: default, **options }
12
+ end
13
+ end
@@ -0,0 +1,30 @@
1
+ #
2
+ # Checks the reader privacy
3
+ #
4
+ module Dry::Initializer::Dispatchers::PrepareReader
5
+ extend self
6
+
7
+ def call(target: nil, reader: :public, **options)
8
+ reader = case reader.to_s
9
+ when "false", "" then nil
10
+ when "true" then :public
11
+ when "public", "private", "protected" then reader.to_sym
12
+ else invalid_reader!(target, reader)
13
+ end
14
+
15
+ { target: target, reader: reader, **options }
16
+ end
17
+
18
+ private
19
+
20
+ def invalid_reader!(target, _reader)
21
+ raise ArgumentError, <<~MESSAGE
22
+ Invalid setting for the ##{target} reader's privacy.
23
+ Use the one of the following values for the `:reader` option:
24
+ - 'public' (true) for the public reader (default)
25
+ - 'private' for the private reader
26
+ - 'protected' for the protected reader
27
+ - nil (false) if no reader should be defined
28
+ MESSAGE
29
+ end
30
+ end
@@ -0,0 +1,28 @@
1
+ #
2
+ # The dispatcher verifies a correctness of the source name
3
+ # of param or option, taken as a `:source` option.
4
+ #
5
+ # We allow any stringified name for the source.
6
+ # For example, this syntax is correct because we accept any key
7
+ # in the original hash of arguments, but give them proper names:
8
+ #
9
+ # ```ruby
10
+ # class Foo
11
+ # extend Dry::Initializer
12
+ #
13
+ # option "", as: :first
14
+ # option 1, as: :second
15
+ # end
16
+ #
17
+ # foo = Foo.new("": 42, 1: 666)
18
+ # foo.first # => 42
19
+ # foo.second # => 666
20
+ # ```
21
+ #
22
+ module Dry::Initializer::Dispatchers::PrepareSource
23
+ module_function
24
+
25
+ def call(source:, **options)
26
+ { source: source.to_s.to_sym, **options }
27
+ end
28
+ end
@@ -0,0 +1,44 @@
1
+ #
2
+ # Prepares the target name of a parameter or an option.
3
+ #
4
+ # Unlike source, the target must satisfy requirements for Ruby variable names.
5
+ # It also shouldn't be in conflict with names used by the gem.
6
+ #
7
+ module Dry::Initializer::Dispatchers::PrepareTarget
8
+ extend self
9
+
10
+ # List of variable names reserved by the gem
11
+ RESERVED = %i[
12
+ __dry_initializer_options__
13
+ __dry_initializer_config__
14
+ __dry_initializer_value__
15
+ __dry_initializer_definition__
16
+ __dry_initializer_initializer__
17
+ ].freeze
18
+
19
+ def call(source:, target: nil, as: nil, **options)
20
+ target ||= as || source
21
+ target = target.to_s.to_sym.downcase
22
+
23
+ check_ruby_name!(target)
24
+ check_reserved_names!(target)
25
+
26
+ { source: source, target: target, **options }
27
+ end
28
+
29
+ private
30
+
31
+ def check_ruby_name!(target)
32
+ return if target[/\A[[:alpha:]_][[:alnum:]_]*\??\z/u]
33
+
34
+ raise ArgumentError,
35
+ "The name `#{target}` is not allowed for Ruby methods"
36
+ end
37
+
38
+ def check_reserved_names!(target)
39
+ return unless RESERVED.include?(target)
40
+
41
+ raise ArgumentError,
42
+ "The method name `#{target}` is reserved by the dry-initializer gem"
43
+ end
44
+ end
@@ -0,0 +1,22 @@
1
+ #
2
+ # Looks at the `:type` option and counts how many nested arrays
3
+ # it contains around either nil or a callable value.
4
+ #
5
+ # The counted number is preserved in the `:wrap` virtual option
6
+ # used by the [WrapType] dispatcher.
7
+ #
8
+ module Dry::Initializer::Dispatchers::UnwrapType
9
+ extend self
10
+
11
+ def call(type: nil, wrap: 0, **options)
12
+ type, wrap = unwrap(type, 0)
13
+
14
+ { type: type, wrap: wrap, **options }
15
+ end
16
+
17
+ private
18
+
19
+ def unwrap(type, count)
20
+ type.is_a?(Array) ? unwrap(type.first, count + 1) : [type, count]
21
+ end
22
+ end
@@ -0,0 +1,27 @@
1
+ #
2
+ # Takes `:type` and `:wrap` to construct the final value coercer
3
+ #
4
+ module Dry::Initializer::Dispatchers::WrapType
5
+ extend self
6
+
7
+ def call(type: nil, wrap: 0, **options)
8
+ { type: wrapped_type(type, wrap), **options }
9
+ end
10
+
11
+ private
12
+
13
+ def wrapped_type(type, count)
14
+ return type if count.zero?
15
+
16
+ ->(value) { wrap_value(value, count, type) }
17
+ end
18
+
19
+ def wrap_value(value, count, type)
20
+ if count.zero?
21
+ type ? type.call(value) : value
22
+ else
23
+ return [wrap_value(value, count - 1, type)] unless value.is_a?(Array)
24
+ value.map { |item| wrap_value(item, count - 1, type) }
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,43 @@
1
+ module Dry::Initializer
2
+ # Module-level DSL
3
+ module DSL
4
+ # Setting for null (undefined value)
5
+ # @return [nil, Dry::Initializer::UNDEFINED]
6
+ attr_reader :null
7
+
8
+ # Returns a version of the module with custom settings
9
+ # @option settings [Boolean] :undefined
10
+ # If unassigned params and options should be treated different from nil
11
+ # @return [Dry::Initializer]
12
+ def [](undefined: true, **)
13
+ null = (undefined == false) ? nil : UNDEFINED
14
+ Module.new.tap do |mod|
15
+ mod.extend DSL
16
+ mod.include self
17
+ mod.send(:instance_variable_set, :@null, null)
18
+ end
19
+ end
20
+
21
+ # Returns mixin module to be included to target class by hand
22
+ # @return [Module]
23
+ # @yield proc defining params and options
24
+ def define(procedure = nil, &block)
25
+ config = Config.new(null: null)
26
+ config.instance_exec(&(procedure || block))
27
+ config.mixin.include Mixin::Root
28
+ config.mixin
29
+ end
30
+
31
+ private
32
+
33
+ def extended(klass)
34
+ config = Config.new(klass, null: null)
35
+ klass.send :instance_variable_set, :@dry_initializer, config
36
+ klass.include Mixin::Root
37
+ end
38
+
39
+ def self.extended(mod)
40
+ mod.instance_variable_set :@null, UNDEFINED
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,15 @@
1
+ module Dry::Initializer
2
+ # @private
3
+ module Mixin
4
+ extend DSL # @deprecated
5
+ include Dry::Initializer # @deprecated
6
+ def self.extended(klass) # @deprecated
7
+ warn "[DEPRECATED] Use Dry::Initializer instead of its alias" \
8
+ " Dry::Initializer::Mixin. The later will be removed in v2.1.0"
9
+ super
10
+ end
11
+
12
+ require_relative "mixin/root"
13
+ require_relative "mixin/local"
14
+ end
15
+ end
@@ -0,0 +1,19 @@
1
+ module Dry::Initializer::Mixin
2
+ # @private
3
+ module Local
4
+ attr_reader :klass
5
+
6
+ def inspect
7
+ "Dry::Initializer::Mixin::Local[#{klass}]"
8
+ end
9
+ alias to_s inspect
10
+ alias to_str inspect
11
+
12
+ private
13
+
14
+ def included(klass)
15
+ @klass = klass
16
+ super
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,10 @@
1
+ module Dry::Initializer::Mixin
2
+ # @private
3
+ module Root
4
+ private
5
+
6
+ def initialize(*args)
7
+ __dry_initializer_initialize__(*args)
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,40 @@
1
+ #
2
+ # The nested structure that takes nested hashes with indifferent access
3
+ #
4
+ class Dry::Initializer::Struct
5
+ extend Dry::Initializer
6
+
7
+ class << self
8
+ undef_method :param
9
+
10
+ def new(options)
11
+ super Hash(options).transform_keys(&:to_sym)
12
+ end
13
+ alias call new
14
+ end
15
+
16
+ #
17
+ # Represents event data as a nested hash with deeply stringified keys
18
+ # @return [Hash<String, ...>]
19
+ #
20
+ def to_h
21
+ self
22
+ .class
23
+ .dry_initializer
24
+ .attributes(self)
25
+ .transform_values { |v| __hashify(v) }
26
+ .stringify_keys
27
+ end
28
+
29
+ private
30
+
31
+ def __hashify(value)
32
+ case value
33
+ when Hash
34
+ value.each_with_object({}) { |(k, v), obj| obj[k.to_s] = __hashify(v) }
35
+ when Array then value.map { |v| __hashify(v) }
36
+ when Dry::Initializer::Struct then value.to_h
37
+ else value
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,2 @@
1
+ module Dry::Initializer::UNDEFINED
2
+ end
@@ -0,0 +1,41 @@
1
+ namespace :benchmark do
2
+ desc "Runs benchmarks for plain params"
3
+ task :plain_params do
4
+ system "ruby benchmarks/plain_params.rb"
5
+ end
6
+
7
+ desc "Runs benchmarks for plain options"
8
+ task :plain_options do
9
+ system "ruby benchmarks/plain_options.rb"
10
+ end
11
+
12
+ desc "Runs benchmarks for value coercion"
13
+ task :with_coercion do
14
+ system "ruby benchmarks/with_coercion.rb"
15
+ end
16
+
17
+ desc "Runs benchmarks with defaults"
18
+ task :with_defaults do
19
+ system "ruby benchmarks/with_defaults.rb"
20
+ end
21
+
22
+ desc "Runs benchmarks with defaults and coercion"
23
+ task :with_defaults_and_coercion do
24
+ system "ruby benchmarks/with_defaults_and_coercion.rb"
25
+ end
26
+
27
+ desc "Runs benchmarks for several defaults"
28
+ task :compare_several_defaults do
29
+ system "ruby benchmarks/with_several_defaults.rb"
30
+ end
31
+ end
32
+
33
+ desc "Runs all benchmarks"
34
+ task benchmark: %i[
35
+ benchmark:plain_params
36
+ benchmark:plain_options
37
+ benchmark:with_coercion
38
+ benchmark:with_defaults
39
+ benchmark:with_defaults_and_coercion
40
+ benchmark:compare_several_defaults
41
+ ]
@@ -0,0 +1,78 @@
1
+ namespace :profile do
2
+ def profile(name, execution, &definition)
3
+ require "dry-initializer"
4
+ require "ruby-prof"
5
+ require "fileutils"
6
+
7
+ definition.call
8
+ result = RubyProf.profile do
9
+ 1_000.times { execution.call }
10
+ end
11
+
12
+ FileUtils.mkdir_p "./tmp"
13
+
14
+ FileUtils.touch "./tmp/#{name}.dot"
15
+ File.open("./tmp/#{name}.dot", "w+") do |output|
16
+ RubyProf::DotPrinter.new(result).print(output, min_percent: 0)
17
+ end
18
+
19
+ FileUtils.touch "./tmp/#{name}.html"
20
+ File.open("./tmp/#{name}.html", "w+") do |output|
21
+ RubyProf::CallStackPrinter.new(result).print(output, min_percent: 0)
22
+ end
23
+
24
+ system "dot -Tpng ./tmp/#{name}.dot > ./tmp/#{name}.png"
25
+ end
26
+
27
+ desc "Profiles initialization with required param and option"
28
+ task :required do
29
+ profile("required", -> { User.new :Andy, email: "andy@example.com" }) do
30
+ class User
31
+ extend Dry::Initializer
32
+ param :name
33
+ option :email
34
+ end
35
+ end
36
+ end
37
+
38
+ desc "Profiles initialization with default param and option"
39
+ task :defaults do
40
+ profile("defaults", -> { User.new }) do
41
+ class User
42
+ extend Dry::Initializer
43
+ param :name, default: -> { :Andy }
44
+ option :email, default: -> { "andy@example.com" }
45
+ end
46
+ end
47
+ end
48
+
49
+ desc "Profiles initialization with coerced param and option"
50
+ task :coercion do
51
+ profile("coercion", -> { User.new :Andy, email: :"andy@example.com" }) do
52
+ class User
53
+ extend Dry::Initializer
54
+ param :name, proc(&:to_s)
55
+ option :email, proc(&:to_s)
56
+ end
57
+ end
58
+ end
59
+
60
+ desc "Profiles initialization with coerced defaults of param and option"
61
+ task :default_coercion do
62
+ profile("default_coercion", -> { User.new }) do
63
+ class User
64
+ extend Dry::Initializer
65
+ param :name, proc(&:to_s), default: -> { :Andy }
66
+ option :email, proc(&:to_s), default: -> { :"andy@example.com" }
67
+ end
68
+ end
69
+ end
70
+ end
71
+
72
+ desc "Makes all profiling at once"
73
+ task profile: %i[
74
+ profile:required
75
+ profile:defaults
76
+ profile:coercion
77
+ profile:default_coercion
78
+ ]