1use crate::value::Value;
8
9#[derive(Debug, Clone)]
10pub struct ExecutionPlanSpec {
11 pub groups: Vec<Vec<usize>>,
12 pub concurrency: i64,
13}
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum PolicyKind {
17 Fail,
18 Retry,
19 Continue,
20}
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub enum RelationKind {
24 Single,
25 Connection,
26}
27
28#[derive(Debug, Clone)]
29pub struct OpSpec {
30 pub id: String,
31 pub parent: Option<usize>,
32 pub bind_field: Option<String>,
33 pub relation_kind: Option<RelationKind>,
34 pub policy: Option<String>,
37}
38
39#[derive(Debug, Clone)]
41pub enum ExecOutcome {
42 Ok(Value),
43 Error(String),
44}
45
46#[derive(Debug, Clone)]
47enum OpState {
48 Ok(Value),
49 Skipped,
50 }
52
53#[derive(Debug, Clone, Copy, PartialEq, Eq)]
54pub enum PlanFailureCode {
55 OpFailed,
56 UnknownPolicy,
57 InvalidPlan,
58}
59
60impl PlanFailureCode {
61 pub fn as_str(self) -> &'static str {
62 match self {
63 PlanFailureCode::OpFailed => "OP_FAILED",
64 PlanFailureCode::UnknownPolicy => "UNKNOWN_POLICY",
65 PlanFailureCode::InvalidPlan => "INVALID_PLAN",
66 }
67 }
68}
69
70#[derive(Debug, Clone)]
71pub struct PlanFailure {
72 pub code: PlanFailureCode,
73 pub message: String,
74}
75
76impl std::fmt::Display for PlanFailure {
77 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
78 write!(f, "{}: {}", self.code.as_str(), self.message)
79 }
80}
81impl std::error::Error for PlanFailure {}
82
83fn fail<T>(code: PlanFailureCode, message: impl Into<String>) -> Result<T, PlanFailure> {
84 Err(PlanFailure {
85 code,
86 message: message.into(),
87 })
88}
89
90#[derive(Debug)]
91pub struct RunResult {
92 states: Vec<Option<OpState>>,
93 pub executed: Vec<String>,
94 pub skipped: Vec<String>,
95 ops_ref: Vec<(String, Option<RelationKind>)>,
96}
97
98impl RunResult {
99 pub fn final_tree(&self) -> Vec<(String, Value)> {
101 let mut tree = Vec::new();
102 for (i, st) in self.states.iter().enumerate() {
103 match st {
104 Some(OpState::Ok(v)) => tree.push((self.ops_ref[i].0.clone(), v.clone())),
105 Some(OpState::Skipped) => tree.push((
106 self.ops_ref[i].0.clone(),
107 unproduced_value(self.ops_ref[i].1),
108 )),
109 None => {}
110 }
111 }
112 tree
113 }
114}
115
116fn unproduced_value(kind: Option<RelationKind>) -> Value {
117 if kind == Some(RelationKind::Connection) {
118 Value::Obj(vec![
119 ("items".into(), Value::Arr(vec![])),
120 ("cursor".into(), Value::Null),
121 ])
122 } else {
123 Value::Null
124 }
125}
126
127fn parse_policy(raw: &Option<String>) -> Result<PolicyKind, PlanFailure> {
128 match raw.as_deref() {
129 None | Some("fail") => Ok(PolicyKind::Fail),
130 Some("retry") => Ok(PolicyKind::Retry),
131 Some("continue") => Ok(PolicyKind::Continue),
132 Some(other) => fail(
133 PlanFailureCode::UnknownPolicy,
134 format!("unknown policy kind: {other}"),
135 ),
136 }
137}
138
139pub fn run_plan<F>(
141 plan: Option<&ExecutionPlanSpec>,
142 ops: &[OpSpec],
143 mut exec: F,
144) -> Result<RunResult, PlanFailure>
145where
146 F: FnMut(&OpSpec, Option<&Value>) -> ExecOutcome,
147{
148 let n = ops.len();
149 let mut states: Vec<Option<OpState>> = vec![None; n];
150 let mut executed_idx: Vec<usize> = Vec::new();
151 let mut skipped_idx: Vec<usize> = Vec::new();
152
153 let stages: Vec<Vec<usize>> = match plan {
154 Some(p) => p.groups.clone(),
155 None => (0..n).map(|i| vec![i]).collect(),
156 };
157
158 validate_coverage(&stages, n)?;
159
160 for stage in &stages {
161 let mut ordered = stage.clone();
162 ordered.sort_unstable(); for &i in &ordered {
164 let st = run_op(
165 i,
166 &ops[i],
167 &states,
168 &mut exec,
169 &mut executed_idx,
170 &mut skipped_idx,
171 )?;
172 states[i] = Some(st);
173 }
174 }
175
176 executed_idx.sort_unstable();
177 skipped_idx.sort_unstable();
178
179 Ok(RunResult {
180 executed: executed_idx.iter().map(|&i| ops[i].id.clone()).collect(),
181 skipped: skipped_idx.iter().map(|&i| ops[i].id.clone()).collect(),
182 ops_ref: ops
183 .iter()
184 .map(|o| (o.id.clone(), o.relation_kind))
185 .collect(),
186 states,
187 })
188}
189
190#[allow(clippy::too_many_arguments)]
191fn run_op<F>(
192 i: usize,
193 op: &OpSpec,
194 states: &[Option<OpState>],
195 exec: &mut F,
196 executed_idx: &mut Vec<usize>,
197 skipped_idx: &mut Vec<usize>,
198) -> Result<OpState, PlanFailure>
199where
200 F: FnMut(&OpSpec, Option<&Value>) -> ExecOutcome,
201{
202 let policy = parse_policy(&op.policy)?; let mut bound_value: Option<Value> = None;
205 if let Some(pidx) = op.parent {
206 match states.get(pidx).and_then(|s| s.as_ref()) {
207 Some(OpState::Ok(pv)) => {
208 if let Some(field) = &op.bind_field {
209 let bound = pv.obj_get(field);
210 match bound {
211 None | Some(Value::Null) => {
212 skipped_idx.push(i);
213 return Ok(OpState::Skipped);
214 }
215 Some(b) => bound_value = Some(b.clone()),
216 }
217 }
218 }
219 _ => {
220 skipped_idx.push(i);
222 return Ok(OpState::Skipped);
223 }
224 }
225 }
226
227 let outcome = exec(op, bound_value.as_ref());
228 executed_idx.push(i);
229
230 match outcome {
231 ExecOutcome::Ok(v) => Ok(OpState::Ok(v)),
232 ExecOutcome::Error(e) => match policy {
233 PolicyKind::Fail => fail(
234 PlanFailureCode::OpFailed,
235 format!("operation '{}' failed under 'fail' policy: {e}", op.id),
236 ),
237 PolicyKind::Retry => fail(
238 PlanFailureCode::OpFailed,
239 format!(
240 "operation '{}' failed under 'retry' policy (exhausted): {e}",
241 op.id
242 ),
243 ),
244 PolicyKind::Continue => {
245 skipped_idx.push(i);
247 executed_idx.pop(); Ok(OpState::Skipped)
249 }
250 },
251 }
252}
253
254fn validate_coverage(stages: &[Vec<usize>], n: usize) -> Result<(), PlanFailure> {
255 let mut seen = std::collections::HashSet::new();
256 for stage in stages {
257 for &i in stage {
258 if i >= n {
259 return fail(
260 PlanFailureCode::InvalidPlan,
261 format!("stage index {i} out of range [0,{n})"),
262 );
263 }
264 if !seen.insert(i) {
265 return fail(
266 PlanFailureCode::InvalidPlan,
267 format!("operation {i} appears in more than one stage"),
268 );
269 }
270 }
271 }
272 if seen.len() != n {
273 return fail(
274 PlanFailureCode::InvalidPlan,
275 format!(
276 "plan does not cover all {n} operations (covered {})",
277 seen.len()
278 ),
279 );
280 }
281 Ok(())
282}