strict 1.5.0 → 2.0.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 (56) hide show
  1. checksums.yaml +4 -4
  2. data/.agents/resume +4 -0
  3. data/.agents/setup +105 -0
  4. data/.rspec +2 -0
  5. data/.rubocop.yml +14 -4
  6. data/.tool-versions +1 -1
  7. data/API.md +335 -0
  8. data/CHANGELOG.md +110 -1
  9. data/Gemfile +13 -0
  10. data/Gemfile.lock +117 -41
  11. data/README.md +232 -6
  12. data/Rakefile +13 -7
  13. data/benchmark/baseline.rb +204 -0
  14. data/lib/strict/assignment_error.rb +5 -2
  15. data/lib/strict/attribute.rb +8 -49
  16. data/lib/strict/attributes/class.rb +0 -6
  17. data/lib/strict/attributes/coercer.rb +4 -3
  18. data/lib/strict/attributes/configuration.rb +15 -0
  19. data/lib/strict/attributes/dsl.rb +18 -10
  20. data/lib/strict/attributes/generated_methods.rb +113 -0
  21. data/lib/strict/attributes/instance.rb +32 -15
  22. data/lib/strict/declaration.rb +109 -0
  23. data/lib/strict/detailed_validator.rb +9 -0
  24. data/lib/strict/error.rb +8 -1
  25. data/lib/strict/initialization_error.rb +11 -2
  26. data/lib/strict/interface.rb +39 -21
  27. data/lib/strict/interfaces/conformance.rb +125 -0
  28. data/lib/strict/interfaces/instance.rb +1 -49
  29. data/lib/strict/method.rb +72 -53
  30. data/lib/strict/method_call_error.rb +12 -2
  31. data/lib/strict/method_return_error.rb +2 -2
  32. data/lib/strict/methods/dsl.rb +16 -8
  33. data/lib/strict/methods/module.rb +27 -8
  34. data/lib/strict/methods/verifiable_method.rb +234 -70
  35. data/lib/strict/object.rb +1 -1
  36. data/lib/strict/parameter.rb +3 -54
  37. data/lib/strict/return.rb +14 -12
  38. data/lib/strict/rspec.rb +159 -0
  39. data/lib/strict/union.rb +180 -0
  40. data/lib/strict/unions/coercer.rb +55 -0
  41. data/lib/strict/validation.rb +40 -0
  42. data/lib/strict/validators/all_of.rb +11 -3
  43. data/lib/strict/validators/array_of.rb +18 -3
  44. data/lib/strict/validators/hash_of.rb +23 -3
  45. data/lib/strict/value.rb +18 -4
  46. data/lib/strict/version.rb +1 -1
  47. data/lib/strict/violation.rb +9 -0
  48. data/lib/strict.rb +10 -9
  49. data/sig/strict/rspec.rbs +10 -0
  50. data/sig/strict.rbs +213 -1
  51. data/strict.gemspec +1 -9
  52. metadata +19 -120
  53. data/lib/strict/accessor/attributes.rb +0 -15
  54. data/lib/strict/accessor/module.rb +0 -45
  55. data/lib/strict/reader/attributes.rb +0 -15
  56. data/lib/strict/reader/module.rb +0 -27
@@ -0,0 +1,159 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "strict"
4
+ require "rspec/core"
5
+ require "rspec/expectations"
6
+ require "rspec/mocks"
7
+ require "rspec/support/fuzzy_matcher"
8
+
9
+ module Strict
10
+ module RSpec
11
+ module Support
12
+ NO_VIOLATIONS = [].freeze
13
+ INSTANCE_VARIABLE_GET = ::Object.instance_method(:instance_variable_get)
14
+ private_constant :NO_VIOLATIONS
15
+ private_constant :INSTANCE_VARIABLE_GET
16
+
17
+ class << self
18
+ def violations(validator, value)
19
+ return NO_VIOLATIONS if test_value?(validator, value)
20
+ return validator.violations(value) if Strict::DetailedValidator === validator
21
+ return NO_VIOLATIONS if validator === value
22
+
23
+ [Strict::Violation.new(path: [], code: :invalid, value: value, validator: validator)]
24
+ end
25
+
26
+ def test_value?(validator, value)
27
+ matcher?(value) || instance_double_valid_for?(validator, value)
28
+ end
29
+
30
+ def no_violations
31
+ NO_VIOLATIONS
32
+ end
33
+
34
+ def stubs_for(strict_class, stubs)
35
+ return stubs unless strict_class.respond_to?(:verify_implementation!)
36
+
37
+ strict_class.strict_instance_methods.to_h { |name, _method| [name, nil] }.merge(stubs)
38
+ end
39
+
40
+ def violation_details(violations)
41
+ descriptions = violations.map { |violation| " - #{violation_description(violation)}" }
42
+ "violations:\n#{descriptions.join("\n")}"
43
+ end
44
+
45
+ private
46
+
47
+ def matcher?(value)
48
+ ::RSpec::Support.is_a_matcher?(value)
49
+ end
50
+
51
+ def instance_double_valid_for?(validator, value)
52
+ return false unless ::RSpec::Mocks::InstanceVerifyingDouble === value
53
+ return false unless ::Module === validator
54
+
55
+ doubled_module = INSTANCE_VARIABLE_GET.bind_call(value, :@doubled_module).target
56
+ doubled_module && !!(doubled_module <= validator)
57
+ end
58
+
59
+ def violation_description(violation)
60
+ location = violation.path.empty? ? "root" : violation.path.inspect
61
+
62
+ case violation.code
63
+ when :invalid
64
+ "at #{location}: expected #{format(violation.validator)}, got #{format(violation.value)}"
65
+ when :missing
66
+ "at #{location}: expected #{format(violation.validator)}, but the value was missing"
67
+ when :unexpected
68
+ "at #{location}: got unexpected #{format(violation.value)}"
69
+ end
70
+ end
71
+
72
+ def format(value)
73
+ ::RSpec::Support::ObjectFormatter.format(value)
74
+ end
75
+ end
76
+ end
77
+
78
+ module AcceptsMatchersAndDoubles
79
+ def violations(value, configuration = Strict.configuration)
80
+ return Support.no_violations if Support.test_value?(validator, value)
81
+
82
+ super
83
+ end
84
+ end
85
+
86
+ module AcceptsNestedMatchersAndDoubles
87
+ def violations(validator, value)
88
+ return Support.no_violations if Support.test_value?(validator, value)
89
+
90
+ super
91
+ end
92
+ end
93
+
94
+ module ComposableValue
95
+ def ===(other)
96
+ return false unless other.instance_of?(self.class)
97
+
98
+ self.class.strict_attributes.all? do |attribute|
99
+ expected = public_send(attribute.name)
100
+ actual = other.public_send(attribute.name)
101
+ ::RSpec::Support::FuzzyMatcher.values_match?(expected, actual)
102
+ end
103
+ end
104
+ end
105
+
106
+ module ExampleMethods
107
+ def strict_double(strict_class, stubs = {})
108
+ instance_double(strict_class, Support.stubs_for(strict_class, stubs))
109
+ end
110
+ end
111
+
112
+ Strict::Declaration.prepend(AcceptsMatchersAndDoubles)
113
+ Strict::Return.prepend(AcceptsMatchersAndDoubles)
114
+ Strict::Validation.singleton_class.prepend(AcceptsNestedMatchersAndDoubles)
115
+ Strict::Value.prepend(ComposableValue)
116
+
117
+ ::RSpec.configure do |config|
118
+ config.include ExampleMethods
119
+ end
120
+
121
+ ::RSpec::Matchers.define :conform_to do |interface|
122
+ match do |implementation|
123
+ @conformance_error = nil
124
+ interface.verify_implementation!(implementation)
125
+ true
126
+ rescue Strict::ImplementationDoesNotConformError => e
127
+ @conformance_error = e
128
+ false
129
+ end
130
+
131
+ failure_message do |implementation|
132
+ "expected #{implementation.inspect} to conform to #{interface.inspect}\n\n#{@conformance_error.message}"
133
+ end
134
+
135
+ failure_message_when_negated do |implementation|
136
+ "expected #{implementation.inspect} not to conform to #{interface.inspect}"
137
+ end
138
+ end
139
+
140
+ ::RSpec::Matchers.define :validate do |value|
141
+ match do |validator|
142
+ @violations = Support.violations(validator, value)
143
+ @violations.empty?
144
+ end
145
+
146
+ failure_message do |validator|
147
+ message = "expected #{validator.inspect} to validate #{value.inspect}"
148
+ "#{message}\n\n#{Support.violation_details(@violations)}"
149
+ end
150
+
151
+ failure_message_when_negated do |validator|
152
+ "expected #{validator.inspect} not to validate #{value.inspect}"
153
+ end
154
+ end
155
+
156
+ private_constant :Support, :AcceptsMatchersAndDoubles, :AcceptsNestedMatchersAndDoubles, :ComposableValue,
157
+ :ExampleMethods
158
+ end
159
+ end
@@ -0,0 +1,180 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Strict
4
+ module Union
5
+ VARIANT_NAME = /\A[a-z][a-z0-9]*(?:_[a-z0-9]+)*\z/
6
+ private_constant :VARIANT_NAME
7
+ Variant = Data.define(:tag, :variant_class)
8
+ private_constant :Variant
9
+
10
+ def self.included(union_class)
11
+ union_class.extend(ClassMethods)
12
+ union_class.include(Instance)
13
+ union_class.instance_variable_set(:@strict_union_discriminator, nil)
14
+ union_class.instance_variable_set(:@strict_union_attributes, nil)
15
+ union_class.instance_variable_set(:@strict_union_variant_names, {})
16
+ union_class.instance_variable_set(:@strict_union_variants, {})
17
+ end
18
+
19
+ module ClassMethods # rubocop:disable Metrics/ModuleLength
20
+ def discriminator(name)
21
+ raise ArgumentError, "discriminator already declared for #{self}" if @strict_union_discriminator
22
+
23
+ discriminator = name.to_sym
24
+ strict_union_validate_attributes!(@strict_union_attributes, discriminator)
25
+ @strict_union_discriminator = discriminator
26
+ nil
27
+ end
28
+
29
+ def attributes(&definition)
30
+ raise ArgumentError, "attributes already declared for #{self}" if @strict_union_attributes
31
+ raise ArgumentError, "attributes must be declared before variants for #{self}" if @strict_union_variants.any?
32
+
33
+ definition ||= -> {}
34
+ attributes = Attributes::Dsl.run(&definition)
35
+ strict_union_validate_attributes!(attributes, @strict_union_discriminator)
36
+ @strict_union_attributes = attributes
37
+ nil
38
+ end
39
+
40
+ def variant(name, tag: name.to_sym, &definition)
41
+ discriminator = strict_union_discriminator!
42
+ name = name.to_sym
43
+ tag_key = strict_union_tag_key(tag)
44
+ constant_name = strict_union_validate_variant!(name, tag, tag_key)
45
+ variant_class = strict_union_build_variant(discriminator, name, tag, definition)
46
+ strict_union_register_variant(name, tag, tag_key, constant_name, variant_class)
47
+ end
48
+
49
+ def coercer
50
+ discriminator = @strict_union_discriminator
51
+ raise ArgumentError, "declare a discriminator before using the coercer for #{self}" unless discriminator
52
+
53
+ Unions::Coercer.new(self, discriminator:, variants: @strict_union_variants)
54
+ end
55
+
56
+ def ===(value)
57
+ @strict_union_variant_names&.value?(value.class) || false
58
+ end
59
+
60
+ private
61
+
62
+ def strict_union_validate_variant!(name, tag, tag_key)
63
+ constant_name = strict_union_constant_name(name)
64
+ raise ArgumentError, "variant #{name.inspect} already declared for #{self}" if
65
+ @strict_union_variant_names.key?(name)
66
+ raise ArgumentError, "tag #{tag.inspect} already declared for #{self}" if @strict_union_variants.key?(tag_key)
67
+ raise ArgumentError, "constant #{constant_name} is already defined for #{self}" if
68
+ const_defined?(constant_name, false)
69
+
70
+ constant_name
71
+ end
72
+
73
+ def strict_union_register_variant(name, tag, tag_key, constant_name, variant_class)
74
+ const_set(constant_name, variant_class)
75
+ @strict_union_variant_names[name] = variant_class
76
+ @strict_union_variants[tag_key] = Variant.new(tag:, variant_class:)
77
+ variant_class
78
+ end
79
+
80
+ def strict_union_discriminator!
81
+ @strict_union_discriminator ||
82
+ raise(ArgumentError, "declare a discriminator before variants for #{self}")
83
+ end
84
+
85
+ def strict_union_build_variant(discriminator, name, tag, definition)
86
+ variant_class = Class.new(self)
87
+ attribute_definition = strict_union_evaluate_variant(variant_class, name, definition)
88
+ variant_class.include(Strict::Value)
89
+ strict_union_define_variant_attributes(variant_class, discriminator, tag, attribute_definition)
90
+ variant_class.define_singleton_method(:===, ::Module.instance_method(:===))
91
+ variant_class
92
+ end
93
+
94
+ def strict_union_evaluate_variant(variant_class, tag, definition)
95
+ return unless definition
96
+
97
+ attribute_definition = nil
98
+ variant_class.define_singleton_method(:attributes) do |&block|
99
+ raise ArgumentError, "attributes already declared for variant #{tag.inspect}" if attribute_definition
100
+
101
+ attribute_definition = block || -> {}
102
+ nil
103
+ end
104
+ variant_class.class_exec(&definition)
105
+ variant_class.singleton_class.remove_method(:attributes)
106
+ attribute_definition
107
+ end
108
+
109
+ # rubocop:disable Metrics/MethodLength
110
+ def strict_union_define_variant_attributes(variant_class, discriminator, tag, attribute_definition)
111
+ discriminator_coercer = strict_union_discriminator_coercer(discriminator, tag)
112
+ shared_attributes = @strict_union_attributes
113
+ variant_class.attributes do
114
+ strict_attribute discriminator, tag, default_value: tag, coerce: discriminator_coercer
115
+ discriminator_attribute = __strict_dsl_internal_attributes.fetch(discriminator)
116
+ shared_attributes&.each do |attribute|
117
+ __strict_dsl_internal_attributes[attribute.name] = attribute
118
+ end
119
+ instance_exec(&attribute_definition) if attribute_definition
120
+ unless __strict_dsl_internal_attributes.fetch(discriminator).equal?(discriminator_attribute)
121
+ ::Kernel.raise ::ArgumentError, "variants cannot redeclare discriminator #{discriminator.inspect}"
122
+ end
123
+ end
124
+ end
125
+ # rubocop:enable Metrics/MethodLength
126
+
127
+ def strict_union_validate_attributes!(attributes, discriminator)
128
+ return unless discriminator && attributes
129
+
130
+ storage = Attribute.instance_variable_for(discriminator)
131
+ conflicting_attribute = attributes.find { |attribute| attribute.instance_variable.eql?(storage) }
132
+ return unless conflicting_attribute
133
+
134
+ if conflicting_attribute.name.eql?(discriminator)
135
+ raise ArgumentError, "union attributes cannot redeclare discriminator #{discriminator.inspect}"
136
+ end
137
+
138
+ raise ArgumentError,
139
+ "union attributes cannot conflict with discriminator #{discriminator.inspect}: " \
140
+ "#{conflicting_attribute.name.inspect} uses #{storage}"
141
+ end
142
+
143
+ def strict_union_discriminator_coercer(discriminator, tag)
144
+ tag_key = strict_union_tag_key(tag)
145
+ lambda do |value|
146
+ unless strict_union_tag_key(value).eql?(tag_key)
147
+ raise ArgumentError, "discriminator #{discriminator.inspect} must equal #{tag.inspect}"
148
+ end
149
+
150
+ tag
151
+ end
152
+ end
153
+
154
+ def strict_union_tag_key(tag)
155
+ unless tag.is_a?(::String) || tag.is_a?(::Symbol)
156
+ raise ArgumentError, "variant tag must be a String or Symbol, got #{tag.inspect}"
157
+ end
158
+
159
+ tag.is_a?(::String) ? tag.to_sym : tag
160
+ end
161
+
162
+ def strict_union_constant_name(tag)
163
+ name = tag.to_s
164
+ unless VARIANT_NAME.match?(name)
165
+ raise ArgumentError, "variant name must be lower snake_case, got #{name.inspect}"
166
+ end
167
+
168
+ name.split("_").map { |part| part.sub(/\A./, &:upcase) }.join
169
+ end
170
+ end # rubocop:enable Metrics/ModuleLength
171
+
172
+ module Instance
173
+ def initialize(...)
174
+ raise TypeError, "cannot instantiate union #{self.class} directly; instantiate one of its variants"
175
+ end
176
+ end
177
+
178
+ private_constant :ClassMethods, :Instance
179
+ end
180
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Strict
4
+ module Unions
5
+ class Coercer
6
+ NOT_PROVIDED = ::Object.new.freeze
7
+ private_constant :NOT_PROVIDED
8
+
9
+ def initialize(union_class, discriminator:, variants:)
10
+ @union_class = union_class
11
+ @discriminator = discriminator
12
+ @variants = variants
13
+ end
14
+
15
+ def call(value)
16
+ return value if value.nil? || union_class === value
17
+ return value unless value.respond_to?(:to_h)
18
+
19
+ coerce(value.to_h)
20
+ end
21
+
22
+ private
23
+
24
+ attr_reader :union_class, :discriminator, :variants
25
+
26
+ def coerce(hash)
27
+ tag = tag_from(hash)
28
+ variant = variant_for(normalized_tag(tag), original_tag: tag)
29
+ variant.variant_class.coercer.call(hash.merge(discriminator => variant.tag))
30
+ end
31
+
32
+ def tag_from(hash)
33
+ tag = hash.fetch(discriminator) { hash.fetch(discriminator.to_s, NOT_PROVIDED) }
34
+ if tag.equal?(NOT_PROVIDED)
35
+ raise ArgumentError, "missing discriminator #{discriminator.inspect} for #{union_class}"
36
+ end
37
+
38
+ tag
39
+ end
40
+
41
+ def variant_for(tag, original_tag:)
42
+ variants.fetch(tag)
43
+ rescue KeyError
44
+ expected = variants.values.map { |variant| variant.tag.inspect }.join(", ")
45
+ raise ArgumentError,
46
+ "unknown discriminator #{discriminator.inspect} for #{union_class}: #{original_tag.inspect}; " \
47
+ "expected one of #{expected}"
48
+ end
49
+
50
+ def normalized_tag(tag)
51
+ tag.is_a?(String) ? tag.to_sym : tag
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Strict
4
+ module Validation
5
+ NONE = [].freeze
6
+ ROOT_PATH = [].freeze
7
+
8
+ module_function
9
+
10
+ def violations(validator, value)
11
+ return validator.violations(value) if DetailedValidator === validator
12
+ return NONE if validator === value
13
+
14
+ invalid(validator, value)
15
+ end
16
+
17
+ def invalid(validator, value)
18
+ [Violation.new(path: ROOT_PATH, code: :invalid, value: value, validator: validator)]
19
+ end
20
+
21
+ def missing(validator, path:)
22
+ Violation.new(path: path, code: :missing, value: nil, validator: validator)
23
+ end
24
+
25
+ def unexpected(value, path:)
26
+ Violation.new(path: path, code: :unexpected, value: value, validator: nil)
27
+ end
28
+
29
+ def prepend_path(violations, *segments)
30
+ violations.map do |violation|
31
+ Violation.new(
32
+ path: segments + violation.path,
33
+ code: violation.code,
34
+ value: violation.value,
35
+ validator: violation.validator
36
+ )
37
+ end
38
+ end
39
+ end
40
+ end
@@ -3,16 +3,24 @@
3
3
  module Strict
4
4
  module Validators
5
5
  class AllOf
6
+ include DetailedValidator
7
+
6
8
  attr_reader :subvalidators
7
9
 
8
10
  def initialize(*subvalidators)
9
11
  @subvalidators = subvalidators
10
12
  end
11
13
 
12
- def ===(value)
13
- subvalidators.all? do |subvalidator|
14
- subvalidator === value
14
+ def violations(value)
15
+ violations = nil
16
+ subvalidators.each do |subvalidator|
17
+ subvalidator_violations = Validation.violations(subvalidator, value)
18
+ next if subvalidator_violations.empty?
19
+
20
+ violations ||= []
21
+ violations.concat(subvalidator_violations)
15
22
  end
23
+ violations || Validation::NONE
16
24
  end
17
25
 
18
26
  def inspect
@@ -3,16 +3,31 @@
3
3
  module Strict
4
4
  module Validators
5
5
  class ArrayOf
6
+ include DetailedValidator
7
+
6
8
  attr_reader :element_validator
7
9
 
8
10
  def initialize(element_validator)
9
11
  @element_validator = element_validator
10
12
  end
11
13
 
12
- def ===(value)
13
- Array === value && value.all? do |v|
14
- element_validator === v
14
+ def coercer
15
+ element_coercer = element_validator.coercer if element_validator.respond_to?(:coercer)
16
+ Coercers::Array.new(element_coercer)
17
+ end
18
+
19
+ def violations(value)
20
+ return Validation.invalid(self, value) unless Array === value
21
+
22
+ violations = nil
23
+ value.each_with_index do |element, index|
24
+ element_violations = Validation.violations(element_validator, element)
25
+ next if element_violations.empty?
26
+
27
+ violations ||= []
28
+ violations.concat(Validation.prepend_path(element_violations, index))
15
29
  end
30
+ violations || Validation::NONE
16
31
  end
17
32
 
18
33
  def inspect
@@ -3,6 +3,8 @@
3
3
  module Strict
4
4
  module Validators
5
5
  class HashOf
6
+ include DetailedValidator
7
+
6
8
  attr_reader :key_validator, :value_validator
7
9
 
8
10
  def initialize(key_validator, value_validator)
@@ -10,11 +12,29 @@ module Strict
10
12
  @value_validator = value_validator
11
13
  end
12
14
 
13
- def ===(value)
14
- Hash === value && value.all? do |k, v|
15
- key_validator === k && value_validator === v
15
+ def coercer
16
+ key_coercer = key_validator.coercer if key_validator.respond_to?(:coercer)
17
+ value_coercer = value_validator.coercer if value_validator.respond_to?(:coercer)
18
+ Coercers::Hash.new(key_coercer, value_coercer)
19
+ end
20
+
21
+ # rubocop:disable Metrics/MethodLength
22
+ def violations(value)
23
+ return Validation.invalid(self, value) unless Hash === value
24
+
25
+ violations = nil
26
+ value.each do |key, entry_value|
27
+ key_violations = Validation.violations(key_validator, key)
28
+ value_violations = Validation.violations(value_validator, entry_value)
29
+ next if key_violations.empty? && value_violations.empty?
30
+
31
+ violations ||= []
32
+ violations.concat(Validation.prepend_path(key_violations, key))
33
+ violations.concat(Validation.prepend_path(value_violations, key))
16
34
  end
35
+ violations || Validation::NONE
17
36
  end
37
+ # rubocop:enable Metrics/MethodLength
18
38
 
19
39
  def inspect
20
40
  "HashOf(#{key_validator.inspect} => #{value_validator.inspect})"
data/lib/strict/value.rb CHANGED
@@ -3,20 +3,34 @@
3
3
  module Strict
4
4
  module Value
5
5
  def self.included(mod)
6
- mod.extend(Reader::Attributes)
6
+ Attributes::GeneratedMethods.install_on(mod, writable: false)
7
7
  end
8
8
 
9
9
  def with(**attributes)
10
- self.class.new(**to_h.merge(attributes))
10
+ self.class.new(**Attributes::Instance.attribute_values(self), **attributes)
11
+ end
12
+
13
+ def deconstruct_keys(keys)
14
+ attributes = Attributes::Instance.attribute_values(self)
15
+ keys ? attributes.slice(*keys) : attributes
11
16
  end
12
17
 
13
18
  def eql?(other)
14
- self.class.equal?(other.class) && to_h.eql?(other.to_h)
19
+ return false unless self.class.equal?(other.class)
20
+
21
+ self.class.strict_attributes.all? do |attribute|
22
+ public_send(attribute.name).eql?(other.public_send(attribute.name))
23
+ end
15
24
  end
16
25
  alias == eql?
17
26
 
18
27
  def hash
19
- [self.class, to_h].hash
28
+ value_class = self.class
29
+ components = [value_class]
30
+ value_class.strict_attributes.each do |attribute|
31
+ components << public_send(attribute.name)
32
+ end
33
+ components.hash
20
34
  end
21
35
  end
22
36
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Strict
4
- VERSION = "1.5.0"
4
+ VERSION = "2.0.0"
5
5
  end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Strict
4
+ Violation = Data.define(:path, :code, :value, :validator) do
5
+ def initialize(path:, code:, value:, validator:)
6
+ super(path: path.dup.freeze, code: code, value: value, validator: validator)
7
+ end
8
+ end
9
+ end
data/lib/strict.rb CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "zeitwerk"
4
4
  loader = Zeitwerk::Loader.for_gem
5
+ loader.ignore("#{__dir__}/strict/rspec.rb")
5
6
  loader.setup
6
7
 
7
8
  module Strict
@@ -9,7 +10,7 @@ module Strict
9
10
 
10
11
  class << self
11
12
  def configuration
12
- thread_configuration || global_configuration
13
+ execution_context_override || global_configuration
13
14
  end
14
15
 
15
16
  def configure
@@ -19,28 +20,28 @@ module Strict
19
20
  end
20
21
 
21
22
  def with_overrides(**overrides)
22
- original_thread_configuration = thread_configuration
23
+ original_override = execution_context_override
23
24
 
24
25
  begin
25
- self.thread_configuration = Strict::Configuration.new(**configuration.to_h.merge(overrides))
26
+ self.execution_context_override = Strict::Configuration.new(**configuration.to_h, **overrides)
26
27
  yield
27
28
  ensure
28
- self.thread_configuration = original_thread_configuration
29
+ self.execution_context_override = original_override
29
30
  end
30
31
  end
31
32
 
32
33
  private
33
34
 
34
35
  def overridden?
35
- !!thread_configuration
36
+ !!execution_context_override
36
37
  end
37
38
 
38
- def thread_configuration
39
- Thread.current[:configuration]
39
+ def execution_context_override
40
+ Thread.current[:__strict_configuration_override]
40
41
  end
41
42
 
42
- def thread_configuration=(configuration)
43
- Thread.current[:configuration] = configuration
43
+ def execution_context_override=(configuration)
44
+ Thread.current[:__strict_configuration_override] = configuration
44
45
  end
45
46
 
46
47
  def global_configuration
@@ -0,0 +1,10 @@
1
+ module Strict
2
+ # Extend this type-only interface in application test signatures for RSpec
3
+ # example contexts that load strict/rspec.
4
+ interface _RSpecExampleMethods
5
+ def strict_double: (Module strict_class, ?Hash[Symbol, untyped] stubs) -> untyped
6
+ end
7
+
8
+ module RSpec
9
+ end
10
+ end