plumb 0.0.1 → 0.0.3
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/.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
|