interactor-validation 0.3.9 → 0.4.1
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/README.md +280 -1417
- data/benchmark/validation_benchmark.rb +0 -3
- data/lib/interactor/validation/configuration.rb +3 -27
- data/lib/interactor/validation/core_ext.rb +120 -0
- data/lib/interactor/validation/errors.rb +49 -0
- data/lib/interactor/validation/params.rb +6 -16
- data/lib/interactor/validation/validates.rb +157 -821
- data/lib/interactor/validation/validators/array.rb +20 -0
- data/lib/interactor/validation/validators/boolean.rb +15 -0
- data/lib/interactor/validation/validators/format.rb +17 -0
- data/lib/interactor/validation/validators/hash.rb +43 -0
- data/lib/interactor/validation/validators/inclusion.rb +17 -0
- data/lib/interactor/validation/validators/length.rb +46 -0
- data/lib/interactor/validation/validators/numeric.rb +46 -0
- data/lib/interactor/validation/validators/presence.rb +16 -0
- data/lib/interactor/validation/version.rb +1 -1
- data/lib/interactor/validation.rb +13 -44
- data/smoke_test.rb +252 -0
- metadata +28 -44
- data/lib/interactor/validation/error_codes.rb +0 -51
|
@@ -4,32 +4,12 @@ module Interactor
|
|
|
4
4
|
module Validation
|
|
5
5
|
# Configuration class for interactor validation behavior
|
|
6
6
|
class Configuration
|
|
7
|
-
attr_accessor :
|
|
8
|
-
:enable_instrumentation, :cache_regex_patterns,
|
|
9
|
-
:skip_validate
|
|
10
|
-
attr_reader :error_mode
|
|
7
|
+
attr_accessor :skip_validate, :mode, :halt
|
|
11
8
|
|
|
12
|
-
# Backward compatibility alias for halt_on_first_error
|
|
13
|
-
alias halt_on_first_error halt
|
|
14
|
-
alias halt_on_first_error= halt=
|
|
15
|
-
|
|
16
|
-
# Available error modes:
|
|
17
|
-
# - :default - Uses ActiveModel-style human-readable messages [DEFAULT]
|
|
18
|
-
# - :code - Returns structured error codes (e.g., USERNAME_IS_REQUIRED)
|
|
19
9
|
def initialize
|
|
20
|
-
@error_mode = :default
|
|
21
|
-
@halt = false
|
|
22
|
-
@regex_timeout = 0.1 # 100ms timeout for regex matching (ReDoS protection)
|
|
23
|
-
@max_array_size = 1000 # Maximum array size for nested validation (memory protection)
|
|
24
|
-
@enable_instrumentation = false # ActiveSupport::Notifications instrumentation
|
|
25
|
-
@cache_regex_patterns = true # Cache compiled regex patterns for performance
|
|
26
10
|
@skip_validate = true # Skip validate! hook if validate_params! has errors
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
def error_mode=(mode)
|
|
30
|
-
raise ArgumentError, "Invalid error_mode: #{mode}. Must be :default or :code" unless %i[default code].include?(mode)
|
|
31
|
-
|
|
32
|
-
@error_mode = mode
|
|
11
|
+
@mode = :default # Error message format mode (:default or :code)
|
|
12
|
+
@halt = false # Stop validation on first error
|
|
33
13
|
end
|
|
34
14
|
end
|
|
35
15
|
|
|
@@ -43,10 +23,6 @@ module Interactor
|
|
|
43
23
|
def configure
|
|
44
24
|
yield(configuration)
|
|
45
25
|
end
|
|
46
|
-
|
|
47
|
-
def reset_configuration!
|
|
48
|
-
@configuration = Configuration.new
|
|
49
|
-
end
|
|
50
26
|
end
|
|
51
27
|
end
|
|
52
28
|
end
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Interactor
|
|
4
|
+
module Validation
|
|
5
|
+
# Minimal core extensions - no external dependencies
|
|
6
|
+
module CoreExt
|
|
7
|
+
# Simple class attribute implementation with inheritance support
|
|
8
|
+
def class_attribute(*names)
|
|
9
|
+
names.each do |name|
|
|
10
|
+
ivar_name = "@#{name}"
|
|
11
|
+
|
|
12
|
+
# Class-level reader - checks own value, then parent
|
|
13
|
+
define_singleton_method(name) do
|
|
14
|
+
if instance_variable_defined?(ivar_name)
|
|
15
|
+
instance_variable_get(ivar_name)
|
|
16
|
+
elsif superclass.respond_to?(name)
|
|
17
|
+
# When reading from parent, ensure we get our own copy first
|
|
18
|
+
parent_value = superclass.public_send(name)
|
|
19
|
+
# Deep copy parent value if it hasn't been set on this class yet
|
|
20
|
+
if parent_value && !instance_variable_defined?(ivar_name)
|
|
21
|
+
copied_value = deep_copy(parent_value)
|
|
22
|
+
instance_variable_set(ivar_name, copied_value)
|
|
23
|
+
copied_value
|
|
24
|
+
else
|
|
25
|
+
parent_value
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Class-level writer
|
|
31
|
+
define_singleton_method("#{name}=") do |val|
|
|
32
|
+
instance_variable_set(ivar_name, val)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Instance-level reader delegates to class
|
|
36
|
+
define_method(name) { self.class.public_send(name) }
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def deep_copy(value)
|
|
43
|
+
case value
|
|
44
|
+
when Hash
|
|
45
|
+
value.transform_values { |v| deep_copy(v) }
|
|
46
|
+
when Array
|
|
47
|
+
value.map { |v| deep_copy(v) }
|
|
48
|
+
else
|
|
49
|
+
# For immutable objects (Symbol, Integer, etc.) or simple objects, return as-is
|
|
50
|
+
value.duplicable? ? value.dup : value
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Simple delegation
|
|
55
|
+
def delegate(*methods, to:)
|
|
56
|
+
methods.each do |method|
|
|
57
|
+
define_method(method) { |*args, &block| public_send(to).public_send(method, *args, &block) }
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Minimal Object extensions
|
|
65
|
+
class Object
|
|
66
|
+
def present?
|
|
67
|
+
!blank?
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def blank?
|
|
71
|
+
respond_to?(:empty?) ? empty? : false
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
class NilClass
|
|
76
|
+
def blank?
|
|
77
|
+
true
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
class FalseClass
|
|
82
|
+
def blank?
|
|
83
|
+
true
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
class String
|
|
88
|
+
def humanize
|
|
89
|
+
tr("_.", " ").sub(/\A./, &:upcase)
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
class Symbol
|
|
94
|
+
def humanize
|
|
95
|
+
to_s.humanize
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def duplicable?
|
|
99
|
+
false
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Make immutable classes non-duplicable
|
|
104
|
+
[NilClass, FalseClass, TrueClass, Symbol, Numeric].each do |klass|
|
|
105
|
+
klass.class_eval do
|
|
106
|
+
def duplicable?
|
|
107
|
+
false
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
class Object
|
|
113
|
+
def duplicable?
|
|
114
|
+
true
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
class Module
|
|
119
|
+
include Interactor::Validation::CoreExt
|
|
120
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Interactor
|
|
4
|
+
module Validation
|
|
5
|
+
# Minimal error collection
|
|
6
|
+
class Errors
|
|
7
|
+
include Enumerable
|
|
8
|
+
|
|
9
|
+
Error = Struct.new(:attribute, :type, :message, :options, keyword_init: true)
|
|
10
|
+
|
|
11
|
+
def initialize(halt_checker: nil)
|
|
12
|
+
@errors = []
|
|
13
|
+
@halt_checker = halt_checker
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def add(attribute, type = :invalid, message: nil, **options)
|
|
17
|
+
@errors << Error.new(
|
|
18
|
+
attribute: attribute,
|
|
19
|
+
type: type,
|
|
20
|
+
message: message || type.to_s,
|
|
21
|
+
options: options
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
# Raise HaltValidation if halt is configured
|
|
25
|
+
raise HaltValidation if @halt_checker&.call
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def empty?
|
|
29
|
+
@errors.empty?
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def any?
|
|
33
|
+
!empty?
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def clear
|
|
37
|
+
@errors.clear
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def each(&)
|
|
41
|
+
@errors.each(&)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def to_a
|
|
45
|
+
@errors.dup
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -3,26 +3,16 @@
|
|
|
3
3
|
module Interactor
|
|
4
4
|
module Validation
|
|
5
5
|
module Params
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
def self.included(base)
|
|
7
|
+
base.extend(ClassMethods)
|
|
8
|
+
base.class_attribute :_declared_params
|
|
9
|
+
base._declared_params = []
|
|
10
10
|
end
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
# Declares parameters that will be delegated from context
|
|
14
|
-
# and registered for validation
|
|
15
|
-
#
|
|
16
|
-
# @param param_names [Array<Symbol>] the parameter names to declare
|
|
17
|
-
# @example
|
|
18
|
-
# params :username, :password
|
|
12
|
+
module ClassMethods
|
|
19
13
|
def params(*param_names)
|
|
20
14
|
param_names.each do |param_name|
|
|
21
|
-
|
|
22
|
-
current_params = _declared_params.dup
|
|
23
|
-
self._declared_params = current_params + [param_name] unless current_params.include?(param_name)
|
|
24
|
-
|
|
25
|
-
# Delegate to context for easy access
|
|
15
|
+
_declared_params << param_name unless _declared_params.include?(param_name)
|
|
26
16
|
delegate param_name, to: :context
|
|
27
17
|
end
|
|
28
18
|
end
|