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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5bcdf4454ef1639db46defba3c2b59377dd0aa719036b27be25b77a7a805fdec
4
- data.tar.gz: eef23d886d551984bbd8f1514e7bdfacc38be0d4b029ec832b1e7c61f2d2036f
3
+ metadata.gz: 67250ca086cc687aff14bda7a2365e1f170eea92c8cd4f88565ec8b678ee8d9c
4
+ data.tar.gz: 3450ffce0b35cadd19798757116e2bbc6a82a9460f4686187131061f63ba8663
5
5
  SHA512:
6
- metadata.gz: bef581ebc5816f79ab0b8d7da98dd9cf53fba824e1e68691e5b39ef8ee0eca3380fa2994dde965ad17db24267f9541b4c537c87fa26a3852ba9fa128afc60237
7
- data.tar.gz: 78d63a0b802ee4050d09a08ab4d6f62a7fa8e71425745ef055d2f02824c6fb92f48c3149fadb275723aefb93b04bf0a04c258246b115c4ad028b5a56179a9188
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** use any `Ask::Graph` subclass as a step in another
6
- graph. The sub-graph runs all its internal steps and merges its context back
7
- into the outer graph.
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
- class NotifyCustomer < Ask::Graph
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 # runs the sub-graph, merges its context
75
+ step NotifyCustomer
18
76
  step ShipOrder
19
77
  end
20
78
  ```
21
79
 
22
- - Sub-graphs read the outer graph's context directly no special wiring
23
- - Sub-graph writes are merged back automatically
24
- - Supports nesting (sub-graphs within sub-graphs)
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
- - Shares the same `storage` backend with the outer graph
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. Used internally by sub-graph composition.
102
+ into this one.
34
103
 
35
- - **Hash inputs now flatten into context keys** when a Hash is passed as
36
- input to a graph, its keys are directly accessible on the context in
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
- g.new({ value: 42 }).call
42
- ctx.value # => 42 (new — directly accessible)
43
- ctx.input # => { value: 42 } (still works)
108
+ def call(context)
109
+ context.run(NotifyCustomerWorkflow)
110
+ end
44
111
  ```
45
112
 
46
- ### Limitations
113
+ ### Changed
47
114
 
48
- - Sub-graphs containing `approve` steps are not yet supported and raise
49
- `ArgumentError`. Nested approval will be added in a future release.
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
- - 87 tests, 114 assertions, 0 failures
122
+ - 85 tests, 111 assertions, 0 failures
54
123
 
55
124
  ## [0.4.0] — 2026-07-27
56
125
 
@@ -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.
@@ -133,29 +133,14 @@ module Ask
133
133
  end
134
134
 
135
135
  def run_single(klass, _name, context)
136
- if klass < Ask::Graph
137
- run_subgraph(klass, context)
138
- else
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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Ask
4
4
  class Graph
5
- VERSION = "0.5.0"
5
+ VERSION = "0.6.0"
6
6
  end
7
7
  end
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 (reuse a graph as a step)
21
- # class NotifyCustomer < Ask::Graph
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 # runs the sub-graph, merges its context
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
- # 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
191
214
  def call(input = nil, storage: nil)
192
- 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
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
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.5.0
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kaka Ruto