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/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,8 +167,13 @@ module Plumb
|
|
176
167
|
self
|
177
168
|
end
|
178
169
|
|
179
|
-
def
|
180
|
-
@_type = @_type.
|
170
|
+
def match(matcher)
|
171
|
+
@_type = @_type.match(matcher)
|
172
|
+
self
|
173
|
+
end
|
174
|
+
|
175
|
+
def policy(...)
|
176
|
+
@_type = @_type.policy(...)
|
181
177
|
self
|
182
178
|
end
|
183
179
|
|
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
@@ -10,6 +10,7 @@ module Plumb
|
|
10
10
|
|
11
11
|
def to_s = inspect
|
12
12
|
def node_name = :undefined
|
13
|
+
def empty? = true
|
13
14
|
end
|
14
15
|
|
15
16
|
TypeError = Class.new(::TypeError)
|
@@ -19,6 +20,7 @@ module Plumb
|
|
19
20
|
BLANK_ARRAY = [].freeze
|
20
21
|
BLANK_HASH = {}.freeze
|
21
22
|
BLANK_RESULT = Result.wrap(Undefined)
|
23
|
+
NOOP = ->(result) { result }
|
22
24
|
|
23
25
|
module Callable
|
24
26
|
def metadata
|
@@ -46,7 +48,7 @@ module Plumb
|
|
46
48
|
|
47
49
|
def self.included(base)
|
48
50
|
nname = base.name.split('::').last
|
49
|
-
nname.gsub!(/([a-z\d])([A-Z])/,'\1_\2')
|
51
|
+
nname.gsub!(/([a-z\d])([A-Z])/, '\1_\2')
|
50
52
|
nname.downcase!
|
51
53
|
nname.gsub!(/_class$/, '')
|
52
54
|
nname = nname.to_sym
|
@@ -56,10 +58,12 @@ module Plumb
|
|
56
58
|
def self.wrap(callable)
|
57
59
|
if callable.is_a?(Steppable)
|
58
60
|
callable
|
61
|
+
elsif callable.is_a?(::Hash)
|
62
|
+
HashClass.new(schema: callable)
|
59
63
|
elsif callable.respond_to?(:call)
|
60
64
|
Step.new(callable)
|
61
65
|
else
|
62
|
-
|
66
|
+
MatchClass.new(callable)
|
63
67
|
end
|
64
68
|
end
|
65
69
|
|
@@ -108,11 +112,7 @@ module Plumb
|
|
108
112
|
end
|
109
113
|
|
110
114
|
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
|
115
|
+
self >> MatchClass.new(block, error: errors, label: errors)
|
116
116
|
end
|
117
117
|
|
118
118
|
def meta(data = {})
|
@@ -137,22 +137,6 @@ module Plumb
|
|
137
137
|
|
138
138
|
def [](val) = match(val)
|
139
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
140
|
class Node
|
157
141
|
include Steppable
|
158
142
|
|
@@ -172,33 +156,28 @@ module Plumb
|
|
172
156
|
Node.new(node_name, self, metadata)
|
173
157
|
end
|
174
158
|
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
159
|
+
# Register a policy for this step.
|
160
|
+
# Mode 1.a: #policy(:name, arg) a single policy with an argument
|
161
|
+
# Mode 1.b: #policy(:name) a single policy without an argument
|
162
|
+
# Mode 2: #policy(p1: value, p2: value) multiple policies with arguments
|
163
|
+
# The latter mode will be expanded to multiple #policy calls.
|
164
|
+
# @return [Step]
|
165
|
+
def policy(*args, &blk)
|
166
|
+
case args
|
167
|
+
in [::Symbol => name, *rest] # #policy(:name, arg)
|
168
|
+
types = Array(metadata[:type]).uniq
|
169
|
+
|
170
|
+
bargs = [self]
|
171
|
+
bargs << rest.first if rest.any?
|
172
|
+
block = Plumb.policies.get(types, name)
|
173
|
+
pol = block.call(*bargs, &blk)
|
174
|
+
|
175
|
+
Policy.new(name, rest.first, pol)
|
176
|
+
in [::Hash => opts] # #policy(p1: value, p2: value)
|
177
|
+
opts.reduce(self) { |step, (name, value)| step.policy(name, value) }
|
193
178
|
else
|
194
|
-
raise ArgumentError, "expected
|
179
|
+
raise ArgumentError, "expected a symbol or hash, got #{args.inspect}"
|
195
180
|
end
|
196
|
-
|
197
|
-
self >> Rules.new(specs, metadata[:type])
|
198
|
-
end
|
199
|
-
|
200
|
-
def is_a(klass)
|
201
|
-
rule(is_a: klass)
|
202
181
|
end
|
203
182
|
|
204
183
|
def ===(other)
|
@@ -210,18 +189,6 @@ module Plumb
|
|
210
189
|
end
|
211
190
|
end
|
212
191
|
|
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
192
|
def build(cns, factory_method = :new, &block)
|
226
193
|
self >> Build.new(cns, factory_method:, &block)
|
227
194
|
end
|
@@ -233,10 +200,30 @@ module Plumb
|
|
233
200
|
def to_s
|
234
201
|
inspect
|
235
202
|
end
|
203
|
+
|
204
|
+
# Build a step that will invoke one or more methods on the value.
|
205
|
+
# Ex 1: Types::String.invoke(:downcase)
|
206
|
+
# Ex 2: Types::Array.invoke(:[], 1)
|
207
|
+
# Ex 3 chain of methods: Types::String.invoke([:downcase, :to_sym])
|
208
|
+
# @return [Step]
|
209
|
+
def invoke(*args, &block)
|
210
|
+
case args
|
211
|
+
in [::Symbol => method_name, *rest]
|
212
|
+
self >> Step.new(
|
213
|
+
->(result) { result.valid(result.value.public_send(method_name, *rest, &block)) },
|
214
|
+
[method_name.inspect, rest.inspect].join(' ')
|
215
|
+
)
|
216
|
+
in [Array => methods] if methods.all? { |m| m.is_a?(Symbol) }
|
217
|
+
methods.reduce(self) { |step, method| step.invoke(method) }
|
218
|
+
else
|
219
|
+
raise ArgumentError, "expected a symbol or array of symbols, got #{args.inspect}"
|
220
|
+
end
|
221
|
+
end
|
236
222
|
end
|
237
223
|
end
|
238
224
|
|
239
225
|
require 'plumb/deferred'
|
240
226
|
require 'plumb/transform'
|
227
|
+
require 'plumb/policy'
|
241
228
|
require 'plumb/build'
|
242
229
|
require 'plumb/metadata'
|
@@ -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
|