callable_tree 0.3.11 → 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.
- checksums.yaml +4 -4
- data/.rubocop_todo.yml +8 -0
- data/AGENTS.md +15 -3
- data/CHANGELOG.md +7 -0
- data/README.md +181 -1
- data/callable_tree.gemspec +8 -2
- data/examples/builder/external-verbosify.rb +2 -1
- data/examples/builder/hooks.rb +2 -1
- data/examples/builder/identity.rb +2 -1
- data/examples/builder/internal-broadcastable.rb +2 -1
- data/examples/builder/internal-composable.rb +2 -1
- data/examples/builder/internal-seekable.rb +2 -1
- data/examples/builder/logging.rb +2 -1
- data/examples/class/external-verbosify.rb +2 -1
- data/examples/class/hooks.rb +2 -1
- data/examples/class/identity.rb +2 -1
- data/examples/class/internal-broadcastable.rb +2 -1
- data/examples/class/internal-composable.rb +2 -1
- data/examples/class/internal-seekable.rb +2 -1
- data/examples/class/logging.rb +2 -1
- data/examples/factory/external-verbosify.rb +69 -0
- data/examples/factory/hooks.rb +67 -0
- data/examples/factory/identity.rb +93 -0
- data/examples/factory/internal-broadcastable.rb +37 -0
- data/examples/factory/internal-composable.rb +37 -0
- data/examples/factory/internal-seekable.rb +71 -0
- data/examples/factory/logging.rb +121 -0
- data/lib/callable_tree/node/external/pod.rb +91 -0
- data/lib/callable_tree/node/internal/pod.rb +91 -0
- data/lib/callable_tree/version.rb +1 -1
- data/lib/callable_tree.rb +2 -0
- metadata +19 -8
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2eaeaea579c042e73c734e49279adb3321ab6008e4f6165ff4a701228217af00
|
|
4
|
+
data.tar.gz: e0c848254ac6802960723abb285423c21a477c228396cc384cd84a22fa31d565
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a144f0e3bcd69957274c4eb80008a812aa0f980163339f9aa0f501cc4ee0b63c1c1097df6dcfb2633aa449be79e620214b81f962b1c0a3b9cc81acdbc037beeb
|
|
7
|
+
data.tar.gz: 1c3c8fb5f5e4043864506b91ee7dd8b87e616ddecefdf593d9a302de92d056f8c505a483e6b6173a6ce01fe016366ccd56d58aa295f46902a7cf70c3bce039f3
|
data/.rubocop_todo.yml
CHANGED
|
@@ -27,6 +27,8 @@ Lint/UnderscorePrefixedVariableName:
|
|
|
27
27
|
- 'examples/builder/identity.rb'
|
|
28
28
|
- 'examples/builder/logging.rb'
|
|
29
29
|
- 'examples/class/logging.rb'
|
|
30
|
+
- 'examples/factory/identity.rb'
|
|
31
|
+
- 'examples/factory/logging.rb'
|
|
30
32
|
|
|
31
33
|
# Offense count: 8
|
|
32
34
|
# This cop supports safe autocorrection (--autocorrect).
|
|
@@ -74,6 +76,10 @@ Naming/FileName:
|
|
|
74
76
|
- 'examples/class/internal-broadcastable.rb'
|
|
75
77
|
- 'examples/class/internal-composable.rb'
|
|
76
78
|
- 'examples/class/internal-seekable.rb'
|
|
79
|
+
- 'examples/factory/external-verbosify.rb'
|
|
80
|
+
- 'examples/factory/internal-broadcastable.rb'
|
|
81
|
+
- 'examples/factory/internal-composable.rb'
|
|
82
|
+
- 'examples/factory/internal-seekable.rb'
|
|
77
83
|
|
|
78
84
|
# Offense count: 1
|
|
79
85
|
# Configuration parameters: Mode, AllowedMethods, AllowedPatterns, AllowBangMethods, WaywardPredicates.
|
|
@@ -103,6 +109,8 @@ Style/MultilineBlockChain:
|
|
|
103
109
|
- 'examples/builder/logging.rb'
|
|
104
110
|
- 'examples/class/hooks.rb'
|
|
105
111
|
- 'examples/class/logging.rb'
|
|
112
|
+
- 'examples/factory/hooks.rb'
|
|
113
|
+
- 'examples/factory/logging.rb'
|
|
106
114
|
- 'lib/callable_tree/node/internal/strategy/seek.rb'
|
|
107
115
|
|
|
108
116
|
# Offense count: 1
|
data/AGENTS.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Agent Guide for callable_tree
|
|
2
2
|
|
|
3
3
|
## Project Overview
|
|
4
|
-
`callable_tree` is a Ruby gem
|
|
4
|
+
`callable_tree` is a Ruby gem for building tree-structured executable workflows. It provides a framework for organizing complex logic into a tree of callable nodes, offering a structured, modular alternative to complex conditional logic. Nodes are matched against input (`match?`) and executed (`call`) in a chain from root to leaf.
|
|
5
5
|
|
|
6
6
|
## Core Concepts
|
|
7
7
|
- **Nodes**:
|
|
@@ -15,11 +15,23 @@
|
|
|
15
15
|
- `match?(input)`: Determines if a node should process the input.
|
|
16
16
|
- `call(input)`: Executes the node logic.
|
|
17
17
|
- `terminate?`: Controls when to stop traversal (mostly for `seekable`).
|
|
18
|
+
- **Hooks** (`hookable`):
|
|
19
|
+
- Enable instrumentation (logging, debugging) by adding callbacks.
|
|
20
|
+
- `before_matcher!`, `after_matcher!`: Hook into matching phase.
|
|
21
|
+
- `before_caller!`, `after_caller!`: Hook into call phase.
|
|
22
|
+
- `before_terminator!`, `after_terminator!`: Hook into termination phase.
|
|
23
|
+
- **Verbosify** (`verbosify`):
|
|
24
|
+
- Wraps External node output in `CallableTree::Node::External::Output` struct.
|
|
25
|
+
- Provides `value`, `options`, and `routes` (call path) for debugging.
|
|
18
26
|
|
|
19
27
|
## Directory Structure
|
|
20
28
|
- `lib/`: Source code.
|
|
21
29
|
- `spec/`: RSpec tests.
|
|
22
|
-
- `examples/`: Usage examples
|
|
30
|
+
- `examples/`: Usage examples.
|
|
31
|
+
- `examples/class/`: Class-style node definitions (using `include CallableTree::Node::*`).
|
|
32
|
+
- `examples/builder/`: Builder-style definitions (using `Builder.new.matcher { }.caller { }.build`).
|
|
33
|
+
- `examples/factory/`: Factory-style definitions (using `External.create(caller: ...)` or `External::Pod.new`).
|
|
34
|
+
- `examples/docs/`: Sample data files (JSON, XML) used by examples.
|
|
23
35
|
|
|
24
36
|
## Development
|
|
25
37
|
- **Tool Version Manager**: mise
|
|
@@ -41,4 +53,4 @@
|
|
|
41
53
|
## Architecture
|
|
42
54
|
- **Composite Pattern**: Used for `Internal` nodes to treat individual objects and compositions uniformly.
|
|
43
55
|
- **Builder Pattern**: `CallableTree::Node::Internal::Builder` and `CallableTree::Node::External::Builder` provide a fluent interface for constructing complex trees.
|
|
44
|
-
|
|
56
|
+
- **Pod Pattern**: `CallableTree::Node::Internal::Pod` and `CallableTree::Node::External::Pod` enable inline node creation via `External.create` / `Internal.create` factory methods with proc-based behaviors.
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.3.12] - 2026-01-07
|
|
4
|
+
|
|
5
|
+
- Add Factory style for inline node creation as a third option alongside Class and Builder styles.
|
|
6
|
+
- `CallableTree::Node::External.create` and `CallableTree::Node::Internal.create` factory methods
|
|
7
|
+
- Supports `hookable: true` option for Hooks (before/around/after callbacks)
|
|
8
|
+
- See `examples/factory/*.rb` for details.
|
|
9
|
+
|
|
3
10
|
## [0.3.11] - 2026-01-03
|
|
4
11
|
|
|
5
12
|
- Fix a typo in `Strategizable#strategize` where it incorrectly called `strategy!` instead of `strategize!`.`
|
data/README.md
CHANGED
|
@@ -3,6 +3,10 @@
|
|
|
3
3
|
[](https://github.com/jsmmr/ruby_callable_tree/actions/workflows/build.yml)
|
|
4
4
|
[](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:
|
|
@@ -32,7 +36,7 @@ Builds a tree of `CallableTree` nodes. Invokes the `call` method on nodes where
|
|
|
32
36
|
|
|
33
37
|
### Basic
|
|
34
38
|
|
|
35
|
-
There are
|
|
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
|
|
|
@@ -257,6 +261,84 @@ Run `examples/builder/internal-seekable.rb`:
|
|
|
257
261
|
---
|
|
258
262
|
```
|
|
259
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
|
+
|
|
260
342
|
#### `CallableTree::Node::Internal#broadcastable`
|
|
261
343
|
|
|
262
344
|
This strategy broadcasts input to all child nodes and returns their results as an array. It ignores child `terminate?` methods by default.
|
|
@@ -403,6 +485,55 @@ Run `examples/builder/internal-broadcastable.rb`:
|
|
|
403
485
|
10 -> [nil, nil]
|
|
404
486
|
```
|
|
405
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
|
+
|
|
406
537
|
#### `CallableTree::Node::Internal#composable`
|
|
407
538
|
|
|
408
539
|
This strategy chains child nodes, passing the output of the previous node as input to the next.
|
|
@@ -550,6 +681,55 @@ Run `examples/builder/internal-composable.rb`:
|
|
|
550
681
|
10 -> 10
|
|
551
682
|
```
|
|
552
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
|
+
|
|
553
733
|
### Advanced
|
|
554
734
|
|
|
555
735
|
#### `CallableTree::Node::External#verbosify`
|
data/callable_tree.gemspec
CHANGED
|
@@ -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
|
|
12
|
-
spec.description
|
|
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')
|
data/examples/builder/hooks.rb
CHANGED
data/examples/builder/logging.rb
CHANGED
data/examples/class/hooks.rb
CHANGED
data/examples/class/identity.rb
CHANGED
data/examples/class/logging.rb
CHANGED
|
@@ -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
|
|
@@ -0,0 +1,93 @@
|
|
|
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
|
+
# Identity example using Factory style with pre-defined procs
|
|
9
|
+
# Custom identity for each node - _node_: is used here
|
|
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
|
+
json_identifier = ->(_node_:) { _node_.object_id }
|
|
21
|
+
|
|
22
|
+
xml_matcher = ->(input, **) { File.extname(input) == '.xml' }
|
|
23
|
+
xml_caller = lambda do |input, **options, &block|
|
|
24
|
+
File.open(input) do |file|
|
|
25
|
+
block.call(REXML::Document.new(file), **options)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
xml_identifier = ->(_node_:) { _node_.object_id }
|
|
29
|
+
|
|
30
|
+
animals_json_matcher = ->(input, **) { !input['animals'].nil? }
|
|
31
|
+
animals_json_caller = ->(input, **) { input['animals'].to_h { |e| [e['name'], e['emoji']] } }
|
|
32
|
+
animals_json_identifier = ->(_node_:) { _node_.object_id }
|
|
33
|
+
|
|
34
|
+
fruits_json_matcher = ->(input, **) { !input['fruits'].nil? }
|
|
35
|
+
fruits_json_caller = ->(input, **) { input['fruits'].to_h { |e| [e['name'], e['emoji']] } }
|
|
36
|
+
fruits_json_identifier = ->(_node_:) { _node_.object_id }
|
|
37
|
+
|
|
38
|
+
animals_xml_matcher = ->(input, **) { !input.get_elements('//animals').empty? }
|
|
39
|
+
animals_xml_caller = ->(input, **) { input.get_elements('//animals').first.to_h { |e| [e['name'], e['emoji']] } }
|
|
40
|
+
animals_xml_identifier = ->(_node_:) { _node_.object_id }
|
|
41
|
+
|
|
42
|
+
fruits_xml_matcher = ->(input, **) { !input.get_elements('//fruits').empty? }
|
|
43
|
+
fruits_xml_caller = ->(input, **) { input.get_elements('//fruits').first.to_h { |e| [e['name'], e['emoji']] } }
|
|
44
|
+
fruits_xml_identifier = ->(_node_:) { _node_.object_id }
|
|
45
|
+
|
|
46
|
+
terminator_true = ->(*) { true }
|
|
47
|
+
|
|
48
|
+
# === Tree Structure ===
|
|
49
|
+
|
|
50
|
+
tree = CallableTree::Node::Root.new.seekable.append(
|
|
51
|
+
CallableTree::Node::Internal.create(
|
|
52
|
+
matcher: json_matcher,
|
|
53
|
+
caller: json_caller,
|
|
54
|
+
terminator: terminator_true,
|
|
55
|
+
identifier: json_identifier
|
|
56
|
+
).seekable.append(
|
|
57
|
+
CallableTree::Node::External.create(
|
|
58
|
+
matcher: animals_json_matcher,
|
|
59
|
+
caller: animals_json_caller,
|
|
60
|
+
identifier: animals_json_identifier
|
|
61
|
+
).verbosify,
|
|
62
|
+
CallableTree::Node::External.create(
|
|
63
|
+
matcher: fruits_json_matcher,
|
|
64
|
+
caller: fruits_json_caller,
|
|
65
|
+
identifier: fruits_json_identifier
|
|
66
|
+
).verbosify
|
|
67
|
+
),
|
|
68
|
+
CallableTree::Node::Internal.create(
|
|
69
|
+
matcher: xml_matcher,
|
|
70
|
+
caller: xml_caller,
|
|
71
|
+
terminator: terminator_true,
|
|
72
|
+
identifier: xml_identifier
|
|
73
|
+
).seekable.append(
|
|
74
|
+
CallableTree::Node::External.create(
|
|
75
|
+
matcher: animals_xml_matcher,
|
|
76
|
+
caller: animals_xml_caller,
|
|
77
|
+
identifier: animals_xml_identifier
|
|
78
|
+
).verbosify,
|
|
79
|
+
CallableTree::Node::External.create(
|
|
80
|
+
matcher: fruits_xml_matcher,
|
|
81
|
+
caller: fruits_xml_caller,
|
|
82
|
+
identifier: fruits_xml_identifier
|
|
83
|
+
).verbosify
|
|
84
|
+
)
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
# === Execution ===
|
|
88
|
+
|
|
89
|
+
Dir.glob("#{__dir__}/../docs/*") do |file|
|
|
90
|
+
options = { foo: :bar }
|
|
91
|
+
pp tree.call(file, **options)
|
|
92
|
+
puts '---'
|
|
93
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../../lib/callable_tree'
|
|
4
|
+
# require 'callable_tree'
|
|
5
|
+
|
|
6
|
+
# Broadcastable example using Factory style with pre-defined procs
|
|
7
|
+
# All matching child nodes are called and results are collected
|
|
8
|
+
|
|
9
|
+
# === Behavior Definitions ===
|
|
10
|
+
|
|
11
|
+
less_than_5_matcher = ->(input, **, &original) { original.call(input) && input < 5 }
|
|
12
|
+
less_than_10_matcher = ->(input, **, &original) { original.call(input) && input < 10 }
|
|
13
|
+
|
|
14
|
+
multiply_2_caller = ->(input, **) { input * 2 }
|
|
15
|
+
add_1_caller = ->(input, **) { input + 1 }
|
|
16
|
+
multiply_3_caller = ->(input, **) { input * 3 }
|
|
17
|
+
subtract_1_caller = ->(input, **) { input - 1 }
|
|
18
|
+
|
|
19
|
+
# === Tree Structure ===
|
|
20
|
+
|
|
21
|
+
tree = CallableTree::Node::Root.new.broadcastable.append(
|
|
22
|
+
CallableTree::Node::Internal.create(matcher: less_than_5_matcher).broadcastable.append(
|
|
23
|
+
CallableTree::Node::External.create(caller: multiply_2_caller),
|
|
24
|
+
CallableTree::Node::External.create(caller: add_1_caller)
|
|
25
|
+
),
|
|
26
|
+
CallableTree::Node::Internal.create(matcher: less_than_10_matcher).broadcastable.append(
|
|
27
|
+
CallableTree::Node::External.create(caller: multiply_3_caller),
|
|
28
|
+
CallableTree::Node::External.create(caller: subtract_1_caller)
|
|
29
|
+
)
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
# === Execution ===
|
|
33
|
+
|
|
34
|
+
(0..10).each do |input|
|
|
35
|
+
output = tree.call(input)
|
|
36
|
+
puts "#{input} -> #{output}"
|
|
37
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../../lib/callable_tree'
|
|
4
|
+
# require 'callable_tree'
|
|
5
|
+
|
|
6
|
+
# Composable example using Factory style with pre-defined procs
|
|
7
|
+
# Output of one node becomes input to the next (pipeline)
|
|
8
|
+
|
|
9
|
+
# === Behavior Definitions ===
|
|
10
|
+
|
|
11
|
+
less_than_5_matcher = ->(input, **, &original) { original.call(input) && input < 5 }
|
|
12
|
+
less_than_10_matcher = ->(input, **, &original) { original.call(input) && input < 10 }
|
|
13
|
+
|
|
14
|
+
multiply_2_caller = ->(input, **) { input * 2 }
|
|
15
|
+
add_1_caller = ->(input, **) { input + 1 }
|
|
16
|
+
multiply_3_caller = ->(input, **) { input * 3 }
|
|
17
|
+
subtract_1_caller = ->(input, **) { input - 1 }
|
|
18
|
+
|
|
19
|
+
# === Tree Structure ===
|
|
20
|
+
|
|
21
|
+
tree = CallableTree::Node::Root.new.composable.append(
|
|
22
|
+
CallableTree::Node::Internal.create(matcher: less_than_5_matcher).composable.append(
|
|
23
|
+
CallableTree::Node::External.create(caller: multiply_2_caller),
|
|
24
|
+
CallableTree::Node::External.create(caller: add_1_caller)
|
|
25
|
+
),
|
|
26
|
+
CallableTree::Node::Internal.create(matcher: less_than_10_matcher).composable.append(
|
|
27
|
+
CallableTree::Node::External.create(caller: multiply_3_caller),
|
|
28
|
+
CallableTree::Node::External.create(caller: subtract_1_caller)
|
|
29
|
+
)
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
# === Execution ===
|
|
33
|
+
|
|
34
|
+
(0..10).each do |input|
|
|
35
|
+
output = tree.call(input)
|
|
36
|
+
puts "#{input} -> #{output}"
|
|
37
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
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
|
+
# Seekable example using Factory style with pre-defined procs
|
|
9
|
+
# This demonstrates the key difference from Builder style:
|
|
10
|
+
# - Behaviors are defined as procs first
|
|
11
|
+
# - Tree structure is assembled separately, making it clearly visible
|
|
12
|
+
|
|
13
|
+
# === Behavior Definitions ===
|
|
14
|
+
|
|
15
|
+
json_matcher = ->(input, **) { File.extname(input) == '.json' }
|
|
16
|
+
json_caller = lambda do |input, **options, &original|
|
|
17
|
+
File.open(input) do |file|
|
|
18
|
+
json = JSON.parse(file.read)
|
|
19
|
+
original.call(json, **options)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
xml_matcher = ->(input, **) { File.extname(input) == '.xml' }
|
|
24
|
+
xml_caller = lambda do |input, **options, &original|
|
|
25
|
+
File.open(input) do |file|
|
|
26
|
+
original.call(REXML::Document.new(file), **options)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
animals_json_matcher = ->(input, **) { !input['animals'].nil? }
|
|
31
|
+
animals_json_caller = ->(input, **) { input['animals'].to_h { |e| [e['name'], e['emoji']] } }
|
|
32
|
+
|
|
33
|
+
fruits_json_matcher = ->(input, **) { !input['fruits'].nil? }
|
|
34
|
+
fruits_json_caller = ->(input, **) { input['fruits'].to_h { |e| [e['name'], e['emoji']] } }
|
|
35
|
+
|
|
36
|
+
animals_xml_matcher = ->(input, **) { !input.get_elements('//animals').empty? }
|
|
37
|
+
animals_xml_caller = ->(input, **) { input.get_elements('//animals').first.to_h { |e| [e['name'], e['emoji']] } }
|
|
38
|
+
|
|
39
|
+
fruits_xml_matcher = ->(input, **) { !input.get_elements('//fruits').empty? }
|
|
40
|
+
fruits_xml_caller = ->(input, **) { input.get_elements('//fruits').first.to_h { |e| [e['name'], e['emoji']] } }
|
|
41
|
+
|
|
42
|
+
terminator_true = ->(*) { true }
|
|
43
|
+
|
|
44
|
+
# === Tree Structure (clearly visible!) ===
|
|
45
|
+
|
|
46
|
+
tree = CallableTree::Node::Root.new.seekable.append(
|
|
47
|
+
CallableTree::Node::Internal.create(
|
|
48
|
+
matcher: json_matcher,
|
|
49
|
+
caller: json_caller,
|
|
50
|
+
terminator: terminator_true
|
|
51
|
+
).seekable.append(
|
|
52
|
+
CallableTree::Node::External.create(matcher: animals_json_matcher, caller: animals_json_caller),
|
|
53
|
+
CallableTree::Node::External.create(matcher: fruits_json_matcher, caller: fruits_json_caller)
|
|
54
|
+
),
|
|
55
|
+
CallableTree::Node::Internal.create(
|
|
56
|
+
matcher: xml_matcher,
|
|
57
|
+
caller: xml_caller,
|
|
58
|
+
terminator: terminator_true
|
|
59
|
+
).seekable.append(
|
|
60
|
+
CallableTree::Node::External.create(matcher: animals_xml_matcher, caller: animals_xml_caller),
|
|
61
|
+
CallableTree::Node::External.create(matcher: fruits_xml_matcher, caller: fruits_xml_caller)
|
|
62
|
+
)
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
# === Execution ===
|
|
66
|
+
|
|
67
|
+
Dir.glob("#{__dir__}/../docs/*") do |file|
|
|
68
|
+
options = { foo: :bar }
|
|
69
|
+
pp tree.call(file, **options)
|
|
70
|
+
puts '---'
|
|
71
|
+
end
|
|
@@ -0,0 +1,121 @@
|
|
|
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
|
+
# Logging example using Factory style with pre-defined procs
|
|
9
|
+
# Uses hooks to add logging to the tree - _node_: is used in hooks
|
|
10
|
+
|
|
11
|
+
# === Behavior Definitions ===
|
|
12
|
+
|
|
13
|
+
json_matcher = ->(input, **) { File.extname(input) == '.json' }
|
|
14
|
+
json_caller = lambda do |input, **options, &original|
|
|
15
|
+
File.open(input) do |file|
|
|
16
|
+
json = JSON.parse(file.read)
|
|
17
|
+
original.call(json, **options)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
xml_matcher = ->(input, **) { File.extname(input) == '.xml' }
|
|
22
|
+
xml_caller = lambda do |input, **options, &original|
|
|
23
|
+
File.open(input) do |file|
|
|
24
|
+
original.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
|
+
# === Logging Module (uses _node_:) ===
|
|
43
|
+
|
|
44
|
+
module Logging
|
|
45
|
+
INDENT_SIZE = 2
|
|
46
|
+
BLANK = ' '
|
|
47
|
+
LIST_STYLE = '*'
|
|
48
|
+
INPUT_LABEL = 'Input :'
|
|
49
|
+
OUTPUT_LABEL = 'Output:'
|
|
50
|
+
|
|
51
|
+
def self.loggable(node)
|
|
52
|
+
node.after_matcher! do |matched, _node_:, **|
|
|
53
|
+
prefix = LIST_STYLE.rjust((_node_.depth * INDENT_SIZE) - INDENT_SIZE + LIST_STYLE.length, BLANK)
|
|
54
|
+
puts "#{prefix} #{_node_.identity}: [matched: #{matched}]"
|
|
55
|
+
matched
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
return unless node.external?
|
|
59
|
+
|
|
60
|
+
node
|
|
61
|
+
.before_caller! do |input, *, _node_:, **|
|
|
62
|
+
input_prefix = INPUT_LABEL.rjust((_node_.depth * INDENT_SIZE) + INPUT_LABEL.length, BLANK)
|
|
63
|
+
puts "#{input_prefix} #{input}"
|
|
64
|
+
input
|
|
65
|
+
end
|
|
66
|
+
.after_caller! do |output, _node_:, **|
|
|
67
|
+
output_prefix = OUTPUT_LABEL.rjust((_node_.depth * INDENT_SIZE) + OUTPUT_LABEL.length, BLANK)
|
|
68
|
+
puts "#{output_prefix} #{output}"
|
|
69
|
+
output
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
loggable = Logging.method(:loggable)
|
|
75
|
+
|
|
76
|
+
# === Tree Structure ===
|
|
77
|
+
|
|
78
|
+
tree = CallableTree::Node::Root.new.seekable.append(
|
|
79
|
+
CallableTree::Node::Internal.create(
|
|
80
|
+
matcher: json_matcher,
|
|
81
|
+
caller: json_caller,
|
|
82
|
+
terminator: terminator_true,
|
|
83
|
+
hookable: true
|
|
84
|
+
).tap(&loggable).seekable.append(
|
|
85
|
+
CallableTree::Node::External.create(
|
|
86
|
+
matcher: animals_json_matcher,
|
|
87
|
+
caller: animals_json_caller,
|
|
88
|
+
hookable: true
|
|
89
|
+
).tap(&loggable).verbosify,
|
|
90
|
+
CallableTree::Node::External.create(
|
|
91
|
+
matcher: fruits_json_matcher,
|
|
92
|
+
caller: fruits_json_caller,
|
|
93
|
+
hookable: true
|
|
94
|
+
).tap(&loggable).verbosify
|
|
95
|
+
),
|
|
96
|
+
CallableTree::Node::Internal.create(
|
|
97
|
+
matcher: xml_matcher,
|
|
98
|
+
caller: xml_caller,
|
|
99
|
+
terminator: terminator_true,
|
|
100
|
+
hookable: true
|
|
101
|
+
).tap(&loggable).seekable.append(
|
|
102
|
+
CallableTree::Node::External.create(
|
|
103
|
+
matcher: animals_xml_matcher,
|
|
104
|
+
caller: animals_xml_caller,
|
|
105
|
+
hookable: true
|
|
106
|
+
).tap(&loggable).verbosify,
|
|
107
|
+
CallableTree::Node::External.create(
|
|
108
|
+
matcher: fruits_xml_matcher,
|
|
109
|
+
caller: fruits_xml_caller,
|
|
110
|
+
hookable: true
|
|
111
|
+
).tap(&loggable).verbosify
|
|
112
|
+
)
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
# === Execution ===
|
|
116
|
+
|
|
117
|
+
Dir.glob("#{__dir__}/../docs/*") do |file|
|
|
118
|
+
options = { foo: :bar }
|
|
119
|
+
pp tree.call(file, **options)
|
|
120
|
+
puts '---'
|
|
121
|
+
end
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CallableTree
|
|
4
|
+
module Node
|
|
5
|
+
module External
|
|
6
|
+
# Pod: A node that can be instantiated directly with proc-based behavior.
|
|
7
|
+
# Provides an alternative to Builder style for inline node creation.
|
|
8
|
+
#
|
|
9
|
+
# Usage patterns:
|
|
10
|
+
# Constructor style: Pod.new(caller: ->(input, **) { input * 2 })
|
|
11
|
+
# Factory style: External.create(caller: ->(input, **) { input * 2 })
|
|
12
|
+
# Block style: External.create { |node| node.caller { |input, **| input * 2 } }
|
|
13
|
+
class Pod
|
|
14
|
+
include External
|
|
15
|
+
|
|
16
|
+
def initialize(matcher: nil, caller: nil, terminator: nil, identifier: nil)
|
|
17
|
+
@_matcher = matcher
|
|
18
|
+
@_caller = caller
|
|
19
|
+
@_terminator = terminator
|
|
20
|
+
@_identifier = identifier
|
|
21
|
+
|
|
22
|
+
yield self if block_given?
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# DSL setters for block syntax
|
|
26
|
+
def matcher(proc = nil, &block)
|
|
27
|
+
@_matcher = proc || block
|
|
28
|
+
self
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def caller(proc = nil, &block)
|
|
32
|
+
@_caller = proc || block
|
|
33
|
+
self
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def terminator(proc = nil, &block)
|
|
37
|
+
@_terminator = proc || block
|
|
38
|
+
self
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def identifier(proc = nil, &block)
|
|
42
|
+
@_identifier = proc || block
|
|
43
|
+
self
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def match?(*inputs, **options)
|
|
47
|
+
return super unless @_matcher
|
|
48
|
+
|
|
49
|
+
@_matcher.call(*inputs, **options, _node_: self) do |*a, **o|
|
|
50
|
+
super(*a, **o)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def call(*inputs, **options)
|
|
55
|
+
raise ::CallableTree::Error, 'caller is not set' unless @_caller
|
|
56
|
+
|
|
57
|
+
@_caller.call(*inputs, **options, _node_: self) do |*a, **o|
|
|
58
|
+
super(*a, **o)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def terminate?(output, *inputs, **options)
|
|
63
|
+
return super unless @_terminator
|
|
64
|
+
|
|
65
|
+
@_terminator.call(output, *inputs, **options, _node_: self) do |o, *a, **opts|
|
|
66
|
+
super(o, *a, **opts)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def identity
|
|
71
|
+
return super unless @_identifier
|
|
72
|
+
|
|
73
|
+
@_identifier.call(_node_: self) { super }
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# HookablePod: Pod with Hooks support (before/around/after callbacks).
|
|
78
|
+
class HookablePod < Pod
|
|
79
|
+
prepend Hooks::Matcher
|
|
80
|
+
prepend Hooks::Caller
|
|
81
|
+
prepend Hooks::Terminator
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Factory method
|
|
85
|
+
def self.create(matcher: nil, caller: nil, terminator: nil, identifier: nil, hookable: false, &block)
|
|
86
|
+
klass = hookable ? HookablePod : Pod
|
|
87
|
+
klass.new(matcher: matcher, caller: caller, terminator: terminator, identifier: identifier, &block)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CallableTree
|
|
4
|
+
module Node
|
|
5
|
+
module Internal
|
|
6
|
+
# Pod: A node that can be instantiated directly with proc-based behavior.
|
|
7
|
+
# Provides an alternative to Builder style for inline node creation.
|
|
8
|
+
#
|
|
9
|
+
# Usage patterns:
|
|
10
|
+
# Constructor style: Pod.new(caller: ->(input, **) { input * 2 })
|
|
11
|
+
# Factory style: Internal.create(caller: ->(input, **) { input * 2 })
|
|
12
|
+
# Block style: Internal.create { |node| node.caller { |input, **| input * 2 } }
|
|
13
|
+
class Pod
|
|
14
|
+
include Internal
|
|
15
|
+
|
|
16
|
+
def initialize(matcher: nil, caller: nil, terminator: nil, identifier: nil)
|
|
17
|
+
@_matcher = matcher
|
|
18
|
+
@_caller = caller
|
|
19
|
+
@_terminator = terminator
|
|
20
|
+
@_identifier = identifier
|
|
21
|
+
|
|
22
|
+
yield self if block_given?
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# DSL setters for block syntax
|
|
26
|
+
def matcher(proc = nil, &block)
|
|
27
|
+
@_matcher = proc || block
|
|
28
|
+
self
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def caller(proc = nil, &block)
|
|
32
|
+
@_caller = proc || block
|
|
33
|
+
self
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def terminator(proc = nil, &block)
|
|
37
|
+
@_terminator = proc || block
|
|
38
|
+
self
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def identifier(proc = nil, &block)
|
|
42
|
+
@_identifier = proc || block
|
|
43
|
+
self
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def match?(*inputs, **options)
|
|
47
|
+
return super unless @_matcher
|
|
48
|
+
|
|
49
|
+
@_matcher.call(*inputs, **options, _node_: self) do |*a, **o|
|
|
50
|
+
super(*a, **o)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def call(*inputs, **options)
|
|
55
|
+
return super unless @_caller
|
|
56
|
+
|
|
57
|
+
@_caller.call(*inputs, **options, _node_: self) do |*a, **o|
|
|
58
|
+
super(*a, **o)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def terminate?(output, *inputs, **options)
|
|
63
|
+
return super unless @_terminator
|
|
64
|
+
|
|
65
|
+
@_terminator.call(output, *inputs, **options, _node_: self) do |o, *a, **opts|
|
|
66
|
+
super(o, *a, **opts)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def identity
|
|
71
|
+
return super unless @_identifier
|
|
72
|
+
|
|
73
|
+
@_identifier.call(_node_: self) { super }
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# HookablePod: Pod with Hooks support (before/around/after callbacks).
|
|
78
|
+
class HookablePod < Pod
|
|
79
|
+
prepend Hooks::Matcher
|
|
80
|
+
prepend Hooks::Caller
|
|
81
|
+
prepend Hooks::Terminator
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Factory method
|
|
85
|
+
def self.create(matcher: nil, caller: nil, terminator: nil, identifier: nil, hookable: false, &block)
|
|
86
|
+
klass = hookable ? HookablePod : Pod
|
|
87
|
+
klass.new(matcher: matcher, caller: caller, terminator: terminator, identifier: identifier, &block)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
data/lib/callable_tree.rb
CHANGED
|
@@ -18,6 +18,8 @@ require_relative 'callable_tree/node/internal/strategizable'
|
|
|
18
18
|
require_relative 'callable_tree/node/external/verbose'
|
|
19
19
|
require_relative 'callable_tree/node/external'
|
|
20
20
|
require_relative 'callable_tree/node/internal'
|
|
21
|
+
require_relative 'callable_tree/node/external/pod'
|
|
22
|
+
require_relative 'callable_tree/node/internal/pod'
|
|
21
23
|
require_relative 'callable_tree/node/builder'
|
|
22
24
|
require_relative 'callable_tree/node/internal/builder'
|
|
23
25
|
require_relative 'callable_tree/node/external/builder'
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: callable_tree
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.3.
|
|
4
|
+
version: 0.3.12
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- jsmmr
|
|
@@ -9,9 +9,12 @@ bindir: exe
|
|
|
9
9
|
cert_chain: []
|
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
11
|
dependencies: []
|
|
12
|
-
description:
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
description: |
|
|
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.
|
|
15
18
|
email:
|
|
16
19
|
- jsmmr@icloud.com
|
|
17
20
|
executables: []
|
|
@@ -54,17 +57,26 @@ files:
|
|
|
54
57
|
- examples/docs/animals.xml
|
|
55
58
|
- examples/docs/fruits.json
|
|
56
59
|
- examples/docs/fruits.xml
|
|
60
|
+
- examples/factory/external-verbosify.rb
|
|
61
|
+
- examples/factory/hooks.rb
|
|
62
|
+
- examples/factory/identity.rb
|
|
63
|
+
- examples/factory/internal-broadcastable.rb
|
|
64
|
+
- examples/factory/internal-composable.rb
|
|
65
|
+
- examples/factory/internal-seekable.rb
|
|
66
|
+
- examples/factory/logging.rb
|
|
57
67
|
- lib/callable_tree.rb
|
|
58
68
|
- lib/callable_tree/node.rb
|
|
59
69
|
- lib/callable_tree/node/builder.rb
|
|
60
70
|
- lib/callable_tree/node/external.rb
|
|
61
71
|
- lib/callable_tree/node/external/builder.rb
|
|
72
|
+
- lib/callable_tree/node/external/pod.rb
|
|
62
73
|
- lib/callable_tree/node/external/verbose.rb
|
|
63
74
|
- lib/callable_tree/node/hooks/caller.rb
|
|
64
75
|
- lib/callable_tree/node/hooks/matcher.rb
|
|
65
76
|
- lib/callable_tree/node/hooks/terminator.rb
|
|
66
77
|
- lib/callable_tree/node/internal.rb
|
|
67
78
|
- lib/callable_tree/node/internal/builder.rb
|
|
79
|
+
- lib/callable_tree/node/internal/pod.rb
|
|
68
80
|
- lib/callable_tree/node/internal/strategizable.rb
|
|
69
81
|
- lib/callable_tree/node/internal/strategy.rb
|
|
70
82
|
- lib/callable_tree/node/internal/strategy/broadcast.rb
|
|
@@ -79,7 +91,7 @@ licenses:
|
|
|
79
91
|
metadata:
|
|
80
92
|
homepage_uri: https://github.com/jsmmr/ruby_callable_tree
|
|
81
93
|
source_code_uri: https://github.com/jsmmr/ruby_callable_tree
|
|
82
|
-
changelog_uri: https://github.com/jsmmr/ruby_callable_tree/blob/v0.3.
|
|
94
|
+
changelog_uri: https://github.com/jsmmr/ruby_callable_tree/blob/v0.3.12/CHANGELOG.md
|
|
83
95
|
rubygems_mfa_required: 'true'
|
|
84
96
|
rdoc_options: []
|
|
85
97
|
require_paths:
|
|
@@ -97,7 +109,6 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
97
109
|
requirements: []
|
|
98
110
|
rubygems_version: 4.0.3
|
|
99
111
|
specification_version: 4
|
|
100
|
-
summary: Builds
|
|
101
|
-
|
|
102
|
-
`if` or `case` expressions.
|
|
112
|
+
summary: Builds executable trees of callable nodes with flexible strategies like seek,
|
|
113
|
+
broadcast, and compose.
|
|
103
114
|
test_files: []
|