ask-graph 0.4.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 +123 -0
- data/lib/ask/graph/context.rb +42 -1
- data/lib/ask/graph/version.rb +1 -1
- data/lib/ask/graph.rb +47 -5
- 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,3 +1,126 @@
|
|
|
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
|
+
|
|
49
|
+
## [0.5.0] — 2026-07-27
|
|
50
|
+
|
|
51
|
+
### Added
|
|
52
|
+
|
|
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.
|
|
57
|
+
|
|
58
|
+
```ruby
|
|
59
|
+
# Define a workflow
|
|
60
|
+
class NotifyCustomerWorkflow < Ask::Graph
|
|
61
|
+
step SendEmail
|
|
62
|
+
step LogNotification
|
|
63
|
+
end
|
|
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
|
|
73
|
+
class HandleOrder < Ask::Graph
|
|
74
|
+
step ValidatePayment
|
|
75
|
+
step NotifyCustomer
|
|
76
|
+
step ShipOrder
|
|
77
|
+
end
|
|
78
|
+
```
|
|
79
|
+
|
|
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)
|
|
84
|
+
- Supports `timeout:`, `retry:`, and `if:`/`unless:` on the outer step
|
|
85
|
+
- Supports parallel steps inside sub-graphs
|
|
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
|
+
```
|
|
97
|
+
|
|
98
|
+
- **`Context#export_data`** — returns the raw store hash without JSON
|
|
99
|
+
serialization. Used internally for sub-graph data passing.
|
|
100
|
+
|
|
101
|
+
- **`Context#import(other)`** — merges data from another context or hash
|
|
102
|
+
into this one.
|
|
103
|
+
|
|
104
|
+
- **`Context#run(graph_class)`** — convenience method wrapping the
|
|
105
|
+
export → create → call → import pattern.
|
|
106
|
+
|
|
107
|
+
```ruby
|
|
108
|
+
def call(context)
|
|
109
|
+
context.run(NotifyCustomerWorkflow)
|
|
110
|
+
end
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Changed
|
|
114
|
+
|
|
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?"
|
|
119
|
+
|
|
120
|
+
### Tested
|
|
121
|
+
|
|
122
|
+
- 85 tests, 111 assertions, 0 failures
|
|
123
|
+
|
|
1
124
|
## [0.4.0] — 2026-07-27
|
|
2
125
|
|
|
3
126
|
### Changed
|
data/lib/ask/graph/context.rb
CHANGED
|
@@ -31,7 +31,12 @@ module Ask
|
|
|
31
31
|
@item = nil
|
|
32
32
|
@mutex = Mutex.new
|
|
33
33
|
@resume_input = nil
|
|
34
|
-
|
|
34
|
+
if input.is_a?(Hash)
|
|
35
|
+
@store[:input] = input
|
|
36
|
+
@store.merge!(input)
|
|
37
|
+
elsif input
|
|
38
|
+
@store[:input] = input
|
|
39
|
+
end
|
|
35
40
|
end
|
|
36
41
|
|
|
37
42
|
# @return [Object, nil] input provided on resume for approval steps
|
|
@@ -102,6 +107,42 @@ module Ask
|
|
|
102
107
|
end
|
|
103
108
|
end
|
|
104
109
|
|
|
110
|
+
# @return [Hash] raw store data with original object references.
|
|
111
|
+
# Unlike {to_h}, this returns the actual Ruby objects without
|
|
112
|
+
# serialization. Useful for passing state to sub-graphs.
|
|
113
|
+
def export_data
|
|
114
|
+
@mutex.synchronize { @store.dup }
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Merge data from another context (or hash) into this one.
|
|
118
|
+
# Existing keys in this context are overwritten.
|
|
119
|
+
#
|
|
120
|
+
# @param other [Context, Hash] source of data to merge in
|
|
121
|
+
def import(other)
|
|
122
|
+
data = other.is_a?(Context) ? other.export_data : other
|
|
123
|
+
@mutex.synchronize { @store.merge!(data) }
|
|
124
|
+
end
|
|
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
|
+
|
|
105
146
|
private
|
|
106
147
|
|
|
107
148
|
# Recursively convert a value to a JSON-safe structure.
|
data/lib/ask/graph/version.rb
CHANGED
data/lib/ask/graph.rb
CHANGED
|
@@ -6,8 +6,9 @@ require_relative "graph/runner"
|
|
|
6
6
|
|
|
7
7
|
module Ask
|
|
8
8
|
# Define durable workflow graphs with named steps, conditional routing,
|
|
9
|
-
# parallel execution, human-in-the-loop approval,
|
|
10
|
-
# loops, step timeouts, retry policies, and
|
|
9
|
+
# parallel execution, sub-graph composition, human-in-the-loop approval,
|
|
10
|
+
# per-item checkpointing loops, step timeouts, retry policies, and
|
|
11
|
+
# lifecycle hooks.
|
|
11
12
|
#
|
|
12
13
|
# @example Basic workflow
|
|
13
14
|
# class HandleCall < Ask::Graph
|
|
@@ -16,6 +17,27 @@ module Ask
|
|
|
16
17
|
# step BookAppointment
|
|
17
18
|
# end
|
|
18
19
|
#
|
|
20
|
+
# @example Sub-graph composition
|
|
21
|
+
# # Define a workflow
|
|
22
|
+
# class NotifyCustomerWorkflow < Ask::Graph
|
|
23
|
+
# step SendEmail
|
|
24
|
+
# step LogNotification
|
|
25
|
+
# end
|
|
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
|
|
35
|
+
# class HandleOrder < Ask::Graph
|
|
36
|
+
# step ValidatePayment
|
|
37
|
+
# step NotifyCustomer
|
|
38
|
+
# step ShipOrder
|
|
39
|
+
# end
|
|
40
|
+
#
|
|
19
41
|
# @example With timeout and retry
|
|
20
42
|
# class ApiWorkflow < Ask::Graph
|
|
21
43
|
# step FetchData, timeout: 30, retry: 3
|
|
@@ -174,9 +196,29 @@ module Ask
|
|
|
174
196
|
declarations << build_declaration(:approve, klass, opts)
|
|
175
197
|
end
|
|
176
198
|
|
|
177
|
-
#
|
|
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
|
|
178
214
|
def call(input = nil, storage: nil)
|
|
179
|
-
|
|
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
|
|
180
222
|
end
|
|
181
223
|
alias run call
|
|
182
224
|
|
|
@@ -211,7 +253,7 @@ module Ask
|
|
|
211
253
|
attr_reader :runner
|
|
212
254
|
attr_reader :context
|
|
213
255
|
|
|
214
|
-
def call
|
|
256
|
+
def call(*)
|
|
215
257
|
@context = Context.new(self, @input)
|
|
216
258
|
@runner.run(@context)
|
|
217
259
|
@context
|