callable_tree 0.3.10 → 0.3.12

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 (47) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/build.yml +19 -5
  3. data/.github/workflows/codeql-analysis.yml +4 -4
  4. data/.gitignore +2 -0
  5. data/.rubocop.yml +3 -0
  6. data/.rubocop_todo.yml +128 -0
  7. data/.ruby-version +1 -1
  8. data/AGENTS.md +56 -0
  9. data/CHANGELOG.md +11 -0
  10. data/CONTRIBUTING.md +68 -0
  11. data/Gemfile +4 -2
  12. data/README.md +195 -21
  13. data/callable_tree.gemspec +8 -2
  14. data/examples/builder/external-verbosify.rb +3 -2
  15. data/examples/builder/hooks.rb +2 -1
  16. data/examples/builder/identity.rb +3 -2
  17. data/examples/builder/internal-broadcastable.rb +2 -1
  18. data/examples/builder/internal-composable.rb +2 -1
  19. data/examples/builder/internal-seekable.rb +3 -2
  20. data/examples/builder/logging.rb +3 -2
  21. data/examples/class/external-verbosify.rb +3 -2
  22. data/examples/class/hooks.rb +2 -1
  23. data/examples/class/identity.rb +3 -2
  24. data/examples/class/internal-broadcastable.rb +2 -1
  25. data/examples/class/internal-composable.rb +2 -1
  26. data/examples/class/internal-seekable.rb +3 -2
  27. data/examples/class/logging.rb +3 -2
  28. data/examples/factory/external-verbosify.rb +69 -0
  29. data/examples/factory/hooks.rb +67 -0
  30. data/examples/factory/identity.rb +93 -0
  31. data/examples/factory/internal-broadcastable.rb +37 -0
  32. data/examples/factory/internal-composable.rb +37 -0
  33. data/examples/factory/internal-seekable.rb +71 -0
  34. data/examples/factory/logging.rb +121 -0
  35. data/lib/callable_tree/node/builder.rb +1 -0
  36. data/lib/callable_tree/node/external/pod.rb +91 -0
  37. data/lib/callable_tree/node/external/verbose.rb +1 -1
  38. data/lib/callable_tree/node/external.rb +1 -0
  39. data/lib/callable_tree/node/hooks/terminator.rb +1 -1
  40. data/lib/callable_tree/node/internal/pod.rb +91 -0
  41. data/lib/callable_tree/node/internal/{strategyable.rb → strategizable.rb} +4 -4
  42. data/lib/callable_tree/node/internal.rb +2 -2
  43. data/lib/callable_tree/version.rb +1 -1
  44. data/lib/callable_tree.rb +3 -1
  45. data/mise.toml +2 -0
  46. metadata +26 -15
  47. data/Gemfile.lock +0 -34
data/README.md CHANGED
@@ -3,6 +3,10 @@
3
3
  [![build](https://github.com/jsmmr/ruby_callable_tree/actions/workflows/build.yml/badge.svg)](https://github.com/jsmmr/ruby_callable_tree/actions/workflows/build.yml)
4
4
  [![CodeQL](https://github.com/jsmmr/ruby_callable_tree/actions/workflows/codeql-analysis.yml/badge.svg)](https://github.com/jsmmr/ruby_callable_tree/actions/workflows/codeql-analysis.yml)
5
5
 
6
+ A framework for building tree-structured executable workflows in Ruby.
7
+
8
+ Construct trees of callable nodes to handle complex execution flows. Supports strategies to seek specific handlers, broadcast to multiple listeners, or compose processing pipelines. Nodes are matched against input and executed in a chain, offering a structured, modular alternative to complex conditional logic.
9
+
6
10
  ## Installation
7
11
 
8
12
  Add this line to your application's Gemfile:
@@ -21,22 +25,22 @@ Or install it yourself as:
21
25
 
22
26
  ## Usage
23
27
 
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.
28
+ Builds a tree of `CallableTree` nodes. Invokes the `call` method on nodes where `match?` returns a truthy value, chaining execution from root to leaf.
25
29
 
26
30
  - `CallableTree::Node::Internal`
27
- - This `module` is used to define a node that can have child nodes. This node has several strategies (`seekable`, `broadcastable`, `composable`).
31
+ - Defines a node that can have child nodes. Supports several strategies (`seekable`, `broadcastable`, `composable`).
28
32
  - `CallableTree::Node::External`
29
- - This `module` is used to define a leaf node that cannot have child nodes.
33
+ - Defines a leaf node, which cannot have child nodes.
30
34
  - `CallableTree::Node::Root`
31
- - This `class` includes `CallableTree::Node::Internal`. When there is no need to customize the internal node, use this `class`.
35
+ - Includes `CallableTree::Node::Internal`. Use this class when customization of the internal node is not required.
32
36
 
33
37
  ### Basic
34
38
 
35
- There are two ways to define the nodes: class style and builder style.
39
+ There are three ways to define nodes: class style, builder style, and factory style.
36
40
 
37
41
  #### `CallableTree::Node::Internal#seekable` (default strategy)
38
42
 
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.
43
+ This strategy stops processing subsequent sibling nodes if the current node's `call` method returns a non-nil value. This behavior is changeable by overriding the `terminate?` method.
40
44
 
41
45
  ##### Class style
42
46
 
@@ -51,8 +55,7 @@ module Node
51
55
  File.extname(input) == '.json'
52
56
  end
53
57
 
54
- # If there is need to convert the input values for
55
- # child nodes, override the `call` method.
58
+ # Override `call` if you need to transform input values for child nodes.
56
59
  def call(input, **options)
57
60
  File.open(input) do |file|
58
61
  json = ::JSON.load(file)
@@ -60,9 +63,7 @@ module Node
60
63
  end
61
64
  end
62
65
 
63
- # If a returned value of the `call` method is `nil`,
64
- # but there is no need to call the sibling nodes,
65
- # override the `terminate?` method to return `true`.
66
+ # Override `terminate?` to return `true` to stop processing sibling nodes even if `call` returns `nil`.
66
67
  def terminate?(_output, *_inputs, **_options)
67
68
  true
68
69
  end
@@ -94,17 +95,14 @@ module Node
94
95
  File.extname(input) == '.xml'
95
96
  end
96
97
 
97
- # If there is need to convert the input values for
98
- # child nodes, override the `call` method.
98
+ # Override `call` if you need to transform input values for child nodes.
99
99
  def call(input, **options)
100
100
  File.open(input) do |file|
101
101
  super(REXML::Document.new(file), **options)
102
102
  end
103
103
  end
104
104
 
105
- # If a returned value of the `call` method is `nil`,
106
- # but there is no need to call the sibling nodes,
107
- # override the `terminate?` method to return `true`.
105
+ # Override `terminate?` to return `true` to stop processing sibling nodes even if `call` returns `nil`.
108
106
  def terminate?(_output, *_inputs, **_options)
109
107
  true
110
108
  end
@@ -131,7 +129,7 @@ module Node
131
129
  end
132
130
  end
133
131
 
134
- # The `seekable` method call can be omitted since it is the default strategy.
132
+ # The `seekable` call can be omitted as it is the default strategy.
135
133
  tree = CallableTree::Node::Root.new.seekable.append(
136
134
  Node::JSON::Parser.new.seekable.append(
137
135
  Node::JSON::Scraper.new(type: :animals),
@@ -263,9 +261,87 @@ Run `examples/builder/internal-seekable.rb`:
263
261
  ---
264
262
  ```
265
263
 
264
+ ##### Factory style
265
+
266
+ Factory style defines behaviors as procs first, then assembles the tree structure separately. This makes the tree structure clearly visible.
267
+
268
+ `examples/factory/internal-seekable.rb`:
269
+ ```ruby
270
+ # === Behavior Definitions ===
271
+
272
+ json_matcher = ->(input, **) { File.extname(input) == '.json' }
273
+ json_caller = lambda do |input, **options, &original|
274
+ File.open(input) do |file|
275
+ json = JSON.parse(file.read)
276
+ original.call(json, **options)
277
+ end
278
+ end
279
+
280
+ xml_matcher = ->(input, **) { File.extname(input) == '.xml' }
281
+ xml_caller = lambda do |input, **options, &original|
282
+ File.open(input) do |file|
283
+ original.call(REXML::Document.new(file), **options)
284
+ end
285
+ end
286
+
287
+ animals_json_matcher = ->(input, **) { !input['animals'].nil? }
288
+ animals_json_caller = ->(input, **) { input['animals'].to_h { |e| [e['name'], e['emoji']] } }
289
+
290
+ fruits_json_matcher = ->(input, **) { !input['fruits'].nil? }
291
+ fruits_json_caller = ->(input, **) { input['fruits'].to_h { |e| [e['name'], e['emoji']] } }
292
+
293
+ animals_xml_matcher = ->(input, **) { !input.get_elements('//animals').empty? }
294
+ animals_xml_caller = ->(input, **) { input.get_elements('//animals').first.to_h { |e| [e['name'], e['emoji']] } }
295
+
296
+ fruits_xml_matcher = ->(input, **) { !input.get_elements('//fruits').empty? }
297
+ fruits_xml_caller = ->(input, **) { input.get_elements('//fruits').first.to_h { |e| [e['name'], e['emoji']] } }
298
+
299
+ terminator_true = ->(*) { true }
300
+
301
+ # === Tree Structure (clearly visible!) ===
302
+
303
+ tree = CallableTree::Node::Root.new.seekable.append(
304
+ CallableTree::Node::Internal.create(
305
+ matcher: json_matcher,
306
+ caller: json_caller,
307
+ terminator: terminator_true
308
+ ).seekable.append(
309
+ CallableTree::Node::External.create(matcher: animals_json_matcher, caller: animals_json_caller),
310
+ CallableTree::Node::External.create(matcher: fruits_json_matcher, caller: fruits_json_caller)
311
+ ),
312
+ CallableTree::Node::Internal.create(
313
+ matcher: xml_matcher,
314
+ caller: xml_caller,
315
+ terminator: terminator_true
316
+ ).seekable.append(
317
+ CallableTree::Node::External.create(matcher: animals_xml_matcher, caller: animals_xml_caller),
318
+ CallableTree::Node::External.create(matcher: fruits_xml_matcher, caller: fruits_xml_caller)
319
+ )
320
+ )
321
+
322
+ Dir.glob("#{__dir__}/../docs/*") do |file|
323
+ options = { foo: :bar }
324
+ pp tree.call(file, **options)
325
+ puts '---'
326
+ end
327
+ ```
328
+
329
+ Run `examples/factory/internal-seekable.rb`:
330
+ ```sh
331
+ % ruby examples/factory/internal-seekable.rb
332
+ {"Dog"=>"🐶", "Cat"=>"🐱"}
333
+ ---
334
+ {"Dog"=>"🐶", "Cat"=>"🐱"}
335
+ ---
336
+ {"Red Apple"=>"🍎", "Green Apple"=>"🍏"}
337
+ ---
338
+ {"Red Apple"=>"🍎", "Green Apple"=>"🍏"}
339
+ ---
340
+ ```
341
+
266
342
  #### `CallableTree::Node::Internal#broadcastable`
267
343
 
268
- This strategy broadcasts to output a result of the child nodes as array. It also ignores their `terminate?` methods by default.
344
+ This strategy broadcasts input to all child nodes and returns their results as an array. It ignores child `terminate?` methods by default.
269
345
 
270
346
  ##### Class style
271
347
 
@@ -409,9 +485,58 @@ Run `examples/builder/internal-broadcastable.rb`:
409
485
  10 -> [nil, nil]
410
486
  ```
411
487
 
488
+ ##### Factory style
489
+
490
+ `examples/factory/internal-broadcastable.rb`:
491
+ ```ruby
492
+ # === Behavior Definitions ===
493
+
494
+ less_than_5_matcher = ->(input, **, &original) { original.call(input) && input < 5 }
495
+ less_than_10_matcher = ->(input, **, &original) { original.call(input) && input < 10 }
496
+
497
+ multiply_2_caller = ->(input, **) { input * 2 }
498
+ add_1_caller = ->(input, **) { input + 1 }
499
+ multiply_3_caller = ->(input, **) { input * 3 }
500
+ subtract_1_caller = ->(input, **) { input - 1 }
501
+
502
+ # === Tree Structure ===
503
+
504
+ tree = CallableTree::Node::Root.new.broadcastable.append(
505
+ CallableTree::Node::Internal.create(matcher: less_than_5_matcher).broadcastable.append(
506
+ CallableTree::Node::External.create(caller: multiply_2_caller),
507
+ CallableTree::Node::External.create(caller: add_1_caller)
508
+ ),
509
+ CallableTree::Node::Internal.create(matcher: less_than_10_matcher).broadcastable.append(
510
+ CallableTree::Node::External.create(caller: multiply_3_caller),
511
+ CallableTree::Node::External.create(caller: subtract_1_caller)
512
+ )
513
+ )
514
+
515
+ (0..10).each do |input|
516
+ output = tree.call(input)
517
+ puts "#{input} -> #{output}"
518
+ end
519
+ ```
520
+
521
+ Run `examples/factory/internal-broadcastable.rb`:
522
+ ```sh
523
+ % ruby examples/factory/internal-broadcastable.rb
524
+ 0 -> [[0, 1], [0, -1]]
525
+ 1 -> [[2, 2], [3, 0]]
526
+ 2 -> [[4, 3], [6, 1]]
527
+ 3 -> [[6, 4], [9, 2]]
528
+ 4 -> [[8, 5], [12, 3]]
529
+ 5 -> [nil, [15, 4]]
530
+ 6 -> [nil, [18, 5]]
531
+ 7 -> [nil, [21, 6]]
532
+ 8 -> [nil, [24, 7]]
533
+ 9 -> [nil, [27, 8]]
534
+ 10 -> [nil, nil]
535
+ ```
536
+
412
537
  #### `CallableTree::Node::Internal#composable`
413
538
 
414
- This strategy composes the child nodes to input the output of the previous node into the next node and to output a result.
539
+ This strategy chains child nodes, passing the output of the previous node as input to the next.
415
540
  It also ignores their `terminate?` methods by default.
416
541
 
417
542
  ##### Class style
@@ -556,11 +681,60 @@ Run `examples/builder/internal-composable.rb`:
556
681
  10 -> 10
557
682
  ```
558
683
 
684
+ ##### Factory style
685
+
686
+ `examples/factory/internal-composable.rb`:
687
+ ```ruby
688
+ # === Behavior Definitions ===
689
+
690
+ less_than_5_matcher = ->(input, **, &original) { original.call(input) && input < 5 }
691
+ less_than_10_matcher = ->(input, **, &original) { original.call(input) && input < 10 }
692
+
693
+ multiply_2_caller = ->(input, **) { input * 2 }
694
+ add_1_caller = ->(input, **) { input + 1 }
695
+ multiply_3_caller = ->(input, **) { input * 3 }
696
+ subtract_1_caller = ->(input, **) { input - 1 }
697
+
698
+ # === Tree Structure ===
699
+
700
+ tree = CallableTree::Node::Root.new.composable.append(
701
+ CallableTree::Node::Internal.create(matcher: less_than_5_matcher).composable.append(
702
+ CallableTree::Node::External.create(caller: multiply_2_caller),
703
+ CallableTree::Node::External.create(caller: add_1_caller)
704
+ ),
705
+ CallableTree::Node::Internal.create(matcher: less_than_10_matcher).composable.append(
706
+ CallableTree::Node::External.create(caller: multiply_3_caller),
707
+ CallableTree::Node::External.create(caller: subtract_1_caller)
708
+ )
709
+ )
710
+
711
+ (0..10).each do |input|
712
+ output = tree.call(input)
713
+ puts "#{input} -> #{output}"
714
+ end
715
+ ```
716
+
717
+ Run `examples/factory/internal-composable.rb`:
718
+ ```sh
719
+ % ruby examples/factory/internal-composable.rb
720
+ 0 -> 2
721
+ 1 -> 8
722
+ 2 -> 14
723
+ 3 -> 20
724
+ 4 -> 26
725
+ 5 -> 14
726
+ 6 -> 17
727
+ 7 -> 20
728
+ 8 -> 23
729
+ 9 -> 26
730
+ 10 -> 10
731
+ ```
732
+
559
733
  ### Advanced
560
734
 
561
735
  #### `CallableTree::Node::External#verbosify`
562
736
 
563
- If you want verbose output results, call this method.
737
+ Use this method to enable verbose output.
564
738
 
565
739
  `examples/builder/external-verbosify.rb`:
566
740
  ```ruby
@@ -739,7 +913,7 @@ Run `examples/builder/logging.rb`:
739
913
 
740
914
  #### `CallableTree::Node#identity`
741
915
 
742
- If you want to customize the node identity, specify identifier.
916
+ Specify an identifier to customize the node identity.
743
917
 
744
918
  `examples/builder/identity.rb`:
745
919
  ```ruby
@@ -8,8 +8,14 @@ Gem::Specification.new do |spec|
8
8
  spec.authors = ['jsmmr']
9
9
  spec.email = ['jsmmr@icloud.com']
10
10
 
11
- spec.summary = 'Builds a tree by linking callable nodes. The nodes that match the conditions are called in a chain from the root node to the leaf node. These are like nested `if` or `case` expressions.'
12
- spec.description = 'Builds a tree by linking callable nodes. The nodes that match the conditions are called in a chain from the root node to the leaf node. These are like nested `if` or `case` expressions.'
11
+ spec.summary = 'Builds executable trees of callable nodes with flexible strategies like seek, broadcast, and compose.'
12
+ spec.description = <<~DESC
13
+ CallableTree provides a framework for organizing complex logic into a tree of callable nodes.
14
+ It allows you to chain execution from a root node to leaf nodes based on matching conditions.
15
+ Key features include multiple traversal strategies: `seekable` (like nested `if`/`case`),
16
+ `broadcastable` (one-to-many execution), and `composable` (pipelined processing).
17
+ Supports class-based, builder-style and factory-style definitions.
18
+ DESC
13
19
  spec.homepage = 'https://github.com/jsmmr/ruby_callable_tree'
14
20
  spec.license = 'MIT'
15
21
  spec.required_ruby_version = Gem::Requirement.new('>= 2.4.0')
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'callable_tree'
3
+ require_relative '../../lib/callable_tree'
4
+ # require 'callable_tree'
4
5
  require 'json'
5
6
  require 'rexml/document'
6
7
 
@@ -12,7 +13,7 @@ JSONParser =
12
13
  end
13
14
  .caller do |input, **options, &block|
14
15
  File.open(input) do |file|
15
- json = JSON.load(file)
16
+ json = JSON.parse(file.read)
16
17
  # The following block call is equivalent to calling `super` in the class style.
17
18
  block.call(json, **options)
18
19
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'callable_tree'
3
+ require_relative '../../lib/callable_tree'
4
+ # require 'callable_tree'
4
5
 
5
6
  HooksSample =
6
7
  CallableTree::Node::Internal::Builder
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'callable_tree'
3
+ require_relative '../../lib/callable_tree'
4
+ # require 'callable_tree'
4
5
  require 'json'
5
6
  require 'rexml/document'
6
7
 
@@ -12,7 +13,7 @@ JSONParser =
12
13
  end
13
14
  .caller do |input, **options, &block|
14
15
  File.open(input) do |file|
15
- json = JSON.load(file)
16
+ json = JSON.parse(file.read)
16
17
  # The following block call is equivalent to calling `super` in the class style.
17
18
  block.call(json, **options)
18
19
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'callable_tree'
3
+ require_relative '../../lib/callable_tree'
4
+ # require 'callable_tree'
4
5
 
5
6
  def less_than(num)
6
7
  # The following block call is equivalent to calling `super` in the class style.
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'callable_tree'
3
+ require_relative '../../lib/callable_tree'
4
+ # require 'callable_tree'
4
5
 
5
6
  def less_than(num)
6
7
  # The following block call is equivalent to calling `super` in the class style.
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'callable_tree'
3
+ require_relative '../../lib/callable_tree'
4
+ # require 'callable_tree'
4
5
  require 'json'
5
6
  require 'rexml/document'
6
7
 
@@ -12,7 +13,7 @@ JSONParser =
12
13
  end
13
14
  .caller do |input, **options, &original|
14
15
  File.open(input) do |file|
15
- json = JSON.load(file)
16
+ json = JSON.parse(file.read)
16
17
  # The following block call is equivalent to calling `super` in the class style.
17
18
  original.call(json, **options)
18
19
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'callable_tree'
3
+ require_relative '../../lib/callable_tree'
4
+ # require 'callable_tree'
4
5
  require 'json'
5
6
  require 'rexml/document'
6
7
 
@@ -12,7 +13,7 @@ JSONParser =
12
13
  end
13
14
  .caller do |input, **options, &original|
14
15
  File.open(input) do |file|
15
- json = JSON.load(file)
16
+ json = JSON.parse(file.read)
16
17
  # The following block call is equivalent to calling `super` in the class style.
17
18
  original.call(json, **options)
18
19
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'callable_tree'
3
+ require_relative '../../lib/callable_tree'
4
+ # require 'callable_tree'
4
5
  require 'json'
5
6
  require 'rexml/document'
6
7
 
@@ -15,7 +16,7 @@ module Node
15
16
 
16
17
  def call(input, **options)
17
18
  File.open(input) do |file|
18
- json = ::JSON.load(file)
19
+ json = ::JSON.parse(file.read)
19
20
  super(json, **options)
20
21
  end
21
22
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'callable_tree'
3
+ require_relative '../../lib/callable_tree'
4
+ # require 'callable_tree'
4
5
 
5
6
  module Node
6
7
  class HooksSample
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'callable_tree'
3
+ require_relative '../../lib/callable_tree'
4
+ # require 'callable_tree'
4
5
  require 'json'
5
6
  require 'rexml/document'
6
7
 
@@ -28,7 +29,7 @@ module Node
28
29
 
29
30
  def call(input, **options)
30
31
  File.open(input) do |file|
31
- json = ::JSON.load(file)
32
+ json = ::JSON.parse(file.read)
32
33
  super(json, **options)
33
34
  end
34
35
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'callable_tree'
3
+ require_relative '../../lib/callable_tree'
4
+ # require 'callable_tree'
4
5
 
5
6
  module Node
6
7
  class LessThan
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'callable_tree'
3
+ require_relative '../../lib/callable_tree'
4
+ # require 'callable_tree'
4
5
 
5
6
  module Node
6
7
  class LessThan
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'callable_tree'
3
+ require_relative '../../lib/callable_tree'
4
+ # require 'callable_tree'
4
5
  require 'json'
5
6
  require 'rexml/document'
6
7
 
@@ -15,7 +16,7 @@ module Node
15
16
 
16
17
  def call(input, **options)
17
18
  File.open(input) do |file|
18
- json = ::JSON.load(file)
19
+ json = ::JSON.parse(file.read)
19
20
  super(json, **options)
20
21
  end
21
22
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'callable_tree'
3
+ require_relative '../../lib/callable_tree'
4
+ # require 'callable_tree'
4
5
  require 'json'
5
6
  require 'rexml/document'
6
7
 
@@ -29,7 +30,7 @@ module Node
29
30
 
30
31
  def call(input, **options)
31
32
  File.open(input) do |file|
32
- json = ::JSON.load(file)
33
+ json = ::JSON.parse(file.read)
33
34
  super(json, **options)
34
35
  end
35
36
  end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../lib/callable_tree'
4
+ # require 'callable_tree'
5
+ require 'json'
6
+ require 'rexml/document'
7
+
8
+ # Verbosify example using Factory style with pre-defined procs
9
+ # Shows verbose output including route information
10
+
11
+ # === Behavior Definitions ===
12
+
13
+ json_matcher = ->(input, **) { File.extname(input) == '.json' }
14
+ json_caller = lambda do |input, **options, &block|
15
+ File.open(input) do |file|
16
+ json = JSON.parse(file.read)
17
+ block.call(json, **options)
18
+ end
19
+ end
20
+
21
+ xml_matcher = ->(input, **) { File.extname(input) == '.xml' }
22
+ xml_caller = lambda do |input, **options, &block|
23
+ File.open(input) do |file|
24
+ block.call(REXML::Document.new(file), **options)
25
+ end
26
+ end
27
+
28
+ animals_json_matcher = ->(input, **) { !input['animals'].nil? }
29
+ animals_json_caller = ->(input, **) { input['animals'].to_h { |e| [e['name'], e['emoji']] } }
30
+
31
+ fruits_json_matcher = ->(input, **) { !input['fruits'].nil? }
32
+ fruits_json_caller = ->(input, **) { input['fruits'].to_h { |e| [e['name'], e['emoji']] } }
33
+
34
+ animals_xml_matcher = ->(input, **) { !input.get_elements('//animals').empty? }
35
+ animals_xml_caller = ->(input, **) { input.get_elements('//animals').first.to_h { |e| [e['name'], e['emoji']] } }
36
+
37
+ fruits_xml_matcher = ->(input, **) { !input.get_elements('//fruits').empty? }
38
+ fruits_xml_caller = ->(input, **) { input.get_elements('//fruits').first.to_h { |e| [e['name'], e['emoji']] } }
39
+
40
+ terminator_true = ->(*) { true }
41
+
42
+ # === Tree Structure ===
43
+
44
+ tree = CallableTree::Node::Root.new.seekable.append(
45
+ CallableTree::Node::Internal.create(
46
+ matcher: json_matcher,
47
+ caller: json_caller,
48
+ terminator: terminator_true
49
+ ).seekable.append(
50
+ CallableTree::Node::External.create(matcher: animals_json_matcher, caller: animals_json_caller).verbosify,
51
+ CallableTree::Node::External.create(matcher: fruits_json_matcher, caller: fruits_json_caller).verbosify
52
+ ),
53
+ CallableTree::Node::Internal.create(
54
+ matcher: xml_matcher,
55
+ caller: xml_caller,
56
+ terminator: terminator_true
57
+ ).seekable.append(
58
+ CallableTree::Node::External.create(matcher: animals_xml_matcher, caller: animals_xml_caller).verbosify,
59
+ CallableTree::Node::External.create(matcher: fruits_xml_matcher, caller: fruits_xml_caller).verbosify
60
+ )
61
+ )
62
+
63
+ # === Execution ===
64
+
65
+ Dir.glob("#{__dir__}/../docs/*") do |file|
66
+ options = { foo: :bar }
67
+ pp tree.call(file, **options)
68
+ puts '---'
69
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../lib/callable_tree'
4
+ # require 'callable_tree'
5
+
6
+ # Hooks example using Factory style with pre-defined procs
7
+ # Demonstrates before/around/after callbacks on matcher, caller, and terminator
8
+
9
+ # === Behavior Definitions ===
10
+
11
+ external_caller = lambda do |input, **_options|
12
+ puts "external input: #{input}"
13
+ input * 2
14
+ end
15
+
16
+ # === Tree Structure with Hooks ===
17
+
18
+ CallableTree::Node::Root.new.append(
19
+ CallableTree::Node::Internal.create(hookable: true)
20
+ .append(external_caller)
21
+ .before_matcher do |input, **_options|
22
+ puts "before_matcher input: #{input}"
23
+ input + 1
24
+ end
25
+ .around_matcher do |input, **_options, &block|
26
+ puts "around_matcher input: #{input}"
27
+ matched = block.call
28
+ puts "around_matcher matched: #{matched}"
29
+ !matched
30
+ end
31
+ .after_matcher do |matched, **_options|
32
+ puts "after_matcher matched: #{matched}"
33
+ !matched
34
+ end
35
+ .before_caller do |input, **_options|
36
+ puts "before_caller input: #{input}"
37
+ input + 1
38
+ end
39
+ .around_caller do |input, **_options, &block|
40
+ puts "around_caller input: #{input}"
41
+ output = block.call
42
+ puts "around_caller output: #{output}"
43
+ output * input
44
+ end
45
+ .after_caller do |output, **_options|
46
+ puts "after_caller output: #{output}"
47
+ output * 2
48
+ end
49
+ .before_terminator do |output, *_inputs, **_options|
50
+ puts "before_terminator output: #{output}"
51
+ output + 1
52
+ end
53
+ .around_terminator do |output, *_inputs, **_options, &block|
54
+ puts "around_terminator output: #{output}"
55
+ terminated = block.call
56
+ puts "around_terminator terminated: #{terminated}"
57
+ !terminated
58
+ end
59
+ .after_terminator do |terminated, **_options|
60
+ puts "after_terminator terminated: #{terminated}"
61
+ !terminated
62
+ end
63
+ ).tap do |tree|
64
+ options = { foo: :bar }
65
+ output = tree.call(1, **options)
66
+ puts "result: #{output}"
67
+ end