Skip to content

Commit 304a905

Browse files
committed
feat(factory): add Factory style for inline node creation
1 parent 8ad51fa commit 304a905

File tree

14 files changed

+990
-1
lines changed

14 files changed

+990
-1
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

CHANGELOG.md

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

3+
- Add Factory style for inline node creation as a third option alongside Class and Builder styles.
4+
- `CallableTree::Node::External::Pod` and `CallableTree::Node::Internal::Pod` classes
5+
- `CallableTree::Node::External.create` and `CallableTree::Node::Internal.create` factory methods
6+
- Supports `hookable: true` option for Hooks (before/around/after callbacks)
7+
- See `examples/factory/*.rb` for details.
8+
39
## [0.3.11] - 2026-01-03
410

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

README.md

Lines changed: 177 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ Builds a tree of `CallableTree` nodes. Invokes the `call` method on nodes where
3636

3737
### Basic
3838

39-
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.
4040

4141
#### `CallableTree::Node::Internal#seekable` (default strategy)
4242

@@ -261,6 +261,84 @@ Run `examples/builder/internal-seekable.rb`:
261261
---
262262
```
263263

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

266344
This strategy broadcasts input to all child nodes and returns their results as an array. It ignores child `terminate?` methods by default.
@@ -407,6 +485,55 @@ Run `examples/builder/internal-broadcastable.rb`:
407485
10 -> [nil, nil]
408486
```
409487
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+
410537
#### `CallableTree::Node::Internal#composable`
411538
412539
This strategy chains child nodes, passing the output of the previous node as input to the next.
@@ -554,6 +681,55 @@ Run `examples/builder/internal-composable.rb`:
554681
10 -> 10
555682
```
556683
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+
557733
### Advanced
558734
559735
#### `CallableTree::Node::External#verbosify`
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
# frozen_string_literal: true
2+
3+
require_relative '../../lib/callable_tree'
4+
require 'json'
5+
require 'rexml/document'
6+
7+
# Verbosify example using Pod style with pre-defined procs
8+
# Shows verbose output including route information
9+
10+
# === Behavior Definitions ===
11+
12+
json_matcher = ->(input, **) { File.extname(input) == '.json' }
13+
json_caller = lambda do |input, **options, &block|
14+
File.open(input) do |file|
15+
json = JSON.parse(file.read)
16+
block.call(json, **options)
17+
end
18+
end
19+
20+
xml_matcher = ->(input, **) { File.extname(input) == '.xml' }
21+
xml_caller = lambda do |input, **options, &block|
22+
File.open(input) do |file|
23+
block.call(REXML::Document.new(file), **options)
24+
end
25+
end
26+
27+
animals_json_matcher = ->(input, **) { !input['animals'].nil? }
28+
animals_json_caller = ->(input, **) { input['animals'].to_h { |e| [e['name'], e['emoji']] } }
29+
30+
fruits_json_matcher = ->(input, **) { !input['fruits'].nil? }
31+
fruits_json_caller = ->(input, **) { input['fruits'].to_h { |e| [e['name'], e['emoji']] } }
32+
33+
animals_xml_matcher = ->(input, **) { !input.get_elements('//animals').empty? }
34+
animals_xml_caller = ->(input, **) { input.get_elements('//animals').first.to_h { |e| [e['name'], e['emoji']] } }
35+
36+
fruits_xml_matcher = ->(input, **) { !input.get_elements('//fruits').empty? }
37+
fruits_xml_caller = ->(input, **) { input.get_elements('//fruits').first.to_h { |e| [e['name'], e['emoji']] } }
38+
39+
terminator_true = ->(*) { true }
40+
41+
# === Tree Structure ===
42+
43+
tree = CallableTree::Node::Root.new.seekable.append(
44+
CallableTree::Node::Internal.create(
45+
matcher: json_matcher,
46+
caller: json_caller,
47+
terminator: terminator_true
48+
).seekable.append(
49+
CallableTree::Node::External.create(matcher: animals_json_matcher, caller: animals_json_caller).verbosify,
50+
CallableTree::Node::External.create(matcher: fruits_json_matcher, caller: fruits_json_caller).verbosify
51+
),
52+
CallableTree::Node::Internal.create(
53+
matcher: xml_matcher,
54+
caller: xml_caller,
55+
terminator: terminator_true
56+
).seekable.append(
57+
CallableTree::Node::External.create(matcher: animals_xml_matcher, caller: animals_xml_caller).verbosify,
58+
CallableTree::Node::External.create(matcher: fruits_xml_matcher, caller: fruits_xml_caller).verbosify
59+
)
60+
)
61+
62+
# === Execution ===
63+
64+
Dir.glob("#{__dir__}/../docs/*") do |file|
65+
options = { foo: :bar }
66+
pp tree.call(file, **options)
67+
puts '---'
68+
end

examples/factory/hooks.rb

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
# frozen_string_literal: true
2+
3+
require_relative '../../lib/callable_tree'
4+
5+
# Hooks example using Pod style with pre-defined procs
6+
# Demonstrates before/around/after callbacks on matcher, caller, and terminator
7+
8+
# === Behavior Definitions ===
9+
10+
external_caller = lambda do |input, **_options|
11+
puts "external input: #{input}"
12+
input * 2
13+
end
14+
15+
# === Tree Structure with Hooks ===
16+
17+
CallableTree::Node::Root.new.append(
18+
CallableTree::Node::Internal.create(hookable: true)
19+
.append(external_caller)
20+
.before_matcher do |input, **_options|
21+
puts "before_matcher input: #{input}"
22+
input + 1
23+
end
24+
.around_matcher do |input, **_options, &block|
25+
puts "around_matcher input: #{input}"
26+
matched = block.call
27+
puts "around_matcher matched: #{matched}"
28+
!matched
29+
end
30+
.after_matcher do |matched, **_options|
31+
puts "after_matcher matched: #{matched}"
32+
!matched
33+
end
34+
.before_caller do |input, **_options|
35+
puts "before_caller input: #{input}"
36+
input + 1
37+
end
38+
.around_caller do |input, **_options, &block|
39+
puts "around_caller input: #{input}"
40+
output = block.call
41+
puts "around_caller output: #{output}"
42+
output * input
43+
end
44+
.after_caller do |output, **_options|
45+
puts "after_caller output: #{output}"
46+
output * 2
47+
end
48+
.before_terminator do |output, *_inputs, **_options|
49+
puts "before_terminator output: #{output}"
50+
output + 1
51+
end
52+
.around_terminator do |output, *_inputs, **_options, &block|
53+
puts "around_terminator output: #{output}"
54+
terminated = block.call
55+
puts "around_terminator terminated: #{terminated}"
56+
!terminated
57+
end
58+
.after_terminator do |terminated, **_options|
59+
puts "after_terminator terminated: #{terminated}"
60+
!terminated
61+
end
62+
).tap do |tree|
63+
options = { foo: :bar }
64+
output = tree.call(1, **options)
65+
puts "result: #{output}"
66+
end

0 commit comments

Comments
 (0)