1use crate::javascript::HttpMethod;
26use crate::types::ExecutionError;
27use serde::Serialize;
28use serde_json::Value as JsonValue;
29use std::collections::{HashMap, HashSet};
30
31use crate::eval::{
33 evaluate as shared_evaluate, is_truthy as shared_is_truthy,
34 json_to_string_with_mode as shared_json_to_string_with_mode, JsonStringMode,
35};
36use swc_common::{FileName, SourceMap};
37use swc_ecma_ast::*;
38use swc_ecma_parser::{lexer::Lexer, Parser, StringInput, Syntax};
39
40pub(crate) enum StepOutcome {
45 None,
47 Return(serde_json::Value),
49 Continue,
51 Break,
53}
54
55#[derive(Debug, Clone)]
57pub struct ExecutionConfig {
58 pub max_api_calls: usize,
60 pub timeout_seconds: u64,
62 pub max_loop_iterations: usize,
64 pub blocked_fields: HashSet<String>,
68 pub output_blocked_fields: HashSet<String>,
72}
73
74impl Default for ExecutionConfig {
75 fn default() -> Self {
76 Self {
77 max_api_calls: 50,
78 timeout_seconds: 30,
79 max_loop_iterations: 100,
80 blocked_fields: HashSet::new(),
81 output_blocked_fields: HashSet::new(),
82 }
83 }
84}
85
86impl ExecutionConfig {
87 pub fn with_blocked_fields(
89 mut self,
90 fields: impl IntoIterator<Item = impl Into<String>>,
91 ) -> Self {
92 self.blocked_fields = fields.into_iter().map(Into::into).collect();
93 self
94 }
95
96 pub fn with_output_blocked_fields(
98 mut self,
99 fields: impl IntoIterator<Item = impl Into<String>>,
100 ) -> Self {
101 self.output_blocked_fields = fields.into_iter().map(Into::into).collect();
102 self
103 }
104}
105
106pub fn filter_blocked_fields(value: JsonValue, blocked_fields: &HashSet<String>) -> JsonValue {
120 if blocked_fields.is_empty() {
121 return value;
122 }
123
124 match value {
125 JsonValue::Object(mut map) => {
126 map.retain(|key, _| !blocked_fields.contains(key));
128
129 let filtered: serde_json::Map<String, JsonValue> = map
131 .into_iter()
132 .map(|(k, v)| (k, filter_blocked_fields(v, blocked_fields)))
133 .collect();
134
135 JsonValue::Object(filtered)
136 },
137 JsonValue::Array(arr) => {
138 let filtered: Vec<JsonValue> = arr
140 .into_iter()
141 .map(|v| filter_blocked_fields(v, blocked_fields))
142 .collect();
143
144 JsonValue::Array(filtered)
145 },
146 other => other,
148 }
149}
150
151pub fn find_blocked_fields_in_output(
167 value: &JsonValue,
168 blocked_fields: &HashSet<String>,
169) -> Vec<String> {
170 if blocked_fields.is_empty() {
171 return Vec::new();
172 }
173
174 let mut violations = Vec::new();
175 find_blocked_fields_recursive(value, blocked_fields, "", &mut violations);
176 violations
177}
178
179fn find_blocked_fields_recursive(
181 value: &JsonValue,
182 blocked_fields: &HashSet<String>,
183 path: &str,
184 violations: &mut Vec<String>,
185) {
186 match value {
187 JsonValue::Object(map) => visit_object(map, blocked_fields, path, violations),
188 JsonValue::Array(arr) => visit_array(arr, blocked_fields, path, violations),
189 _ => {},
191 }
192}
193
194fn visit_object(
197 map: &serde_json::Map<String, JsonValue>,
198 blocked_fields: &HashSet<String>,
199 path: &str,
200 violations: &mut Vec<String>,
201) {
202 for (key, v) in map {
203 let key_path = join_field_path(path, key);
204 if blocked_fields.contains(key) {
205 violations.push(key_path.clone());
206 }
207 find_blocked_fields_recursive(v, blocked_fields, &key_path, violations);
208 }
209}
210
211fn visit_array(
213 arr: &[JsonValue],
214 blocked_fields: &HashSet<String>,
215 path: &str,
216 violations: &mut Vec<String>,
217) {
218 for (i, v) in arr.iter().enumerate() {
219 let elem_path = join_index_path(path, i);
220 find_blocked_fields_recursive(v, blocked_fields, &elem_path, violations);
221 }
222}
223
224fn join_field_path(path: &str, key: &str) -> String {
226 if path.is_empty() {
227 key.to_string()
228 } else {
229 format!("{}.{}", path, key)
230 }
231}
232
233fn join_index_path(path: &str, idx: usize) -> String {
235 if path.is_empty() {
236 format!("[{}]", idx)
237 } else {
238 format!("{}[{}]", path, idx)
239 }
240}
241
242#[derive(Debug, Clone, Serialize)]
244pub struct ExecutionPlan {
245 pub steps: Vec<PlanStep>,
247 pub metadata: PlanMetadata,
249}
250
251#[derive(Debug, Clone, Serialize)]
253pub struct PlanMetadata {
254 pub api_call_count: usize,
256 pub has_mutations: bool,
258 pub endpoints: Vec<String>,
260 pub methods_used: Vec<String>,
262}
263
264#[derive(Debug, Clone, Serialize)]
266pub enum PlanStep {
267 ApiCall {
269 result_var: String,
270 method: String,
271 path: PathTemplate,
272 body: Option<ValueExpr>,
273 },
274
275 Assign { var: String, expr: ValueExpr },
277
278 Conditional {
280 condition: ValueExpr,
281 then_steps: Vec<PlanStep>,
282 else_steps: Vec<PlanStep>,
283 },
284
285 BoundedLoop {
287 item_var: String,
288 collection: ValueExpr,
289 max_iterations: usize,
290 body: Vec<PlanStep>,
291 },
292
293 Return { value: ValueExpr },
295
296 TryCatch {
298 try_steps: Vec<PlanStep>,
299 catch_var: Option<String>,
300 catch_steps: Vec<PlanStep>,
301 finally_steps: Vec<PlanStep>,
302 },
303
304 ParallelApiCalls {
306 result_var: String,
307 calls: Vec<(String, String, PathTemplate, Option<ValueExpr>)>, },
309
310 Continue,
312
313 Break,
315
316 #[cfg(feature = "mcp-code-mode")]
318 McpCall {
319 result_var: String,
320 server_id: String,
321 tool_name: String,
322 args: Option<ValueExpr>,
323 },
324
325 SdkCall {
327 result_var: String,
328 operation: String, args: Option<ValueExpr>, },
331}
332
333#[derive(Debug, Clone, Serialize)]
335pub struct PathTemplate {
336 pub parts: Vec<PathPart>,
338}
339
340impl PathTemplate {
341 pub fn static_path(path: String) -> Self {
343 Self {
344 parts: vec![PathPart::Literal(path)],
345 }
346 }
347
348 pub fn is_dynamic(&self) -> bool {
350 self.parts
351 .iter()
352 .any(|p| matches!(p, PathPart::Variable(_) | PathPart::Expression(_)))
353 }
354}
355
356#[derive(Debug, Clone, Serialize)]
358pub enum PathPart {
359 Literal(String),
361 Variable(String),
363 Expression(ValueExpr),
365}
366
367#[derive(Debug, Clone, Serialize)]
369pub enum ValueExpr {
370 Literal(JsonValue),
372
373 Variable(String),
375
376 PropertyAccess {
378 object: Box<ValueExpr>,
379 property: String,
380 },
381
382 ArrayIndex {
384 array: Box<ValueExpr>,
385 index: Box<ValueExpr>,
386 },
387
388 ObjectLiteral { fields: Vec<ObjectField> },
390
391 ArrayLiteral { items: Vec<ValueExpr> },
393
394 ArrayMethod {
396 array: Box<ValueExpr>,
397 method: ArrayMethodCall,
398 },
399
400 NumberMethod {
402 number: Box<ValueExpr>,
403 method: NumberMethodCall,
404 },
405
406 BinaryOp {
408 left: Box<ValueExpr>,
409 op: BinaryOperator,
410 right: Box<ValueExpr>,
411 },
412
413 UnaryOp {
415 op: UnaryOperator,
416 operand: Box<ValueExpr>,
417 },
418
419 Ternary {
421 condition: Box<ValueExpr>,
422 consequent: Box<ValueExpr>,
423 alternate: Box<ValueExpr>,
424 },
425
426 OptionalChain {
428 object: Box<ValueExpr>,
429 property: String,
430 },
431
432 NullishCoalesce {
434 left: Box<ValueExpr>,
435 right: Box<ValueExpr>,
436 },
437
438 Await { expr: Box<ValueExpr> },
440
441 PromiseAll { items: Vec<ValueExpr> },
443
444 ApiCall {
446 method: String,
447 path: PathTemplate,
448 body: Option<Box<ValueExpr>>,
449 },
450
451 Block {
454 bindings: Vec<(String, ValueExpr)>,
456 result: Box<ValueExpr>,
458 },
459
460 #[cfg(feature = "mcp-code-mode")]
462 McpCall {
463 server_id: String,
464 tool_name: String,
465 args: Option<Box<ValueExpr>>,
466 },
467
468 SdkCall {
470 operation: String,
471 args: Option<Box<ValueExpr>>,
472 },
473
474 BuiltinCall {
476 func: BuiltinFunction,
477 args: Vec<ValueExpr>,
478 },
479}
480
481#[derive(Debug, Clone, Serialize)]
483pub enum ObjectField {
484 KeyValue { key: String, value: ValueExpr },
486 Spread { expr: ValueExpr },
488}
489
490#[derive(Debug, Clone, Serialize)]
492pub enum ArrayMethodCall {
493 Map {
495 item_var: String,
496 body: Box<ValueExpr>,
497 },
498 Filter {
500 item_var: String,
501 predicate: Box<ValueExpr>,
502 },
503 Find {
505 item_var: String,
506 predicate: Box<ValueExpr>,
507 },
508 Slice { start: usize, end: Option<usize> },
510 Length,
512 Some {
514 item_var: String,
515 predicate: Box<ValueExpr>,
516 },
517 Every {
519 item_var: String,
520 predicate: Box<ValueExpr>,
521 },
522 Reduce {
524 acc_var: String,
525 item_var: String,
526 body: Box<ValueExpr>,
527 initial: Box<ValueExpr>,
528 },
529 Push { item: Box<ValueExpr> },
531 Concat { other: Box<ValueExpr> },
533 Includes { item: Box<ValueExpr> },
535 IndexOf { item: Box<ValueExpr> },
537 Join { separator: Option<String> },
539 Reverse,
541 Sort {
543 comparator: Option<(String, String, Box<ValueExpr>)>,
544 },
545 Flat,
547 FlatMap {
549 item_var: String,
550 body: Box<ValueExpr>,
551 },
552 First,
554 Last,
556 ToLowerCase,
558 ToUpperCase,
560 StartsWith { search: Box<ValueExpr> },
562 EndsWith { search: Box<ValueExpr> },
564 Trim,
566 Replace {
568 search: Box<ValueExpr>,
569 replacement: Box<ValueExpr>,
570 },
571 Split { separator: Box<ValueExpr> },
573 Substring {
575 start: Box<ValueExpr>,
576 end: Option<Box<ValueExpr>>,
577 },
578 ToString,
580}
581
582#[derive(Debug, Clone, Serialize)]
584pub enum NumberMethodCall {
585 ToFixed { digits: usize },
587 ToString,
589}
590
591#[derive(Debug, Clone, Serialize)]
597pub enum BuiltinFunction {
598 ParseFloat,
600 ParseInt,
601 NumberCast,
602 MathAbs,
604 MathMax,
605 MathMin,
606 MathRound,
607 MathFloor,
608 MathCeil,
609 ObjectKeys,
611 ObjectValues,
612 ObjectEntries,
613}
614
615#[derive(Debug, Clone, Copy, Serialize)]
617pub enum BinaryOperator {
618 Add,
620 Sub,
621 Mul,
622 Div,
623 Mod,
624 BitwiseOr,
626 Eq,
628 NotEq,
629 StrictEq,
630 StrictNotEq,
631 Lt,
632 Lte,
633 Gt,
634 Gte,
635 And,
637 Or,
638 Concat,
640}
641
642#[derive(Debug, Clone, Copy, Serialize)]
644pub enum UnaryOperator {
645 Not,
646 Neg,
647 Plus,
648 TypeOf,
649}
650
651#[derive(Debug, thiserror::Error)]
653pub enum CompileError {
654 #[error("Unsupported statement type: {0}")]
655 UnsupportedStatement(String),
656
657 #[error("Unsupported expression type: {0}")]
658 UnsupportedExpression(String),
659
660 #[error("Invalid API call: {0}")]
661 InvalidApiCall(String),
662
663 #[error("Unbounded loop detected")]
664 UnboundedLoop,
665
666 #[error("Invalid path template: {0}")]
667 InvalidPath(String),
668
669 #[error("Too many API calls in plan: {count} (max: {max})")]
670 TooManyApiCalls { count: usize, max: usize },
671
672 #[error("Unsupported array method: {0}")]
673 UnsupportedArrayMethod(String),
674
675 #[error("Parse error: {0}")]
676 ParseError(String),
677
678 #[error("Missing variable name")]
679 MissingVariableName,
680}
681
682enum ExtractedCall {
687 Http {
689 method: String,
690 path: PathTemplate,
691 body: Option<ValueExpr>,
692 },
693 Sdk {
695 operation: String,
696 args: Option<ValueExpr>,
697 },
698}
699
700pub struct PlanCompiler {
702 api_call_count: usize,
703 endpoints: Vec<String>,
704 methods_used: Vec<String>,
705 has_mutations: bool,
706 sdk_operations: HashSet<String>,
708 destructure_counter: usize,
710}
711
712impl PlanCompiler {
713 pub fn new() -> Self {
715 Self::with_config(&ExecutionConfig::default())
716 }
717
718 pub fn with_config(_config: &ExecutionConfig) -> Self {
720 Self {
721 api_call_count: 0,
722 endpoints: Vec::new(),
723 methods_used: Vec::new(),
724 has_mutations: false,
725 sdk_operations: HashSet::new(),
726 destructure_counter: 0,
727 }
728 }
729
730 pub fn with_sdk_operations(mut self, operations: HashSet<String>) -> Self {
733 self.sdk_operations = operations;
734 self
735 }
736
737 pub fn compile_code(&mut self, code: &str) -> Result<ExecutionPlan, CompileError> {
741 let cm = SourceMap::default();
743 let fm = cm.new_source_file(FileName::Anon.into(), code.to_string());
744
745 let lexer = Lexer::new(
746 Syntax::Es(Default::default()),
747 EsVersion::Es2022,
748 StringInput::from(&*fm),
749 None,
750 );
751
752 let mut parser = Parser::new_from(lexer);
753
754 let module = parser.parse_module().map_err(|e| {
755 CompileError::ParseError(format!("JavaScript parse error: {:?}", e.into_kind()))
756 })?;
757
758 self.compile(&module)
760 }
761
762 pub fn compile(&mut self, module: &Module) -> Result<ExecutionPlan, CompileError> {
764 let mut steps = Vec::new();
765
766 for item in &module.body {
767 match item {
768 ModuleItem::Stmt(stmt) => {
769 self.compile_statement(stmt, &mut steps)?;
770 },
771 _ => {
772 return Err(CompileError::UnsupportedStatement(
773 "import/export not allowed".into(),
774 ));
775 },
776 }
777 }
778
779 self.methods_used.sort();
781 self.methods_used.dedup();
782
783 Ok(ExecutionPlan {
784 steps,
785 metadata: PlanMetadata {
786 api_call_count: self.api_call_count,
787 has_mutations: self.has_mutations,
788 endpoints: self.endpoints.clone(),
789 methods_used: self.methods_used.clone(),
790 },
791 })
792 }
793
794 fn compile_statement(
795 &mut self,
796 stmt: &Stmt,
797 steps: &mut Vec<PlanStep>,
798 ) -> Result<(), CompileError> {
799 match stmt {
800 Stmt::Decl(Decl::Var(var_decl)) => {
802 for decl in &var_decl.decls {
803 if let Some(init) = &decl.init {
804 match &decl.name {
805 Pat::Ident(ident) => {
806 let var_name = ident.id.sym.to_string();
807 self.compile_var_init(&var_name, init, steps)?;
808 },
809 Pat::Object(obj_pat) => {
810 self.compile_object_destructuring(obj_pat, init, steps)?;
811 },
812 Pat::Array(arr_pat) => {
813 self.compile_array_destructuring(arr_pat, init, steps)?;
814 },
815 _ => {
816 return Err(CompileError::UnsupportedExpression(
817 "complex destructuring pattern".into(),
818 ));
819 },
820 }
821 }
822 }
823 },
824
825 Stmt::Expr(expr_stmt) => {
827 if let Expr::Assign(assign) = expr_stmt.expr.as_ref() {
829 if assign.op == swc_ecma_ast::AssignOp::Assign {
830 if let Some(ident) = assign.left.as_ident() {
831 let var_name = ident.sym.to_string();
832 self.compile_var_init(&var_name, &assign.right, steps)?;
833 return Ok(());
834 }
835 }
836 }
837
838 let expr = self.compile_expr(&expr_stmt.expr)?;
839 match expr {
840 ValueExpr::ApiCall { method, path, body } => {
842 self.record_api_call(&method, &path);
843 steps.push(PlanStep::ApiCall {
844 result_var: "_".into(), method,
846 path,
847 body: body.map(|b| *b),
848 });
849 },
850 ValueExpr::SdkCall { operation, args } => {
852 steps.push(PlanStep::SdkCall {
853 result_var: "_".into(), operation,
855 args: args.map(|a| *a),
856 });
857 },
858 ValueExpr::ArrayMethod {
861 ref array,
862 ref method,
863 } if matches!(
864 method,
865 ArrayMethodCall::Push { .. } | ArrayMethodCall::Concat { .. }
866 ) =>
867 {
868 if let ValueExpr::Variable(var_name) = array.as_ref() {
869 steps.push(PlanStep::Assign {
870 var: var_name.clone(),
871 expr,
872 });
873 }
874 },
877 _ => {},
879 }
880 },
881
882 Stmt::If(if_stmt) => {
884 let condition = self.compile_expr(&if_stmt.test)?;
885 let mut then_steps = Vec::new();
886 self.compile_statement(&if_stmt.cons, &mut then_steps)?;
887
888 let mut else_steps = Vec::new();
889 if let Some(alt) = &if_stmt.alt {
890 self.compile_statement(alt, &mut else_steps)?;
891 }
892
893 steps.push(PlanStep::Conditional {
894 condition,
895 then_steps,
896 else_steps,
897 });
898 },
899
900 Stmt::ForOf(for_of) => {
902 let (item_var, destructure_bindings) = match &for_of.left {
903 ForHead::VarDecl(decl) => {
904 if let Some(first) = decl.decls.first() {
905 self.extract_loop_var(&first.name)?
906 } else {
907 return Err(CompileError::MissingVariableName);
908 }
909 },
910 ForHead::Pat(pat) => self.extract_loop_var(pat)?,
911 _ => return Err(CompileError::MissingVariableName),
912 };
913
914 let collection = self.compile_expr(&for_of.right)?;
915
916 let max_iterations = self.extract_bound(&collection).unwrap_or(100);
918
919 let mut body = destructure_bindings;
920 self.compile_statement(&for_of.body, &mut body)?;
921
922 steps.push(PlanStep::BoundedLoop {
923 item_var,
924 collection,
925 max_iterations,
926 body,
927 });
928 },
929
930 Stmt::Block(block) => {
932 for stmt in &block.stmts {
933 self.compile_statement(stmt, steps)?;
934 }
935 },
936
937 Stmt::Return(ret) => {
939 let value = if let Some(arg) = &ret.arg {
940 self.compile_expr(arg)?
941 } else {
942 ValueExpr::Literal(JsonValue::Null)
943 };
944 steps.push(PlanStep::Return { value });
945 },
946
947 Stmt::Empty(_) => {},
949
950 Stmt::Try(try_stmt) => {
952 let mut try_steps = Vec::new();
953 for stmt in &try_stmt.block.stmts {
954 self.compile_statement(stmt, &mut try_steps)?;
955 }
956
957 let (catch_var, catch_steps) = if let Some(handler) = &try_stmt.handler {
958 let var_name = handler.param.as_ref().map(|p| match p {
959 swc_ecma_ast::Pat::Ident(ident) => ident.sym.to_string(),
960 _ => "error".to_string(),
961 });
962 let mut catch_stmts = Vec::new();
963 for stmt in &handler.body.stmts {
964 self.compile_statement(stmt, &mut catch_stmts)?;
965 }
966 (var_name, catch_stmts)
967 } else {
968 (None, Vec::new())
969 };
970
971 let finally_steps = if let Some(finalizer) = &try_stmt.finalizer {
972 let mut finally_stmts = Vec::new();
973 for stmt in &finalizer.stmts {
974 self.compile_statement(stmt, &mut finally_stmts)?;
975 }
976 finally_stmts
977 } else {
978 Vec::new()
979 };
980
981 steps.push(PlanStep::TryCatch {
982 try_steps,
983 catch_var,
984 catch_steps,
985 finally_steps,
986 });
987 },
988
989 Stmt::Continue(_) => {
991 steps.push(PlanStep::Continue);
992 },
993
994 Stmt::Break(_) => {
996 steps.push(PlanStep::Break);
997 },
998
999 Stmt::Decl(decl) => {
1000 let msg = match decl {
1001 Decl::Fn(_) => "Function declarations are not supported. Use arrow functions inside array methods (.map, .filter) instead",
1002 Decl::Class(_) => "Class declarations are not supported",
1003 _ => "This declaration type is not supported",
1004 };
1005 return Err(CompileError::UnsupportedStatement(msg.into()));
1006 },
1007 Stmt::Switch(_) => {
1008 return Err(CompileError::UnsupportedStatement(
1009 "'switch' statements are not supported. Use if/else if/else instead".into(),
1010 ));
1011 },
1012 Stmt::Throw(_) => {
1013 return Err(CompileError::UnsupportedStatement(
1014 "'throw' statements are not supported. Use try/catch for error handling".into(),
1015 ));
1016 },
1017 Stmt::While(_) => {
1018 return Err(CompileError::UnsupportedStatement(
1019 "'while' loops are not supported. Use for-of with .slice() instead: for (const item of array.slice(0, N)) { }".into(),
1020 ));
1021 },
1022 Stmt::DoWhile(_) => {
1023 return Err(CompileError::UnsupportedStatement(
1024 "'do-while' loops are not supported. Use for-of with .slice() instead: for (const item of array.slice(0, N)) { }".into(),
1025 ));
1026 },
1027 Stmt::For(_) => {
1028 return Err(CompileError::UnsupportedStatement(
1029 "'for(;;)' loops are not supported. Use for-of with .slice() instead: for (const item of array.slice(0, N)) { }".into(),
1030 ));
1031 },
1032 Stmt::ForIn(_) => {
1033 return Err(CompileError::UnsupportedStatement(
1034 "'for-in' loops are not supported. Use for-of with .slice() instead".into(),
1035 ));
1036 },
1037 Stmt::Labeled(_) => {
1038 return Err(CompileError::UnsupportedStatement(
1039 "Labeled statements are not supported".into(),
1040 ));
1041 },
1042 Stmt::With(_) => {
1043 return Err(CompileError::UnsupportedStatement(
1044 "'with' statements are not supported".into(),
1045 ));
1046 },
1047 Stmt::Debugger(_) => {
1048 return Err(CompileError::UnsupportedStatement(
1049 "'debugger' statements are not supported".into(),
1050 ));
1051 },
1052 }
1053
1054 Ok(())
1055 }
1056
1057 fn compile_var_init(
1058 &mut self,
1059 var_name: &str,
1060 init: &Expr,
1061 steps: &mut Vec<PlanStep>,
1062 ) -> Result<(), CompileError> {
1063 if let Expr::Await(await_expr) = init {
1065 if let Some(extracted) = self.try_extract_api_call(&await_expr.arg)? {
1067 match extracted {
1068 ExtractedCall::Http { method, path, body } => {
1069 self.record_api_call(&method, &path);
1070 steps.push(PlanStep::ApiCall {
1071 result_var: var_name.into(),
1072 method,
1073 path,
1074 body,
1075 });
1076 },
1077 ExtractedCall::Sdk { operation, args } => {
1078 steps.push(PlanStep::SdkCall {
1079 result_var: var_name.into(),
1080 operation,
1081 args,
1082 });
1083 },
1084 }
1085 return Ok(());
1086 }
1087
1088 #[cfg(feature = "mcp-code-mode")]
1090 if let Some((server_id, tool_name, args)) =
1091 self.try_extract_mcp_call(&await_expr.arg)?
1092 {
1093 steps.push(PlanStep::McpCall {
1094 result_var: var_name.into(),
1095 server_id,
1096 tool_name,
1097 args,
1098 });
1099 return Ok(());
1100 }
1101
1102 if let Expr::Call(call) = await_expr.arg.as_ref() {
1104 let inner = self.compile_call(call)?;
1105 if let ValueExpr::PromiseAll { items } = inner {
1106 return self.compile_promise_all(var_name, items, steps);
1107 }
1108 }
1109 }
1110
1111 let expr = self.compile_expr(init)?;
1113 steps.push(PlanStep::Assign {
1114 var: var_name.into(),
1115 expr,
1116 });
1117 Ok(())
1118 }
1119
1120 fn compile_promise_all(
1123 &mut self,
1124 result_var: &str,
1125 items: Vec<ValueExpr>,
1126 steps: &mut Vec<PlanStep>,
1127 ) -> Result<(), CompileError> {
1128 let mut calls = Vec::new();
1129 let mut all_api_calls = true;
1130
1131 for (i, item) in items.iter().enumerate() {
1132 match item {
1133 ValueExpr::ApiCall { method, path, body } => {
1134 let temp_var = format!("__promise_all_{}_{}", result_var, i);
1135 calls.push((
1136 temp_var,
1137 method.clone(),
1138 path.clone(),
1139 body.as_ref().map(|b| *b.clone()),
1140 ));
1141 },
1142 _ => {
1143 all_api_calls = false;
1144 break;
1145 },
1146 }
1147 }
1148
1149 if all_api_calls && !calls.is_empty() {
1150 for (_, method, path, _) in &calls {
1152 self.record_api_call(method, path);
1153 }
1154 steps.push(PlanStep::ParallelApiCalls {
1155 result_var: result_var.into(),
1156 calls,
1157 });
1158 Ok(())
1159 } else {
1160 Err(CompileError::UnsupportedExpression(
1163 "Promise.all with non-API-call expressions".into(),
1164 ))
1165 }
1166 }
1167
1168 fn compile_expr(&mut self, expr: &Expr) -> Result<ValueExpr, CompileError> {
1169 match expr {
1170 Expr::Lit(lit) => Ok(ValueExpr::Literal(self.lit_to_json(lit))),
1172
1173 Expr::Ident(ident) => Ok(ValueExpr::Variable(ident.sym.to_string())),
1175
1176 Expr::Member(member) => {
1178 let object = Box::new(self.compile_expr(&member.obj)?);
1179
1180 if let MemberProp::Ident(prop) = &member.prop {
1182 let prop_name = prop.sym.to_string();
1183 if prop_name == "length" {
1184 return Ok(ValueExpr::ArrayMethod {
1185 array: object,
1186 method: ArrayMethodCall::Length,
1187 });
1188 }
1189 }
1190
1191 match &member.prop {
1192 MemberProp::Ident(ident) => Ok(ValueExpr::PropertyAccess {
1193 object,
1194 property: ident.sym.to_string(),
1195 }),
1196 MemberProp::Computed(computed) => {
1197 let index = Box::new(self.compile_expr(&computed.expr)?);
1198 Ok(ValueExpr::ArrayIndex {
1199 array: object,
1200 index,
1201 })
1202 }
1203 _ => Err(CompileError::UnsupportedExpression("private property".into())),
1204 }
1205 }
1206
1207 Expr::Call(call) => self.compile_call(call),
1209
1210 Expr::Object(obj) => {
1212 let mut fields = Vec::new();
1213 for prop in &obj.props {
1214 match prop {
1215 PropOrSpread::Prop(prop) => {
1216 if let Prop::KeyValue(kv) = prop.as_ref() {
1217 let key = self.prop_name_to_string(&kv.key)?;
1218 let value = self.compile_expr(&kv.value)?;
1219 fields.push(ObjectField::KeyValue { key, value });
1220 } else if let Prop::Shorthand(ident) = prop.as_ref() {
1221 let name = ident.sym.to_string();
1222 fields.push(ObjectField::KeyValue {
1223 key: name.clone(),
1224 value: ValueExpr::Variable(name),
1225 });
1226 }
1227 }
1228 PropOrSpread::Spread(spread) => {
1229 let expr = self.compile_expr(&spread.expr)?;
1230 fields.push(ObjectField::Spread { expr });
1231 }
1232 }
1233 }
1234 Ok(ValueExpr::ObjectLiteral { fields })
1235 }
1236
1237 Expr::Array(arr) => {
1239 let mut items = Vec::new();
1240 for elem in arr.elems.iter().flatten() {
1241 if elem.spread.is_some() {
1242 return Err(CompileError::UnsupportedExpression("spread".into()));
1243 }
1244 items.push(self.compile_expr(&elem.expr)?);
1245 }
1246 Ok(ValueExpr::ArrayLiteral { items })
1247 }
1248
1249 Expr::Tpl(tpl) => {
1251 let mut parts = Vec::new();
1254 for (i, quasi) in tpl.quasis.iter().enumerate() {
1255 let raw = quasi.raw.to_string();
1256 if !raw.is_empty() {
1257 parts.push(ValueExpr::Literal(JsonValue::String(raw)));
1258 }
1259 if i < tpl.exprs.len() {
1260 parts.push(self.compile_expr(&tpl.exprs[i])?);
1261 }
1262 }
1263
1264 if parts.len() == 1 {
1266 return Ok(parts.remove(0));
1267 }
1268
1269 let mut result = parts.remove(0);
1271 for part in parts {
1272 result = ValueExpr::BinaryOp {
1273 left: Box::new(result),
1274 op: BinaryOperator::Concat,
1275 right: Box::new(part),
1276 };
1277 }
1278 Ok(result)
1279 }
1280
1281 Expr::Bin(bin) => {
1283 let left = Box::new(self.compile_expr(&bin.left)?);
1284 let right = Box::new(self.compile_expr(&bin.right)?);
1285 let op = self.compile_bin_op(bin.op)?;
1286 Ok(ValueExpr::BinaryOp { left, op, right })
1287 }
1288
1289 Expr::Unary(unary) => {
1291 let operand = Box::new(self.compile_expr(&unary.arg)?);
1292 let op = match unary.op {
1293 UnaryOp::Bang => UnaryOperator::Not,
1294 UnaryOp::Minus => UnaryOperator::Neg,
1295 UnaryOp::TypeOf => UnaryOperator::TypeOf,
1296 UnaryOp::Plus => UnaryOperator::Plus,
1297 _ => return Err(CompileError::UnsupportedExpression("unary op".into())),
1298 };
1299 Ok(ValueExpr::UnaryOp { op, operand })
1300 }
1301
1302 Expr::Cond(cond) => {
1304 let condition = Box::new(self.compile_expr(&cond.test)?);
1305 let consequent = Box::new(self.compile_expr(&cond.cons)?);
1306 let alternate = Box::new(self.compile_expr(&cond.alt)?);
1307 Ok(ValueExpr::Ternary {
1308 condition,
1309 consequent,
1310 alternate,
1311 })
1312 }
1313
1314 Expr::Await(await_expr) => {
1316 if let Some(extracted) = self.try_extract_api_call(&await_expr.arg)? {
1318 return match extracted {
1319 ExtractedCall::Http { method, path, body } => {
1320 self.record_api_call(&method, &path);
1321 Ok(ValueExpr::ApiCall {
1322 method,
1323 path,
1324 body: body.map(Box::new),
1325 })
1326 }
1327 ExtractedCall::Sdk { operation, args } => {
1328 Ok(ValueExpr::SdkCall {
1329 operation,
1330 args: args.map(Box::new),
1331 })
1332 }
1333 };
1334 }
1335 #[cfg(feature = "mcp-code-mode")]
1337 if let Some((server_id, tool_name, args)) = self.try_extract_mcp_call(&await_expr.arg)? {
1338 return Ok(ValueExpr::McpCall {
1339 server_id,
1340 tool_name,
1341 args: args.map(Box::new),
1342 });
1343 }
1344 let inner = self.compile_expr(&await_expr.arg)?;
1346 Ok(ValueExpr::Await {
1347 expr: Box::new(inner),
1348 })
1349 }
1350
1351 Expr::Arrow(_) => {
1353 Err(CompileError::UnsupportedExpression(
1355 "arrow function outside array method".into(),
1356 ))
1357 }
1358
1359 Expr::Paren(paren) => self.compile_expr(&paren.expr),
1361
1362 Expr::OptChain(opt) => {
1364 match opt.base.as_ref() {
1365 OptChainBase::Member(member) => {
1366 let object = Box::new(self.compile_expr(&member.obj)?);
1367 if let MemberProp::Ident(ident) = &member.prop {
1368 Ok(ValueExpr::OptionalChain {
1369 object,
1370 property: ident.sym.to_string(),
1371 })
1372 } else {
1373 Err(CompileError::UnsupportedExpression("computed optional chain".into()))
1374 }
1375 }
1376 _ => Err(CompileError::UnsupportedExpression("optional call".into())),
1377 }
1378 }
1379
1380 Expr::This(_) => Err(CompileError::UnsupportedExpression(
1381 "'this' keyword is not supported".into(),
1382 )),
1383 Expr::Fn(_) => Err(CompileError::UnsupportedExpression(
1384 "Function expressions are not supported. Use arrow functions inside array methods (.map, .filter) instead".into(),
1385 )),
1386 Expr::Update(_) => Err(CompileError::UnsupportedExpression(
1387 "Increment/decrement operators (++, --) are not supported. Use 'x = x + 1' instead".into(),
1388 )),
1389 Expr::New(_) => Err(CompileError::UnsupportedExpression(
1390 "'new' keyword is not supported".into(),
1391 )),
1392 Expr::Seq(_) => Err(CompileError::UnsupportedExpression(
1393 "Sequence expressions (comma operator) are not supported. Use separate statements instead".into(),
1394 )),
1395 Expr::TaggedTpl(_) => Err(CompileError::UnsupportedExpression(
1396 "Tagged template literals are not supported. Use regular template literals instead".into(),
1397 )),
1398 Expr::Class(_) => Err(CompileError::UnsupportedExpression(
1399 "Class expressions are not supported".into(),
1400 )),
1401 Expr::Yield(_) => Err(CompileError::UnsupportedExpression(
1402 "Generator yield is not supported".into(),
1403 )),
1404 Expr::SuperProp(_) => Err(CompileError::UnsupportedExpression(
1405 "'super' is not supported".into(),
1406 )),
1407 Expr::Assign(_) => Err(CompileError::UnsupportedExpression(
1408 "Assignment expressions are not supported here. Use a separate variable declaration instead".into(),
1409 )),
1410 _ => Err(CompileError::UnsupportedExpression(
1411 "This expression type is not supported in the JavaScript subset".into(),
1412 )),
1413 }
1414 }
1415
1416 fn compile_call(&mut self, call: &CallExpr) -> Result<ValueExpr, CompileError> {
1417 if let Some(extracted) = self.try_extract_api_call(&Expr::Call(call.clone()))? {
1419 return match extracted {
1420 ExtractedCall::Http { method, path, body } => {
1421 self.record_api_call(&method, &path);
1422 Ok(ValueExpr::ApiCall {
1423 method,
1424 path,
1425 body: body.map(Box::new),
1426 })
1427 },
1428 ExtractedCall::Sdk { operation, args } => Ok(ValueExpr::SdkCall {
1429 operation,
1430 args: args.map(Box::new),
1431 }),
1432 };
1433 }
1434
1435 #[cfg(feature = "mcp-code-mode")]
1437 if let Some((server_id, tool_name, args)) =
1438 self.try_extract_mcp_call(&Expr::Call(call.clone()))?
1439 {
1440 return Ok(ValueExpr::McpCall {
1441 server_id,
1442 tool_name,
1443 args: args.map(Box::new),
1444 });
1445 }
1446
1447 if let Callee::Expr(callee) = &call.callee {
1449 if let Expr::Member(member) = callee.as_ref() {
1450 if let Expr::Ident(obj) = member.obj.as_ref() {
1451 if obj.sym.as_ref() == "Promise" {
1452 if let MemberProp::Ident(prop) = &member.prop {
1453 if prop.sym.as_ref() == "all" {
1454 if let Some(arg) = call.args.first() {
1455 if let Expr::Array(arr) = arg.expr.as_ref() {
1456 let mut items = Vec::new();
1457 for elem in arr.elems.iter().flatten() {
1458 items.push(self.compile_expr(&elem.expr)?);
1459 }
1460 return Ok(ValueExpr::PromiseAll { items });
1461 }
1462 }
1463 }
1464 }
1465 }
1466 }
1467 }
1468 }
1469
1470 if let Callee::Expr(callee) = &call.callee {
1472 if let Expr::Member(member) = callee.as_ref() {
1473 let array = Box::new(self.compile_expr(&member.obj)?);
1474
1475 if let MemberProp::Ident(method_ident) = &member.prop {
1476 let method_name = method_ident.sym.as_ref();
1477
1478 match method_name {
1479 "map" => {
1480 let (item_var, body) = self.extract_arrow_callback(call)?;
1481 return Ok(ValueExpr::ArrayMethod {
1482 array,
1483 method: ArrayMethodCall::Map {
1484 item_var,
1485 body: Box::new(body),
1486 },
1487 });
1488 },
1489 "filter" => {
1490 let (item_var, predicate) = self.extract_arrow_callback(call)?;
1491 return Ok(ValueExpr::ArrayMethod {
1492 array,
1493 method: ArrayMethodCall::Filter {
1494 item_var,
1495 predicate: Box::new(predicate),
1496 },
1497 });
1498 },
1499 "find" => {
1500 let (item_var, predicate) = self.extract_arrow_callback(call)?;
1501 return Ok(ValueExpr::ArrayMethod {
1502 array,
1503 method: ArrayMethodCall::Find {
1504 item_var,
1505 predicate: Box::new(predicate),
1506 },
1507 });
1508 },
1509 "some" => {
1510 let (item_var, predicate) = self.extract_arrow_callback(call)?;
1511 return Ok(ValueExpr::ArrayMethod {
1512 array,
1513 method: ArrayMethodCall::Some {
1514 item_var,
1515 predicate: Box::new(predicate),
1516 },
1517 });
1518 },
1519 "every" => {
1520 let (item_var, predicate) = self.extract_arrow_callback(call)?;
1521 return Ok(ValueExpr::ArrayMethod {
1522 array,
1523 method: ArrayMethodCall::Every {
1524 item_var,
1525 predicate: Box::new(predicate),
1526 },
1527 });
1528 },
1529 "flatMap" => {
1530 let (item_var, body) = self.extract_arrow_callback(call)?;
1531 return Ok(ValueExpr::ArrayMethod {
1532 array,
1533 method: ArrayMethodCall::FlatMap {
1534 item_var,
1535 body: Box::new(body),
1536 },
1537 });
1538 },
1539 "slice" => {
1540 let start = self.extract_number_arg(call, 0)?.unwrap_or(0) as usize;
1541 let end = self.extract_number_arg(call, 1)?.map(|n| n as usize);
1542 return Ok(ValueExpr::ArrayMethod {
1543 array,
1544 method: ArrayMethodCall::Slice { start, end },
1545 });
1546 },
1547 "push" => {
1548 if let Some(arg) = call.args.first() {
1549 let item = Box::new(self.compile_expr(&arg.expr)?);
1550 return Ok(ValueExpr::ArrayMethod {
1551 array,
1552 method: ArrayMethodCall::Push { item },
1553 });
1554 }
1555 },
1556 "concat" => {
1557 if let Some(arg) = call.args.first() {
1558 let other = Box::new(self.compile_expr(&arg.expr)?);
1559 return Ok(ValueExpr::ArrayMethod {
1560 array,
1561 method: ArrayMethodCall::Concat { other },
1562 });
1563 }
1564 },
1565 "includes" => {
1566 if let Some(arg) = call.args.first() {
1567 let item = Box::new(self.compile_expr(&arg.expr)?);
1568 return Ok(ValueExpr::ArrayMethod {
1569 array,
1570 method: ArrayMethodCall::Includes { item },
1571 });
1572 }
1573 },
1574 "indexOf" => {
1575 if let Some(arg) = call.args.first() {
1576 let item = Box::new(self.compile_expr(&arg.expr)?);
1577 return Ok(ValueExpr::ArrayMethod {
1578 array,
1579 method: ArrayMethodCall::IndexOf { item },
1580 });
1581 }
1582 },
1583 "join" => {
1584 let separator = if let Some(arg) = call.args.first() {
1585 if let Expr::Lit(Lit::Str(s)) = arg.expr.as_ref() {
1586 Some(s.value.to_string_lossy().into_owned())
1587 } else {
1588 None
1589 }
1590 } else {
1591 None
1592 };
1593 return Ok(ValueExpr::ArrayMethod {
1594 array,
1595 method: ArrayMethodCall::Join { separator },
1596 });
1597 },
1598 "reverse" => {
1599 return Ok(ValueExpr::ArrayMethod {
1600 array,
1601 method: ArrayMethodCall::Reverse,
1602 });
1603 },
1604 "sort" => {
1605 let comparator = if !call.args.is_empty() {
1606 let (a_var, b_var, body) = self.extract_reduce_callback(call)?;
1607 Some((a_var, b_var, Box::new(body)))
1608 } else {
1609 None
1610 };
1611 return Ok(ValueExpr::ArrayMethod {
1612 array,
1613 method: ArrayMethodCall::Sort { comparator },
1614 });
1615 },
1616 "flat" => {
1617 return Ok(ValueExpr::ArrayMethod {
1618 array,
1619 method: ArrayMethodCall::Flat,
1620 });
1621 },
1622 "at" => {
1623 if let Some(n) = self.extract_number_arg(call, 0)? {
1624 if n == 0 {
1625 return Ok(ValueExpr::ArrayMethod {
1626 array,
1627 method: ArrayMethodCall::First,
1628 });
1629 } else if n == -1 {
1630 return Ok(ValueExpr::ArrayMethod {
1631 array,
1632 method: ArrayMethodCall::Last,
1633 });
1634 }
1635 }
1636 },
1637 "reduce" if call.args.len() >= 2 => {
1639 let (acc_var, item_var, body) = self.extract_reduce_callback(call)?;
1640 let initial = Box::new(self.compile_expr(&call.args[1].expr)?);
1641 return Ok(ValueExpr::ArrayMethod {
1642 array,
1643 method: ArrayMethodCall::Reduce {
1644 acc_var,
1645 item_var,
1646 body: Box::new(body),
1647 initial,
1648 },
1649 });
1650 },
1651 "toFixed" => {
1652 let digits = self.extract_number_arg(call, 0)?.unwrap_or(0) as usize;
1654 return Ok(ValueExpr::NumberMethod {
1655 number: array, method: NumberMethodCall::ToFixed { digits },
1657 });
1658 },
1659 "toLowerCase" => {
1660 return Ok(ValueExpr::ArrayMethod {
1661 array,
1662 method: ArrayMethodCall::ToLowerCase,
1663 });
1664 },
1665 "toUpperCase" => {
1666 return Ok(ValueExpr::ArrayMethod {
1667 array,
1668 method: ArrayMethodCall::ToUpperCase,
1669 });
1670 },
1671 "startsWith" => {
1672 let arg = call.args.first().ok_or_else(|| {
1673 CompileError::UnsupportedExpression(
1674 "startsWith() requires a search argument".into(),
1675 )
1676 })?;
1677 let search = Box::new(self.compile_expr(&arg.expr)?);
1678 return Ok(ValueExpr::ArrayMethod {
1679 array,
1680 method: ArrayMethodCall::StartsWith { search },
1681 });
1682 },
1683 "endsWith" => {
1684 let arg = call.args.first().ok_or_else(|| {
1685 CompileError::UnsupportedExpression(
1686 "endsWith() requires a search argument".into(),
1687 )
1688 })?;
1689 let search = Box::new(self.compile_expr(&arg.expr)?);
1690 return Ok(ValueExpr::ArrayMethod {
1691 array,
1692 method: ArrayMethodCall::EndsWith { search },
1693 });
1694 },
1695 "trim" => {
1696 return Ok(ValueExpr::ArrayMethod {
1697 array,
1698 method: ArrayMethodCall::Trim,
1699 });
1700 },
1701 "replace" => {
1702 if call.args.len() < 2 {
1703 return Err(CompileError::UnsupportedExpression(
1704 "replace() requires search and replacement arguments".into(),
1705 ));
1706 }
1707 let search = Box::new(self.compile_expr(&call.args[0].expr)?);
1708 let replacement = Box::new(self.compile_expr(&call.args[1].expr)?);
1709 return Ok(ValueExpr::ArrayMethod {
1710 array,
1711 method: ArrayMethodCall::Replace {
1712 search,
1713 replacement,
1714 },
1715 });
1716 },
1717 "split" => {
1718 let arg = call.args.first().ok_or_else(|| {
1719 CompileError::UnsupportedExpression(
1720 "split() requires a separator argument".into(),
1721 )
1722 })?;
1723 let separator = Box::new(self.compile_expr(&arg.expr)?);
1724 return Ok(ValueExpr::ArrayMethod {
1725 array,
1726 method: ArrayMethodCall::Split { separator },
1727 });
1728 },
1729 "substring" => {
1730 let arg = call.args.first().ok_or_else(|| {
1731 CompileError::UnsupportedExpression(
1732 "substring() requires a start argument".into(),
1733 )
1734 })?;
1735 let start = Box::new(self.compile_expr(&arg.expr)?);
1736 let end = if call.args.len() >= 2 {
1737 Some(Box::new(self.compile_expr(&call.args[1].expr)?))
1738 } else {
1739 None
1740 };
1741 return Ok(ValueExpr::ArrayMethod {
1742 array,
1743 method: ArrayMethodCall::Substring { start, end },
1744 });
1745 },
1746 "toString" => {
1747 return Ok(ValueExpr::ArrayMethod {
1748 array,
1749 method: ArrayMethodCall::ToString,
1750 });
1751 },
1752 _ => {},
1753 }
1754 }
1755 }
1756 }
1757
1758 if let Callee::Expr(callee) = &call.callee {
1760 if let Expr::Ident(ident) = callee.as_ref() {
1761 let func = match ident.sym.as_ref() {
1762 "parseFloat" => Some(BuiltinFunction::ParseFloat),
1763 "parseInt" => Some(BuiltinFunction::ParseInt),
1764 "Number" => Some(BuiltinFunction::NumberCast),
1765 _ => None,
1766 };
1767 if let Some(func) = func {
1768 let args = call
1769 .args
1770 .iter()
1771 .map(|a| self.compile_expr(&a.expr))
1772 .collect::<Result<Vec<_>, _>>()?;
1773 return Ok(ValueExpr::BuiltinCall { func, args });
1774 }
1775 }
1776
1777 if let Expr::Member(member) = callee.as_ref() {
1779 if let Expr::Ident(obj) = member.obj.as_ref() {
1780 if let MemberProp::Ident(prop) = &member.prop {
1781 let func = match (obj.sym.as_ref(), prop.sym.as_ref()) {
1782 ("Math", "abs") => Some(BuiltinFunction::MathAbs),
1783 ("Math", "max") => Some(BuiltinFunction::MathMax),
1784 ("Math", "min") => Some(BuiltinFunction::MathMin),
1785 ("Math", "round") => Some(BuiltinFunction::MathRound),
1786 ("Math", "floor") => Some(BuiltinFunction::MathFloor),
1787 ("Math", "ceil") => Some(BuiltinFunction::MathCeil),
1788 ("Object", "keys") => Some(BuiltinFunction::ObjectKeys),
1789 ("Object", "values") => Some(BuiltinFunction::ObjectValues),
1790 ("Object", "entries") => Some(BuiltinFunction::ObjectEntries),
1791 _ => None,
1792 };
1793 if let Some(func) = func {
1794 let args = call
1795 .args
1796 .iter()
1797 .map(|a| self.compile_expr(&a.expr))
1798 .collect::<Result<Vec<_>, _>>()?;
1799 return Ok(ValueExpr::BuiltinCall { func, args });
1800 }
1801 }
1802 }
1803 }
1804 }
1805
1806 Err(CompileError::UnsupportedExpression("function call".into()))
1807 }
1808
1809 fn try_extract_api_call(&mut self, expr: &Expr) -> Result<Option<ExtractedCall>, CompileError> {
1810 let call = match expr {
1811 Expr::Call(c) => c,
1812 _ => return Ok(None),
1813 };
1814
1815 if let Callee::Expr(callee) = &call.callee {
1816 if let Expr::Member(member) = callee.as_ref() {
1817 if let Expr::Ident(obj) = member.obj.as_ref() {
1818 if obj.sym.as_ref() == "api" {
1819 if let MemberProp::Ident(method_ident) = &member.prop {
1820 let method_name = method_ident.sym.as_ref();
1821
1822 if !self.sdk_operations.is_empty() {
1823 if !self.sdk_operations.contains(method_name) {
1825 return Err(CompileError::InvalidApiCall(format!(
1826 "Unknown SDK operation: api.{}(). Check the code mode schema resource for available operations.",
1827 method_name
1828 )));
1829 }
1830 let args = if let Some(arg) = call.args.first() {
1831 Some(self.compile_expr(&arg.expr)?)
1832 } else {
1833 None
1834 };
1835 self.api_call_count += 1;
1836 let op_endpoint = format!("sdk:{}", method_name);
1837 if !self.endpoints.contains(&op_endpoint) {
1838 self.endpoints.push(op_endpoint);
1839 }
1840 if !self.methods_used.contains(&method_name.to_string()) {
1841 self.methods_used.push(method_name.to_string());
1842 }
1843 return Ok(Some(ExtractedCall::Sdk {
1844 operation: method_name.to_string(),
1845 args,
1846 }));
1847 }
1848
1849 if HttpMethod::from_str(method_name).is_none() {
1851 return Err(CompileError::InvalidApiCall(format!(
1852 "Unknown method: api.{}",
1853 method_name
1854 )));
1855 }
1856
1857 let path = if let Some(arg) = call.args.first() {
1859 self.extract_path_template(&arg.expr)?
1860 } else {
1861 return Err(CompileError::InvalidApiCall(
1862 "API call requires path".into(),
1863 ));
1864 };
1865
1866 let body = if let Some(arg) = call.args.get(1) {
1868 Some(self.compile_expr(&arg.expr)?)
1869 } else {
1870 None
1871 };
1872
1873 return Ok(Some(ExtractedCall::Http {
1874 method: method_name.to_uppercase(),
1875 path,
1876 body,
1877 }));
1878 }
1879 }
1880 }
1881 }
1882 }
1883
1884 Ok(None)
1885 }
1886
1887 #[cfg(feature = "mcp-code-mode")]
1891 fn try_extract_mcp_call(
1892 &mut self,
1893 expr: &Expr,
1894 ) -> Result<Option<(String, String, Option<ValueExpr>)>, CompileError> {
1895 let call = match expr {
1896 Expr::Call(c) => c,
1897 _ => return Ok(None),
1898 };
1899
1900 if let Callee::Expr(callee) = &call.callee {
1901 if let Expr::Member(member) = callee.as_ref() {
1902 if let Expr::Ident(obj) = member.obj.as_ref() {
1903 if obj.sym.as_ref() == "mcp" {
1904 if let MemberProp::Ident(method_ident) = &member.prop {
1905 if method_ident.sym.as_ref() == "call" {
1906 let server_id = call.args.first()
1908 .and_then(|a| {
1909 if let Expr::Lit(Lit::Str(s)) = a.expr.as_ref() {
1910 Some(s.value.to_string_lossy().into_owned())
1911 } else {
1912 None
1913 }
1914 })
1915 .ok_or_else(|| CompileError::UnsupportedExpression(
1916 "mcp.call() first argument must be a string literal (server_id)".into(),
1917 ))?;
1918
1919 let tool_name = call.args.get(1)
1921 .and_then(|a| {
1922 if let Expr::Lit(Lit::Str(s)) = a.expr.as_ref() {
1923 Some(s.value.to_string_lossy().into_owned())
1924 } else {
1925 None
1926 }
1927 })
1928 .ok_or_else(|| CompileError::UnsupportedExpression(
1929 "mcp.call() second argument must be a string literal (tool_name)".into(),
1930 ))?;
1931
1932 let args = call
1934 .args
1935 .get(2)
1936 .map(|a| self.compile_expr(&a.expr))
1937 .transpose()?;
1938
1939 return Ok(Some((server_id, tool_name, args)));
1940 }
1941 }
1942 }
1943 }
1944 }
1945 }
1946
1947 Ok(None)
1948 }
1949
1950 fn extract_path_template(&mut self, expr: &Expr) -> Result<PathTemplate, CompileError> {
1951 match expr {
1952 Expr::Lit(Lit::Str(s)) => Ok(PathTemplate::static_path(
1954 s.value.to_string_lossy().into_owned(),
1955 )),
1956
1957 Expr::Tpl(tpl) => {
1959 let mut parts = Vec::new();
1960 for (i, quasi) in tpl.quasis.iter().enumerate() {
1961 let raw = quasi.raw.to_string();
1962 if !raw.is_empty() {
1963 parts.push(PathPart::Literal(raw));
1964 }
1965 if i < tpl.exprs.len() {
1966 if let Expr::Ident(ident) = tpl.exprs[i].as_ref() {
1968 parts.push(PathPart::Variable(ident.sym.to_string()));
1969 } else {
1970 let expr = self.compile_expr(&tpl.exprs[i])?;
1972 parts.push(PathPart::Expression(expr));
1973 }
1974 }
1975 }
1976 Ok(PathTemplate { parts })
1977 },
1978
1979 _ => Err(CompileError::InvalidPath(
1980 "Path must be a string or template literal".into(),
1981 )),
1982 }
1983 }
1984
1985 fn extract_arrow_callback(
1986 &mut self,
1987 call: &CallExpr,
1988 ) -> Result<(String, ValueExpr), CompileError> {
1989 let arg = call
1990 .args
1991 .first()
1992 .ok_or_else(|| CompileError::UnsupportedExpression("missing callback".into()))?;
1993
1994 if let Expr::Arrow(arrow) = arg.expr.as_ref() {
1995 let param_name = if let Some(Pat::Ident(ident)) = arrow.params.first() {
1997 ident.id.sym.to_string()
1998 } else {
1999 return Err(CompileError::UnsupportedExpression(
2000 "complex callback parameter".into(),
2001 ));
2002 };
2003
2004 let body = match &*arrow.body {
2006 BlockStmtOrExpr::Expr(expr) => self.compile_expr(expr)?,
2007 BlockStmtOrExpr::BlockStmt(block) => {
2008 let mut bindings: Vec<(String, ValueExpr)> = Vec::new();
2010 let mut return_expr: Option<ValueExpr> = None;
2011
2012 for stmt in &block.stmts {
2013 match stmt {
2014 Stmt::Decl(Decl::Var(var_decl)) => {
2016 for decl in &var_decl.decls {
2017 let var_name = self.get_var_name(&decl.name)?;
2018 if let Some(init) = &decl.init {
2019 let expr = self.compile_expr(init)?;
2020 bindings.push((var_name, expr));
2021 }
2022 }
2023 },
2024 Stmt::Return(ret) => {
2026 if let Some(arg) = &ret.arg {
2027 return_expr = Some(self.compile_expr(arg)?);
2028 }
2029 break; },
2031 Stmt::Expr(_) => {
2033 },
2035 _ => {},
2036 }
2037 }
2038
2039 match return_expr {
2040 Some(result) if bindings.is_empty() => result,
2041 Some(result) => ValueExpr::Block {
2042 bindings,
2043 result: Box::new(result),
2044 },
2045 None => {
2046 return Err(CompileError::UnsupportedExpression(
2047 "callback block without return".into(),
2048 ));
2049 },
2050 }
2051 },
2052 };
2053
2054 Ok((param_name, body))
2055 } else {
2056 Err(CompileError::UnsupportedExpression(
2057 "callback must be arrow function".into(),
2058 ))
2059 }
2060 }
2061
2062 fn extract_reduce_callback(
2064 &mut self,
2065 call: &CallExpr,
2066 ) -> Result<(String, String, ValueExpr), CompileError> {
2067 let arg = call
2068 .args
2069 .first()
2070 .ok_or_else(|| CompileError::UnsupportedExpression("missing callback".into()))?;
2071
2072 if let Expr::Arrow(arrow) = arg.expr.as_ref() {
2073 if arrow.params.len() < 2 {
2075 return Err(CompileError::UnsupportedExpression(
2076 "reduce callback must have 2 parameters".into(),
2077 ));
2078 }
2079
2080 let acc_name = if let Pat::Ident(ident) = &arrow.params[0] {
2081 ident.id.sym.to_string()
2082 } else {
2083 return Err(CompileError::UnsupportedExpression(
2084 "complex callback parameter".into(),
2085 ));
2086 };
2087
2088 let item_name = if let Pat::Ident(ident) = &arrow.params[1] {
2089 ident.id.sym.to_string()
2090 } else {
2091 return Err(CompileError::UnsupportedExpression(
2092 "complex callback parameter".into(),
2093 ));
2094 };
2095
2096 let body = match &*arrow.body {
2098 BlockStmtOrExpr::Expr(expr) => self.compile_expr(expr)?,
2099 BlockStmtOrExpr::BlockStmt(block) => {
2100 for stmt in &block.stmts {
2102 if let Stmt::Return(ret) = stmt {
2103 if let Some(arg) = &ret.arg {
2104 return Ok((acc_name, item_name, self.compile_expr(arg)?));
2105 }
2106 }
2107 }
2108 return Err(CompileError::UnsupportedExpression(
2109 "callback block without return".into(),
2110 ));
2111 },
2112 };
2113
2114 Ok((acc_name, item_name, body))
2115 } else {
2116 Err(CompileError::UnsupportedExpression(
2117 "callback must be arrow function".into(),
2118 ))
2119 }
2120 }
2121
2122 fn extract_number_arg(
2123 &self,
2124 call: &CallExpr,
2125 index: usize,
2126 ) -> Result<Option<i64>, CompileError> {
2127 if let Some(arg) = call.args.get(index) {
2128 if let Expr::Lit(Lit::Num(n)) = arg.expr.as_ref() {
2129 return Ok(Some(n.value as i64));
2130 }
2131 if let Expr::Unary(unary) = arg.expr.as_ref() {
2132 if unary.op == UnaryOp::Minus {
2133 if let Expr::Lit(Lit::Num(n)) = unary.arg.as_ref() {
2134 return Ok(Some(-(n.value as i64)));
2135 }
2136 }
2137 }
2138 }
2139 Ok(None)
2140 }
2141
2142 fn extract_bound(&self, expr: &ValueExpr) -> Option<usize> {
2143 if let ValueExpr::ArrayMethod {
2144 method: ArrayMethodCall::Slice { end, .. },
2145 ..
2146 } = expr
2147 {
2148 return *end;
2149 }
2150 None
2151 }
2152
2153 fn get_var_name(&self, pat: &Pat) -> Result<String, CompileError> {
2154 match pat {
2155 Pat::Ident(ident) => Ok(ident.id.sym.to_string()),
2156 _ => Err(CompileError::UnsupportedExpression(
2157 "complex destructuring".into(),
2158 )),
2159 }
2160 }
2161
2162 fn next_temp_var(&mut self) -> String {
2164 let name = format!("__destructure_{}", self.destructure_counter);
2165 self.destructure_counter += 1;
2166 name
2167 }
2168
2169 fn extract_loop_var(&mut self, pat: &Pat) -> Result<(String, Vec<PlanStep>), CompileError> {
2172 match pat {
2173 Pat::Ident(ident) => Ok((ident.id.sym.to_string(), Vec::new())),
2174 Pat::Object(obj_pat) => {
2175 let temp_var = self.next_temp_var();
2176 let bindings = Self::extract_object_bindings(obj_pat)?;
2177 let steps = bindings
2178 .into_iter()
2179 .map(|(var_name, property)| PlanStep::Assign {
2180 var: var_name,
2181 expr: ValueExpr::PropertyAccess {
2182 object: Box::new(ValueExpr::Variable(temp_var.clone())),
2183 property,
2184 },
2185 })
2186 .collect();
2187 Ok((temp_var, steps))
2188 },
2189 Pat::Array(arr_pat) => {
2190 let temp_var = self.next_temp_var();
2191 let mut steps = Vec::new();
2192 for (i, elem) in arr_pat.elems.iter().enumerate() {
2193 if let Some(p) = elem {
2194 let var_name = self.get_var_name(p)?;
2195 steps.push(PlanStep::Assign {
2196 var: var_name,
2197 expr: ValueExpr::ArrayIndex {
2198 array: Box::new(ValueExpr::Variable(temp_var.clone())),
2199 index: Box::new(ValueExpr::Literal(JsonValue::Number(
2200 (i as i64).into(),
2201 ))),
2202 },
2203 });
2204 }
2205 }
2206 Ok((temp_var, steps))
2207 },
2208 _ => Err(CompileError::UnsupportedExpression(
2209 "complex loop variable pattern".into(),
2210 )),
2211 }
2212 }
2213
2214 fn extract_object_bindings(obj_pat: &ObjectPat) -> Result<Vec<(String, String)>, CompileError> {
2218 let mut bindings = Vec::new();
2219 for prop in &obj_pat.props {
2220 match prop {
2221 ObjectPatProp::Assign(assign) => {
2222 if assign.value.is_some() {
2224 return Err(CompileError::UnsupportedExpression(
2225 "default values in destructuring".into(),
2226 ));
2227 }
2228 let name = assign.key.sym.to_string();
2229 bindings.push((name.clone(), name));
2230 },
2231 ObjectPatProp::KeyValue(kv) => {
2232 let key = match &kv.key {
2234 PropName::Ident(ident) => ident.sym.to_string(),
2235 PropName::Str(s) => s.value.to_string_lossy().into_owned(),
2236 _ => {
2237 return Err(CompileError::UnsupportedExpression(
2238 "computed destructuring key".into(),
2239 ));
2240 },
2241 };
2242 let var_name = match kv.value.as_ref() {
2243 Pat::Ident(ident) => ident.id.sym.to_string(),
2244 _ => {
2245 return Err(CompileError::UnsupportedExpression(
2246 "nested destructuring".into(),
2247 ));
2248 },
2249 };
2250 bindings.push((var_name, key));
2251 },
2252 ObjectPatProp::Rest(_) => {
2253 return Err(CompileError::UnsupportedExpression(
2254 "rest pattern in destructuring".into(),
2255 ));
2256 },
2257 }
2258 }
2259 Ok(bindings)
2260 }
2261
2262 fn compile_object_destructuring(
2265 &mut self,
2266 obj_pat: &ObjectPat,
2267 init: &Expr,
2268 steps: &mut Vec<PlanStep>,
2269 ) -> Result<(), CompileError> {
2270 let bindings = Self::extract_object_bindings(obj_pat)?;
2271 let temp_var = self.next_temp_var();
2272
2273 self.compile_var_init(&temp_var, init, steps)?;
2275
2276 for (var_name, property) in bindings {
2278 steps.push(PlanStep::Assign {
2279 var: var_name,
2280 expr: ValueExpr::PropertyAccess {
2281 object: Box::new(ValueExpr::Variable(temp_var.clone())),
2282 property,
2283 },
2284 });
2285 }
2286 Ok(())
2287 }
2288
2289 fn compile_array_destructuring(
2292 &mut self,
2293 arr_pat: &ArrayPat,
2294 init: &Expr,
2295 steps: &mut Vec<PlanStep>,
2296 ) -> Result<(), CompileError> {
2297 let temp_var = self.next_temp_var();
2298
2299 self.compile_var_init(&temp_var, init, steps)?;
2301
2302 for (i, elem) in arr_pat.elems.iter().enumerate() {
2304 if let Some(pat) = elem {
2305 let var_name = self.get_var_name(pat)?;
2306 steps.push(PlanStep::Assign {
2307 var: var_name,
2308 expr: ValueExpr::ArrayIndex {
2309 array: Box::new(ValueExpr::Variable(temp_var.clone())),
2310 index: Box::new(ValueExpr::Literal(JsonValue::Number((i as i64).into()))),
2311 },
2312 });
2313 }
2314 }
2316 Ok(())
2317 }
2318
2319 fn lit_to_json(&self, lit: &Lit) -> JsonValue {
2320 match lit {
2321 Lit::Str(s) => JsonValue::String(s.value.to_string_lossy().into_owned()),
2322 Lit::Num(n) => {
2323 if n.value.fract() == 0.0 {
2324 JsonValue::Number((n.value as i64).into())
2325 } else {
2326 serde_json::Number::from_f64(n.value)
2327 .map(JsonValue::Number)
2328 .unwrap_or(JsonValue::Null)
2329 }
2330 },
2331 Lit::Bool(b) => JsonValue::Bool(b.value),
2332 Lit::Null(_) => JsonValue::Null,
2333 _ => JsonValue::Null,
2334 }
2335 }
2336
2337 fn prop_name_to_string(&self, prop: &PropName) -> Result<String, CompileError> {
2338 match prop {
2339 PropName::Ident(ident) => Ok(ident.sym.to_string()),
2340 PropName::Str(s) => Ok(s.value.to_string_lossy().into_owned()),
2341 PropName::Num(n) => Ok(n.value.to_string()),
2342 _ => Err(CompileError::UnsupportedExpression(
2343 "computed property".into(),
2344 )),
2345 }
2346 }
2347
2348 fn compile_bin_op(&self, op: BinaryOp) -> Result<BinaryOperator, CompileError> {
2349 match op {
2350 BinaryOp::Add => Ok(BinaryOperator::Add),
2351 BinaryOp::Sub => Ok(BinaryOperator::Sub),
2352 BinaryOp::Mul => Ok(BinaryOperator::Mul),
2353 BinaryOp::Div => Ok(BinaryOperator::Div),
2354 BinaryOp::Mod => Ok(BinaryOperator::Mod),
2355 BinaryOp::BitOr => Ok(BinaryOperator::BitwiseOr),
2356 BinaryOp::EqEq => Ok(BinaryOperator::Eq),
2357 BinaryOp::NotEq => Ok(BinaryOperator::NotEq),
2358 BinaryOp::EqEqEq => Ok(BinaryOperator::StrictEq),
2359 BinaryOp::NotEqEq => Ok(BinaryOperator::StrictNotEq),
2360 BinaryOp::Lt => Ok(BinaryOperator::Lt),
2361 BinaryOp::LtEq => Ok(BinaryOperator::Lte),
2362 BinaryOp::Gt => Ok(BinaryOperator::Gt),
2363 BinaryOp::GtEq => Ok(BinaryOperator::Gte),
2364 BinaryOp::LogicalAnd => Ok(BinaryOperator::And),
2365 BinaryOp::LogicalOr => Ok(BinaryOperator::Or),
2366 BinaryOp::NullishCoalescing => {
2367 Err(CompileError::UnsupportedExpression(
2369 "nullish coalescing".into(),
2370 ))
2371 },
2372 _ => Err(CompileError::UnsupportedExpression(format!(
2373 "binary operator {:?}",
2374 op
2375 ))),
2376 }
2377 }
2378
2379 fn record_api_call(&mut self, method: &str, path: &PathTemplate) {
2380 self.api_call_count += 1;
2381
2382 if !self.methods_used.contains(&method.to_string()) {
2384 self.methods_used.push(method.to_string());
2385 }
2386
2387 if method != "GET" && method != "HEAD" && method != "OPTIONS" {
2389 self.has_mutations = true;
2390 }
2391
2392 let endpoint = if !path.is_dynamic() {
2394 path.parts
2395 .iter()
2396 .filter_map(|p| match p {
2397 PathPart::Literal(s) => Some(s.clone()),
2398 _ => None,
2399 })
2400 .collect::<String>()
2401 } else {
2402 "{dynamic}".to_string()
2403 };
2404 if !self.endpoints.contains(&endpoint) {
2405 self.endpoints.push(endpoint);
2406 }
2407 }
2408}
2409
2410impl Default for PlanCompiler {
2411 fn default() -> Self {
2412 Self::new()
2413 }
2414}
2415
2416#[async_trait::async_trait]
2425pub trait HttpExecutor: Send + Sync {
2426 async fn execute_request(
2428 &self,
2429 method: &str,
2430 path: &str,
2431 body: Option<JsonValue>,
2432 ) -> Result<JsonValue, ExecutionError>;
2433}
2434
2435#[cfg(feature = "mcp-code-mode")]
2441#[async_trait::async_trait]
2442pub trait McpExecutor: Send + Sync {
2443 async fn call_tool(
2445 &self,
2446 server_id: &str,
2447 tool_name: &str,
2448 args: JsonValue,
2449 ) -> Result<JsonValue, ExecutionError>;
2450}
2451
2452#[async_trait::async_trait]
2458pub trait SdkExecutor: Send + Sync {
2459 async fn execute_operation(
2464 &self,
2465 operation: &str,
2466 args: Option<JsonValue>,
2467 ) -> Result<JsonValue, ExecutionError>;
2468}
2469
2470#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
2476pub enum MockExecutionMode {
2477 #[default]
2479 DryRun,
2480 Testing,
2482 Record,
2484}
2485
2486pub struct MockHttpExecutor {
2516 responses: std::sync::RwLock<HashMap<String, JsonValue>>,
2518 default_response: JsonValue,
2520 recorded_calls: std::sync::RwLock<Vec<MockedCall>>,
2522}
2523
2524#[derive(Debug, Clone, Serialize)]
2526pub struct MockedCall {
2527 pub method: String,
2529 pub path: String,
2531 pub body: Option<JsonValue>,
2533 pub response: JsonValue,
2535}
2536
2537impl MockHttpExecutor {
2538 pub fn new_dry_run() -> Self {
2541 Self {
2542 responses: std::sync::RwLock::new(HashMap::new()),
2543 default_response: JsonValue::Object(serde_json::Map::new()),
2544 recorded_calls: std::sync::RwLock::new(Vec::new()),
2545 }
2546 }
2547
2548 pub fn new_testing() -> Self {
2551 Self {
2552 responses: std::sync::RwLock::new(HashMap::new()),
2553 default_response: JsonValue::Object(serde_json::Map::new()),
2554 recorded_calls: std::sync::RwLock::new(Vec::new()),
2555 }
2556 }
2557
2558 pub fn with_default_response(mut self, response: JsonValue) -> Self {
2560 self.default_response = response;
2561 self
2562 }
2563
2564 pub fn with_response(self, path_pattern: &str, response: JsonValue) -> Self {
2575 self.responses
2576 .write()
2577 .unwrap()
2578 .insert(path_pattern.to_string(), response);
2579 self
2580 }
2581
2582 pub fn add_response(&self, path_pattern: &str, response: JsonValue) {
2584 self.responses
2585 .write()
2586 .unwrap()
2587 .insert(path_pattern.to_string(), response);
2588 }
2589
2590 pub fn recorded_calls(&self) -> Vec<MockedCall> {
2592 self.recorded_calls.read().unwrap().clone()
2593 }
2594
2595 pub fn clear_calls(&self) {
2597 self.recorded_calls.write().unwrap().clear();
2598 }
2599
2600 pub fn call_count(&self) -> usize {
2602 self.recorded_calls.read().unwrap().len()
2603 }
2604
2605 pub fn was_called(&self, path: &str) -> bool {
2607 self.recorded_calls
2608 .read()
2609 .unwrap()
2610 .iter()
2611 .any(|c| c.path == path)
2612 }
2613
2614 pub fn was_called_with_method(&self, method: &str, path: &str) -> bool {
2616 self.recorded_calls
2617 .read()
2618 .unwrap()
2619 .iter()
2620 .any(|c| c.method == method && c.path == path)
2621 }
2622
2623 fn find_response(&self, path: &str) -> JsonValue {
2625 let responses = self.responses.read().unwrap();
2626
2627 if let Some(response) = responses.get(path) {
2629 return response.clone();
2630 }
2631
2632 for (pattern, response) in responses.iter() {
2634 if Self::matches_pattern(pattern, path) {
2635 return response.clone();
2636 }
2637 }
2638
2639 self.default_response.clone()
2641 }
2642
2643 fn matches_pattern(pattern: &str, path: &str) -> bool {
2645 if !pattern.contains('*') {
2646 return pattern == path;
2647 }
2648
2649 let pattern_parts: Vec<&str> = pattern.split('/').collect();
2650 let path_parts: Vec<&str> = path.split('/').collect();
2651
2652 if pattern_parts.len() != path_parts.len() {
2653 if pattern.ends_with("*") && path_parts.len() >= pattern_parts.len() - 1 {
2655 } else {
2657 return false;
2658 }
2659 }
2660
2661 for (p, s) in pattern_parts.iter().zip(path_parts.iter()) {
2662 if *p != "*" && *p != *s {
2663 return false;
2664 }
2665 }
2666
2667 true
2668 }
2669}
2670
2671#[async_trait::async_trait]
2672impl HttpExecutor for MockHttpExecutor {
2673 async fn execute_request(
2674 &self,
2675 method: &str,
2676 path: &str,
2677 body: Option<JsonValue>,
2678 ) -> Result<JsonValue, ExecutionError> {
2679 let response = self.find_response(path);
2680
2681 let call = MockedCall {
2683 method: method.to_string(),
2684 path: path.to_string(),
2685 body,
2686 response: response.clone(),
2687 };
2688 self.recorded_calls.write().unwrap().push(call);
2689
2690 Ok(response)
2691 }
2692}
2693
2694unsafe impl Send for MockHttpExecutor {}
2696unsafe impl Sync for MockHttpExecutor {}
2697
2698#[derive(Debug, Clone, Serialize)]
2700pub struct ExecutionResult {
2701 pub value: JsonValue,
2703 pub api_calls: Vec<ApiCallLog>,
2705 pub execution_time_ms: u64,
2707}
2708
2709#[derive(Debug, Clone, Serialize)]
2711pub struct ApiCallLog {
2712 pub method: String,
2714 pub path: String,
2716 pub body: Option<JsonValue>,
2718 pub response: JsonValue,
2720 pub duration_ms: u64,
2722}
2723
2724pub struct PlanExecutor<H: HttpExecutor> {
2726 http: H,
2727 config: ExecutionConfig,
2728 variables: HashMap<String, JsonValue>,
2729 api_calls: Vec<ApiCallLog>,
2730 api_call_count: usize,
2731 #[cfg(feature = "mcp-code-mode")]
2732 mcp: Option<Box<dyn McpExecutor>>,
2733 sdk: Option<Box<dyn SdkExecutor>>,
2735}
2736
2737impl<H: HttpExecutor> PlanExecutor<H> {
2738 pub fn new(http: H, config: ExecutionConfig) -> Self {
2740 Self {
2741 http,
2742 config,
2743 variables: HashMap::new(),
2744 api_calls: Vec::new(),
2745 api_call_count: 0,
2746 #[cfg(feature = "mcp-code-mode")]
2747 mcp: None,
2748 sdk: None,
2749 }
2750 }
2751
2752 #[cfg(feature = "mcp-code-mode")]
2754 pub fn set_mcp_executor(&mut self, executor: impl McpExecutor + 'static) {
2755 self.mcp = Some(Box::new(executor));
2756 }
2757
2758 pub fn set_sdk_executor(&mut self, executor: impl SdkExecutor + 'static) {
2760 self.sdk = Some(Box::new(executor));
2761 }
2762
2763 pub fn set_variable(&mut self, name: impl Into<String>, value: JsonValue) {
2765 self.variables.insert(name.into(), value);
2766 }
2767
2768 pub async fn execute(
2770 &mut self,
2771 plan: &ExecutionPlan,
2772 ) -> Result<ExecutionResult, ExecutionError> {
2773 let start = std::time::Instant::now();
2774
2775 let mut return_value = JsonValue::Null;
2776
2777 for step in &plan.steps {
2778 match self.execute_step(step).await? {
2779 StepOutcome::Return(value) => {
2780 return_value = value;
2781 break; },
2783 StepOutcome::None | StepOutcome::Continue | StepOutcome::Break => {},
2784 }
2785 }
2786
2787 let blocked_in_output =
2790 find_blocked_fields_in_output(&return_value, &self.config.output_blocked_fields);
2791
2792 if !blocked_in_output.is_empty() {
2793 return Err(ExecutionError::RuntimeError {
2794 message: format!(
2795 "Script output contains blocked fields: {}",
2796 blocked_in_output.join(", ")
2797 ),
2798 });
2799 }
2800
2801 Ok(ExecutionResult {
2802 value: return_value,
2803 api_calls: std::mem::take(&mut self.api_calls),
2804 execution_time_ms: start.elapsed().as_millis() as u64,
2805 })
2806 }
2807
2808 fn execute_step<'a>(
2811 &'a mut self,
2812 step: &'a PlanStep,
2813 ) -> std::pin::Pin<
2814 Box<dyn std::future::Future<Output = Result<StepOutcome, ExecutionError>> + Send + 'a>,
2815 > {
2816 Box::pin(async move {
2817 match step {
2818 PlanStep::ApiCall {
2819 result_var,
2820 method,
2821 path,
2822 body,
2823 } => {
2824 self.api_call_count += 1;
2825 if self.api_call_count > self.config.max_api_calls {
2826 return Err(ExecutionError::RuntimeError {
2827 message: format!(
2828 "Too many API calls: {} (max: {})",
2829 self.api_call_count, self.config.max_api_calls
2830 ),
2831 });
2832 }
2833
2834 let resolved_path = self.resolve_path(path)?;
2835 let resolved_body = match body {
2836 Some(expr) => Some(self.evaluate(expr)?),
2837 None => None,
2838 };
2839
2840 let call_start = std::time::Instant::now();
2841 let raw_response = self
2842 .http
2843 .execute_request(method, &resolved_path, resolved_body.clone())
2844 .await
2845 .map_err(|e| ExecutionError::RuntimeError {
2846 message: format!("{} {} failed: {}", method, resolved_path, e),
2847 })?;
2848 let duration_ms = call_start.elapsed().as_millis() as u64;
2849
2850 let response = filter_blocked_fields(raw_response, &self.config.blocked_fields);
2853
2854 self.api_calls.push(ApiCallLog {
2855 method: method.clone(),
2856 path: resolved_path,
2857 body: resolved_body,
2858 response: response.clone(),
2859 duration_ms,
2860 });
2861
2862 if result_var != "_" {
2863 self.variables.insert(result_var.clone(), response);
2864 }
2865 Ok(StepOutcome::None)
2866 },
2867
2868 PlanStep::Assign { var, expr } => {
2869 let value = self.evaluate(expr)?;
2870 self.variables.insert(var.clone(), value);
2871 Ok(StepOutcome::None)
2872 },
2873
2874 PlanStep::Conditional {
2875 condition,
2876 then_steps,
2877 else_steps,
2878 } => {
2879 let cond_value = self.evaluate(condition)?;
2880 let steps = if shared_is_truthy(&cond_value) {
2881 then_steps
2882 } else {
2883 else_steps
2884 };
2885
2886 for step in steps {
2887 match self.execute_step(step).await? {
2888 StepOutcome::None => {},
2889 outcome => return Ok(outcome),
2890 }
2891 }
2892 Ok(StepOutcome::None)
2893 },
2894
2895 PlanStep::BoundedLoop {
2896 item_var,
2897 collection,
2898 max_iterations,
2899 body,
2900 } => {
2901 let collection_value = self.evaluate(collection)?;
2902 let items = match collection_value {
2903 JsonValue::Array(arr) => arr,
2904 _ => {
2905 return Err(ExecutionError::RuntimeError {
2906 message: "Loop collection must be an array".into(),
2907 })
2908 },
2909 };
2910
2911 let limit = (*max_iterations).min(self.config.max_loop_iterations);
2912 'outer: for item in items.into_iter().take(limit) {
2913 self.variables.insert(item_var.clone(), item);
2914
2915 for step in body {
2916 match self.execute_step(step).await? {
2917 StepOutcome::Return(value) => {
2918 return Ok(StepOutcome::Return(value))
2919 },
2920 StepOutcome::None => {},
2921 StepOutcome::Continue => continue 'outer,
2922 StepOutcome::Break => break 'outer,
2923 }
2924 }
2925 }
2926 Ok(StepOutcome::None)
2927 },
2928
2929 PlanStep::Return { value } => {
2930 let result = self.evaluate(value)?;
2931 Ok(StepOutcome::Return(result))
2932 },
2933
2934 PlanStep::TryCatch {
2935 try_steps,
2936 catch_var,
2937 catch_steps,
2938 finally_steps,
2939 } => {
2940 let try_result = async {
2942 for step in try_steps {
2943 match self.execute_step(step).await? {
2944 StepOutcome::None => {},
2945 outcome => return Ok::<StepOutcome, ExecutionError>(outcome),
2946 }
2947 }
2948 Ok(StepOutcome::None)
2949 }
2950 .await;
2951
2952 let result = match try_result {
2954 Ok(outcome) => {
2955 outcome
2957 },
2958 Err(error) => {
2959 if let Some(var) = catch_var {
2961 let error_obj = JsonValue::Object(serde_json::Map::from_iter([(
2963 "message".to_string(),
2964 JsonValue::String(format!("{}", error)),
2965 )]));
2966 self.variables.insert(var.clone(), error_obj);
2967 }
2968
2969 let mut catch_outcome = StepOutcome::None;
2971 for step in catch_steps {
2972 match self.execute_step(step).await? {
2973 StepOutcome::None => {},
2974 outcome => {
2975 catch_outcome = outcome;
2976 break;
2977 },
2978 }
2979 }
2980 catch_outcome
2981 },
2982 };
2983
2984 for step in finally_steps {
2986 match self.execute_step(step).await? {
2987 StepOutcome::None => {},
2988 outcome => return Ok(outcome),
2989 }
2990 }
2991
2992 Ok(result)
2993 },
2994
2995 PlanStep::ParallelApiCalls { result_var, calls } => {
2999 let mut results = Vec::with_capacity(calls.len());
3000 for (_temp_var, method, path, body) in calls {
3001 self.api_call_count += 1;
3002 if self.api_call_count > self.config.max_api_calls {
3003 return Err(ExecutionError::RuntimeError {
3004 message: format!(
3005 "Maximum API calls exceeded ({})",
3006 self.config.max_api_calls
3007 ),
3008 });
3009 }
3010
3011 let resolved_path = self.resolve_path(path)?;
3012 let resolved_body = body.as_ref().map(|b| self.evaluate(b)).transpose()?;
3013 let call_start = std::time::Instant::now();
3014 let raw_response = self
3015 .http
3016 .execute_request(method, &resolved_path, resolved_body.clone())
3017 .await
3018 .map_err(|e| ExecutionError::RuntimeError {
3019 message: format!("{} {} failed: {}", method, resolved_path, e),
3020 })?;
3021 let duration_ms = call_start.elapsed().as_millis() as u64;
3022 let response =
3023 filter_blocked_fields(raw_response, &self.config.blocked_fields);
3024
3025 self.api_calls.push(ApiCallLog {
3026 method: method.clone(),
3027 path: resolved_path,
3028 body: resolved_body,
3029 response: response.clone(),
3030 duration_ms,
3031 });
3032
3033 results.push(response);
3034 }
3035 self.variables
3036 .insert(result_var.clone(), JsonValue::Array(results));
3037 Ok(StepOutcome::None)
3038 },
3039
3040 PlanStep::Continue => Ok(StepOutcome::Continue),
3042
3043 PlanStep::Break => Ok(StepOutcome::Break),
3045
3046 #[cfg(feature = "mcp-code-mode")]
3048 PlanStep::McpCall {
3049 result_var,
3050 server_id,
3051 tool_name,
3052 args,
3053 } => {
3054 self.api_call_count += 1;
3055 if self.api_call_count > self.config.max_api_calls {
3056 return Err(ExecutionError::RuntimeError {
3057 message: format!(
3058 "Too many calls: {} (max: {})",
3059 self.api_call_count, self.config.max_api_calls
3060 ),
3061 });
3062 }
3063
3064 let resolved_args = match args {
3065 Some(expr) => self.evaluate(expr)?,
3066 None => JsonValue::Object(Default::default()),
3067 };
3068
3069 let mcp_executor =
3070 self.mcp
3071 .as_ref()
3072 .ok_or_else(|| ExecutionError::RuntimeError {
3073 message: "MCP executor not configured".into(),
3074 })?;
3075
3076 let call_start = std::time::Instant::now();
3077 let result = mcp_executor
3078 .call_tool(server_id, tool_name, resolved_args.clone())
3079 .await?;
3080 let duration_ms = call_start.elapsed().as_millis() as u64;
3081
3082 self.api_calls.push(ApiCallLog {
3083 method: format!("MCP:{}.{}", server_id, tool_name),
3084 path: format!("{}/{}", server_id, tool_name),
3085 body: Some(resolved_args),
3086 response: result.clone(),
3087 duration_ms,
3088 });
3089
3090 if result_var != "_" {
3091 self.variables.insert(result_var.clone(), result);
3092 }
3093 Ok(StepOutcome::None)
3094 },
3095
3096 PlanStep::SdkCall {
3098 result_var,
3099 operation,
3100 args,
3101 } => {
3102 self.api_call_count += 1;
3103 if self.api_call_count > self.config.max_api_calls {
3104 return Err(ExecutionError::RuntimeError {
3105 message: format!(
3106 "Too many calls: {} (max: {})",
3107 self.api_call_count, self.config.max_api_calls
3108 ),
3109 });
3110 }
3111
3112 let resolved_args =
3113 args.as_ref().map(|expr| self.evaluate(expr)).transpose()?;
3114
3115 let sdk_executor =
3116 self.sdk
3117 .as_ref()
3118 .ok_or_else(|| ExecutionError::RuntimeError {
3119 message: "SDK executor not configured".into(),
3120 })?;
3121
3122 let call_start = std::time::Instant::now();
3123 let result = sdk_executor
3124 .execute_operation(operation, resolved_args.clone())
3125 .await?;
3126 let duration_ms = call_start.elapsed().as_millis() as u64;
3127
3128 self.api_calls.push(ApiCallLog {
3129 method: operation.clone(),
3130 path: format!("sdk:{}", operation),
3131 body: resolved_args,
3132 response: result.clone(),
3133 duration_ms,
3134 });
3135
3136 if result_var != "_" {
3137 self.variables.insert(result_var.clone(), result);
3138 }
3139 Ok(StepOutcome::None)
3140 },
3141 }
3142 })
3143 }
3144
3145 fn resolve_path(&self, path: &PathTemplate) -> Result<String, ExecutionError> {
3147 let mut result = String::new();
3148 for part in &path.parts {
3149 match part {
3150 PathPart::Literal(s) => result.push_str(s),
3151 PathPart::Variable(var) => {
3152 let value =
3153 self.variables
3154 .get(var)
3155 .ok_or_else(|| ExecutionError::RuntimeError {
3156 message: format!("Undefined variable in path: {}", var),
3157 })?;
3158 result.push_str(&shared_json_to_string_with_mode(
3159 value,
3160 JsonStringMode::Json,
3161 ));
3162 },
3163 PathPart::Expression(expr) => {
3164 let value = self.evaluate(expr)?;
3165 result.push_str(&shared_json_to_string_with_mode(
3166 &value,
3167 JsonStringMode::Json,
3168 ));
3169 },
3170 }
3171 }
3172 Ok(result)
3173 }
3174
3175 fn evaluate(&self, expr: &ValueExpr) -> Result<JsonValue, ExecutionError> {
3178 shared_evaluate(expr, &self.variables)
3179 }
3180}
3181
3182pub type JsExecutor = PlanCompiler;
3188
3189#[cfg(test)]
3190mod tests {
3191 use super::*;
3192
3193 #[test]
3194 fn test_execution_config_default() {
3195 let config = ExecutionConfig::default();
3196 assert_eq!(config.max_api_calls, 50);
3197 assert_eq!(config.timeout_seconds, 30);
3198 assert_eq!(config.max_loop_iterations, 100);
3199 }
3200
3201 #[test]
3202 fn test_path_template_static() {
3203 let path = PathTemplate::static_path("/users".into());
3204 assert!(!path.is_dynamic());
3205 }
3206
3207 #[test]
3208 fn test_path_template_dynamic() {
3209 let path = PathTemplate {
3210 parts: vec![
3211 PathPart::Literal("/users/".into()),
3212 PathPart::Variable("id".into()),
3213 ],
3214 };
3215 assert!(path.is_dynamic());
3216 }
3217
3218 #[test]
3219 fn test_plan_metadata() {
3220 let metadata = PlanMetadata {
3221 api_call_count: 2,
3222 has_mutations: false,
3223 endpoints: vec!["/users".into(), "/products".into()],
3224 methods_used: vec!["GET".into()],
3225 };
3226 assert_eq!(metadata.api_call_count, 2);
3227 assert!(!metadata.has_mutations);
3228 }
3229
3230 #[test]
3231 fn test_compile_simple_api_call() {
3232 let code = r#"
3233 const user = await api.get('/users/1');
3234 return user;
3235 "#;
3236
3237 let mut compiler = PlanCompiler::new();
3238 let plan = compiler.compile_code(code).expect("Should compile");
3239
3240 assert_eq!(plan.metadata.api_call_count, 1);
3241 assert!(!plan.metadata.has_mutations);
3242 assert_eq!(plan.steps.len(), 2); }
3244
3245 #[test]
3246 fn test_compile_multiple_api_calls() {
3247 let code = r#"
3248 const users = await api.get('/users');
3249 const products = await api.get('/products');
3250 return { users, products };
3251 "#;
3252
3253 let mut compiler = PlanCompiler::new();
3254 let plan = compiler.compile_code(code).expect("Should compile");
3255
3256 assert_eq!(plan.metadata.api_call_count, 2);
3257 assert!(!plan.metadata.has_mutations);
3258 }
3259
3260 #[test]
3261 fn test_compile_mutation() {
3262 let code = r#"
3263 const result = await api.post('/users', { name: 'Test' });
3264 return result;
3265 "#;
3266
3267 let mut compiler = PlanCompiler::new();
3268 let plan = compiler.compile_code(code).expect("Should compile");
3269
3270 assert_eq!(plan.metadata.api_call_count, 1);
3271 assert!(plan.metadata.has_mutations);
3272 }
3273
3274 #[test]
3275 fn test_compile_dynamic_path() {
3276 let code = r#"
3277 const id = 123;
3278 const user = await api.get(`/users/${id}`);
3279 return user;
3280 "#;
3281
3282 let mut compiler = PlanCompiler::new();
3283 let plan = compiler.compile_code(code).expect("Should compile");
3284
3285 assert_eq!(plan.metadata.api_call_count, 1);
3286 }
3287
3288 #[test]
3289 fn test_compile_bounded_loop() {
3290 let code = r#"
3291 const items = [];
3292 const users = [{ id: 1 }, { id: 2 }, { id: 3 }];
3293 for (const user of users.slice(0, 2)) {
3294 const detail = await api.get(`/users/${user.id}`);
3295 items.push(detail);
3296 }
3297 return items;
3298 "#;
3299
3300 let mut compiler = PlanCompiler::new();
3301 let plan = compiler.compile_code(code).expect("Should compile");
3302
3303 assert!(plan
3305 .steps
3306 .iter()
3307 .any(|s| matches!(s, PlanStep::BoundedLoop { .. })));
3308 }
3309
3310 #[test]
3311 fn test_compile_unbounded_loop_detection() {
3312 let code = r#"
3316 const users = [{ id: 1 }, { id: 2 }, { id: 3 }];
3317 for (const user of users) {
3318 const detail = await api.get(`/users/${user.id}`);
3319 }
3320 return users;
3321 "#;
3322
3323 let mut compiler = PlanCompiler::new();
3324 let result = compiler.compile_code(code);
3325
3326 assert!(result.is_ok(), "Loop compiled: {:?}", result);
3329 }
3330
3331 #[test]
3332 fn test_compile_conditional() {
3333 let code = r#"
3334 const user = await api.get('/users/1');
3335 if (user.active) {
3336 const orders = await api.get(`/users/${user.id}/orders`);
3337 return orders;
3338 } else {
3339 return [];
3340 }
3341 "#;
3342
3343 let mut compiler = PlanCompiler::new();
3344 let plan = compiler.compile_code(code).expect("Should compile");
3345
3346 assert!(plan
3347 .steps
3348 .iter()
3349 .any(|s| matches!(s, PlanStep::Conditional { .. })));
3350 }
3351
3352 struct MockHttpExecutor {
3354 responses: std::collections::HashMap<String, JsonValue>,
3355 }
3356
3357 impl MockHttpExecutor {
3358 fn new() -> Self {
3359 Self {
3360 responses: std::collections::HashMap::new(),
3361 }
3362 }
3363
3364 fn add_response(&mut self, path: &str, response: JsonValue) {
3365 self.responses.insert(path.to_string(), response);
3366 }
3367 }
3368
3369 #[async_trait::async_trait]
3370 impl HttpExecutor for MockHttpExecutor {
3371 async fn execute_request(
3372 &self,
3373 _method: &str,
3374 path: &str,
3375 _body: Option<JsonValue>,
3376 ) -> Result<JsonValue, ExecutionError> {
3377 self.responses
3378 .get(path)
3379 .cloned()
3380 .ok_or_else(|| ExecutionError::RuntimeError {
3381 message: format!("No mock response for path: {}", path),
3382 })
3383 }
3384 }
3385
3386 #[tokio::test]
3387 async fn test_execute_simple_api_call() {
3388 let code = r#"
3389 const user = await api.get('/users/1');
3390 return user;
3391 "#;
3392
3393 let mut compiler = PlanCompiler::new();
3394 let plan = compiler.compile_code(code).expect("Should compile");
3395
3396 let mut mock_http = MockHttpExecutor::new();
3397 mock_http.add_response("/users/1", serde_json::json!({ "id": 1, "name": "Alice" }));
3398
3399 let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
3400 let result = executor.execute(&plan).await.expect("Should execute");
3401
3402 assert_eq!(result.value["id"], 1);
3403 assert_eq!(result.value["name"], "Alice");
3404 assert_eq!(result.api_calls.len(), 1);
3405 }
3406
3407 #[tokio::test]
3408 async fn test_execute_multiple_api_calls() {
3409 let code = r#"
3410 const users = await api.get('/users');
3411 const products = await api.get('/products');
3412 return { users, products };
3413 "#;
3414
3415 let mut compiler = PlanCompiler::new();
3416 let plan = compiler.compile_code(code).expect("Should compile");
3417
3418 let mut mock_http = MockHttpExecutor::new();
3419 mock_http.add_response("/users", serde_json::json!([{ "id": 1, "name": "Alice" }]));
3420 mock_http.add_response(
3421 "/products",
3422 serde_json::json!([{ "id": 100, "name": "Widget" }]),
3423 );
3424
3425 let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
3426 let result = executor.execute(&plan).await.expect("Should execute");
3427
3428 assert!(result.value["users"].is_array());
3429 assert!(result.value["products"].is_array());
3430 assert_eq!(result.api_calls.len(), 2);
3431 }
3432
3433 #[tokio::test]
3434 async fn test_execute_with_template_path() {
3435 let code = r#"
3436 const userId = 42;
3437 const user = await api.get(`/users/${userId}`);
3438 return user;
3439 "#;
3440
3441 let mut compiler = PlanCompiler::new();
3442 let plan = compiler.compile_code(code).expect("Should compile");
3443
3444 let mut mock_http = MockHttpExecutor::new();
3445 mock_http.add_response("/users/42", serde_json::json!({ "id": 42, "name": "Bob" }));
3446
3447 let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
3448 let result = executor.execute(&plan).await.expect("Should execute");
3449
3450 assert_eq!(result.value["id"], 42);
3451 assert_eq!(result.value["name"], "Bob");
3452 }
3453
3454 #[tokio::test]
3455 async fn test_execute_conditional_true_branch() {
3456 let code = r#"
3457 const user = await api.get('/users/1');
3458 if (user.active) {
3459 return { status: "active", user: user };
3460 } else {
3461 return { status: "inactive" };
3462 }
3463 "#;
3464
3465 let mut compiler = PlanCompiler::new();
3466 let plan = compiler.compile_code(code).expect("Should compile");
3467
3468 let mut mock_http = MockHttpExecutor::new();
3469 mock_http.add_response("/users/1", serde_json::json!({ "id": 1, "active": true }));
3470
3471 let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
3472 let result = executor.execute(&plan).await.expect("Should execute");
3473
3474 assert_eq!(result.value["status"], "active");
3475 }
3476
3477 #[tokio::test]
3478 async fn test_execute_conditional_false_branch() {
3479 let code = r#"
3480 const user = await api.get('/users/1');
3481 if (user.active) {
3482 return { status: "active" };
3483 } else {
3484 return { status: "inactive", user: user };
3485 }
3486 "#;
3487
3488 let mut compiler = PlanCompiler::new();
3489 let plan = compiler.compile_code(code).expect("Should compile");
3490
3491 let mut mock_http = MockHttpExecutor::new();
3492 mock_http.add_response("/users/1", serde_json::json!({ "id": 1, "active": false }));
3493
3494 let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
3495 let result = executor.execute(&plan).await.expect("Should execute");
3496
3497 assert_eq!(result.value["status"], "inactive");
3498 }
3499
3500 #[tokio::test]
3501 async fn test_compile_and_execute_reduce() {
3502 let code = r#"
3503 const products = await api.get('/products');
3504 const totalPrice = products.reduce((sum, p) => sum + p.price, 0);
3505 return { total: totalPrice };
3506 "#;
3507
3508 let mut compiler = PlanCompiler::new();
3509 let plan = compiler.compile_code(code).expect("Should compile reduce");
3510
3511 let mut mock_http = MockHttpExecutor::new();
3512 mock_http.add_response(
3513 "/products",
3514 serde_json::json!([
3515 { "id": 1, "name": "Widget", "price": 10 },
3516 { "id": 2, "name": "Gadget", "price": 25 },
3517 { "id": 3, "name": "Gizmo", "price": 15 }
3518 ]),
3519 );
3520
3521 let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
3522 let result = executor.execute(&plan).await.expect("Should execute");
3523
3524 assert_eq!(result.value["total"].as_f64().unwrap(), 50.0);
3526 }
3527
3528 #[tokio::test]
3529 async fn test_compile_and_execute_to_fixed() {
3530 let code = r#"
3531 const products = await api.get('/products');
3532 const totalPrice = products.reduce((sum, p) => sum + p.price, 0);
3533 const averagePrice = products.length > 0 ? totalPrice / products.length : 0;
3534 return { averagePrice: averagePrice.toFixed(2) };
3535 "#;
3536
3537 let mut compiler = PlanCompiler::new();
3538 let plan = compiler.compile_code(code).expect("Should compile toFixed");
3539
3540 let mut mock_http = MockHttpExecutor::new();
3541 mock_http.add_response(
3542 "/products",
3543 serde_json::json!([
3544 { "id": 1, "name": "Widget", "price": 10 },
3545 { "id": 2, "name": "Gadget", "price": 25 },
3546 { "id": 3, "name": "Gizmo", "price": 15 }
3547 ]),
3548 );
3549
3550 let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
3551 let result = executor.execute(&plan).await.expect("Should execute");
3552
3553 assert_eq!(result.value["averagePrice"], "16.67");
3555 }
3556
3557 #[test]
3562 fn test_filter_blocked_fields_simple() {
3563 let value = serde_json::json!({
3564 "id": 1,
3565 "name": "Alice",
3566 "password": "secret123",
3567 "email": "alice@example.com"
3568 });
3569
3570 let blocked: HashSet<String> = ["password"].iter().map(|s| s.to_string()).collect();
3571 let filtered = filter_blocked_fields(value, &blocked);
3572
3573 assert_eq!(filtered["id"], 1);
3574 assert_eq!(filtered["name"], "Alice");
3575 assert_eq!(filtered["email"], "alice@example.com");
3576 assert!(filtered.get("password").is_none());
3577 }
3578
3579 #[test]
3580 fn test_filter_blocked_fields_multiple() {
3581 let value = serde_json::json!({
3582 "id": 1,
3583 "name": "Alice",
3584 "password": "secret123",
3585 "ssn": "123-45-6789",
3586 "apiKey": "key-abc123"
3587 });
3588
3589 let blocked: HashSet<String> = ["password", "ssn", "apiKey"]
3590 .iter()
3591 .map(|s| s.to_string())
3592 .collect();
3593 let filtered = filter_blocked_fields(value, &blocked);
3594
3595 assert_eq!(filtered["id"], 1);
3596 assert_eq!(filtered["name"], "Alice");
3597 assert!(filtered.get("password").is_none());
3598 assert!(filtered.get("ssn").is_none());
3599 assert!(filtered.get("apiKey").is_none());
3600 }
3601
3602 #[test]
3603 fn test_filter_blocked_fields_nested() {
3604 let value = serde_json::json!({
3605 "user": {
3606 "id": 1,
3607 "profile": {
3608 "name": "Alice",
3609 "password": "secret123"
3610 }
3611 }
3612 });
3613
3614 let blocked: HashSet<String> = ["password"].iter().map(|s| s.to_string()).collect();
3615 let filtered = filter_blocked_fields(value, &blocked);
3616
3617 assert_eq!(filtered["user"]["id"], 1);
3618 assert_eq!(filtered["user"]["profile"]["name"], "Alice");
3619 assert!(filtered["user"]["profile"].get("password").is_none());
3620 }
3621
3622 #[test]
3623 fn test_filter_blocked_fields_in_array() {
3624 let value = serde_json::json!([
3625 { "id": 1, "name": "Alice", "password": "secret1" },
3626 { "id": 2, "name": "Bob", "password": "secret2" }
3627 ]);
3628
3629 let blocked: HashSet<String> = ["password"].iter().map(|s| s.to_string()).collect();
3630 let filtered = filter_blocked_fields(value, &blocked);
3631
3632 let arr = filtered.as_array().unwrap();
3633 assert_eq!(arr.len(), 2);
3634 assert_eq!(arr[0]["id"], 1);
3635 assert_eq!(arr[0]["name"], "Alice");
3636 assert!(arr[0].get("password").is_none());
3637 assert_eq!(arr[1]["id"], 2);
3638 assert_eq!(arr[1]["name"], "Bob");
3639 assert!(arr[1].get("password").is_none());
3640 }
3641
3642 #[test]
3643 fn test_filter_blocked_fields_empty_blocklist() {
3644 let value = serde_json::json!({
3645 "id": 1,
3646 "password": "secret123"
3647 });
3648
3649 let blocked: HashSet<String> = HashSet::new();
3650 let filtered = filter_blocked_fields(value.clone(), &blocked);
3651
3652 assert_eq!(filtered, value);
3654 }
3655
3656 #[test]
3657 fn test_filter_blocked_fields_primitive_values() {
3658 let blocked: HashSet<String> = ["password"].iter().map(|s| s.to_string()).collect();
3660
3661 assert_eq!(
3662 filter_blocked_fields(JsonValue::String("test".into()), &blocked),
3663 JsonValue::String("test".into())
3664 );
3665 assert_eq!(
3666 filter_blocked_fields(JsonValue::Number(42.into()), &blocked),
3667 JsonValue::Number(42.into())
3668 );
3669 assert_eq!(
3670 filter_blocked_fields(JsonValue::Bool(true), &blocked),
3671 JsonValue::Bool(true)
3672 );
3673 assert_eq!(
3674 filter_blocked_fields(JsonValue::Null, &blocked),
3675 JsonValue::Null
3676 );
3677 }
3678
3679 #[tokio::test]
3680 async fn test_execute_with_blocked_fields() {
3681 let code = r#"
3682 const user = await api.get('/users/1');
3683 return user;
3684 "#;
3685
3686 let mut compiler = PlanCompiler::new();
3687 let plan = compiler.compile_code(code).expect("Should compile");
3688
3689 let mut mock_http = MockHttpExecutor::new();
3690 mock_http.add_response(
3691 "/users/1",
3692 serde_json::json!({
3693 "id": 1,
3694 "name": "Alice",
3695 "password": "secret123",
3696 "apiKey": "key-abc"
3697 }),
3698 );
3699
3700 let config = ExecutionConfig::default().with_blocked_fields(["password", "apiKey"]);
3702
3703 let mut executor = PlanExecutor::new(mock_http, config);
3704 let result = executor.execute(&plan).await.expect("Should execute");
3705
3706 assert_eq!(result.value["id"], 1);
3708 assert_eq!(result.value["name"], "Alice");
3709 assert!(result.value.get("password").is_none());
3710 assert!(result.value.get("apiKey").is_none());
3711 }
3712
3713 #[tokio::test]
3714 async fn test_execute_nested_blocked_fields() {
3715 let code = r#"
3716 const data = await api.get('/data');
3717 return data;
3718 "#;
3719
3720 let mut compiler = PlanCompiler::new();
3721 let plan = compiler.compile_code(code).expect("Should compile");
3722
3723 let mut mock_http = MockHttpExecutor::new();
3724 mock_http.add_response(
3725 "/data",
3726 serde_json::json!({
3727 "users": [
3728 { "id": 1, "name": "Alice", "secret": "hidden1" },
3729 { "id": 2, "name": "Bob", "secret": "hidden2" }
3730 ],
3731 "config": {
3732 "setting": "value",
3733 "secret": "also-hidden"
3734 }
3735 }),
3736 );
3737
3738 let config = ExecutionConfig::default().with_blocked_fields(["secret"]);
3740
3741 let mut executor = PlanExecutor::new(mock_http, config);
3742 let result = executor.execute(&plan).await.expect("Should execute");
3743
3744 let users = result.value["users"].as_array().unwrap();
3746 assert_eq!(users[0]["name"], "Alice");
3747 assert!(users[0].get("secret").is_none());
3748 assert_eq!(users[1]["name"], "Bob");
3749 assert!(users[1].get("secret").is_none());
3750
3751 assert_eq!(result.value["config"]["setting"], "value");
3752 assert!(result.value["config"].get("secret").is_none());
3753 }
3754
3755 #[test]
3760 fn test_find_blocked_fields_in_output_simple() {
3761 let value = serde_json::json!({
3762 "id": 1,
3763 "name": "Alice",
3764 "ssn": "123-45-6789"
3765 });
3766
3767 let blocked: HashSet<String> = ["ssn"].iter().map(|s| s.to_string()).collect();
3768 let violations = find_blocked_fields_in_output(&value, &blocked);
3769
3770 assert_eq!(violations.len(), 1);
3771 assert_eq!(violations[0], "ssn");
3772 }
3773
3774 #[test]
3775 fn test_find_blocked_fields_in_output_nested() {
3776 let value = serde_json::json!({
3777 "user": {
3778 "profile": {
3779 "name": "Alice",
3780 "salary": 100000
3781 }
3782 }
3783 });
3784
3785 let blocked: HashSet<String> = ["salary"].iter().map(|s| s.to_string()).collect();
3786 let violations = find_blocked_fields_in_output(&value, &blocked);
3787
3788 assert_eq!(violations.len(), 1);
3789 assert!(violations[0].contains("salary"));
3790 }
3791
3792 #[test]
3793 fn test_find_blocked_fields_in_output_array() {
3794 let value = serde_json::json!([
3795 { "id": 1, "ssn": "111" },
3796 { "id": 2, "ssn": "222" }
3797 ]);
3798
3799 let blocked: HashSet<String> = ["ssn"].iter().map(|s| s.to_string()).collect();
3800 let violations = find_blocked_fields_in_output(&value, &blocked);
3801
3802 assert_eq!(violations.len(), 2);
3804 }
3805
3806 #[test]
3807 fn test_find_blocked_fields_in_output_empty_blocklist() {
3808 let value = serde_json::json!({
3809 "id": 1,
3810 "ssn": "123-45-6789"
3811 });
3812
3813 let blocked: HashSet<String> = HashSet::new();
3814 let violations = find_blocked_fields_in_output(&value, &blocked);
3815
3816 assert!(violations.is_empty());
3817 }
3818
3819 #[test]
3820 fn test_find_blocked_fields_in_output_no_violations() {
3821 let value = serde_json::json!({
3822 "id": 1,
3823 "name": "Alice"
3824 });
3825
3826 let blocked: HashSet<String> = ["ssn", "salary"].iter().map(|s| s.to_string()).collect();
3827 let violations = find_blocked_fields_in_output(&value, &blocked);
3828
3829 assert!(violations.is_empty());
3830 }
3831
3832 #[tokio::test]
3833 async fn test_execute_output_blocked_fields_rejected() {
3834 let code = r#"
3835 const user = await api.get('/users/1');
3836 return { name: user.name, ssn: user.ssn };
3837 "#;
3838
3839 let mut compiler = PlanCompiler::new();
3840 let plan = compiler.compile_code(code).expect("Should compile");
3841
3842 let mut mock_http = MockHttpExecutor::new();
3843 mock_http.add_response(
3844 "/users/1",
3845 serde_json::json!({
3846 "id": 1,
3847 "name": "Alice",
3848 "ssn": "123-45-6789"
3849 }),
3850 );
3851
3852 let config = ExecutionConfig::default().with_output_blocked_fields(["ssn"]);
3855
3856 let mut executor = PlanExecutor::new(mock_http, config);
3857 let result = executor.execute(&plan).await;
3858
3859 assert!(result.is_err());
3861 let err = result.unwrap_err();
3862 assert!(format!("{:?}", err).contains("ssn"));
3863 }
3864
3865 #[tokio::test]
3866 async fn test_execute_output_blocked_fields_internal_use_allowed() {
3867 let code = r#"
3869 const user = await api.get('/users/1');
3870 return { id: user.id, name: user.name };
3871 "#;
3872
3873 let mut compiler = PlanCompiler::new();
3874 let plan = compiler.compile_code(code).expect("Should compile");
3875
3876 let mut mock_http = MockHttpExecutor::new();
3877 mock_http.add_response(
3878 "/users/1",
3879 serde_json::json!({
3880 "id": 1,
3881 "name": "Alice",
3882 "ssn": "123-45-6789"
3883 }),
3884 );
3885
3886 let config = ExecutionConfig::default().with_output_blocked_fields(["ssn"]);
3889
3890 let mut executor = PlanExecutor::new(mock_http, config);
3891 let result = executor.execute(&plan).await.expect("Should succeed");
3892
3893 assert_eq!(result.value["id"], 1);
3895 assert_eq!(result.value["name"], "Alice");
3896 assert!(result.value.get("ssn").is_none());
3897 }
3898
3899 #[tokio::test]
3900 async fn test_execute_both_blocklists() {
3901 let code = r#"
3903 const user = await api.get('/users/1');
3904 return { name: user.name, dateOfBirth: user.dateOfBirth };
3905 "#;
3906
3907 let mut compiler = PlanCompiler::new();
3908 let plan = compiler.compile_code(code).expect("Should compile");
3909
3910 let mut mock_http = MockHttpExecutor::new();
3911 mock_http.add_response(
3912 "/users/1",
3913 serde_json::json!({
3914 "id": 1,
3915 "name": "Alice",
3916 "password": "secret123",
3917 "dateOfBirth": "1990-01-01"
3918 }),
3919 );
3920
3921 let config = ExecutionConfig::default()
3924 .with_blocked_fields(["password"])
3925 .with_output_blocked_fields(["dateOfBirth"]);
3926
3927 let mut executor = PlanExecutor::new(mock_http, config);
3928 let result = executor.execute(&plan).await;
3929
3930 assert!(result.is_err());
3932 let err = result.unwrap_err();
3933 assert!(format!("{:?}", err).contains("dateOfBirth"));
3934 }
3935
3936 #[tokio::test]
3941 async fn test_prebound_args_comparison() {
3942 let code = r#"
3944 if (args.k > args.n) {
3945 return { error: 'k must be <= n' };
3946 }
3947 return { ok: true };
3948 "#;
3949
3950 let mut compiler = PlanCompiler::new();
3951 let plan = compiler.compile_code(code).expect("Should compile");
3952
3953 let mock_http = MockHttpExecutor::new();
3954 let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
3955 executor.set_variable("args", serde_json::json!({"n": 3, "k": 5}));
3956
3957 let result = executor.execute(&plan).await.expect("Should execute");
3958 assert_eq!(
3959 result.value["error"], "k must be <= n",
3960 "Expected error for k > n, got: {:?}",
3961 result.value
3962 );
3963 }
3964
3965 #[tokio::test]
3966 async fn test_prebound_args_strict_equality() {
3967 let code = r#"
3969 if (args.k === 0) {
3970 return { result: 1 };
3971 }
3972 return { result: 'not zero' };
3973 "#;
3974
3975 let mut compiler = PlanCompiler::new();
3976 let plan = compiler.compile_code(code).expect("Should compile");
3977
3978 let mock_http = MockHttpExecutor::new();
3979 let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
3980 executor.set_variable("args", serde_json::json!({"k": 0}));
3981
3982 let result = executor.execute(&plan).await.expect("Should execute");
3983 assert_eq!(result.value["result"], 1);
3984 }
3985
3986 #[tokio::test]
3987 async fn test_assignment_expression_in_statement() {
3988 let code = r#"
3990 let k = 5;
3991 k = 2;
3992 return { k: k };
3993 "#;
3994
3995 let mut compiler = PlanCompiler::new();
3996 let plan = compiler.compile_code(code).expect("Should compile");
3997
3998 let mock_http = MockHttpExecutor::new();
3999 let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4000
4001 let result = executor.execute(&plan).await.expect("Should execute");
4002 assert_eq!(result.value["k"], 2);
4003 }
4004
4005 #[tokio::test]
4006 async fn test_assignment_swap_variables() {
4007 let code = r#"
4009 let a = 3;
4010 let b = 7;
4011 if (a < b) {
4012 const old_a = a;
4013 a = b;
4014 b = old_a;
4015 }
4016 return { a: a, b: b };
4017 "#;
4018
4019 let mut compiler = PlanCompiler::new();
4020 let plan = compiler.compile_code(code).expect("Should compile");
4021
4022 let mock_http = MockHttpExecutor::new();
4023 let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4024
4025 let result = executor.execute(&plan).await.expect("Should execute");
4026 assert_eq!(result.value["a"], 7);
4027 assert_eq!(result.value["b"], 3);
4028 }
4029
4030 #[cfg(feature = "mcp-code-mode")]
4035 mod mcp_tests {
4036 use super::*;
4037
4038 struct MockCalculatorExecutor;
4040
4041 #[async_trait::async_trait]
4042 impl McpExecutor for MockCalculatorExecutor {
4043 async fn call_tool(
4044 &self,
4045 _server_id: &str,
4046 tool_name: &str,
4047 args: JsonValue,
4048 ) -> Result<JsonValue, ExecutionError> {
4049 match tool_name {
4050 "add" => {
4051 let a = args["a"].as_f64().unwrap_or(0.0);
4052 let b = args["b"].as_f64().unwrap_or(0.0);
4053 Ok(serde_json::json!({"result": a + b}))
4054 },
4055 "subtract" => {
4056 let a = args["a"].as_f64().unwrap_or(0.0);
4057 let b = args["b"].as_f64().unwrap_or(0.0);
4058 Ok(serde_json::json!({"result": a - b}))
4059 },
4060 "multiply" => {
4061 let a = args["a"].as_f64().unwrap_or(0.0);
4062 let b = args["b"].as_f64().unwrap_or(0.0);
4063 Ok(serde_json::json!({"result": a * b}))
4064 },
4065 "divide" => {
4066 let a = args["a"].as_f64().unwrap_or(0.0);
4067 let b = args["b"].as_f64().unwrap_or(1.0);
4068 Ok(serde_json::json!({"result": a / b}))
4069 },
4070 "power" => {
4071 let base = args["base"].as_f64().unwrap_or(0.0);
4072 let exponent = args["exponent"].as_f64().unwrap_or(1.0);
4073 Ok(serde_json::json!({"result": base.powf(exponent)}))
4074 },
4075 "sqrt" => {
4076 let n = args["n"].as_f64().unwrap_or(0.0);
4077 Ok(serde_json::json!({"result": n.sqrt()}))
4078 },
4079 _ => Err(ExecutionError::RuntimeError {
4080 message: format!("Unknown tool: {}", tool_name),
4081 }),
4082 }
4083 }
4084 }
4085
4086 #[tokio::test]
4087 async fn test_mcp_call_simple() {
4088 let code = r#"
4089 const result = await mcp.call('calculator', 'add', { a: 5, b: 3 });
4090 return result;
4091 "#;
4092
4093 let mut compiler = PlanCompiler::new();
4094 let plan = compiler.compile_code(code).expect("Should compile");
4095
4096 let mock_http = MockHttpExecutor::new();
4097 let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4098 executor.set_mcp_executor(MockCalculatorExecutor);
4099
4100 let result = executor.execute(&plan).await.expect("Should execute");
4101 assert_eq!(result.value["result"], 8.0);
4102 }
4103
4104 #[tokio::test]
4105 async fn test_mcp_call_with_args() {
4106 let code = r#"
4108 const result = await mcp.call('calculator', 'add', { a: args.x, b: args.y });
4109 return { sum: result.result };
4110 "#;
4111
4112 let mut compiler = PlanCompiler::new();
4113 let plan = compiler.compile_code(code).expect("Should compile");
4114
4115 let mock_http = MockHttpExecutor::new();
4116 let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4117 executor.set_mcp_executor(MockCalculatorExecutor);
4118 executor.set_variable("args", serde_json::json!({"x": 10, "y": 20}));
4119
4120 let result = executor.execute(&plan).await.expect("Should execute");
4121 assert_eq!(result.value["sum"], 30.0);
4122 }
4123
4124 #[tokio::test]
4125 async fn test_mcp_assignment_in_loop() {
4126 let code = r#"
4128 let result = { result: 1 };
4129 for (const i of [2, 3, 4, 5]) {
4130 const mul = await mcp.call('calculator', 'multiply', { a: result.result, b: i });
4131 result = mul;
4132 }
4133 return { factorial: result.result };
4134 "#;
4135
4136 let mut compiler = PlanCompiler::new();
4137 let plan = compiler.compile_code(code).expect("Should compile");
4138
4139 let mock_http = MockHttpExecutor::new();
4140 let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4141 executor.set_mcp_executor(MockCalculatorExecutor);
4142
4143 let result = executor.execute(&plan).await.expect("Should execute");
4144 assert_eq!(result.value["factorial"], 120.0);
4146 }
4147
4148 #[tokio::test]
4149 async fn test_combinations_c_5_3() {
4150 let code = r#"
4152if (args.k > args.n) {
4153 return { error: 'k must be <= n', n: args.n, k: args.k };
4154}
4155if (args.k === 0 || args.k === args.n) {
4156 return { n: args.n, k: args.k, result: 1 };
4157}
4158let k = args.k;
4159const complement = await mcp.call('calculator', 'subtract', { a: args.n, b: args.k });
4160let nmk = complement.result;
4161if (nmk < k) {
4162 const old_k = k;
4163 k = nmk;
4164 nmk = old_k;
4165}
4166let result = { result: 1 };
4167for (const i of [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]) {
4168 if (i > k) { break; }
4169 const nki = await mcp.call('calculator', 'add', { a: nmk, b: i });
4170 const num = await mcp.call('calculator', 'multiply', { a: result.result, b: nki.result });
4171 result = await mcp.call('calculator', 'divide', { a: num.result, b: i });
4172}
4173return { n: args.n, k: args.k, result: result.result };
4174 "#;
4175
4176 let mut compiler = PlanCompiler::new();
4177 let plan = compiler.compile_code(code).expect("Should compile");
4178
4179 let mock_http = MockHttpExecutor::new();
4180 let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4181 executor.set_mcp_executor(MockCalculatorExecutor);
4182 executor.set_variable("args", serde_json::json!({"n": 5, "k": 3}));
4183
4184 let result = executor.execute(&plan).await.expect("Should execute");
4185 assert_eq!(
4186 result.value["result"], 10.0,
4187 "C(5,3) should be 10, got: {:?}",
4188 result.value
4189 );
4190 }
4191
4192 #[tokio::test]
4193 async fn test_combinations_k_greater_than_n() {
4194 let code = r#"
4196if (args.k > args.n) {
4197 return { error: 'k must be <= n', n: args.n, k: args.k };
4198}
4199return { result: 'should not reach here' };
4200 "#;
4201
4202 let mut compiler = PlanCompiler::new();
4203 let plan = compiler.compile_code(code).expect("Should compile");
4204
4205 let mock_http = MockHttpExecutor::new();
4206 let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4207 executor.set_mcp_executor(MockCalculatorExecutor);
4208 executor.set_variable("args", serde_json::json!({"n": 3, "k": 5}));
4209
4210 let result = executor.execute(&plan).await.expect("Should execute");
4211 assert_eq!(
4212 result.value["error"], "k must be <= n",
4213 "C(3,5) should return error, got: {:?}",
4214 result.value
4215 );
4216 }
4217
4218 #[tokio::test]
4219 async fn test_combinations_c_5_2() {
4220 let code = r#"
4222if (args.k > args.n) {
4223 return { error: 'k must be <= n', n: args.n, k: args.k };
4224}
4225if (args.k === 0 || args.k === args.n) {
4226 return { n: args.n, k: args.k, result: 1 };
4227}
4228let k = args.k;
4229const complement = await mcp.call('calculator', 'subtract', { a: args.n, b: args.k });
4230let nmk = complement.result;
4231if (nmk < k) {
4232 const old_k = k;
4233 k = nmk;
4234 nmk = old_k;
4235}
4236let result = { result: 1 };
4237for (const i of [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]) {
4238 if (i > k) { break; }
4239 const nki = await mcp.call('calculator', 'add', { a: nmk, b: i });
4240 const num = await mcp.call('calculator', 'multiply', { a: result.result, b: nki.result });
4241 result = await mcp.call('calculator', 'divide', { a: num.result, b: i });
4242}
4243return { n: args.n, k: args.k, result: result.result };
4244 "#;
4245
4246 let mut compiler = PlanCompiler::new();
4247 let plan = compiler.compile_code(code).expect("Should compile");
4248
4249 let mock_http = MockHttpExecutor::new();
4250 let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4251 executor.set_mcp_executor(MockCalculatorExecutor);
4252 executor.set_variable("args", serde_json::json!({"n": 5, "k": 2}));
4253
4254 let result = executor.execute(&plan).await.expect("Should execute");
4255 assert_eq!(
4256 result.value["result"], 10.0,
4257 "C(5,2) should be 10, got: {:?}",
4258 result.value
4259 );
4260 }
4261
4262 #[tokio::test]
4263 async fn test_combinations_edge_cases() {
4264 let code = r#"
4265if (args.k === 0 || args.k === args.n) {
4266 return { result: 1 };
4267}
4268return { result: 'not edge case' };
4269 "#;
4270
4271 let mut compiler = PlanCompiler::new();
4272 let plan = compiler.compile_code(code).expect("Should compile");
4273
4274 let mock_http = MockHttpExecutor::new();
4276 let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4277 executor.set_variable("args", serde_json::json!({"n": 5, "k": 0}));
4278 let result = executor.execute(&plan).await.expect("Should execute");
4279 assert_eq!(result.value["result"], 1, "C(5,0) should be 1");
4280
4281 let mock_http = MockHttpExecutor::new();
4283 let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4284 executor.set_variable("args", serde_json::json!({"n": 5, "k": 5}));
4285 let result = executor.execute(&plan).await.expect("Should execute");
4286 assert_eq!(result.value["result"], 1, "C(5,5) should be 1");
4287 }
4288
4289 #[tokio::test]
4290 async fn test_solve_quadratic() {
4291 let code = r#"
4293const b_sq = await mcp.call('calculator', 'power', { base: args.b, exponent: 2 });
4294const four_a = await mcp.call('calculator', 'multiply', { a: 4, b: args.a });
4295const four_ac = await mcp.call('calculator', 'multiply', { a: four_a.result, b: args.c });
4296const discriminant = await mcp.call('calculator', 'subtract', { a: b_sq.result, b: four_ac.result });
4297const root_type = discriminant.result > 0 ? 'two_real'
4298 : discriminant.result === 0 ? 'one_real' : 'complex';
4299if (discriminant.result < 0) {
4300 return { discriminant: discriminant.result, root_type: root_type, roots: [] };
4301}
4302const sqrt_disc = await mcp.call('calculator', 'sqrt', { n: discriminant.result });
4303const neg_b = await mcp.call('calculator', 'multiply', { a: -1, b: args.b });
4304const two_a = await mcp.call('calculator', 'multiply', { a: 2, b: args.a });
4305const x1_num = await mcp.call('calculator', 'add', { a: neg_b.result, b: sqrt_disc.result });
4306const x2_num = await mcp.call('calculator', 'subtract', { a: neg_b.result, b: sqrt_disc.result });
4307const x1 = await mcp.call('calculator', 'divide', { a: x1_num.result, b: two_a.result });
4308const x2 = await mcp.call('calculator', 'divide', { a: x2_num.result, b: two_a.result });
4309return { discriminant: discriminant.result, root_type: root_type, roots: [x1.result, x2.result] };
4310 "#;
4311
4312 let mut compiler = PlanCompiler::new();
4313 let plan = compiler.compile_code(code).expect("Should compile");
4314
4315 let mock_http = MockHttpExecutor::new();
4316 let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4317 executor.set_mcp_executor(MockCalculatorExecutor);
4318 executor.set_variable("args", serde_json::json!({"a": 1, "b": -3, "c": 2}));
4319
4320 let result = executor.execute(&plan).await.expect("Should execute");
4321 assert_eq!(result.value["root_type"], "two_real");
4322 assert_eq!(result.value["discriminant"], 1.0);
4323 let roots = result.value["roots"]
4324 .as_array()
4325 .expect("roots should be array");
4326 assert_eq!(roots.len(), 2);
4327 assert_eq!(roots[0], 2.0);
4328 assert_eq!(roots[1], 1.0);
4329 }
4330 }
4331
4332 #[tokio::test]
4337 async fn test_string_includes() {
4338 let code = r#"
4339 const text = "hello world";
4340 return { found: text.includes("world"), miss: text.includes("xyz") };
4341 "#;
4342
4343 let mut compiler = PlanCompiler::new();
4344 let plan = compiler.compile_code(code).expect("Should compile");
4345
4346 let mock_http = MockHttpExecutor::new();
4347 let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4348 let result = executor.execute(&plan).await.expect("Should execute");
4349
4350 assert_eq!(result.value["found"], true);
4351 assert_eq!(result.value["miss"], false);
4352 }
4353
4354 #[tokio::test]
4355 async fn test_string_index_of() {
4356 let code = r#"
4357 const text = "abcdef";
4358 return { idx: text.indexOf("cd"), miss: text.indexOf("xyz") };
4359 "#;
4360
4361 let mut compiler = PlanCompiler::new();
4362 let plan = compiler.compile_code(code).expect("Should compile");
4363
4364 let mock_http = MockHttpExecutor::new();
4365 let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4366 let result = executor.execute(&plan).await.expect("Should execute");
4367
4368 assert_eq!(result.value["idx"], 2);
4369 assert_eq!(result.value["miss"], -1);
4370 }
4371
4372 #[tokio::test]
4373 async fn test_string_length() {
4374 let code = r#"
4375 const text = "hello";
4376 return { len: text.length };
4377 "#;
4378
4379 let mut compiler = PlanCompiler::new();
4380 let plan = compiler.compile_code(code).expect("Should compile");
4381
4382 let mock_http = MockHttpExecutor::new();
4383 let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4384 let result = executor.execute(&plan).await.expect("Should execute");
4385
4386 assert_eq!(result.value["len"], 5);
4387 }
4388
4389 #[tokio::test]
4390 async fn test_string_slice() {
4391 let code = r#"
4392 const text = "hello world";
4393 return { first: text.slice(0, 5), rest: text.slice(6, 11) };
4394 "#;
4395
4396 let mut compiler = PlanCompiler::new();
4397 let plan = compiler.compile_code(code).expect("Should compile");
4398
4399 let mock_http = MockHttpExecutor::new();
4400 let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4401 let result = executor.execute(&plan).await.expect("Should execute");
4402
4403 assert_eq!(result.value["first"], "hello");
4404 assert_eq!(result.value["rest"], "world");
4405 }
4406
4407 #[tokio::test]
4408 async fn test_string_concat() {
4409 let code = r#"
4410 const greeting = "hello";
4411 return { result: greeting.concat(" world") };
4412 "#;
4413
4414 let mut compiler = PlanCompiler::new();
4415 let plan = compiler.compile_code(code).expect("Should compile");
4416
4417 let mock_http = MockHttpExecutor::new();
4418 let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4419 let result = executor.execute(&plan).await.expect("Should execute");
4420
4421 assert_eq!(result.value["result"], "hello world");
4422 }
4423
4424 #[tokio::test]
4425 async fn test_string_includes_in_filter() {
4426 let code = r#"
4428 const items = [
4429 { name: "TIMESTAMP_2024", desc: "A timestamped record" },
4430 { name: "PERSON_1", desc: "A person entity" },
4431 { name: "TIMESTAMP_2025", desc: "Another timestamped record" }
4432 ];
4433 const timestamped = items.filter(item => item.name.includes("TIMESTAMP"));
4434 return { count: timestamped.length, names: timestamped.map(t => t.name) };
4435 "#;
4436
4437 let mut compiler = PlanCompiler::new();
4438 let plan = compiler.compile_code(code).expect("Should compile");
4439
4440 let mock_http = MockHttpExecutor::new();
4441 let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4442 let result = executor.execute(&plan).await.expect("Should execute");
4443
4444 assert_eq!(result.value["count"], 2);
4445 let names = result.value["names"].as_array().unwrap();
4446 assert_eq!(names[0], "TIMESTAMP_2024");
4447 assert_eq!(names[1], "TIMESTAMP_2025");
4448 }
4449
4450 #[tokio::test]
4451 async fn test_array_includes_still_works() {
4452 let code = r#"
4454 const ids = ["alice", "bob", "charlie"];
4455 return { has_bob: ids.includes("bob"), has_dave: ids.includes("dave") };
4456 "#;
4457
4458 let mut compiler = PlanCompiler::new();
4459 let plan = compiler.compile_code(code).expect("Should compile");
4460
4461 let mock_http = MockHttpExecutor::new();
4462 let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4463 let result = executor.execute(&plan).await.expect("Should execute");
4464
4465 assert_eq!(result.value["has_bob"], true);
4466 assert_eq!(result.value["has_dave"], false);
4467 }
4468
4469 #[test]
4474 fn test_compile_parse_float() {
4475 let code = r#"
4476 const x = parseFloat("3.14");
4477 return x;
4478 "#;
4479 let mut compiler = PlanCompiler::new();
4480 let plan = compiler
4481 .compile_code(code)
4482 .expect("parseFloat should compile");
4483 assert_eq!(plan.steps.len(), 2); }
4485
4486 #[test]
4487 fn test_compile_parse_int() {
4488 let code = r#"
4489 const x = parseInt("42");
4490 return x;
4491 "#;
4492 let mut compiler = PlanCompiler::new();
4493 compiler
4494 .compile_code(code)
4495 .expect("parseInt should compile");
4496 }
4497
4498 #[test]
4499 fn test_compile_math_abs() {
4500 let code = r#"
4501 const x = Math.abs(-5);
4502 return x;
4503 "#;
4504 let mut compiler = PlanCompiler::new();
4505 compiler
4506 .compile_code(code)
4507 .expect("Math.abs should compile");
4508 }
4509
4510 #[test]
4511 fn test_compile_math_max() {
4512 let code = r#"
4513 const x = Math.max(1, 2, 3);
4514 return x;
4515 "#;
4516 let mut compiler = PlanCompiler::new();
4517 compiler
4518 .compile_code(code)
4519 .expect("Math.max should compile");
4520 }
4521
4522 #[test]
4523 fn test_compile_object_keys() {
4524 let code = r#"
4525 const obj = { a: 1, b: 2 };
4526 const keys = Object.keys(obj);
4527 return keys;
4528 "#;
4529 let mut compiler = PlanCompiler::new();
4530 compiler
4531 .compile_code(code)
4532 .expect("Object.keys should compile");
4533 }
4534
4535 #[test]
4536 fn test_compile_object_entries() {
4537 let code = r#"
4538 const obj = { x: 10 };
4539 const entries = Object.entries(obj);
4540 return entries;
4541 "#;
4542 let mut compiler = PlanCompiler::new();
4543 compiler
4544 .compile_code(code)
4545 .expect("Object.entries should compile");
4546 }
4547
4548 #[test]
4549 fn test_compile_unary_plus() {
4550 let code = r#"
4551 const x = +"42";
4552 return x;
4553 "#;
4554 let mut compiler = PlanCompiler::new();
4555 compiler.compile_code(code).expect("unary + should compile");
4556 }
4557
4558 #[test]
4559 fn test_compile_sort_with_comparator() {
4560 let code = r#"
4561 const arr = [3, 1, 2];
4562 const sorted = arr.sort((a, b) => a - b);
4563 return sorted;
4564 "#;
4565 let mut compiler = PlanCompiler::new();
4566 compiler
4567 .compile_code(code)
4568 .expect("sort with comparator should compile");
4569 }
4570
4571 #[test]
4572 fn test_compile_sort_without_comparator() {
4573 let code = r#"
4574 const arr = ["b", "a", "c"];
4575 const sorted = arr.sort();
4576 return sorted;
4577 "#;
4578 let mut compiler = PlanCompiler::new();
4579 compiler
4580 .compile_code(code)
4581 .expect("sort without comparator should compile");
4582 }
4583
4584 #[tokio::test]
4589 async fn test_execute_parse_float() {
4590 let code = r#"
4591 const x = parseFloat("3.14");
4592 return x;
4593 "#;
4594 let mut compiler = PlanCompiler::new();
4595 let plan = compiler.compile_code(code).unwrap();
4596 let mock_http = MockHttpExecutor::new();
4597 let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4598 let result = executor.execute(&plan).await.unwrap();
4599 #[allow(clippy::approx_constant)]
4602 let expected = serde_json::json!(3.14);
4603 assert_eq!(result.value, expected);
4604 }
4605
4606 #[tokio::test]
4607 async fn test_execute_math_abs_and_sort() {
4608 let code = r#"
4609 const items = [
4610 { name: "a", val: -5 },
4611 { name: "b", val: 3 },
4612 { name: "c", val: -1 }
4613 ];
4614 const sorted = items.sort((a, b) => Math.abs(b.val) - Math.abs(a.val));
4615 return sorted.map(x => x.name);
4616 "#;
4617 let mut compiler = PlanCompiler::new();
4618 let plan = compiler.compile_code(code).unwrap();
4619 let mock_http = MockHttpExecutor::new();
4620 let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4621 let result = executor.execute(&plan).await.unwrap();
4622 assert_eq!(result.value, serde_json::json!(["a", "b", "c"]));
4623 }
4624
4625 #[tokio::test]
4626 async fn test_execute_object_keys() {
4627 let code = r#"
4628 const obj = { x: 1, y: 2, z: 3 };
4629 return Object.keys(obj).length;
4630 "#;
4631 let mut compiler = PlanCompiler::new();
4632 let plan = compiler.compile_code(code).unwrap();
4633 let mock_http = MockHttpExecutor::new();
4634 let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4635 let result = executor.execute(&plan).await.unwrap();
4636 assert_eq!(result.value, serde_json::json!(3));
4637 }
4638
4639 #[tokio::test]
4640 async fn test_execute_unary_plus() {
4641 let code = r#"
4642 const x = +"42";
4643 return x;
4644 "#;
4645 let mut compiler = PlanCompiler::new();
4646 let plan = compiler.compile_code(code).unwrap();
4647 let mock_http = MockHttpExecutor::new();
4648 let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4649 let result = executor.execute(&plan).await.unwrap();
4650 assert_eq!(result.value, serde_json::json!(42.0));
4651 }
4652
4653 #[tokio::test]
4654 async fn test_execute_number_cast() {
4655 let code = r#"
4656 const x = Number("99.5");
4657 return x;
4658 "#;
4659 let mut compiler = PlanCompiler::new();
4660 let plan = compiler.compile_code(code).unwrap();
4661 let mock_http = MockHttpExecutor::new();
4662 let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4663 let result = executor.execute(&plan).await.unwrap();
4664 assert_eq!(result.value, serde_json::json!(99.5));
4665 }
4666
4667 #[tokio::test]
4668 async fn test_execute_math_round_floor_ceil() {
4669 let code = r#"
4670 return {
4671 round: Math.round(3.7),
4672 floor: Math.floor(3.7),
4673 ceil: Math.ceil(3.2)
4674 };
4675 "#;
4676 let mut compiler = PlanCompiler::new();
4677 let plan = compiler.compile_code(code).unwrap();
4678 let mock_http = MockHttpExecutor::new();
4679 let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4680 let result = executor.execute(&plan).await.unwrap();
4681 assert_eq!(result.value["round"], serde_json::json!(4.0));
4682 assert_eq!(result.value["floor"], serde_json::json!(3.0));
4683 assert_eq!(result.value["ceil"], serde_json::json!(4.0));
4684 }
4685
4686 #[test]
4691 fn test_compile_object_spread_basic() {
4692 let code = r#"
4693 const base = { id: 1, name: "Alice" };
4694 const extended = { ...base, age: 30 };
4695 return extended;
4696 "#;
4697 let mut compiler = PlanCompiler::new();
4698 let plan = compiler
4699 .compile_code(code)
4700 .expect("Object spread should compile");
4701 assert!(plan.steps.len() >= 2);
4702 }
4703
4704 #[tokio::test]
4705 async fn test_execute_object_spread_basic() {
4706 let code = r#"
4707 const base = { id: 1, name: "Alice" };
4708 const extended = { ...base, age: 30 };
4709 return extended;
4710 "#;
4711 let mut compiler = PlanCompiler::new();
4712 let plan = compiler.compile_code(code).unwrap();
4713 let mock_http = MockHttpExecutor::new();
4714 let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4715 let result = executor.execute(&plan).await.unwrap();
4716 assert_eq!(result.value["id"], serde_json::json!(1));
4717 assert_eq!(result.value["name"], serde_json::json!("Alice"));
4718 assert_eq!(result.value["age"], serde_json::json!(30));
4719 }
4720
4721 #[tokio::test]
4722 async fn test_execute_object_spread_override() {
4723 let code = r#"
4725 const obj = { id: 1, name: "old" };
4726 const updated = { ...obj, name: "new" };
4727 return updated;
4728 "#;
4729 let mut compiler = PlanCompiler::new();
4730 let plan = compiler.compile_code(code).unwrap();
4731 let mock_http = MockHttpExecutor::new();
4732 let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4733 let result = executor.execute(&plan).await.unwrap();
4734 assert_eq!(result.value["id"], serde_json::json!(1));
4735 assert_eq!(result.value["name"], serde_json::json!("new"));
4736 }
4737
4738 #[tokio::test]
4739 async fn test_execute_object_spread_multiple() {
4740 let code = r#"
4741 const a = { x: 1 };
4742 const b = { y: 2 };
4743 const merged = { ...a, ...b, z: 3 };
4744 return merged;
4745 "#;
4746 let mut compiler = PlanCompiler::new();
4747 let plan = compiler.compile_code(code).unwrap();
4748 let mock_http = MockHttpExecutor::new();
4749 let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4750 let result = executor.execute(&plan).await.unwrap();
4751 assert_eq!(result.value["x"], serde_json::json!(1));
4752 assert_eq!(result.value["y"], serde_json::json!(2));
4753 assert_eq!(result.value["z"], serde_json::json!(3));
4754 }
4755
4756 #[tokio::test]
4757 async fn test_execute_object_spread_with_api_result() {
4758 let code = r#"
4760 const config = await api.get('/config');
4761 const result = { ...config, extra: "added" };
4762 return result;
4763 "#;
4764 let mut compiler = PlanCompiler::new();
4765 let plan = compiler.compile_code(code).unwrap();
4766 let mut mock_http = MockHttpExecutor::new();
4767 mock_http.add_response(
4768 "/config",
4769 serde_json::json!({ "key": "value", "enabled": true }),
4770 );
4771 let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4772 let result = executor.execute(&plan).await.unwrap();
4773 assert_eq!(result.value["key"], serde_json::json!("value"));
4774 assert_eq!(result.value["enabled"], serde_json::json!(true));
4775 assert_eq!(result.value["extra"], serde_json::json!("added"));
4776 }
4777
4778 #[tokio::test]
4779 async fn test_execute_object_spread_non_object_noop() {
4780 let code = r#"
4782 const x = 42;
4783 const obj = { ...x, name: "test" };
4784 return obj;
4785 "#;
4786 let mut compiler = PlanCompiler::new();
4787 let plan = compiler.compile_code(code).unwrap();
4788 let mock_http = MockHttpExecutor::new();
4789 let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4790 let result = executor.execute(&plan).await.unwrap();
4791 assert_eq!(result.value["name"], serde_json::json!("test"));
4792 assert!(result.value.as_object().unwrap().len() == 1);
4794 }
4795
4796 #[tokio::test]
4797 async fn test_execute_object_spread_preserves_order() {
4798 let code = r#"
4801 const obj = { a: 1, b: 2 };
4802 const result = { b: 99, ...obj, a: 100 };
4803 return result;
4804 "#;
4805 let mut compiler = PlanCompiler::new();
4806 let plan = compiler.compile_code(code).unwrap();
4807 let mock_http = MockHttpExecutor::new();
4808 let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4809 let result = executor.execute(&plan).await.unwrap();
4810 assert_eq!(result.value["a"], serde_json::json!(100));
4812 assert_eq!(result.value["b"], serde_json::json!(2));
4813 }
4814
4815 #[test]
4820 fn test_compile_object_destructuring_simple() {
4821 let code = r#"
4822 const obj = { id: 1, name: "Alice" };
4823 const { id, name } = obj;
4824 return { id, name };
4825 "#;
4826 let mut compiler = PlanCompiler::new();
4827 let plan = compiler
4828 .compile_code(code)
4829 .expect("Object destructuring should compile");
4830 assert!(plan.steps.len() >= 4);
4832 }
4833
4834 #[tokio::test]
4835 async fn test_execute_object_destructuring_simple() {
4836 let code = r#"
4837 const obj = { id: 1, name: "Alice", extra: "ignored" };
4838 const { id, name } = obj;
4839 return { id, name };
4840 "#;
4841 let mut compiler = PlanCompiler::new();
4842 let plan = compiler.compile_code(code).unwrap();
4843 let mock_http = MockHttpExecutor::new();
4844 let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4845 let result = executor.execute(&plan).await.unwrap();
4846 assert_eq!(result.value["id"], serde_json::json!(1));
4847 assert_eq!(result.value["name"], serde_json::json!("Alice"));
4848 assert!(result.value.get("extra").is_none());
4850 }
4851
4852 #[tokio::test]
4853 async fn test_execute_object_destructuring_renamed() {
4854 let code = r#"
4855 const user = { id: 1, name: "Alice" };
4856 const { id: userId, name: userName } = user;
4857 return { userId, userName };
4858 "#;
4859 let mut compiler = PlanCompiler::new();
4860 let plan = compiler.compile_code(code).unwrap();
4861 let mock_http = MockHttpExecutor::new();
4862 let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4863 let result = executor.execute(&plan).await.unwrap();
4864 assert_eq!(result.value["userId"], serde_json::json!(1));
4865 assert_eq!(result.value["userName"], serde_json::json!("Alice"));
4866 }
4867
4868 #[tokio::test]
4869 async fn test_execute_object_destructuring_with_api_call() {
4870 let code = r#"
4872 const { data, status } = await api.get('/users');
4873 return { data, status };
4874 "#;
4875 let mut compiler = PlanCompiler::new();
4876 let plan = compiler.compile_code(code).unwrap();
4877 let mut mock_http = MockHttpExecutor::new();
4878 mock_http.add_response(
4879 "/users",
4880 serde_json::json!({ "data": [{"id": 1}], "status": "ok", "meta": "hidden" }),
4881 );
4882 let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4883 let result = executor.execute(&plan).await.unwrap();
4884 assert_eq!(result.value["data"], serde_json::json!([{"id": 1}]));
4885 assert_eq!(result.value["status"], serde_json::json!("ok"));
4886 }
4887
4888 #[tokio::test]
4889 async fn test_execute_object_destructuring_missing_property() {
4890 let code = r#"
4892 const obj = { id: 1 };
4893 const { id, name } = obj;
4894 return { id, name };
4895 "#;
4896 let mut compiler = PlanCompiler::new();
4897 let plan = compiler.compile_code(code).unwrap();
4898 let mock_http = MockHttpExecutor::new();
4899 let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4900 let result = executor.execute(&plan).await.unwrap();
4901 assert_eq!(result.value["id"], serde_json::json!(1));
4902 assert_eq!(result.value["name"], serde_json::json!(null));
4903 }
4904
4905 #[tokio::test]
4910 async fn test_execute_array_destructuring_simple() {
4911 let code = r#"
4912 const arr = [10, 20, 30];
4913 const [a, b] = arr;
4914 return { a, b };
4915 "#;
4916 let mut compiler = PlanCompiler::new();
4917 let plan = compiler.compile_code(code).unwrap();
4918 let mock_http = MockHttpExecutor::new();
4919 let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4920 let result = executor.execute(&plan).await.unwrap();
4921 assert_eq!(result.value["a"], serde_json::json!(10));
4922 assert_eq!(result.value["b"], serde_json::json!(20));
4923 }
4924
4925 #[tokio::test]
4926 async fn test_execute_array_destructuring_with_promise_all() {
4927 let code = r#"
4929 const [users, products] = await Promise.all([
4930 api.get('/users'),
4931 api.get('/products')
4932 ]);
4933 return { users, products };
4934 "#;
4935 let mut compiler = PlanCompiler::new();
4936 let plan = compiler.compile_code(code).unwrap();
4937 let mut mock_http = MockHttpExecutor::new();
4938 mock_http.add_response("/users", serde_json::json!([{"id": 1}]));
4939 mock_http.add_response("/products", serde_json::json!([{"sku": "A"}]));
4940 let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4941 let result = executor.execute(&plan).await.unwrap();
4942 assert_eq!(result.value["users"], serde_json::json!([{"id": 1}]));
4943 assert_eq!(result.value["products"], serde_json::json!([{"sku": "A"}]));
4944 }
4945
4946 #[test]
4951 fn test_compile_for_of_destructuring() {
4952 let code = r#"
4953 const items = [{ id: 1, name: "A" }, { id: 2, name: "B" }];
4954 const results = [];
4955 for (const { id, name } of items.slice(0, 10)) {
4956 results.push({ id, name });
4957 }
4958 return results;
4959 "#;
4960 let mut compiler = PlanCompiler::new();
4961 compiler
4962 .compile_code(code)
4963 .expect("For-of with destructuring should compile");
4964 }
4965
4966 #[tokio::test]
4967 async fn test_execute_for_of_destructuring() {
4968 let code = r#"
4969 const items = [{ id: 1, name: "A" }, { id: 2, name: "B" }];
4970 const results = [];
4971 for (const { id, name } of items.slice(0, 10)) {
4972 results.push({ label: name, num: id });
4973 }
4974 return results;
4975 "#;
4976 let mut compiler = PlanCompiler::new();
4977 let plan = compiler.compile_code(code).unwrap();
4978 let mock_http = MockHttpExecutor::new();
4979 let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4980 let result = executor.execute(&plan).await.unwrap();
4981 let arr = result.value.as_array().unwrap();
4982 assert_eq!(arr.len(), 2);
4983 assert_eq!(arr[0]["label"], serde_json::json!("A"));
4984 assert_eq!(arr[0]["num"], serde_json::json!(1));
4985 assert_eq!(arr[1]["label"], serde_json::json!("B"));
4986 assert_eq!(arr[1]["num"], serde_json::json!(2));
4987 }
4988
4989 #[tokio::test]
4990 async fn test_execute_for_of_destructuring_with_api_calls() {
4991 let code = r#"
4993 const users = [{ id: 1, role: "admin" }, { id: 2, role: "user" }];
4994 const results = [];
4995 for (const { id, role } of users.slice(0, 10)) {
4996 const detail = await api.get(`/users/${id}`);
4997 results.push({ role, detail });
4998 }
4999 return results;
5000 "#;
5001 let mut compiler = PlanCompiler::new();
5002 let plan = compiler.compile_code(code).unwrap();
5003 let mut mock_http = MockHttpExecutor::new();
5004 mock_http.add_response("/users/1", serde_json::json!({ "name": "Alice" }));
5005 mock_http.add_response("/users/2", serde_json::json!({ "name": "Bob" }));
5006 let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
5007 let result = executor.execute(&plan).await.unwrap();
5008 let arr = result.value.as_array().unwrap();
5009 assert_eq!(arr.len(), 2);
5010 assert_eq!(arr[0]["role"], serde_json::json!("admin"));
5011 assert_eq!(arr[0]["detail"]["name"], serde_json::json!("Alice"));
5012 assert_eq!(arr[1]["role"], serde_json::json!("user"));
5013 assert_eq!(arr[1]["detail"]["name"], serde_json::json!("Bob"));
5014 }
5015
5016 #[tokio::test]
5021 async fn test_execute_spread_and_destructuring_combined() {
5022 let code = r#"
5024 const { data, token } = await api.get('/auth');
5025 const result = await api.post('/action', { ...data, token });
5026 return result;
5027 "#;
5028 let mut compiler = PlanCompiler::new();
5029 let plan = compiler.compile_code(code).unwrap();
5030 let mut mock_http = MockHttpExecutor::new();
5031 mock_http.add_response(
5032 "/auth",
5033 serde_json::json!({ "data": { "user": "alice" }, "token": "abc123" }),
5034 );
5035 mock_http.add_response("/action", serde_json::json!({ "success": true }));
5036 let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
5037 let result = executor.execute(&plan).await.unwrap();
5038 assert_eq!(result.value["success"], serde_json::json!(true));
5039 }
5040}