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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c3062914e848e9fea15dd9b02bd001946b7a3fd0ab015a1805b4e21a371b68bf
4
- data.tar.gz: 17866f6bfaf40d6e195c71655ac134430747e3690ce32c355036a8e30a62bbf1
3
+ metadata.gz: 67250ca086cc687aff14bda7a2365e1f170eea92c8cd4f88565ec8b678ee8d9c
4
+ data.tar.gz: 3450ffce0b35cadd19798757116e2bbc6a82a9460f4686187131061f63ba8663
5
5
  SHA512:
6
- metadata.gz: ac9dad679a58246c1bd04d67de6812e9126f8f341edad4a72e842b80b759bcf2e020a2ad0c5bedd625eff861f22f0cbb224d4d2dc058a136d4c13087c4420248
7
- data.tar.gz: b0f7285fd1e447c3f48e06d061aa441f64bb37de1952022e22e44835c3b549c639bceec13d9a690d0d14becb5eb724e1d2f31c05ea47a514ba2be57d9680de3b
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
@@ -31,7 +31,12 @@ module Ask
31
31
  @item = nil
32
32
  @mutex = Mutex.new
33
33
  @resume_input = nil
34
- @store[:input] = input if input
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.
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Ask
4
4
  class Graph
5
- VERSION = "0.4.0"
5
+ VERSION = "0.6.0"
6
6
  end
7
7
  end
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, per-item checkpointing
10
- # loops, step timeouts, retry policies, and lifecycle hooks.
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
- # Convenience: create instance and call.
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
- new(input, storage: storage).call
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
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ask-graph
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kaka Ruto