1#[cfg(feature = "ir")]
34use crate::expr::evaluate as evaluate_expression;
35use crate::expr::ExprFailure;
36#[cfg(feature = "ir")]
37use crate::plan::{run_plan, ExecutionPlanSpec, OpSpec, RelationKind};
38use crate::plan::{ExecOutcome, PlanFailure};
39use crate::value::Value;
40#[cfg(feature = "ir")]
41use serde_json::Value as J;
42
43#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45pub enum BehaviorFailureCode {
46 UnknownComponent,
47 UnknownNodeKind,
48 MapOverNotArray,
49 MapIntoElementNotObject,
50 MapBatchResultMismatch,
51 FanoutOverNotArray,
52 FanoutBatchResultMismatch,
53 UnknownEntry,
54}
55
56impl BehaviorFailureCode {
57 pub fn as_str(self) -> &'static str {
58 match self {
59 BehaviorFailureCode::UnknownComponent => "UNKNOWN_COMPONENT",
60 BehaviorFailureCode::UnknownNodeKind => "UNKNOWN_NODE_KIND",
61 BehaviorFailureCode::MapOverNotArray => "MAP_OVER_NOT_ARRAY",
62 BehaviorFailureCode::MapIntoElementNotObject => "MAP_INTO_ELEMENT_NOT_OBJECT",
63 BehaviorFailureCode::MapBatchResultMismatch => "MAP_BATCH_RESULT_MISMATCH",
64 BehaviorFailureCode::FanoutOverNotArray => "FANOUT_OVER_NOT_ARRAY",
65 BehaviorFailureCode::FanoutBatchResultMismatch => "FANOUT_BATCH_RESULT_MISMATCH",
66 BehaviorFailureCode::UnknownEntry => "UNKNOWN_ENTRY",
67 }
68 }
69}
70
71#[derive(Debug, Clone)]
74pub struct BehaviorError {
75 code: String,
76 pub message: String,
77}
78
79impl BehaviorError {
80 pub fn new(code: impl Into<String>, message: impl Into<String>) -> Self {
85 BehaviorError {
86 code: code.into(),
87 message: message.into(),
88 }
89 }
90
91 pub fn code(&self) -> &str {
93 &self.code
94 }
95}
96
97impl std::fmt::Display for BehaviorError {
98 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
99 write!(f, "{}: {}", self.code, self.message)
100 }
101}
102impl std::error::Error for BehaviorError {}
103
104impl From<ExprFailure> for BehaviorError {
105 fn from(e: ExprFailure) -> Self {
106 BehaviorError {
107 code: e.code.as_str().to_string(),
108 message: e.message,
109 }
110 }
111}
112impl From<PlanFailure> for BehaviorError {
113 fn from(e: PlanFailure) -> Self {
114 BehaviorError {
115 code: e.code.as_str().to_string(),
116 message: e.message,
117 }
118 }
119}
120
121#[cfg(feature = "ir")]
122fn bfail<T>(code: BehaviorFailureCode, message: impl Into<String>) -> Result<T, BehaviorError> {
123 Err(BehaviorError {
124 code: code.as_str().to_string(),
125 message: message.into(),
126 })
127}
128
129pub trait ComponentExec {
134 fn exec(
135 &mut self,
136 component: &str,
137 ports: &[(String, Value)],
138 bound: Option<&Value>,
139 ) -> Option<ExecOutcome>;
140
141 fn exec_ctx(
147 &mut self,
148 node_id: &str,
149 component: &str,
150 ports: &[(String, Value)],
151 bound: Option<&Value>,
152 ) -> Option<ExecOutcome> {
153 let _ = node_id;
154 self.exec(component, ports, bound)
155 }
156}
157
158impl ComponentExec for &mut dyn ComponentExec {
175 fn exec(
176 &mut self,
177 component: &str,
178 ports: &[(String, Value)],
179 bound: Option<&Value>,
180 ) -> Option<ExecOutcome> {
181 (**self).exec(component, ports, bound)
182 }
183
184 fn exec_ctx(
185 &mut self,
186 node_id: &str,
187 component: &str,
188 ports: &[(String, Value)],
189 bound: Option<&Value>,
190 ) -> Option<ExecOutcome> {
191 (**self).exec_ctx(node_id, component, ports, bound)
192 }
193}
194
195#[cfg(feature = "ir")]
196fn scope_get<'a>(scope: &'a [(String, Value)], key: &str) -> Option<&'a Value> {
197 scope.iter().find(|(k, _)| k == key).map(|(_, v)| v)
198}
199
200#[cfg(feature = "ir")]
206fn fanout_dedup_drop(
207 aligned_bodies: Vec<Value>,
208 dedupe_key: &str,
209 drop: &str,
210 implicit_source: Option<&str>,
211) -> Vec<Value> {
212 use std::collections::HashSet;
213 let mut items: Vec<Value> = Vec::with_capacity(aligned_bodies.len());
214 let mut seen: HashSet<String> = HashSet::new();
215 for body in aligned_bodies.into_iter() {
216 let fields = match &body {
217 Value::Obj(f) => Some(f),
218 _ => None,
219 };
220 let key_val = fields.and_then(|f| f.iter().find(|(k, _)| k == dedupe_key).map(|(_, v)| v));
221 let has_key = matches!(key_val, Some(v) if !matches!(v, Value::Null));
222 if !has_key {
223 if drop == "dangling" {
224 continue;
225 }
226 items.push(body); continue;
228 }
229 let seen_key = match key_val.unwrap() {
230 Value::Str(s) => format!("s:{s}"),
231 other => format!("j:{other:?}"),
232 };
233 if seen.contains(&seen_key) {
234 continue;
235 }
236 seen.insert(seen_key);
237 match (implicit_source, fields) {
238 (Some(src), Some(f)) if f.iter().any(|(k, _)| k == src) => {
239 let stripped: Vec<(String, Value)> =
240 f.iter().filter(|(k, _)| k != src).cloned().collect();
241 items.push(Value::Obj(stripped));
242 }
243 _ => items.push(body),
244 }
245 }
246 items
247}
248
249#[cfg(feature = "ir")]
250fn node_kind(n: &J) -> Result<&'static str, BehaviorError> {
251 if n.get("fanout").is_some() {
252 Ok("fanout")
253 } else if n.get("map").is_some() {
254 Ok("map")
255 } else if n.get("cond").is_some() {
256 Ok("cond")
257 } else if n.get("component").is_some() {
258 Ok("componentRef")
259 } else {
260 bfail(
261 BehaviorFailureCode::UnknownNodeKind,
262 format!(
263 "body node '{}' is not componentRef/map/cond/fanout",
264 n.get("id").and_then(|v| v.as_str()).unwrap_or("?")
265 ),
266 )
267 }
268}
269
270#[cfg(feature = "ir")]
271fn node_sub(n: &J) -> &J {
272 if let Some(f) = n.get("fanout") {
273 f
274 } else if let Some(m) = n.get("map") {
275 m
276 } else if let Some(c) = n.get("cond") {
277 c
278 } else {
279 n
280 }
281}
282
283#[cfg(feature = "ir")]
284fn node_parent(n: &J) -> Option<&str> {
285 node_sub(n).get("parent").and_then(|v| v.as_str())
286}
287
288#[cfg(feature = "ir")]
289fn node_bind_field(n: &J) -> Option<String> {
290 if n.get("map").is_some() || n.get("cond").is_some() || n.get("fanout").is_some() {
291 return None;
292 }
293 n.get("bindField")
294 .and_then(|v| v.as_str())
295 .map(str::to_string)
296}
297
298#[cfg(feature = "ir")]
299fn node_relation_kind(n: &J) -> Option<RelationKind> {
300 if n.get("cond").is_some() {
301 return None;
302 }
303 let sub = if n.get("map").is_some() || n.get("fanout").is_some() {
304 node_sub(n)
305 } else {
306 n
307 };
308 match sub.get("relationKind").and_then(|v| v.as_str()) {
309 Some("connection") => Some(RelationKind::Connection),
310 Some(_) => Some(RelationKind::Single),
311 None => None,
312 }
313}
314
315#[cfg(feature = "ir")]
316fn node_relation_kind_str(n: &J) -> Option<&str> {
317 if n.get("cond").is_some() {
318 return None;
319 }
320 let sub = if n.get("map").is_some() || n.get("fanout").is_some() {
321 node_sub(n)
322 } else {
323 n
324 };
325 sub.get("relationKind").and_then(|v| v.as_str())
326}
327
328#[cfg(feature = "ir")]
329fn node_policy(n: &J) -> Option<String> {
330 if n.get("cond").is_some() {
331 return None;
332 }
333 let sub = if n.get("map").is_some() || n.get("fanout").is_some() {
334 node_sub(n)
335 } else {
336 n
337 };
338 sub.get("policy")
339 .and_then(|v| v.as_str())
340 .map(str::to_string)
341}
342
343#[cfg(feature = "ir")]
344fn eval_ports(ports: &J, scope: &[(String, Value)]) -> Result<Vec<(String, Value)>, BehaviorError> {
345 let obj = ports.as_object().ok_or_else(|| BehaviorError {
346 code: BehaviorFailureCode::UnknownNodeKind.as_str().to_string(),
347 message: "ports must be an object".into(),
348 })?;
349 let mut out = Vec::with_capacity(obj.len());
350 for (k, v) in obj {
351 out.push((k.clone(), evaluate_expression(v, scope)?));
352 }
353 Ok(out)
354}
355
356#[cfg(feature = "ir")]
358fn parse_plan(plan: Option<&J>) -> Option<ExecutionPlanSpec> {
359 let p = plan?;
360 if p.is_null() {
361 return None;
362 }
363 let groups = p
364 .get("groups")?
365 .as_array()?
366 .iter()
367 .map(|g| {
368 g.as_array()
369 .map(|row| {
370 row.iter()
371 .filter_map(|i| i.as_u64().map(|x| x as usize))
372 .collect()
373 })
374 .unwrap_or_default()
375 })
376 .collect();
377 let concurrency = p.get("concurrency").and_then(|c| c.as_i64()).unwrap_or(1);
378 Some(ExecutionPlanSpec {
379 groups,
380 concurrency,
381 })
382}
383
384#[cfg(feature = "ir")]
393pub fn run_behavior(
394 ir: &J,
395 handlers: &mut dyn ComponentExec,
396 input: &[(String, Value)],
397 entry: Option<&str>,
398) -> Result<Value, BehaviorError> {
399 let components = ir
400 .get("components")
401 .and_then(|c| c.as_array())
402 .ok_or_else(|| BehaviorError {
403 code: BehaviorFailureCode::UnknownEntry.as_str().to_string(),
404 message: "IR.components must be an array".into(),
405 })?;
406 let comp = match entry {
407 Some(name) => components
408 .iter()
409 .find(|c| c.get("name").and_then(|n| n.as_str()) == Some(name)),
410 None => components.first(),
411 };
412 let comp = match comp {
413 Some(c) => c,
414 None => {
415 return bfail(
416 BehaviorFailureCode::UnknownEntry,
417 format!("component '{}' not found in IR", entry.unwrap_or("<first>")),
418 )
419 }
420 };
421
422 let body: Vec<J> = comp
423 .get("body")
424 .and_then(|b| b.as_array())
425 .cloned()
426 .unwrap_or_default();
427
428 let index_of = |id: &str| {
429 body.iter()
430 .position(|n| n.get("id").and_then(|v| v.as_str()) == Some(id))
431 };
432
433 let mut ops: Vec<OpSpec> = Vec::with_capacity(body.len());
435 for n in &body {
436 let parent = node_parent(n).and_then(index_of);
437 ops.push(OpSpec {
438 id: n
439 .get("id")
440 .and_then(|v| v.as_str())
441 .unwrap_or_default()
442 .to_string(),
443 parent,
444 bind_field: node_bind_field(n),
445 relation_kind: node_relation_kind(n),
446 policy: node_policy(n),
447 });
448 }
449
450 run_behavior_inner(
451 &body,
452 &ops,
453 parse_plan(comp.get("plan")),
454 comp.get("output").cloned().unwrap_or(J::Null),
455 input,
456 handlers,
457 )
458}
459
460#[cfg(feature = "ir")]
464fn run_behavior_inner(
465 body: &[J],
466 ops: &[OpSpec],
467 plan: Option<ExecutionPlanSpec>,
468 output: J,
469 input: &[(String, Value)],
470 handlers: &mut dyn ComponentExec,
471) -> Result<Value, BehaviorError> {
472 let index_of = |id: &str| {
473 body.iter()
474 .position(|n| n.get("id").and_then(|v| v.as_str()) == Some(id))
475 };
476
477 let mut results: Vec<(String, Value)> = Vec::new();
478 let mut pending_err: Option<BehaviorError> = None;
479
480 let run = {
481 let results_cell = &mut results;
482 let err_cell = &mut pending_err;
483 let exec = |op: &OpSpec, _bound: Option<&Value>| -> ExecOutcome {
484 if err_cell.is_some() {
485 return ExecOutcome::Error("aborted".into());
486 }
487 let idx = match index_of(&op.id) {
488 Some(i) => i,
489 None => {
490 *err_cell = Some(BehaviorError {
491 code: BehaviorFailureCode::UnknownNodeKind.as_str().to_string(),
492 message: format!("no body node for op '{}'", op.id),
493 });
494 return ExecOutcome::Error("aborted".into());
495 }
496 };
497 let node = &body[idx];
498
499 let base_scope = |extra: Option<(&str, &Value)>| -> Vec<(String, Value)> {
500 let mut s: Vec<(String, Value)> = input.to_vec();
501 for (k, v) in results_cell.iter() {
502 s.push((k.clone(), v.clone()));
503 }
504 if let Some((k, v)) = extra {
505 s.push((k.to_string(), v.clone()));
506 }
507 s
508 };
509
510 let outcome = match node_kind(node) {
511 Err(e) => {
512 *err_cell = Some(e);
513 return ExecOutcome::Error("aborted".into());
514 }
515 Ok("cond") => {
516 let c = node.get("cond").unwrap();
517 let cond_expr = serde_json::json!({
518 "cond": [c.get("if"), c.get("then"), c.get("else")]
519 });
520 match evaluate_expression(&cond_expr, &base_scope(None)) {
521 Ok(v) => ExecOutcome::Ok(v),
522 Err(e) => {
523 *err_cell = Some(e.into());
524 return ExecOutcome::Error("aborted".into());
525 }
526 }
527 }
528 Ok("map") => {
529 let m = node.get("map").unwrap();
530 let over = match evaluate_expression(
531 m.get("over").unwrap_or(&J::Null),
532 &base_scope(None),
533 ) {
534 Ok(v) => v,
535 Err(e) => {
536 *err_cell = Some(e.into());
537 return ExecOutcome::Error("aborted".into());
538 }
539 };
540 let arr = match &over {
541 Value::Arr(a) => a.clone(),
542 _ => {
543 *err_cell = Some(BehaviorError {
544 code: BehaviorFailureCode::MapOverNotArray.as_str().to_string(),
545 message: format!("map '{}': 'over' is not an array", op.id),
546 });
547 return ExecOutcome::Error("aborted".into());
548 }
549 };
550 let component = m.get("component").and_then(|v| v.as_str()).unwrap_or("");
551 let as_name = m.get("as").and_then(|v| v.as_str()).unwrap_or("$");
552 let ports_j = m
553 .get("ports")
554 .cloned()
555 .unwrap_or(J::Object(Default::default()));
556 let when = m.get("when");
557 let batched = m.get("batched").and_then(|v| v.as_bool()).unwrap_or(false);
558 let into = m.get("into").and_then(|v| v.as_str());
559
560 let keep = |scope: &[(String, Value)]| -> Result<bool, BehaviorError> {
563 match when {
564 None => Ok(true),
565 Some(w) => {
566 let cond_expr = serde_json::json!({ "cond": [w, true, false] });
567 Ok(matches!(
568 evaluate_expression(&cond_expr, scope)?,
569 Value::Bool(true)
570 ))
571 }
572 }
573 };
574
575 let mut kept_idx: Vec<usize> = Vec::new(); let collected: Vec<Value>;
577 if batched {
578 let mut items: Vec<Value> = Vec::new();
580 for (i, el) in arr.iter().enumerate() {
581 let scope = base_scope(Some((as_name, el)));
582 match keep(&scope) {
583 Ok(false) => continue,
584 Ok(true) => {}
585 Err(e) => {
586 *err_cell = Some(e);
587 return ExecOutcome::Error("aborted".into());
588 }
589 }
590 match eval_ports(&ports_j, &scope) {
591 Ok(p) => items.push(Value::Obj(p)),
592 Err(e) => {
593 *err_cell = Some(e);
594 return ExecOutcome::Error("aborted".into());
595 }
596 }
597 kept_idx.push(i);
598 }
599 if items.is_empty() {
600 collected = Vec::new(); } else {
602 let want = items.len();
603 let batch_ports = vec![("items".to_string(), Value::Arr(items))];
604 match handlers.exec_ctx(&op.id, component, &batch_ports, None) {
605 None => {
606 *err_cell = Some(BehaviorError {
607 code: BehaviorFailureCode::UnknownComponent
608 .as_str()
609 .to_string(),
610 message: format!(
611 "component '{component}' has no handler (fail-closed)"
612 ),
613 });
614 return ExecOutcome::Error("aborted".into());
615 }
616 Some(ExecOutcome::Error(e)) => return ExecOutcome::Error(e),
617 Some(ExecOutcome::Ok(Value::Arr(r))) if r.len() == want => {
618 collected = r;
619 }
620 Some(ExecOutcome::Ok(_)) => {
621 *err_cell = Some(BehaviorError {
622 code: BehaviorFailureCode::MapBatchResultMismatch
623 .as_str()
624 .to_string(),
625 message: format!(
626 "map '{}': batched handler must return a list aligned to items (want {want})",
627 op.id
628 ),
629 });
630 return ExecOutcome::Error("aborted".into());
631 }
632 }
633 }
634 } else {
635 let mut out: Vec<Value> = Vec::with_capacity(arr.len());
636 for (i, el) in arr.iter().enumerate() {
637 let scope = base_scope(Some((as_name, el)));
638 match keep(&scope) {
639 Ok(false) => continue,
640 Ok(true) => {}
641 Err(e) => {
642 *err_cell = Some(e);
643 return ExecOutcome::Error("aborted".into());
644 }
645 }
646 let ports = match eval_ports(&ports_j, &scope) {
647 Ok(p) => p,
648 Err(e) => {
649 *err_cell = Some(e);
650 return ExecOutcome::Error("aborted".into());
651 }
652 };
653 match handlers.exec_ctx(&op.id, component, &ports, Some(el)) {
654 None => {
655 *err_cell = Some(BehaviorError {
656 code: BehaviorFailureCode::UnknownComponent
657 .as_str()
658 .to_string(),
659 message: format!(
660 "component '{component}' has no handler (fail-closed)"
661 ),
662 });
663 return ExecOutcome::Error("aborted".into());
664 }
665 Some(ExecOutcome::Error(e)) => return ExecOutcome::Error(e),
666 Some(ExecOutcome::Ok(v)) => out.push(v),
667 }
668 kept_idx.push(i);
669 }
670 collected = out;
671 }
672
673 match into {
674 None => ExecOutcome::Ok(Value::Arr(collected)),
675 Some(key) => {
676 let mut augmented: Vec<Value> = Vec::with_capacity(arr.len());
679 let mut k = 0usize;
680 for (i, el) in arr.iter().enumerate() {
681 if k < kept_idx.len() && kept_idx[k] == i {
682 let fields = match el {
683 Value::Obj(f) => f.clone(),
684 _ => {
685 *err_cell = Some(BehaviorError {
686 code: BehaviorFailureCode::MapIntoElementNotObject
687 .as_str()
688 .to_string(),
689 message: format!(
690 "map '{}': 'into' requires object elements (element {i} is not an object)",
691 op.id
692 ),
693 });
694 return ExecOutcome::Error("aborted".into());
695 }
696 };
697 let mut fields = fields;
698 let val = collected[k].clone();
699 match fields.iter_mut().find(|(fk, _)| fk == key) {
700 Some(slot) => slot.1 = val,
701 None => fields.push((key.to_string(), val)),
702 }
703 augmented.push(Value::Obj(fields));
704 k += 1;
705 } else {
706 augmented.push(el.clone());
707 }
708 }
709 ExecOutcome::Ok(Value::Arr(augmented))
710 }
711 }
712 }
713 Ok("fanout") => {
714 let f = node.get("fanout").unwrap();
718 let over = match evaluate_expression(
719 f.get("over").unwrap_or(&J::Null),
720 &base_scope(None),
721 ) {
722 Ok(v) => v,
723 Err(e) => {
724 *err_cell = Some(e.into());
725 return ExecOutcome::Error("aborted".into());
726 }
727 };
728 let arr = match &over {
729 Value::Arr(a) => a.clone(),
730 _ => {
731 *err_cell = Some(BehaviorError {
732 code: BehaviorFailureCode::FanoutOverNotArray.as_str().to_string(),
733 message: format!("fanout '{}': 'over' is not an array", op.id),
734 });
735 return ExecOutcome::Error("aborted".into());
736 }
737 };
738 let component = f.get("component").and_then(|v| v.as_str()).unwrap_or("");
739 let as_name = f.get("as").and_then(|v| v.as_str()).unwrap_or("$");
740 let ports_j = f
741 .get("ports")
742 .cloned()
743 .unwrap_or(J::Object(Default::default()));
744 let dedupe_key = f.get("dedupeKey").and_then(|v| v.as_str()).unwrap_or("");
745 let drop = f.get("drop").and_then(|v| v.as_str()).unwrap_or("dangling");
746 let implicit_source = f.get("implicitSource").and_then(|v| v.as_str());
747 let mut items: Vec<Value> = Vec::with_capacity(arr.len());
748 for el in arr.iter() {
749 let scope = base_scope(Some((as_name, el)));
750 match eval_ports(&ports_j, &scope) {
751 Ok(p) => items.push(Value::Obj(p)),
752 Err(e) => {
753 *err_cell = Some(e);
754 return ExecOutcome::Error("aborted".into());
755 }
756 }
757 }
758 if items.is_empty() {
759 ExecOutcome::Ok(Value::Obj(vec![
760 ("items".into(), Value::Arr(vec![])),
761 ("cursor".into(), Value::Null),
762 ]))
763 } else {
764 let want = items.len();
765 let batch_ports = vec![("items".to_string(), Value::Arr(items))];
766 match handlers.exec_ctx(&op.id, component, &batch_ports, None) {
767 None => {
768 *err_cell = Some(BehaviorError {
769 code: BehaviorFailureCode::UnknownComponent
770 .as_str()
771 .to_string(),
772 message: format!(
773 "component '{component}' has no handler (fail-closed)"
774 ),
775 });
776 ExecOutcome::Error("aborted".into())
777 }
778 Some(ExecOutcome::Error(e)) => ExecOutcome::Error(e),
779 Some(ExecOutcome::Ok(Value::Arr(r))) if r.len() == want => {
780 let deduped =
781 fanout_dedup_drop(r, dedupe_key, drop, implicit_source);
782 ExecOutcome::Ok(Value::Obj(vec![
783 ("items".into(), Value::Arr(deduped)),
784 ("cursor".into(), Value::Null),
785 ]))
786 }
787 Some(ExecOutcome::Ok(_)) => {
788 *err_cell = Some(BehaviorError {
789 code: BehaviorFailureCode::FanoutBatchResultMismatch
790 .as_str()
791 .to_string(),
792 message: format!(
793 "fanout '{}': batched handler must return a list aligned to the deduped id list (want {want})",
794 op.id
795 ),
796 });
797 ExecOutcome::Error("aborted".into())
798 }
799 }
800 }
801 }
802 Ok(_) => {
803 let component = node.get("component").and_then(|v| v.as_str()).unwrap_or("");
804 let ports_j = node
805 .get("ports")
806 .cloned()
807 .unwrap_or(J::Object(Default::default()));
808 let ports = match eval_ports(&ports_j, &base_scope(None)) {
809 Ok(p) => p,
810 Err(e) => {
811 *err_cell = Some(e);
812 return ExecOutcome::Error("aborted".into());
813 }
814 };
815 match handlers.exec_ctx(&op.id, component, &ports, None) {
816 None => {
817 *err_cell = Some(BehaviorError {
818 code: BehaviorFailureCode::UnknownComponent.as_str().to_string(),
819 message: format!(
820 "component '{component}' has no handler (fail-closed)"
821 ),
822 });
823 return ExecOutcome::Error("aborted".into());
824 }
825 Some(o) => o,
826 }
827 }
828 };
829
830 if let ExecOutcome::Ok(v) = &outcome {
831 results_cell.push((op.id.clone(), v.clone()));
832 }
833 outcome
834 };
835
836 run_plan(plan.as_ref(), ops, exec)
837 };
838
839 if let Some(e) = pending_err {
842 return Err(e);
843 }
844 let run = run?;
845
846 for (i, id_val) in run.skipped.iter().enumerate() {
849 let _ = i;
850 if let Some(idx) = index_of(id_val) {
851 let rk = node_relation_kind_str(&body[idx]);
852 let unproduced = if rk == Some("connection") {
853 Value::Obj(vec![
854 ("items".into(), Value::Arr(vec![])),
855 ("cursor".into(), Value::Null),
856 ])
857 } else {
858 Value::Null
859 };
860 if scope_get(&results, id_val).is_none() {
861 results.push((id_val.clone(), unproduced));
862 }
863 }
864 }
865
866 let scope: Vec<(String, Value)> = {
867 let mut s: Vec<(String, Value)> = input.to_vec();
868 s.extend(results.iter().cloned());
869 s
870 };
871 Ok(evaluate_expression(&output, &scope)?)
872}