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
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Strict
|
|
4
|
+
class Declaration
|
|
5
|
+
NOT_PROVIDED = ::Object.new.freeze
|
|
6
|
+
DECLARATION_NAME = /\A[a-z_][a-zA-Z0-9_]*[!?]?\z/
|
|
7
|
+
private_constant :DECLARATION_NAME
|
|
8
|
+
|
|
9
|
+
class << self
|
|
10
|
+
def make(
|
|
11
|
+
name,
|
|
12
|
+
validator = Validators::Anything.instance,
|
|
13
|
+
coerce: validator.respond_to?(:coercer) ? validator.coercer : false,
|
|
14
|
+
**defaults
|
|
15
|
+
)
|
|
16
|
+
validate_name!(name)
|
|
17
|
+
validate_defaults!(**defaults)
|
|
18
|
+
validate_coercer!(coerce)
|
|
19
|
+
|
|
20
|
+
new(
|
|
21
|
+
name: name.to_sym,
|
|
22
|
+
validator: validator,
|
|
23
|
+
default_generator: make_default_generator(**defaults),
|
|
24
|
+
coercer: coerce
|
|
25
|
+
)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def validate_name!(name)
|
|
31
|
+
supported_type = name.is_a?(::String) || name.is_a?(::Symbol)
|
|
32
|
+
return if supported_type && DECLARATION_NAME.match?(name.to_s)
|
|
33
|
+
|
|
34
|
+
raise ArgumentError, "Declaration name must be a supported String or Symbol, got #{name.inspect}"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def validate_defaults!(**defaults)
|
|
38
|
+
unless valid_defaults?(**defaults)
|
|
39
|
+
raise ArgumentError, "Only one of 'default', 'default_value', or 'default_generator' can be provided"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
validate_default_generator!(**defaults)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def valid_defaults?(default: NOT_PROVIDED, default_value: NOT_PROVIDED, default_generator: NOT_PROVIDED)
|
|
46
|
+
defaults_provided = [default, default_value, default_generator].count do |default_option|
|
|
47
|
+
!default_option.equal?(NOT_PROVIDED)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
defaults_provided <= 1
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def validate_default_generator!(default_generator: NOT_PROVIDED, **)
|
|
54
|
+
return if default_generator.equal?(NOT_PROVIDED) || default_generator.respond_to?(:call)
|
|
55
|
+
|
|
56
|
+
raise ArgumentError, "default_generator must be callable, got #{default_generator.inspect}"
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def validate_coercer!(coercer)
|
|
60
|
+
return if coercer_supported?(coercer)
|
|
61
|
+
|
|
62
|
+
raise ArgumentError, "Unsupported coercer: #{coercer.inspect}"
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def coercer_supported?(coercer)
|
|
66
|
+
coercer.equal?(false)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def make_default_generator(default: NOT_PROVIDED, default_value: NOT_PROVIDED, default_generator: NOT_PROVIDED)
|
|
70
|
+
if !default.equal?(NOT_PROVIDED)
|
|
71
|
+
default.respond_to?(:call) ? default : -> { default }
|
|
72
|
+
elsif !default_value.equal?(NOT_PROVIDED)
|
|
73
|
+
-> { default_value }
|
|
74
|
+
elsif !default_generator.equal?(NOT_PROVIDED)
|
|
75
|
+
default_generator
|
|
76
|
+
else
|
|
77
|
+
NOT_PROVIDED
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
attr_reader :name, :validator, :default_generator, :coercer
|
|
83
|
+
|
|
84
|
+
def initialize(name:, validator:, default_generator:, coercer:)
|
|
85
|
+
@name = name.to_sym
|
|
86
|
+
@validator = validator
|
|
87
|
+
@default_generator = default_generator
|
|
88
|
+
@coercer = coercer
|
|
89
|
+
@optional = !default_generator.equal?(NOT_PROVIDED)
|
|
90
|
+
@detailed_validator = DetailedValidator === validator
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def optional?
|
|
94
|
+
@optional
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def valid?(value, configuration = Strict.configuration)
|
|
98
|
+
violations(value, configuration).empty?
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def violations(value, configuration = Strict.configuration)
|
|
102
|
+
return Validation::NONE unless configuration.validate?
|
|
103
|
+
return validator.violations(value) if @detailed_validator
|
|
104
|
+
return Validation::NONE if validator === value
|
|
105
|
+
|
|
106
|
+
Validation.invalid(validator, value)
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
data/lib/strict/error.rb
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Strict
|
|
4
|
-
Error
|
|
4
|
+
class Error < StandardError
|
|
5
|
+
attr_reader :violations
|
|
6
|
+
|
|
7
|
+
def initialize(message = nil, violations: Validation::NONE)
|
|
8
|
+
@violations = violations.freeze
|
|
9
|
+
super(message)
|
|
10
|
+
end
|
|
11
|
+
end
|
|
5
12
|
end
|
|
@@ -4,20 +4,29 @@ module Strict
|
|
|
4
4
|
class InitializationError < Error
|
|
5
5
|
attr_reader :remaining_attributes, :invalid_attributes, :missing_attributes
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
# rubocop:disable Metrics/MethodLength
|
|
8
|
+
def initialize(
|
|
9
|
+
initializable_class:,
|
|
10
|
+
remaining_attributes:,
|
|
11
|
+
invalid_attributes:,
|
|
12
|
+
missing_attributes:,
|
|
13
|
+
violations: Validation::NONE
|
|
14
|
+
)
|
|
8
15
|
super(
|
|
9
16
|
message_from(
|
|
10
17
|
initializable_class: initializable_class,
|
|
11
18
|
remaining_attributes: remaining_attributes,
|
|
12
19
|
invalid_attributes: invalid_attributes,
|
|
13
20
|
missing_attributes: missing_attributes
|
|
14
|
-
)
|
|
21
|
+
),
|
|
22
|
+
violations: violations
|
|
15
23
|
)
|
|
16
24
|
|
|
17
25
|
@remaining_attributes = remaining_attributes
|
|
18
26
|
@invalid_attributes = invalid_attributes
|
|
19
27
|
@missing_attributes = missing_attributes
|
|
20
28
|
end
|
|
29
|
+
# rubocop:enable Metrics/MethodLength
|
|
21
30
|
|
|
22
31
|
private
|
|
23
32
|
|
data/lib/strict/interface.rb
CHANGED
|
@@ -2,31 +2,49 @@
|
|
|
2
2
|
|
|
3
3
|
module Strict
|
|
4
4
|
module Interface
|
|
5
|
-
def self.
|
|
6
|
-
mod.
|
|
5
|
+
def self.included(mod)
|
|
6
|
+
mod.include(Strict::Method)
|
|
7
7
|
mod.include(Interfaces::Instance)
|
|
8
|
+
mod.extend(ClassMethods)
|
|
8
9
|
end
|
|
9
10
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
module ClassMethods
|
|
12
|
+
def coercer
|
|
13
|
+
Interfaces::Coercer.new(self)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def implemented_by?(implementation)
|
|
17
|
+
verify_implementation!(implementation)
|
|
18
|
+
true
|
|
19
|
+
rescue Strict::ImplementationDoesNotConformError
|
|
20
|
+
false
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def verify_implementation!(implementation)
|
|
24
|
+
strict_interface_conformance.verify!(implementation)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def expose(method_name, &)
|
|
28
|
+
method_name = method_name.to_sym
|
|
29
|
+
configuration = Methods::Dsl.run(&)
|
|
30
|
+
verifiable_method = Methods::VerifiableMethod.for_interface(
|
|
31
|
+
owner: self,
|
|
32
|
+
name: method_name,
|
|
33
|
+
configuration: configuration
|
|
34
|
+
)
|
|
13
35
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
module_eval(<<~RUBY, __FILE__, __LINE__ + 1)
|
|
26
|
-
def #{method_name}(#{parameter_list}) # def method_name(one:, two:, three:, &block)
|
|
27
|
-
implementation.#{method_name}(#{argument_list}) # implementation.method_name(one: one, two: two, three: three, &block)
|
|
28
|
-
end # end
|
|
29
|
-
RUBY
|
|
36
|
+
strict_instance_methods[method_name] = verifiable_method
|
|
37
|
+
strict_interface_conformance.compile(verifiable_method)
|
|
38
|
+
strict_method_wrapper(self).wrap(verifiable_method, invocation_target: :implementation)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def strict_interface_conformance
|
|
44
|
+
@strict_interface_conformance ||= Interfaces::Conformance.new(self)
|
|
45
|
+
end
|
|
30
46
|
end
|
|
47
|
+
|
|
48
|
+
private_constant :ClassMethods
|
|
31
49
|
end
|
|
32
50
|
end
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Strict
|
|
4
|
+
module Interfaces
|
|
5
|
+
class Conformance
|
|
6
|
+
def initialize(interface)
|
|
7
|
+
@interface = interface
|
|
8
|
+
@method_expectations = []
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def compile(verifiable_method)
|
|
12
|
+
parameter_names = verifiable_method.parameters.map(&:name).freeze
|
|
13
|
+
|
|
14
|
+
method_expectations << MethodExpectation.new(
|
|
15
|
+
name: verifiable_method.name,
|
|
16
|
+
parameter_names: parameter_names,
|
|
17
|
+
expected_parameter_mask: (1 << parameter_names.length) - 1
|
|
18
|
+
)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def verify!(receiver)
|
|
22
|
+
errors = errors_for(receiver)
|
|
23
|
+
return unless errors
|
|
24
|
+
|
|
25
|
+
missing_methods, invalid_method_definitions = errors
|
|
26
|
+
raise Strict::ImplementationDoesNotConformError.new(
|
|
27
|
+
interface: interface,
|
|
28
|
+
receiver: receiver,
|
|
29
|
+
missing_methods: missing_methods,
|
|
30
|
+
invalid_method_definitions: invalid_method_definitions
|
|
31
|
+
)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
MethodExpectation = Data.define(:name, :parameter_names, :expected_parameter_mask)
|
|
37
|
+
private_constant :MethodExpectation
|
|
38
|
+
|
|
39
|
+
attr_reader :interface, :method_expectations
|
|
40
|
+
|
|
41
|
+
# rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
42
|
+
def errors_for(receiver)
|
|
43
|
+
missing_methods = nil
|
|
44
|
+
invalid_method_definitions = nil
|
|
45
|
+
|
|
46
|
+
method_expectations.each do |expectation|
|
|
47
|
+
unless receiver.respond_to?(expectation.name)
|
|
48
|
+
(missing_methods ||= []) << expectation.name
|
|
49
|
+
next
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
invalid_definition = invalid_definition_for(expectation, receiver.method(expectation.name).parameters)
|
|
53
|
+
next unless invalid_definition
|
|
54
|
+
|
|
55
|
+
(invalid_method_definitions ||= {})[expectation.name] = invalid_definition
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
return unless missing_methods || invalid_method_definitions
|
|
59
|
+
|
|
60
|
+
[missing_methods, invalid_method_definitions || {}]
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# rubocop:enable Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
64
|
+
|
|
65
|
+
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
66
|
+
def invalid_definition_for(expectation, implementation_parameters)
|
|
67
|
+
matched_parameter_mask = 0
|
|
68
|
+
has_keyword_splat = false
|
|
69
|
+
implementation_parameters.each do |kind, parameter_name|
|
|
70
|
+
case kind
|
|
71
|
+
when :keyrest
|
|
72
|
+
has_keyword_splat = true
|
|
73
|
+
when :keyreq, :key
|
|
74
|
+
parameter_index = expectation.parameter_names.index(parameter_name)
|
|
75
|
+
matched_parameter_mask |= 1 << parameter_index if parameter_index
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
additional_parameters = nil
|
|
80
|
+
non_keyword_parameters = nil
|
|
81
|
+
|
|
82
|
+
implementation_parameters.each do |kind, parameter_name|
|
|
83
|
+
parameter_index = expectation.parameter_names.index(parameter_name)
|
|
84
|
+
case kind
|
|
85
|
+
when :keyreq
|
|
86
|
+
(additional_parameters ||= []) << parameter_name unless parameter_index
|
|
87
|
+
when :req
|
|
88
|
+
if parameter_index
|
|
89
|
+
matched_parameter_mask |= 1 << parameter_index
|
|
90
|
+
(non_keyword_parameters ||= []) << parameter_name
|
|
91
|
+
else
|
|
92
|
+
(additional_parameters ||= []) << parameter_name
|
|
93
|
+
end
|
|
94
|
+
when :opt
|
|
95
|
+
next unless parameter_index && !has_keyword_splat
|
|
96
|
+
next if matched_parameter_mask.anybits?(1 << parameter_index)
|
|
97
|
+
|
|
98
|
+
matched_parameter_mask |= 1 << parameter_index
|
|
99
|
+
(non_keyword_parameters ||= []) << parameter_name
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
missing_parameters = missing_parameters_from(expectation, matched_parameter_mask) unless has_keyword_splat
|
|
104
|
+
return unless missing_parameters || additional_parameters || non_keyword_parameters
|
|
105
|
+
|
|
106
|
+
{
|
|
107
|
+
additional_parameters: additional_parameters || [],
|
|
108
|
+
missing_parameters: missing_parameters || [],
|
|
109
|
+
non_keyword_parameters: non_keyword_parameters || []
|
|
110
|
+
}
|
|
111
|
+
end
|
|
112
|
+
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
113
|
+
|
|
114
|
+
def missing_parameters_from(expectation, matched_parameter_mask)
|
|
115
|
+
return if matched_parameter_mask == expectation.expected_parameter_mask
|
|
116
|
+
|
|
117
|
+
Set.new.tap do |missing_parameters|
|
|
118
|
+
expectation.parameter_names.each_with_index do |parameter_name, index|
|
|
119
|
+
missing_parameters << parameter_name if matched_parameter_mask.nobits?(1 << index)
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
@@ -5,59 +5,11 @@ module Strict
|
|
|
5
5
|
module Instance
|
|
6
6
|
attr_reader :implementation
|
|
7
7
|
|
|
8
|
-
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
9
8
|
def initialize(implementation)
|
|
10
|
-
|
|
11
|
-
invalid_method_definitions = Hash.new do |h, k|
|
|
12
|
-
h[k] = { additional_parameters: [], missing_parameters: [], non_keyword_parameters: [] }
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
self.class.strict_instance_methods.each do |method_name, strict_method| # rubocop:disable Metrics/BlockLength
|
|
16
|
-
unless implementation.respond_to?(method_name)
|
|
17
|
-
missing_methods ||= []
|
|
18
|
-
missing_methods << method_name
|
|
19
|
-
next
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
expected_parameters = Set.new(strict_method.parameters.map(&:name))
|
|
23
|
-
defined_parameters = Set.new
|
|
24
|
-
has_keyword_splat = false
|
|
25
|
-
|
|
26
|
-
implementation.method(method_name).parameters.each do |kind, parameter_name|
|
|
27
|
-
case kind
|
|
28
|
-
when :block, :rest
|
|
29
|
-
next
|
|
30
|
-
when :keyrest
|
|
31
|
-
has_keyword_splat = true
|
|
32
|
-
next
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
if expected_parameters.include?(parameter_name)
|
|
36
|
-
defined_parameters.add(parameter_name)
|
|
37
|
-
invalid_method_definitions[method_name][:non_keyword_parameters] << parameter_name if kind != :keyreq
|
|
38
|
-
else
|
|
39
|
-
invalid_method_definitions[method_name][:additional_parameters] << parameter_name
|
|
40
|
-
end
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
next if has_keyword_splat
|
|
44
|
-
|
|
45
|
-
missing_parameters = expected_parameters - defined_parameters
|
|
46
|
-
invalid_method_definitions[method_name][:missing_parameters] = missing_parameters if missing_parameters.any?
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
if missing_methods || !invalid_method_definitions.empty?
|
|
50
|
-
raise Strict::ImplementationDoesNotConformError.new(
|
|
51
|
-
interface: self.class,
|
|
52
|
-
receiver: implementation,
|
|
53
|
-
missing_methods: missing_methods,
|
|
54
|
-
invalid_method_definitions: invalid_method_definitions
|
|
55
|
-
)
|
|
56
|
-
end
|
|
9
|
+
self.class.verify_implementation!(implementation)
|
|
57
10
|
|
|
58
11
|
@implementation = implementation
|
|
59
12
|
end
|
|
60
|
-
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
61
13
|
end
|
|
62
14
|
end
|
|
63
15
|
end
|
data/lib/strict/method.rb
CHANGED
|
@@ -2,71 +2,90 @@
|
|
|
2
2
|
|
|
3
3
|
module Strict
|
|
4
4
|
module Method
|
|
5
|
-
def self.
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
mod.singleton_class.extend(self)
|
|
5
|
+
def self.included(mod)
|
|
6
|
+
mod.extend(ClassMethods)
|
|
9
7
|
end
|
|
10
8
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
end
|
|
9
|
+
module ClassMethods
|
|
10
|
+
def self.extended(mod)
|
|
11
|
+
return if mod.singleton_class?
|
|
15
12
|
|
|
16
|
-
|
|
17
|
-
instance = singleton_class? ? self : singleton_class
|
|
18
|
-
if instance.instance_variable_defined?(:@__strict_method_internal_class_methods)
|
|
19
|
-
instance.instance_variable_get(:@__strict_method_internal_class_methods)
|
|
20
|
-
else
|
|
21
|
-
instance.instance_variable_set(:@__strict_method_internal_class_methods, {})
|
|
13
|
+
mod.singleton_class.extend(self)
|
|
22
14
|
end
|
|
23
|
-
end
|
|
24
15
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
instance.instance_variable_get(:@__strict_method_internal_instance_methods)
|
|
29
|
-
else
|
|
30
|
-
instance.instance_variable_set(:@__strict_method_internal_instance_methods, {})
|
|
16
|
+
def sig(&)
|
|
17
|
+
instance = singleton_class? ? self : singleton_class
|
|
18
|
+
instance.instance_variable_set(:@__strict_method_internal_last_sig_configuration, Methods::Dsl.run(&))
|
|
31
19
|
end
|
|
32
|
-
end
|
|
33
20
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
21
|
+
def strict_class_methods
|
|
22
|
+
instance = singleton_class? ? self : singleton_class
|
|
23
|
+
if instance.instance_variable_defined?(:@__strict_method_internal_class_methods)
|
|
24
|
+
instance.instance_variable_get(:@__strict_method_internal_class_methods)
|
|
25
|
+
else
|
|
26
|
+
instance.instance_variable_set(:@__strict_method_internal_class_methods, {})
|
|
27
|
+
end
|
|
28
|
+
end
|
|
37
29
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
30
|
+
def strict_instance_methods
|
|
31
|
+
instance = singleton_class? ? self : singleton_class
|
|
32
|
+
if instance.instance_variable_defined?(:@__strict_method_internal_instance_methods)
|
|
33
|
+
instance.instance_variable_get(:@__strict_method_internal_instance_methods)
|
|
34
|
+
else
|
|
35
|
+
instance.instance_variable_set(:@__strict_method_internal_instance_methods, {})
|
|
36
|
+
end
|
|
37
|
+
end
|
|
41
38
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
returns: sig.returns,
|
|
46
|
-
instance: false
|
|
47
|
-
)
|
|
48
|
-
verifiable_method.verify_definition!
|
|
49
|
-
strict_class_methods[method_name] = verifiable_method
|
|
50
|
-
singleton_class.prepend(Methods::Module.new(verifiable_method))
|
|
51
|
-
end
|
|
39
|
+
# rubocop:disable Metrics/MethodLength
|
|
40
|
+
def singleton_method_added(method_name)
|
|
41
|
+
super
|
|
52
42
|
|
|
53
|
-
|
|
54
|
-
|
|
43
|
+
sig = singleton_class.instance_variable_get(:@__strict_method_internal_last_sig_configuration)
|
|
44
|
+
singleton_class.instance_variable_set(:@__strict_method_internal_last_sig_configuration, nil)
|
|
45
|
+
return unless sig
|
|
55
46
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
47
|
+
verifiable_method = Methods::VerifiableMethod.from_method(
|
|
48
|
+
method: singleton_class.instance_method(method_name),
|
|
49
|
+
configuration: sig,
|
|
50
|
+
instance: false
|
|
51
|
+
)
|
|
52
|
+
verifiable_method.verify_definition!
|
|
53
|
+
strict_class_methods[method_name] = verifiable_method
|
|
54
|
+
strict_method_wrapper(singleton_class).wrap(verifiable_method)
|
|
55
|
+
end
|
|
59
56
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
57
|
+
def method_added(method_name)
|
|
58
|
+
super
|
|
59
|
+
|
|
60
|
+
sig = singleton_class.instance_variable_get(:@__strict_method_internal_last_sig_configuration)
|
|
61
|
+
singleton_class.instance_variable_set(:@__strict_method_internal_last_sig_configuration, nil)
|
|
62
|
+
return unless sig
|
|
63
|
+
|
|
64
|
+
verifiable_method = Methods::VerifiableMethod.from_method(
|
|
65
|
+
method: instance_method(method_name),
|
|
66
|
+
configuration: sig,
|
|
67
|
+
instance: true
|
|
68
|
+
)
|
|
69
|
+
verifiable_method.verify_definition!
|
|
70
|
+
strict_instance_methods[method_name] = verifiable_method
|
|
71
|
+
strict_method_wrapper(self).wrap(verifiable_method)
|
|
72
|
+
end
|
|
73
|
+
# rubocop:enable Metrics/MethodLength
|
|
74
|
+
|
|
75
|
+
private
|
|
76
|
+
|
|
77
|
+
def strict_method_wrapper(owner)
|
|
78
|
+
if owner.instance_variable_defined?(:@__strict_method_internal_wrapper)
|
|
79
|
+
owner.instance_variable_get(:@__strict_method_internal_wrapper)
|
|
80
|
+
else
|
|
81
|
+
Methods::Module.new.tap do |wrapper|
|
|
82
|
+
owner.instance_variable_set(:@__strict_method_internal_wrapper, wrapper)
|
|
83
|
+
owner.prepend(wrapper)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
69
87
|
end
|
|
70
|
-
|
|
88
|
+
|
|
89
|
+
private_constant :ClassMethods
|
|
71
90
|
end
|
|
72
91
|
end
|
|
@@ -4,7 +4,15 @@ module Strict
|
|
|
4
4
|
class MethodCallError < Error
|
|
5
5
|
attr_reader :verifiable_method, :remaining_args, :remaining_kwargs, :invalid_parameters, :missing_parameters
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
# rubocop:disable Metrics/MethodLength, Metrics/ParameterLists
|
|
8
|
+
def initialize(
|
|
9
|
+
verifiable_method:,
|
|
10
|
+
remaining_args:,
|
|
11
|
+
remaining_kwargs:,
|
|
12
|
+
invalid_parameters:,
|
|
13
|
+
missing_parameters:,
|
|
14
|
+
violations: Validation::NONE
|
|
15
|
+
)
|
|
8
16
|
super(
|
|
9
17
|
message_from(
|
|
10
18
|
verifiable_method: verifiable_method,
|
|
@@ -12,7 +20,8 @@ module Strict
|
|
|
12
20
|
remaining_kwargs: remaining_kwargs,
|
|
13
21
|
invalid_parameters: invalid_parameters,
|
|
14
22
|
missing_parameters: missing_parameters
|
|
15
|
-
)
|
|
23
|
+
),
|
|
24
|
+
violations: violations
|
|
16
25
|
)
|
|
17
26
|
|
|
18
27
|
@verifiable_method = verifiable_method
|
|
@@ -21,6 +30,7 @@ module Strict
|
|
|
21
30
|
@invalid_parameters = invalid_parameters
|
|
22
31
|
@missing_parameters = missing_parameters
|
|
23
32
|
end
|
|
33
|
+
# rubocop:enable Metrics/MethodLength, Metrics/ParameterLists
|
|
24
34
|
|
|
25
35
|
private
|
|
26
36
|
|
|
@@ -4,8 +4,8 @@ module Strict
|
|
|
4
4
|
class MethodReturnError < Error
|
|
5
5
|
attr_reader :verifiable_method, :value
|
|
6
6
|
|
|
7
|
-
def initialize(verifiable_method:, value:)
|
|
8
|
-
super(message_from(verifiable_method: verifiable_method, value: value))
|
|
7
|
+
def initialize(verifiable_method:, value:, violations: Validation::NONE)
|
|
8
|
+
super(message_from(verifiable_method: verifiable_method, value: value), violations: violations)
|
|
9
9
|
|
|
10
10
|
@verifiable_method = verifiable_method
|
|
11
11
|
@value = value
|
data/lib/strict/methods/dsl.rb
CHANGED
|
@@ -4,9 +4,9 @@ module Strict
|
|
|
4
4
|
module Methods
|
|
5
5
|
class Dsl < BasicObject
|
|
6
6
|
class << self
|
|
7
|
-
def run(&
|
|
7
|
+
def run(&)
|
|
8
8
|
dsl = new
|
|
9
|
-
dsl.instance_eval(&
|
|
9
|
+
dsl.instance_eval(&)
|
|
10
10
|
::Strict::Methods::Configuration.new(
|
|
11
11
|
parameters: dsl.__strict_dsl_internal_parameters.values,
|
|
12
12
|
returns: dsl.__strict_dsl_internal_returns
|
|
@@ -22,22 +22,30 @@ module Strict
|
|
|
22
22
|
def initialize
|
|
23
23
|
@__strict_dsl_internal_parameters = {}
|
|
24
24
|
@__strict_dsl_internal_returns = ::Strict::Return.make
|
|
25
|
+
@__strict_dsl_internal_returns_declared = false
|
|
25
26
|
end
|
|
26
27
|
|
|
27
|
-
def returns(
|
|
28
|
-
|
|
28
|
+
def returns(*, **)
|
|
29
|
+
::Kernel.raise ::ArgumentError, "Return value is already declared" if @__strict_dsl_internal_returns_declared
|
|
30
|
+
|
|
31
|
+
self.__strict_dsl_internal_returns = ::Strict::Return.make(*, **)
|
|
32
|
+
@__strict_dsl_internal_returns_declared = true
|
|
29
33
|
nil
|
|
30
34
|
end
|
|
31
35
|
|
|
32
|
-
def strict_parameter(
|
|
33
|
-
parameter = ::Strict::Parameter.make(
|
|
36
|
+
def strict_parameter(*, **)
|
|
37
|
+
parameter = ::Strict::Parameter.make(*, **)
|
|
38
|
+
if __strict_dsl_internal_parameters.key?(parameter.name)
|
|
39
|
+
::Kernel.raise ::ArgumentError, "Parameter #{parameter.name.inspect} is already declared"
|
|
40
|
+
end
|
|
41
|
+
|
|
34
42
|
__strict_dsl_internal_parameters[parameter.name] = parameter
|
|
35
43
|
nil
|
|
36
44
|
end
|
|
37
45
|
|
|
38
|
-
def method_missing(name,
|
|
46
|
+
def method_missing(name, *, **)
|
|
39
47
|
if respond_to_missing?(name)
|
|
40
|
-
strict_parameter(name,
|
|
48
|
+
strict_parameter(name, *, **)
|
|
41
49
|
else
|
|
42
50
|
super
|
|
43
51
|
end
|