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.
Files changed (57) 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 +118 -42
  11. data/README.md +278 -4
  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 -47
  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/configuration.rb +44 -0
  23. data/lib/strict/declaration.rb +109 -0
  24. data/lib/strict/detailed_validator.rb +9 -0
  25. data/lib/strict/error.rb +8 -1
  26. data/lib/strict/initialization_error.rb +11 -2
  27. data/lib/strict/interface.rb +39 -21
  28. data/lib/strict/interfaces/conformance.rb +125 -0
  29. data/lib/strict/interfaces/instance.rb +1 -49
  30. data/lib/strict/method.rb +72 -53
  31. data/lib/strict/method_call_error.rb +12 -2
  32. data/lib/strict/method_return_error.rb +2 -2
  33. data/lib/strict/methods/dsl.rb +16 -8
  34. data/lib/strict/methods/module.rb +27 -8
  35. data/lib/strict/methods/verifiable_method.rb +234 -70
  36. data/lib/strict/object.rb +1 -1
  37. data/lib/strict/parameter.rb +3 -52
  38. data/lib/strict/return.rb +14 -10
  39. data/lib/strict/rspec.rb +159 -0
  40. data/lib/strict/union.rb +180 -0
  41. data/lib/strict/unions/coercer.rb +55 -0
  42. data/lib/strict/validation.rb +40 -0
  43. data/lib/strict/validators/all_of.rb +11 -3
  44. data/lib/strict/validators/array_of.rb +18 -3
  45. data/lib/strict/validators/hash_of.rb +23 -3
  46. data/lib/strict/value.rb +18 -4
  47. data/lib/strict/version.rb +1 -1
  48. data/lib/strict/violation.rb +9 -0
  49. data/lib/strict.rb +42 -0
  50. data/sig/strict/rspec.rbs +10 -0
  51. data/sig/strict.rbs +213 -1
  52. data/strict.gemspec +1 -9
  53. metadata +20 -120
  54. data/lib/strict/accessor/attributes.rb +0 -15
  55. data/lib/strict/accessor/module.rb +0 -45
  56. data/lib/strict/reader/attributes.rb +0 -15
  57. 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
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Strict
4
+ module DetailedValidator
5
+ def ===(value)
6
+ violations(value).empty?
7
+ end
8
+ end
9
+ 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 = Class.new(StandardError)
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
- def initialize(initializable_class:, remaining_attributes:, invalid_attributes:, missing_attributes:) # rubocop:disable Metrics/MethodLength
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
 
@@ -2,31 +2,49 @@
2
2
 
3
3
  module Strict
4
4
  module Interface
5
- def self.extended(mod)
6
- mod.extend(Strict::Method)
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
- def coercer
11
- Interfaces::Coercer.new(self)
12
- end
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
- def expose(method_name, &block)
15
- sig = sig(&block)
16
- parameter_list = [
17
- *sig.parameters.map { |parameter| "#{parameter.name}:" },
18
- "&block"
19
- ].join(", ")
20
- argument_list = [
21
- *sig.parameters.map { |parameter| "#{parameter.name}: #{parameter.name}" },
22
- "&block"
23
- ].join(", ")
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
- missing_methods = nil
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.extended(mod)
6
- return if mod.singleton_class?
7
-
8
- mod.singleton_class.extend(self)
5
+ def self.included(mod)
6
+ mod.extend(ClassMethods)
9
7
  end
10
8
 
11
- def sig(&block)
12
- instance = singleton_class? ? self : singleton_class
13
- instance.instance_variable_set(:@__strict_method_internal_last_sig_configuration, Methods::Dsl.run(&block))
14
- end
9
+ module ClassMethods
10
+ def self.extended(mod)
11
+ return if mod.singleton_class?
15
12
 
16
- def strict_class_methods
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
- def strict_instance_methods
26
- instance = singleton_class? ? self : singleton_class
27
- if instance.instance_variable_defined?(:@__strict_method_internal_instance_methods)
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
- # rubocop:disable Metrics/MethodLength
35
- def singleton_method_added(method_name)
36
- super
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
- sig = singleton_class.instance_variable_get(:@__strict_method_internal_last_sig_configuration)
39
- singleton_class.instance_variable_set(:@__strict_method_internal_last_sig_configuration, nil)
40
- return unless sig
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
- verifiable_method = Methods::VerifiableMethod.new(
43
- method: singleton_class.instance_method(method_name),
44
- parameters: sig.parameters,
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
- def method_added(method_name)
54
- super
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
- sig = singleton_class.instance_variable_get(:@__strict_method_internal_last_sig_configuration)
57
- singleton_class.instance_variable_set(:@__strict_method_internal_last_sig_configuration, nil)
58
- return unless sig
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
- verifiable_method = Methods::VerifiableMethod.new(
61
- method: instance_method(method_name),
62
- parameters: sig.parameters,
63
- returns: sig.returns,
64
- instance: true
65
- )
66
- verifiable_method.verify_definition!
67
- strict_instance_methods[method_name] = verifiable_method
68
- prepend(Methods::Module.new(verifiable_method))
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
- # rubocop:enable Metrics/MethodLength
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
- def initialize(verifiable_method:, remaining_args:, remaining_kwargs:, invalid_parameters:, missing_parameters:) # rubocop:disable Metrics/MethodLength
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
@@ -4,9 +4,9 @@ module Strict
4
4
  module Methods
5
5
  class Dsl < BasicObject
6
6
  class << self
7
- def run(&block)
7
+ def run(&)
8
8
  dsl = new
9
- dsl.instance_eval(&block)
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(*args, **kwargs)
28
- self.__strict_dsl_internal_returns = ::Strict::Return.make(*args, **kwargs)
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(*args, **kwargs)
33
- parameter = ::Strict::Parameter.make(*args, **kwargs)
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, *args, **kwargs)
46
+ def method_missing(name, *, **)
39
47
  if respond_to_missing?(name)
40
- strict_parameter(name, *args, **kwargs)
48
+ strict_parameter(name, *, **)
41
49
  else
42
50
  super
43
51
  end