dry-initializer 3.0.2

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 (88) hide show
  1. checksums.yaml +7 -0
  2. data/.codeclimate.yml +12 -0
  3. data/.github/ISSUE_TEMPLATE/----please-don-t-ask-for-support-via-issues.md +10 -0
  4. data/.github/ISSUE_TEMPLATE/---bug-report.md +34 -0
  5. data/.github/ISSUE_TEMPLATE/---feature-request.md +18 -0
  6. data/.github/workflows/custom_ci.yml +74 -0
  7. data/.github/workflows/docsite.yml +34 -0
  8. data/.github/workflows/sync_configs.yml +34 -0
  9. data/.gitignore +12 -0
  10. data/.rspec +4 -0
  11. data/.rubocop.yml +89 -0
  12. data/CHANGELOG.md +890 -0
  13. data/CODE_OF_CONDUCT.md +13 -0
  14. data/CONTRIBUTING.md +29 -0
  15. data/Gemfile +38 -0
  16. data/Guardfile +5 -0
  17. data/LICENSE +20 -0
  18. data/LICENSE.txt +21 -0
  19. data/README.md +89 -0
  20. data/Rakefile +8 -0
  21. data/benchmarks/compare_several_defaults.rb +82 -0
  22. data/benchmarks/plain_options.rb +63 -0
  23. data/benchmarks/plain_params.rb +84 -0
  24. data/benchmarks/with_coercion.rb +71 -0
  25. data/benchmarks/with_defaults.rb +66 -0
  26. data/benchmarks/with_defaults_and_coercion.rb +59 -0
  27. data/docsite/source/attributes.html.md +106 -0
  28. data/docsite/source/container-version.html.md +39 -0
  29. data/docsite/source/index.html.md +43 -0
  30. data/docsite/source/inheritance.html.md +43 -0
  31. data/docsite/source/optionals-and-defaults.html.md +130 -0
  32. data/docsite/source/options-tolerance.html.md +27 -0
  33. data/docsite/source/params-and-options.html.md +74 -0
  34. data/docsite/source/rails-support.html.md +101 -0
  35. data/docsite/source/readers.html.md +43 -0
  36. data/docsite/source/skip-undefined.html.md +59 -0
  37. data/docsite/source/type-constraints.html.md +160 -0
  38. data/dry-initializer.gemspec +20 -0
  39. data/lib/dry-initializer.rb +1 -0
  40. data/lib/dry/initializer.rb +61 -0
  41. data/lib/dry/initializer/builders.rb +7 -0
  42. data/lib/dry/initializer/builders/attribute.rb +81 -0
  43. data/lib/dry/initializer/builders/initializer.rb +61 -0
  44. data/lib/dry/initializer/builders/reader.rb +50 -0
  45. data/lib/dry/initializer/builders/signature.rb +32 -0
  46. data/lib/dry/initializer/config.rb +184 -0
  47. data/lib/dry/initializer/definition.rb +65 -0
  48. data/lib/dry/initializer/dispatchers.rb +112 -0
  49. data/lib/dry/initializer/dispatchers/build_nested_type.rb +59 -0
  50. data/lib/dry/initializer/dispatchers/check_type.rb +43 -0
  51. data/lib/dry/initializer/dispatchers/prepare_default.rb +40 -0
  52. data/lib/dry/initializer/dispatchers/prepare_ivar.rb +12 -0
  53. data/lib/dry/initializer/dispatchers/prepare_optional.rb +13 -0
  54. data/lib/dry/initializer/dispatchers/prepare_reader.rb +30 -0
  55. data/lib/dry/initializer/dispatchers/prepare_source.rb +28 -0
  56. data/lib/dry/initializer/dispatchers/prepare_target.rb +44 -0
  57. data/lib/dry/initializer/dispatchers/unwrap_type.rb +22 -0
  58. data/lib/dry/initializer/dispatchers/wrap_type.rb +27 -0
  59. data/lib/dry/initializer/dsl.rb +43 -0
  60. data/lib/dry/initializer/mixin.rb +15 -0
  61. data/lib/dry/initializer/mixin/local.rb +19 -0
  62. data/lib/dry/initializer/mixin/root.rb +11 -0
  63. data/lib/dry/initializer/struct.rb +39 -0
  64. data/lib/dry/initializer/undefined.rb +2 -0
  65. data/lib/tasks/benchmark.rake +41 -0
  66. data/lib/tasks/profile.rake +78 -0
  67. data/spec/attributes_spec.rb +38 -0
  68. data/spec/coercion_of_nil_spec.rb +25 -0
  69. data/spec/custom_dispatchers_spec.rb +35 -0
  70. data/spec/custom_initializer_spec.rb +30 -0
  71. data/spec/default_values_spec.rb +83 -0
  72. data/spec/definition_spec.rb +111 -0
  73. data/spec/invalid_default_spec.rb +13 -0
  74. data/spec/list_type_spec.rb +32 -0
  75. data/spec/missed_default_spec.rb +14 -0
  76. data/spec/nested_type_spec.rb +48 -0
  77. data/spec/optional_spec.rb +71 -0
  78. data/spec/options_tolerance_spec.rb +11 -0
  79. data/spec/public_attributes_utility_spec.rb +22 -0
  80. data/spec/reader_spec.rb +87 -0
  81. data/spec/repetitive_definitions_spec.rb +69 -0
  82. data/spec/several_assignments_spec.rb +41 -0
  83. data/spec/spec_helper.rb +29 -0
  84. data/spec/subclassing_spec.rb +49 -0
  85. data/spec/type_argument_spec.rb +35 -0
  86. data/spec/type_constraint_spec.rb +78 -0
  87. data/spec/value_coercion_via_dry_types_spec.rb +29 -0
  88. metadata +209 -0
@@ -0,0 +1,20 @@
1
+ Gem::Specification.new do |gem|
2
+ gem.name = "dry-initializer"
3
+ gem.version = "3.0.2"
4
+ gem.author = ["Vladimir Kochnev (marshall-lee)", "Andrew Kozin (nepalez)"]
5
+ gem.email = "andrew.kozin@gmail.com"
6
+ gem.homepage = "https://github.com/dry-rb/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.3"
15
+
16
+ gem.add_development_dependency "rspec", "~> 3.0"
17
+ gem.add_development_dependency "rake", "> 10"
18
+ gem.add_development_dependency "dry-types", "> 0.5.1"
19
+ gem.add_development_dependency "rubocop", "~> 0.49.0"
20
+ end
@@ -0,0 +1 @@
1
+ require_relative "dry/initializer"
@@ -0,0 +1,61 @@
1
+ require "set"
2
+
3
+ # Namespace for gems in a dry-rb community
4
+ module Dry
5
+ #
6
+ # DSL for declaring params and options of class initializers
7
+ #
8
+ module Initializer
9
+ require_relative "initializer/undefined"
10
+ require_relative "initializer/dsl"
11
+ require_relative "initializer/definition"
12
+ require_relative "initializer/builders"
13
+ require_relative "initializer/config"
14
+ require_relative "initializer/mixin"
15
+ require_relative "initializer/dispatchers"
16
+
17
+ # Adds methods [.[]] and [.define]
18
+ extend DSL
19
+
20
+ # Gem-related configuration
21
+ # @return [Dry::Initializer::Config]
22
+ def dry_initializer
23
+ @dry_initializer ||= Config.new(self)
24
+ end
25
+
26
+ # Adds or redefines a parameter of [#dry_initializer]
27
+ # @param [Symbol] name
28
+ # @param [#call, nil] type (nil)
29
+ # @option opts [Proc] :default
30
+ # @option opts [Boolean] :optional
31
+ # @option opts [Symbol] :as
32
+ # @option opts [true, false, :protected, :public, :private] :reader
33
+ # @yield block with nested definition
34
+ # @return [self] itself
35
+ def param(name, type = nil, **opts, &block)
36
+ dry_initializer.param(name, type, **opts, &block)
37
+ self
38
+ end
39
+
40
+ # Adds or redefines an option of [#dry_initializer]
41
+ # @param (see #param)
42
+ # @option (see #param)
43
+ # @yield (see #param)
44
+ # @return (see #param)
45
+ def option(name, type = nil, **opts, &block)
46
+ dry_initializer.option(name, type, **opts, &block)
47
+ self
48
+ end
49
+
50
+ private
51
+
52
+ def inherited(klass)
53
+ super
54
+ config = Config.new(klass, null: dry_initializer.null)
55
+ klass.send(:instance_variable_set, :@dry_initializer, config)
56
+ dry_initializer.children << config
57
+ end
58
+
59
+ require_relative "initializer/struct"
60
+ end
61
+ end
@@ -0,0 +1,7 @@
1
+ module Dry::Initializer
2
+ # @private
3
+ module Builders
4
+ require_relative "builders/reader"
5
+ require_relative "builders/initializer"
6
+ end
7
+ end
@@ -0,0 +1,81 @@
1
+ module Dry::Initializer::Builders
2
+ # @private
3
+ class Attribute
4
+ def self.[](definition)
5
+ new(definition).call
6
+ end
7
+
8
+ def call
9
+ lines.compact
10
+ end
11
+
12
+ private
13
+
14
+ # rubocop: disable Metrics/MethodLength
15
+ def initialize(definition)
16
+ @definition = definition
17
+ @option = definition.option
18
+ @type = definition.type
19
+ @optional = definition.optional
20
+ @default = definition.default
21
+ @source = definition.source
22
+ @ivar = definition.ivar
23
+ @null = definition.null ? "Dry::Initializer::UNDEFINED" : "nil"
24
+ @opts = "__dry_initializer_options__"
25
+ @congif = "__dry_initializer_config__"
26
+ @item = "__dry_initializer_definition__"
27
+ @val = @option ? "__dry_initializer_value__" : @source
28
+ end
29
+ # rubocop: enable Metrics/MethodLength
30
+
31
+ def lines
32
+ [
33
+ "",
34
+ definition_line,
35
+ reader_line,
36
+ default_line,
37
+ coercion_line,
38
+ assignment_line
39
+ ]
40
+ end
41
+
42
+ def reader_line
43
+ return unless @option
44
+ @optional ? optional_reader : required_reader
45
+ end
46
+
47
+ def optional_reader
48
+ "#{@val} = #{@opts}.fetch(:'#{@source}', #{@null})"
49
+ end
50
+
51
+ def required_reader
52
+ "#{@val} = #{@opts}.fetch(:'#{@source}')" \
53
+ " { raise KeyError, \"\#{self.class}: #{@definition} is required\" }"
54
+ end
55
+
56
+ def definition_line
57
+ return unless @type || @default
58
+ "#{@item} = __dry_initializer_config__.definitions[:'#{@source}']"
59
+ end
60
+
61
+ def default_line
62
+ return unless @default
63
+ "#{@val} = instance_exec(&#{@item}.default) if #{@null} == #{@val}"
64
+ end
65
+
66
+ def coercion_line
67
+ return unless @type
68
+ arity = @type.is_a?(Proc) ? @type.arity : @type.method(:call).arity
69
+ if arity.abs == 1
70
+ "#{@val} = #{@item}.type.call(#{@val}) unless #{@null} == #{@val}"
71
+ else
72
+ "#{@val} = #{@item}.type.call(#{@val}, self) unless #{@null} == #{@val}"
73
+ end
74
+ end
75
+
76
+ def assignment_line
77
+ "#{@ivar} = #{@val}" \
78
+ " unless #{@null} == #{@val} && instance_variable_defined?(:#{@ivar})"
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,61 @@
1
+ module Dry::Initializer::Builders
2
+ # @private
3
+ class Initializer
4
+ require_relative "signature"
5
+ require_relative "attribute"
6
+
7
+ def self.[](config)
8
+ new(config).call
9
+ end
10
+
11
+ def call
12
+ lines.flatten.compact.join("\n")
13
+ end
14
+
15
+ private
16
+
17
+ def initialize(config)
18
+ @config = config
19
+ @definitions = config.definitions.values
20
+ end
21
+
22
+ def lines
23
+ [
24
+ undef_line,
25
+ define_line,
26
+ params_lines,
27
+ options_lines,
28
+ end_line
29
+ ]
30
+ end
31
+
32
+ def undef_line
33
+ "undef :__dry_initializer_initialize__" \
34
+ " if private_method_defined? :__dry_initializer_initialize__"
35
+ end
36
+
37
+ def define_line
38
+ "private def __dry_initializer_initialize__(#{Signature[@config]})"
39
+ end
40
+
41
+ def params_lines
42
+ @definitions.reject(&:option)
43
+ .flat_map { |item| Attribute[item] }
44
+ .map { |line| " " << line }
45
+ end
46
+
47
+ def options_lines
48
+ @definitions.select(&:option)
49
+ .flat_map { |item| Attribute[item] }
50
+ .map { |line| " " << line }
51
+ end
52
+
53
+ def end_line
54
+ "end"
55
+ end
56
+
57
+ def private_line
58
+ "private :__dry_initializer_initialize__"
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,50 @@
1
+ module Dry::Initializer::Builders
2
+ # @private
3
+ class Reader
4
+ def self.[](definition)
5
+ new(definition).call
6
+ end
7
+
8
+ def call
9
+ lines.flatten.compact.join("\n")
10
+ end
11
+
12
+ private
13
+
14
+ def initialize(definition)
15
+ @target = definition.target
16
+ @ivar = definition.ivar
17
+ @null = definition.null
18
+ @reader = definition.reader
19
+ end
20
+
21
+ def lines
22
+ [undef_line, attribute_line, method_lines, type_line]
23
+ end
24
+
25
+ def undef_line
26
+ "undef :#{@target} if method_defined?(:#{@target})" \
27
+ " || private_method_defined?(:#{@target})" \
28
+ " || protected_method_defined?(:#{@target})"
29
+ end
30
+
31
+ def attribute_line
32
+ return unless @reader
33
+ "attr_reader :#{@target}" unless @null
34
+ end
35
+
36
+ def method_lines
37
+ return unless @reader
38
+ return unless @null
39
+ [
40
+ "def #{@target}",
41
+ " #{@ivar} unless Dry::Initializer::UNDEFINED == #{@ivar}",
42
+ "end"
43
+ ]
44
+ end
45
+
46
+ def type_line
47
+ "#{@reader} :#{@target}" if %i[private protected].include? @reader
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,32 @@
1
+ module Dry::Initializer::Builders
2
+ # @private
3
+ class Signature
4
+ def self.[](config)
5
+ new(config).call
6
+ end
7
+
8
+ def call
9
+ [*required_params, *optional_params, "*", options].compact.join(", ")
10
+ end
11
+
12
+ private
13
+
14
+ def initialize(config)
15
+ @config = config
16
+ @options = config.options.any?
17
+ @null = config.null ? "Dry::Initializer::UNDEFINED" : "nil"
18
+ end
19
+
20
+ def required_params
21
+ @config.params.reject(&:optional).map(&:source)
22
+ end
23
+
24
+ def optional_params
25
+ @config.params.select(&:optional).map { |rec| "#{rec.source} = #{@null}" }
26
+ end
27
+
28
+ def options
29
+ "**__dry_initializer_options__" if @options
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,184 @@
1
+ module Dry::Initializer
2
+ #
3
+ # Gem-related configuration of some class
4
+ #
5
+ class Config
6
+ # @!attribute [r] null
7
+ # @return [Dry::Initializer::UNDEFINED, nil] value of unassigned variable
8
+
9
+ # @!attribute [r] extended_class
10
+ # @return [Class] the class whose config collected by current object
11
+
12
+ # @!attribute [r] parent
13
+ # @return [Dry::Initializer::Config] parent configuration
14
+
15
+ # @!attribute [r] definitions
16
+ # @return [Hash<Symbol, Dry::Initializer::Definition>]
17
+ # hash of attribute definitions with their source names
18
+
19
+ attr_reader :null, :extended_class, :parent, :definitions
20
+
21
+ # @!attribute [r] mixin
22
+ # @return [Module] reference to the module to be included into class
23
+ def mixin
24
+ @mixin ||= Module.new.tap do |mod|
25
+ __dry_initializer__ = self
26
+ mod.extend(Mixin::Local)
27
+ mod.send :define_method, :__dry_initializer_config__ do
28
+ __dry_initializer__
29
+ end
30
+ mod.send :private, :__dry_initializer_config__
31
+ end
32
+ end
33
+
34
+ # List of configs of all subclasses of the [#extended_class]
35
+ # @return [Array<Dry::Initializer::Config>]
36
+ def children
37
+ @children ||= Set.new
38
+ end
39
+
40
+ # List of definitions for initializer params
41
+ # @return [Array<Dry::Initializer::Definition>]
42
+ def params
43
+ definitions.values.reject(&:option)
44
+ end
45
+
46
+ # List of definitions for initializer options
47
+ # @return [Array<Dry::Initializer::Definition>]
48
+ def options
49
+ definitions.values.select(&:option)
50
+ end
51
+
52
+ # Adds or redefines a parameter
53
+ # @param [Symbol] name
54
+ # @param [#call, nil] type (nil)
55
+ # @option opts [Proc] :default
56
+ # @option opts [Boolean] :optional
57
+ # @option opts [Symbol] :as
58
+ # @option opts [true, false, :protected, :public, :private] :reader
59
+ # @return [self] itself
60
+ def param(name, type = nil, **opts, &block)
61
+ add_definition(false, name, type, block, **opts)
62
+ end
63
+
64
+ # Adds or redefines an option of [#dry_initializer]
65
+ #
66
+ # @param (see #param)
67
+ # @option (see #param)
68
+ # @return (see #param)
69
+ #
70
+ def option(name, type = nil, **opts, &block)
71
+ add_definition(true, name, type, block, **opts)
72
+ end
73
+
74
+ # The hash of public attributes for an instance of the [#extended_class]
75
+ # @param [Dry::Initializer::Instance] instance
76
+ # @return [Hash<Symbol, Object>]
77
+ def public_attributes(instance)
78
+ definitions.values.each_with_object({}) do |item, obj|
79
+ key = item.target
80
+ next unless instance.respond_to? key
81
+ val = instance.send(key)
82
+ obj[key] = val unless null == val
83
+ end
84
+ end
85
+
86
+ # The hash of assigned attributes for an instance of the [#extended_class]
87
+ # @param [Dry::Initializer::Instance] instance
88
+ # @return [Hash<Symbol, Object>]
89
+ def attributes(instance)
90
+ definitions.values.each_with_object({}) do |item, obj|
91
+ key = item.target
92
+ val = instance.send(:instance_variable_get, item.ivar)
93
+ obj[key] = val unless null == val
94
+ end
95
+ end
96
+
97
+ # Code of the `#__initialize__` method
98
+ # @return [String]
99
+ def code
100
+ Builders::Initializer[self]
101
+ end
102
+
103
+ # Finalizes config
104
+ # @return [self]
105
+ def finalize
106
+ @definitions = final_definitions
107
+ check_order_of_params
108
+ mixin.class_eval(code)
109
+ children.each(&:finalize)
110
+ self
111
+ end
112
+
113
+ # Human-readable representation of configured params and options
114
+ # @return [String]
115
+ def inch
116
+ line = Builders::Signature[self]
117
+ line = line.gsub("__dry_initializer_options__", "options")
118
+ lines = ["@!method initialize(#{line})"]
119
+ lines += ["Initializes an instance of #{extended_class}"]
120
+ lines += definitions.values.map(&:inch)
121
+ lines += ["@return [#{extended_class}]"]
122
+ lines.join("\n")
123
+ end
124
+
125
+ private
126
+
127
+ def initialize(extended_class = nil, null: UNDEFINED)
128
+ @extended_class = extended_class.tap { |klass| klass&.include mixin }
129
+ sklass = extended_class&.superclass
130
+ @parent = sklass.dry_initializer if sklass.is_a? Dry::Initializer
131
+ @null = null || parent&.null
132
+ @definitions = {}
133
+ finalize
134
+ end
135
+
136
+ # rubocop: disable Metrics/MethodLength
137
+ def add_definition(option, name, type, block, **opts)
138
+ opts = {
139
+ parent: extended_class,
140
+ option: option,
141
+ null: null,
142
+ source: name,
143
+ type: type,
144
+ block: block,
145
+ **opts
146
+ }
147
+
148
+ options = Dispatchers.call(**opts)
149
+ definition = Definition.new(**options)
150
+ definitions[definition.source] = definition
151
+ finalize
152
+ mixin.class_eval definition.code
153
+ end
154
+ # rubocop: enable Metrics/MethodLength
155
+
156
+ def final_definitions
157
+ parent_definitions = Hash(parent&.definitions&.dup)
158
+ definitions.each_with_object(parent_definitions) do |(key, val), obj|
159
+ obj[key] = check_type(obj[key], val)
160
+ end
161
+ end
162
+
163
+ def check_type(previous, current)
164
+ return current unless previous
165
+ return current if previous.option == current.option
166
+ raise SyntaxError,
167
+ "cannot reload #{previous} of #{extended_class.superclass}" \
168
+ " by #{current} of its subclass #{extended_class}"
169
+ end
170
+
171
+ def check_order_of_params
172
+ params.inject(nil) do |optional, current|
173
+ if current.optional
174
+ current
175
+ elsif optional
176
+ raise SyntaxError, "#{extended_class}: required #{current}" \
177
+ " goes after optional #{optional}"
178
+ else
179
+ optional
180
+ end
181
+ end
182
+ end
183
+ end
184
+ end