strict 0.0.0 → 1.1.0

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 (46) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +14 -2
  3. data/Gemfile.lock +19 -2
  4. data/README.md +165 -6
  5. data/lib/strict/accessor/attributes.rb +15 -0
  6. data/lib/strict/accessor/module.rb +45 -0
  7. data/lib/strict/assignment_error.rb +25 -0
  8. data/lib/strict/attribute.rb +71 -0
  9. data/lib/strict/attributes/class.rb +17 -0
  10. data/lib/strict/attributes/coercer.rb +32 -0
  11. data/lib/strict/attributes/configuration.rb +46 -0
  12. data/lib/strict/attributes/dsl.rb +43 -0
  13. data/lib/strict/attributes/instance.rb +68 -0
  14. data/lib/strict/coercers/array.rb +22 -0
  15. data/lib/strict/coercers/hash.rb +34 -0
  16. data/lib/strict/dsl/coercible.rb +14 -0
  17. data/lib/strict/dsl/validatable.rb +28 -0
  18. data/lib/strict/implementation_does_not_conform_error.rb +88 -0
  19. data/lib/strict/initialization_error.rb +57 -0
  20. data/lib/strict/interface.rb +21 -0
  21. data/lib/strict/interfaces/instance.rb +54 -0
  22. data/lib/strict/method.rb +72 -0
  23. data/lib/strict/method_call_error.rb +72 -0
  24. data/lib/strict/method_definition_error.rb +46 -0
  25. data/lib/strict/method_return_error.rb +25 -0
  26. data/lib/strict/methods/configuration.rb +14 -0
  27. data/lib/strict/methods/dsl.rb +56 -0
  28. data/lib/strict/methods/module.rb +26 -0
  29. data/lib/strict/methods/verifiable_method.rb +158 -0
  30. data/lib/strict/object.rb +9 -0
  31. data/lib/strict/parameter.rb +63 -0
  32. data/lib/strict/reader/attributes.rb +15 -0
  33. data/lib/strict/reader/module.rb +27 -0
  34. data/lib/strict/return.rb +28 -0
  35. data/lib/strict/validators/all_of.rb +24 -0
  36. data/lib/strict/validators/any_of.rb +24 -0
  37. data/lib/strict/validators/anything.rb +20 -0
  38. data/lib/strict/validators/array_of.rb +24 -0
  39. data/lib/strict/validators/boolean.rb +20 -0
  40. data/lib/strict/validators/hash_of.rb +25 -0
  41. data/lib/strict/validators/range_of.rb +24 -0
  42. data/lib/strict/value.rb +22 -0
  43. data/lib/strict/version.rb +1 -1
  44. data/lib/strict.rb +1 -0
  45. data/strict.gemspec +41 -0
  46. metadata +97 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b23b160a9d38c3450223118fcf7c11f7833f1bd25c7f384c966e553e52430e52
4
- data.tar.gz: 8588a01fe2783f8ad7955089af408d969e11713309e97db7eaaa38e344d8b070
3
+ metadata.gz: 2bf58678470f8cc840c19af0f64ce47ea55abe7124779d1b37ebe10707708d41
4
+ data.tar.gz: 02fb60b1183ab1d5eb90c8438a2271512db588438e75f1f89958e67325788360
5
5
  SHA512:
6
- metadata.gz: 284acd977a15b2d81a05d91b3dd2bfa3a0cfc9cad3e029cf2c10880020fbbbe23f6f7bfaf3b50d09b23ebc58e78cd65983ea156c7bc1cdd4bbd38337da6efcb6
7
- data.tar.gz: f9c133d678e635b1d14dbb46acbcf71538aa9fc7508cfa3d75d47ae21015fef61f097956f6f3e89998b805e2ea16d4777f7c36802fc59e82783645b80525506c
6
+ metadata.gz: e333d0836e79b87dfe89fbdc88c915506609a815b1bfce94b4432f40cdf87696c94da52294d3410d11faa48de6c2197194f8be1216cc50ccce2fc9871ab543fc
7
+ data.tar.gz: 14c429b220b1a1c890f164daf50dbda22b86d85659abd1c4a8980abd90785d6c52efc7840af85a75740f2dd928c45861d1244061eb703f5e1461b4cabbe8c2f9
data/.rubocop.yml CHANGED
@@ -1,9 +1,21 @@
1
+ require:
2
+ - rubocop-minitest
3
+ - rubocop-rake
4
+
1
5
  AllCops:
2
6
  NewCops: enable
3
7
  TargetRubyVersion: 3.1.2
4
8
 
5
- Style/StringLiterals:
6
- EnforcedStyle: double_quotes
9
+ Style/CaseEquality:
10
+ Enabled: false
7
11
 
8
12
  Style/Documentation:
9
13
  Enabled: false
14
+
15
+ Style/StringLiterals:
16
+ EnforcedStyle: double_quotes
17
+
18
+ Metrics/BlockLength:
19
+ Exclude:
20
+ - "test/**/*"
21
+ - "*.gemspec"
data/Gemfile.lock CHANGED
@@ -1,22 +1,31 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- strict (0.0.0)
4
+ strict (1.1.0)
5
5
  zeitwerk (~> 2.6)
6
6
 
7
7
  GEM
8
8
  remote: https://rubygems.org/
9
9
  specs:
10
10
  ast (2.4.2)
11
+ debug (1.6.1)
12
+ irb (>= 1.3.6)
13
+ reline (>= 0.3.1)
11
14
  gem-release (2.2.2)
15
+ io-console (0.5.11)
16
+ irb (1.4.1)
17
+ reline (>= 0.3.0)
12
18
  json (2.6.2)
13
19
  minitest (5.16.2)
20
+ minitest-spec-context (0.0.4)
14
21
  parallel (1.22.1)
15
22
  parser (3.1.2.1)
16
23
  ast (~> 2.4.1)
17
24
  rainbow (3.1.1)
18
25
  rake (13.0.6)
19
26
  regexp_parser (2.6.0)
27
+ reline (0.3.1)
28
+ io-console (~> 0.5)
20
29
  rexml (3.2.5)
21
30
  rubocop (1.36.0)
22
31
  json (~> 2.3)
@@ -30,19 +39,27 @@ GEM
30
39
  unicode-display_width (>= 1.4.0, < 3.0)
31
40
  rubocop-ast (1.21.0)
32
41
  parser (>= 3.1.1.0)
42
+ rubocop-minitest (0.22.2)
43
+ rubocop (>= 0.90, < 2.0)
44
+ rubocop-rake (0.6.0)
45
+ rubocop (~> 1.0)
33
46
  ruby-progressbar (1.11.0)
34
47
  unicode-display_width (2.3.0)
35
- zeitwerk (2.6.0)
48
+ zeitwerk (2.6.1)
36
49
 
37
50
  PLATFORMS
38
51
  arm64-darwin-21
39
52
  x86_64-linux
40
53
 
41
54
  DEPENDENCIES
55
+ debug (>= 1.0.0)
42
56
  gem-release (~> 2.2)
43
57
  minitest (~> 5.0)
58
+ minitest-spec-context (~> 0.0.4)
44
59
  rake (~> 13.0)
45
60
  rubocop (~> 1.21)
61
+ rubocop-minitest (~> 0.22)
62
+ rubocop-rake (~> 0.6)
46
63
  strict!
47
64
 
48
65
  BUNDLED WITH
data/README.md CHANGED
@@ -1,22 +1,177 @@
1
1
  # Strict
2
2
 
3
- Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/strict`. To experiment with that code, run `bin/console` for an interactive prompt.
4
-
5
- TODO: Delete this and the text above, and describe your gem
3
+ Strict provides a means to strictly validate instantiation of values, instantiation and attribute assignment of objects, and method calls at runtime.
6
4
 
7
5
  ## Installation
8
6
 
9
7
  Install the gem and add to the application's Gemfile by executing:
10
8
 
11
- $ bundle add strict
9
+ ```sh
10
+ $ bundle add strict
11
+ ```
12
12
 
13
13
  If bundler is not being used to manage dependencies, install the gem by executing:
14
14
 
15
- $ gem install strict
15
+ ```sh
16
+ $ gem install strict
17
+ ```
16
18
 
17
19
  ## Usage
18
20
 
19
- TODO: Write usage instructions here
21
+ ### `Strict::Value`
22
+
23
+ ```rb
24
+ class Money
25
+ include Strict::Value
26
+
27
+ attributes do
28
+ amount_in_cents Integer
29
+ currency AnyOf("USD", "CAD"), default: "USD"
30
+ end
31
+ end
32
+
33
+ Money.new(amount_in_cents: 100_00)
34
+ # => #<Money amount_in_cents=100_00 currency="USD">
35
+
36
+ Money.new(amount_in_cents: 100_00, currency: "CAD")
37
+ # => #<Money amount_in_cents=100_00 currency="CAD">
38
+
39
+ Money.new(amount_in_cents: 100.00)
40
+ # => Strict::InitializationError
41
+
42
+ Money.new(amount_in_cents: 100_00).with(amount_in_cents: 200_00)
43
+ # => #<Money amount_in_cents=200_00 currency="USD">
44
+
45
+ Money.new(amount_in_cents: 100_00).amount_in_cents = 50_00
46
+ # => NoMethodError
47
+
48
+ Money.new(amount_in_cents: 100_00) == Money.new(amount_in_cents: 100_00)
49
+ # => true
50
+ ```
51
+
52
+ ### `Strict::Object`
53
+
54
+ ```rb
55
+ class Stateful
56
+ include Strict::Object
57
+
58
+ attributes do
59
+ some_state String
60
+ dependency Anything(), default: nil
61
+ end
62
+ end
63
+
64
+ Stateful.new(some_state: "123")
65
+ # => #<Stateful some_state="123" dependency=nil>
66
+
67
+ Stateful.new(some_state: "123").with(some_state: "456")
68
+ # => NoMethodError
69
+
70
+ Stateful.new(some_state: "123").some_state = "456"
71
+ # => "456"
72
+ # => #<Stateful some_state="456" dependency=nil>
73
+
74
+ Stateful.new(some_state: "123").some_state = 456
75
+ # => Strict::AssignmentError
76
+
77
+ Stateful.new(some_state: "123") == Stateful.new(some_state: "123")
78
+ # => false
79
+ ```
80
+
81
+ ### `Strict::Method`
82
+
83
+ ```rb
84
+ class UpdateEmail
85
+ extend Strict::Method
86
+
87
+ sig do
88
+ user_id String, coerce: ->(value) { value.to_s }
89
+ email String
90
+ returns AnyOf(true, nil)
91
+ end
92
+ def call(user_id:, email:)
93
+ # contrived logic
94
+ user_id == email
95
+ end
96
+ end
97
+
98
+ UpdateEmail.new.call(user_id: 123, email: "123")
99
+ # => true
100
+
101
+ UpdateEmail.new.call(user_id: "123", email: "123")
102
+ # => true
103
+
104
+ UpdateEmail.new.call(user_id: "123", email: 123)
105
+ # => Strict::MethodCallError
106
+
107
+ UpdateEmail.new.call(user_id: "123", email: "456")
108
+ # => Strict::MethodReturnError
109
+ ```
110
+
111
+ ### `Strict::Interface`
112
+
113
+ ```rb
114
+ class Storage
115
+ extend Strict::Interface
116
+
117
+ expose(:write) do
118
+ key String
119
+ contents String
120
+ returns Boolean()
121
+ end
122
+
123
+ expose(:read) do
124
+ key String
125
+ returns AnyOf(String, nil)
126
+ end
127
+ end
128
+
129
+ module Storages
130
+ class Memory
131
+ def initialize
132
+ @storage = {}
133
+ end
134
+
135
+ def write(key:, contents:)
136
+ storage[key] = contents
137
+ true
138
+ end
139
+
140
+ def read(key:)
141
+ storage[key]
142
+ end
143
+
144
+ private
145
+
146
+ attr_reader :storage
147
+ end
148
+ end
149
+
150
+ storage = Storage.new(Storages::Memory.new)
151
+ # => #<Storage implementation=#<Storages::Memory>>
152
+
153
+ storage.write(key: "some/path/to/file.rb", contents: "Hello")
154
+ # => true
155
+
156
+ storage.write(key: "some/path/to/file.rb", contents: {})
157
+ # => Strict::MethodCallError
158
+
159
+ storage.read(key: "some/path/to/file.rb")
160
+ # => "Hello"
161
+
162
+ storage.read(key: "some/path/to/other.rb")
163
+ # => nil
164
+
165
+ module Storages
166
+ class Wat
167
+ def write(key:)
168
+ end
169
+ end
170
+ end
171
+
172
+ storage = Storage.new(Storages::Wat.new)
173
+ # => Strict::ImplementationDoesNotConformError
174
+ ```
20
175
 
21
176
  ## Development
22
177
 
@@ -35,3 +190,7 @@ The gem is available as open source under the terms of the [MIT License](https:/
35
190
  ## Code of Conduct
36
191
 
37
192
  Everyone interacting in the Strict project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/kylekthompson/strict/blob/main/CODE_OF_CONDUCT.md).
193
+
194
+ ## Credit
195
+
196
+ I can't thank [Tom Dalling](https://github.com/tomdalling) enough for his excellent [ValueSemantics](https://github.com/tomdalling/value_semantics) gem. Strict is heavily inspired and influenced by Tom's work and has some borrowed concepts and code.
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Strict
4
+ module Accessor
5
+ module Attributes
6
+ def attributes(&block)
7
+ block ||= -> {}
8
+ configuration = Strict::Attributes::Dsl.run(&block)
9
+ include Module.new(configuration)
10
+ include Strict::Attributes::Instance
11
+ extend Strict::Attributes::Class
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Strict
4
+ module Accessor
5
+ class Module < ::Module
6
+ attr_reader :configuration
7
+
8
+ # rubocop:disable Metrics/MethodLength
9
+ def initialize(configuration)
10
+ super()
11
+
12
+ @configuration = configuration
13
+ const_set(Strict::Attributes::Class::CONSTANT, configuration)
14
+ configuration.attributes.each do |attribute|
15
+ module_eval(
16
+ "def #{attribute.name} = #{attribute.instance_variable}", # def name = @instance_variable
17
+ __FILE__,
18
+ __LINE__ - 2
19
+ )
20
+
21
+ module_eval(<<~RUBY, __FILE__, __LINE__ + 1)
22
+ def #{attribute.name}=(value) # def name=(value)
23
+ attribute = self.class.strict_attributes.named!(:#{attribute.name}) # attribute = self.class.strict_attributes.named!(:name)
24
+ value = attribute.coerce(value, for_class: self.class) # value = attribute.coerce(value, for_class: self.class)
25
+ if attribute.valid?(value) # if attribute.valid?(value)
26
+ #{attribute.instance_variable} = value # @instance_variable = value
27
+ else # else
28
+ raise Strict::AssignmentError.new( # raise Strict::AssignmentError.new(
29
+ assignable_class: self.class, # assignable_class: self.class,
30
+ invalid_attribute: attribute, # invalid_attribute: attribute,
31
+ value: # value:
32
+ ) # )
33
+ end # end
34
+ end # end
35
+ RUBY
36
+ end
37
+ end
38
+ # rubocop:enable Metrics/MethodLength
39
+
40
+ def inspect
41
+ "#<#{self.class} (#{configuration.attributes.map(&:name).join(', ')})>"
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Strict
4
+ class AssignmentError < Error
5
+ attr_reader :invalid_attribute, :value
6
+
7
+ def initialize(assignable_class:, invalid_attribute:, value:)
8
+ super(message_from(assignable_class:, invalid_attribute:, value:))
9
+
10
+ @invalid_attribute = invalid_attribute
11
+ @value = value
12
+ end
13
+
14
+ private
15
+
16
+ def message_from(assignable_class:, invalid_attribute:, value:)
17
+ details = invalid_attribute_message_from(invalid_attribute, value)
18
+ "Assignment to #{invalid_attribute.name} of #{assignable_class} failed because:\n#{details}"
19
+ end
20
+
21
+ def invalid_attribute_message_from(invalid_attribute, value)
22
+ " - got #{value.inspect}, expected #{invalid_attribute.validator.inspect}"
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Strict
4
+ class Attribute
5
+ NOT_PROVIDED = ::Object.new.freeze
6
+
7
+ class << self
8
+ def make(name, validator = Validators::Anything.instance, coerce: false, **defaults)
9
+ unless valid_defaults?(**defaults)
10
+ raise ArgumentError, "Only one of 'default', 'default_value', or 'default_generator' can be provided"
11
+ end
12
+
13
+ new(name: name.to_sym, validator:, default_generator: make_default_generator(**defaults), coercer: coerce)
14
+ end
15
+
16
+ private
17
+
18
+ def valid_defaults?(default: NOT_PROVIDED, default_value: NOT_PROVIDED, default_generator: NOT_PROVIDED)
19
+ defaults_provided = [default, default_value, default_generator].count do |default_option|
20
+ !default_option.equal?(NOT_PROVIDED)
21
+ end
22
+
23
+ defaults_provided <= 1
24
+ end
25
+
26
+ def make_default_generator(default: NOT_PROVIDED, default_value: NOT_PROVIDED, default_generator: NOT_PROVIDED)
27
+ if !default.equal?(NOT_PROVIDED)
28
+ default.respond_to?(:call) ? default : -> { default }
29
+ elsif !default_value.equal?(NOT_PROVIDED)
30
+ -> { default_value }
31
+ elsif !default_generator.equal?(NOT_PROVIDED)
32
+ default_generator
33
+ else
34
+ NOT_PROVIDED
35
+ end
36
+ end
37
+ end
38
+
39
+ attr_reader :name, :validator, :default_generator, :coercer, :instance_variable
40
+
41
+ def initialize(name:, validator:, default_generator:, coercer:)
42
+ @name = name.to_sym
43
+ @validator = validator
44
+ @default_generator = default_generator
45
+ @coercer = coercer
46
+ @optional = !default_generator.equal?(NOT_PROVIDED)
47
+ @instance_variable = "@#{name.to_s.chomp('!').chomp('?')}"
48
+ end
49
+
50
+ def optional?
51
+ @optional
52
+ end
53
+
54
+ def valid?(value)
55
+ validator === value
56
+ end
57
+
58
+ def coerce(value, for_class:)
59
+ return value unless coercer
60
+
61
+ case coercer
62
+ when Symbol
63
+ for_class.public_send(coercer, value)
64
+ when true
65
+ for_class.public_send("coerce_#{name}", value)
66
+ else
67
+ coercer.call(value)
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Strict
4
+ module Attributes
5
+ module Class
6
+ CONSTANT = :STRICT_INTERNAL_ATTRIBUTES_CONFIGURATION__
7
+
8
+ def strict_attributes
9
+ self::STRICT_INTERNAL_ATTRIBUTES_CONFIGURATION__
10
+ end
11
+
12
+ def coercer
13
+ Coercer.new(self)
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Strict
4
+ module Attributes
5
+ class Coercer
6
+ attr_reader :attributes_class
7
+
8
+ def initialize(attributes_class)
9
+ @attributes_class = attributes_class
10
+ end
11
+
12
+ def call(value)
13
+ return value if value.nil? || !value.respond_to?(:to_h)
14
+
15
+ coerce(value.to_h)
16
+ end
17
+
18
+ private
19
+
20
+ NOT_PROVIDED = ::Object.new.freeze
21
+
22
+ def coerce(hash)
23
+ attributes_class.new(
24
+ **attributes_class.strict_attributes.each_with_object({}) do |attribute, attributes|
25
+ value = hash.fetch(attribute.name) { hash.fetch(attribute.name.to_s, NOT_PROVIDED) }
26
+ attributes[attribute.name] = value unless value.equal?(NOT_PROVIDED)
27
+ end
28
+ )
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "forwardable"
4
+
5
+ module Strict
6
+ module Attributes
7
+ class Configuration
8
+ include Enumerable
9
+ extend Forwardable
10
+
11
+ class UnknownAttributeError < Error
12
+ attr_reader :attribute_name
13
+
14
+ def initialize(attribute_name:)
15
+ super(message_from(attribute_name:))
16
+
17
+ @attribute_name = attribute_name
18
+ end
19
+
20
+ private
21
+
22
+ def message_from(attribute_name:)
23
+ "Strict tried to find an attribute named #{attribute_name} but was unable. " \
24
+ "It's likely this in an internal bug, feel free to open an issue at #{Strict::ISSUE_TRACKER} for help."
25
+ end
26
+ end
27
+
28
+ def_delegator :attributes, :each
29
+
30
+ attr_reader :attributes
31
+
32
+ def initialize(attributes:)
33
+ @attributes = attributes
34
+ @attributes_index = attributes.to_h { |a| [a.name, a] }
35
+ end
36
+
37
+ def named!(name)
38
+ attributes_index.fetch(name) { raise UnknownAttributeError.new(attribute_name: name) }
39
+ end
40
+
41
+ private
42
+
43
+ attr_reader :attributes_index
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Strict
4
+ module Attributes
5
+ class Dsl < BasicObject
6
+ class << self
7
+ def run(&)
8
+ dsl = new
9
+ dsl.instance_eval(&)
10
+ ::Strict::Attributes::Configuration.new(attributes: dsl.__strict_dsl_internal_attributes.values)
11
+ end
12
+ end
13
+
14
+ include ::Strict::Dsl::Coercible
15
+ include ::Strict::Dsl::Validatable
16
+
17
+ attr_reader :__strict_dsl_internal_attributes
18
+
19
+ def initialize
20
+ @__strict_dsl_internal_attributes = {}
21
+ end
22
+
23
+ def strict_attribute(*args, **kwargs)
24
+ attribute = ::Strict::Attribute.make(*args, **kwargs)
25
+ __strict_dsl_internal_attributes[attribute.name] = attribute
26
+ nil
27
+ end
28
+
29
+ def method_missing(name, *args, **kwargs)
30
+ if respond_to_missing?(name)
31
+ strict_attribute(name, *args, **kwargs)
32
+ else
33
+ super
34
+ end
35
+ end
36
+
37
+ def respond_to_missing?(method_name, _include_private = nil)
38
+ first_letter = method_name.to_s.each_char.first
39
+ first_letter.eql?(first_letter.downcase)
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Strict
4
+ module Attributes
5
+ module Instance
6
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
7
+ def initialize(**attributes)
8
+ remaining_attributes = Set.new(attributes.keys)
9
+ invalid_attributes = nil
10
+ missing_attributes = nil
11
+
12
+ self.class.strict_attributes.each do |attribute|
13
+ if remaining_attributes.delete?(attribute.name)
14
+ value = attributes.fetch(attribute.name)
15
+ elsif attribute.optional?
16
+ value = attribute.default_generator.call
17
+ else
18
+ missing_attributes ||= []
19
+ missing_attributes << attribute.name
20
+ next
21
+ end
22
+
23
+ value = attribute.coerce(value, for_class: self.class)
24
+ if attribute.valid?(value)
25
+ instance_variable_set(attribute.instance_variable, value)
26
+ else
27
+ invalid_attributes ||= {}
28
+ invalid_attributes[attribute] = value
29
+ end
30
+ end
31
+
32
+ return if remaining_attributes.none? && invalid_attributes.nil? && missing_attributes.nil?
33
+
34
+ raise InitializationError.new(
35
+ initializable_class: self.class,
36
+ remaining_attributes:,
37
+ invalid_attributes:,
38
+ missing_attributes:
39
+ )
40
+ end
41
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
42
+
43
+ def to_h
44
+ self.class.strict_attributes.to_h do |attribute|
45
+ [attribute.name, public_send(attribute.name)]
46
+ end
47
+ end
48
+
49
+ def inspect
50
+ if self.class.strict_attributes.any?
51
+ "#<#{self.class} #{to_h.map { |key, value| "#{key}=#{value.inspect}" }.join(' ')}>"
52
+ else
53
+ "#<#{self.class}>"
54
+ end
55
+ end
56
+
57
+ def pretty_print(pp)
58
+ pp.object_group(self) do
59
+ to_h.each do |key, value|
60
+ pp.breakable
61
+ pp.text("#{key}=")
62
+ pp.pp(value)
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Strict
4
+ module Coercers
5
+ class Array
6
+ attr_reader :element_coercer
7
+
8
+ def initialize(element_coercer = nil)
9
+ @element_coercer = element_coercer
10
+ end
11
+
12
+ def call(value)
13
+ return value if value.nil? || !value.respond_to?(:to_a)
14
+
15
+ array = value.to_a
16
+ return array unless element_coercer
17
+
18
+ array.map { |element| element_coercer.call(element) }
19
+ end
20
+ end
21
+ end
22
+ end