callable_tree 0.3.7 → 0.3.9
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/.github/dependabot.yml +7 -0
- data/.github/workflows/build.yml +3 -5
- data/.github/workflows/codeql-analysis.yml +4 -4
- data/.rubocop.yml +6 -0
- data/.ruby-version +1 -1
- data/CHANGELOG.md +12 -0
- data/Gemfile.lock +3 -3
- data/README.md +460 -276
- data/callable_tree.gemspec +1 -0
- data/examples/builder/external-verbosify.rb +87 -0
- data/examples/builder/hooks.rb +67 -0
- data/examples/builder/identity.rb +91 -0
- data/examples/builder/internal-broadcastable.rb +11 -11
- data/examples/builder/internal-composable.rb +11 -11
- data/examples/builder/internal-seekable.rb +9 -15
- data/examples/builder/logging.rb +36 -39
- data/examples/{external-verbosify.rb → class/external-verbosify.rb} +3 -5
- data/examples/class/hooks.rb +70 -0
- data/examples/{identity.rb → class/identity.rb} +3 -5
- data/examples/{internal-broadcastable.rb → class/internal-broadcastable.rb} +0 -0
- data/examples/{internal-composable.rb → class/internal-composable.rb} +0 -0
- data/examples/{internal-seekable.rb → class/internal-seekable.rb} +3 -5
- data/examples/{logging.rb → class/logging.rb} +47 -47
- data/lib/callable_tree/node/builder.rb +13 -0
- data/lib/callable_tree/node/hooks/terminator.rb +99 -0
- data/lib/callable_tree/node/internal/strategy/broadcast.rb +19 -2
- data/lib/callable_tree/node/internal/strategy/compose.rb +15 -3
- data/lib/callable_tree/node/internal/strategy/seek.rb +11 -1
- data/lib/callable_tree/node/internal/strategy.rb +10 -2
- data/lib/callable_tree/node/internal.rb +22 -28
- data/lib/callable_tree/node/root.rb +1 -0
- data/lib/callable_tree/version.rb +1 -1
- data/lib/callable_tree.rb +1 -0
- metadata +17 -11
- data/examples/builder/hooks-caller.rb +0 -38
- data/examples/hooks-caller.rb +0 -39
data/README.md
CHANGED
@@ -21,10 +21,10 @@ Or install it yourself as:
|
|
21
21
|
|
22
22
|
## Usage
|
23
23
|
|
24
|
-
Builds a tree by linking
|
24
|
+
Builds a tree by linking `CallableTree` node instances. The `call` methods of the nodes where the `match?` method returns a truthy value are called in a chain from the root node to the leaf node.
|
25
25
|
|
26
26
|
- `CallableTree::Node::Internal`
|
27
|
-
- This `module` is used to define a node that can have child nodes.
|
27
|
+
- This `module` is used to define a node that can have child nodes. This node has several strategies (`seekable`, `broadcastable`, `composable`).
|
28
28
|
- `CallableTree::Node::External`
|
29
29
|
- This `module` is used to define a leaf node that cannot have child nodes.
|
30
30
|
- `CallableTree::Node::Root`
|
@@ -32,11 +32,15 @@ Builds a tree by linking instances of the nodes. The `call` method of the node w
|
|
32
32
|
|
33
33
|
### Basic
|
34
34
|
|
35
|
-
|
35
|
+
There are two ways to define the nodes: class style and builder style.
|
36
|
+
|
37
|
+
#### `CallableTree::Node::Internal#seekable` (default strategy)
|
36
38
|
|
37
39
|
This strategy does not call the next sibling node if the `call` method of the current node returns a value other than `nil`. This behavior is changeable by overriding the `terminate?` method.
|
38
40
|
|
39
|
-
|
41
|
+
##### Class style
|
42
|
+
|
43
|
+
`examples/class/internal-seekable.rb`:
|
40
44
|
```ruby
|
41
45
|
module Node
|
42
46
|
module JSON
|
@@ -77,8 +81,7 @@ module Node
|
|
77
81
|
|
78
82
|
def call(input, **_options)
|
79
83
|
input[@type.to_s]
|
80
|
-
.
|
81
|
-
.to_h
|
84
|
+
.to_h { |element| [element['name'], element['emoji']] }
|
82
85
|
end
|
83
86
|
end
|
84
87
|
end
|
@@ -122,8 +125,7 @@ module Node
|
|
122
125
|
input
|
123
126
|
.get_elements("//#{@type}")
|
124
127
|
.first
|
125
|
-
.
|
126
|
-
.to_h
|
128
|
+
.to_h { |element| [element['name'], element['emoji']] }
|
127
129
|
end
|
128
130
|
end
|
129
131
|
end
|
@@ -148,9 +150,109 @@ Dir.glob("#{__dir__}/docs/*") do |file|
|
|
148
150
|
end
|
149
151
|
```
|
150
152
|
|
151
|
-
Run `examples/internal-seekable.rb`:
|
153
|
+
Run `examples/class/internal-seekable.rb`:
|
154
|
+
```sh
|
155
|
+
% ruby examples/class/internal-seekable.rb
|
156
|
+
{"Dog"=>"🐶", "Cat"=>"🐱"}
|
157
|
+
---
|
158
|
+
{"Dog"=>"🐶", "Cat"=>"🐱"}
|
159
|
+
---
|
160
|
+
{"Red Apple"=>"🍎", "Green Apple"=>"🍏"}
|
161
|
+
---
|
162
|
+
{"Red Apple"=>"🍎", "Green Apple"=>"🍏"}
|
163
|
+
---
|
164
|
+
```
|
165
|
+
|
166
|
+
##### Builder style
|
167
|
+
|
168
|
+
`examples/builder/internal-seekable.rb`:
|
169
|
+
```ruby
|
170
|
+
JSONParser =
|
171
|
+
CallableTree::Node::Internal::Builder
|
172
|
+
.new
|
173
|
+
.matcher do |input, **_options|
|
174
|
+
File.extname(input) == '.json'
|
175
|
+
end
|
176
|
+
.caller do |input, **options, &original|
|
177
|
+
File.open(input) do |file|
|
178
|
+
json = ::JSON.load(file)
|
179
|
+
# The following block call is equivalent to calling `super` in the class style.
|
180
|
+
original.call(json, **options)
|
181
|
+
end
|
182
|
+
end
|
183
|
+
.terminator { true }
|
184
|
+
.build
|
185
|
+
|
186
|
+
XMLParser =
|
187
|
+
CallableTree::Node::Internal::Builder
|
188
|
+
.new
|
189
|
+
.matcher do |input, **_options|
|
190
|
+
File.extname(input) == '.xml'
|
191
|
+
end
|
192
|
+
.caller do |input, **options, &original|
|
193
|
+
File.open(input) do |file|
|
194
|
+
# The following block call is equivalent to calling `super` in the class style.
|
195
|
+
original.call(REXML::Document.new(file), **options)
|
196
|
+
end
|
197
|
+
end
|
198
|
+
.terminator { true }
|
199
|
+
.build
|
200
|
+
|
201
|
+
def build_json_scraper(type)
|
202
|
+
CallableTree::Node::External::Builder
|
203
|
+
.new
|
204
|
+
.matcher do |input, **_options|
|
205
|
+
!!input[type.to_s]
|
206
|
+
end
|
207
|
+
.caller do |input, **_options|
|
208
|
+
input[type.to_s]
|
209
|
+
.to_h { |element| [element['name'], element['emoji']] }
|
210
|
+
end
|
211
|
+
.build
|
212
|
+
end
|
213
|
+
|
214
|
+
AnimalsJSONScraper = build_json_scraper(:animals)
|
215
|
+
FruitsJSONScraper = build_json_scraper(:fruits)
|
216
|
+
|
217
|
+
def build_xml_scraper(type)
|
218
|
+
CallableTree::Node::External::Builder
|
219
|
+
.new
|
220
|
+
.matcher do |input, **_options|
|
221
|
+
!input.get_elements("//#{type}").empty?
|
222
|
+
end
|
223
|
+
.caller do |input, **_options|
|
224
|
+
input
|
225
|
+
.get_elements("//#{type}")
|
226
|
+
.first
|
227
|
+
.to_h { |element| [element['name'], element['emoji']] }
|
228
|
+
end
|
229
|
+
.build
|
230
|
+
end
|
231
|
+
|
232
|
+
AnimalsXMLScraper = build_xml_scraper(:animals)
|
233
|
+
FruitsXMLScraper = build_xml_scraper(:fruits)
|
234
|
+
|
235
|
+
tree = CallableTree::Node::Root.new.seekable.append(
|
236
|
+
JSONParser.new.seekable.append(
|
237
|
+
AnimalsJSONScraper.new,
|
238
|
+
FruitsJSONScraper.new
|
239
|
+
),
|
240
|
+
XMLParser.new.seekable.append(
|
241
|
+
AnimalsXMLScraper.new,
|
242
|
+
FruitsXMLScraper.new
|
243
|
+
)
|
244
|
+
)
|
245
|
+
|
246
|
+
Dir.glob("#{__dir__}/../docs/*") do |file|
|
247
|
+
options = { foo: :bar }
|
248
|
+
pp tree.call(file, **options)
|
249
|
+
puts '---'
|
250
|
+
end
|
251
|
+
```
|
252
|
+
|
253
|
+
Run `examples/builder/internal-seekable.rb`:
|
152
254
|
```sh
|
153
|
-
% ruby examples/internal-seekable.rb
|
255
|
+
% ruby examples/builder/internal-seekable.rb
|
154
256
|
{"Dog"=>"🐶", "Cat"=>"🐱"}
|
155
257
|
---
|
156
258
|
{"Dog"=>"🐶", "Cat"=>"🐱"}
|
@@ -163,9 +265,11 @@ Run `examples/internal-seekable.rb`:
|
|
163
265
|
|
164
266
|
#### `CallableTree::Node::Internal#broadcastable`
|
165
267
|
|
166
|
-
This strategy
|
268
|
+
This strategy broadcasts to output a result of the child nodes as array. It also ignores their `terminate?` methods by default.
|
167
269
|
|
168
|
-
|
270
|
+
##### Class style
|
271
|
+
|
272
|
+
`examples/class/internal-broadcastable.rb`:
|
169
273
|
```ruby
|
170
274
|
module Node
|
171
275
|
class LessThan
|
@@ -199,9 +303,99 @@ end
|
|
199
303
|
|
200
304
|
```
|
201
305
|
|
202
|
-
Run `examples/internal-broadcastable.rb`:
|
306
|
+
Run `examples/class/internal-broadcastable.rb`:
|
307
|
+
```sh
|
308
|
+
% ruby examples/class/internal-broadcastable.rb
|
309
|
+
0 -> [[0, 1], [0, -1]]
|
310
|
+
1 -> [[2, 2], [3, 0]]
|
311
|
+
2 -> [[4, 3], [6, 1]]
|
312
|
+
3 -> [[6, 4], [9, 2]]
|
313
|
+
4 -> [[8, 5], [12, 3]]
|
314
|
+
5 -> [nil, [15, 4]]
|
315
|
+
6 -> [nil, [18, 5]]
|
316
|
+
7 -> [nil, [21, 6]]
|
317
|
+
8 -> [nil, [24, 7]]
|
318
|
+
9 -> [nil, [27, 8]]
|
319
|
+
10 -> [nil, nil]
|
320
|
+
```
|
321
|
+
|
322
|
+
##### Builder style
|
323
|
+
|
324
|
+
`examples/builder/internal-broadcastable.rb`:
|
325
|
+
```ruby
|
326
|
+
def less_than(num)
|
327
|
+
# The following block call is equivalent to calling `super` in the class style.
|
328
|
+
proc { |input, &original| original.call(input) && input < num }
|
329
|
+
end
|
330
|
+
|
331
|
+
LessThan5 =
|
332
|
+
CallableTree::Node::Internal::Builder
|
333
|
+
.new
|
334
|
+
.matcher(&method(:less_than).call(5))
|
335
|
+
.build
|
336
|
+
|
337
|
+
LessThan10 =
|
338
|
+
CallableTree::Node::Internal::Builder
|
339
|
+
.new
|
340
|
+
.matcher(&method(:less_than).call(10))
|
341
|
+
.build
|
342
|
+
|
343
|
+
def add(num)
|
344
|
+
proc { |input| input + num }
|
345
|
+
end
|
346
|
+
|
347
|
+
Add1 =
|
348
|
+
CallableTree::Node::External::Builder
|
349
|
+
.new
|
350
|
+
.caller(&method(:add).call(1))
|
351
|
+
.build
|
352
|
+
|
353
|
+
def subtract(num)
|
354
|
+
proc { |input| input - num }
|
355
|
+
end
|
356
|
+
|
357
|
+
Subtract1 =
|
358
|
+
CallableTree::Node::External::Builder
|
359
|
+
.new
|
360
|
+
.caller(&method(:subtract).call(1))
|
361
|
+
.build
|
362
|
+
|
363
|
+
def multiply(num)
|
364
|
+
proc { |input| input * num }
|
365
|
+
end
|
366
|
+
|
367
|
+
Multiply2 =
|
368
|
+
CallableTree::Node::External::Builder
|
369
|
+
.new
|
370
|
+
.caller(&method(:multiply).call(2))
|
371
|
+
.build
|
372
|
+
|
373
|
+
Multiply3 =
|
374
|
+
CallableTree::Node::External::Builder
|
375
|
+
.new
|
376
|
+
.caller(&method(:multiply).call(3))
|
377
|
+
.build
|
378
|
+
|
379
|
+
tree = CallableTree::Node::Root.new.broadcastable.append(
|
380
|
+
LessThan5.new.broadcastable.append(
|
381
|
+
Multiply2.new,
|
382
|
+
Add1.new
|
383
|
+
),
|
384
|
+
LessThan10.new.broadcastable.append(
|
385
|
+
Multiply3.new,
|
386
|
+
Subtract1.new
|
387
|
+
)
|
388
|
+
)
|
389
|
+
|
390
|
+
(0..10).each do |input|
|
391
|
+
output = tree.call(input)
|
392
|
+
puts "#{input} -> #{output}"
|
393
|
+
end
|
394
|
+
```
|
395
|
+
|
396
|
+
Run `examples/builder/internal-broadcastable.rb`:
|
203
397
|
```sh
|
204
|
-
% ruby examples/internal-broadcastable.rb
|
398
|
+
% ruby examples/builder/internal-broadcastable.rb
|
205
399
|
0 -> [[0, 1], [0, -1]]
|
206
400
|
1 -> [[2, 2], [3, 0]]
|
207
401
|
2 -> [[4, 3], [6, 1]]
|
@@ -217,9 +411,12 @@ Run `examples/internal-broadcastable.rb`:
|
|
217
411
|
|
218
412
|
#### `CallableTree::Node::Internal#composable`
|
219
413
|
|
220
|
-
This strategy
|
414
|
+
This strategy composes the child nodes to input the output of the previous node into the next node and to output a result.
|
415
|
+
It also ignores their `terminate?` methods by default.
|
416
|
+
|
417
|
+
##### Class style
|
221
418
|
|
222
|
-
`examples/internal-composable.rb`:
|
419
|
+
`examples/class/internal-composable.rb`:
|
223
420
|
```ruby
|
224
421
|
module Node
|
225
422
|
class LessThan
|
@@ -253,9 +450,99 @@ end
|
|
253
450
|
|
254
451
|
```
|
255
452
|
|
256
|
-
Run `examples/internal-composable.rb`:
|
453
|
+
Run `examples/class/internal-composable.rb`:
|
454
|
+
```sh
|
455
|
+
% ruby examples/class/internal-composable.rb
|
456
|
+
0 -> 2
|
457
|
+
1 -> 8
|
458
|
+
2 -> 14
|
459
|
+
3 -> 20
|
460
|
+
4 -> 26
|
461
|
+
5 -> 14
|
462
|
+
6 -> 17
|
463
|
+
7 -> 20
|
464
|
+
8 -> 23
|
465
|
+
9 -> 26
|
466
|
+
10 -> 10
|
467
|
+
```
|
468
|
+
|
469
|
+
##### Builder style
|
470
|
+
|
471
|
+
`examples/builder/internal-composable.rb`:
|
472
|
+
```ruby
|
473
|
+
def less_than(num)
|
474
|
+
# The following block call is equivalent to calling `super` in the class style.
|
475
|
+
proc { |input, &original| original.call(input) && input < num }
|
476
|
+
end
|
477
|
+
|
478
|
+
LessThan5 =
|
479
|
+
CallableTree::Node::Internal::Builder
|
480
|
+
.new
|
481
|
+
.matcher(&method(:less_than).call(5))
|
482
|
+
.build
|
483
|
+
|
484
|
+
LessThan10 =
|
485
|
+
CallableTree::Node::Internal::Builder
|
486
|
+
.new
|
487
|
+
.matcher(&method(:less_than).call(10))
|
488
|
+
.build
|
489
|
+
|
490
|
+
def add(num)
|
491
|
+
proc { |input| input + num }
|
492
|
+
end
|
493
|
+
|
494
|
+
Add1 =
|
495
|
+
CallableTree::Node::External::Builder
|
496
|
+
.new
|
497
|
+
.caller(&method(:add).call(1))
|
498
|
+
.build
|
499
|
+
|
500
|
+
def subtract(num)
|
501
|
+
proc { |input| input - num }
|
502
|
+
end
|
503
|
+
|
504
|
+
Subtract1 =
|
505
|
+
CallableTree::Node::External::Builder
|
506
|
+
.new
|
507
|
+
.caller(&method(:subtract).call(1))
|
508
|
+
.build
|
509
|
+
|
510
|
+
def multiply(num)
|
511
|
+
proc { |input| input * num }
|
512
|
+
end
|
513
|
+
|
514
|
+
Multiply2 =
|
515
|
+
CallableTree::Node::External::Builder
|
516
|
+
.new
|
517
|
+
.caller(&method(:multiply).call(2))
|
518
|
+
.build
|
519
|
+
|
520
|
+
Multiply3 =
|
521
|
+
CallableTree::Node::External::Builder
|
522
|
+
.new
|
523
|
+
.caller(&method(:multiply).call(3))
|
524
|
+
.build
|
525
|
+
|
526
|
+
tree = CallableTree::Node::Root.new.composable.append(
|
527
|
+
LessThan5.new.composable.append(
|
528
|
+
Multiply2.new,
|
529
|
+
Add1.new
|
530
|
+
),
|
531
|
+
LessThan10.new.composable.append(
|
532
|
+
Multiply3.new,
|
533
|
+
Subtract1.new
|
534
|
+
)
|
535
|
+
)
|
536
|
+
|
537
|
+
(0..10).each do |input|
|
538
|
+
output = tree.call(input)
|
539
|
+
puts "#{input} -> #{output}"
|
540
|
+
end
|
541
|
+
```
|
542
|
+
|
543
|
+
Run `examples/builder/internal-composable.rb`:
|
257
544
|
```sh
|
258
|
-
% ruby examples/internal-composable.rb
|
545
|
+
% ruby examples/builder/internal-composable.rb
|
259
546
|
0 -> 2
|
260
547
|
1 -> 8
|
261
548
|
2 -> 14
|
@@ -275,350 +562,247 @@ Run `examples/internal-composable.rb`:
|
|
275
562
|
|
276
563
|
If you want verbose output results, call this method.
|
277
564
|
|
278
|
-
`examples/external-verbosify.rb`:
|
565
|
+
`examples/builder/external-verbosify.rb`:
|
279
566
|
```ruby
|
280
567
|
...
|
281
568
|
|
282
|
-
tree = CallableTree::Node::Root.new.append(
|
283
|
-
|
284
|
-
|
285
|
-
|
569
|
+
tree = CallableTree::Node::Root.new.seekable.append(
|
570
|
+
JSONParser.new.seekable.append(
|
571
|
+
AnimalsJSONScraper.new.verbosify,
|
572
|
+
FruitsJSONScraper.new.verbosify
|
286
573
|
),
|
287
|
-
|
288
|
-
|
289
|
-
|
574
|
+
XMLParser.new.seekable.append(
|
575
|
+
AnimalsXMLScraper.new.verbosify,
|
576
|
+
FruitsXMLScraper.new.verbosify
|
290
577
|
)
|
291
578
|
)
|
292
579
|
|
293
580
|
...
|
294
581
|
```
|
295
582
|
|
296
|
-
Run `examples/external-verbosify.rb`:
|
583
|
+
Run `examples/builder/external-verbosify.rb`:
|
297
584
|
```sh
|
298
|
-
% ruby examples/external-verbosify.rb
|
585
|
+
% ruby examples/class/external-verbosify.rb
|
299
586
|
#<struct CallableTree::Node::External::Output
|
300
|
-
|
301
|
-
|
302
|
-
|
587
|
+
value={"Dog"=>"🐶", "Cat"=>"🐱"},
|
588
|
+
options={:foo=>:bar},
|
589
|
+
routes=[AnimalsJSONScraper, JSONParser, CallableTree::Node::Root]>
|
303
590
|
---
|
304
591
|
#<struct CallableTree::Node::External::Output
|
305
|
-
|
306
|
-
|
307
|
-
|
592
|
+
value={"Dog"=>"🐶", "Cat"=>"🐱"},
|
593
|
+
options={:foo=>:bar},
|
594
|
+
routes=[AnimalsXMLScraper, XMLParser, CallableTree::Node::Root]>
|
308
595
|
---
|
309
596
|
#<struct CallableTree::Node::External::Output
|
310
|
-
|
311
|
-
|
312
|
-
|
597
|
+
value={"Red Apple"=>"🍎", "Green Apple"=>"🍏"},
|
598
|
+
options={:foo=>:bar},
|
599
|
+
routes=[FruitsJSONScraper, JSONParser, CallableTree::Node::Root]>
|
313
600
|
---
|
314
601
|
#<struct CallableTree::Node::External::Output
|
315
|
-
|
316
|
-
|
317
|
-
|
602
|
+
value={"Red Apple"=>"🍎", "Green Apple"=>"🍏"},
|
603
|
+
options={:foo=>:bar},
|
604
|
+
routes=[FruitsXMLScraper, XMLParser, CallableTree::Node::Root]>
|
318
605
|
---
|
319
606
|
```
|
320
607
|
|
321
|
-
|
322
|
-
You can work around it by overriding the `identity` method of the node.
|
323
|
-
|
324
|
-
#### `CallableTree::Node#identity`
|
608
|
+
#### Logging
|
325
609
|
|
326
|
-
|
610
|
+
This is an example of logging.
|
327
611
|
|
328
|
-
`examples/
|
612
|
+
`examples/builder/logging.rb`:
|
329
613
|
```ruby
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
@type = type
|
337
|
-
end
|
614
|
+
JSONParser =
|
615
|
+
CallableTree::Node::Internal::Builder
|
616
|
+
.new
|
617
|
+
...
|
618
|
+
.hookable
|
619
|
+
.build
|
338
620
|
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
621
|
+
XMLParser =
|
622
|
+
CallableTree::Node::Internal::Builder
|
623
|
+
.new
|
624
|
+
...
|
625
|
+
.hookable
|
626
|
+
.build
|
343
627
|
|
344
|
-
|
628
|
+
def build_json_scraper(type)
|
629
|
+
CallableTree::Node::External::Builder
|
630
|
+
.new
|
345
631
|
...
|
632
|
+
.hookable
|
633
|
+
.build
|
634
|
+
end
|
346
635
|
|
347
|
-
|
348
|
-
include CallableTree::Node::External
|
636
|
+
...
|
349
637
|
|
350
|
-
|
351
|
-
|
352
|
-
|
638
|
+
def build_xml_scraper(type)
|
639
|
+
CallableTree::Node::External::Builder
|
640
|
+
.new
|
641
|
+
...
|
642
|
+
.hookable
|
643
|
+
.build
|
644
|
+
end
|
353
645
|
|
354
|
-
|
355
|
-
Identity.new(klass: super, type: @type)
|
356
|
-
end
|
646
|
+
...
|
357
647
|
|
358
|
-
|
648
|
+
module Logging
|
649
|
+
INDENT_SIZE = 2
|
650
|
+
BLANK = ' '
|
651
|
+
LIST_STYLE = '*'
|
652
|
+
INPUT_LABEL = 'Input :'
|
653
|
+
OUTPUT_LABEL = 'Output:'
|
654
|
+
|
655
|
+
def self.loggable(node)
|
656
|
+
node.after_matcher! do |matched, _node_:, **|
|
657
|
+
prefix = LIST_STYLE.rjust((_node_.depth * INDENT_SIZE) - INDENT_SIZE + LIST_STYLE.length, BLANK)
|
658
|
+
puts "#{prefix} #{_node_.identity}: [matched: #{matched}]"
|
659
|
+
matched
|
359
660
|
end
|
360
|
-
end
|
361
|
-
|
362
|
-
module XML
|
363
|
-
...
|
364
|
-
|
365
|
-
class Scraper
|
366
|
-
include CallableTree::Node::External
|
367
|
-
|
368
|
-
def initialize(type:)
|
369
|
-
@type = type
|
370
|
-
end
|
371
|
-
|
372
|
-
def identity
|
373
|
-
Identity.new(klass: super, type: @type)
|
374
|
-
end
|
375
661
|
|
376
|
-
|
662
|
+
if node.external?
|
663
|
+
node
|
664
|
+
.before_caller! do |input, *, _node_:, **|
|
665
|
+
input_prefix = INPUT_LABEL.rjust((_node_.depth * INDENT_SIZE) + INPUT_LABEL.length, BLANK)
|
666
|
+
puts "#{input_prefix} #{input}"
|
667
|
+
input
|
668
|
+
end
|
669
|
+
.after_caller! do |output, _node_:, **|
|
670
|
+
output_prefix = OUTPUT_LABEL.rjust((_node_.depth * INDENT_SIZE) + OUTPUT_LABEL.length, BLANK)
|
671
|
+
puts "#{output_prefix} #{output}"
|
672
|
+
output
|
673
|
+
end
|
377
674
|
end
|
378
675
|
end
|
379
676
|
end
|
380
677
|
|
678
|
+
loggable = Logging.method(:loggable)
|
679
|
+
|
680
|
+
tree = CallableTree::Node::Root.new.seekable.append(
|
681
|
+
JSONParser.new.tap(&loggable).seekable.append(
|
682
|
+
AnimalsJSONScraper.new.tap(&loggable).verbosify,
|
683
|
+
FruitsJSONScraper.new.tap(&loggable).verbosify
|
684
|
+
),
|
685
|
+
XMLParser.new.tap(&loggable).seekable.append(
|
686
|
+
AnimalsXMLScraper.new.tap(&loggable).verbosify,
|
687
|
+
FruitsXMLScraper.new.tap(&loggable).verbosify
|
688
|
+
)
|
689
|
+
)
|
690
|
+
|
381
691
|
...
|
382
692
|
```
|
383
693
|
|
384
|
-
|
694
|
+
Also, see `examples/builder/hooks.rb` for detail about `CallableTree::Node::Hooks::*`.
|
695
|
+
|
696
|
+
Run `examples/builder/logging.rb`:
|
385
697
|
```sh
|
386
|
-
% ruby examples/
|
698
|
+
% ruby examples/builder/logging.rb
|
699
|
+
* JSONParser: [matched: true]
|
700
|
+
* AnimalsJSONScraper: [matched: true]
|
701
|
+
Input : {"animals"=>[{"name"=>"Dog", "emoji"=>"🐶"}, {"name"=>"Cat", "emoji"=>"🐱"}]}
|
702
|
+
Output: {"Dog"=>"🐶", "Cat"=>"🐱"}
|
387
703
|
#<struct CallableTree::Node::External::Output
|
388
704
|
value={"Dog"=>"🐶", "Cat"=>"🐱"},
|
389
705
|
options={:foo=>:bar},
|
390
|
-
routes=
|
391
|
-
[#<Node::Identity:0x00007fb4378a9718
|
392
|
-
@klass=Node::JSON::Scraper,
|
393
|
-
@type=:animals>,
|
394
|
-
Node::JSON::Parser,
|
395
|
-
CallableTree::Node::Root]>
|
706
|
+
routes=[AnimalsJSONScraper, JSONParser, CallableTree::Node::Root]>
|
396
707
|
---
|
708
|
+
* JSONParser: [matched: false]
|
709
|
+
* XMLParser: [matched: true]
|
710
|
+
* AnimalsXMLScraper: [matched: true]
|
711
|
+
Input : <root><animals><animal emoji='🐶' name='Dog'/><animal emoji='🐱' name='Cat'/></animals></root>
|
712
|
+
Output: {"Dog"=>"🐶", "Cat"=>"🐱"}
|
397
713
|
#<struct CallableTree::Node::External::Output
|
398
714
|
value={"Dog"=>"🐶", "Cat"=>"🐱"},
|
399
715
|
options={:foo=>:bar},
|
400
|
-
routes=
|
401
|
-
[#<Node::Identity:0x00007fb41002b6d0
|
402
|
-
@klass=Node::XML::Scraper,
|
403
|
-
@type=:animals>,
|
404
|
-
Node::XML::Parser,
|
405
|
-
CallableTree::Node::Root]>
|
716
|
+
routes=[AnimalsXMLScraper, XMLParser, CallableTree::Node::Root]>
|
406
717
|
---
|
718
|
+
* JSONParser: [matched: true]
|
719
|
+
* AnimalsJSONScraper: [matched: false]
|
720
|
+
* FruitsJSONScraper: [matched: true]
|
721
|
+
Input : {"fruits"=>[{"name"=>"Red Apple", "emoji"=>"🍎"}, {"name"=>"Green Apple", "emoji"=>"🍏"}]}
|
722
|
+
Output: {"Red Apple"=>"🍎", "Green Apple"=>"🍏"}
|
407
723
|
#<struct CallableTree::Node::External::Output
|
408
724
|
value={"Red Apple"=>"🍎", "Green Apple"=>"🍏"},
|
409
725
|
options={:foo=>:bar},
|
410
|
-
routes=
|
411
|
-
[#<Node::Identity:0x00007fb41001b3e8
|
412
|
-
@klass=Node::JSON::Scraper,
|
413
|
-
@type=:fruits>,
|
414
|
-
Node::JSON::Parser,
|
415
|
-
CallableTree::Node::Root]>
|
726
|
+
routes=[FruitsJSONScraper, JSONParser, CallableTree::Node::Root]>
|
416
727
|
---
|
728
|
+
* JSONParser: [matched: false]
|
729
|
+
* XMLParser: [matched: true]
|
730
|
+
* AnimalsXMLScraper: [matched: false]
|
731
|
+
* FruitsXMLScraper: [matched: true]
|
732
|
+
Input : <root><fruits><fruit emoji='🍎' name='Red Apple'/><fruit emoji='🍏' name='Green Apple'/></fruits></root>
|
733
|
+
Output: {"Red Apple"=>"🍎", "Green Apple"=>"🍏"}
|
417
734
|
#<struct CallableTree::Node::External::Output
|
418
735
|
value={"Red Apple"=>"🍎", "Green Apple"=>"🍏"},
|
419
736
|
options={:foo=>:bar},
|
420
|
-
routes=
|
421
|
-
[#<Node::Identity:0x00007fb410049d38
|
422
|
-
@klass=Node::XML::Scraper,
|
423
|
-
@type=:fruits>,
|
424
|
-
Node::XML::Parser,
|
425
|
-
CallableTree::Node::Root]>
|
426
|
-
---
|
737
|
+
routes=[FruitsXMLScraper, XMLParser, CallableTree::Node::Root]>
|
427
738
|
```
|
428
739
|
|
429
|
-
####
|
740
|
+
#### `CallableTree::Node#identity`
|
430
741
|
|
431
|
-
|
742
|
+
If you want to customize the node identity, specify identifier.
|
432
743
|
|
433
|
-
`examples/
|
744
|
+
`examples/builder/identity.rb`:
|
434
745
|
```ruby
|
435
|
-
|
436
|
-
|
437
|
-
|
438
|
-
BLANK = ' '
|
439
|
-
|
440
|
-
module Match
|
441
|
-
LIST_STYLE = '*'
|
442
|
-
|
443
|
-
def match?(_input, **_options)
|
444
|
-
super.tap do |matched|
|
445
|
-
prefix = LIST_STYLE.rjust(depth * INDENT_SIZE - INDENT_SIZE + LIST_STYLE.length, BLANK)
|
446
|
-
puts "#{prefix} #{identity}: [matched: #{matched}]"
|
447
|
-
end
|
448
|
-
end
|
449
|
-
end
|
450
|
-
|
451
|
-
module Call
|
452
|
-
INPUT_LABEL = 'Input :'
|
453
|
-
OUTPUT_LABEL = 'Output:'
|
454
|
-
|
455
|
-
def call(input, **_options)
|
456
|
-
super.tap do |output|
|
457
|
-
input_prefix = INPUT_LABEL.rjust(depth * INDENT_SIZE + INPUT_LABEL.length, BLANK)
|
458
|
-
puts "#{input_prefix} #{input}"
|
459
|
-
output_prefix = OUTPUT_LABEL.rjust(depth * INDENT_SIZE + OUTPUT_LABEL.length, BLANK)
|
460
|
-
puts "#{output_prefix} #{output}"
|
461
|
-
end
|
462
|
-
end
|
463
|
-
end
|
464
|
-
end
|
465
|
-
|
746
|
+
JSONParser =
|
747
|
+
CallableTree::Node::Internal::Builder
|
748
|
+
.new
|
466
749
|
...
|
750
|
+
.identifier { |_node_:| _node_.object_id }
|
751
|
+
.build
|
467
752
|
|
468
|
-
|
469
|
-
|
470
|
-
|
471
|
-
|
472
|
-
|
473
|
-
|
474
|
-
end
|
475
|
-
|
476
|
-
class Scraper
|
477
|
-
include CallableTree::Node::External
|
478
|
-
prepend Logging::Match
|
479
|
-
prepend Logging::Call
|
480
|
-
|
481
|
-
...
|
482
|
-
end
|
483
|
-
end
|
484
|
-
|
485
|
-
module XML
|
486
|
-
class Parser
|
487
|
-
include CallableTree::Node::Internal
|
488
|
-
prepend Logging::Match
|
753
|
+
XMLParser =
|
754
|
+
CallableTree::Node::Internal::Builder
|
755
|
+
.new
|
756
|
+
...
|
757
|
+
.identifier { |_node_:| _node_.object_id }
|
758
|
+
.build
|
489
759
|
|
490
|
-
|
491
|
-
|
760
|
+
def build_json_scraper(type)
|
761
|
+
CallableTree::Node::External::Builder
|
762
|
+
.new
|
763
|
+
...
|
764
|
+
.identifier { |_node_:| _node_.object_id }
|
765
|
+
.build
|
766
|
+
end
|
492
767
|
|
493
|
-
|
494
|
-
include CallableTree::Node::External
|
495
|
-
prepend Logging::Match
|
496
|
-
prepend Logging::Call
|
768
|
+
...
|
497
769
|
|
498
|
-
|
499
|
-
|
500
|
-
|
770
|
+
def build_xml_scraper(type)
|
771
|
+
CallableTree::Node::External::Builder
|
772
|
+
.new
|
773
|
+
...
|
774
|
+
.identifier { |_node_:| _node_.object_id }
|
775
|
+
.build
|
501
776
|
end
|
502
777
|
|
503
778
|
...
|
504
779
|
```
|
505
780
|
|
506
|
-
Run `examples/
|
781
|
+
Run `examples/class/identity.rb`:
|
507
782
|
```sh
|
508
|
-
% ruby examples/
|
509
|
-
* Node::JSON::Parser: [matched: true]
|
510
|
-
* Node::JSON::Scraper(animals): [matched: true]
|
511
|
-
Input : {"animals"=>[{"name"=>"Dog", "emoji"=>"🐶"}, {"name"=>"Cat", "emoji"=>"🐱"}]}
|
512
|
-
Output: {"Dog"=>"🐶", "Cat"=>"🐱"}
|
783
|
+
% ruby examples/builder/identity.rb
|
513
784
|
#<struct CallableTree::Node::External::Output
|
514
|
-
|
515
|
-
|
516
|
-
|
517
|
-
[#<Node::Identity:0x00007ffd840347b8
|
518
|
-
@klass=Node::JSON::Scraper,
|
519
|
-
@type=:animals>,
|
520
|
-
Node::JSON::Parser,
|
521
|
-
CallableTree::Node::Root]>
|
785
|
+
value={"Dog"=>"🐶", "Cat"=>"🐱"},
|
786
|
+
options={:foo=>:bar},
|
787
|
+
routes=[60, 80, CallableTree::Node::Root]>
|
522
788
|
---
|
523
|
-
* Node::JSON::Parser: [matched: false]
|
524
|
-
* Node::XML::Parser: [matched: true]
|
525
|
-
* Node::XML::Scraper(animals): [matched: true]
|
526
|
-
Input : <root><animals><animal emoji='🐶' name='Dog'/><animal emoji='🐱' name='Cat'/></animals></root>
|
527
|
-
Output: {"Dog"=>"🐶", "Cat"=>"🐱"}
|
528
789
|
#<struct CallableTree::Node::External::Output
|
529
|
-
|
530
|
-
|
531
|
-
|
532
|
-
[#<Node::Identity:0x00007ffd7403f1f0
|
533
|
-
@klass=Node::XML::Scraper,
|
534
|
-
@type=:animals>,
|
535
|
-
Node::XML::Parser,
|
536
|
-
CallableTree::Node::Root]>
|
790
|
+
value={"Dog"=>"🐶", "Cat"=>"🐱"},
|
791
|
+
options={:foo=>:bar},
|
792
|
+
routes=[220, 240, CallableTree::Node::Root]>
|
537
793
|
---
|
538
|
-
* Node::JSON::Parser: [matched: true]
|
539
|
-
* Node::JSON::Scraper(animals): [matched: false]
|
540
|
-
* Node::JSON::Scraper(fruits): [matched: true]
|
541
|
-
Input : {"fruits"=>[{"name"=>"Red Apple", "emoji"=>"🍎"}, {"name"=>"Green Apple", "emoji"=>"🍏"}]}
|
542
|
-
Output: {"Red Apple"=>"🍎", "Green Apple"=>"🍏"}
|
543
794
|
#<struct CallableTree::Node::External::Output
|
544
795
|
value={"Red Apple"=>"🍎", "Green Apple"=>"🍏"},
|
545
796
|
options={:foo=>:bar},
|
546
|
-
routes=
|
547
|
-
[#<Node::Identity:0x00007ffd8512bdf0
|
548
|
-
@klass=Node::JSON::Scraper,
|
549
|
-
@type=:fruits>,
|
550
|
-
Node::JSON::Parser,
|
551
|
-
CallableTree::Node::Root]>
|
797
|
+
routes=[260, 80, CallableTree::Node::Root]>
|
552
798
|
---
|
553
|
-
* Node::JSON::Parser: [matched: false]
|
554
|
-
* Node::XML::Parser: [matched: true]
|
555
|
-
* Node::XML::Scraper(animals): [matched: false]
|
556
|
-
* Node::XML::Scraper(fruits): [matched: true]
|
557
|
-
Input : <root><fruits><fruit emoji='🍎' name='Red Apple'/><fruit emoji='🍏' name='Green Apple'/></fruits></root>
|
558
|
-
Output: {"Red Apple"=>"🍎", "Green Apple"=>"🍏"}
|
559
799
|
#<struct CallableTree::Node::External::Output
|
560
800
|
value={"Red Apple"=>"🍎", "Green Apple"=>"🍏"},
|
561
801
|
options={:foo=>:bar},
|
562
|
-
routes=
|
563
|
-
[#<Node::Identity:0x00007ffd8407a740
|
564
|
-
@klass=Node::XML::Scraper,
|
565
|
-
@type=:fruits>,
|
566
|
-
Node::XML::Parser,
|
567
|
-
CallableTree::Node::Root]>
|
802
|
+
routes=[400, 240, CallableTree::Node::Root]>
|
568
803
|
---
|
569
804
|
```
|
570
805
|
|
571
|
-
#### `CallableTree::Node::Hooks::Caller` (experimental)
|
572
|
-
|
573
|
-
`examples/hooks-call.rb`:
|
574
|
-
```ruby
|
575
|
-
module Node
|
576
|
-
class HooksSample
|
577
|
-
include CallableTree::Node::Internal
|
578
|
-
prepend CallableTree::Node::Hooks::Caller
|
579
|
-
end
|
580
|
-
end
|
581
|
-
|
582
|
-
Node::HooksSample.new
|
583
|
-
.before_caller do |input, **_options|
|
584
|
-
puts "before_caller input: #{input}";
|
585
|
-
input + 1
|
586
|
-
end
|
587
|
-
.append(
|
588
|
-
# anonymous external node
|
589
|
-
lambda do |input, **_options|
|
590
|
-
puts "external input: #{input}"
|
591
|
-
input * 2
|
592
|
-
end
|
593
|
-
)
|
594
|
-
.around_caller do |input, **_options, &block|
|
595
|
-
puts "around_caller input: #{input}"
|
596
|
-
output = block.call
|
597
|
-
puts "around_caller output: #{output}"
|
598
|
-
output * input
|
599
|
-
end
|
600
|
-
.after_caller do |output, **_options|
|
601
|
-
puts "after_caller output: #{output}"
|
602
|
-
output * 2
|
603
|
-
end
|
604
|
-
.tap do |tree|
|
605
|
-
options = { foo: :bar }
|
606
|
-
output = tree.call(1, **options)
|
607
|
-
puts "result: #{output}"
|
608
|
-
end
|
609
|
-
```
|
610
|
-
|
611
|
-
Run `examples/hooks-caller.rb`:
|
612
|
-
```sh
|
613
|
-
% ruby examples/hooks-caller.rb
|
614
|
-
before_caller input: 1
|
615
|
-
external input: 2
|
616
|
-
around_caller input: 2
|
617
|
-
around_caller output: 4
|
618
|
-
after_caller output: 8
|
619
|
-
result: 16
|
620
|
-
```
|
621
|
-
|
622
806
|
## Contributing
|
623
807
|
|
624
808
|
Bug reports and pull requests are welcome on GitHub at https://github.com/jsmmr/ruby_callable_tree.
|