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 pub detail: Option<Box<crate::plan::ErrorDetail>>,
81}
82
83impl BehaviorError {
84 pub fn new(code: impl Into<String>, message: impl Into<String>) -> Self {
89 BehaviorError {
90 code: code.into(),
91 message: message.into(),
92 detail: None,
93 }
94 }
95
96 pub fn code(&self) -> &str {
98 &self.code
99 }
100
101 pub fn detail(&self) -> Option<&crate::plan::ErrorDetail> {
103 self.detail.as_deref()
104 }
105}
106
107impl std::fmt::Display for BehaviorError {
108 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
109 write!(f, "{}: {}", self.code, self.message)
110 }
111}
112impl std::error::Error for BehaviorError {}
113
114impl From<ExprFailure> for BehaviorError {
115 fn from(e: ExprFailure) -> Self {
116 BehaviorError {
117 code: e.code.as_str().to_string(),
118 message: e.message,
119 detail: e.detail,
120 }
121 }
122}
123impl From<PlanFailure> for BehaviorError {
124 fn from(e: PlanFailure) -> Self {
125 BehaviorError {
126 code: e.code.as_str().to_string(),
127 message: e.message,
128 detail: e.detail,
129 }
130 }
131}
132
133#[cfg(feature = "ir")]
134fn bfail<T>(code: BehaviorFailureCode, message: impl Into<String>) -> Result<T, BehaviorError> {
135 Err(BehaviorError {
136 code: code.as_str().to_string(),
137 message: message.into(),
138 detail: None,
139 })
140}
141
142#[cfg(feature = "ir")]
145fn wire_type_name(v: &Value) -> &'static str {
146 v.type_name()
147}
148
149#[cfg(feature = "ir")]
151fn raw_value_of(v: &Value) -> String {
152 match v {
153 Value::Str(s) => s.clone(),
154 Value::Int(i) => i.to_string(),
155 Value::Float(f) => crate::canonical::py_float_repr(*f).unwrap_or_else(|_| f.to_string()),
156 Value::Bool(b) => b.to_string(),
157 Value::Null => "null".to_string(),
158 _ => crate::canonical::canonical_json(v).unwrap_or_else(|_| v.type_name().to_string()),
159 }
160}
161
162#[cfg(feature = "ir")]
164fn portable_type_notation(t: &J) -> String {
165 if let Some(s) = t.as_str() {
166 return s.to_string();
167 }
168 for key in ["opt", "arr", "map"] {
169 if let Some(inner) = t.get(key) {
170 return format!("{key}({})", portable_type_notation(inner));
171 }
172 }
173 if let Some(J::Object(o)) = t.get("obj") {
174 let parts: Vec<String> = o
175 .iter()
176 .map(|(k, v)| format!("{k}:{}", portable_type_notation(v)))
177 .collect();
178 return format!("obj{{{}}}", parts.join(","));
179 }
180 "?".to_string()
181}
182
183#[cfg(feature = "ir")]
186fn result_type_of(node: &J, out_type: &J) -> J {
187 if node.get("map").is_some() {
188 serde_json::json!({ "arr": out_type })
189 } else {
190 out_type.clone()
191 }
192}
193
194#[cfg(feature = "ir")]
195fn conform_fail(
196 code: crate::expr::ExprFailureCode,
197 message: String,
198 kind: crate::plan::ErrorKind,
199 node_id: &str,
200 field: &str,
201 expected: &J,
202 actual: Option<&Value>,
203) -> ExprFailure {
204 let mut detail = crate::plan::ErrorDetail {
205 kind: Some(kind),
206 model: Some(node_id.to_string()),
207 field: Some(field.to_string()),
208 expected_type: Some(portable_type_notation(expected)),
209 ..Default::default()
210 };
211 if let Some(a) = actual {
212 detail.actual_wire_type = Some(wire_type_name(a).to_string());
213 detail.raw_value = Some(raw_value_of(a));
214 }
215 ExprFailure {
216 code,
217 message,
218 detail: Some(Box::new(detail)),
219 }
220}
221
222#[cfg(feature = "ir")]
224fn obj_set(pairs: &mut Vec<(String, Value)>, key: &str, val: Value) {
225 if let Some(slot) = pairs.iter_mut().find(|(k, _)| k == key) {
226 slot.1 = val;
227 } else {
228 pairs.push((key.to_string(), val));
229 }
230}
231
232#[cfg(feature = "ir")]
239fn assert_conforms_to_out_type(
240 node_id: &str,
241 v: &Value,
242 t: &J,
243 field: &str,
244) -> Result<Value, ExprFailure> {
245 use crate::expr::ExprFailureCode::{MissingProp, TypeMismatch};
246 use crate::plan::ErrorKind;
247 if let Some(scalar) = t.as_str() {
248 if scalar == "float" {
249 if let Value::Int(i) = v {
250 return Ok(Value::Float(*i as f64)); }
252 }
253 let ok = match scalar {
254 "string" => matches!(v, Value::Str(_)),
255 "int" => matches!(v, Value::Int(_)),
256 "float" => matches!(v, Value::Float(_)),
257 "bool" => matches!(v, Value::Bool(_)),
258 "null" => matches!(v, Value::Null),
259 _ => true,
260 };
261 if !ok {
262 return Err(conform_fail(
263 TypeMismatch,
264 format!(
265 "node '{node_id}': {field}: expected {scalar}, got {}",
266 wire_type_name(v)
267 ),
268 ErrorKind::TypeMismatch,
269 node_id,
270 field,
271 t,
272 Some(v),
273 ));
274 }
275 return Ok(v.clone());
276 }
277 if let Some(inner) = t.get("opt") {
278 if matches!(v, Value::Null) {
279 return Ok(Value::Null);
280 }
281 return assert_conforms_to_out_type(node_id, v, inner, field);
282 }
283 if let Some(inner) = t.get("arr") {
284 match v {
285 Value::Arr(items) => {
286 let mut out = Vec::with_capacity(items.len());
287 for (i, el) in items.iter().enumerate() {
288 out.push(assert_conforms_to_out_type(
289 node_id,
290 el,
291 inner,
292 &format!("{field}[{i}]"),
293 )?);
294 }
295 return Ok(Value::Arr(out));
296 }
297 _ => {
298 return Err(conform_fail(
299 TypeMismatch,
300 format!(
301 "node '{node_id}': {field}: expected arr, got {}",
302 wire_type_name(v)
303 ),
304 ErrorKind::TypeMismatch,
305 node_id,
306 field,
307 t,
308 Some(v),
309 ))
310 }
311 }
312 }
313 if let Some(inner) = t.get("map") {
314 match v {
315 Value::Obj(pairs) => {
316 let mut out = Vec::with_capacity(pairs.len());
317 for (k, mv) in pairs {
318 out.push((
319 k.clone(),
320 assert_conforms_to_out_type(node_id, mv, inner, &format!("{field}.{k}"))?,
321 ));
322 }
323 return Ok(Value::Obj(out));
324 }
325 _ => {
326 return Err(conform_fail(
327 TypeMismatch,
328 format!(
329 "node '{node_id}': {field}: expected map, got {}",
330 wire_type_name(v)
331 ),
332 ErrorKind::TypeMismatch,
333 node_id,
334 field,
335 t,
336 Some(v),
337 ))
338 }
339 }
340 }
341 if let Some(J::Object(fields)) = t.get("obj") {
343 let mut out: Vec<(String, Value)> = match v {
344 Value::Obj(pairs) => pairs.clone(),
345 _ => {
346 return Err(conform_fail(
347 TypeMismatch,
348 format!(
349 "node '{node_id}': {field}: expected obj, got {}",
350 wire_type_name(v)
351 ),
352 ErrorKind::TypeMismatch,
353 node_id,
354 field,
355 t,
356 Some(v),
357 ));
358 }
359 };
360 for (k, ft) in fields {
361 match v.obj_get(k) {
362 None => {
363 if ft.get("opt").is_some() {
364 obj_set(&mut out, k, Value::Null); } else {
366 return Err(conform_fail(
367 MissingProp,
368 format!("node '{node_id}': {field}: missing property .{k}"),
369 ErrorKind::MissingField,
370 node_id,
371 &format!("{field}.{k}"),
372 ft,
373 None,
374 ));
375 }
376 }
377 Some(fv) => {
378 let nv = assert_conforms_to_out_type(node_id, fv, ft, &format!("{field}.{k}"))?;
379 obj_set(&mut out, k, nv);
380 }
381 }
382 }
383 return Ok(Value::Obj(out));
384 }
385 Ok(v.clone())
386}
387
388pub trait ComponentExec {
393 fn exec(
394 &mut self,
395 component: &str,
396 ports: &[(String, Value)],
397 bound: Option<&Value>,
398 ) -> Option<ExecOutcome>;
399
400 fn exec_ctx(
406 &mut self,
407 node_id: &str,
408 component: &str,
409 ports: &[(String, Value)],
410 bound: Option<&Value>,
411 ) -> Option<ExecOutcome> {
412 let _ = node_id;
413 self.exec(component, ports, bound)
414 }
415}
416
417impl ComponentExec for &mut dyn ComponentExec {
434 fn exec(
435 &mut self,
436 component: &str,
437 ports: &[(String, Value)],
438 bound: Option<&Value>,
439 ) -> Option<ExecOutcome> {
440 (**self).exec(component, ports, bound)
441 }
442
443 fn exec_ctx(
444 &mut self,
445 node_id: &str,
446 component: &str,
447 ports: &[(String, Value)],
448 bound: Option<&Value>,
449 ) -> Option<ExecOutcome> {
450 (**self).exec_ctx(node_id, component, ports, bound)
451 }
452}
453
454#[cfg(feature = "ir")]
455fn scope_get<'a>(scope: &'a [(String, Value)], key: &str) -> Option<&'a Value> {
456 scope.iter().find(|(k, _)| k == key).map(|(_, v)| v)
457}
458
459#[cfg(feature = "ir")]
465fn fanout_dedup_drop(
466 aligned_bodies: Vec<Value>,
467 dedupe_key: &str,
468 drop: &str,
469 implicit_source: Option<&str>,
470) -> Vec<Value> {
471 use std::collections::HashSet;
472 let mut items: Vec<Value> = Vec::with_capacity(aligned_bodies.len());
473 let mut seen: HashSet<String> = HashSet::new();
474 for body in aligned_bodies.into_iter() {
475 let fields = match &body {
476 Value::Obj(f) => Some(f),
477 _ => None,
478 };
479 let key_val = fields.and_then(|f| f.iter().find(|(k, _)| k == dedupe_key).map(|(_, v)| v));
480 let has_key = matches!(key_val, Some(v) if !matches!(v, Value::Null));
481 if !has_key {
482 if drop == "dangling" {
483 continue;
484 }
485 items.push(body); continue;
487 }
488 let seen_key = match key_val.unwrap() {
489 Value::Str(s) => format!("s:{s}"),
490 other => format!("j:{other:?}"),
491 };
492 if seen.contains(&seen_key) {
493 continue;
494 }
495 seen.insert(seen_key);
496 match (implicit_source, fields) {
497 (Some(src), Some(f)) if f.iter().any(|(k, _)| k == src) => {
498 let stripped: Vec<(String, Value)> =
499 f.iter().filter(|(k, _)| k != src).cloned().collect();
500 items.push(Value::Obj(stripped));
501 }
502 _ => items.push(body),
503 }
504 }
505 items
506}
507
508#[cfg(feature = "ir")]
509fn node_kind(n: &J) -> Result<&'static str, BehaviorError> {
510 if n.get("fanout").is_some() {
511 Ok("fanout")
512 } else if n.get("map").is_some() {
513 Ok("map")
514 } else if n.get("cond").is_some() {
515 Ok("cond")
516 } else if n.get("component").is_some() {
517 Ok("componentRef")
518 } else {
519 bfail(
520 BehaviorFailureCode::UnknownNodeKind,
521 format!(
522 "body node '{}' is not componentRef/map/cond/fanout",
523 n.get("id").and_then(|v| v.as_str()).unwrap_or("?")
524 ),
525 )
526 }
527}
528
529#[cfg(feature = "ir")]
530fn node_sub(n: &J) -> &J {
531 if let Some(f) = n.get("fanout") {
532 f
533 } else if let Some(m) = n.get("map") {
534 m
535 } else if let Some(c) = n.get("cond") {
536 c
537 } else {
538 n
539 }
540}
541
542#[cfg(feature = "ir")]
543fn node_parent(n: &J) -> Option<&str> {
544 node_sub(n).get("parent").and_then(|v| v.as_str())
545}
546
547#[cfg(feature = "ir")]
548fn node_bind_field(n: &J) -> Option<String> {
549 if n.get("map").is_some() || n.get("cond").is_some() || n.get("fanout").is_some() {
550 return None;
551 }
552 n.get("bindField")
553 .and_then(|v| v.as_str())
554 .map(str::to_string)
555}
556
557#[cfg(feature = "ir")]
558fn node_relation_kind(n: &J) -> Option<RelationKind> {
559 if n.get("cond").is_some() {
560 return None;
561 }
562 let sub = if n.get("map").is_some() || n.get("fanout").is_some() {
563 node_sub(n)
564 } else {
565 n
566 };
567 match sub.get("relationKind").and_then(|v| v.as_str()) {
568 Some("connection") => Some(RelationKind::Connection),
569 Some(_) => Some(RelationKind::Single),
570 None => None,
571 }
572}
573
574#[cfg(feature = "ir")]
575fn node_relation_kind_str(n: &J) -> Option<&str> {
576 if n.get("cond").is_some() {
577 return None;
578 }
579 let sub = if n.get("map").is_some() || n.get("fanout").is_some() {
580 node_sub(n)
581 } else {
582 n
583 };
584 sub.get("relationKind").and_then(|v| v.as_str())
585}
586
587#[cfg(feature = "ir")]
588fn node_policy(n: &J) -> Option<String> {
589 if n.get("cond").is_some() {
590 return None;
591 }
592 let sub = if n.get("map").is_some() || n.get("fanout").is_some() {
593 node_sub(n)
594 } else {
595 n
596 };
597 sub.get("policy")
598 .and_then(|v| v.as_str())
599 .map(str::to_string)
600}
601
602#[cfg(feature = "ir")]
603fn eval_ports(ports: &J, scope: &[(String, Value)]) -> Result<Vec<(String, Value)>, BehaviorError> {
604 let obj = ports.as_object().ok_or_else(|| BehaviorError {
605 code: BehaviorFailureCode::UnknownNodeKind.as_str().to_string(),
606 message: "ports must be an object".into(),
607 detail: None,
608 })?;
609 let mut out = Vec::with_capacity(obj.len());
610 for (k, v) in obj {
611 out.push((k.clone(), evaluate_expression(v, scope)?));
612 }
613 Ok(out)
614}
615
616#[cfg(feature = "ir")]
618fn parse_plan(plan: Option<&J>) -> Option<ExecutionPlanSpec> {
619 let p = plan?;
620 if p.is_null() {
621 return None;
622 }
623 let groups = p
624 .get("groups")?
625 .as_array()?
626 .iter()
627 .map(|g| {
628 g.as_array()
629 .map(|row| {
630 row.iter()
631 .filter_map(|i| i.as_u64().map(|x| x as usize))
632 .collect()
633 })
634 .unwrap_or_default()
635 })
636 .collect();
637 let concurrency = p.get("concurrency").and_then(|c| c.as_i64()).unwrap_or(1);
638 Some(ExecutionPlanSpec {
639 groups,
640 concurrency,
641 })
642}
643
644#[cfg(feature = "ir")]
653pub fn run_behavior(
654 ir: &J,
655 handlers: &mut dyn ComponentExec,
656 input: &[(String, Value)],
657 entry: Option<&str>,
658) -> Result<Value, BehaviorError> {
659 let components = ir
660 .get("components")
661 .and_then(|c| c.as_array())
662 .ok_or_else(|| BehaviorError {
663 code: BehaviorFailureCode::UnknownEntry.as_str().to_string(),
664 message: "IR.components must be an array".into(),
665 detail: None,
666 })?;
667 let comp = match entry {
668 Some(name) => components
669 .iter()
670 .find(|c| c.get("name").and_then(|n| n.as_str()) == Some(name)),
671 None => components.first(),
672 };
673 let comp = match comp {
674 Some(c) => c,
675 None => {
676 return bfail(
677 BehaviorFailureCode::UnknownEntry,
678 format!("component '{}' not found in IR", entry.unwrap_or("<first>")),
679 )
680 }
681 };
682
683 let body: Vec<J> = comp
684 .get("body")
685 .and_then(|b| b.as_array())
686 .cloned()
687 .unwrap_or_default();
688
689 let index_of = |id: &str| {
690 body.iter()
691 .position(|n| n.get("id").and_then(|v| v.as_str()) == Some(id))
692 };
693
694 let mut ops: Vec<OpSpec> = Vec::with_capacity(body.len());
696 for n in &body {
697 let parent = node_parent(n).and_then(index_of);
698 ops.push(OpSpec {
699 id: n
700 .get("id")
701 .and_then(|v| v.as_str())
702 .unwrap_or_default()
703 .to_string(),
704 parent,
705 bind_field: node_bind_field(n),
706 relation_kind: node_relation_kind(n),
707 policy: node_policy(n),
708 });
709 }
710
711 run_behavior_inner(
712 &body,
713 &ops,
714 parse_plan(comp.get("plan")),
715 comp.get("output").cloned().unwrap_or(J::Null),
716 &bind_input_scope(comp, input),
717 handlers,
718 )
719}
720
721#[cfg(feature = "ir")]
733fn bind_input_scope(comp: &J, input: &[(String, Value)]) -> Vec<(String, Value)> {
734 let mut scope: Vec<(String, Value)> = input.to_vec();
735 if let Some(ports) = comp.get("inputPorts").and_then(|p| p.as_object()) {
736 for (name, schema) in ports {
737 let optional = schema.get("required").and_then(|r| r.as_bool()) == Some(false);
738 if optional && !scope.iter().any(|(k, _)| k == name) {
739 scope.push((name.clone(), Value::Null));
740 }
741 }
742 }
743 scope
744}
745
746#[cfg(feature = "ir")]
750fn run_behavior_inner(
751 body: &[J],
752 ops: &[OpSpec],
753 plan: Option<ExecutionPlanSpec>,
754 output: J,
755 input: &[(String, Value)],
756 handlers: &mut dyn ComponentExec,
757) -> Result<Value, BehaviorError> {
758 let index_of = |id: &str| {
759 body.iter()
760 .position(|n| n.get("id").and_then(|v| v.as_str()) == Some(id))
761 };
762
763 let mut results: Vec<(String, Value)> = Vec::new();
764 let mut pending_err: Option<BehaviorError> = None;
765
766 let run = {
767 let results_cell = &mut results;
768 let err_cell = &mut pending_err;
769 let exec = |op: &OpSpec, _bound: Option<&Value>| -> ExecOutcome {
770 if err_cell.is_some() {
771 return ExecOutcome::error("aborted");
772 }
773 let idx = match index_of(&op.id) {
774 Some(i) => i,
775 None => {
776 *err_cell = Some(BehaviorError {
777 code: BehaviorFailureCode::UnknownNodeKind.as_str().to_string(),
778 message: format!("no body node for op '{}'", op.id),
779 detail: None,
780 });
781 return ExecOutcome::error("aborted");
782 }
783 };
784 let node = &body[idx];
785
786 let base_scope = |extra: Option<(&str, &Value)>| -> Vec<(String, Value)> {
787 let mut s: Vec<(String, Value)> = input.to_vec();
788 for (k, v) in results_cell.iter() {
789 s.push((k.clone(), v.clone()));
790 }
791 if let Some((k, v)) = extra {
792 s.push((k.to_string(), v.clone()));
793 }
794 s
795 };
796
797 let outcome = match node_kind(node) {
798 Err(e) => {
799 *err_cell = Some(e);
800 return ExecOutcome::error("aborted");
801 }
802 Ok("cond") => {
803 let c = node.get("cond").unwrap();
804 let cond_expr = serde_json::json!({
805 "cond": [c.get("if"), c.get("then"), c.get("else")]
806 });
807 match evaluate_expression(&cond_expr, &base_scope(None)) {
808 Ok(v) => ExecOutcome::Ok(v),
809 Err(e) => {
810 *err_cell = Some(e.into());
811 return ExecOutcome::error("aborted");
812 }
813 }
814 }
815 Ok("map") => {
816 let m = node.get("map").unwrap();
817 let over = match evaluate_expression(
818 m.get("over").unwrap_or(&J::Null),
819 &base_scope(None),
820 ) {
821 Ok(v) => v,
822 Err(e) => {
823 *err_cell = Some(e.into());
824 return ExecOutcome::error("aborted");
825 }
826 };
827 let arr = match &over {
828 Value::Arr(a) => a.clone(),
829 _ => {
830 *err_cell = Some(BehaviorError {
831 code: BehaviorFailureCode::MapOverNotArray.as_str().to_string(),
832 message: format!("map '{}': 'over' is not an array", op.id),
833 detail: None,
834 });
835 return ExecOutcome::error("aborted");
836 }
837 };
838 let component = m.get("component").and_then(|v| v.as_str()).unwrap_or("");
839 let as_name = m.get("as").and_then(|v| v.as_str()).unwrap_or("$");
840 let ports_j = m
841 .get("ports")
842 .cloned()
843 .unwrap_or(J::Object(Default::default()));
844 let when = m.get("when");
845 let batched = m.get("batched").and_then(|v| v.as_bool()).unwrap_or(false);
846 let element_policy = {
850 let raw = m
851 .get("elementPolicy")
852 .and_then(|v| v.as_str())
853 .unwrap_or("error");
854 match crate::plan::ElementPolicyKind::parse(raw) {
855 None => {
856 *err_cell = Some(BehaviorError {
857 code: "UNKNOWN_ELEMENT_POLICY".to_string(),
858 message: format!(
859 "map '{}': unknown element policy '{raw}' (fail-closed)",
860 op.id
861 ),
862 detail: None,
863 });
864 return ExecOutcome::error("aborted");
865 }
866 Some(crate::plan::ElementPolicyKind::Skip) if batched => {
867 *err_cell = Some(BehaviorError {
868 code: "ELEMENT_POLICY_NOT_APPLICABLE".to_string(),
869 message: format!("map '{}': elementPolicy 'skip' needs a per-element Failure, but a batched map takes ONE outcome for the whole batch (fail-closed)", op.id),
870 detail: None,
871 });
872 return ExecOutcome::error("aborted");
873 }
874 Some(p) => p,
875 }
876 };
877 let into = m.get("into").and_then(|v| v.as_str());
878
879 let keep = |scope: &[(String, Value)]| -> Result<bool, BehaviorError> {
882 match when {
883 None => Ok(true),
884 Some(w) => {
885 let cond_expr = serde_json::json!({ "cond": [w, true, false] });
886 Ok(matches!(
887 evaluate_expression(&cond_expr, scope)?,
888 Value::Bool(true)
889 ))
890 }
891 }
892 };
893
894 let mut kept_idx: Vec<usize> = Vec::new(); let collected: Vec<Value>;
896 if batched {
897 let mut items: Vec<Value> = Vec::new();
899 for (i, el) in arr.iter().enumerate() {
900 let scope = base_scope(Some((as_name, el)));
901 match keep(&scope) {
902 Ok(false) => continue,
903 Ok(true) => {}
904 Err(e) => {
905 *err_cell = Some(e);
906 return ExecOutcome::error("aborted");
907 }
908 }
909 match eval_ports(&ports_j, &scope) {
910 Ok(p) => items.push(Value::Obj(p)),
911 Err(e) => {
912 *err_cell = Some(e);
913 return ExecOutcome::error("aborted");
914 }
915 }
916 kept_idx.push(i);
917 }
918 if items.is_empty() {
919 collected = Vec::new(); } else {
921 let want = items.len();
922 let batch_ports = vec![("items".to_string(), Value::Arr(items))];
923 match handlers.exec_ctx(&op.id, component, &batch_ports, None) {
924 None => {
925 *err_cell = Some(BehaviorError {
926 code: BehaviorFailureCode::UnknownComponent
927 .as_str()
928 .to_string(),
929 message: format!(
930 "component '{component}' has no handler (fail-closed)"
931 ),
932 detail: None,
933 });
934 return ExecOutcome::error("aborted");
935 }
936 Some(ExecOutcome::Error(e, d)) => return ExecOutcome::Error(e, d),
937 Some(ExecOutcome::Ok(Value::Arr(r))) if r.len() == want => {
938 collected = r;
939 }
940 Some(ExecOutcome::Ok(_)) => {
941 *err_cell = Some(BehaviorError {
942 code: BehaviorFailureCode::MapBatchResultMismatch
943 .as_str()
944 .to_string(),
945 message: format!(
946 "map '{}': batched handler must return a list aligned to items (want {want})",
947 op.id
948 ),
949 detail: None,
950 });
951 return ExecOutcome::error("aborted");
952 }
953 }
954 }
955 } else {
956 let mut out: Vec<Value> = Vec::with_capacity(arr.len());
957 for (i, el) in arr.iter().enumerate() {
958 let scope = base_scope(Some((as_name, el)));
959 match keep(&scope) {
960 Ok(false) => continue,
961 Ok(true) => {}
962 Err(e) => {
963 *err_cell = Some(e);
964 return ExecOutcome::error("aborted");
965 }
966 }
967 let ports = match eval_ports(&ports_j, &scope) {
968 Ok(p) => p,
969 Err(e) => {
970 *err_cell = Some(e);
971 return ExecOutcome::error("aborted");
972 }
973 };
974 match handlers.exec_ctx(&op.id, component, &ports, Some(el)) {
975 None => {
976 *err_cell = Some(BehaviorError {
977 code: BehaviorFailureCode::UnknownComponent
978 .as_str()
979 .to_string(),
980 message: format!(
981 "component '{component}' has no handler (fail-closed)"
982 ),
983 detail: None,
984 });
985 return ExecOutcome::error("aborted");
986 }
987 Some(ExecOutcome::Error(e, d)) => {
991 if element_policy == crate::plan::ElementPolicyKind::Skip {
992 continue;
993 }
994 return ExecOutcome::Error(e, d);
995 }
996 Some(ExecOutcome::Ok(v)) => out.push(v),
997 }
998 kept_idx.push(i);
999 }
1000 collected = out;
1001 }
1002
1003 match into {
1004 None => ExecOutcome::Ok(Value::Arr(collected)),
1005 Some(key) => {
1006 let mut augmented: Vec<Value> = Vec::with_capacity(arr.len());
1009 let mut k = 0usize;
1010 for (i, el) in arr.iter().enumerate() {
1011 if k < kept_idx.len() && kept_idx[k] == i {
1012 let fields = match el {
1013 Value::Obj(f) => f.clone(),
1014 _ => {
1015 *err_cell = Some(BehaviorError {
1016 code: BehaviorFailureCode::MapIntoElementNotObject
1017 .as_str()
1018 .to_string(),
1019 message: format!(
1020 "map '{}': 'into' requires object elements (element {i} is not an object)",
1021 op.id
1022 ),
1023 detail: None,
1024 });
1025 return ExecOutcome::error("aborted");
1026 }
1027 };
1028 let mut fields = fields;
1029 let val = collected[k].clone();
1030 match fields.iter_mut().find(|(fk, _)| fk == key) {
1031 Some(slot) => slot.1 = val,
1032 None => fields.push((key.to_string(), val)),
1033 }
1034 augmented.push(Value::Obj(fields));
1035 k += 1;
1036 } else {
1037 augmented.push(el.clone());
1038 }
1039 }
1040 ExecOutcome::Ok(Value::Arr(augmented))
1041 }
1042 }
1043 }
1044 Ok("fanout") => {
1045 let f = node.get("fanout").unwrap();
1049 let over = match evaluate_expression(
1050 f.get("over").unwrap_or(&J::Null),
1051 &base_scope(None),
1052 ) {
1053 Ok(v) => v,
1054 Err(e) => {
1055 *err_cell = Some(e.into());
1056 return ExecOutcome::error("aborted");
1057 }
1058 };
1059 let arr = match &over {
1060 Value::Arr(a) => a.clone(),
1061 _ => {
1062 *err_cell = Some(BehaviorError {
1063 code: BehaviorFailureCode::FanoutOverNotArray.as_str().to_string(),
1064 message: format!("fanout '{}': 'over' is not an array", op.id),
1065 detail: None,
1066 });
1067 return ExecOutcome::error("aborted");
1068 }
1069 };
1070 let component = f.get("component").and_then(|v| v.as_str()).unwrap_or("");
1071 let as_name = f.get("as").and_then(|v| v.as_str()).unwrap_or("$");
1072 let ports_j = f
1073 .get("ports")
1074 .cloned()
1075 .unwrap_or(J::Object(Default::default()));
1076 let dedupe_key = f.get("dedupeKey").and_then(|v| v.as_str()).unwrap_or("");
1077 let drop = f.get("drop").and_then(|v| v.as_str()).unwrap_or("dangling");
1078 let implicit_source = f.get("implicitSource").and_then(|v| v.as_str());
1079 let mut items: Vec<Value> = Vec::with_capacity(arr.len());
1080 for el in arr.iter() {
1081 let scope = base_scope(Some((as_name, el)));
1082 match eval_ports(&ports_j, &scope) {
1083 Ok(p) => items.push(Value::Obj(p)),
1084 Err(e) => {
1085 *err_cell = Some(e);
1086 return ExecOutcome::error("aborted");
1087 }
1088 }
1089 }
1090 if items.is_empty() {
1091 ExecOutcome::Ok(Value::Obj(vec![
1092 ("items".into(), Value::Arr(vec![])),
1093 ("cursor".into(), Value::Null),
1094 ]))
1095 } else {
1096 let want = items.len();
1097 let batch_ports = vec![("items".to_string(), Value::Arr(items))];
1098 match handlers.exec_ctx(&op.id, component, &batch_ports, None) {
1099 None => {
1100 *err_cell = Some(BehaviorError {
1101 code: BehaviorFailureCode::UnknownComponent
1102 .as_str()
1103 .to_string(),
1104 message: format!(
1105 "component '{component}' has no handler (fail-closed)"
1106 ),
1107 detail: None,
1108 });
1109 ExecOutcome::error("aborted")
1110 }
1111 Some(ExecOutcome::Error(e, d)) => ExecOutcome::Error(e, d),
1112 Some(ExecOutcome::Ok(Value::Arr(r))) if r.len() == want => {
1113 let deduped =
1114 fanout_dedup_drop(r, dedupe_key, drop, implicit_source);
1115 ExecOutcome::Ok(Value::Obj(vec![
1116 ("items".into(), Value::Arr(deduped)),
1117 ("cursor".into(), Value::Null),
1118 ]))
1119 }
1120 Some(ExecOutcome::Ok(_)) => {
1121 *err_cell = Some(BehaviorError {
1122 code: BehaviorFailureCode::FanoutBatchResultMismatch
1123 .as_str()
1124 .to_string(),
1125 message: format!(
1126 "fanout '{}': batched handler must return a list aligned to the deduped id list (want {want})",
1127 op.id
1128 ),
1129 detail: None,
1130 });
1131 ExecOutcome::error("aborted")
1132 }
1133 }
1134 }
1135 }
1136 Ok(_) => {
1137 let component = node.get("component").and_then(|v| v.as_str()).unwrap_or("");
1138 let ports_j = node
1139 .get("ports")
1140 .cloned()
1141 .unwrap_or(J::Object(Default::default()));
1142 let ports = match eval_ports(&ports_j, &base_scope(None)) {
1143 Ok(p) => p,
1144 Err(e) => {
1145 *err_cell = Some(e);
1146 return ExecOutcome::error("aborted");
1147 }
1148 };
1149 match handlers.exec_ctx(&op.id, component, &ports, None) {
1150 None => {
1151 *err_cell = Some(BehaviorError {
1152 code: BehaviorFailureCode::UnknownComponent.as_str().to_string(),
1153 message: format!(
1154 "component '{component}' has no handler (fail-closed)"
1155 ),
1156 detail: None,
1157 });
1158 return ExecOutcome::error("aborted");
1159 }
1160 Some(o) => o,
1161 }
1162 }
1163 };
1164
1165 if let ExecOutcome::Ok(v) = &outcome {
1166 let mut value = v.clone();
1171 if let Some(ot) = node.get("outType") {
1172 let want = result_type_of(node, ot);
1173 match assert_conforms_to_out_type(&op.id, &value, &want, "result") {
1174 Ok(normalized) => value = normalized,
1175 Err(e) => {
1176 *err_cell = Some(e.into());
1177 return ExecOutcome::error("aborted");
1178 }
1179 }
1180 }
1181 results_cell.push((op.id.clone(), value));
1182 }
1183 outcome
1184 };
1185
1186 run_plan(plan.as_ref(), ops, exec)
1187 };
1188
1189 if let Some(e) = pending_err {
1192 return Err(e);
1193 }
1194 let run = run?;
1195
1196 for (i, id_val) in run.skipped.iter().enumerate() {
1199 let _ = i;
1200 if let Some(idx) = index_of(id_val) {
1201 let rk = node_relation_kind_str(&body[idx]);
1202 let unproduced = if rk == Some("connection") {
1203 Value::Obj(vec![
1204 ("items".into(), Value::Arr(vec![])),
1205 ("cursor".into(), Value::Null),
1206 ])
1207 } else {
1208 Value::Null
1209 };
1210 if scope_get(&results, id_val).is_none() {
1211 results.push((id_val.clone(), unproduced));
1212 }
1213 }
1214 }
1215
1216 let scope: Vec<(String, Value)> = {
1217 let mut s: Vec<(String, Value)> = input.to_vec();
1218 s.extend(results.iter().cloned());
1219 s
1220 };
1221 Ok(evaluate_expression(&output, &scope)?)
1222}