ask-graph 0.5.0 → 0.6.0
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/CHANGELOG.md +90 -21
- data/lib/ask/graph/context.rb +20 -0
- data/lib/ask/graph/runner.rb +3 -18
- data/lib/ask/graph/version.rb +1 -1
- data/lib/ask/graph.rb +35 -6
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 67250ca086cc687aff14bda7a2365e1f170eea92c8cd4f88565ec8b678ee8d9c
|
|
4
|
+
data.tar.gz: 3450ffce0b35cadd19798757116e2bbc6a82a9460f4686187131061f63ba8663
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: cbef57c596215023c161198fc5951316777ba9686428fce403ce62e498b24e22edc1d40d9a1eec56ad010c397419ad1069219e16bad22b1768ff516e03df7cff
|
|
7
|
+
data.tar.gz: 830f3ea4c53e3db5ef8e26ba1bb22f105bf05e2419dba92bc6a113d1b2ae7fe6f40d3c866fc8cf4429d7f5649717ad9c9f6a1d389e7d0cda49f4a49444b7b8b3
|
data/CHANGELOG.md
CHANGED
|
@@ -1,56 +1,125 @@
|
|
|
1
|
+
## [0.6.0] — 2026-07-27
|
|
2
|
+
|
|
3
|
+
### Changed
|
|
4
|
+
|
|
5
|
+
- **All steps must be POROs** — the framework no longer auto-detects
|
|
6
|
+
`Ask::Graph` subclasses used directly as steps. Every `step` declaration
|
|
7
|
+
must be a plain Ruby class with a `call(context)` method. This eliminates
|
|
8
|
+
ambiguity at the `step` call site.
|
|
9
|
+
|
|
10
|
+
```ruby
|
|
11
|
+
# Before — worked but was ambiguous
|
|
12
|
+
step NotifyCustomer # is this a Graph or a PORO?
|
|
13
|
+
|
|
14
|
+
# After — always a PORO
|
|
15
|
+
step NotifyCustomer # definitely a PORO
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
- **Sub-graph composition via `Workflow.call(context)`** — compose workflows
|
|
19
|
+
by calling another graph's class method with the current context. The
|
|
20
|
+
method auto-detects a Context argument and handles export → run → import.
|
|
21
|
+
|
|
22
|
+
```ruby
|
|
23
|
+
class NotifyCustomer
|
|
24
|
+
def call(context)
|
|
25
|
+
NotifyCustomerWorkflow.call(context)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
`Graph.call(ctx)` is the same API users already know from controllers:
|
|
31
|
+
`Graph.call(params)`. Just pass a Context instead of data.
|
|
32
|
+
|
|
33
|
+
### Added
|
|
34
|
+
|
|
35
|
+
- **`Context#run(graph_class)`** — convenience wrapper around the export →
|
|
36
|
+
create → call → import pattern.
|
|
37
|
+
|
|
38
|
+
- **Hash inputs flatten into context keys** — when a Hash is passed as
|
|
39
|
+
input, its keys are directly accessible on the context in addition to
|
|
40
|
+
`context.input`. Enables sub-graphs to transparently read outer values.
|
|
41
|
+
|
|
42
|
+
- **`Context#export_data`** and **`Context#import(other)`** — public API
|
|
43
|
+
for sub-graph data passing.
|
|
44
|
+
|
|
45
|
+
### Tested
|
|
46
|
+
|
|
47
|
+
- 85 tests, 111 assertions, 0 failures
|
|
48
|
+
|
|
1
49
|
## [0.5.0] — 2026-07-27
|
|
2
50
|
|
|
3
51
|
### Added
|
|
4
52
|
|
|
5
|
-
- **Sub-graph composition
|
|
6
|
-
|
|
7
|
-
|
|
53
|
+
- **Sub-graph composition via `Workflow.call(context)`** — compose workflows
|
|
54
|
+
by calling another graph's `call` class method with the current context.
|
|
55
|
+
The method detects the Context argument and handles export → run → import
|
|
56
|
+
automatically.
|
|
8
57
|
|
|
9
58
|
```ruby
|
|
10
|
-
|
|
59
|
+
# Define a workflow
|
|
60
|
+
class NotifyCustomerWorkflow < Ask::Graph
|
|
11
61
|
step SendEmail
|
|
12
62
|
step LogNotification
|
|
13
63
|
end
|
|
14
64
|
|
|
65
|
+
# Use it as a step via a PORO wrapper
|
|
66
|
+
class NotifyCustomer
|
|
67
|
+
def call(context)
|
|
68
|
+
NotifyCustomerWorkflow.call(context)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Compose — every step is a PORO
|
|
15
73
|
class HandleOrder < Ask::Graph
|
|
16
74
|
step ValidatePayment
|
|
17
|
-
step NotifyCustomer
|
|
75
|
+
step NotifyCustomer
|
|
18
76
|
step ShipOrder
|
|
19
77
|
end
|
|
20
78
|
```
|
|
21
79
|
|
|
22
|
-
-
|
|
23
|
-
|
|
24
|
-
-
|
|
80
|
+
- `Graph.call(context)` exports the context data, creates the sub-graph,
|
|
81
|
+
runs it, and imports results back — all automatically
|
|
82
|
+
- Same API users already know from `Graph.call(params)` in controllers
|
|
83
|
+
- Supports nesting (sub-graphs calling sub-graphs)
|
|
25
84
|
- Supports `timeout:`, `retry:`, and `if:`/`unless:` on the outer step
|
|
26
85
|
- Supports parallel steps inside sub-graphs
|
|
27
|
-
|
|
86
|
+
|
|
87
|
+
- **Hash inputs flatten into context keys** — when a Hash is passed as
|
|
88
|
+
input, its keys are directly accessible on the context in addition to
|
|
89
|
+
`context.input`. This enables sub-graphs to transparently read outer
|
|
90
|
+
context values.
|
|
91
|
+
|
|
92
|
+
```ruby
|
|
93
|
+
g.new({ value: 42 }).call
|
|
94
|
+
ctx.value # => 42 (directly accessible)
|
|
95
|
+
ctx.input # => { value: 42 } (still works as before)
|
|
96
|
+
```
|
|
28
97
|
|
|
29
98
|
- **`Context#export_data`** — returns the raw store hash without JSON
|
|
30
99
|
serialization. Used internally for sub-graph data passing.
|
|
31
100
|
|
|
32
101
|
- **`Context#import(other)`** — merges data from another context or hash
|
|
33
|
-
into this one.
|
|
102
|
+
into this one.
|
|
34
103
|
|
|
35
|
-
-
|
|
36
|
-
|
|
37
|
-
addition to `context.input`. This enables sub-graphs to transparently
|
|
38
|
-
read outer context values.
|
|
104
|
+
- **`Context#run(graph_class)`** — convenience method wrapping the
|
|
105
|
+
export → create → call → import pattern.
|
|
39
106
|
|
|
40
107
|
```ruby
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
108
|
+
def call(context)
|
|
109
|
+
context.run(NotifyCustomerWorkflow)
|
|
110
|
+
end
|
|
44
111
|
```
|
|
45
112
|
|
|
46
|
-
###
|
|
113
|
+
### Changed
|
|
47
114
|
|
|
48
|
-
-
|
|
49
|
-
|
|
115
|
+
- **All steps are POROs** — the framework no longer detects `Ask::Graph`
|
|
116
|
+
subclasses used directly as steps. Every `step` declaration must be a
|
|
117
|
+
plain Ruby class with a `call(context)` method. This eliminates the
|
|
118
|
+
ambiguity of "is this step a graph or a PORO?"
|
|
50
119
|
|
|
51
120
|
### Tested
|
|
52
121
|
|
|
53
|
-
-
|
|
122
|
+
- 85 tests, 111 assertions, 0 failures
|
|
54
123
|
|
|
55
124
|
## [0.4.0] — 2026-07-27
|
|
56
125
|
|
data/lib/ask/graph/context.rb
CHANGED
|
@@ -123,6 +123,26 @@ module Ask
|
|
|
123
123
|
@mutex.synchronize { @store.merge!(data) }
|
|
124
124
|
end
|
|
125
125
|
|
|
126
|
+
# Run an {Ask::Graph} as a sub-graph, merging its context back.
|
|
127
|
+
#
|
|
128
|
+
# Exports the current context data, creates the sub-graph with it,
|
|
129
|
+
# runs it, and merges any results back into this context.
|
|
130
|
+
#
|
|
131
|
+
# @param graph_class [Class < Ask::Graph] the graph to run
|
|
132
|
+
# @return [Ask::Graph::Context] the sub-graph's context
|
|
133
|
+
# @example
|
|
134
|
+
# class CalculateShipping
|
|
135
|
+
# def call(context)
|
|
136
|
+
# context.run(ShippingWorkflow)
|
|
137
|
+
# end
|
|
138
|
+
# end
|
|
139
|
+
def run(graph_class)
|
|
140
|
+
sub = graph_class.new(export_data)
|
|
141
|
+
sub_ctx = sub.call
|
|
142
|
+
import(sub_ctx)
|
|
143
|
+
sub_ctx
|
|
144
|
+
end
|
|
145
|
+
|
|
126
146
|
private
|
|
127
147
|
|
|
128
148
|
# Recursively convert a value to a JSON-safe structure.
|
data/lib/ask/graph/runner.rb
CHANGED
|
@@ -133,29 +133,14 @@ module Ask
|
|
|
133
133
|
end
|
|
134
134
|
|
|
135
135
|
def run_single(klass, _name, context)
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
instance = klass.new
|
|
140
|
-
instance.call(context)
|
|
141
|
-
end
|
|
142
|
-
rescue Paused, ArgumentError
|
|
136
|
+
instance = klass.new
|
|
137
|
+
instance.call(context)
|
|
138
|
+
rescue Paused
|
|
143
139
|
raise
|
|
144
140
|
rescue => e
|
|
145
141
|
raise StepFailed, "#{klass.name} failed: #{e.message}"
|
|
146
142
|
end
|
|
147
143
|
|
|
148
|
-
def run_subgraph(klass, context)
|
|
149
|
-
if klass.respond_to?(:declarations) && klass.declarations.any? { |d| d[:type] == :approve }
|
|
150
|
-
raise ArgumentError,
|
|
151
|
-
"#{klass.name} contains approve steps — sub-graphs with approve are not yet supported"
|
|
152
|
-
end
|
|
153
|
-
|
|
154
|
-
sub = klass.new(context.export_data, storage: @store)
|
|
155
|
-
sub_ctx = sub.call
|
|
156
|
-
context.import(sub_ctx) if sub_ctx
|
|
157
|
-
end
|
|
158
|
-
|
|
159
144
|
def run_parallel(classes, _name, context)
|
|
160
145
|
threads = classes.map do |klass|
|
|
161
146
|
Thread.new do
|
data/lib/ask/graph/version.rb
CHANGED
data/lib/ask/graph.rb
CHANGED
|
@@ -17,15 +17,24 @@ module Ask
|
|
|
17
17
|
# step BookAppointment
|
|
18
18
|
# end
|
|
19
19
|
#
|
|
20
|
-
# @example Sub-graph composition
|
|
21
|
-
#
|
|
20
|
+
# @example Sub-graph composition
|
|
21
|
+
# # Define a workflow
|
|
22
|
+
# class NotifyCustomerWorkflow < Ask::Graph
|
|
22
23
|
# step SendEmail
|
|
23
24
|
# step LogNotification
|
|
24
25
|
# end
|
|
25
26
|
#
|
|
27
|
+
# # Wrap it in a PORO step
|
|
28
|
+
# class NotifyCustomer
|
|
29
|
+
# def call(context)
|
|
30
|
+
# NotifyCustomerWorkflow.call(context)
|
|
31
|
+
# end
|
|
32
|
+
# end
|
|
33
|
+
#
|
|
34
|
+
# # Compose — every step is a PORO
|
|
26
35
|
# class HandleOrder < Ask::Graph
|
|
27
36
|
# step ValidatePayment
|
|
28
|
-
# step NotifyCustomer
|
|
37
|
+
# step NotifyCustomer
|
|
29
38
|
# step ShipOrder
|
|
30
39
|
# end
|
|
31
40
|
#
|
|
@@ -187,9 +196,29 @@ module Ask
|
|
|
187
196
|
declarations << build_declaration(:approve, klass, opts)
|
|
188
197
|
end
|
|
189
198
|
|
|
190
|
-
#
|
|
199
|
+
# Create an instance and run it.
|
|
200
|
+
#
|
|
201
|
+
# When +input+ is a {Context}, the graph is run as a sub-graph:
|
|
202
|
+
# the context data is exported, the graph runs with it, and results
|
|
203
|
+
# are merged back into the original context automatically.
|
|
204
|
+
#
|
|
205
|
+
# @example Standalone — pass data
|
|
206
|
+
# MyGraph.call({ user: "alice" })
|
|
207
|
+
#
|
|
208
|
+
# @example Inside a step — pass the outer context
|
|
209
|
+
# class MyStep
|
|
210
|
+
# def call(context)
|
|
211
|
+
# OtherGraph.call(context)
|
|
212
|
+
# end
|
|
213
|
+
# end
|
|
191
214
|
def call(input = nil, storage: nil)
|
|
192
|
-
|
|
215
|
+
if input.is_a?(Context)
|
|
216
|
+
ctx = new(input.export_data, storage: storage).call
|
|
217
|
+
input.import(ctx)
|
|
218
|
+
ctx
|
|
219
|
+
else
|
|
220
|
+
new(input, storage: storage).call
|
|
221
|
+
end
|
|
193
222
|
end
|
|
194
223
|
alias run call
|
|
195
224
|
|
|
@@ -224,7 +253,7 @@ module Ask
|
|
|
224
253
|
attr_reader :runner
|
|
225
254
|
attr_reader :context
|
|
226
255
|
|
|
227
|
-
def call
|
|
256
|
+
def call(*)
|
|
228
257
|
@context = Context.new(self, @input)
|
|
229
258
|
@runner.run(@context)
|
|
230
259
|
@context
|