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