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.
- checksums.yaml +4 -4
- data/.github/workflows/build.yml +19 -5
- data/.github/workflows/codeql-analysis.yml +4 -4
- data/.gitignore +2 -0
- data/.rubocop.yml +3 -0
- data/.rubocop_todo.yml +128 -0
- data/.ruby-version +1 -1
- data/AGENTS.md +56 -0
- data/CHANGELOG.md +11 -0
- data/CONTRIBUTING.md +68 -0
- data/Gemfile +4 -2
- data/README.md +195 -21
- data/callable_tree.gemspec +8 -2
- data/examples/builder/external-verbosify.rb +3 -2
- data/examples/builder/hooks.rb +2 -1
- data/examples/builder/identity.rb +3 -2
- data/examples/builder/internal-broadcastable.rb +2 -1
- data/examples/builder/internal-composable.rb +2 -1
- data/examples/builder/internal-seekable.rb +3 -2
- data/examples/builder/logging.rb +3 -2
- data/examples/class/external-verbosify.rb +3 -2
- data/examples/class/hooks.rb +2 -1
- data/examples/class/identity.rb +3 -2
- data/examples/class/internal-broadcastable.rb +2 -1
- data/examples/class/internal-composable.rb +2 -1
- data/examples/class/internal-seekable.rb +3 -2
- data/examples/class/logging.rb +3 -2
- 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/builder.rb +1 -0
- data/lib/callable_tree/node/external/pod.rb +91 -0
- data/lib/callable_tree/node/external/verbose.rb +1 -1
- data/lib/callable_tree/node/external.rb +1 -0
- data/lib/callable_tree/node/hooks/terminator.rb +1 -1
- data/lib/callable_tree/node/internal/pod.rb +91 -0
- data/lib/callable_tree/node/internal/{strategyable.rb → strategizable.rb} +4 -4
- data/lib/callable_tree/node/internal.rb +2 -2
- data/lib/callable_tree/version.rb +1 -1
- data/lib/callable_tree.rb +3 -1
- data/mise.toml +2 -0
- metadata +26 -15
- data/Gemfile.lock +0 -34
|
@@ -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
|
|
@@ -42,7 +42,7 @@ module CallableTree
|
|
|
42
42
|
|
|
43
43
|
terminated =
|
|
44
44
|
if around_terminator_callbacks.empty?
|
|
45
|
-
super
|
|
45
|
+
super
|
|
46
46
|
else
|
|
47
47
|
around_terminator_callbacks_head, *around_terminator_callbacks_tail = around_terminator_callbacks
|
|
48
48
|
terminator = proc { super(output, *inputs, **options) }
|
|
@@ -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
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
module CallableTree
|
|
4
4
|
module Node
|
|
5
5
|
module Internal
|
|
6
|
-
module
|
|
6
|
+
module Strategizable
|
|
7
7
|
def self.included(mod)
|
|
8
8
|
mod.extend ClassMethods
|
|
9
9
|
end
|
|
@@ -70,8 +70,8 @@ module CallableTree
|
|
|
70
70
|
key = key.to_sym
|
|
71
71
|
config[:alias] = key unless config[:alias]
|
|
72
72
|
config[:factory] = DEFAUTL_FACTORY unless config[:factory]
|
|
73
|
-
|
|
74
|
-
|
|
73
|
+
Strategizable.__send__(:strategy_configs)[key] = config
|
|
74
|
+
Strategizable.__send__(:define_strategy_methods, key, config)
|
|
75
75
|
end
|
|
76
76
|
end
|
|
77
77
|
|
|
@@ -95,7 +95,7 @@ module CallableTree
|
|
|
95
95
|
private
|
|
96
96
|
|
|
97
97
|
def strategize(name, *args, matchable:, terminable:, **kwargs)
|
|
98
|
-
clone.
|
|
98
|
+
clone.strategize!(name, *args, matchable: matchable, terminable: terminable, **kwargs)
|
|
99
99
|
end
|
|
100
100
|
|
|
101
101
|
def strategize!(name, *args, matchable:, terminable:, **kwargs)
|
|
@@ -5,7 +5,7 @@ module CallableTree
|
|
|
5
5
|
module Internal
|
|
6
6
|
extend ::Forwardable
|
|
7
7
|
include Node
|
|
8
|
-
include
|
|
8
|
+
include Strategizable
|
|
9
9
|
|
|
10
10
|
def self.included(mod)
|
|
11
11
|
return unless mod.include?(External)
|
|
@@ -46,7 +46,7 @@ module CallableTree
|
|
|
46
46
|
.lazy
|
|
47
47
|
.select(&:internal?)
|
|
48
48
|
.map { |node| node.find(recursive: true, &block) }
|
|
49
|
-
.reject(&:nil?)
|
|
49
|
+
.reject(&:nil?) # rubocop:disable Style/CollectionCompact
|
|
50
50
|
.first
|
|
51
51
|
end
|
|
52
52
|
|
data/lib/callable_tree.rb
CHANGED
|
@@ -14,10 +14,12 @@ require_relative 'callable_tree/node/internal/strategy'
|
|
|
14
14
|
require_relative 'callable_tree/node/internal/strategy/broadcast'
|
|
15
15
|
require_relative 'callable_tree/node/internal/strategy/seek'
|
|
16
16
|
require_relative 'callable_tree/node/internal/strategy/compose'
|
|
17
|
-
require_relative 'callable_tree/node/internal/
|
|
17
|
+
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'
|
data/mise.toml
ADDED