plumb 0.0.2 → 0.0.4
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/README.md +636 -129
- data/examples/concurrent_downloads.rb +3 -3
- data/examples/env_config.rb +122 -0
- data/examples/event_registry.rb +120 -0
- data/examples/weekdays.rb +1 -1
- data/lib/plumb/and.rb +4 -3
- data/lib/plumb/any_class.rb +4 -4
- data/lib/plumb/array_class.rb +8 -5
- data/lib/plumb/attributes.rb +262 -0
- data/lib/plumb/build.rb +4 -3
- data/lib/plumb/{steppable.rb → composable.rb} +85 -67
- data/lib/plumb/decorator.rb +57 -0
- data/lib/plumb/deferred.rb +1 -1
- data/lib/plumb/hash_class.rb +20 -11
- data/lib/plumb/hash_map.rb +8 -6
- data/lib/plumb/interface_class.rb +6 -2
- data/lib/plumb/json_schema_visitor.rb +97 -36
- data/lib/plumb/match_class.rb +7 -7
- data/lib/plumb/metadata.rb +5 -1
- data/lib/plumb/metadata_visitor.rb +18 -38
- data/lib/plumb/not.rb +4 -3
- data/lib/plumb/or.rb +10 -4
- data/lib/plumb/pipeline.rb +6 -5
- data/lib/plumb/policies.rb +81 -0
- data/lib/plumb/policy.rb +38 -0
- data/lib/plumb/schema.rb +13 -12
- data/lib/plumb/static_class.rb +4 -3
- data/lib/plumb/step.rb +4 -3
- data/lib/plumb/stream_class.rb +8 -7
- data/lib/plumb/tagged_hash.rb +10 -10
- data/lib/plumb/transform.rb +4 -3
- data/lib/plumb/tuple_class.rb +8 -8
- data/lib/plumb/type_registry.rb +5 -2
- data/lib/plumb/types.rb +119 -23
- data/lib/plumb/value_class.rb +4 -3
- data/lib/plumb/version.rb +1 -1
- data/lib/plumb/visitor_handlers.rb +12 -1
- data/lib/plumb.rb +59 -2
- metadata +12 -7
- data/lib/plumb/rules.rb +0 -102
@@ -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)
|
@@ -22,10 +23,6 @@ module Plumb
|
|
22
23
|
NOOP = ->(result) { result }
|
23
24
|
|
24
25
|
module Callable
|
25
|
-
def metadata
|
26
|
-
MetadataVisitor.call(self)
|
27
|
-
end
|
28
|
-
|
29
26
|
def resolve(value = Undefined)
|
30
27
|
call(Result.wrap(value))
|
31
28
|
end
|
@@ -42,9 +39,15 @@ module Plumb
|
|
42
39
|
end
|
43
40
|
end
|
44
41
|
|
45
|
-
module
|
46
|
-
|
42
|
+
# This module gets included by Composable,
|
43
|
+
# but only when Composable is `included` in classes, not `extended`.
|
44
|
+
# The rule of this module is to assign a name to constants that point to Composable instances.
|
45
|
+
module Naming
|
46
|
+
attr_reader :name
|
47
47
|
|
48
|
+
# When including this module,
|
49
|
+
# define a #node_name method on the Composable instance
|
50
|
+
# #node_name is used by Visitors to determine the type of node.
|
48
51
|
def self.included(base)
|
49
52
|
nname = base.name.split('::').last
|
50
53
|
nname.gsub!(/([a-z\d])([A-Z])/, '\1_\2')
|
@@ -54,20 +57,6 @@ module Plumb
|
|
54
57
|
base.define_method(:node_name) { nname }
|
55
58
|
end
|
56
59
|
|
57
|
-
def self.wrap(callable)
|
58
|
-
if callable.is_a?(Steppable)
|
59
|
-
callable
|
60
|
-
elsif callable.is_a?(::Hash)
|
61
|
-
HashClass.new(schema: callable)
|
62
|
-
elsif callable.respond_to?(:call)
|
63
|
-
Step.new(callable)
|
64
|
-
else
|
65
|
-
MatchClass.new(callable)
|
66
|
-
end
|
67
|
-
end
|
68
|
-
|
69
|
-
attr_reader :name
|
70
|
-
|
71
60
|
class Name
|
72
61
|
def initialize(name)
|
73
62
|
@name = name
|
@@ -93,17 +82,42 @@ module Plumb
|
|
93
82
|
def inspect = name.to_s
|
94
83
|
|
95
84
|
def node_name = self.class.name.split('::').last.to_sym
|
85
|
+
end
|
86
|
+
|
87
|
+
# Composable mixes in composition methods to classes.
|
88
|
+
# such as #>>, #|, #not, and others.
|
89
|
+
# Any Composable class can participate in Plumb compositions.
|
90
|
+
module Composable
|
91
|
+
include Callable
|
92
|
+
|
93
|
+
# This only runs when including Composable,
|
94
|
+
# not extending classes with it.
|
95
|
+
def self.included(base)
|
96
|
+
base.send(:include, Naming)
|
97
|
+
end
|
98
|
+
|
99
|
+
def self.wrap(callable)
|
100
|
+
if callable.is_a?(Composable)
|
101
|
+
callable
|
102
|
+
elsif callable.is_a?(::Hash)
|
103
|
+
HashClass.new(schema: callable)
|
104
|
+
elsif callable.respond_to?(:call)
|
105
|
+
Step.new(callable)
|
106
|
+
else
|
107
|
+
MatchClass.new(callable)
|
108
|
+
end
|
109
|
+
end
|
96
110
|
|
97
111
|
def defer(definition = nil, &block)
|
98
112
|
Deferred.new(definition || block)
|
99
113
|
end
|
100
114
|
|
101
115
|
def >>(other)
|
102
|
-
And.new(self,
|
116
|
+
And.new(self, Composable.wrap(other))
|
103
117
|
end
|
104
118
|
|
105
119
|
def |(other)
|
106
|
-
Or.new(self,
|
120
|
+
Or.new(self, Composable.wrap(other))
|
107
121
|
end
|
108
122
|
|
109
123
|
def transform(target_type, callable = nil, &block)
|
@@ -111,11 +125,15 @@ module Plumb
|
|
111
125
|
end
|
112
126
|
|
113
127
|
def check(errors = 'did not pass the check', &block)
|
114
|
-
self >> MatchClass.new(block, error: errors)
|
128
|
+
self >> MatchClass.new(block, error: errors, label: errors)
|
115
129
|
end
|
116
130
|
|
117
|
-
def
|
118
|
-
|
131
|
+
def metadata(data = Undefined)
|
132
|
+
if data == Undefined
|
133
|
+
MetadataVisitor.call(self)
|
134
|
+
else
|
135
|
+
self >> Metadata.new(data)
|
136
|
+
end
|
119
137
|
end
|
120
138
|
|
121
139
|
def not(other = self)
|
@@ -136,24 +154,8 @@ module Plumb
|
|
136
154
|
|
137
155
|
def [](val) = match(val)
|
138
156
|
|
139
|
-
DefaultProc = proc do |callable|
|
140
|
-
proc do |result|
|
141
|
-
result.valid(callable.call)
|
142
|
-
end
|
143
|
-
end
|
144
|
-
|
145
|
-
def default(val = Undefined, &block)
|
146
|
-
val_type = if val == Undefined
|
147
|
-
DefaultProc.call(block)
|
148
|
-
else
|
149
|
-
Types::Static[val]
|
150
|
-
end
|
151
|
-
|
152
|
-
self | (Types::Undefined >> val_type)
|
153
|
-
end
|
154
|
-
|
155
157
|
class Node
|
156
|
-
include
|
158
|
+
include Composable
|
157
159
|
|
158
160
|
attr_reader :node_name, :type, :attributes
|
159
161
|
|
@@ -171,40 +173,49 @@ module Plumb
|
|
171
173
|
Node.new(node_name, self, metadata)
|
172
174
|
end
|
173
175
|
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
176
|
+
# Register a policy for this step.
|
177
|
+
# Mode 1.a: #policy(:name, arg) a single policy with an argument
|
178
|
+
# Mode 1.b: #policy(:name) a single policy without an argument
|
179
|
+
# Mode 2: #policy(p1: value, p2: value) multiple policies with arguments
|
180
|
+
# The latter mode will be expanded to multiple #policy calls.
|
181
|
+
# @return [Step]
|
182
|
+
def policy(*args, &blk)
|
183
|
+
case args
|
184
|
+
in [::Symbol => name, *rest] # #policy(:name, arg)
|
185
|
+
types = Array(metadata[:type]).uniq
|
186
|
+
|
187
|
+
bargs = [self]
|
188
|
+
arg = Undefined
|
189
|
+
if rest.any?
|
190
|
+
bargs << rest.first
|
191
|
+
arg = rest.first
|
192
|
+
end
|
193
|
+
block = Plumb.policies.get(types, name)
|
194
|
+
pol = block.call(*bargs, &blk)
|
195
|
+
|
196
|
+
Policy.new(name, arg, pol)
|
197
|
+
in [::Hash => opts] # #policy(p1: value, p2: value)
|
198
|
+
opts.reduce(self) { |step, (name, value)| step.policy(name, value) }
|
199
|
+
else
|
200
|
+
raise ArgumentError, "expected a symbol or hash, got #{args.inspect}"
|
201
|
+
end
|
197
202
|
end
|
198
203
|
|
199
204
|
def ===(other)
|
200
205
|
case other
|
201
|
-
when
|
206
|
+
when Composable
|
202
207
|
other == self
|
203
208
|
else
|
204
209
|
resolve(other).valid?
|
205
210
|
end
|
206
211
|
end
|
207
212
|
|
213
|
+
def ==(other)
|
214
|
+
other.is_a?(self.class) && other.children == children
|
215
|
+
end
|
216
|
+
|
217
|
+
def children = BLANK_ARRAY
|
218
|
+
|
208
219
|
def build(cns, factory_method = :new, &block)
|
209
220
|
self >> Build.new(cns, factory_method:, &block)
|
210
221
|
end
|
@@ -217,7 +228,13 @@ module Plumb
|
|
217
228
|
inspect
|
218
229
|
end
|
219
230
|
|
220
|
-
#
|
231
|
+
# @option root [Boolean] whether to include JSON Schema $schema property
|
232
|
+
# @return [Hash]
|
233
|
+
def to_json_schema(root: false)
|
234
|
+
JSONSchemaVisitor.call(self, root:)
|
235
|
+
end
|
236
|
+
|
237
|
+
# Build a step that will invoke one or more methods on the value.
|
221
238
|
# Ex 1: Types::String.invoke(:downcase)
|
222
239
|
# Ex 2: Types::Array.invoke(:[], 1)
|
223
240
|
# Ex 3 chain of methods: Types::String.invoke([:downcase, :to_sym])
|
@@ -240,5 +257,6 @@ end
|
|
240
257
|
|
241
258
|
require 'plumb/deferred'
|
242
259
|
require 'plumb/transform'
|
260
|
+
require 'plumb/policy'
|
243
261
|
require 'plumb/build'
|
244
262
|
require 'plumb/metadata'
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Plumb
|
4
|
+
# A class to help decorate all or some types in a
|
5
|
+
# type composition.
|
6
|
+
# Example:
|
7
|
+
# Type = Types::Array[Types::String | Types::Integer]
|
8
|
+
# Decorated = Plumb::Decorator.(Type) do |type|
|
9
|
+
# if type.is_a?(Plumb::ArrayClass)
|
10
|
+
# LoggerType.new(type, 'array')
|
11
|
+
# else
|
12
|
+
# type
|
13
|
+
# end
|
14
|
+
# end
|
15
|
+
class Decorator
|
16
|
+
def self.call(type, &block)
|
17
|
+
new(block).visit(type)
|
18
|
+
end
|
19
|
+
|
20
|
+
def initialize(block)
|
21
|
+
@block = block
|
22
|
+
end
|
23
|
+
|
24
|
+
# @param type [Composable]
|
25
|
+
# @return [Composable]
|
26
|
+
def visit(type)
|
27
|
+
type = case type
|
28
|
+
when And
|
29
|
+
left, right = visit_children(type)
|
30
|
+
And.new(left, right)
|
31
|
+
when Or
|
32
|
+
left, right = visit_children(type)
|
33
|
+
Or.new(left, right)
|
34
|
+
when Not
|
35
|
+
child = visit_children(type).first
|
36
|
+
Not.new(child, errors: type.errors)
|
37
|
+
when Policy
|
38
|
+
child = visit_children(type).first
|
39
|
+
Policy.new(type.policy_name, type.arg, child)
|
40
|
+
else
|
41
|
+
type
|
42
|
+
end
|
43
|
+
|
44
|
+
decorate(type)
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def visit_children(type)
|
50
|
+
type.children.map { |child| visit(child) }
|
51
|
+
end
|
52
|
+
|
53
|
+
def decorate(type)
|
54
|
+
@block.call(type)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
data/lib/plumb/deferred.rb
CHANGED
data/lib/plumb/hash_class.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require 'plumb/
|
3
|
+
require 'plumb/composable'
|
4
4
|
require 'plumb/key'
|
5
5
|
require 'plumb/static_class'
|
6
6
|
require 'plumb/hash_map'
|
@@ -8,7 +8,9 @@ require 'plumb/tagged_hash'
|
|
8
8
|
|
9
9
|
module Plumb
|
10
10
|
class HashClass
|
11
|
-
include
|
11
|
+
include Composable
|
12
|
+
|
13
|
+
NOT_A_HASH = { _: 'must be a Hash' }.freeze
|
12
14
|
|
13
15
|
attr_reader :_schema
|
14
16
|
|
@@ -30,10 +32,8 @@ module Plumb
|
|
30
32
|
case args
|
31
33
|
in [::Hash => hash]
|
32
34
|
self.class.new(schema: _schema.merge(wrap_keys_and_values(hash)), inclusive: @inclusive)
|
33
|
-
in [
|
34
|
-
HashMap.new(key_type,
|
35
|
-
in [Class => key_type, value_type]
|
36
|
-
HashMap.new(Steppable.wrap(key_type), Steppable.wrap(value_type))
|
35
|
+
in [key_type, value_type]
|
36
|
+
HashMap.new(Composable.wrap(key_type), Composable.wrap(value_type))
|
37
37
|
else
|
38
38
|
raise ::ArgumentError, "unexpected value to Types::Hash#schema #{args.inspect}"
|
39
39
|
end
|
@@ -46,9 +46,14 @@ module Plumb
|
|
46
46
|
# we need to keep the right-side key, because even if the key name is the same,
|
47
47
|
# it's optional flag might have changed
|
48
48
|
def +(other)
|
49
|
-
|
50
|
-
|
51
|
-
|
49
|
+
other_schema = case other
|
50
|
+
when HashClass then other._schema
|
51
|
+
when ::Hash then other
|
52
|
+
else
|
53
|
+
raise ArgumentError, "expected a HashClass or Hash, got #{other.class}"
|
54
|
+
end
|
55
|
+
|
56
|
+
self.class.new(schema: merge_rightmost_keys(_schema, other_schema), inclusive: @inclusive)
|
52
57
|
end
|
53
58
|
|
54
59
|
def &(other)
|
@@ -99,7 +104,7 @@ module Plumb
|
|
99
104
|
end
|
100
105
|
|
101
106
|
def call(result)
|
102
|
-
return result.invalid(errors:
|
107
|
+
return result.invalid(errors: NOT_A_HASH) unless result.value.is_a?(::Hash)
|
103
108
|
return result unless _schema.any?
|
104
109
|
|
105
110
|
input = result.value
|
@@ -123,6 +128,10 @@ module Plumb
|
|
123
128
|
errors.any? ? result.invalid(output, errors:) : result.valid(output)
|
124
129
|
end
|
125
130
|
|
131
|
+
def ==(other)
|
132
|
+
other.is_a?(self.class) && other._schema == _schema
|
133
|
+
end
|
134
|
+
|
126
135
|
private
|
127
136
|
|
128
137
|
def _inspect
|
@@ -140,7 +149,7 @@ module Plumb
|
|
140
149
|
when Callable
|
141
150
|
hash
|
142
151
|
else # leaf values
|
143
|
-
|
152
|
+
Composable.wrap(hash)
|
144
153
|
end
|
145
154
|
end
|
146
155
|
|
data/lib/plumb/hash_map.rb
CHANGED
@@ -1,16 +1,17 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require 'plumb/
|
3
|
+
require 'plumb/composable'
|
4
4
|
|
5
5
|
module Plumb
|
6
6
|
class HashMap
|
7
|
-
include
|
7
|
+
include Composable
|
8
8
|
|
9
|
-
attr_reader :
|
9
|
+
attr_reader :children
|
10
10
|
|
11
11
|
def initialize(key_type, value_type)
|
12
12
|
@key_type = key_type
|
13
13
|
@value_type = value_type
|
14
|
+
@children = [key_type, value_type].freeze
|
14
15
|
freeze
|
15
16
|
end
|
16
17
|
|
@@ -35,19 +36,20 @@ module Plumb
|
|
35
36
|
end
|
36
37
|
|
37
38
|
def filtered
|
38
|
-
FilteredHashMap.new(key_type, value_type)
|
39
|
+
FilteredHashMap.new(@key_type, @value_type)
|
39
40
|
end
|
40
41
|
|
41
42
|
private def _inspect = "HashMap[#{@key_type.inspect}, #{@value_type.inspect}]"
|
42
43
|
|
43
44
|
class FilteredHashMap
|
44
|
-
include
|
45
|
+
include Composable
|
45
46
|
|
46
|
-
attr_reader :
|
47
|
+
attr_reader :children
|
47
48
|
|
48
49
|
def initialize(key_type, value_type)
|
49
50
|
@key_type = key_type
|
50
51
|
@value_type = value_type
|
52
|
+
@children = [key_type, value_type].freeze
|
51
53
|
freeze
|
52
54
|
end
|
53
55
|
|
@@ -1,10 +1,10 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require 'plumb/
|
3
|
+
require 'plumb/composable'
|
4
4
|
|
5
5
|
module Plumb
|
6
6
|
class InterfaceClass
|
7
|
-
include
|
7
|
+
include Composable
|
8
8
|
|
9
9
|
attr_reader :method_names
|
10
10
|
|
@@ -13,6 +13,10 @@ module Plumb
|
|
13
13
|
freeze
|
14
14
|
end
|
15
15
|
|
16
|
+
def ==(other)
|
17
|
+
other.is_a?(self.class) && other.method_names == method_names
|
18
|
+
end
|
19
|
+
|
16
20
|
def of(*args)
|
17
21
|
case args
|
18
22
|
in Array => symbols if symbols.all? { |s| s.is_a?(::Symbol) }
|