dry-initializer 3.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 (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
+ ]