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.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/.github/dependabot.yml +7 -0
  3. data/.github/workflows/build.yml +3 -5
  4. data/.github/workflows/codeql-analysis.yml +4 -4
  5. data/.rubocop.yml +6 -0
  6. data/.ruby-version +1 -1
  7. data/CHANGELOG.md +12 -0
  8. data/Gemfile.lock +3 -3
  9. data/README.md +460 -276
  10. data/callable_tree.gemspec +1 -0
  11. data/examples/builder/external-verbosify.rb +87 -0
  12. data/examples/builder/hooks.rb +67 -0
  13. data/examples/builder/identity.rb +91 -0
  14. data/examples/builder/internal-broadcastable.rb +11 -11
  15. data/examples/builder/internal-composable.rb +11 -11
  16. data/examples/builder/internal-seekable.rb +9 -15
  17. data/examples/builder/logging.rb +36 -39
  18. data/examples/{external-verbosify.rb → class/external-verbosify.rb} +3 -5
  19. data/examples/class/hooks.rb +70 -0
  20. data/examples/{identity.rb → class/identity.rb} +3 -5
  21. data/examples/{internal-broadcastable.rb → class/internal-broadcastable.rb} +0 -0
  22. data/examples/{internal-composable.rb → class/internal-composable.rb} +0 -0
  23. data/examples/{internal-seekable.rb → class/internal-seekable.rb} +3 -5
  24. data/examples/{logging.rb → class/logging.rb} +47 -47
  25. data/lib/callable_tree/node/builder.rb +13 -0
  26. data/lib/callable_tree/node/hooks/terminator.rb +99 -0
  27. data/lib/callable_tree/node/internal/strategy/broadcast.rb +19 -2
  28. data/lib/callable_tree/node/internal/strategy/compose.rb +15 -3
  29. data/lib/callable_tree/node/internal/strategy/seek.rb +11 -1
  30. data/lib/callable_tree/node/internal/strategy.rb +10 -2
  31. data/lib/callable_tree/node/internal.rb +22 -28
  32. data/lib/callable_tree/node/root.rb +1 -0
  33. data/lib/callable_tree/version.rb +1 -1
  34. data/lib/callable_tree.rb +1 -0
  35. metadata +17 -11
  36. data/examples/builder/hooks-caller.rb +0 -38
  37. 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 instances of the nodes. The `call` method of the node where the `match?` method returns a truthy value is called in a chain from the root node to the leaf node.
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. An instance of this node has several strategies. The strategy can be changed by calling the method of the instance.
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
- #### `CallableTree::Node::Internal#seekable` (default)
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
- `examples/internal-seekable.rb`:
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
- .map { |element| [element['name'], element['emoji']] }
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
- .map { |element| [element['name'], element['emoji']] }
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 calls all child nodes of the internal node and ignores their `terminate?` methods, and then outputs their results as array.
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
- `examples/internal-broadcastable.rb`:
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 calls all child nodes of the internal node in order to input the output of the previous node to the next node and ignores their `terminate?` methods, and then outputs a single result.
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
- Node::JSON::Parser.new.append(
284
- Node::JSON::Scraper.new(type: :animals).verbosify,
285
- Node::JSON::Scraper.new(type: :fruits).verbosify
569
+ tree = CallableTree::Node::Root.new.seekable.append(
570
+ JSONParser.new.seekable.append(
571
+ AnimalsJSONScraper.new.verbosify,
572
+ FruitsJSONScraper.new.verbosify
286
573
  ),
287
- Node::XML::Parser.new.append(
288
- Node::XML::Scraper.new(type: :animals).verbosify,
289
- Node::XML::Scraper.new(type: :fruits).verbosify
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
- value={"Dog"=>"🐶", "Cat"=>"🐱"},
301
- options={:foo=>:bar},
302
- routes=[Node::JSON::Scraper, Node::JSON::Parser, CallableTree::Node::Root]>
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
- value={"Dog"=>"🐶", "Cat"=>"🐱"},
306
- options={:foo=>:bar},
307
- routes=[Node::XML::Scraper, Node::XML::Parser, CallableTree::Node::Root]>
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
- value={"Red Apple"=>"🍎", "Green Apple"=>"🍏"},
311
- options={:foo=>:bar},
312
- routes=[Node::JSON::Scraper, Node::JSON::Parser, CallableTree::Node::Root]>
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
- value={"Red Apple"=>"🍎", "Green Apple"=>"🍏"},
316
- options={:foo=>:bar},
317
- routes=[Node::XML::Scraper, Node::XML::Parser, CallableTree::Node::Root]>
602
+ value={"Red Apple"=>"🍎", "Green Apple"=>"🍏"},
603
+ options={:foo=>:bar},
604
+ routes=[FruitsXMLScraper, XMLParser, CallableTree::Node::Root]>
318
605
  ---
319
606
  ```
320
607
 
321
- At first glance, this looks good, but the `routes` are ambiguous when there are multiple nodes of the same class.
322
- You can work around it by overriding the `identity` method of the node.
323
-
324
- #### `CallableTree::Node#identity`
608
+ #### Logging
325
609
 
326
- If you want to customize the node identity, override this method.
610
+ This is an example of logging.
327
611
 
328
- `examples/identity.rb`:
612
+ `examples/builder/logging.rb`:
329
613
  ```ruby
330
- module Node
331
- class Identity
332
- attr_reader :klass, :type
333
-
334
- def initialize(klass:, type:)
335
- @klass = klass
336
- @type = type
337
- end
614
+ JSONParser =
615
+ CallableTree::Node::Internal::Builder
616
+ .new
617
+ ...
618
+ .hookable
619
+ .build
338
620
 
339
- def to_s
340
- "#{klass}(#{type})"
341
- end
342
- end
621
+ XMLParser =
622
+ CallableTree::Node::Internal::Builder
623
+ .new
624
+ ...
625
+ .hookable
626
+ .build
343
627
 
344
- module JSON
628
+ def build_json_scraper(type)
629
+ CallableTree::Node::External::Builder
630
+ .new
345
631
  ...
632
+ .hookable
633
+ .build
634
+ end
346
635
 
347
- class Scraper
348
- include CallableTree::Node::External
636
+ ...
349
637
 
350
- def initialize(type:)
351
- @type = type
352
- end
638
+ def build_xml_scraper(type)
639
+ CallableTree::Node::External::Builder
640
+ .new
641
+ ...
642
+ .hookable
643
+ .build
644
+ end
353
645
 
354
- def identity
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
- Run `examples/identity.rb`:
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/identity.rb
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
- #### Logging
740
+ #### `CallableTree::Node#identity`
430
741
 
431
- This is an example of logging.
742
+ If you want to customize the node identity, specify identifier.
432
743
 
433
- `examples/logging.rb`:
744
+ `examples/builder/identity.rb`:
434
745
  ```ruby
435
- module Node
436
- module Logging
437
- INDENT_SIZE = 2
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
- module JSON
469
- class Parser
470
- include CallableTree::Node::Internal
471
- prepend Logging::Match
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
- end
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
- class Scraper
494
- include CallableTree::Node::External
495
- prepend Logging::Match
496
- prepend Logging::Call
768
+ ...
497
769
 
498
- ...
499
- end
500
- end
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/logging.rb`:
781
+ Run `examples/class/identity.rb`:
507
782
  ```sh
508
- % ruby examples/logging.rb
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
- value={"Dog"=>"🐶", "Cat"=>"🐱"},
515
- options={:foo=>:bar},
516
- routes=
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
- value={"Dog"=>"🐶", "Cat"=>"🐱"},
530
- options={:foo=>:bar},
531
- routes=
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.