plumb 0.0.3 → 0.0.5
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 +609 -57
- data/bench/compare_parametric_schema.rb +102 -0
- data/bench/compare_parametric_struct.rb +68 -0
- data/bench/parametric_schema.rb +229 -0
- data/bench/plumb_hash.rb +109 -0
- data/examples/concurrent_downloads.rb +3 -3
- data/examples/env_config.rb +2 -2
- data/examples/event_registry.rb +127 -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 +268 -0
- data/lib/plumb/build.rb +4 -3
- data/lib/plumb/composable.rb +381 -0
- data/lib/plumb/decorator.rb +57 -0
- data/lib/plumb/deferred.rb +1 -1
- data/lib/plumb/hash_class.rb +19 -8
- data/lib/plumb/hash_map.rb +8 -6
- data/lib/plumb/interface_class.rb +6 -2
- data/lib/plumb/json_schema_visitor.rb +59 -32
- data/lib/plumb/match_class.rb +5 -4
- data/lib/plumb/metadata.rb +5 -1
- data/lib/plumb/metadata_visitor.rb +13 -42
- data/lib/plumb/not.rb +4 -3
- data/lib/plumb/or.rb +10 -4
- data/lib/plumb/pipeline.rb +27 -7
- data/lib/plumb/policy.rb +10 -3
- data/lib/plumb/schema.rb +11 -10
- 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 +11 -11
- 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 +30 -1
- data/lib/plumb/value_class.rb +4 -3
- data/lib/plumb/version.rb +1 -1
- data/lib/plumb/visitor_handlers.rb +6 -0
- data/lib/plumb.rb +11 -5
- metadata +10 -3
- data/lib/plumb/steppable.rb +0 -229
@@ -0,0 +1,381 @@
|
|
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
|
+
def empty? = true
|
14
|
+
end
|
15
|
+
|
16
|
+
ParseError = Class.new(::TypeError)
|
17
|
+
Undefined = UndefinedClass.new.freeze
|
18
|
+
|
19
|
+
BLANK_STRING = ''
|
20
|
+
BLANK_ARRAY = [].freeze
|
21
|
+
BLANK_HASH = {}.freeze
|
22
|
+
BLANK_RESULT = Result.wrap(Undefined)
|
23
|
+
NOOP = ->(result) { result }
|
24
|
+
|
25
|
+
module Callable
|
26
|
+
def resolve(value = Undefined)
|
27
|
+
call(Result.wrap(value))
|
28
|
+
end
|
29
|
+
|
30
|
+
def parse(value = Undefined)
|
31
|
+
result = resolve(value)
|
32
|
+
raise ParseError, result.errors if result.invalid?
|
33
|
+
|
34
|
+
result.value
|
35
|
+
end
|
36
|
+
|
37
|
+
def call(result)
|
38
|
+
raise NotImplementedError, "Implement #call(Result) => Result in #{self.class}"
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
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
|
+
|
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.
|
51
|
+
def self.included(base)
|
52
|
+
nname = base.name.split('::').last
|
53
|
+
nname.gsub!(/([a-z\d])([A-Z])/, '\1_\2')
|
54
|
+
nname.downcase!
|
55
|
+
nname.gsub!(/_class$/, '')
|
56
|
+
nname = nname.to_sym
|
57
|
+
base.define_method(:node_name) { nname }
|
58
|
+
end
|
59
|
+
|
60
|
+
class Name
|
61
|
+
def initialize(name)
|
62
|
+
@name = name
|
63
|
+
end
|
64
|
+
|
65
|
+
def to_s = @name
|
66
|
+
|
67
|
+
def set(n)
|
68
|
+
@name = n
|
69
|
+
self
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def freeze
|
74
|
+
return self if frozen?
|
75
|
+
|
76
|
+
@name = Name.new(_inspect)
|
77
|
+
super
|
78
|
+
end
|
79
|
+
|
80
|
+
private def _inspect = self.class.name
|
81
|
+
|
82
|
+
def inspect = name.to_s
|
83
|
+
|
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
|
+
# A host object only needs to implement the Step interface `call(Result::Valid) => Result::Valid | Result::Invalid`
|
91
|
+
module Composable
|
92
|
+
include Callable
|
93
|
+
|
94
|
+
# This only runs when including Composable,
|
95
|
+
# not extending classes with it.
|
96
|
+
def self.included(base)
|
97
|
+
base.send(:include, Naming)
|
98
|
+
end
|
99
|
+
|
100
|
+
# Wrap an object in a Composable instance.
|
101
|
+
# Anything that includes Composable is a noop.
|
102
|
+
# A Hash is assumed to be a HashClass schema.
|
103
|
+
# Any `#call(Result) => Result` interface is wrapped in a Step.
|
104
|
+
# Anything else is assumed to be something you want to match against via `#===`.
|
105
|
+
#
|
106
|
+
# @example
|
107
|
+
# ten = Composable.wrap(10)
|
108
|
+
# ten.resolve(10) # => Result::Valid
|
109
|
+
# ten.resolve(11) # => Result::Invalid
|
110
|
+
#
|
111
|
+
# @param callable [Object]
|
112
|
+
# @return [Composable]
|
113
|
+
def self.wrap(callable)
|
114
|
+
if callable.is_a?(Composable)
|
115
|
+
callable
|
116
|
+
elsif callable.is_a?(::Hash)
|
117
|
+
HashClass.new(schema: callable)
|
118
|
+
elsif callable.respond_to?(:call)
|
119
|
+
Step.new(callable)
|
120
|
+
else
|
121
|
+
MatchClass.new(callable)
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
# A helper to wrap a block in a Step that will defer execution.
|
126
|
+
# This so that types can be used recursively in compositions.
|
127
|
+
# @example
|
128
|
+
# LinkedList = Types::Hash[
|
129
|
+
# value: Types::Any,
|
130
|
+
# next: Types::Any.defer { LinkedList }
|
131
|
+
# ]
|
132
|
+
def defer(definition = nil, &block)
|
133
|
+
Deferred.new(definition || block)
|
134
|
+
end
|
135
|
+
|
136
|
+
# Chain two composable objects together.
|
137
|
+
# A.K.A "and" or "sequence"
|
138
|
+
# @example
|
139
|
+
# Step1 >> Step2 >> Step3
|
140
|
+
#
|
141
|
+
# @param other [Composable]
|
142
|
+
# @return [And]
|
143
|
+
def >>(other)
|
144
|
+
And.new(self, Composable.wrap(other))
|
145
|
+
end
|
146
|
+
|
147
|
+
# Chain two composable objects together as a disjunction ("or").
|
148
|
+
#
|
149
|
+
# @param other [Composable]
|
150
|
+
# @return [Or]
|
151
|
+
def |(other)
|
152
|
+
Or.new(self, Composable.wrap(other))
|
153
|
+
end
|
154
|
+
|
155
|
+
# Transform value. Requires specifying the resulting type of the value after transformation.
|
156
|
+
# @example
|
157
|
+
# Types::String.transform(Types::Symbol, &:to_sym)
|
158
|
+
#
|
159
|
+
# @param target_type [Class] what type this step will transform the value to
|
160
|
+
# @param callable [#call, nil] a callable that will be applied to the value, or nil if block provided
|
161
|
+
# @param block [Proc] a block that will be applied to the value, or nil if callable provided
|
162
|
+
# @return [And]
|
163
|
+
def transform(target_type, callable = nil, &block)
|
164
|
+
self >> Transform.new(target_type, callable || block)
|
165
|
+
end
|
166
|
+
|
167
|
+
# Pass the value through an arbitrary validation
|
168
|
+
# @example
|
169
|
+
# type = Types::String.check('must start with "Role:"') { |value| value.start_with?('Role:') }
|
170
|
+
#
|
171
|
+
# @param errors [String] error message to use when validation fails
|
172
|
+
# @param block [Proc] a block that will be applied to the value
|
173
|
+
# @return [And]
|
174
|
+
def check(errors = 'did not pass the check', &block)
|
175
|
+
self >> MatchClass.new(block, error: errors, label: errors)
|
176
|
+
end
|
177
|
+
|
178
|
+
# Return a new Step with added metadata, or build step metadata if no argument is provided.
|
179
|
+
# @example
|
180
|
+
# type = Types::String.metadata(label: 'Name')
|
181
|
+
# type.metadata # => { type: String, label: 'Name' }
|
182
|
+
#
|
183
|
+
# @param data [Hash] metadata to add to the step
|
184
|
+
# @return [Hash, And]
|
185
|
+
def metadata(data = Undefined)
|
186
|
+
if data == Undefined
|
187
|
+
MetadataVisitor.call(self)
|
188
|
+
else
|
189
|
+
self >> Metadata.new(data)
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
# Negate the result of a step.
|
194
|
+
# Ie. if the step is valid, it will be invalid, and vice versa.
|
195
|
+
# @example
|
196
|
+
# type = Types::String.not
|
197
|
+
# type.resolve('foo') # invalid
|
198
|
+
# type.resolve(10) # valid
|
199
|
+
#
|
200
|
+
# @return [Not]
|
201
|
+
def not(other = self)
|
202
|
+
Not.new(other)
|
203
|
+
end
|
204
|
+
|
205
|
+
# Like #not, but with a custom error message.
|
206
|
+
#
|
207
|
+
# @option errors [String] error message to use when validation fails
|
208
|
+
# @return [Not]
|
209
|
+
def invalid(errors: nil)
|
210
|
+
Not.new(self, errors:)
|
211
|
+
end
|
212
|
+
|
213
|
+
# Match a value using `#==`
|
214
|
+
# Normally you'll build matchers via ``#[]`, which uses `#===`.
|
215
|
+
# Use this if you want to match against concrete instances of things that respond to `#===`
|
216
|
+
# @example
|
217
|
+
# regex = Types::Any.value(/foo/)
|
218
|
+
# regex.resolve('foo') # invalid. We're matching against the regex itself.
|
219
|
+
# regex.resolve(/foo/) # valid
|
220
|
+
#
|
221
|
+
# @param value [Object]
|
222
|
+
# @rerurn [And]
|
223
|
+
def value(val)
|
224
|
+
self >> ValueClass.new(val)
|
225
|
+
end
|
226
|
+
|
227
|
+
# Alias of `#[]`
|
228
|
+
# Match a value using `#===`
|
229
|
+
# @example
|
230
|
+
# email = Types::String['@']
|
231
|
+
#
|
232
|
+
# @param args [Array<Object>]
|
233
|
+
# @return [And]
|
234
|
+
def match(*args)
|
235
|
+
self >> MatchClass.new(*args)
|
236
|
+
end
|
237
|
+
|
238
|
+
def [](val) = match(val)
|
239
|
+
|
240
|
+
# Support #as_node.
|
241
|
+
class Node
|
242
|
+
include Composable
|
243
|
+
|
244
|
+
attr_reader :node_name, :type, :attributes
|
245
|
+
|
246
|
+
def initialize(node_name, type, attributes = BLANK_HASH)
|
247
|
+
@node_name = node_name
|
248
|
+
@type = type
|
249
|
+
@attributes = attributes
|
250
|
+
freeze
|
251
|
+
end
|
252
|
+
|
253
|
+
def call(result) = type.call(result)
|
254
|
+
end
|
255
|
+
|
256
|
+
# Wrap a Step in a node with a custom #node_name
|
257
|
+
# which is expected by visitors.
|
258
|
+
# So that we can define special visitors for certain compositions.
|
259
|
+
# Ex. Types::Boolean is a compoition of Types::True | Types::False, but we want to treat it as a single node.
|
260
|
+
#
|
261
|
+
# @param node_name [Symbol]
|
262
|
+
# @param metadata [Hash]
|
263
|
+
# @return [Node]
|
264
|
+
def as_node(node_name, metadata = BLANK_HASH)
|
265
|
+
Node.new(node_name, self, metadata)
|
266
|
+
end
|
267
|
+
|
268
|
+
# Register a policy for this step.
|
269
|
+
# Mode 1.a: #policy(:name, arg) a single policy with an argument
|
270
|
+
# Mode 1.b: #policy(:name) a single policy without an argument
|
271
|
+
# Mode 2: #policy(p1: value, p2: value) multiple policies with arguments
|
272
|
+
# The latter mode will be expanded to multiple #policy calls.
|
273
|
+
# @return [Step]
|
274
|
+
def policy(*args, &blk)
|
275
|
+
case args
|
276
|
+
in [::Symbol => name, *rest] # #policy(:name, arg)
|
277
|
+
types = Array(metadata[:type]).uniq
|
278
|
+
|
279
|
+
bargs = [self]
|
280
|
+
arg = Undefined
|
281
|
+
if rest.size.positive?
|
282
|
+
bargs << rest.first
|
283
|
+
arg = rest.first
|
284
|
+
end
|
285
|
+
block = Plumb.policies.get(types, name)
|
286
|
+
pol = block.call(*bargs, &blk)
|
287
|
+
|
288
|
+
Policy.new(name, arg, pol)
|
289
|
+
in [::Hash => opts] # #policy(p1: value, p2: value)
|
290
|
+
opts.reduce(self) { |step, (name, value)| step.policy(name, value) }
|
291
|
+
else
|
292
|
+
raise ArgumentError, "expected a symbol or hash, got #{args.inspect}"
|
293
|
+
end
|
294
|
+
end
|
295
|
+
|
296
|
+
# `#===` equality. So that Plumb steps can be used in case statements and pattern matching.
|
297
|
+
# @param other [Object]
|
298
|
+
# @return [Boolean]
|
299
|
+
def ===(other)
|
300
|
+
case other
|
301
|
+
when Composable
|
302
|
+
other == self
|
303
|
+
else
|
304
|
+
resolve(other).valid?
|
305
|
+
end
|
306
|
+
end
|
307
|
+
|
308
|
+
def ==(other)
|
309
|
+
other.is_a?(self.class) && other.respond_to?(:children) && other.children == children
|
310
|
+
end
|
311
|
+
|
312
|
+
# Visitors expect a #node_name and #children interface.
|
313
|
+
# @return [Array<Composable>]
|
314
|
+
def children = BLANK_ARRAY
|
315
|
+
|
316
|
+
# Compose a step that instantiates a class.
|
317
|
+
# @example
|
318
|
+
# type = Types::String.build(MyClass, :new)
|
319
|
+
# thing = type.parse('foo') # same as MyClass.new('foo')
|
320
|
+
#
|
321
|
+
# It sets the class as the output type of the step.
|
322
|
+
# Optionally takes a block.
|
323
|
+
#
|
324
|
+
# type = Types::String.build(Money) { |value| Monetize.parse(value) }
|
325
|
+
#
|
326
|
+
# @param cns [Class] constructor class or object.
|
327
|
+
# @param factory_method [Symbol] method to call on the class to instantiate it.
|
328
|
+
# @return [And]
|
329
|
+
def build(cns, factory_method = :new, &block)
|
330
|
+
self >> Build.new(cns, factory_method:, &block)
|
331
|
+
end
|
332
|
+
|
333
|
+
# Build a Plumb::Pipeline with this object as the starting step.
|
334
|
+
# @example
|
335
|
+
# pipe = Types::Data[name: String].pipeline do |pl|
|
336
|
+
# pl.step Validate
|
337
|
+
# pl.step Debug
|
338
|
+
# pl.step Log
|
339
|
+
# end
|
340
|
+
#
|
341
|
+
# @return [Pipeline]
|
342
|
+
def pipeline(&block)
|
343
|
+
Pipeline.new(self, &block)
|
344
|
+
end
|
345
|
+
|
346
|
+
def to_s
|
347
|
+
inspect
|
348
|
+
end
|
349
|
+
|
350
|
+
# @option root [Boolean] whether to include JSON Schema $schema property
|
351
|
+
# @return [Hash]
|
352
|
+
def to_json_schema(root: false)
|
353
|
+
JSONSchemaVisitor.call(self, root:)
|
354
|
+
end
|
355
|
+
|
356
|
+
# Build a step that will invoke one or more methods on the value.
|
357
|
+
# Ex 1: Types::String.invoke(:downcase)
|
358
|
+
# Ex 2: Types::Array.invoke(:[], 1)
|
359
|
+
# Ex 3 chain of methods: Types::String.invoke([:downcase, :to_sym])
|
360
|
+
# @return [Step]
|
361
|
+
def invoke(*args, &block)
|
362
|
+
case args
|
363
|
+
in [::Symbol => method_name, *rest]
|
364
|
+
self >> Step.new(
|
365
|
+
->(result) { result.valid(result.value.public_send(method_name, *rest, &block)) },
|
366
|
+
[method_name.inspect, rest.inspect].join(' ')
|
367
|
+
)
|
368
|
+
in [Array => methods] if methods.all? { |m| m.is_a?(Symbol) }
|
369
|
+
methods.reduce(self) { |step, method| step.invoke(method) }
|
370
|
+
else
|
371
|
+
raise ArgumentError, "expected a symbol or array of symbols, got #{args.inspect}"
|
372
|
+
end
|
373
|
+
end
|
374
|
+
end
|
375
|
+
end
|
376
|
+
|
377
|
+
require 'plumb/deferred'
|
378
|
+
require 'plumb/transform'
|
379
|
+
require 'plumb/policy'
|
380
|
+
require 'plumb/build'
|
381
|
+
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
|
|
@@ -31,7 +33,7 @@ module Plumb
|
|
31
33
|
in [::Hash => hash]
|
32
34
|
self.class.new(schema: _schema.merge(wrap_keys_and_values(hash)), inclusive: @inclusive)
|
33
35
|
in [key_type, value_type]
|
34
|
-
HashMap.new(
|
36
|
+
HashMap.new(Composable.wrap(key_type), Composable.wrap(value_type))
|
35
37
|
else
|
36
38
|
raise ::ArgumentError, "unexpected value to Types::Hash#schema #{args.inspect}"
|
37
39
|
end
|
@@ -44,9 +46,14 @@ module Plumb
|
|
44
46
|
# we need to keep the right-side key, because even if the key name is the same,
|
45
47
|
# it's optional flag might have changed
|
46
48
|
def +(other)
|
47
|
-
|
48
|
-
|
49
|
-
|
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)
|
50
57
|
end
|
51
58
|
|
52
59
|
def &(other)
|
@@ -97,7 +104,7 @@ module Plumb
|
|
97
104
|
end
|
98
105
|
|
99
106
|
def call(result)
|
100
|
-
return result.invalid(errors:
|
107
|
+
return result.invalid(errors: NOT_A_HASH) unless result.value.is_a?(::Hash)
|
101
108
|
return result unless _schema.any?
|
102
109
|
|
103
110
|
input = result.value
|
@@ -121,6 +128,10 @@ module Plumb
|
|
121
128
|
errors.any? ? result.invalid(output, errors:) : result.valid(output)
|
122
129
|
end
|
123
130
|
|
131
|
+
def ==(other)
|
132
|
+
other.is_a?(self.class) && other._schema == _schema
|
133
|
+
end
|
134
|
+
|
124
135
|
private
|
125
136
|
|
126
137
|
def _inspect
|
@@ -138,7 +149,7 @@ module Plumb
|
|
138
149
|
when Callable
|
139
150
|
hash
|
140
151
|
else # leaf values
|
141
|
-
|
152
|
+
Composable.wrap(hash)
|
142
153
|
end
|
143
154
|
end
|
144
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) }
|