plumb 0.0.1 → 0.0.2
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 +291 -19
- data/examples/command_objects.rb +207 -0
- data/examples/concurrent_downloads.rb +107 -0
- data/examples/csv_stream.rb +97 -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 +44 -13
- data/lib/plumb/hash_map.rb +34 -0
- data/lib/plumb/interface_class.rb +6 -4
- data/lib/plumb/json_schema_visitor.rb +117 -74
- data/lib/plumb/match_class.rb +8 -5
- data/lib/plumb/metadata.rb +3 -0
- data/lib/plumb/metadata_visitor.rb +45 -40
- data/lib/plumb/rules.rb +6 -7
- data/lib/plumb/schema.rb +37 -41
- data/lib/plumb/static_class.rb +4 -4
- data/lib/plumb/step.rb +6 -1
- data/lib/plumb/steppable.rb +36 -34
- 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 +19 -60
- data/lib/plumb/value_class.rb +5 -2
- data/lib/plumb/version.rb +1 -1
- data/lib/plumb/visitor_handlers.rb +13 -9
- data/lib/plumb.rb +1 -0
- metadata +8 -2
data/lib/plumb/schema.rb
CHANGED
@@ -7,13 +7,13 @@ module Plumb
|
|
7
7
|
class Schema
|
8
8
|
include Steppable
|
9
9
|
|
10
|
-
def self.wrap(
|
11
|
-
raise ArgumentError, 'expected a block or a schema' if
|
10
|
+
def self.wrap(sch = nil, &block)
|
11
|
+
raise ArgumentError, 'expected a block or a schema' if sch.nil? && !block_given?
|
12
12
|
|
13
|
-
if
|
14
|
-
raise ArgumentError, 'expected a Steppable' unless
|
13
|
+
if sch
|
14
|
+
raise ArgumentError, 'expected a Steppable' unless sch.is_a?(Steppable)
|
15
15
|
|
16
|
-
return
|
16
|
+
return sch
|
17
17
|
end
|
18
18
|
|
19
19
|
new(&block)
|
@@ -25,9 +25,10 @@ module Plumb
|
|
25
25
|
@pipeline = Types::Any
|
26
26
|
@before = Types::Any
|
27
27
|
@after = Types::Any
|
28
|
-
@_schema = {}
|
29
28
|
@_hash = hash
|
30
|
-
@fields = SymbolAccessHash.new({})
|
29
|
+
@fields = @_hash._schema.each.with_object(SymbolAccessHash.new({})) do |(k, v), memo|
|
30
|
+
memo[k] = Field.new(k, v)
|
31
|
+
end
|
31
32
|
|
32
33
|
setup(&block) if block_given?
|
33
34
|
|
@@ -71,18 +72,17 @@ module Plumb
|
|
71
72
|
|
72
73
|
private def finish
|
73
74
|
@pipeline = @before.freeze >> @_hash.freeze >> @after.freeze
|
74
|
-
@_schema.clear.freeze
|
75
75
|
freeze
|
76
76
|
end
|
77
77
|
|
78
|
-
def field(key)
|
78
|
+
def field(key, type = nil, &block)
|
79
79
|
key = Key.new(key.to_sym)
|
80
|
-
@fields[key] = Field.new(key)
|
80
|
+
@fields[key] = Field.new(key, type, &block)
|
81
81
|
end
|
82
82
|
|
83
|
-
def field?(key)
|
83
|
+
def field?(key, type = nil, &block)
|
84
84
|
key = Key.new(key.to_sym, optional: true)
|
85
|
-
@fields[key] = Field.new(key)
|
85
|
+
@fields[key] = Field.new(key, type, &block)
|
86
86
|
end
|
87
87
|
|
88
88
|
def +(other)
|
@@ -102,10 +102,6 @@ module Plumb
|
|
102
102
|
|
103
103
|
attr_reader :_hash
|
104
104
|
|
105
|
-
private
|
106
|
-
|
107
|
-
attr_reader :_schema
|
108
|
-
|
109
105
|
class SymbolAccessHash < SimpleDelegator
|
110
106
|
def [](key)
|
111
107
|
__getobj__[Key.wrap(key)]
|
@@ -117,33 +113,28 @@ module Plumb
|
|
117
113
|
|
118
114
|
attr_reader :_type, :key
|
119
115
|
|
120
|
-
def initialize(key)
|
116
|
+
def initialize(key, type = nil, &block)
|
121
117
|
@key = key.to_sym
|
122
|
-
@_type =
|
118
|
+
@_type = case type
|
119
|
+
when ArrayClass, Array
|
120
|
+
block_given? ? ArrayClass.new(element_type: Schema.new(&block)) : type
|
121
|
+
when nil
|
122
|
+
block_given? ? Schema.new(&block) : Types::Any
|
123
|
+
when Steppable
|
124
|
+
type
|
125
|
+
when Class
|
126
|
+
if type == Array && block_given?
|
127
|
+
ArrayClass.new(element_type: Schema.new(&block))
|
128
|
+
else
|
129
|
+
Types::Any[type]
|
130
|
+
end
|
131
|
+
else
|
132
|
+
raise ArgumentError, "expected a Plumb type, but got #{type.inspect}"
|
133
|
+
end
|
123
134
|
end
|
124
135
|
|
125
136
|
def call(result) = _type.call(result)
|
126
137
|
|
127
|
-
def type(steppable)
|
128
|
-
unless steppable.respond_to?(:call)
|
129
|
-
raise ArgumentError,
|
130
|
-
"expected a Plumb type, but got #{steppable.inspect}"
|
131
|
-
end
|
132
|
-
|
133
|
-
@_type >>= steppable
|
134
|
-
self
|
135
|
-
end
|
136
|
-
|
137
|
-
def schema(...)
|
138
|
-
@_type >>= Schema.wrap(...)
|
139
|
-
self
|
140
|
-
end
|
141
|
-
|
142
|
-
def array(...)
|
143
|
-
@_type >>= Types::Array[Schema.wrap(...)]
|
144
|
-
self
|
145
|
-
end
|
146
|
-
|
147
138
|
def default(v, &block)
|
148
139
|
@_type = @_type.default(v, &block)
|
149
140
|
self
|
@@ -157,12 +148,12 @@ module Plumb
|
|
157
148
|
def metadata = @_type.metadata
|
158
149
|
|
159
150
|
def options(opts)
|
160
|
-
@_type = @_type.
|
151
|
+
@_type = @_type.options(opts)
|
161
152
|
self
|
162
153
|
end
|
163
154
|
|
164
|
-
def
|
165
|
-
@_type =
|
155
|
+
def nullable
|
156
|
+
@_type = @_type.nullable
|
166
157
|
self
|
167
158
|
end
|
168
159
|
|
@@ -176,6 +167,11 @@ module Plumb
|
|
176
167
|
self
|
177
168
|
end
|
178
169
|
|
170
|
+
def match(matcher)
|
171
|
+
@_type = @_type.match(matcher)
|
172
|
+
self
|
173
|
+
end
|
174
|
+
|
179
175
|
def rule(...)
|
180
176
|
@_type = @_type.rule(...)
|
181
177
|
self
|
data/lib/plumb/static_class.rb
CHANGED
data/lib/plumb/step.rb
CHANGED
@@ -8,14 +8,19 @@ module Plumb
|
|
8
8
|
|
9
9
|
attr_reader :_metadata
|
10
10
|
|
11
|
-
def initialize(callable = nil, &block)
|
11
|
+
def initialize(callable = nil, inspect = nil, &block)
|
12
12
|
@_metadata = callable.respond_to?(:metadata) ? callable.metadata : BLANK_HASH
|
13
13
|
@callable = callable || block
|
14
|
+
@inspect = inspect || @callable.inspect
|
14
15
|
freeze
|
15
16
|
end
|
16
17
|
|
17
18
|
def call(result)
|
18
19
|
@callable.call(result)
|
19
20
|
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def _inspect = "Step[#{@inspect}]"
|
20
25
|
end
|
21
26
|
end
|
data/lib/plumb/steppable.rb
CHANGED
@@ -19,6 +19,7 @@ module Plumb
|
|
19
19
|
BLANK_ARRAY = [].freeze
|
20
20
|
BLANK_HASH = {}.freeze
|
21
21
|
BLANK_RESULT = Result.wrap(Undefined)
|
22
|
+
NOOP = ->(result) { result }
|
22
23
|
|
23
24
|
module Callable
|
24
25
|
def metadata
|
@@ -46,7 +47,7 @@ module Plumb
|
|
46
47
|
|
47
48
|
def self.included(base)
|
48
49
|
nname = base.name.split('::').last
|
49
|
-
nname.gsub!(/([a-z\d])([A-Z])/,'\1_\2')
|
50
|
+
nname.gsub!(/([a-z\d])([A-Z])/, '\1_\2')
|
50
51
|
nname.downcase!
|
51
52
|
nname.gsub!(/_class$/, '')
|
52
53
|
nname = nname.to_sym
|
@@ -56,10 +57,12 @@ module Plumb
|
|
56
57
|
def self.wrap(callable)
|
57
58
|
if callable.is_a?(Steppable)
|
58
59
|
callable
|
60
|
+
elsif callable.is_a?(::Hash)
|
61
|
+
HashClass.new(schema: callable)
|
59
62
|
elsif callable.respond_to?(:call)
|
60
63
|
Step.new(callable)
|
61
64
|
else
|
62
|
-
|
65
|
+
MatchClass.new(callable)
|
63
66
|
end
|
64
67
|
end
|
65
68
|
|
@@ -108,11 +111,7 @@ module Plumb
|
|
108
111
|
end
|
109
112
|
|
110
113
|
def check(errors = 'did not pass the check', &block)
|
111
|
-
|
112
|
-
block.call(result.value) ? result : result.invalid(errors:)
|
113
|
-
}
|
114
|
-
|
115
|
-
self >> a_check
|
114
|
+
self >> MatchClass.new(block, error: errors)
|
116
115
|
end
|
117
116
|
|
118
117
|
def meta(data = {})
|
@@ -145,10 +144,10 @@ module Plumb
|
|
145
144
|
|
146
145
|
def default(val = Undefined, &block)
|
147
146
|
val_type = if val == Undefined
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
147
|
+
DefaultProc.call(block)
|
148
|
+
else
|
149
|
+
Types::Static[val]
|
150
|
+
end
|
152
151
|
|
153
152
|
self | (Types::Undefined >> val_type)
|
154
153
|
end
|
@@ -186,21 +185,17 @@ module Plumb
|
|
186
185
|
|
187
186
|
def rule(*args)
|
188
187
|
specs = case args
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
188
|
+
in [::Symbol => rule_name, value]
|
189
|
+
{ rule_name => value }
|
190
|
+
in [::Hash => rules]
|
191
|
+
rules
|
192
|
+
else
|
193
|
+
raise ArgumentError, "expected 1 or 2 arguments, but got #{args.size}"
|
194
|
+
end
|
196
195
|
|
197
196
|
self >> Rules.new(specs, metadata[:type])
|
198
197
|
end
|
199
198
|
|
200
|
-
def is_a(klass)
|
201
|
-
rule(is_a: klass)
|
202
|
-
end
|
203
|
-
|
204
199
|
def ===(other)
|
205
200
|
case other
|
206
201
|
when Steppable
|
@@ -210,18 +205,6 @@ module Plumb
|
|
210
205
|
end
|
211
206
|
end
|
212
207
|
|
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
208
|
def build(cns, factory_method = :new, &block)
|
226
209
|
self >> Build.new(cns, factory_method:, &block)
|
227
210
|
end
|
@@ -233,6 +216,25 @@ module Plumb
|
|
233
216
|
def to_s
|
234
217
|
inspect
|
235
218
|
end
|
219
|
+
|
220
|
+
# Build a step that will invoke onr or more methods on the value.
|
221
|
+
# Ex 1: Types::String.invoke(:downcase)
|
222
|
+
# Ex 2: Types::Array.invoke(:[], 1)
|
223
|
+
# Ex 3 chain of methods: Types::String.invoke([:downcase, :to_sym])
|
224
|
+
# @return [Step]
|
225
|
+
def invoke(*args, &block)
|
226
|
+
case args
|
227
|
+
in [::Symbol => method_name, *rest]
|
228
|
+
self >> Step.new(
|
229
|
+
->(result) { result.valid(result.value.public_send(method_name, *rest, &block)) },
|
230
|
+
[method_name.inspect, rest.inspect].join(' ')
|
231
|
+
)
|
232
|
+
in [Array => methods] if methods.all? { |m| m.is_a?(Symbol) }
|
233
|
+
methods.reduce(self) { |step, method| step.invoke(method) }
|
234
|
+
else
|
235
|
+
raise ArgumentError, "expected a symbol or array of symbols, got #{args.inspect}"
|
236
|
+
end
|
237
|
+
end
|
236
238
|
end
|
237
239
|
end
|
238
240
|
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'thread'
|
4
|
+
require 'plumb/steppable'
|
5
|
+
|
6
|
+
module Plumb
|
7
|
+
# A stream that validates each element.
|
8
|
+
# Example:
|
9
|
+
# row = Types::Tuple[String, Types::Lax::Integer]
|
10
|
+
# csv_stream = Types::Stream[row]
|
11
|
+
#
|
12
|
+
# stream = csv_stream.parse(CSV.new(File.new('data.csv')).to_enum)
|
13
|
+
# stream.each |result|
|
14
|
+
# result.valid? # => true
|
15
|
+
# result.value # => ['name', 10]
|
16
|
+
# end
|
17
|
+
class StreamClass
|
18
|
+
include Steppable
|
19
|
+
|
20
|
+
attr_reader :element_type
|
21
|
+
|
22
|
+
# @option element_type [Steppable] the type of the elements in the stream
|
23
|
+
def initialize(element_type: Types::Any)
|
24
|
+
@element_type = Steppable.wrap(element_type)
|
25
|
+
freeze
|
26
|
+
end
|
27
|
+
|
28
|
+
# return a new Stream definition.
|
29
|
+
# @param element_type [Steppable] the type of the elements in the stream
|
30
|
+
def [](element_type)
|
31
|
+
self.class.new(element_type:)
|
32
|
+
end
|
33
|
+
|
34
|
+
# The [Step] interface
|
35
|
+
# @param result [Result::Valid]
|
36
|
+
# @return [Result::Valid, Result::Invalid]
|
37
|
+
def call(result)
|
38
|
+
return result.invalid(errors: 'is not an Enumerable') unless result.value.respond_to?(:each)
|
39
|
+
|
40
|
+
enum = Enumerator.new do |y|
|
41
|
+
result.value.each do |e|
|
42
|
+
y << element_type.resolve(e)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
result.valid(enum)
|
47
|
+
end
|
48
|
+
|
49
|
+
# @return [Step] a step that resolves to an Enumerator that filters out invalid elements
|
50
|
+
def filtered
|
51
|
+
self >> Step.new(nil, 'filtered') do |result|
|
52
|
+
set = result.value.lazy.filter_map { |e| e.value if e.valid? }
|
53
|
+
result.valid(set)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
def _inspect = "Stream[#{@element_type.inspect}]"
|
60
|
+
end
|
61
|
+
end
|
data/lib/plumb/tagged_hash.rb
CHANGED
@@ -13,15 +13,20 @@ module Plumb
|
|
13
13
|
@key = Key.wrap(key)
|
14
14
|
@types = types
|
15
15
|
|
16
|
-
raise ArgumentError, 'all types must be HashClass' if @types.size
|
16
|
+
raise ArgumentError, 'all types must be HashClass' if @types.size.zero? || @types.any? do |t|
|
17
17
|
!t.is_a?(HashClass)
|
18
18
|
end
|
19
19
|
raise ArgumentError, "all types must define key #{@key}" unless @types.all? { |t| !!t.at_key(@key) }
|
20
20
|
|
21
|
-
# types are assumed to have
|
21
|
+
# types are assumed to have literal values for the index field :key
|
22
22
|
@index = @types.each.with_object({}) do |t, memo|
|
23
|
-
|
23
|
+
key_type = t.at_key(@key)
|
24
|
+
raise TypeError, "key type at :#{@key} #{key_type} must be a Match type" unless key_type.is_a?(MatchClass)
|
25
|
+
|
26
|
+
memo[key_type.matcher] = t
|
24
27
|
end
|
28
|
+
|
29
|
+
freeze
|
25
30
|
end
|
26
31
|
|
27
32
|
def call(result)
|
@@ -33,5 +38,9 @@ module Plumb
|
|
33
38
|
|
34
39
|
child.call(result)
|
35
40
|
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def _inspect = "TaggedHash[#{@key.inspect}, #{@types.map(&:inspect).join(', ')}]"
|
36
45
|
end
|
37
46
|
end
|
data/lib/plumb/transform.rb
CHANGED
@@ -10,11 +10,16 @@ module Plumb
|
|
10
10
|
|
11
11
|
def initialize(target_type, callable)
|
12
12
|
@target_type = target_type
|
13
|
-
@callable = callable
|
13
|
+
@callable = callable || Plumb::NOOP
|
14
|
+
freeze
|
14
15
|
end
|
15
16
|
|
16
17
|
def call(result)
|
17
18
|
result.valid(@callable.call(result.value))
|
18
19
|
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def _inspect = "->(#{@target_type.inspect})"
|
19
24
|
end
|
20
25
|
end
|
data/lib/plumb/tuple_class.rb
CHANGED
@@ -9,7 +9,8 @@ module Plumb
|
|
9
9
|
attr_reader :types
|
10
10
|
|
11
11
|
def initialize(*types)
|
12
|
-
@types = types.map { |t|
|
12
|
+
@types = types.map { |t| Steppable.wrap(t) }
|
13
|
+
freeze
|
13
14
|
end
|
14
15
|
|
15
16
|
def of(*types)
|
@@ -18,10 +19,6 @@ module Plumb
|
|
18
19
|
|
19
20
|
alias [] of
|
20
21
|
|
21
|
-
private def _inspect
|
22
|
-
"#{name}[#{@types.map(&:inspect).join(', ')}]"
|
23
|
-
end
|
24
|
-
|
25
22
|
def call(result)
|
26
23
|
return result.invalid(errors: 'must be an Array') unless result.value.is_a?(::Array)
|
27
24
|
return result.invalid(errors: 'must have the same size') unless result.value.size == @types.size
|
@@ -38,5 +35,11 @@ module Plumb
|
|
38
35
|
|
39
36
|
result.invalid(errors:)
|
40
37
|
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def _inspect
|
42
|
+
"Tuple[#{@types.map(&:inspect).join(', ')}]"
|
43
|
+
end
|
41
44
|
end
|
42
45
|
end
|
data/lib/plumb/types.rb
CHANGED
@@ -3,56 +3,10 @@
|
|
3
3
|
require 'bigdecimal'
|
4
4
|
|
5
5
|
module Plumb
|
6
|
-
Rules.define :
|
7
|
-
value
|
6
|
+
Rules.define :included_in, 'elements must be included in %<value>s', expects: ::Array do |result, opts|
|
7
|
+
result.value.all? { |v| opts.include?(v) }
|
8
8
|
end
|
9
|
-
Rules.define :
|
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|
|
9
|
+
Rules.define :included_in, 'must be included in %<value>s' do |result, opts|
|
56
10
|
opts.include? result.value
|
57
11
|
end
|
58
12
|
Rules.define :excluded_from, 'elements must not be included in %<value>s', expects: ::Array do |result, value|
|
@@ -64,7 +18,7 @@ module Plumb
|
|
64
18
|
Rules.define :respond_to, 'must respond to %<value>s' do |result, value|
|
65
19
|
Array(value).all? { |m| result.value.respond_to?(m) }
|
66
20
|
end
|
67
|
-
Rules.define :size, 'must be of size %<value>s', expects: :size
|
21
|
+
Rules.define :size, 'must be of size %<value>s', expects: :size do |result, value|
|
68
22
|
value === result.value.size
|
69
23
|
end
|
70
24
|
|
@@ -85,6 +39,7 @@ module Plumb
|
|
85
39
|
False = Any[::FalseClass]
|
86
40
|
Boolean = (True | False).as_node(:boolean)
|
87
41
|
Array = ArrayClass.new
|
42
|
+
Stream = StreamClass.new
|
88
43
|
Tuple = TupleClass.new
|
89
44
|
Hash = HashClass.new
|
90
45
|
Interface = InterfaceClass.new
|
@@ -104,8 +59,8 @@ module Plumb
|
|
104
59
|
NUMBER_EXPR = /^\d{1,3}(?:,\d{3})*(?:\.\d+)?$/
|
105
60
|
|
106
61
|
String = Types::String \
|
107
|
-
|
|
108
|
-
|
|
62
|
+
| Types::Decimal.transform(::String) { |v| v.to_s('F') } \
|
63
|
+
| Types::Numeric.transform(::String, &:to_s)
|
109
64
|
|
110
65
|
Symbol = Types::Symbol | Types::String.transform(::Symbol, &:to_sym)
|
111
66
|
|
@@ -115,22 +70,26 @@ module Plumb
|
|
115
70
|
Numeric = Types::Numeric | CoercibleNumberString.transform(::Numeric, &:to_f)
|
116
71
|
|
117
72
|
Decimal = Types::Decimal | \
|
118
|
-
|
119
|
-
|
73
|
+
(Types::Numeric.transform(::String, &:to_s) | CoercibleNumberString) \
|
74
|
+
.transform(::BigDecimal) { |v| BigDecimal(v) }
|
120
75
|
|
121
76
|
Integer = Numeric.transform(::Integer, &:to_i)
|
122
77
|
end
|
123
78
|
|
124
79
|
module Forms
|
125
80
|
True = Types::True \
|
126
|
-
|
|
127
|
-
|
128
|
-
|
81
|
+
| (
|
82
|
+
Types::String[/^true$/i] \
|
83
|
+
| Types::String['1'] \
|
84
|
+
| Types::Integer[1]
|
85
|
+
).transform(::TrueClass) { |_| true }
|
129
86
|
|
130
87
|
False = Types::False \
|
131
|
-
|
|
132
|
-
|
133
|
-
|
88
|
+
| (
|
89
|
+
Types::String[/^false$/i] \
|
90
|
+
| Types::String['0'] \
|
91
|
+
| Types::Integer[0]
|
92
|
+
).transform(::FalseClass) { |_| false }
|
134
93
|
|
135
94
|
Boolean = True | False
|
136
95
|
|
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,30 @@ 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 =
|
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
|
22
26
|
method_name = "visit_#{method_name}"
|
23
27
|
if respond_to?(method_name)
|
24
|
-
send(method_name,
|
28
|
+
send(method_name, node, props)
|
25
29
|
else
|
26
|
-
on_missing_handler(
|
30
|
+
on_missing_handler(node, props, method_name)
|
27
31
|
end
|
28
32
|
end
|
29
33
|
|
30
|
-
def on_missing_handler(
|
31
|
-
raise "No handler for #{
|
34
|
+
def on_missing_handler(node, _props, method_name)
|
35
|
+
raise "No handler for #{node.inspect} with :#{method_name}"
|
32
36
|
end
|
33
37
|
end
|
34
38
|
end
|