plumb 0.0.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 +7 -0
- data/.rspec +2 -0
- data/.rubocop.yml +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +628 -0
- data/Rakefile +8 -0
- data/lib/plumb/and.rb +25 -0
- data/lib/plumb/any_class.rb +19 -0
- data/lib/plumb/array_class.rb +87 -0
- data/lib/plumb/build.rb +18 -0
- data/lib/plumb/deferred.rb +31 -0
- data/lib/plumb/hash_class.rb +126 -0
- data/lib/plumb/hash_map.rb +35 -0
- data/lib/plumb/interface_class.rb +35 -0
- data/lib/plumb/json_schema_visitor.rb +222 -0
- data/lib/plumb/key.rb +41 -0
- data/lib/plumb/match_class.rb +39 -0
- data/lib/plumb/metadata.rb +15 -0
- data/lib/plumb/metadata_visitor.rb +116 -0
- data/lib/plumb/not.rb +26 -0
- data/lib/plumb/or.rb +29 -0
- data/lib/plumb/pipeline.rb +73 -0
- data/lib/plumb/result.rb +64 -0
- data/lib/plumb/rules.rb +103 -0
- data/lib/plumb/schema.rb +193 -0
- data/lib/plumb/static_class.rb +30 -0
- data/lib/plumb/step.rb +21 -0
- data/lib/plumb/steppable.rb +242 -0
- data/lib/plumb/tagged_hash.rb +37 -0
- data/lib/plumb/transform.rb +20 -0
- data/lib/plumb/tuple_class.rb +42 -0
- data/lib/plumb/type_registry.rb +37 -0
- data/lib/plumb/types.rb +140 -0
- data/lib/plumb/value_class.rb +23 -0
- data/lib/plumb/version.rb +5 -0
- data/lib/plumb/visitor_handlers.rb +34 -0
- data/lib/plumb.rb +25 -0
- metadata +107 -0
@@ -0,0 +1,242 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'plumb/metadata_visitor'
|
4
|
+
|
5
|
+
module Plumb
|
6
|
+
class UndefinedClass
|
7
|
+
def inspect
|
8
|
+
%(Undefined)
|
9
|
+
end
|
10
|
+
|
11
|
+
def to_s = inspect
|
12
|
+
def node_name = :undefined
|
13
|
+
end
|
14
|
+
|
15
|
+
TypeError = Class.new(::TypeError)
|
16
|
+
Undefined = UndefinedClass.new.freeze
|
17
|
+
|
18
|
+
BLANK_STRING = ''
|
19
|
+
BLANK_ARRAY = [].freeze
|
20
|
+
BLANK_HASH = {}.freeze
|
21
|
+
BLANK_RESULT = Result.wrap(Undefined)
|
22
|
+
|
23
|
+
module Callable
|
24
|
+
def metadata
|
25
|
+
MetadataVisitor.call(self)
|
26
|
+
end
|
27
|
+
|
28
|
+
def resolve(value = Undefined)
|
29
|
+
call(Result.wrap(value))
|
30
|
+
end
|
31
|
+
|
32
|
+
def parse(value = Undefined)
|
33
|
+
result = resolve(value)
|
34
|
+
raise TypeError, result.errors if result.invalid?
|
35
|
+
|
36
|
+
result.value
|
37
|
+
end
|
38
|
+
|
39
|
+
def call(result)
|
40
|
+
raise NotImplementedError, "Implement #call(Result) => Result in #{self.class}"
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
module Steppable
|
45
|
+
include Callable
|
46
|
+
|
47
|
+
def self.included(base)
|
48
|
+
nname = base.name.split('::').last
|
49
|
+
nname.gsub!(/([a-z\d])([A-Z])/,'\1_\2')
|
50
|
+
nname.downcase!
|
51
|
+
nname.gsub!(/_class$/, '')
|
52
|
+
nname = nname.to_sym
|
53
|
+
base.define_method(:node_name) { nname }
|
54
|
+
end
|
55
|
+
|
56
|
+
def self.wrap(callable)
|
57
|
+
if callable.is_a?(Steppable)
|
58
|
+
callable
|
59
|
+
elsif callable.respond_to?(:call)
|
60
|
+
Step.new(callable)
|
61
|
+
else
|
62
|
+
StaticClass.new(callable)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
attr_reader :name
|
67
|
+
|
68
|
+
class Name
|
69
|
+
def initialize(name)
|
70
|
+
@name = name
|
71
|
+
end
|
72
|
+
|
73
|
+
def to_s = @name
|
74
|
+
|
75
|
+
def set(n)
|
76
|
+
@name = n
|
77
|
+
self
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def freeze
|
82
|
+
return self if frozen?
|
83
|
+
|
84
|
+
@name = Name.new(_inspect)
|
85
|
+
super
|
86
|
+
end
|
87
|
+
|
88
|
+
private def _inspect = self.class.name
|
89
|
+
|
90
|
+
def inspect = name.to_s
|
91
|
+
|
92
|
+
def node_name = self.class.name.split('::').last.to_sym
|
93
|
+
|
94
|
+
def defer(definition = nil, &block)
|
95
|
+
Deferred.new(definition || block)
|
96
|
+
end
|
97
|
+
|
98
|
+
def >>(other)
|
99
|
+
And.new(self, Steppable.wrap(other))
|
100
|
+
end
|
101
|
+
|
102
|
+
def |(other)
|
103
|
+
Or.new(self, Steppable.wrap(other))
|
104
|
+
end
|
105
|
+
|
106
|
+
def transform(target_type, callable = nil, &block)
|
107
|
+
self >> Transform.new(target_type, callable || block)
|
108
|
+
end
|
109
|
+
|
110
|
+
def check(errors = 'did not pass the check', &block)
|
111
|
+
a_check = lambda { |result|
|
112
|
+
block.call(result.value) ? result : result.invalid(errors:)
|
113
|
+
}
|
114
|
+
|
115
|
+
self >> a_check
|
116
|
+
end
|
117
|
+
|
118
|
+
def meta(data = {})
|
119
|
+
self >> Metadata.new(data)
|
120
|
+
end
|
121
|
+
|
122
|
+
def not(other = self)
|
123
|
+
Not.new(other)
|
124
|
+
end
|
125
|
+
|
126
|
+
def invalid(errors: nil)
|
127
|
+
Not.new(self, errors:)
|
128
|
+
end
|
129
|
+
|
130
|
+
def value(val)
|
131
|
+
self >> ValueClass.new(val)
|
132
|
+
end
|
133
|
+
|
134
|
+
def match(*args)
|
135
|
+
self >> MatchClass.new(*args)
|
136
|
+
end
|
137
|
+
|
138
|
+
def [](val) = match(val)
|
139
|
+
|
140
|
+
DefaultProc = proc do |callable|
|
141
|
+
proc do |result|
|
142
|
+
result.valid(callable.call)
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
def default(val = Undefined, &block)
|
147
|
+
val_type = if val == Undefined
|
148
|
+
DefaultProc.call(block)
|
149
|
+
else
|
150
|
+
Types::Static[val]
|
151
|
+
end
|
152
|
+
|
153
|
+
self | (Types::Undefined >> val_type)
|
154
|
+
end
|
155
|
+
|
156
|
+
class Node
|
157
|
+
include Steppable
|
158
|
+
|
159
|
+
attr_reader :node_name, :type, :attributes
|
160
|
+
|
161
|
+
def initialize(node_name, type, attributes = BLANK_HASH)
|
162
|
+
@node_name = node_name
|
163
|
+
@type = type
|
164
|
+
@attributes = attributes
|
165
|
+
freeze
|
166
|
+
end
|
167
|
+
|
168
|
+
def call(result) = type.call(result)
|
169
|
+
end
|
170
|
+
|
171
|
+
def as_node(node_name, metadata = BLANK_HASH)
|
172
|
+
Node.new(node_name, self, metadata)
|
173
|
+
end
|
174
|
+
|
175
|
+
def nullable
|
176
|
+
Types::Nil | self
|
177
|
+
end
|
178
|
+
|
179
|
+
def present
|
180
|
+
Types::Present >> self
|
181
|
+
end
|
182
|
+
|
183
|
+
def options(opts = [])
|
184
|
+
rule(included_in: opts)
|
185
|
+
end
|
186
|
+
|
187
|
+
def rule(*args)
|
188
|
+
specs = case args
|
189
|
+
in [::Symbol => rule_name, value]
|
190
|
+
{ rule_name => value }
|
191
|
+
in [::Hash => rules]
|
192
|
+
rules
|
193
|
+
else
|
194
|
+
raise ArgumentError, "expected 1 or 2 arguments, but got #{args.size}"
|
195
|
+
end
|
196
|
+
|
197
|
+
self >> Rules.new(specs, metadata[:type])
|
198
|
+
end
|
199
|
+
|
200
|
+
def is_a(klass)
|
201
|
+
rule(is_a: klass)
|
202
|
+
end
|
203
|
+
|
204
|
+
def ===(other)
|
205
|
+
case other
|
206
|
+
when Steppable
|
207
|
+
other == self
|
208
|
+
else
|
209
|
+
resolve(other).valid?
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
def coerce(type, coercion = nil, &block)
|
214
|
+
coercion ||= block
|
215
|
+
step = lambda { |result|
|
216
|
+
if type === result.value
|
217
|
+
result.valid(coercion.call(result.value))
|
218
|
+
else
|
219
|
+
result.invalid(errors: "%s can't be coerced" % result.value.inspect)
|
220
|
+
end
|
221
|
+
}
|
222
|
+
self >> step
|
223
|
+
end
|
224
|
+
|
225
|
+
def build(cns, factory_method = :new, &block)
|
226
|
+
self >> Build.new(cns, factory_method:, &block)
|
227
|
+
end
|
228
|
+
|
229
|
+
def pipeline(&block)
|
230
|
+
Pipeline.new(self, &block)
|
231
|
+
end
|
232
|
+
|
233
|
+
def to_s
|
234
|
+
inspect
|
235
|
+
end
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
require 'plumb/deferred'
|
240
|
+
require 'plumb/transform'
|
241
|
+
require 'plumb/build'
|
242
|
+
require 'plumb/metadata'
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'plumb/steppable'
|
4
|
+
|
5
|
+
module Plumb
|
6
|
+
class TaggedHash
|
7
|
+
include Steppable
|
8
|
+
|
9
|
+
attr_reader :key, :types
|
10
|
+
|
11
|
+
def initialize(hash_type, key, types)
|
12
|
+
@hash_type = hash_type
|
13
|
+
@key = Key.wrap(key)
|
14
|
+
@types = types
|
15
|
+
|
16
|
+
raise ArgumentError, 'all types must be HashClass' if @types.size == 0 || @types.any? do |t|
|
17
|
+
!t.is_a?(HashClass)
|
18
|
+
end
|
19
|
+
raise ArgumentError, "all types must define key #{@key}" unless @types.all? { |t| !!t.at_key(@key) }
|
20
|
+
|
21
|
+
# types are assumed to have static values for the index field :key
|
22
|
+
@index = @types.each.with_object({}) do |t, memo|
|
23
|
+
memo[t.at_key(@key).resolve.value] = t
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def call(result)
|
28
|
+
result = @hash_type.call(result)
|
29
|
+
return result unless result.valid?
|
30
|
+
|
31
|
+
child = @index[result.value[@key.to_sym]]
|
32
|
+
return result.invalid(errors: "expected :#{@key.to_sym} to be one of #{@index.keys.join(', ')}") unless child
|
33
|
+
|
34
|
+
child.call(result)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'plumb/steppable'
|
4
|
+
|
5
|
+
module Plumb
|
6
|
+
class Transform
|
7
|
+
include Steppable
|
8
|
+
|
9
|
+
attr_reader :target_type
|
10
|
+
|
11
|
+
def initialize(target_type, callable)
|
12
|
+
@target_type = target_type
|
13
|
+
@callable = callable
|
14
|
+
end
|
15
|
+
|
16
|
+
def call(result)
|
17
|
+
result.valid(@callable.call(result.value))
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'plumb/steppable'
|
4
|
+
|
5
|
+
module Plumb
|
6
|
+
class TupleClass
|
7
|
+
include Steppable
|
8
|
+
|
9
|
+
attr_reader :types
|
10
|
+
|
11
|
+
def initialize(*types)
|
12
|
+
@types = types.map { |t| t.is_a?(Steppable) ? t : Types::Any.value(t) }
|
13
|
+
end
|
14
|
+
|
15
|
+
def of(*types)
|
16
|
+
self.class.new(*types)
|
17
|
+
end
|
18
|
+
|
19
|
+
alias [] of
|
20
|
+
|
21
|
+
private def _inspect
|
22
|
+
"#{name}[#{@types.map(&:inspect).join(', ')}]"
|
23
|
+
end
|
24
|
+
|
25
|
+
def call(result)
|
26
|
+
return result.invalid(errors: 'must be an Array') unless result.value.is_a?(::Array)
|
27
|
+
return result.invalid(errors: 'must have the same size') unless result.value.size == @types.size
|
28
|
+
|
29
|
+
errors = {}
|
30
|
+
values = @types.map.with_index do |type, idx|
|
31
|
+
val = result.value[idx]
|
32
|
+
r = type.resolve(val)
|
33
|
+
errors[idx] = ["expected #{type.inspect}, got #{val.inspect}", r.errors].flatten unless r.valid?
|
34
|
+
r.value
|
35
|
+
end
|
36
|
+
|
37
|
+
return result.valid(values) unless errors.any?
|
38
|
+
|
39
|
+
result.invalid(errors:)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Plumb
|
4
|
+
module TypeRegistry
|
5
|
+
def const_added(const_name)
|
6
|
+
obj = const_get(const_name)
|
7
|
+
case obj
|
8
|
+
when Module
|
9
|
+
obj.extend TypeRegistry
|
10
|
+
when Steppable
|
11
|
+
anc = [name, const_name].join('::')
|
12
|
+
obj.freeze.name.set(anc)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def included(host)
|
17
|
+
host.extend TypeRegistry
|
18
|
+
constants(false).each do |const_name|
|
19
|
+
const = const_get(const_name)
|
20
|
+
anc = [host.name, const_name].join('::')
|
21
|
+
case const
|
22
|
+
when Module
|
23
|
+
child_mod = Module.new
|
24
|
+
child_mod.define_singleton_method(:name) do
|
25
|
+
anc
|
26
|
+
end
|
27
|
+
child_mod.send(:include, const)
|
28
|
+
host.const_set(const_name, child_mod)
|
29
|
+
when Steppable
|
30
|
+
type = const.dup
|
31
|
+
type.freeze.name.set(anc)
|
32
|
+
host.const_set(const_name, type)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
data/lib/plumb/types.rb
ADDED
@@ -0,0 +1,140 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bigdecimal'
|
4
|
+
|
5
|
+
module Plumb
|
6
|
+
Rules.define :eq, 'must be equal to %<value>s' do |result, value|
|
7
|
+
value == result.value
|
8
|
+
end
|
9
|
+
Rules.define :not_eq, 'must not be equal to %<value>s' do |result, value|
|
10
|
+
value != result.value
|
11
|
+
end
|
12
|
+
# :gt for numbers and #size (arrays, strings, hashes)
|
13
|
+
[::String, ::Array, ::Hash].each do |klass|
|
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
|
21
|
+
end
|
22
|
+
|
23
|
+
Rules.define :gte, 'must be size greater or equal to %<value>s', expects: klass do |result, value|
|
24
|
+
value <= result.value.size
|
25
|
+
end
|
26
|
+
|
27
|
+
Rules.define :lte, 'must be size less or equal to %<value>s', expects: klass do |result, value|
|
28
|
+
value >= result.value
|
29
|
+
end
|
30
|
+
end
|
31
|
+
# :gt and :lt for numbers, BigDecimal
|
32
|
+
[::Numeric].each do |klass|
|
33
|
+
Rules.define :gt, 'must be greater than %<value>s', expects: klass do |result, value|
|
34
|
+
value < result.value
|
35
|
+
end
|
36
|
+
Rules.define :lt, 'must be greater than %<value>s', expects: klass do |result, value|
|
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
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
Rules.define :match, 'must match %<value>s', metadata_key: :pattern do |result, value|
|
49
|
+
value === result.value
|
50
|
+
end
|
51
|
+
Rules.define :included_in, 'elements must be included in %<value>s', expects: ::Array,
|
52
|
+
metadata_key: :options do |result, opts|
|
53
|
+
result.value.all? { |v| opts.include?(v) }
|
54
|
+
end
|
55
|
+
Rules.define :included_in, 'must be included in %<value>s', metadata_key: :options do |result, opts|
|
56
|
+
opts.include? result.value
|
57
|
+
end
|
58
|
+
Rules.define :excluded_from, 'elements must not be included in %<value>s', expects: ::Array do |result, value|
|
59
|
+
result.value.all? { |v| !value.include?(v) }
|
60
|
+
end
|
61
|
+
Rules.define :excluded_from, 'must not be included in %<value>s' do |result, value|
|
62
|
+
!value.include?(result.value)
|
63
|
+
end
|
64
|
+
Rules.define :respond_to, 'must respond to %<value>s' do |result, value|
|
65
|
+
Array(value).all? { |m| result.value.respond_to?(m) }
|
66
|
+
end
|
67
|
+
Rules.define :size, 'must be of size %<value>s', expects: :size, metadata_key: :size do |result, value|
|
68
|
+
value === result.value.size
|
69
|
+
end
|
70
|
+
|
71
|
+
module Types
|
72
|
+
extend TypeRegistry
|
73
|
+
|
74
|
+
Any = AnyClass.new
|
75
|
+
Undefined = Any.value(Plumb::Undefined)
|
76
|
+
String = Any[::String]
|
77
|
+
Symbol = Any[::Symbol]
|
78
|
+
Numeric = Any[::Numeric]
|
79
|
+
Integer = Any[::Integer]
|
80
|
+
Decimal = Any[BigDecimal]
|
81
|
+
Static = StaticClass.new
|
82
|
+
Value = ValueClass.new
|
83
|
+
Nil = Any[::NilClass]
|
84
|
+
True = Any[::TrueClass]
|
85
|
+
False = Any[::FalseClass]
|
86
|
+
Boolean = (True | False).as_node(:boolean)
|
87
|
+
Array = ArrayClass.new
|
88
|
+
Tuple = TupleClass.new
|
89
|
+
Hash = HashClass.new
|
90
|
+
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
|
+
|
103
|
+
module Lax
|
104
|
+
NUMBER_EXPR = /^\d{1,3}(?:,\d{3})*(?:\.\d+)?$/
|
105
|
+
|
106
|
+
String = Types::String \
|
107
|
+
| Any.coerce(BigDecimal) { |v| v.to_s('F') } \
|
108
|
+
| Any.coerce(::Numeric, &:to_s)
|
109
|
+
|
110
|
+
Symbol = Types::Symbol | Types::String.transform(::Symbol, &:to_sym)
|
111
|
+
|
112
|
+
NumberString = Types::String.match(NUMBER_EXPR)
|
113
|
+
CoercibleNumberString = NumberString.transform(::String) { |v| v.tr(',', '') }
|
114
|
+
|
115
|
+
Numeric = Types::Numeric | CoercibleNumberString.transform(::Numeric, &:to_f)
|
116
|
+
|
117
|
+
Decimal = Types::Decimal | \
|
118
|
+
(Types::Numeric.transform(::String, &:to_s) | CoercibleNumberString) \
|
119
|
+
.transform(::BigDecimal) { |v| BigDecimal(v) }
|
120
|
+
|
121
|
+
Integer = Numeric.transform(::Integer, &:to_i)
|
122
|
+
end
|
123
|
+
|
124
|
+
module Forms
|
125
|
+
True = Types::True \
|
126
|
+
| Types::String >> Any.coerce(/^true$/i) { |_| true } \
|
127
|
+
| Any.coerce('1') { |_| true } \
|
128
|
+
| Any.coerce(1) { |_| true }
|
129
|
+
|
130
|
+
False = Types::False \
|
131
|
+
| Types::String >> Any.coerce(/^false$/i) { |_| false } \
|
132
|
+
| Any.coerce('0') { |_| false } \
|
133
|
+
| Any.coerce(0) { |_| false }
|
134
|
+
|
135
|
+
Boolean = True | False
|
136
|
+
|
137
|
+
Nil = Nil | (String[BLANK_STRING] >> nil)
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'plumb/steppable'
|
4
|
+
|
5
|
+
module Plumb
|
6
|
+
class ValueClass
|
7
|
+
include Steppable
|
8
|
+
|
9
|
+
attr_reader :value
|
10
|
+
|
11
|
+
def initialize(value = Undefined)
|
12
|
+
@value = value
|
13
|
+
end
|
14
|
+
|
15
|
+
def inspect = @value.inspect
|
16
|
+
|
17
|
+
def [](value) = self.class.new(value)
|
18
|
+
|
19
|
+
def call(result)
|
20
|
+
@value == result.value ? result : result.invalid(errors: "Must be equal to #{@value}")
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Plumb
|
4
|
+
module VisitorHandlers
|
5
|
+
def self.included(base)
|
6
|
+
base.extend(ClassMethods)
|
7
|
+
end
|
8
|
+
|
9
|
+
module ClassMethods
|
10
|
+
def on(node_name, &block)
|
11
|
+
name = node_name.is_a?(Symbol) ? node_name : :"#{node_name}_class"
|
12
|
+
self.define_method("visit_#{name}", &block)
|
13
|
+
end
|
14
|
+
|
15
|
+
def visit(type, props = BLANK_HASH)
|
16
|
+
new.visit(type, props)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def visit(type, props = BLANK_HASH)
|
21
|
+
method_name = type.respond_to?(:node_name) ? type.node_name : :"#{(type.is_a?(::Class) ? type : type.class)}_class"
|
22
|
+
method_name = "visit_#{method_name}"
|
23
|
+
if respond_to?(method_name)
|
24
|
+
send(method_name, type, props)
|
25
|
+
else
|
26
|
+
on_missing_handler(type, props, method_name)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def on_missing_handler(type, _props, method_name)
|
31
|
+
raise "No handler for #{type.inspect} with :#{method_name}"
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
data/lib/plumb.rb
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Plumb
|
4
|
+
end
|
5
|
+
|
6
|
+
require 'plumb/result'
|
7
|
+
require 'plumb/type_registry'
|
8
|
+
require 'plumb/steppable'
|
9
|
+
require 'plumb/any_class'
|
10
|
+
require 'plumb/step'
|
11
|
+
require 'plumb/and'
|
12
|
+
require 'plumb/pipeline'
|
13
|
+
require 'plumb/rules'
|
14
|
+
require 'plumb/static_class'
|
15
|
+
require 'plumb/value_class'
|
16
|
+
require 'plumb/match_class'
|
17
|
+
require 'plumb/not'
|
18
|
+
require 'plumb/or'
|
19
|
+
require 'plumb/tuple_class'
|
20
|
+
require 'plumb/array_class'
|
21
|
+
require 'plumb/hash_class'
|
22
|
+
require 'plumb/interface_class'
|
23
|
+
require 'plumb/types'
|
24
|
+
require 'plumb/json_schema_visitor'
|
25
|
+
require 'plumb/schema'
|