plumb 0.0.1 → 0.0.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +2 -0
- data/README.md +558 -118
- data/examples/command_objects.rb +207 -0
- data/examples/concurrent_downloads.rb +107 -0
- data/examples/csv_stream.rb +97 -0
- data/examples/env_config.rb +122 -0
- data/examples/programmers.csv +201 -0
- data/examples/weekdays.rb +66 -0
- data/lib/plumb/array_class.rb +25 -19
- data/lib/plumb/build.rb +3 -0
- data/lib/plumb/hash_class.rb +42 -13
- data/lib/plumb/hash_map.rb +34 -0
- data/lib/plumb/interface_class.rb +6 -4
- data/lib/plumb/json_schema_visitor.rb +157 -71
- data/lib/plumb/match_class.rb +8 -6
- data/lib/plumb/metadata.rb +3 -0
- data/lib/plumb/metadata_visitor.rb +54 -40
- data/lib/plumb/policies.rb +81 -0
- data/lib/plumb/policy.rb +31 -0
- data/lib/plumb/schema.rb +39 -43
- data/lib/plumb/static_class.rb +4 -4
- data/lib/plumb/step.rb +6 -1
- data/lib/plumb/steppable.rb +47 -60
- data/lib/plumb/stream_class.rb +61 -0
- data/lib/plumb/tagged_hash.rb +12 -3
- data/lib/plumb/transform.rb +6 -1
- data/lib/plumb/tuple_class.rb +8 -5
- data/lib/plumb/types.rb +119 -69
- data/lib/plumb/value_class.rb +5 -2
- data/lib/plumb/version.rb +1 -1
- data/lib/plumb/visitor_handlers.rb +19 -10
- data/lib/plumb.rb +53 -1
- metadata +14 -6
- data/lib/plumb/rules.rb +0 -103
data/lib/plumb/types.rb
CHANGED
@@ -3,71 +3,127 @@
|
|
3
3
|
require 'bigdecimal'
|
4
4
|
|
5
5
|
module Plumb
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
Rules.define :gt, 'must contain more than %<value>s elements', expects: klass do |result, value|
|
15
|
-
value < result.value.size
|
16
|
-
end
|
17
|
-
|
18
|
-
# :lt for numbers and #size (arrays, strings, hashes)
|
19
|
-
Rules.define :lt, 'must contain fewer than %<value>s elements', expects: klass do |result, value|
|
20
|
-
value > result.value.size
|
6
|
+
# Define core policies
|
7
|
+
# Allowed options for an array type.
|
8
|
+
# It validates that each element is in the options array.
|
9
|
+
# Usage:
|
10
|
+
# type = Types::Array.options(['a', 'b'])
|
11
|
+
policy :options, helper: true, for_type: ::Array do |type, opts|
|
12
|
+
type.check("must be included in #{opts.inspect}") do |v|
|
13
|
+
v.all? { |val| opts.include?(val) }
|
21
14
|
end
|
15
|
+
end
|
22
16
|
|
23
|
-
|
24
|
-
|
17
|
+
# Generic options policy for all other types.
|
18
|
+
# Usage:
|
19
|
+
# type = Types::String.options(['a', 'b'])
|
20
|
+
policy :options do |type, opts|
|
21
|
+
type.check("must be included in #{opts.inspect}") do |v|
|
22
|
+
opts.include?(v)
|
25
23
|
end
|
24
|
+
end
|
26
25
|
|
27
|
-
|
28
|
-
|
26
|
+
# Validate that array elements are NOT in the options array.
|
27
|
+
# Usage:
|
28
|
+
# type = Types::Array.policy(excluded_from: ['a', 'b'])
|
29
|
+
policy :excluded_from, for_type: ::Array do |type, opts|
|
30
|
+
type.check("must not be included in #{opts.inspect}") do |v|
|
31
|
+
v.none? { |val| opts.include?(val) }
|
29
32
|
end
|
30
33
|
end
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
value > result.value
|
38
|
-
end
|
39
|
-
Rules.define :gte, 'must be greater or equal to %<value>s', expects: klass do |result, value|
|
40
|
-
value <= result.value
|
41
|
-
end
|
42
|
-
# :lte for numbers and #size (arrays, strings, hashes)
|
43
|
-
Rules.define :lte, 'must be less or equal to %<value>s', expects: klass do |result, value|
|
44
|
-
value >= result.value
|
34
|
+
|
35
|
+
# Usage:
|
36
|
+
# type = Types::String.policy(excluded_from: ['a', 'b'])
|
37
|
+
policy :excluded_from do |type, opts|
|
38
|
+
type.check("must not be included in #{opts.inspect}") do |v|
|
39
|
+
!opts.include?(v)
|
45
40
|
end
|
46
41
|
end
|
47
42
|
|
48
|
-
|
49
|
-
|
43
|
+
# Validate #size against a number or any object that responds to #===.
|
44
|
+
# This works with any type that repsonds to #size.
|
45
|
+
# Usage:
|
46
|
+
# type = Types::String.policy(size: 10)
|
47
|
+
# type = Types::Integer.policy(size: 1..10)
|
48
|
+
# type = Types::Array.policy(size: 1..)
|
49
|
+
# type = Types::Any[Set].policy(size: 1..)
|
50
|
+
policy :size, for_type: :size do |type, size|
|
51
|
+
type.check("must be of size #{size}") { |v| size === v.size }
|
50
52
|
end
|
51
|
-
|
52
|
-
|
53
|
-
|
53
|
+
|
54
|
+
# Validate that an object is not #empty? nor #nil?
|
55
|
+
# Usage:
|
56
|
+
# Types::String.present
|
57
|
+
# Types::Array.present
|
58
|
+
policy :present, helper: true do |type, *_args|
|
59
|
+
type.check('must be present') do |v|
|
60
|
+
if v.respond_to?(:empty?)
|
61
|
+
!v.empty?
|
62
|
+
else
|
63
|
+
!v.nil?
|
64
|
+
end
|
54
65
|
end
|
55
|
-
Rules.define :included_in, 'must be included in %<value>s', metadata_key: :options do |result, opts|
|
56
|
-
opts.include? result.value
|
57
66
|
end
|
58
|
-
|
59
|
-
|
67
|
+
|
68
|
+
# Allow nil values for a type.
|
69
|
+
# Usage:
|
70
|
+
# nullable_int = Types::Integer.nullable
|
71
|
+
# nullable_int.parse(nil) # => nil
|
72
|
+
# nullable_int.parse(10) # => 10
|
73
|
+
# nullable_int.parse('nope') # => error: not an Integer
|
74
|
+
policy :nullable, helper: true do |type, *_args|
|
75
|
+
Types::Nil | type
|
60
76
|
end
|
61
|
-
|
62
|
-
|
77
|
+
|
78
|
+
# Validate that a value responds to a method
|
79
|
+
# Usage:
|
80
|
+
# type = Types::Any.policy(respond_to: :upcase)
|
81
|
+
# type = Types::Any.policy(respond_to: [:upcase, :strip])
|
82
|
+
policy :respond_to do |type, method_names|
|
83
|
+
type.check("must respond to #{method_names.inspect}") do |value|
|
84
|
+
Array(method_names).all? { |m| value.respond_to?(m) }
|
85
|
+
end
|
63
86
|
end
|
64
|
-
|
65
|
-
|
87
|
+
|
88
|
+
# Return a default value if the input value is Undefined (ie key not present in a Hash).
|
89
|
+
# Usage:
|
90
|
+
# type = Types::String.default('default')
|
91
|
+
# type.parse(Undefined) # => 'default'
|
92
|
+
# type.parse('yes') # => 'yes'
|
93
|
+
#
|
94
|
+
# Works with a block too:
|
95
|
+
# date = Type::Any[Date].default { Date.today }
|
96
|
+
#
|
97
|
+
policy :default, helper: true do |type, value = Undefined, &block|
|
98
|
+
val_type = if value == Undefined
|
99
|
+
Step.new(->(result) { result.valid(block.call) }, 'default proc')
|
100
|
+
else
|
101
|
+
Types::Static[value]
|
102
|
+
end
|
103
|
+
|
104
|
+
type | (Types::Undefined >> val_type)
|
66
105
|
end
|
67
|
-
|
68
|
-
|
106
|
+
|
107
|
+
# Split a string into an array. Default separator is /\s*,\s*/
|
108
|
+
# Usage:
|
109
|
+
# type = Types::String.split
|
110
|
+
# type.parse('a,b,c') # => ['a', 'b', 'c']
|
111
|
+
#
|
112
|
+
# Custom separator:
|
113
|
+
# type = Types::String.split(';')
|
114
|
+
module SplitPolicy
|
115
|
+
DEFAULT_SEPARATOR = /\s*,\s*/
|
116
|
+
|
117
|
+
def self.call(type, separator = DEFAULT_SEPARATOR)
|
118
|
+
type.invoke(:split, separator) >> Types::Array[String]
|
119
|
+
end
|
120
|
+
|
121
|
+
def self.for_type = ::String
|
122
|
+
def self.helper = false
|
69
123
|
end
|
70
124
|
|
125
|
+
policy :split, SplitPolicy
|
126
|
+
|
71
127
|
module Types
|
72
128
|
extend TypeRegistry
|
73
129
|
|
@@ -85,27 +141,17 @@ module Plumb
|
|
85
141
|
False = Any[::FalseClass]
|
86
142
|
Boolean = (True | False).as_node(:boolean)
|
87
143
|
Array = ArrayClass.new
|
144
|
+
Stream = StreamClass.new
|
88
145
|
Tuple = TupleClass.new
|
89
146
|
Hash = HashClass.new
|
90
147
|
Interface = InterfaceClass.new
|
91
|
-
# TODO: type-speficic concept of blank, via Rules
|
92
|
-
Blank = (
|
93
|
-
Undefined \
|
94
|
-
| Nil \
|
95
|
-
| String.value(BLANK_STRING) \
|
96
|
-
| Hash.value(BLANK_HASH) \
|
97
|
-
| Array.value(BLANK_ARRAY)
|
98
|
-
)
|
99
|
-
|
100
|
-
Present = Blank.invalid(errors: 'must be present')
|
101
|
-
Split = String.transform(::String) { |v| v.split(/\s*,\s*/) }
|
102
148
|
|
103
149
|
module Lax
|
104
150
|
NUMBER_EXPR = /^\d{1,3}(?:,\d{3})*(?:\.\d+)?$/
|
105
151
|
|
106
152
|
String = Types::String \
|
107
|
-
|
|
108
|
-
|
|
153
|
+
| Types::Decimal.transform(::String) { |v| v.to_s('F') } \
|
154
|
+
| Types::Numeric.transform(::String, &:to_s)
|
109
155
|
|
110
156
|
Symbol = Types::Symbol | Types::String.transform(::Symbol, &:to_sym)
|
111
157
|
|
@@ -115,22 +161,26 @@ module Plumb
|
|
115
161
|
Numeric = Types::Numeric | CoercibleNumberString.transform(::Numeric, &:to_f)
|
116
162
|
|
117
163
|
Decimal = Types::Decimal | \
|
118
|
-
|
119
|
-
|
164
|
+
(Types::Numeric.transform(::String, &:to_s) | CoercibleNumberString) \
|
165
|
+
.transform(::BigDecimal) { |v| BigDecimal(v) }
|
120
166
|
|
121
167
|
Integer = Numeric.transform(::Integer, &:to_i)
|
122
168
|
end
|
123
169
|
|
124
170
|
module Forms
|
125
171
|
True = Types::True \
|
126
|
-
|
|
127
|
-
|
128
|
-
|
172
|
+
| (
|
173
|
+
Types::String[/^true$/i] \
|
174
|
+
| Types::String['1'] \
|
175
|
+
| Types::Integer[1]
|
176
|
+
).transform(::TrueClass) { |_| true }
|
129
177
|
|
130
178
|
False = Types::False \
|
131
|
-
|
|
132
|
-
|
133
|
-
|
179
|
+
| (
|
180
|
+
Types::String[/^false$/i] \
|
181
|
+
| Types::String['0'] \
|
182
|
+
| Types::Integer[0]
|
183
|
+
).transform(::FalseClass) { |_| false }
|
134
184
|
|
135
185
|
Boolean = True | False
|
136
186
|
|
data/lib/plumb/value_class.rb
CHANGED
@@ -10,14 +10,17 @@ module Plumb
|
|
10
10
|
|
11
11
|
def initialize(value = Undefined)
|
12
12
|
@value = value
|
13
|
+
freeze
|
13
14
|
end
|
14
15
|
|
15
|
-
def inspect = @value.inspect
|
16
|
-
|
17
16
|
def [](value) = self.class.new(value)
|
18
17
|
|
19
18
|
def call(result)
|
20
19
|
@value == result.value ? result : result.invalid(errors: "Must be equal to #{@value}")
|
21
20
|
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def _inspect = @value.inspect
|
22
25
|
end
|
23
26
|
end
|
data/lib/plumb/version.rb
CHANGED
@@ -9,26 +9,35 @@ module Plumb
|
|
9
9
|
module ClassMethods
|
10
10
|
def on(node_name, &block)
|
11
11
|
name = node_name.is_a?(Symbol) ? node_name : :"#{node_name}_class"
|
12
|
-
|
12
|
+
define_method("visit_#{name}", &block)
|
13
13
|
end
|
14
14
|
|
15
|
-
def visit(
|
16
|
-
new.visit(
|
15
|
+
def visit(node, props = BLANK_HASH)
|
16
|
+
new.visit(node, props)
|
17
17
|
end
|
18
18
|
end
|
19
19
|
|
20
|
-
def visit(
|
21
|
-
method_name =
|
22
|
-
|
20
|
+
def visit(node, props = BLANK_HASH)
|
21
|
+
method_name = if node.respond_to?(:node_name)
|
22
|
+
node.node_name
|
23
|
+
else
|
24
|
+
:"#{(node.is_a?(::Class) ? node : node.class)}_class"
|
25
|
+
end
|
26
|
+
|
27
|
+
visit_name(method_name, node, props)
|
28
|
+
end
|
29
|
+
|
30
|
+
def visit_name(method_name, node, props = BLANK_HASH)
|
31
|
+
method_name = :"visit_#{method_name}"
|
23
32
|
if respond_to?(method_name)
|
24
|
-
send(method_name,
|
33
|
+
send(method_name, node, props)
|
25
34
|
else
|
26
|
-
on_missing_handler(
|
35
|
+
on_missing_handler(node, props, method_name)
|
27
36
|
end
|
28
37
|
end
|
29
38
|
|
30
|
-
def on_missing_handler(
|
31
|
-
raise "No handler for #{
|
39
|
+
def on_missing_handler(node, _props, method_name)
|
40
|
+
raise "No handler for #{node.inspect} with :#{method_name}"
|
32
41
|
end
|
33
42
|
end
|
34
43
|
end
|
data/lib/plumb.rb
CHANGED
@@ -1,6 +1,58 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'plumb/policies'
|
4
|
+
|
3
5
|
module Plumb
|
6
|
+
@policies = Policies.new
|
7
|
+
|
8
|
+
def self.policies
|
9
|
+
@policies
|
10
|
+
end
|
11
|
+
|
12
|
+
# Register a policy with the given name and block.
|
13
|
+
# Optionally define a method on the Steppable method to call the policy.
|
14
|
+
# Example:
|
15
|
+
# Plumb.policy(:multiply_by, for_type: Integer, helper: true) do |step, factor, &block|
|
16
|
+
# step.transform(Integer) { |number| number * factor }
|
17
|
+
# end
|
18
|
+
#
|
19
|
+
# type = Types::Integer.multiply_by(2)
|
20
|
+
# type.parse(10) # => 20
|
21
|
+
#
|
22
|
+
# @param name [Symbol] the name of the policy
|
23
|
+
# @param opts [Hash] options for the policy
|
24
|
+
# @yield [Step, Object, &block] the step (type), policy argument, and policy block, if any.
|
25
|
+
def self.policy(name, opts = {}, &block)
|
26
|
+
name = name.to_sym
|
27
|
+
if opts.is_a?(Hash) && block_given?
|
28
|
+
for_type = opts[:for_type] || Object
|
29
|
+
helper = opts[:helper] || false
|
30
|
+
elsif opts.respond_to?(:call) && opts.respond_to?(:for_type) && opts.respond_to?(:helper)
|
31
|
+
for_type = opts.for_type
|
32
|
+
helper = opts.helper
|
33
|
+
block = opts.method(:call)
|
34
|
+
else
|
35
|
+
raise ArgumentError, 'Expected a block or a hash with :for_type and :helper keys'
|
36
|
+
end
|
37
|
+
|
38
|
+
policies.register(for_type, name, block)
|
39
|
+
|
40
|
+
return self unless helper
|
41
|
+
|
42
|
+
if Steppable.instance_methods.include?(name)
|
43
|
+
raise Policies::MethodAlreadyDefinedError, "Method #{name} is already defined on Steppable"
|
44
|
+
end
|
45
|
+
|
46
|
+
Steppable.define_method(name) do |arg = Undefined, &bl|
|
47
|
+
if arg == Undefined
|
48
|
+
policy(name, &bl)
|
49
|
+
else
|
50
|
+
policy(name, arg, &bl)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
self
|
55
|
+
end
|
4
56
|
end
|
5
57
|
|
6
58
|
require 'plumb/result'
|
@@ -10,7 +62,6 @@ require 'plumb/any_class'
|
|
10
62
|
require 'plumb/step'
|
11
63
|
require 'plumb/and'
|
12
64
|
require 'plumb/pipeline'
|
13
|
-
require 'plumb/rules'
|
14
65
|
require 'plumb/static_class'
|
15
66
|
require 'plumb/value_class'
|
16
67
|
require 'plumb/match_class'
|
@@ -18,6 +69,7 @@ require 'plumb/not'
|
|
18
69
|
require 'plumb/or'
|
19
70
|
require 'plumb/tuple_class'
|
20
71
|
require 'plumb/array_class'
|
72
|
+
require 'plumb/stream_class'
|
21
73
|
require 'plumb/hash_class'
|
22
74
|
require 'plumb/interface_class'
|
23
75
|
require 'plumb/types'
|
metadata
CHANGED
@@ -1,17 +1,17 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: plumb
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ismael Celis
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-07-
|
11
|
+
date: 2024-07-18 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
|
-
name:
|
14
|
+
name: bigdecimal
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
17
|
- - ">="
|
@@ -25,7 +25,7 @@ dependencies:
|
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: '0'
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
|
-
name:
|
28
|
+
name: concurrent-ruby
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
31
|
- - ">="
|
@@ -50,6 +50,12 @@ files:
|
|
50
50
|
- LICENSE.txt
|
51
51
|
- README.md
|
52
52
|
- Rakefile
|
53
|
+
- examples/command_objects.rb
|
54
|
+
- examples/concurrent_downloads.rb
|
55
|
+
- examples/csv_stream.rb
|
56
|
+
- examples/env_config.rb
|
57
|
+
- examples/programmers.csv
|
58
|
+
- examples/weekdays.rb
|
53
59
|
- lib/plumb.rb
|
54
60
|
- lib/plumb/and.rb
|
55
61
|
- lib/plumb/any_class.rb
|
@@ -67,12 +73,14 @@ files:
|
|
67
73
|
- lib/plumb/not.rb
|
68
74
|
- lib/plumb/or.rb
|
69
75
|
- lib/plumb/pipeline.rb
|
76
|
+
- lib/plumb/policies.rb
|
77
|
+
- lib/plumb/policy.rb
|
70
78
|
- lib/plumb/result.rb
|
71
|
-
- lib/plumb/rules.rb
|
72
79
|
- lib/plumb/schema.rb
|
73
80
|
- lib/plumb/static_class.rb
|
74
81
|
- lib/plumb/step.rb
|
75
82
|
- lib/plumb/steppable.rb
|
83
|
+
- lib/plumb/stream_class.rb
|
76
84
|
- lib/plumb/tagged_hash.rb
|
77
85
|
- lib/plumb/transform.rb
|
78
86
|
- lib/plumb/tuple_class.rb
|
@@ -81,7 +89,7 @@ files:
|
|
81
89
|
- lib/plumb/value_class.rb
|
82
90
|
- lib/plumb/version.rb
|
83
91
|
- lib/plumb/visitor_handlers.rb
|
84
|
-
homepage:
|
92
|
+
homepage: https://github.com/ismasan/plumb
|
85
93
|
licenses:
|
86
94
|
- MIT
|
87
95
|
metadata: {}
|
data/lib/plumb/rules.rb
DELETED
@@ -1,103 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'plumb/steppable'
|
4
|
-
|
5
|
-
module Plumb
|
6
|
-
class Rules
|
7
|
-
UnsupportedRuleError = Class.new(StandardError)
|
8
|
-
UndefinedRuleError = Class.new(KeyError)
|
9
|
-
|
10
|
-
class Registry
|
11
|
-
RuleDef = Data.define(:name, :error_tpl, :callable, :metadata_key, :expects) do
|
12
|
-
def supports?(type)
|
13
|
-
types = [type].flatten # may be an array of types for OR logic
|
14
|
-
case expects
|
15
|
-
when Symbol
|
16
|
-
types.all? { |type| type.public_instance_methods.include?(expects) }
|
17
|
-
when Class then types.all? { |type| type <= expects }
|
18
|
-
when Object then true
|
19
|
-
else raise "Unexpected expects: #{expects}"
|
20
|
-
end
|
21
|
-
end
|
22
|
-
end
|
23
|
-
|
24
|
-
Rule = Data.define(:rule_def, :arg_value, :error_str) do
|
25
|
-
def self.build(rule_def, arg_value)
|
26
|
-
error_str = format(rule_def.error_tpl, value: arg_value)
|
27
|
-
new(rule_def, arg_value, error_str)
|
28
|
-
end
|
29
|
-
|
30
|
-
def node_name = :"rule_#{rule_def.name}"
|
31
|
-
def name = rule_def.name
|
32
|
-
def metadata_key = rule_def.metadata_key
|
33
|
-
|
34
|
-
def error_for(result)
|
35
|
-
return nil if rule_def.callable.call(result, arg_value)
|
36
|
-
|
37
|
-
error_str
|
38
|
-
end
|
39
|
-
end
|
40
|
-
|
41
|
-
def initialize
|
42
|
-
@definitions = Hash.new { |h, k| h[k] = Set.new }
|
43
|
-
end
|
44
|
-
|
45
|
-
def define(name, error_tpl, callable = nil, metadata_key: name, expects: Object, &block)
|
46
|
-
name = name.to_sym
|
47
|
-
callable ||= block
|
48
|
-
@definitions[name] << RuleDef.new(name:, error_tpl:, callable:, metadata_key:, expects:)
|
49
|
-
end
|
50
|
-
|
51
|
-
# Ex. size: 3, match: /foo/
|
52
|
-
def resolve(rule_specs, for_type)
|
53
|
-
rule_specs.map do |(name, arg_value)|
|
54
|
-
rule_defs = @definitions.fetch(name.to_sym) { raise UndefinedRuleError, "no rule defined with :#{name}" }
|
55
|
-
rule_def = rule_defs.find { |rd| rd.supports?(for_type) }
|
56
|
-
unless rule_def
|
57
|
-
raise UnsupportedRuleError, "No :#{name} rule for type #{for_type}" unless for_type.is_a?(Array)
|
58
|
-
|
59
|
-
raise UnsupportedRuleError,
|
60
|
-
"Can't apply :#{name} rule for types #{for_type}. All types must support the same rule implementation"
|
61
|
-
|
62
|
-
end
|
63
|
-
|
64
|
-
Rule.build(rule_def, arg_value)
|
65
|
-
end
|
66
|
-
end
|
67
|
-
end
|
68
|
-
|
69
|
-
include Steppable
|
70
|
-
|
71
|
-
def self.registry
|
72
|
-
@registry ||= Registry.new
|
73
|
-
end
|
74
|
-
|
75
|
-
def self.define(...)
|
76
|
-
registry.define(...)
|
77
|
-
end
|
78
|
-
|
79
|
-
# Ex. new(size: 3, match: /foo/)
|
80
|
-
attr_reader :rules
|
81
|
-
|
82
|
-
def initialize(rule_specs, for_type)
|
83
|
-
@rules = self.class.registry.resolve(rule_specs, for_type).freeze
|
84
|
-
freeze
|
85
|
-
end
|
86
|
-
|
87
|
-
def call(result)
|
88
|
-
errors = []
|
89
|
-
err = nil
|
90
|
-
@rules.each do |rule|
|
91
|
-
err = rule.error_for(result)
|
92
|
-
errors << err if err
|
93
|
-
end
|
94
|
-
return result unless errors.any?
|
95
|
-
|
96
|
-
result.invalid(errors: errors.join(', '))
|
97
|
-
end
|
98
|
-
|
99
|
-
private def _inspect
|
100
|
-
+'Rules(' << @rules.map { |r| [r.name, r.arg_value].join(': ') }.join(', ') << +')'
|
101
|
-
end
|
102
|
-
end
|
103
|
-
end
|