Skip to content

Commit 324de73

Browse files
committed
Merge branch 'develop'
* develop: chore: prepare release 0.3.12 docs(examples): add commented gem require line for easy switching feat(factory): add Factory style for inline node creation docs(agents): improve documentation with hooks, verbosify, and examples details docs(readme): add conceptual overview docs(gemspec): update summary and description
2 parents 76d9d63 + 3e39a5d commit 324de73

31 files changed

+1054
-21
lines changed

.rubocop_todo.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ Lint/UnderscorePrefixedVariableName:
2727
- 'examples/builder/identity.rb'
2828
- 'examples/builder/logging.rb'
2929
- 'examples/class/logging.rb'
30+
- 'examples/factory/identity.rb'
31+
- 'examples/factory/logging.rb'
3032

3133
# Offense count: 8
3234
# This cop supports safe autocorrection (--autocorrect).
@@ -74,6 +76,10 @@ Naming/FileName:
7476
- 'examples/class/internal-broadcastable.rb'
7577
- 'examples/class/internal-composable.rb'
7678
- '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'
7783

7884
# Offense count: 1
7985
# Configuration parameters: Mode, AllowedMethods, AllowedPatterns, AllowBangMethods, WaywardPredicates.
@@ -103,6 +109,8 @@ Style/MultilineBlockChain:
103109
- 'examples/builder/logging.rb'
104110
- 'examples/class/hooks.rb'
105111
- 'examples/class/logging.rb'
112+
- 'examples/factory/hooks.rb'
113+
- 'examples/factory/logging.rb'
106114
- 'lib/callable_tree/node/internal/strategy/seek.rb'
107115

108116
# Offense count: 1

AGENTS.md

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# Agent Guide for callable_tree
22

33
## Project Overview
4-
`callable_tree` is a Ruby gem that builds a tree of callable nodes. It allows for complex logic flow (like nested `if`/`case`) to be represented as a tree of objects. Nodes are traversed based on matching conditions (`match?`), and executed (`call`).
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.
55

66
## Core Concepts
77
- **Nodes**:
@@ -15,11 +15,23 @@
1515
- `match?(input)`: Determines if a node should process the input.
1616
- `call(input)`: Executes the node logic.
1717
- `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.
1826

1927
## Directory Structure
2028
- `lib/`: Source code.
2129
- `spec/`: RSpec tests.
22-
- `examples/`: Usage examples (Class-style and Builder-style).
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.
2335

2436
## Development
2537
- **Tool Version Manager**: mise
@@ -41,4 +53,4 @@
4153
## Architecture
4254
- **Composite Pattern**: Used for `Internal` nodes to treat individual objects and compositions uniformly.
4355
- **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.

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
## [Unreleased]
22

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+
310
## [0.3.11] - 2026-01-03
411

512
- Fix a typo in `Strategizable#strategize` where it incorrectly called `strategy!` instead of `strategize!`.`

README.md

Lines changed: 181 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@
33
[![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)
44
[![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)
55

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+
610
## Installation
711

812
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
3236

3337
### Basic
3438

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.
3640

3741
#### `CallableTree::Node::Internal#seekable` (default strategy)
3842

@@ -257,6 +261,84 @@ Run `examples/builder/internal-seekable.rb`:
257261
---
258262
```
259263

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+
260342
#### `CallableTree::Node::Internal#broadcastable`
261343

262344
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`:
403485
10 -> [nil, nil]
404486
```
405487
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+
406537
#### `CallableTree::Node::Internal#composable`
407538
408539
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`:
550681
10 -> 10
551682
```
552683
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+
553733
### Advanced
554734
555735
#### `CallableTree::Node::External#verbosify`

callable_tree.gemspec

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,14 @@ Gem::Specification.new do |spec|
88
spec.authors = ['jsmmr']
99
spec.email = ['jsmmr@icloud.com']
1010

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
1319
spec.homepage = 'https://github.com/jsmmr/ruby_callable_tree'
1420
spec.license = 'MIT'
1521
spec.required_ruby_version = Gem::Requirement.new('>= 2.4.0')

examples/builder/external-verbosify.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# frozen_string_literal: true
22

3-
require 'callable_tree'
3+
require_relative '../../lib/callable_tree'
4+
# require 'callable_tree'
45
require 'json'
56
require 'rexml/document'
67

examples/builder/hooks.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# frozen_string_literal: true
22

3-
require 'callable_tree'
3+
require_relative '../../lib/callable_tree'
4+
# require 'callable_tree'
45

56
HooksSample =
67
CallableTree::Node::Internal::Builder

examples/builder/identity.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# frozen_string_literal: true
22

3-
require 'callable_tree'
3+
require_relative '../../lib/callable_tree'
4+
# require 'callable_tree'
45
require 'json'
56
require 'rexml/document'
67

examples/builder/internal-broadcastable.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# frozen_string_literal: true
22

3-
require 'callable_tree'
3+
require_relative '../../lib/callable_tree'
4+
# require 'callable_tree'
45

56
def less_than(num)
67
# The following block call is equivalent to calling `super` in the class style.

examples/builder/internal-composable.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# frozen_string_literal: true
22

3-
require 'callable_tree'
3+
require_relative '../../lib/callable_tree'
4+
# require 'callable_tree'
45

56
def less_than(num)
67
# The following block call is equivalent to calling `super` in the class style.

0 commit comments

Comments
 (0)