strict 1.4.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.
- checksums.yaml +4 -4
- data/.agents/resume +4 -0
- data/.agents/setup +105 -0
- data/.rspec +2 -0
- data/.rubocop.yml +14 -4
- data/.tool-versions +1 -1
- data/API.md +335 -0
- data/CHANGELOG.md +110 -1
- data/Gemfile +13 -0
- data/Gemfile.lock +118 -42
- data/README.md +278 -4
- data/Rakefile +13 -7
- data/benchmark/baseline.rb +204 -0
- data/lib/strict/assignment_error.rb +5 -2
- data/lib/strict/attribute.rb +8 -47
- data/lib/strict/attributes/class.rb +0 -6
- data/lib/strict/attributes/coercer.rb +4 -3
- data/lib/strict/attributes/configuration.rb +15 -0
- data/lib/strict/attributes/dsl.rb +18 -10
- data/lib/strict/attributes/generated_methods.rb +113 -0
- data/lib/strict/attributes/instance.rb +32 -15
- data/lib/strict/configuration.rb +44 -0
- data/lib/strict/declaration.rb +109 -0
- data/lib/strict/detailed_validator.rb +9 -0
- data/lib/strict/error.rb +8 -1
- data/lib/strict/initialization_error.rb +11 -2
- data/lib/strict/interface.rb +39 -21
- data/lib/strict/interfaces/conformance.rb +125 -0
- data/lib/strict/interfaces/instance.rb +1 -49
- data/lib/strict/method.rb +72 -53
- data/lib/strict/method_call_error.rb +12 -2
- data/lib/strict/method_return_error.rb +2 -2
- data/lib/strict/methods/dsl.rb +16 -8
- data/lib/strict/methods/module.rb +27 -8
- data/lib/strict/methods/verifiable_method.rb +234 -70
- data/lib/strict/object.rb +1 -1
- data/lib/strict/parameter.rb +3 -52
- data/lib/strict/return.rb +14 -10
- data/lib/strict/rspec.rb +159 -0
- data/lib/strict/union.rb +180 -0
- data/lib/strict/unions/coercer.rb +55 -0
- data/lib/strict/validation.rb +40 -0
- data/lib/strict/validators/all_of.rb +11 -3
- data/lib/strict/validators/array_of.rb +18 -3
- data/lib/strict/validators/hash_of.rb +23 -3
- data/lib/strict/value.rb +18 -4
- data/lib/strict/version.rb +1 -1
- data/lib/strict/violation.rb +9 -0
- data/lib/strict.rb +42 -0
- data/sig/strict/rspec.rbs +10 -0
- data/sig/strict.rbs +213 -1
- data/strict.gemspec +1 -9
- metadata +20 -120
- data/lib/strict/accessor/attributes.rb +0 -15
- data/lib/strict/accessor/module.rb +0 -45
- data/lib/strict/reader/attributes.rb +0 -15
- data/lib/strict/reader/module.rb +0 -27
data/lib/strict/rspec.rb
ADDED
|
@@ -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
|
data/lib/strict/union.rb
ADDED
|
@@ -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
|
|
13
|
-
|
|
14
|
-
|
|
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
|
|
13
|
-
|
|
14
|
-
|
|
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
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
6
|
+
Attributes::GeneratedMethods.install_on(mod, writable: false)
|
|
7
7
|
end
|
|
8
8
|
|
|
9
9
|
def with(**attributes)
|
|
10
|
-
self.class.new(**
|
|
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)
|
|
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
|
-
|
|
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
|
data/lib/strict/version.rb
CHANGED
data/lib/strict.rb
CHANGED
|
@@ -2,8 +2,50 @@
|
|
|
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
|
|
8
9
|
ISSUE_TRACKER = "https://github.com/kylekthompson/strict/issues"
|
|
10
|
+
|
|
11
|
+
class << self
|
|
12
|
+
def configuration
|
|
13
|
+
execution_context_override || global_configuration
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def configure
|
|
17
|
+
raise Strict::Error, "cannot reconfigure overridden configuration" if overridden?
|
|
18
|
+
|
|
19
|
+
yield(configuration)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def with_overrides(**overrides)
|
|
23
|
+
original_override = execution_context_override
|
|
24
|
+
|
|
25
|
+
begin
|
|
26
|
+
self.execution_context_override = Strict::Configuration.new(**configuration.to_h, **overrides)
|
|
27
|
+
yield
|
|
28
|
+
ensure
|
|
29
|
+
self.execution_context_override = original_override
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def overridden?
|
|
36
|
+
!!execution_context_override
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def execution_context_override
|
|
40
|
+
Thread.current[:__strict_configuration_override]
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def execution_context_override=(configuration)
|
|
44
|
+
Thread.current[:__strict_configuration_override] = configuration
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def global_configuration
|
|
48
|
+
@global_configuration ||= Strict::Configuration.new
|
|
49
|
+
end
|
|
50
|
+
end
|
|
9
51
|
end
|
|
@@ -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
|