1use crate::javascript::HttpMethod;
26use crate::types::ExecutionError;
27use serde::Serialize;
28use serde_json::Value as JsonValue;
29use std::collections::{HashMap, HashSet};
30
31use crate::eval::{
33 evaluate as shared_evaluate, is_truthy as shared_is_truthy,
34 json_to_string_with_mode as shared_json_to_string_with_mode, JsonStringMode,
35};
36use swc_common::{FileName, SourceMap};
37use swc_ecma_ast::*;
38use swc_ecma_parser::{lexer::Lexer, Parser, StringInput, Syntax};
39
40pub(crate) enum StepOutcome {
45 None,
47 Return(serde_json::Value),
49 Continue,
51 Break,
53}
54
55#[derive(Debug, Clone)]
57pub struct ExecutionConfig {
58 pub max_api_calls: usize,
60 pub timeout_seconds: u64,
62 pub max_loop_iterations: usize,
64 pub blocked_fields: HashSet<String>,
68 pub output_blocked_fields: HashSet<String>,
72}
73
74impl Default for ExecutionConfig {
75 fn default() -> Self {
76 Self {
77 max_api_calls: 50,
78 timeout_seconds: 30,
79 max_loop_iterations: 100,
80 blocked_fields: HashSet::new(),
81 output_blocked_fields: HashSet::new(),
82 }
83 }
84}
85
86impl ExecutionConfig {
87 pub fn with_blocked_fields(
89 mut self,
90 fields: impl IntoIterator<Item = impl Into<String>>,
91 ) -> Self {
92 self.blocked_fields = fields.into_iter().map(Into::into).collect();
93 self
94 }
95
96 pub fn with_output_blocked_fields(
98 mut self,
99 fields: impl IntoIterator<Item = impl Into<String>>,
100 ) -> Self {
101 self.output_blocked_fields = fields.into_iter().map(Into::into).collect();
102 self
103 }
104}
105
106pub fn filter_blocked_fields(value: JsonValue, blocked_fields: &HashSet<String>) -> JsonValue {
120 if blocked_fields.is_empty() {
121 return value;
122 }
123
124 match value {
125 JsonValue::Object(mut map) => {
126 map.retain(|key, _| !blocked_fields.contains(key));
128
129 let filtered: serde_json::Map<String, JsonValue> = map
131 .into_iter()
132 .map(|(k, v)| (k, filter_blocked_fields(v, blocked_fields)))
133 .collect();
134
135 JsonValue::Object(filtered)
136 },
137 JsonValue::Array(arr) => {
138 let filtered: Vec<JsonValue> = arr
140 .into_iter()
141 .map(|v| filter_blocked_fields(v, blocked_fields))
142 .collect();
143
144 JsonValue::Array(filtered)
145 },
146 other => other,
148 }
149}
150
151pub fn find_blocked_fields_in_output(
167 value: &JsonValue,
168 blocked_fields: &HashSet<String>,
169) -> Vec<String> {
170 if blocked_fields.is_empty() {
171 return Vec::new();
172 }
173
174 let mut violations = Vec::new();
175 find_blocked_fields_recursive(value, blocked_fields, "", &mut violations);
176 violations
177}
178
179fn find_blocked_fields_recursive(
181 value: &JsonValue,
182 blocked_fields: &HashSet<String>,
183 path: &str,
184 violations: &mut Vec<String>,
185) {
186 match value {
187 JsonValue::Object(map) => visit_object(map, blocked_fields, path, violations),
188 JsonValue::Array(arr) => visit_array(arr, blocked_fields, path, violations),
189 _ => {},
191 }
192}
193
194fn visit_object(
197 map: &serde_json::Map<String, JsonValue>,
198 blocked_fields: &HashSet<String>,
199 path: &str,
200 violations: &mut Vec<String>,
201) {
202 for (key, v) in map {
203 let key_path = join_field_path(path, key);
204 if blocked_fields.contains(key) {
205 violations.push(key_path.clone());
206 }
207 find_blocked_fields_recursive(v, blocked_fields, &key_path, violations);
208 }
209}
210
211fn visit_array(
213 arr: &[JsonValue],
214 blocked_fields: &HashSet<String>,
215 path: &str,
216 violations: &mut Vec<String>,
217) {
218 for (i, v) in arr.iter().enumerate() {
219 let elem_path = join_index_path(path, i);
220 find_blocked_fields_recursive(v, blocked_fields, &elem_path, violations);
221 }
222}
223
224fn join_field_path(path: &str, key: &str) -> String {
226 if path.is_empty() {
227 key.to_string()
228 } else {
229 format!("{}.{}", path, key)
230 }
231}
232
233fn join_index_path(path: &str, idx: usize) -> String {
235 if path.is_empty() {
236 format!("[{}]", idx)
237 } else {
238 format!("{}[{}]", path, idx)
239 }
240}
241
242#[derive(Debug, Clone, Serialize)]
244pub struct ExecutionPlan {
245 pub steps: Vec<PlanStep>,
247 pub metadata: PlanMetadata,
249}
250
251#[derive(Debug, Clone, Serialize)]
253pub struct PlanMetadata {
254 pub api_call_count: usize,
256 pub has_mutations: bool,
258 pub endpoints: Vec<String>,
260 pub methods_used: Vec<String>,
262}
263
264#[derive(Debug, Clone, Serialize)]
266pub enum PlanStep {
267 ApiCall {
269 result_var: String,
270 method: String,
271 path: PathTemplate,
272 body: Option<ValueExpr>,
273 },
274
275 Assign { var: String, expr: ValueExpr },
277
278 Conditional {
280 condition: ValueExpr,
281 then_steps: Vec<PlanStep>,
282 else_steps: Vec<PlanStep>,
283 },
284
285 BoundedLoop {
287 item_var: String,
288 collection: ValueExpr,
289 max_iterations: usize,
290 body: Vec<PlanStep>,
291 },
292
293 Return { value: ValueExpr },
295
296 TryCatch {
298 try_steps: Vec<PlanStep>,
299 catch_var: Option<String>,
300 catch_steps: Vec<PlanStep>,
301 finally_steps: Vec<PlanStep>,
302 },
303
304 ParallelApiCalls {
306 result_var: String,
307 calls: Vec<(String, String, PathTemplate, Option<ValueExpr>)>, },
309
310 Continue,
312
313 Break,
315
316 #[cfg(feature = "mcp-code-mode")]
318 McpCall {
319 result_var: String,
320 server_id: String,
321 tool_name: String,
322 args: Option<ValueExpr>,
323 },
324
325 SdkCall {
327 result_var: String,
328 operation: String, args: Option<ValueExpr>, },
331}
332
333#[derive(Debug, Clone, Serialize)]
335pub struct PathTemplate {
336 pub parts: Vec<PathPart>,
338}
339
340impl PathTemplate {
341 pub fn static_path(path: String) -> Self {
343 Self {
344 parts: vec![PathPart::Literal(path)],
345 }
346 }
347
348 pub fn is_dynamic(&self) -> bool {
350 self.parts
351 .iter()
352 .any(|p| matches!(p, PathPart::Variable(_) | PathPart::Expression(_)))
353 }
354}
355
356#[derive(Debug, Clone, Serialize)]
358pub enum PathPart {
359 Literal(String),
361 Variable(String),
363 Expression(ValueExpr),
365}
366
367#[derive(Debug, Clone, Serialize)]
369pub enum ValueExpr {
370 Literal(JsonValue),
372
373 Variable(String),
375
376 PropertyAccess {
378 object: Box<ValueExpr>,
379 property: String,
380 },
381
382 ArrayIndex {
384 array: Box<ValueExpr>,
385 index: Box<ValueExpr>,
386 },
387
388 ObjectLiteral { fields: Vec<ObjectField> },
390
391 ArrayLiteral { items: Vec<ValueExpr> },
393
394 ArrayMethod {
396 array: Box<ValueExpr>,
397 method: ArrayMethodCall,
398 },
399
400 NumberMethod {
402 number: Box<ValueExpr>,
403 method: NumberMethodCall,
404 },
405
406 BinaryOp {
408 left: Box<ValueExpr>,
409 op: BinaryOperator,
410 right: Box<ValueExpr>,
411 },
412
413 UnaryOp {
415 op: UnaryOperator,
416 operand: Box<ValueExpr>,
417 },
418
419 Ternary {
421 condition: Box<ValueExpr>,
422 consequent: Box<ValueExpr>,
423 alternate: Box<ValueExpr>,
424 },
425
426 OptionalChain {
428 object: Box<ValueExpr>,
429 property: String,
430 },
431
432 NullishCoalesce {
434 left: Box<ValueExpr>,
435 right: Box<ValueExpr>,
436 },
437
438 Await { expr: Box<ValueExpr> },
440
441 PromiseAll { items: Vec<ValueExpr> },
443
444 ApiCall {
446 method: String,
447 path: PathTemplate,
448 body: Option<Box<ValueExpr>>,
449 },
450
451 Block {
454 bindings: Vec<(String, ValueExpr)>,
456 result: Box<ValueExpr>,
458 },
459
460 #[cfg(feature = "mcp-code-mode")]
462 McpCall {
463 server_id: String,
464 tool_name: String,
465 args: Option<Box<ValueExpr>>,
466 },
467
468 SdkCall {
470 operation: String,
471 args: Option<Box<ValueExpr>>,
472 },
473
474 BuiltinCall {
476 func: BuiltinFunction,
477 args: Vec<ValueExpr>,
478 },
479}
480
481#[derive(Debug, Clone, Serialize)]
483pub enum ObjectField {
484 KeyValue { key: String, value: ValueExpr },
486 Spread { expr: ValueExpr },
488}
489
490#[derive(Debug, Clone, Serialize)]
492pub enum ArrayMethodCall {
493 Map {
495 item_var: String,
496 body: Box<ValueExpr>,
497 },
498 Filter {
500 item_var: String,
501 predicate: Box<ValueExpr>,
502 },
503 Find {
505 item_var: String,
506 predicate: Box<ValueExpr>,
507 },
508 Slice { start: usize, end: Option<usize> },
510 Length,
512 Some {
514 item_var: String,
515 predicate: Box<ValueExpr>,
516 },
517 Every {
519 item_var: String,
520 predicate: Box<ValueExpr>,
521 },
522 Reduce {
524 acc_var: String,
525 item_var: String,
526 body: Box<ValueExpr>,
527 initial: Box<ValueExpr>,
528 },
529 Push { item: Box<ValueExpr> },
531 Concat { other: Box<ValueExpr> },
533 Includes { item: Box<ValueExpr> },
535 IndexOf { item: Box<ValueExpr> },
537 Join { separator: Option<String> },
539 Reverse,
541 Sort {
543 comparator: Option<(String, String, Box<ValueExpr>)>,
544 },
545 Flat,
547 FlatMap {
549 item_var: String,
550 body: Box<ValueExpr>,
551 },
552 First,
554 Last,
556 ToLowerCase,
558 ToUpperCase,
560 StartsWith { search: Box<ValueExpr> },
562 EndsWith { search: Box<ValueExpr> },
564 Trim,
566 Replace {
568 search: Box<ValueExpr>,
569 replacement: Box<ValueExpr>,
570 },
571 Split { separator: Box<ValueExpr> },
573 Substring {
575 start: Box<ValueExpr>,
576 end: Option<Box<ValueExpr>>,
577 },
578 ToString,
580}
581
582#[derive(Debug, Clone, Serialize)]
584pub enum NumberMethodCall {
585 ToFixed { digits: usize },
587 ToString,
589}
590
591#[derive(Debug, Clone, Serialize)]
597pub enum BuiltinFunction {
598 ParseFloat,
600 ParseInt,
601 NumberCast,
602 MathAbs,
604 MathMax,
605 MathMin,
606 MathRound,
607 MathFloor,
608 MathCeil,
609 ObjectKeys,
611 ObjectValues,
612 ObjectEntries,
613}
614
615#[derive(Debug, Clone, Copy, Serialize)]
617pub enum BinaryOperator {
618 Add,
620 Sub,
621 Mul,
622 Div,
623 Mod,
624 BitwiseOr,
626 Eq,
628 NotEq,
629 StrictEq,
630 StrictNotEq,
631 Lt,
632 Lte,
633 Gt,
634 Gte,
635 And,
637 Or,
638 Concat,
640}
641
642#[derive(Debug, Clone, Copy, Serialize)]
644pub enum UnaryOperator {
645 Not,
646 Neg,
647 Plus,
648 TypeOf,
649}
650
651#[derive(Debug, thiserror::Error)]
653pub enum CompileError {
654 #[error("Unsupported statement type: {0}")]
655 UnsupportedStatement(String),
656
657 #[error("Unsupported expression type: {0}")]
658 UnsupportedExpression(String),
659
660 #[error("Invalid API call: {0}")]
661 InvalidApiCall(String),
662
663 #[error("Unbounded loop detected")]
664 UnboundedLoop,
665
666 #[error("Invalid path template: {0}")]
667 InvalidPath(String),
668
669 #[error("Too many API calls in plan: {count} (max: {max})")]
670 TooManyApiCalls { count: usize, max: usize },
671
672 #[error("Unsupported array method: {0}")]
673 UnsupportedArrayMethod(String),
674
675 #[error("Parse error: {0}")]
676 ParseError(String),
677
678 #[error("Missing variable name")]
679 MissingVariableName,
680}
681
682enum ExtractedCall {
687 Http {
689 method: String,
690 path: PathTemplate,
691 body: Option<ValueExpr>,
692 },
693 Sdk {
695 operation: String,
696 args: Option<ValueExpr>,
697 },
698}
699
700pub struct PlanCompiler {
702 api_call_count: usize,
703 endpoints: Vec<String>,
704 methods_used: Vec<String>,
705 has_mutations: bool,
706 sdk_operations: HashSet<String>,
708 destructure_counter: usize,
710}
711
712impl PlanCompiler {
713 pub fn new() -> Self {
715 Self::with_config(&ExecutionConfig::default())
716 }
717
718 pub fn with_config(_config: &ExecutionConfig) -> Self {
720 Self {
721 api_call_count: 0,
722 endpoints: Vec::new(),
723 methods_used: Vec::new(),
724 has_mutations: false,
725 sdk_operations: HashSet::new(),
726 destructure_counter: 0,
727 }
728 }
729
730 pub fn with_sdk_operations(mut self, operations: HashSet<String>) -> Self {
733 self.sdk_operations = operations;
734 self
735 }
736
737 pub fn compile_code(&mut self, code: &str) -> Result<ExecutionPlan, CompileError> {
741 let cm = SourceMap::default();
743 let fm = cm.new_source_file(FileName::Anon.into(), code.to_string());
744
745 let lexer = Lexer::new(
746 Syntax::Es(Default::default()),
747 EsVersion::Es2022,
748 StringInput::from(&*fm),
749 None,
750 );
751
752 let mut parser = Parser::new_from(lexer);
753
754 let module = parser.parse_module().map_err(|e| {
755 CompileError::ParseError(format!("JavaScript parse error: {:?}", e.into_kind()))
756 })?;
757
758 self.compile(&module)
760 }
761
762 pub fn compile(&mut self, module: &Module) -> Result<ExecutionPlan, CompileError> {
764 let mut steps = Vec::new();
765
766 for item in &module.body {
767 match item {
768 ModuleItem::Stmt(stmt) => {
769 self.compile_statement(stmt, &mut steps)?;
770 },
771 _ => {
772 return Err(CompileError::UnsupportedStatement(
773 "import/export not allowed".into(),
774 ));
775 },
776 }
777 }
778
779 self.methods_used.sort();
781 self.methods_used.dedup();
782
783 Ok(ExecutionPlan {
784 steps,
785 metadata: PlanMetadata {
786 api_call_count: self.api_call_count,
787 has_mutations: self.has_mutations,
788 endpoints: self.endpoints.clone(),
789 methods_used: self.methods_used.clone(),
790 },
791 })
792 }
793
794 fn compile_statement(
795 &mut self,
796 stmt: &Stmt,
797 steps: &mut Vec<PlanStep>,
798 ) -> Result<(), CompileError> {
799 match stmt {
800 Stmt::Decl(Decl::Var(var_decl)) => {
802 for decl in &var_decl.decls {
803 if let Some(init) = &decl.init {
804 match &decl.name {
805 Pat::Ident(ident) => {
806 let var_name = ident.id.sym.to_string();
807 self.compile_var_init(&var_name, init, steps)?;
808 },
809 Pat::Object(obj_pat) => {
810 self.compile_object_destructuring(obj_pat, init, steps)?;
811 },
812 Pat::Array(arr_pat) => {
813 self.compile_array_destructuring(arr_pat, init, steps)?;
814 },
815 _ => {
816 return Err(CompileError::UnsupportedExpression(
817 "complex destructuring pattern".into(),
818 ));
819 },
820 }
821 }
822 }
823 },
824
825 Stmt::Expr(expr_stmt) => {
827 if let Expr::Assign(assign) = expr_stmt.expr.as_ref() {
829 if assign.op == swc_ecma_ast::AssignOp::Assign {
830 if let Some(ident) = assign.left.as_ident() {
831 let var_name = ident.sym.to_string();
832 self.compile_var_init(&var_name, &assign.right, steps)?;
833 return Ok(());
834 }
835 }
836 }
837
838 let expr = self.compile_expr(&expr_stmt.expr)?;
839 match expr {
840 ValueExpr::ApiCall { method, path, body } => {
842 self.record_api_call(&method, &path);
843 steps.push(PlanStep::ApiCall {
844 result_var: "_".into(), method,
846 path,
847 body: body.map(|b| *b),
848 });
849 },
850 ValueExpr::SdkCall { operation, args } => {
852 steps.push(PlanStep::SdkCall {
853 result_var: "_".into(), operation,
855 args: args.map(|a| *a),
856 });
857 },
858 ValueExpr::ArrayMethod {
861 ref array,
862 ref method,
863 } if matches!(
864 method,
865 ArrayMethodCall::Push { .. } | ArrayMethodCall::Concat { .. }
866 ) =>
867 {
868 if let ValueExpr::Variable(var_name) = array.as_ref() {
869 steps.push(PlanStep::Assign {
870 var: var_name.clone(),
871 expr,
872 });
873 }
874 },
877 _ => {},
879 }
880 },
881
882 Stmt::If(if_stmt) => {
884 let condition = self.compile_expr(&if_stmt.test)?;
885 let mut then_steps = Vec::new();
886 self.compile_statement(&if_stmt.cons, &mut then_steps)?;
887
888 let mut else_steps = Vec::new();
889 if let Some(alt) = &if_stmt.alt {
890 self.compile_statement(alt, &mut else_steps)?;
891 }
892
893 steps.push(PlanStep::Conditional {
894 condition,
895 then_steps,
896 else_steps,
897 });
898 },
899
900 Stmt::ForOf(for_of) => {
902 let (item_var, destructure_bindings) = match &for_of.left {
903 ForHead::VarDecl(decl) => {
904 if let Some(first) = decl.decls.first() {
905 self.extract_loop_var(&first.name)?
906 } else {
907 return Err(CompileError::MissingVariableName);
908 }
909 },
910 ForHead::Pat(pat) => self.extract_loop_var(pat)?,
911 _ => return Err(CompileError::MissingVariableName),
912 };
913
914 let collection = self.compile_expr(&for_of.right)?;
915
916 let max_iterations = self.extract_bound(&collection).unwrap_or(100);
918
919 let mut body = destructure_bindings;
920 self.compile_statement(&for_of.body, &mut body)?;
921
922 steps.push(PlanStep::BoundedLoop {
923 item_var,
924 collection,
925 max_iterations,
926 body,
927 });
928 },
929
930 Stmt::Block(block) => {
932 for stmt in &block.stmts {
933 self.compile_statement(stmt, steps)?;
934 }
935 },
936
937 Stmt::Return(ret) => {
939 let value = if let Some(arg) = &ret.arg {
940 self.compile_expr(arg)?
941 } else {
942 ValueExpr::Literal(JsonValue::Null)
943 };
944 steps.push(PlanStep::Return { value });
945 },
946
947 Stmt::Empty(_) => {},
949
950 Stmt::Try(try_stmt) => {
952 let mut try_steps = Vec::new();
953 for stmt in &try_stmt.block.stmts {
954 self.compile_statement(stmt, &mut try_steps)?;
955 }
956
957 let (catch_var, catch_steps) = if let Some(handler) = &try_stmt.handler {
958 let var_name = handler.param.as_ref().map(|p| match p {
959 swc_ecma_ast::Pat::Ident(ident) => ident.sym.to_string(),
960 _ => "error".to_string(),
961 });
962 let mut catch_stmts = Vec::new();
963 for stmt in &handler.body.stmts {
964 self.compile_statement(stmt, &mut catch_stmts)?;
965 }
966 (var_name, catch_stmts)
967 } else {
968 (None, Vec::new())
969 };
970
971 let finally_steps = if let Some(finalizer) = &try_stmt.finalizer {
972 let mut finally_stmts = Vec::new();
973 for stmt in &finalizer.stmts {
974 self.compile_statement(stmt, &mut finally_stmts)?;
975 }
976 finally_stmts
977 } else {
978 Vec::new()
979 };
980
981 steps.push(PlanStep::TryCatch {
982 try_steps,
983 catch_var,
984 catch_steps,
985 finally_steps,
986 });
987 },
988
989 Stmt::Continue(_) => {
991 steps.push(PlanStep::Continue);
992 },
993
994 Stmt::Break(_) => {
996 steps.push(PlanStep::Break);
997 },
998
999 Stmt::Decl(decl) => {
1000 let msg = match decl {
1001 Decl::Fn(_) => "Function declarations are not supported. Use arrow functions inside array methods (.map, .filter) instead",
1002 Decl::Class(_) => "Class declarations are not supported",
1003 _ => "This declaration type is not supported",
1004 };
1005 return Err(CompileError::UnsupportedStatement(msg.into()));
1006 },
1007 Stmt::Switch(_) => {
1008 return Err(CompileError::UnsupportedStatement(
1009 "'switch' statements are not supported. Use if/else if/else instead".into(),
1010 ));
1011 },
1012 Stmt::Throw(_) => {
1013 return Err(CompileError::UnsupportedStatement(
1014 "'throw' statements are not supported. Use try/catch for error handling".into(),
1015 ));
1016 },
1017 Stmt::While(_) => {
1018 return Err(CompileError::UnsupportedStatement(
1019 "'while' loops are not supported. Use for-of with .slice() instead: for (const item of array.slice(0, N)) { }".into(),
1020 ));
1021 },
1022 Stmt::DoWhile(_) => {
1023 return Err(CompileError::UnsupportedStatement(
1024 "'do-while' loops are not supported. Use for-of with .slice() instead: for (const item of array.slice(0, N)) { }".into(),
1025 ));
1026 },
1027 Stmt::For(_) => {
1028 return Err(CompileError::UnsupportedStatement(
1029 "'for(;;)' loops are not supported. Use for-of with .slice() instead: for (const item of array.slice(0, N)) { }".into(),
1030 ));
1031 },
1032 Stmt::ForIn(_) => {
1033 return Err(CompileError::UnsupportedStatement(
1034 "'for-in' loops are not supported. Use for-of with .slice() instead".into(),
1035 ));
1036 },
1037 Stmt::Labeled(_) => {
1038 return Err(CompileError::UnsupportedStatement(
1039 "Labeled statements are not supported".into(),
1040 ));
1041 },
1042 Stmt::With(_) => {
1043 return Err(CompileError::UnsupportedStatement(
1044 "'with' statements are not supported".into(),
1045 ));
1046 },
1047 Stmt::Debugger(_) => {
1048 return Err(CompileError::UnsupportedStatement(
1049 "'debugger' statements are not supported".into(),
1050 ));
1051 },
1052 }
1053
1054 Ok(())
1055 }
1056
1057 fn compile_var_init(
1058 &mut self,
1059 var_name: &str,
1060 init: &Expr,
1061 steps: &mut Vec<PlanStep>,
1062 ) -> Result<(), CompileError> {
1063 if let Expr::Await(await_expr) = init {
1065 if let Some(extracted) = self.try_extract_api_call(&await_expr.arg)? {
1067 match extracted {
1068 ExtractedCall::Http { method, path, body } => {
1069 self.record_api_call(&method, &path);
1070 steps.push(PlanStep::ApiCall {
1071 result_var: var_name.into(),
1072 method,
1073 path,
1074 body,
1075 });
1076 },
1077 ExtractedCall::Sdk { operation, args } => {
1078 steps.push(PlanStep::SdkCall {
1079 result_var: var_name.into(),
1080 operation,
1081 args,
1082 });
1083 },
1084 }
1085 return Ok(());
1086 }
1087
1088 #[cfg(feature = "mcp-code-mode")]
1090 if let Some((server_id, tool_name, args)) =
1091 self.try_extract_mcp_call(&await_expr.arg)?
1092 {
1093 steps.push(PlanStep::McpCall {
1094 result_var: var_name.into(),
1095 server_id,
1096 tool_name,
1097 args,
1098 });
1099 return Ok(());
1100 }
1101
1102 if let Expr::Call(call) = await_expr.arg.as_ref() {
1104 let inner = self.compile_call(call)?;
1105 if let ValueExpr::PromiseAll { items } = inner {
1106 return self.compile_promise_all(var_name, items, steps);
1107 }
1108 }
1109 }
1110
1111 let expr = self.compile_expr(init)?;
1113 steps.push(PlanStep::Assign {
1114 var: var_name.into(),
1115 expr,
1116 });
1117 Ok(())
1118 }
1119
1120 fn compile_promise_all(
1123 &mut self,
1124 result_var: &str,
1125 items: Vec<ValueExpr>,
1126 steps: &mut Vec<PlanStep>,
1127 ) -> Result<(), CompileError> {
1128 let mut calls = Vec::new();
1129 let mut all_api_calls = true;
1130
1131 for (i, item) in items.iter().enumerate() {
1132 match item {
1133 ValueExpr::ApiCall { method, path, body } => {
1134 let temp_var = format!("__promise_all_{}_{}", result_var, i);
1135 calls.push((
1136 temp_var,
1137 method.clone(),
1138 path.clone(),
1139 body.as_ref().map(|b| *b.clone()),
1140 ));
1141 },
1142 _ => {
1143 all_api_calls = false;
1144 break;
1145 },
1146 }
1147 }
1148
1149 if all_api_calls && !calls.is_empty() {
1150 for (_, method, path, _) in &calls {
1152 self.record_api_call(method, path);
1153 }
1154 steps.push(PlanStep::ParallelApiCalls {
1155 result_var: result_var.into(),
1156 calls,
1157 });
1158 Ok(())
1159 } else {
1160 Err(CompileError::UnsupportedExpression(
1163 "Promise.all with non-API-call expressions".into(),
1164 ))
1165 }
1166 }
1167
1168 fn compile_expr(&mut self, expr: &Expr) -> Result<ValueExpr, CompileError> {
1169 match expr {
1170 Expr::Lit(lit) => Ok(ValueExpr::Literal(self.lit_to_json(lit))),
1172
1173 Expr::Ident(ident) => Ok(ValueExpr::Variable(ident.sym.to_string())),
1175
1176 Expr::Member(member) => {
1178 let object = Box::new(self.compile_expr(&member.obj)?);
1179
1180 if let MemberProp::Ident(prop) = &member.prop {
1182 let prop_name = prop.sym.to_string();
1183 if prop_name == "length" {
1184 return Ok(ValueExpr::ArrayMethod {
1185 array: object,
1186 method: ArrayMethodCall::Length,
1187 });
1188 }
1189 }
1190
1191 match &member.prop {
1192 MemberProp::Ident(ident) => Ok(ValueExpr::PropertyAccess {
1193 object,
1194 property: ident.sym.to_string(),
1195 }),
1196 MemberProp::Computed(computed) => {
1197 let index = Box::new(self.compile_expr(&computed.expr)?);
1198 Ok(ValueExpr::ArrayIndex {
1199 array: object,
1200 index,
1201 })
1202 }
1203 _ => Err(CompileError::UnsupportedExpression("private property".into())),
1204 }
1205 }
1206
1207 Expr::Call(call) => self.compile_call(call),
1209
1210 Expr::Object(obj) => {
1212 let mut fields = Vec::new();
1213 for prop in &obj.props {
1214 match prop {
1215 PropOrSpread::Prop(prop) => {
1216 if let Prop::KeyValue(kv) = prop.as_ref() {
1217 let key = self.prop_name_to_string(&kv.key)?;
1218 let value = self.compile_expr(&kv.value)?;
1219 fields.push(ObjectField::KeyValue { key, value });
1220 } else if let Prop::Shorthand(ident) = prop.as_ref() {
1221 let name = ident.sym.to_string();
1222 fields.push(ObjectField::KeyValue {
1223 key: name.clone(),
1224 value: ValueExpr::Variable(name),
1225 });
1226 }
1227 }
1228 PropOrSpread::Spread(spread) => {
1229 let expr = self.compile_expr(&spread.expr)?;
1230 fields.push(ObjectField::Spread { expr });
1231 }
1232 }
1233 }
1234 Ok(ValueExpr::ObjectLiteral { fields })
1235 }
1236
1237 Expr::Array(arr) => {
1239 let mut items = Vec::new();
1240 for elem in arr.elems.iter().flatten() {
1241 if elem.spread.is_some() {
1242 return Err(CompileError::UnsupportedExpression("spread".into()));
1243 }
1244 items.push(self.compile_expr(&elem.expr)?);
1245 }
1246 Ok(ValueExpr::ArrayLiteral { items })
1247 }
1248
1249 Expr::Tpl(tpl) => {
1251 let mut parts = Vec::new();
1254 for (i, quasi) in tpl.quasis.iter().enumerate() {
1255 let raw = quasi.raw.to_string();
1256 if !raw.is_empty() {
1257 parts.push(ValueExpr::Literal(JsonValue::String(raw)));
1258 }
1259 if i < tpl.exprs.len() {
1260 parts.push(self.compile_expr(&tpl.exprs[i])?);
1261 }
1262 }
1263
1264 if parts.len() == 1 {
1266 return Ok(parts.remove(0));
1267 }
1268
1269 let mut result = parts.remove(0);
1271 for part in parts {
1272 result = ValueExpr::BinaryOp {
1273 left: Box::new(result),
1274 op: BinaryOperator::Concat,
1275 right: Box::new(part),
1276 };
1277 }
1278 Ok(result)
1279 }
1280
1281 Expr::Bin(bin) => {
1283 let left = Box::new(self.compile_expr(&bin.left)?);
1284 let right = Box::new(self.compile_expr(&bin.right)?);
1285 let op = self.compile_bin_op(bin.op)?;
1286 Ok(ValueExpr::BinaryOp { left, op, right })
1287 }
1288
1289 Expr::Unary(unary) => {
1291 let operand = Box::new(self.compile_expr(&unary.arg)?);
1292 let op = match unary.op {
1293 UnaryOp::Bang => UnaryOperator::Not,
1294 UnaryOp::Minus => UnaryOperator::Neg,
1295 UnaryOp::TypeOf => UnaryOperator::TypeOf,
1296 UnaryOp::Plus => UnaryOperator::Plus,
1297 _ => return Err(CompileError::UnsupportedExpression("unary op".into())),
1298 };
1299 Ok(ValueExpr::UnaryOp { op, operand })
1300 }
1301
1302 Expr::Cond(cond) => {
1304 let condition = Box::new(self.compile_expr(&cond.test)?);
1305 let consequent = Box::new(self.compile_expr(&cond.cons)?);
1306 let alternate = Box::new(self.compile_expr(&cond.alt)?);
1307 Ok(ValueExpr::Ternary {
1308 condition,
1309 consequent,
1310 alternate,
1311 })
1312 }
1313
1314 Expr::Await(await_expr) => {
1316 if let Some(extracted) = self.try_extract_api_call(&await_expr.arg)? {
1318 return match extracted {
1319 ExtractedCall::Http { method, path, body } => {
1320 self.record_api_call(&method, &path);
1321 Ok(ValueExpr::ApiCall {
1322 method,
1323 path,
1324 body: body.map(Box::new),
1325 })
1326 }
1327 ExtractedCall::Sdk { operation, args } => {
1328 Ok(ValueExpr::SdkCall {
1329 operation,
1330 args: args.map(Box::new),
1331 })
1332 }
1333 };
1334 }
1335 #[cfg(feature = "mcp-code-mode")]
1337 if let Some((server_id, tool_name, args)) = self.try_extract_mcp_call(&await_expr.arg)? {
1338 return Ok(ValueExpr::McpCall {
1339 server_id,
1340 tool_name,
1341 args: args.map(Box::new),
1342 });
1343 }
1344 let inner = self.compile_expr(&await_expr.arg)?;
1346 Ok(ValueExpr::Await {
1347 expr: Box::new(inner),
1348 })
1349 }
1350
1351 Expr::Arrow(_) => {
1353 Err(CompileError::UnsupportedExpression(
1355 "arrow function outside array method".into(),
1356 ))
1357 }
1358
1359 Expr::Paren(paren) => self.compile_expr(&paren.expr),
1361
1362 Expr::OptChain(opt) => {
1364 match opt.base.as_ref() {
1365 OptChainBase::Member(member) => {
1366 let object = Box::new(self.compile_expr(&member.obj)?);
1367 if let MemberProp::Ident(ident) = &member.prop {
1368 Ok(ValueExpr::OptionalChain {
1369 object,
1370 property: ident.sym.to_string(),
1371 })
1372 } else {
1373 Err(CompileError::UnsupportedExpression("computed optional chain".into()))
1374 }
1375 }
1376 _ => Err(CompileError::UnsupportedExpression("optional call".into())),
1377 }
1378 }
1379
1380 Expr::This(_) => Err(CompileError::UnsupportedExpression(
1381 "'this' keyword is not supported".into(),
1382 )),
1383 Expr::Fn(_) => Err(CompileError::UnsupportedExpression(
1384 "Function expressions are not supported. Use arrow functions inside array methods (.map, .filter) instead".into(),
1385 )),
1386 Expr::Update(_) => Err(CompileError::UnsupportedExpression(
1387 "Increment/decrement operators (++, --) are not supported. Use 'x = x + 1' instead".into(),
1388 )),
1389 Expr::New(_) => Err(CompileError::UnsupportedExpression(
1390 "'new' keyword is not supported".into(),
1391 )),
1392 Expr::Seq(_) => Err(CompileError::UnsupportedExpression(
1393 "Sequence expressions (comma operator) are not supported. Use separate statements instead".into(),
1394 )),
1395 Expr::TaggedTpl(_) => Err(CompileError::UnsupportedExpression(
1396 "Tagged template literals are not supported. Use regular template literals instead".into(),
1397 )),
1398 Expr::Class(_) => Err(CompileError::UnsupportedExpression(
1399 "Class expressions are not supported".into(),
1400 )),
1401 Expr::Yield(_) => Err(CompileError::UnsupportedExpression(
1402 "Generator yield is not supported".into(),
1403 )),
1404 Expr::SuperProp(_) => Err(CompileError::UnsupportedExpression(
1405 "'super' is not supported".into(),
1406 )),
1407 Expr::Assign(_) => Err(CompileError::UnsupportedExpression(
1408 "Assignment expressions are not supported here. Use a separate variable declaration instead".into(),
1409 )),
1410 _ => Err(CompileError::UnsupportedExpression(
1411 "This expression type is not supported in the JavaScript subset".into(),
1412 )),
1413 }
1414 }
1415
1416 fn compile_call(&mut self, call: &CallExpr) -> Result<ValueExpr, CompileError> {
1417 if let Some(extracted) = self.try_extract_api_call(&Expr::Call(call.clone()))? {
1419 return match extracted {
1420 ExtractedCall::Http { method, path, body } => {
1421 self.record_api_call(&method, &path);
1422 Ok(ValueExpr::ApiCall {
1423 method,
1424 path,
1425 body: body.map(Box::new),
1426 })
1427 },
1428 ExtractedCall::Sdk { operation, args } => Ok(ValueExpr::SdkCall {
1429 operation,
1430 args: args.map(Box::new),
1431 }),
1432 };
1433 }
1434
1435 #[cfg(feature = "mcp-code-mode")]
1437 if let Some((server_id, tool_name, args)) =
1438 self.try_extract_mcp_call(&Expr::Call(call.clone()))?
1439 {
1440 return Ok(ValueExpr::McpCall {
1441 server_id,
1442 tool_name,
1443 args: args.map(Box::new),
1444 });
1445 }
1446
1447 if let Callee::Expr(callee) = &call.callee {
1449 if let Expr::Member(member) = callee.as_ref() {
1450 if let Expr::Ident(obj) = member.obj.as_ref() {
1451 if obj.sym.as_ref() == "Promise" {
1452 if let MemberProp::Ident(prop) = &member.prop {
1453 if prop.sym.as_ref() == "all" {
1454 if let Some(arg) = call.args.first() {
1455 if let Expr::Array(arr) = arg.expr.as_ref() {
1456 let mut items = Vec::new();
1457 for elem in arr.elems.iter().flatten() {
1458 items.push(self.compile_expr(&elem.expr)?);
1459 }
1460 return Ok(ValueExpr::PromiseAll { items });
1461 }
1462 }
1463 }
1464 }
1465 }
1466 }
1467 }
1468 }
1469
1470 if let Callee::Expr(callee) = &call.callee {
1472 if let Expr::Member(member) = callee.as_ref() {
1473 let array = Box::new(self.compile_expr(&member.obj)?);
1474
1475 if let MemberProp::Ident(method_ident) = &member.prop {
1476 let method_name = method_ident.sym.as_ref();
1477
1478 match method_name {
1479 "map" => {
1480 let (item_var, body) = self.extract_arrow_callback(call)?;
1481 return Ok(ValueExpr::ArrayMethod {
1482 array,
1483 method: ArrayMethodCall::Map {
1484 item_var,
1485 body: Box::new(body),
1486 },
1487 });
1488 },
1489 "filter" => {
1490 let (item_var, predicate) = self.extract_arrow_callback(call)?;
1491 return Ok(ValueExpr::ArrayMethod {
1492 array,
1493 method: ArrayMethodCall::Filter {
1494 item_var,
1495 predicate: Box::new(predicate),
1496 },
1497 });
1498 },
1499 "find" => {
1500 let (item_var, predicate) = self.extract_arrow_callback(call)?;
1501 return Ok(ValueExpr::ArrayMethod {
1502 array,
1503 method: ArrayMethodCall::Find {
1504 item_var,
1505 predicate: Box::new(predicate),
1506 },
1507 });
1508 },
1509 "some" => {
1510 let (item_var, predicate) = self.extract_arrow_callback(call)?;
1511 return Ok(ValueExpr::ArrayMethod {
1512 array,
1513 method: ArrayMethodCall::Some {
1514 item_var,
1515 predicate: Box::new(predicate),
1516 },
1517 });
1518 },
1519 "every" => {
1520 let (item_var, predicate) = self.extract_arrow_callback(call)?;
1521 return Ok(ValueExpr::ArrayMethod {
1522 array,
1523 method: ArrayMethodCall::Every {
1524 item_var,
1525 predicate: Box::new(predicate),
1526 },
1527 });
1528 },
1529 "flatMap" => {
1530 let (item_var, body) = self.extract_arrow_callback(call)?;
1531 return Ok(ValueExpr::ArrayMethod {
1532 array,
1533 method: ArrayMethodCall::FlatMap {
1534 item_var,
1535 body: Box::new(body),
1536 },
1537 });
1538 },
1539 "slice" => {
1540 let start = self.extract_number_arg(call, 0)?.unwrap_or(0) as usize;
1541 let end = self.extract_number_arg(call, 1)?.map(|n| n as usize);
1542 return Ok(ValueExpr::ArrayMethod {
1543 array,
1544 method: ArrayMethodCall::Slice { start, end },
1545 });
1546 },
1547 "push" => {
1548 if let Some(arg) = call.args.first() {
1549 let item = Box::new(self.compile_expr(&arg.expr)?);
1550 return Ok(ValueExpr::ArrayMethod {
1551 array,
1552 method: ArrayMethodCall::Push { item },
1553 });
1554 }
1555 },
1556 "concat" => {
1557 if let Some(arg) = call.args.first() {
1558 let other = Box::new(self.compile_expr(&arg.expr)?);
1559 return Ok(ValueExpr::ArrayMethod {
1560 array,
1561 method: ArrayMethodCall::Concat { other },
1562 });
1563 }
1564 },
1565 "includes" => {
1566 if let Some(arg) = call.args.first() {
1567 let item = Box::new(self.compile_expr(&arg.expr)?);
1568 return Ok(ValueExpr::ArrayMethod {
1569 array,
1570 method: ArrayMethodCall::Includes { item },
1571 });
1572 }
1573 },
1574 "indexOf" => {
1575 if let Some(arg) = call.args.first() {
1576 let item = Box::new(self.compile_expr(&arg.expr)?);
1577 return Ok(ValueExpr::ArrayMethod {
1578 array,
1579 method: ArrayMethodCall::IndexOf { item },
1580 });
1581 }
1582 },
1583 "join" => {
1584 let separator = if let Some(arg) = call.args.first() {
1585 if let Expr::Lit(Lit::Str(s)) = arg.expr.as_ref() {
1586 Some(s.value.to_string_lossy().into_owned())
1587 } else {
1588 None
1589 }
1590 } else {
1591 None
1592 };
1593 return Ok(ValueExpr::ArrayMethod {
1594 array,
1595 method: ArrayMethodCall::Join { separator },
1596 });
1597 },
1598 "reverse" => {
1599 return Ok(ValueExpr::ArrayMethod {
1600 array,
1601 method: ArrayMethodCall::Reverse,
1602 });
1603 },
1604 "sort" => {
1605 let comparator = if !call.args.is_empty() {
1606 let (a_var, b_var, body) = self.extract_reduce_callback(call)?;
1607 Some((a_var, b_var, Box::new(body)))
1608 } else {
1609 None
1610 };
1611 return Ok(ValueExpr::ArrayMethod {
1612 array,
1613 method: ArrayMethodCall::Sort { comparator },
1614 });
1615 },
1616 "flat" => {
1617 return Ok(ValueExpr::ArrayMethod {
1618 array,
1619 method: ArrayMethodCall::Flat,
1620 });
1621 },
1622 "at" => {
1623 if let Some(n) = self.extract_number_arg(call, 0)? {
1624 if n == 0 {
1625 return Ok(ValueExpr::ArrayMethod {
1626 array,
1627 method: ArrayMethodCall::First,
1628 });
1629 } else if n == -1 {
1630 return Ok(ValueExpr::ArrayMethod {
1631 array,
1632 method: ArrayMethodCall::Last,
1633 });
1634 }
1635 }
1636 },
1637 "reduce" if call.args.len() >= 2 => {
1639 let (acc_var, item_var, body) = self.extract_reduce_callback(call)?;
1640 let initial = Box::new(self.compile_expr(&call.args[1].expr)?);
1641 return Ok(ValueExpr::ArrayMethod {
1642 array,
1643 method: ArrayMethodCall::Reduce {
1644 acc_var,
1645 item_var,
1646 body: Box::new(body),
1647 initial,
1648 },
1649 });
1650 },
1651 "toFixed" => {
1652 let digits = self.extract_number_arg(call, 0)?.unwrap_or(0) as usize;
1654 return Ok(ValueExpr::NumberMethod {
1655 number: array, method: NumberMethodCall::ToFixed { digits },
1657 });
1658 },
1659 "toLowerCase" => {
1660 return Ok(ValueExpr::ArrayMethod {
1661 array,
1662 method: ArrayMethodCall::ToLowerCase,
1663 });
1664 },
1665 "toUpperCase" => {
1666 return Ok(ValueExpr::ArrayMethod {
1667 array,
1668 method: ArrayMethodCall::ToUpperCase,
1669 });
1670 },
1671 "startsWith" => {
1672 let arg = call.args.first().ok_or_else(|| {
1673 CompileError::UnsupportedExpression(
1674 "startsWith() requires a search argument".into(),
1675 )
1676 })?;
1677 let search = Box::new(self.compile_expr(&arg.expr)?);
1678 return Ok(ValueExpr::ArrayMethod {
1679 array,
1680 method: ArrayMethodCall::StartsWith { search },
1681 });
1682 },
1683 "endsWith" => {
1684 let arg = call.args.first().ok_or_else(|| {
1685 CompileError::UnsupportedExpression(
1686 "endsWith() requires a search argument".into(),
1687 )
1688 })?;
1689 let search = Box::new(self.compile_expr(&arg.expr)?);
1690 return Ok(ValueExpr::ArrayMethod {
1691 array,
1692 method: ArrayMethodCall::EndsWith { search },
1693 });
1694 },
1695 "trim" => {
1696 return Ok(ValueExpr::ArrayMethod {
1697 array,
1698 method: ArrayMethodCall::Trim,
1699 });
1700 },
1701 "replace" => {
1702 if call.args.len() < 2 {
1703 return Err(CompileError::UnsupportedExpression(
1704 "replace() requires search and replacement arguments".into(),
1705 ));
1706 }
1707 let search = Box::new(self.compile_expr(&call.args[0].expr)?);
1708 let replacement = Box::new(self.compile_expr(&call.args[1].expr)?);
1709 return Ok(ValueExpr::ArrayMethod {
1710 array,
1711 method: ArrayMethodCall::Replace {
1712 search,
1713 replacement,
1714 },
1715 });
1716 },
1717 "split" => {
1718 let arg = call.args.first().ok_or_else(|| {
1719 CompileError::UnsupportedExpression(
1720 "split() requires a separator argument".into(),
1721 )
1722 })?;
1723 let separator = Box::new(self.compile_expr(&arg.expr)?);
1724 return Ok(ValueExpr::ArrayMethod {
1725 array,
1726 method: ArrayMethodCall::Split { separator },
1727 });
1728 },
1729 "substring" => {
1730 let arg = call.args.first().ok_or_else(|| {
1731 CompileError::UnsupportedExpression(
1732 "substring() requires a start argument".into(),
1733 )
1734 })?;
1735 let start = Box::new(self.compile_expr(&arg.expr)?);
1736 let end = if call.args.len() >= 2 {
1737 Some(Box::new(self.compile_expr(&call.args[1].expr)?))
1738 } else {
1739 None
1740 };
1741 return Ok(ValueExpr::ArrayMethod {
1742 array,
1743 method: ArrayMethodCall::Substring { start, end },
1744 });
1745 },
1746 "toString" => {
1747 return Ok(ValueExpr::ArrayMethod {
1748 array,
1749 method: ArrayMethodCall::ToString,
1750 });
1751 },
1752 _ => {},
1753 }
1754 }
1755 }
1756 }
1757
1758 if let Callee::Expr(callee) = &call.callee {
1760 if let Expr::Ident(ident) = callee.as_ref() {
1761 let func = match ident.sym.as_ref() {
1762 "parseFloat" => Some(BuiltinFunction::ParseFloat),
1763 "parseInt" => Some(BuiltinFunction::ParseInt),
1764 "Number" => Some(BuiltinFunction::NumberCast),
1765 _ => None,
1766 };
1767 if let Some(func) = func {
1768 let args = call
1769 .args
1770 .iter()
1771 .map(|a| self.compile_expr(&a.expr))
1772 .collect::<Result<Vec<_>, _>>()?;
1773 return Ok(ValueExpr::BuiltinCall { func, args });
1774 }
1775 }
1776
1777 if let Expr::Member(member) = callee.as_ref() {
1779 if let Expr::Ident(obj) = member.obj.as_ref() {
1780 if let MemberProp::Ident(prop) = &member.prop {
1781 let func = match (obj.sym.as_ref(), prop.sym.as_ref()) {
1782 ("Math", "abs") => Some(BuiltinFunction::MathAbs),
1783 ("Math", "max") => Some(BuiltinFunction::MathMax),
1784 ("Math", "min") => Some(BuiltinFunction::MathMin),
1785 ("Math", "round") => Some(BuiltinFunction::MathRound),
1786 ("Math", "floor") => Some(BuiltinFunction::MathFloor),
1787 ("Math", "ceil") => Some(BuiltinFunction::MathCeil),
1788 ("Object", "keys") => Some(BuiltinFunction::ObjectKeys),
1789 ("Object", "values") => Some(BuiltinFunction::ObjectValues),
1790 ("Object", "entries") => Some(BuiltinFunction::ObjectEntries),
1791 _ => None,
1792 };
1793 if let Some(func) = func {
1794 let args = call
1795 .args
1796 .iter()
1797 .map(|a| self.compile_expr(&a.expr))
1798 .collect::<Result<Vec<_>, _>>()?;
1799 return Ok(ValueExpr::BuiltinCall { func, args });
1800 }
1801 }
1802 }
1803 }
1804 }
1805
1806 Err(CompileError::UnsupportedExpression("function call".into()))
1807 }
1808
1809 fn try_extract_api_call(&mut self, expr: &Expr) -> Result<Option<ExtractedCall>, CompileError> {
1810 let call = match expr {
1811 Expr::Call(c) => c,
1812 _ => return Ok(None),
1813 };
1814
1815 if let Callee::Expr(callee) = &call.callee {
1816 if let Expr::Member(member) = callee.as_ref() {
1817 if let Expr::Ident(obj) = member.obj.as_ref() {
1818 if obj.sym.as_ref() == "api" {
1819 if let MemberProp::Ident(method_ident) = &member.prop {
1820 let method_name = method_ident.sym.as_ref();
1821
1822 if !self.sdk_operations.is_empty() {
1823 if !self.sdk_operations.contains(method_name) {
1825 return Err(CompileError::InvalidApiCall(format!(
1826 "Unknown SDK operation: api.{}(). Check the code mode schema resource for available operations.",
1827 method_name
1828 )));
1829 }
1830 let args = if let Some(arg) = call.args.first() {
1831 Some(self.compile_expr(&arg.expr)?)
1832 } else {
1833 None
1834 };
1835 self.api_call_count += 1;
1836 let op_endpoint = format!("sdk:{}", method_name);
1837 if !self.endpoints.contains(&op_endpoint) {
1838 self.endpoints.push(op_endpoint);
1839 }
1840 if !self.methods_used.contains(&method_name.to_string()) {
1841 self.methods_used.push(method_name.to_string());
1842 }
1843 return Ok(Some(ExtractedCall::Sdk {
1844 operation: method_name.to_string(),
1845 args,
1846 }));
1847 }
1848
1849 if HttpMethod::from_str(method_name).is_none() {
1851 return Err(CompileError::InvalidApiCall(format!(
1852 "Unknown method: api.{}",
1853 method_name
1854 )));
1855 }
1856
1857 let path = if let Some(arg) = call.args.first() {
1859 self.extract_path_template(&arg.expr)?
1860 } else {
1861 return Err(CompileError::InvalidApiCall(
1862 "API call requires path".into(),
1863 ));
1864 };
1865
1866 let body = if let Some(arg) = call.args.get(1) {
1868 Some(self.compile_expr(&arg.expr)?)
1869 } else {
1870 None
1871 };
1872
1873 return Ok(Some(ExtractedCall::Http {
1874 method: method_name.to_uppercase(),
1875 path,
1876 body,
1877 }));
1878 }
1879 }
1880 }
1881 }
1882 }
1883
1884 Ok(None)
1885 }
1886
1887 #[cfg(feature = "mcp-code-mode")]
1891 fn try_extract_mcp_call(
1892 &mut self,
1893 expr: &Expr,
1894 ) -> Result<Option<(String, String, Option<ValueExpr>)>, CompileError> {
1895 let call = match expr {
1896 Expr::Call(c) => c,
1897 _ => return Ok(None),
1898 };
1899
1900 if let Callee::Expr(callee) = &call.callee {
1901 if let Expr::Member(member) = callee.as_ref() {
1902 if let Expr::Ident(obj) = member.obj.as_ref() {
1903 if obj.sym.as_ref() == "mcp" {
1904 if let MemberProp::Ident(method_ident) = &member.prop {
1905 if method_ident.sym.as_ref() == "call" {
1906 let server_id = call.args.first()
1908 .and_then(|a| {
1909 if let Expr::Lit(Lit::Str(s)) = a.expr.as_ref() {
1910 Some(s.value.to_string_lossy().into_owned())
1911 } else {
1912 None
1913 }
1914 })
1915 .ok_or_else(|| CompileError::UnsupportedExpression(
1916 "mcp.call() first argument must be a string literal (server_id)".into(),
1917 ))?;
1918
1919 let tool_name = call.args.get(1)
1921 .and_then(|a| {
1922 if let Expr::Lit(Lit::Str(s)) = a.expr.as_ref() {
1923 Some(s.value.to_string_lossy().into_owned())
1924 } else {
1925 None
1926 }
1927 })
1928 .ok_or_else(|| CompileError::UnsupportedExpression(
1929 "mcp.call() second argument must be a string literal (tool_name)".into(),
1930 ))?;
1931
1932 let args = call
1934 .args
1935 .get(2)
1936 .map(|a| self.compile_expr(&a.expr))
1937 .transpose()?;
1938
1939 return Ok(Some((server_id, tool_name, args)));
1940 }
1941 }
1942 }
1943 }
1944 }
1945 }
1946
1947 Ok(None)
1948 }
1949
1950 fn extract_path_template(&mut self, expr: &Expr) -> Result<PathTemplate, CompileError> {
1951 match expr {
1952 Expr::Lit(Lit::Str(s)) => Ok(PathTemplate::static_path(
1954 s.value.to_string_lossy().into_owned(),
1955 )),
1956
1957 Expr::Tpl(tpl) => {
1959 let mut parts = Vec::new();
1960 for (i, quasi) in tpl.quasis.iter().enumerate() {
1961 let raw = quasi.raw.to_string();
1962 if !raw.is_empty() {
1963 parts.push(PathPart::Literal(raw));
1964 }
1965 if i < tpl.exprs.len() {
1966 if let Expr::Ident(ident) = tpl.exprs[i].as_ref() {
1968 parts.push(PathPart::Variable(ident.sym.to_string()));
1969 } else {
1970 let expr = self.compile_expr(&tpl.exprs[i])?;
1972 parts.push(PathPart::Expression(expr));
1973 }
1974 }
1975 }
1976 Ok(PathTemplate { parts })
1977 },
1978
1979 _ => Err(CompileError::InvalidPath(
1980 "Path must be a string or template literal".into(),
1981 )),
1982 }
1983 }
1984
1985 fn extract_arrow_callback(
1986 &mut self,
1987 call: &CallExpr,
1988 ) -> Result<(String, ValueExpr), CompileError> {
1989 let arg = call
1990 .args
1991 .first()
1992 .ok_or_else(|| CompileError::UnsupportedExpression("missing callback".into()))?;
1993
1994 if let Expr::Arrow(arrow) = arg.expr.as_ref() {
1995 let param_name = if let Some(Pat::Ident(ident)) = arrow.params.first() {
1997 ident.id.sym.to_string()
1998 } else {
1999 return Err(CompileError::UnsupportedExpression(
2000 "complex callback parameter".into(),
2001 ));
2002 };
2003
2004 let body = match &*arrow.body {
2006 BlockStmtOrExpr::Expr(expr) => self.compile_expr(expr)?,
2007 BlockStmtOrExpr::BlockStmt(block) => {
2008 let mut bindings: Vec<(String, ValueExpr)> = Vec::new();
2010 let mut return_expr: Option<ValueExpr> = None;
2011
2012 for stmt in &block.stmts {
2013 match stmt {
2014 Stmt::Decl(Decl::Var(var_decl)) => {
2016 for decl in &var_decl.decls {
2017 let var_name = self.get_var_name(&decl.name)?;
2018 if let Some(init) = &decl.init {
2019 let expr = self.compile_expr(init)?;
2020 bindings.push((var_name, expr));
2021 }
2022 }
2023 },
2024 Stmt::Return(ret) => {
2026 if let Some(arg) = &ret.arg {
2027 return_expr = Some(self.compile_expr(arg)?);
2028 }
2029 break; },
2031 Stmt::Expr(_) => {
2033 },
2035 _ => {},
2036 }
2037 }
2038
2039 match return_expr {
2040 Some(result) if bindings.is_empty() => result,
2041 Some(result) => ValueExpr::Block {
2042 bindings,
2043 result: Box::new(result),
2044 },
2045 None => {
2046 return Err(CompileError::UnsupportedExpression(
2047 "callback block without return".into(),
2048 ));
2049 },
2050 }
2051 },
2052 };
2053
2054 Ok((param_name, body))
2055 } else {
2056 Err(CompileError::UnsupportedExpression(
2057 "callback must be arrow function".into(),
2058 ))
2059 }
2060 }
2061
2062 fn extract_reduce_callback(
2064 &mut self,
2065 call: &CallExpr,
2066 ) -> Result<(String, String, ValueExpr), CompileError> {
2067 let arg = call
2068 .args
2069 .first()
2070 .ok_or_else(|| CompileError::UnsupportedExpression("missing callback".into()))?;
2071
2072 if let Expr::Arrow(arrow) = arg.expr.as_ref() {
2073 if arrow.params.len() < 2 {
2075 return Err(CompileError::UnsupportedExpression(
2076 "reduce callback must have 2 parameters".into(),
2077 ));
2078 }
2079
2080 let acc_name = if let Pat::Ident(ident) = &arrow.params[0] {
2081 ident.id.sym.to_string()
2082 } else {
2083 return Err(CompileError::UnsupportedExpression(
2084 "complex callback parameter".into(),
2085 ));
2086 };
2087
2088 let item_name = if let Pat::Ident(ident) = &arrow.params[1] {
2089 ident.id.sym.to_string()
2090 } else {
2091 return Err(CompileError::UnsupportedExpression(
2092 "complex callback parameter".into(),
2093 ));
2094 };
2095
2096 let body = match &*arrow.body {
2098 BlockStmtOrExpr::Expr(expr) => self.compile_expr(expr)?,
2099 BlockStmtOrExpr::BlockStmt(block) => {
2100 for stmt in &block.stmts {
2102 if let Stmt::Return(ret) = stmt {
2103 if let Some(arg) = &ret.arg {
2104 return Ok((acc_name, item_name, self.compile_expr(arg)?));
2105 }
2106 }
2107 }
2108 return Err(CompileError::UnsupportedExpression(
2109 "callback block without return".into(),
2110 ));
2111 },
2112 };
2113
2114 Ok((acc_name, item_name, body))
2115 } else {
2116 Err(CompileError::UnsupportedExpression(
2117 "callback must be arrow function".into(),
2118 ))
2119 }
2120 }
2121
2122 fn extract_number_arg(
2123 &self,
2124 call: &CallExpr,
2125 index: usize,
2126 ) -> Result<Option<i64>, CompileError> {
2127 if let Some(arg) = call.args.get(index) {
2128 if let Expr::Lit(Lit::Num(n)) = arg.expr.as_ref() {
2129 return Ok(Some(n.value as i64));
2130 }
2131 if let Expr::Unary(unary) = arg.expr.as_ref() {
2132 if unary.op == UnaryOp::Minus {
2133 if let Expr::Lit(Lit::Num(n)) = unary.arg.as_ref() {
2134 return Ok(Some(-(n.value as i64)));
2135 }
2136 }
2137 }
2138 }
2139 Ok(None)
2140 }
2141
2142 fn extract_bound(&self, expr: &ValueExpr) -> Option<usize> {
2143 if let ValueExpr::ArrayMethod {
2144 method: ArrayMethodCall::Slice { end, .. },
2145 ..
2146 } = expr
2147 {
2148 return *end;
2149 }
2150 None
2151 }
2152
2153 fn get_var_name(&self, pat: &Pat) -> Result<String, CompileError> {
2154 match pat {
2155 Pat::Ident(ident) => Ok(ident.id.sym.to_string()),
2156 _ => Err(CompileError::UnsupportedExpression(
2157 "complex destructuring".into(),
2158 )),
2159 }
2160 }
2161
2162 fn next_temp_var(&mut self) -> String {
2164 let name = format!("__destructure_{}", self.destructure_counter);
2165 self.destructure_counter += 1;
2166 name
2167 }
2168
2169 fn extract_loop_var(&mut self, pat: &Pat) -> Result<(String, Vec<PlanStep>), CompileError> {
2172 match pat {
2173 Pat::Ident(ident) => Ok((ident.id.sym.to_string(), Vec::new())),
2174 Pat::Object(obj_pat) => {
2175 let temp_var = self.next_temp_var();
2176 let bindings = Self::extract_object_bindings(obj_pat)?;
2177 let steps = bindings
2178 .into_iter()
2179 .map(|(var_name, property)| PlanStep::Assign {
2180 var: var_name,
2181 expr: ValueExpr::PropertyAccess {
2182 object: Box::new(ValueExpr::Variable(temp_var.clone())),
2183 property,
2184 },
2185 })
2186 .collect();
2187 Ok((temp_var, steps))
2188 },
2189 Pat::Array(arr_pat) => {
2190 let temp_var = self.next_temp_var();
2191 let mut steps = Vec::new();
2192 for (i, elem) in arr_pat.elems.iter().enumerate() {
2193 if let Some(p) = elem {
2194 let var_name = self.get_var_name(p)?;
2195 steps.push(PlanStep::Assign {
2196 var: var_name,
2197 expr: ValueExpr::ArrayIndex {
2198 array: Box::new(ValueExpr::Variable(temp_var.clone())),
2199 index: Box::new(ValueExpr::Literal(JsonValue::Number(
2200 (i as i64).into(),
2201 ))),
2202 },
2203 });
2204 }
2205 }
2206 Ok((temp_var, steps))
2207 },
2208 _ => Err(CompileError::UnsupportedExpression(
2209 "complex loop variable pattern".into(),
2210 )),
2211 }
2212 }
2213
2214 fn extract_object_bindings(obj_pat: &ObjectPat) -> Result<Vec<(String, String)>, CompileError> {
2218 let mut bindings = Vec::new();
2219 for prop in &obj_pat.props {
2220 match prop {
2221 ObjectPatProp::Assign(assign) => {
2222 if assign.value.is_some() {
2224 return Err(CompileError::UnsupportedExpression(
2225 "default values in destructuring".into(),
2226 ));
2227 }
2228 let name = assign.key.sym.to_string();
2229 bindings.push((name.clone(), name));
2230 },
2231 ObjectPatProp::KeyValue(kv) => {
2232 let key = match &kv.key {
2234 PropName::Ident(ident) => ident.sym.to_string(),
2235 PropName::Str(s) => s.value.to_string_lossy().into_owned(),
2236 _ => {
2237 return Err(CompileError::UnsupportedExpression(
2238 "computed destructuring key".into(),
2239 ));
2240 },
2241 };
2242 let var_name = match kv.value.as_ref() {
2243 Pat::Ident(ident) => ident.id.sym.to_string(),
2244 _ => {
2245 return Err(CompileError::UnsupportedExpression(
2246 "nested destructuring".into(),
2247 ));
2248 },
2249 };
2250 bindings.push((var_name, key));
2251 },
2252 ObjectPatProp::Rest(_) => {
2253 return Err(CompileError::UnsupportedExpression(
2254 "rest pattern in destructuring".into(),
2255 ));
2256 },
2257 }
2258 }
2259 Ok(bindings)
2260 }
2261
2262 fn compile_object_destructuring(
2265 &mut self,
2266 obj_pat: &ObjectPat,
2267 init: &Expr,
2268 steps: &mut Vec<PlanStep>,
2269 ) -> Result<(), CompileError> {
2270 let bindings = Self::extract_object_bindings(obj_pat)?;
2271 let temp_var = self.next_temp_var();
2272
2273 self.compile_var_init(&temp_var, init, steps)?;
2275
2276 for (var_name, property) in bindings {
2278 steps.push(PlanStep::Assign {
2279 var: var_name,
2280 expr: ValueExpr::PropertyAccess {
2281 object: Box::new(ValueExpr::Variable(temp_var.clone())),
2282 property,
2283 },
2284 });
2285 }
2286 Ok(())
2287 }
2288
2289 fn compile_array_destructuring(
2292 &mut self,
2293 arr_pat: &ArrayPat,
2294 init: &Expr,
2295 steps: &mut Vec<PlanStep>,
2296 ) -> Result<(), CompileError> {
2297 let temp_var = self.next_temp_var();
2298
2299 self.compile_var_init(&temp_var, init, steps)?;
2301
2302 for (i, elem) in arr_pat.elems.iter().enumerate() {
2304 if let Some(pat) = elem {
2305 let var_name = self.get_var_name(pat)?;
2306 steps.push(PlanStep::Assign {
2307 var: var_name,
2308 expr: ValueExpr::ArrayIndex {
2309 array: Box::new(ValueExpr::Variable(temp_var.clone())),
2310 index: Box::new(ValueExpr::Literal(JsonValue::Number((i as i64).into()))),
2311 },
2312 });
2313 }
2314 }
2316 Ok(())
2317 }
2318
2319 fn lit_to_json(&self, lit: &Lit) -> JsonValue {
2320 match lit {
2321 Lit::Str(s) => JsonValue::String(s.value.to_string_lossy().into_owned()),
2322 Lit::Num(n) => {
2323 if n.value.fract() == 0.0 {
2324 JsonValue::Number((n.value as i64).into())
2325 } else {
2326 serde_json::Number::from_f64(n.value)
2327 .map(JsonValue::Number)
2328 .unwrap_or(JsonValue::Null)
2329 }
2330 },
2331 Lit::Bool(b) => JsonValue::Bool(b.value),
2332 Lit::Null(_) => JsonValue::Null,
2333 _ => JsonValue::Null,
2334 }
2335 }
2336
2337 fn prop_name_to_string(&self, prop: &PropName) -> Result<String, CompileError> {
2338 match prop {
2339 PropName::Ident(ident) => Ok(ident.sym.to_string()),
2340 PropName::Str(s) => Ok(s.value.to_string_lossy().into_owned()),
2341 PropName::Num(n) => Ok(n.value.to_string()),
2342 _ => Err(CompileError::UnsupportedExpression(
2343 "computed property".into(),
2344 )),
2345 }
2346 }
2347
2348 fn compile_bin_op(&self, op: BinaryOp) -> Result<BinaryOperator, CompileError> {
2349 match op {
2350 BinaryOp::Add => Ok(BinaryOperator::Add),
2351 BinaryOp::Sub => Ok(BinaryOperator::Sub),
2352 BinaryOp::Mul => Ok(BinaryOperator::Mul),
2353 BinaryOp::Div => Ok(BinaryOperator::Div),
2354 BinaryOp::Mod => Ok(BinaryOperator::Mod),
2355 BinaryOp::BitOr => Ok(BinaryOperator::BitwiseOr),
2356 BinaryOp::EqEq => Ok(BinaryOperator::Eq),
2357 BinaryOp::NotEq => Ok(BinaryOperator::NotEq),
2358 BinaryOp::EqEqEq => Ok(BinaryOperator::StrictEq),
2359 BinaryOp::NotEqEq => Ok(BinaryOperator::StrictNotEq),
2360 BinaryOp::Lt => Ok(BinaryOperator::Lt),
2361 BinaryOp::LtEq => Ok(BinaryOperator::Lte),
2362 BinaryOp::Gt => Ok(BinaryOperator::Gt),
2363 BinaryOp::GtEq => Ok(BinaryOperator::Gte),
2364 BinaryOp::LogicalAnd => Ok(BinaryOperator::And),
2365 BinaryOp::LogicalOr => Ok(BinaryOperator::Or),
2366 BinaryOp::NullishCoalescing => {
2367 Err(CompileError::UnsupportedExpression(
2369 "nullish coalescing".into(),
2370 ))
2371 },
2372 _ => Err(CompileError::UnsupportedExpression(format!(
2373 "binary operator {:?}",
2374 op
2375 ))),
2376 }
2377 }
2378
2379 fn record_api_call(&mut self, method: &str, path: &PathTemplate) {
2380 self.api_call_count += 1;
2381
2382 if !self.methods_used.contains(&method.to_string()) {
2384 self.methods_used.push(method.to_string());
2385 }
2386
2387 if method != "GET" && method != "HEAD" && method != "OPTIONS" {
2389 self.has_mutations = true;
2390 }
2391
2392 let endpoint = if !path.is_dynamic() {
2394 path.parts
2395 .iter()
2396 .filter_map(|p| match p {
2397 PathPart::Literal(s) => Some(s.clone()),
2398 _ => None,
2399 })
2400 .collect::<String>()
2401 } else {
2402 "{dynamic}".to_string()
2403 };
2404 if !self.endpoints.contains(&endpoint) {
2405 self.endpoints.push(endpoint);
2406 }
2407 }
2408}
2409
2410impl Default for PlanCompiler {
2411 fn default() -> Self {
2412 Self::new()
2413 }
2414}
2415
2416#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2463pub struct ResolvedPath<'a>(&'a str);
2464
2465impl<'a> ResolvedPath<'a> {
2466 pub fn from_checked(path: &'a str) -> Result<Self, crate::PlaceholderRefusal> {
2516 crate::validate_resolved_target(path)?;
2517 Ok(Self(path))
2518 }
2519
2520 #[must_use]
2522 pub fn as_str(&self) -> &'a str {
2523 self.0
2524 }
2525}
2526
2527impl std::fmt::Display for ResolvedPath<'_> {
2528 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2529 f.write_str(self.0)
2530 }
2531}
2532
2533#[async_trait::async_trait]
2554pub trait HttpExecutor: Send + Sync {
2555 async fn execute_request(
2561 &self,
2562 method: &str,
2563 path: ResolvedPath<'_>,
2564 body: Option<JsonValue>,
2565 ) -> Result<JsonValue, ExecutionError>;
2566
2567 fn placeholder_rules(
2593 &self,
2594 method: &str,
2595 path_template: &str,
2596 param: &str,
2597 ) -> crate::PlaceholderRules<'_> {
2598 let _ = (method, path_template, param);
2599 crate::PlaceholderRules::default()
2600 }
2601}
2602
2603#[cfg(feature = "mcp-code-mode")]
2609#[async_trait::async_trait]
2610pub trait McpExecutor: Send + Sync {
2611 async fn call_tool(
2613 &self,
2614 server_id: &str,
2615 tool_name: &str,
2616 args: JsonValue,
2617 ) -> Result<JsonValue, ExecutionError>;
2618}
2619
2620#[async_trait::async_trait]
2626pub trait SdkExecutor: Send + Sync {
2627 async fn execute_operation(
2632 &self,
2633 operation: &str,
2634 args: Option<JsonValue>,
2635 ) -> Result<JsonValue, ExecutionError>;
2636}
2637
2638#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
2644pub enum MockExecutionMode {
2645 #[default]
2647 DryRun,
2648 Testing,
2650 Record,
2652}
2653
2654pub struct MockHttpExecutor {
2692 responses: std::sync::RwLock<HashMap<String, JsonValue>>,
2694 default_response: JsonValue,
2696 recorded_calls: std::sync::RwLock<Vec<MockedCall>>,
2698}
2699
2700#[derive(Debug, Clone, Serialize)]
2702pub struct MockedCall {
2703 pub method: String,
2705 pub path: String,
2707 pub body: Option<JsonValue>,
2709 pub response: JsonValue,
2711}
2712
2713impl MockHttpExecutor {
2714 pub fn new_dry_run() -> Self {
2717 Self {
2718 responses: std::sync::RwLock::new(HashMap::new()),
2719 default_response: JsonValue::Object(serde_json::Map::new()),
2720 recorded_calls: std::sync::RwLock::new(Vec::new()),
2721 }
2722 }
2723
2724 pub fn new_testing() -> Self {
2727 Self {
2728 responses: std::sync::RwLock::new(HashMap::new()),
2729 default_response: JsonValue::Object(serde_json::Map::new()),
2730 recorded_calls: std::sync::RwLock::new(Vec::new()),
2731 }
2732 }
2733
2734 pub fn with_default_response(mut self, response: JsonValue) -> Self {
2736 self.default_response = response;
2737 self
2738 }
2739
2740 pub fn with_response(self, path_pattern: &str, response: JsonValue) -> Self {
2751 self.responses
2752 .write()
2753 .unwrap()
2754 .insert(path_pattern.to_string(), response);
2755 self
2756 }
2757
2758 pub fn add_response(&self, path_pattern: &str, response: JsonValue) {
2760 self.responses
2761 .write()
2762 .unwrap()
2763 .insert(path_pattern.to_string(), response);
2764 }
2765
2766 pub fn recorded_calls(&self) -> Vec<MockedCall> {
2768 self.recorded_calls.read().unwrap().clone()
2769 }
2770
2771 pub fn clear_calls(&self) {
2773 self.recorded_calls.write().unwrap().clear();
2774 }
2775
2776 pub fn call_count(&self) -> usize {
2778 self.recorded_calls.read().unwrap().len()
2779 }
2780
2781 pub fn was_called(&self, path: &str) -> bool {
2783 self.recorded_calls
2784 .read()
2785 .unwrap()
2786 .iter()
2787 .any(|c| c.path == path)
2788 }
2789
2790 pub fn was_called_with_method(&self, method: &str, path: &str) -> bool {
2792 self.recorded_calls
2793 .read()
2794 .unwrap()
2795 .iter()
2796 .any(|c| c.method == method && c.path == path)
2797 }
2798
2799 fn find_response(&self, path: &str) -> JsonValue {
2801 let responses = self.responses.read().unwrap();
2802
2803 if let Some(response) = responses.get(path) {
2805 return response.clone();
2806 }
2807
2808 for (pattern, response) in responses.iter() {
2810 if Self::matches_pattern(pattern, path) {
2811 return response.clone();
2812 }
2813 }
2814
2815 self.default_response.clone()
2817 }
2818
2819 fn matches_pattern(pattern: &str, path: &str) -> bool {
2821 if !pattern.contains('*') {
2822 return pattern == path;
2823 }
2824
2825 let pattern_parts: Vec<&str> = pattern.split('/').collect();
2826 let path_parts: Vec<&str> = path.split('/').collect();
2827
2828 if pattern_parts.len() != path_parts.len() {
2829 if pattern.ends_with("*") && path_parts.len() >= pattern_parts.len() - 1 {
2831 } else {
2833 return false;
2834 }
2835 }
2836
2837 for (p, s) in pattern_parts.iter().zip(path_parts.iter()) {
2838 if *p != "*" && *p != *s {
2839 return false;
2840 }
2841 }
2842
2843 true
2844 }
2845}
2846
2847#[async_trait::async_trait]
2848impl HttpExecutor for MockHttpExecutor {
2849 async fn execute_request(
2850 &self,
2851 method: &str,
2852 path: ResolvedPath<'_>,
2853 body: Option<JsonValue>,
2854 ) -> Result<JsonValue, ExecutionError> {
2855 let path = path.as_str();
2856 let response = self.find_response(path);
2857
2858 let call = MockedCall {
2860 method: method.to_string(),
2861 path: path.to_string(),
2862 body,
2863 response: response.clone(),
2864 };
2865 self.recorded_calls.write().unwrap().push(call);
2866
2867 Ok(response)
2868 }
2869}
2870
2871unsafe impl Send for MockHttpExecutor {}
2873unsafe impl Sync for MockHttpExecutor {}
2874
2875#[derive(Debug, Clone, Serialize)]
2877pub struct ExecutionResult {
2878 pub value: JsonValue,
2880 pub api_calls: Vec<ApiCallLog>,
2882 pub execution_time_ms: u64,
2884}
2885
2886#[derive(Debug, Clone, Serialize)]
2888pub struct ApiCallLog {
2889 pub method: String,
2891 pub path: String,
2901 pub body: Option<JsonValue>,
2903 pub response: JsonValue,
2905 pub duration_ms: u64,
2907}
2908
2909fn refusal_to_execution_error(refusal: crate::PlaceholderRefusal) -> ExecutionError {
2917 ExecutionError::RuntimeError {
2918 message: refusal.to_string(),
2919 }
2920}
2921
2922fn floor_layer_one_contribution(param: &str, rendered: &str) -> Result<(), ExecutionError> {
2932 crate::validate_path_placeholder(param, rendered, &crate::PlaceholderRules::default())
2933 .map_err(refusal_to_execution_error)
2934}
2935
2936fn render_path_scalar(key: &str, value: JsonValue) -> Result<String, ExecutionError> {
2950 match value {
2951 JsonValue::String(s) => Ok(s),
2952 JsonValue::Null => Ok("null".to_string()),
2953 JsonValue::Number(n) => Ok(n.to_string()),
2954 JsonValue::Bool(b) => Ok(b.to_string()),
2955 JsonValue::Object(_) | JsonValue::Array(_) => Err(ExecutionError::RuntimeError {
2956 message: format!("path/query param '{key}' must be a scalar"),
2957 }),
2958 }
2959}
2960
2961fn apply_substitutions(template: &str, substitutions: &[(String, String)]) -> String {
2982 let mut order: Vec<(&str, &str)> = substitutions
2983 .iter()
2984 .map(|(placeholder, rendered)| (placeholder.as_str(), rendered.as_str()))
2985 .collect();
2986 order.sort_by_key(|(placeholder, _)| std::cmp::Reverse(placeholder.len()));
2987
2988 let mut resolved = String::with_capacity(template.len());
2989 let mut rest = template;
2990 while let Some(ch) = rest.chars().next() {
2991 match order.iter().copied().find(|(p, _)| rest.starts_with(*p)) {
2992 Some((placeholder, rendered)) => {
2993 resolved.push_str(rendered);
2994 rest = &rest[placeholder.len()..];
2995 },
2996 None => {
2997 resolved.push(ch);
2998 rest = &rest[ch.len_utf8()..];
2999 },
3000 }
3001 }
3002 resolved
3003}
3004
3005fn resolve_layer_two_placeholders<H: HttpExecutor + ?Sized>(
3030 http: &H,
3031 method: &str,
3032 template: &str,
3033 body: Option<JsonValue>,
3034) -> Result<(String, Option<JsonValue>), ExecutionError> {
3035 let obj = match body {
3040 Some(JsonValue::Object(obj)) => obj,
3041 other => return Ok((template.to_string(), other)),
3042 };
3043
3044 let mut substitutions: Vec<(String, String)> = Vec::new();
3046 let mut remaining = serde_json::Map::new();
3047 let mut probe = String::new();
3052 for (key, value) in obj {
3053 probe.clear();
3054 probe.push('{');
3055 probe.push_str(&key);
3056 probe.push('}');
3057 if template.contains(probe.as_str()) {
3058 let rules = http.placeholder_rules(method, template, &key);
3059 let rendered = render_path_scalar(&key, value)?;
3062 crate::validate_path_placeholder(&key, &rendered, &rules)
3063 .map_err(refusal_to_execution_error)?;
3064 substitutions.push((probe.clone(), rendered));
3065 } else {
3066 remaining.insert(key, value);
3067 }
3068 }
3069
3070 let resolved = apply_substitutions(template, &substitutions);
3072
3073 let remaining = if remaining.is_empty() {
3074 None
3075 } else {
3076 Some(JsonValue::Object(remaining))
3077 };
3078 Ok((resolved, remaining))
3079}
3080
3081pub struct PlanExecutor<H: HttpExecutor> {
3083 http: H,
3084 config: ExecutionConfig,
3085 variables: HashMap<String, JsonValue>,
3086 api_calls: Vec<ApiCallLog>,
3087 api_call_count: usize,
3088 #[cfg(feature = "mcp-code-mode")]
3089 mcp: Option<Box<dyn McpExecutor>>,
3090 sdk: Option<Box<dyn SdkExecutor>>,
3092}
3093
3094impl<H: HttpExecutor> PlanExecutor<H> {
3095 pub fn new(http: H, config: ExecutionConfig) -> Self {
3097 Self {
3098 http,
3099 config,
3100 variables: HashMap::new(),
3101 api_calls: Vec::new(),
3102 api_call_count: 0,
3103 #[cfg(feature = "mcp-code-mode")]
3104 mcp: None,
3105 sdk: None,
3106 }
3107 }
3108
3109 #[cfg(feature = "mcp-code-mode")]
3111 pub fn set_mcp_executor(&mut self, executor: impl McpExecutor + 'static) {
3112 self.mcp = Some(Box::new(executor));
3113 }
3114
3115 pub fn set_sdk_executor(&mut self, executor: impl SdkExecutor + 'static) {
3117 self.sdk = Some(Box::new(executor));
3118 }
3119
3120 pub fn set_variable(&mut self, name: impl Into<String>, value: JsonValue) {
3122 self.variables.insert(name.into(), value);
3123 }
3124
3125 pub async fn execute(
3127 &mut self,
3128 plan: &ExecutionPlan,
3129 ) -> Result<ExecutionResult, ExecutionError> {
3130 let start = std::time::Instant::now();
3131
3132 let mut return_value = JsonValue::Null;
3133
3134 for step in &plan.steps {
3135 match self.execute_step(step).await? {
3136 StepOutcome::Return(value) => {
3137 return_value = value;
3138 break; },
3140 StepOutcome::None | StepOutcome::Continue | StepOutcome::Break => {},
3141 }
3142 }
3143
3144 let blocked_in_output =
3147 find_blocked_fields_in_output(&return_value, &self.config.output_blocked_fields);
3148
3149 if !blocked_in_output.is_empty() {
3150 return Err(ExecutionError::RuntimeError {
3151 message: format!(
3152 "Script output contains blocked fields: {}",
3153 blocked_in_output.join(", ")
3154 ),
3155 });
3156 }
3157
3158 Ok(ExecutionResult {
3159 value: return_value,
3160 api_calls: std::mem::take(&mut self.api_calls),
3161 execution_time_ms: start.elapsed().as_millis() as u64,
3162 })
3163 }
3164
3165 fn execute_step<'a>(
3168 &'a mut self,
3169 step: &'a PlanStep,
3170 ) -> std::pin::Pin<
3171 Box<dyn std::future::Future<Output = Result<StepOutcome, ExecutionError>> + Send + 'a>,
3172 > {
3173 Box::pin(async move {
3174 match step {
3175 PlanStep::ApiCall {
3176 result_var,
3177 method,
3178 path,
3179 body,
3180 } => {
3181 self.api_call_count += 1;
3182 if self.api_call_count > self.config.max_api_calls {
3183 return Err(ExecutionError::RuntimeError {
3184 message: format!(
3185 "Too many API calls: {} (max: {})",
3186 self.api_call_count, self.config.max_api_calls
3187 ),
3188 });
3189 }
3190
3191 let templated_path = self.resolve_path(path)?;
3194 let evaluated_body = match body {
3195 Some(expr) => Some(self.evaluate(expr)?),
3196 None => None,
3197 };
3198 let (resolved_path, resolved_body) = resolve_layer_two_placeholders(
3200 &self.http,
3201 method,
3202 &templated_path,
3203 evaluated_body,
3204 )?;
3205 let checked_path = ResolvedPath::from_checked(&resolved_path)
3211 .map_err(refusal_to_execution_error)?;
3212
3213 let call_start = std::time::Instant::now();
3214 let raw_response = self
3215 .http
3216 .execute_request(method, checked_path, resolved_body.clone())
3217 .await
3218 .map_err(|e| ExecutionError::RuntimeError {
3219 message: format!("{method} api call '{result_var}' failed: {e}"),
3225 })?;
3226 let duration_ms = call_start.elapsed().as_millis() as u64;
3227
3228 let response = filter_blocked_fields(raw_response, &self.config.blocked_fields);
3231
3232 self.api_calls.push(ApiCallLog {
3233 method: method.clone(),
3234 path: resolved_path,
3235 body: resolved_body,
3236 response: response.clone(),
3237 duration_ms,
3238 });
3239
3240 if result_var != "_" {
3241 self.variables.insert(result_var.clone(), response);
3242 }
3243 Ok(StepOutcome::None)
3244 },
3245
3246 PlanStep::Assign { var, expr } => {
3247 let value = self.evaluate(expr)?;
3248 self.variables.insert(var.clone(), value);
3249 Ok(StepOutcome::None)
3250 },
3251
3252 PlanStep::Conditional {
3253 condition,
3254 then_steps,
3255 else_steps,
3256 } => {
3257 let cond_value = self.evaluate(condition)?;
3258 let steps = if shared_is_truthy(&cond_value) {
3259 then_steps
3260 } else {
3261 else_steps
3262 };
3263
3264 for step in steps {
3265 match self.execute_step(step).await? {
3266 StepOutcome::None => {},
3267 outcome => return Ok(outcome),
3268 }
3269 }
3270 Ok(StepOutcome::None)
3271 },
3272
3273 PlanStep::BoundedLoop {
3274 item_var,
3275 collection,
3276 max_iterations,
3277 body,
3278 } => {
3279 let collection_value = self.evaluate(collection)?;
3280 let items = match collection_value {
3281 JsonValue::Array(arr) => arr,
3282 _ => {
3283 return Err(ExecutionError::RuntimeError {
3284 message: "Loop collection must be an array".into(),
3285 })
3286 },
3287 };
3288
3289 let limit = (*max_iterations).min(self.config.max_loop_iterations);
3290 'outer: for item in items.into_iter().take(limit) {
3291 self.variables.insert(item_var.clone(), item);
3292
3293 for step in body {
3294 match self.execute_step(step).await? {
3295 StepOutcome::Return(value) => {
3296 return Ok(StepOutcome::Return(value))
3297 },
3298 StepOutcome::None => {},
3299 StepOutcome::Continue => continue 'outer,
3300 StepOutcome::Break => break 'outer,
3301 }
3302 }
3303 }
3304 Ok(StepOutcome::None)
3305 },
3306
3307 PlanStep::Return { value } => {
3308 let result = self.evaluate(value)?;
3309 Ok(StepOutcome::Return(result))
3310 },
3311
3312 PlanStep::TryCatch {
3313 try_steps,
3314 catch_var,
3315 catch_steps,
3316 finally_steps,
3317 } => {
3318 let try_result = async {
3320 for step in try_steps {
3321 match self.execute_step(step).await? {
3322 StepOutcome::None => {},
3323 outcome => return Ok::<StepOutcome, ExecutionError>(outcome),
3324 }
3325 }
3326 Ok(StepOutcome::None)
3327 }
3328 .await;
3329
3330 let result = match try_result {
3332 Ok(outcome) => {
3333 outcome
3335 },
3336 Err(error) => {
3337 if let Some(var) = catch_var {
3339 let error_obj = JsonValue::Object(serde_json::Map::from_iter([(
3341 "message".to_string(),
3342 JsonValue::String(format!("{}", error)),
3343 )]));
3344 self.variables.insert(var.clone(), error_obj);
3345 }
3346
3347 let mut catch_outcome = StepOutcome::None;
3349 for step in catch_steps {
3350 match self.execute_step(step).await? {
3351 StepOutcome::None => {},
3352 outcome => {
3353 catch_outcome = outcome;
3354 break;
3355 },
3356 }
3357 }
3358 catch_outcome
3359 },
3360 };
3361
3362 for step in finally_steps {
3364 match self.execute_step(step).await? {
3365 StepOutcome::None => {},
3366 outcome => return Ok(outcome),
3367 }
3368 }
3369
3370 Ok(result)
3371 },
3372
3373 PlanStep::ParallelApiCalls { result_var, calls } => {
3377 let mut results = Vec::with_capacity(calls.len());
3378 for (temp_var, method, path, body) in calls {
3379 self.api_call_count += 1;
3380 if self.api_call_count > self.config.max_api_calls {
3381 return Err(ExecutionError::RuntimeError {
3382 message: format!(
3383 "Maximum API calls exceeded ({})",
3384 self.config.max_api_calls
3385 ),
3386 });
3387 }
3388
3389 let templated_path = self.resolve_path(path)?;
3394 let evaluated_body = body.as_ref().map(|b| self.evaluate(b)).transpose()?;
3395 let (resolved_path, resolved_body) = resolve_layer_two_placeholders(
3396 &self.http,
3397 method,
3398 &templated_path,
3399 evaluated_body,
3400 )?;
3401 let checked_path = ResolvedPath::from_checked(&resolved_path)
3402 .map_err(refusal_to_execution_error)?;
3403 let call_start = std::time::Instant::now();
3404 let raw_response = self
3405 .http
3406 .execute_request(method, checked_path, resolved_body.clone())
3407 .await
3408 .map_err(|e| ExecutionError::RuntimeError {
3409 message: format!("{method} api call '{temp_var}' failed: {e}"),
3411 })?;
3412 let duration_ms = call_start.elapsed().as_millis() as u64;
3413 let response =
3414 filter_blocked_fields(raw_response, &self.config.blocked_fields);
3415
3416 self.api_calls.push(ApiCallLog {
3417 method: method.clone(),
3418 path: resolved_path,
3419 body: resolved_body,
3420 response: response.clone(),
3421 duration_ms,
3422 });
3423
3424 results.push(response);
3425 }
3426 self.variables
3427 .insert(result_var.clone(), JsonValue::Array(results));
3428 Ok(StepOutcome::None)
3429 },
3430
3431 PlanStep::Continue => Ok(StepOutcome::Continue),
3433
3434 PlanStep::Break => Ok(StepOutcome::Break),
3436
3437 #[cfg(feature = "mcp-code-mode")]
3439 PlanStep::McpCall {
3440 result_var,
3441 server_id,
3442 tool_name,
3443 args,
3444 } => {
3445 self.api_call_count += 1;
3446 if self.api_call_count > self.config.max_api_calls {
3447 return Err(ExecutionError::RuntimeError {
3448 message: format!(
3449 "Too many calls: {} (max: {})",
3450 self.api_call_count, self.config.max_api_calls
3451 ),
3452 });
3453 }
3454
3455 let resolved_args = match args {
3456 Some(expr) => self.evaluate(expr)?,
3457 None => JsonValue::Object(Default::default()),
3458 };
3459
3460 let mcp_executor =
3461 self.mcp
3462 .as_ref()
3463 .ok_or_else(|| ExecutionError::RuntimeError {
3464 message: "MCP executor not configured".into(),
3465 })?;
3466
3467 let call_start = std::time::Instant::now();
3468 let result = mcp_executor
3469 .call_tool(server_id, tool_name, resolved_args.clone())
3470 .await?;
3471 let duration_ms = call_start.elapsed().as_millis() as u64;
3472
3473 self.api_calls.push(ApiCallLog {
3474 method: format!("MCP:{}.{}", server_id, tool_name),
3475 path: format!("{}/{}", server_id, tool_name),
3476 body: Some(resolved_args),
3477 response: result.clone(),
3478 duration_ms,
3479 });
3480
3481 if result_var != "_" {
3482 self.variables.insert(result_var.clone(), result);
3483 }
3484 Ok(StepOutcome::None)
3485 },
3486
3487 PlanStep::SdkCall {
3489 result_var,
3490 operation,
3491 args,
3492 } => {
3493 self.api_call_count += 1;
3494 if self.api_call_count > self.config.max_api_calls {
3495 return Err(ExecutionError::RuntimeError {
3496 message: format!(
3497 "Too many calls: {} (max: {})",
3498 self.api_call_count, self.config.max_api_calls
3499 ),
3500 });
3501 }
3502
3503 let resolved_args =
3504 args.as_ref().map(|expr| self.evaluate(expr)).transpose()?;
3505
3506 let sdk_executor =
3507 self.sdk
3508 .as_ref()
3509 .ok_or_else(|| ExecutionError::RuntimeError {
3510 message: "SDK executor not configured".into(),
3511 })?;
3512
3513 let call_start = std::time::Instant::now();
3514 let result = sdk_executor
3515 .execute_operation(operation, resolved_args.clone())
3516 .await?;
3517 let duration_ms = call_start.elapsed().as_millis() as u64;
3518
3519 self.api_calls.push(ApiCallLog {
3520 method: operation.clone(),
3521 path: format!("sdk:{}", operation),
3522 body: resolved_args,
3523 response: result.clone(),
3524 duration_ms,
3525 });
3526
3527 if result_var != "_" {
3528 self.variables.insert(result_var.clone(), result);
3529 }
3530 Ok(StepOutcome::None)
3531 },
3532 }
3533 })
3534 }
3535
3536 fn resolve_path(&self, path: &PathTemplate) -> Result<String, ExecutionError> {
3559 let mut result = String::new();
3560 for (index, part) in path.parts.iter().enumerate() {
3561 match part {
3562 PathPart::Literal(s) => result.push_str(s),
3563 PathPart::Variable(var) => {
3564 let value =
3565 self.variables
3566 .get(var)
3567 .ok_or_else(|| ExecutionError::RuntimeError {
3568 message: format!("Undefined variable in path: {}", var),
3569 })?;
3570 let rendered = shared_json_to_string_with_mode(value, JsonStringMode::Json);
3571 floor_layer_one_contribution(var, &rendered)?;
3597 result.push_str(&rendered);
3598 },
3599 PathPart::Expression(expr) => {
3600 let value = self.evaluate(expr)?;
3601 let rendered = shared_json_to_string_with_mode(&value, JsonStringMode::Json);
3602 floor_layer_one_contribution(&format!("path expression #{index}"), &rendered)?;
3607 result.push_str(&rendered);
3608 },
3609 }
3610 }
3611 Ok(result)
3612 }
3613
3614 fn evaluate(&self, expr: &ValueExpr) -> Result<JsonValue, ExecutionError> {
3617 shared_evaluate(expr, &self.variables)
3618 }
3619}
3620
3621pub type JsExecutor = PlanCompiler;
3627
3628#[cfg(test)]
3629mod tests {
3630 use super::*;
3631
3632 #[test]
3633 fn test_execution_config_default() {
3634 let config = ExecutionConfig::default();
3635 assert_eq!(config.max_api_calls, 50);
3636 assert_eq!(config.timeout_seconds, 30);
3637 assert_eq!(config.max_loop_iterations, 100);
3638 }
3639
3640 #[test]
3641 fn test_path_template_static() {
3642 let path = PathTemplate::static_path("/users".into());
3643 assert!(!path.is_dynamic());
3644 }
3645
3646 #[test]
3647 fn test_path_template_dynamic() {
3648 let path = PathTemplate {
3649 parts: vec![
3650 PathPart::Literal("/users/".into()),
3651 PathPart::Variable("id".into()),
3652 ],
3653 };
3654 assert!(path.is_dynamic());
3655 }
3656
3657 #[test]
3658 fn test_plan_metadata() {
3659 let metadata = PlanMetadata {
3660 api_call_count: 2,
3661 has_mutations: false,
3662 endpoints: vec!["/users".into(), "/products".into()],
3663 methods_used: vec!["GET".into()],
3664 };
3665 assert_eq!(metadata.api_call_count, 2);
3666 assert!(!metadata.has_mutations);
3667 }
3668
3669 #[test]
3670 fn test_compile_simple_api_call() {
3671 let code = r#"
3672 const user = await api.get('/users/1');
3673 return user;
3674 "#;
3675
3676 let mut compiler = PlanCompiler::new();
3677 let plan = compiler.compile_code(code).expect("Should compile");
3678
3679 assert_eq!(plan.metadata.api_call_count, 1);
3680 assert!(!plan.metadata.has_mutations);
3681 assert_eq!(plan.steps.len(), 2); }
3683
3684 #[test]
3685 fn test_compile_multiple_api_calls() {
3686 let code = r#"
3687 const users = await api.get('/users');
3688 const products = await api.get('/products');
3689 return { users, products };
3690 "#;
3691
3692 let mut compiler = PlanCompiler::new();
3693 let plan = compiler.compile_code(code).expect("Should compile");
3694
3695 assert_eq!(plan.metadata.api_call_count, 2);
3696 assert!(!plan.metadata.has_mutations);
3697 }
3698
3699 #[test]
3700 fn test_compile_mutation() {
3701 let code = r#"
3702 const result = await api.post('/users', { name: 'Test' });
3703 return result;
3704 "#;
3705
3706 let mut compiler = PlanCompiler::new();
3707 let plan = compiler.compile_code(code).expect("Should compile");
3708
3709 assert_eq!(plan.metadata.api_call_count, 1);
3710 assert!(plan.metadata.has_mutations);
3711 }
3712
3713 #[test]
3714 fn test_compile_dynamic_path() {
3715 let code = r#"
3716 const id = 123;
3717 const user = await api.get(`/users/${id}`);
3718 return user;
3719 "#;
3720
3721 let mut compiler = PlanCompiler::new();
3722 let plan = compiler.compile_code(code).expect("Should compile");
3723
3724 assert_eq!(plan.metadata.api_call_count, 1);
3725 }
3726
3727 #[test]
3728 fn test_compile_bounded_loop() {
3729 let code = r#"
3730 const items = [];
3731 const users = [{ id: 1 }, { id: 2 }, { id: 3 }];
3732 for (const user of users.slice(0, 2)) {
3733 const detail = await api.get(`/users/${user.id}`);
3734 items.push(detail);
3735 }
3736 return items;
3737 "#;
3738
3739 let mut compiler = PlanCompiler::new();
3740 let plan = compiler.compile_code(code).expect("Should compile");
3741
3742 assert!(plan
3744 .steps
3745 .iter()
3746 .any(|s| matches!(s, PlanStep::BoundedLoop { .. })));
3747 }
3748
3749 #[test]
3750 fn test_compile_unbounded_loop_detection() {
3751 let code = r#"
3755 const users = [{ id: 1 }, { id: 2 }, { id: 3 }];
3756 for (const user of users) {
3757 const detail = await api.get(`/users/${user.id}`);
3758 }
3759 return users;
3760 "#;
3761
3762 let mut compiler = PlanCompiler::new();
3763 let result = compiler.compile_code(code);
3764
3765 assert!(result.is_ok(), "Loop compiled: {:?}", result);
3768 }
3769
3770 #[test]
3771 fn test_compile_conditional() {
3772 let code = r#"
3773 const user = await api.get('/users/1');
3774 if (user.active) {
3775 const orders = await api.get(`/users/${user.id}/orders`);
3776 return orders;
3777 } else {
3778 return [];
3779 }
3780 "#;
3781
3782 let mut compiler = PlanCompiler::new();
3783 let plan = compiler.compile_code(code).expect("Should compile");
3784
3785 assert!(plan
3786 .steps
3787 .iter()
3788 .any(|s| matches!(s, PlanStep::Conditional { .. })));
3789 }
3790
3791 struct MockHttpExecutor {
3793 responses: std::collections::HashMap<String, JsonValue>,
3794 }
3795
3796 impl MockHttpExecutor {
3797 fn new() -> Self {
3798 Self {
3799 responses: std::collections::HashMap::new(),
3800 }
3801 }
3802
3803 fn add_response(&mut self, path: &str, response: JsonValue) {
3804 self.responses.insert(path.to_string(), response);
3805 }
3806 }
3807
3808 #[async_trait::async_trait]
3809 impl HttpExecutor for MockHttpExecutor {
3810 async fn execute_request(
3811 &self,
3812 _method: &str,
3813 path: ResolvedPath<'_>,
3814 _body: Option<JsonValue>,
3815 ) -> Result<JsonValue, ExecutionError> {
3816 let path = path.as_str();
3817 self.responses
3818 .get(path)
3819 .cloned()
3820 .ok_or_else(|| ExecutionError::RuntimeError {
3821 message: format!("No mock response for path: {}", path),
3822 })
3823 }
3824 }
3825
3826 #[tokio::test]
3827 async fn test_execute_simple_api_call() {
3828 let code = r#"
3829 const user = await api.get('/users/1');
3830 return user;
3831 "#;
3832
3833 let mut compiler = PlanCompiler::new();
3834 let plan = compiler.compile_code(code).expect("Should compile");
3835
3836 let mut mock_http = MockHttpExecutor::new();
3837 mock_http.add_response("/users/1", serde_json::json!({ "id": 1, "name": "Alice" }));
3838
3839 let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
3840 let result = executor.execute(&plan).await.expect("Should execute");
3841
3842 assert_eq!(result.value["id"], 1);
3843 assert_eq!(result.value["name"], "Alice");
3844 assert_eq!(result.api_calls.len(), 1);
3845 }
3846
3847 #[tokio::test]
3848 async fn test_execute_multiple_api_calls() {
3849 let code = r#"
3850 const users = await api.get('/users');
3851 const products = await api.get('/products');
3852 return { users, products };
3853 "#;
3854
3855 let mut compiler = PlanCompiler::new();
3856 let plan = compiler.compile_code(code).expect("Should compile");
3857
3858 let mut mock_http = MockHttpExecutor::new();
3859 mock_http.add_response("/users", serde_json::json!([{ "id": 1, "name": "Alice" }]));
3860 mock_http.add_response(
3861 "/products",
3862 serde_json::json!([{ "id": 100, "name": "Widget" }]),
3863 );
3864
3865 let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
3866 let result = executor.execute(&plan).await.expect("Should execute");
3867
3868 assert!(result.value["users"].is_array());
3869 assert!(result.value["products"].is_array());
3870 assert_eq!(result.api_calls.len(), 2);
3871 }
3872
3873 #[tokio::test]
3874 async fn test_execute_with_template_path() {
3875 let code = r#"
3876 const userId = 42;
3877 const user = await api.get(`/users/${userId}`);
3878 return user;
3879 "#;
3880
3881 let mut compiler = PlanCompiler::new();
3882 let plan = compiler.compile_code(code).expect("Should compile");
3883
3884 let mut mock_http = MockHttpExecutor::new();
3885 mock_http.add_response("/users/42", serde_json::json!({ "id": 42, "name": "Bob" }));
3886
3887 let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
3888 let result = executor.execute(&plan).await.expect("Should execute");
3889
3890 assert_eq!(result.value["id"], 42);
3891 assert_eq!(result.value["name"], "Bob");
3892 }
3893
3894 #[tokio::test]
3895 async fn test_execute_conditional_true_branch() {
3896 let code = r#"
3897 const user = await api.get('/users/1');
3898 if (user.active) {
3899 return { status: "active", user: user };
3900 } else {
3901 return { status: "inactive" };
3902 }
3903 "#;
3904
3905 let mut compiler = PlanCompiler::new();
3906 let plan = compiler.compile_code(code).expect("Should compile");
3907
3908 let mut mock_http = MockHttpExecutor::new();
3909 mock_http.add_response("/users/1", serde_json::json!({ "id": 1, "active": true }));
3910
3911 let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
3912 let result = executor.execute(&plan).await.expect("Should execute");
3913
3914 assert_eq!(result.value["status"], "active");
3915 }
3916
3917 #[tokio::test]
3918 async fn test_execute_conditional_false_branch() {
3919 let code = r#"
3920 const user = await api.get('/users/1');
3921 if (user.active) {
3922 return { status: "active" };
3923 } else {
3924 return { status: "inactive", user: user };
3925 }
3926 "#;
3927
3928 let mut compiler = PlanCompiler::new();
3929 let plan = compiler.compile_code(code).expect("Should compile");
3930
3931 let mut mock_http = MockHttpExecutor::new();
3932 mock_http.add_response("/users/1", serde_json::json!({ "id": 1, "active": false }));
3933
3934 let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
3935 let result = executor.execute(&plan).await.expect("Should execute");
3936
3937 assert_eq!(result.value["status"], "inactive");
3938 }
3939
3940 #[tokio::test]
3941 async fn test_compile_and_execute_reduce() {
3942 let code = r#"
3943 const products = await api.get('/products');
3944 const totalPrice = products.reduce((sum, p) => sum + p.price, 0);
3945 return { total: totalPrice };
3946 "#;
3947
3948 let mut compiler = PlanCompiler::new();
3949 let plan = compiler.compile_code(code).expect("Should compile reduce");
3950
3951 let mut mock_http = MockHttpExecutor::new();
3952 mock_http.add_response(
3953 "/products",
3954 serde_json::json!([
3955 { "id": 1, "name": "Widget", "price": 10 },
3956 { "id": 2, "name": "Gadget", "price": 25 },
3957 { "id": 3, "name": "Gizmo", "price": 15 }
3958 ]),
3959 );
3960
3961 let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
3962 let result = executor.execute(&plan).await.expect("Should execute");
3963
3964 assert_eq!(result.value["total"].as_f64().unwrap(), 50.0);
3966 }
3967
3968 #[tokio::test]
3969 async fn test_compile_and_execute_to_fixed() {
3970 let code = r#"
3971 const products = await api.get('/products');
3972 const totalPrice = products.reduce((sum, p) => sum + p.price, 0);
3973 const averagePrice = products.length > 0 ? totalPrice / products.length : 0;
3974 return { averagePrice: averagePrice.toFixed(2) };
3975 "#;
3976
3977 let mut compiler = PlanCompiler::new();
3978 let plan = compiler.compile_code(code).expect("Should compile toFixed");
3979
3980 let mut mock_http = MockHttpExecutor::new();
3981 mock_http.add_response(
3982 "/products",
3983 serde_json::json!([
3984 { "id": 1, "name": "Widget", "price": 10 },
3985 { "id": 2, "name": "Gadget", "price": 25 },
3986 { "id": 3, "name": "Gizmo", "price": 15 }
3987 ]),
3988 );
3989
3990 let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
3991 let result = executor.execute(&plan).await.expect("Should execute");
3992
3993 assert_eq!(result.value["averagePrice"], "16.67");
3995 }
3996
3997 #[test]
4002 fn test_filter_blocked_fields_simple() {
4003 let value = serde_json::json!({
4004 "id": 1,
4005 "name": "Alice",
4006 "password": "secret123",
4007 "email": "alice@example.com"
4008 });
4009
4010 let blocked: HashSet<String> = ["password"].iter().map(|s| s.to_string()).collect();
4011 let filtered = filter_blocked_fields(value, &blocked);
4012
4013 assert_eq!(filtered["id"], 1);
4014 assert_eq!(filtered["name"], "Alice");
4015 assert_eq!(filtered["email"], "alice@example.com");
4016 assert!(filtered.get("password").is_none());
4017 }
4018
4019 #[test]
4020 fn test_filter_blocked_fields_multiple() {
4021 let value = serde_json::json!({
4022 "id": 1,
4023 "name": "Alice",
4024 "password": "secret123",
4025 "ssn": "123-45-6789",
4026 "apiKey": "key-abc123"
4027 });
4028
4029 let blocked: HashSet<String> = ["password", "ssn", "apiKey"]
4030 .iter()
4031 .map(|s| s.to_string())
4032 .collect();
4033 let filtered = filter_blocked_fields(value, &blocked);
4034
4035 assert_eq!(filtered["id"], 1);
4036 assert_eq!(filtered["name"], "Alice");
4037 assert!(filtered.get("password").is_none());
4038 assert!(filtered.get("ssn").is_none());
4039 assert!(filtered.get("apiKey").is_none());
4040 }
4041
4042 #[test]
4043 fn test_filter_blocked_fields_nested() {
4044 let value = serde_json::json!({
4045 "user": {
4046 "id": 1,
4047 "profile": {
4048 "name": "Alice",
4049 "password": "secret123"
4050 }
4051 }
4052 });
4053
4054 let blocked: HashSet<String> = ["password"].iter().map(|s| s.to_string()).collect();
4055 let filtered = filter_blocked_fields(value, &blocked);
4056
4057 assert_eq!(filtered["user"]["id"], 1);
4058 assert_eq!(filtered["user"]["profile"]["name"], "Alice");
4059 assert!(filtered["user"]["profile"].get("password").is_none());
4060 }
4061
4062 #[test]
4063 fn test_filter_blocked_fields_in_array() {
4064 let value = serde_json::json!([
4065 { "id": 1, "name": "Alice", "password": "secret1" },
4066 { "id": 2, "name": "Bob", "password": "secret2" }
4067 ]);
4068
4069 let blocked: HashSet<String> = ["password"].iter().map(|s| s.to_string()).collect();
4070 let filtered = filter_blocked_fields(value, &blocked);
4071
4072 let arr = filtered.as_array().unwrap();
4073 assert_eq!(arr.len(), 2);
4074 assert_eq!(arr[0]["id"], 1);
4075 assert_eq!(arr[0]["name"], "Alice");
4076 assert!(arr[0].get("password").is_none());
4077 assert_eq!(arr[1]["id"], 2);
4078 assert_eq!(arr[1]["name"], "Bob");
4079 assert!(arr[1].get("password").is_none());
4080 }
4081
4082 #[test]
4083 fn test_filter_blocked_fields_empty_blocklist() {
4084 let value = serde_json::json!({
4085 "id": 1,
4086 "password": "secret123"
4087 });
4088
4089 let blocked: HashSet<String> = HashSet::new();
4090 let filtered = filter_blocked_fields(value.clone(), &blocked);
4091
4092 assert_eq!(filtered, value);
4094 }
4095
4096 #[test]
4097 fn test_filter_blocked_fields_primitive_values() {
4098 let blocked: HashSet<String> = ["password"].iter().map(|s| s.to_string()).collect();
4100
4101 assert_eq!(
4102 filter_blocked_fields(JsonValue::String("test".into()), &blocked),
4103 JsonValue::String("test".into())
4104 );
4105 assert_eq!(
4106 filter_blocked_fields(JsonValue::Number(42.into()), &blocked),
4107 JsonValue::Number(42.into())
4108 );
4109 assert_eq!(
4110 filter_blocked_fields(JsonValue::Bool(true), &blocked),
4111 JsonValue::Bool(true)
4112 );
4113 assert_eq!(
4114 filter_blocked_fields(JsonValue::Null, &blocked),
4115 JsonValue::Null
4116 );
4117 }
4118
4119 #[tokio::test]
4120 async fn test_execute_with_blocked_fields() {
4121 let code = r#"
4122 const user = await api.get('/users/1');
4123 return user;
4124 "#;
4125
4126 let mut compiler = PlanCompiler::new();
4127 let plan = compiler.compile_code(code).expect("Should compile");
4128
4129 let mut mock_http = MockHttpExecutor::new();
4130 mock_http.add_response(
4131 "/users/1",
4132 serde_json::json!({
4133 "id": 1,
4134 "name": "Alice",
4135 "password": "secret123",
4136 "apiKey": "key-abc"
4137 }),
4138 );
4139
4140 let config = ExecutionConfig::default().with_blocked_fields(["password", "apiKey"]);
4142
4143 let mut executor = PlanExecutor::new(mock_http, config);
4144 let result = executor.execute(&plan).await.expect("Should execute");
4145
4146 assert_eq!(result.value["id"], 1);
4148 assert_eq!(result.value["name"], "Alice");
4149 assert!(result.value.get("password").is_none());
4150 assert!(result.value.get("apiKey").is_none());
4151 }
4152
4153 #[tokio::test]
4154 async fn test_execute_nested_blocked_fields() {
4155 let code = r#"
4156 const data = await api.get('/data');
4157 return data;
4158 "#;
4159
4160 let mut compiler = PlanCompiler::new();
4161 let plan = compiler.compile_code(code).expect("Should compile");
4162
4163 let mut mock_http = MockHttpExecutor::new();
4164 mock_http.add_response(
4165 "/data",
4166 serde_json::json!({
4167 "users": [
4168 { "id": 1, "name": "Alice", "secret": "hidden1" },
4169 { "id": 2, "name": "Bob", "secret": "hidden2" }
4170 ],
4171 "config": {
4172 "setting": "value",
4173 "secret": "also-hidden"
4174 }
4175 }),
4176 );
4177
4178 let config = ExecutionConfig::default().with_blocked_fields(["secret"]);
4180
4181 let mut executor = PlanExecutor::new(mock_http, config);
4182 let result = executor.execute(&plan).await.expect("Should execute");
4183
4184 let users = result.value["users"].as_array().unwrap();
4186 assert_eq!(users[0]["name"], "Alice");
4187 assert!(users[0].get("secret").is_none());
4188 assert_eq!(users[1]["name"], "Bob");
4189 assert!(users[1].get("secret").is_none());
4190
4191 assert_eq!(result.value["config"]["setting"], "value");
4192 assert!(result.value["config"].get("secret").is_none());
4193 }
4194
4195 #[test]
4200 fn test_find_blocked_fields_in_output_simple() {
4201 let value = serde_json::json!({
4202 "id": 1,
4203 "name": "Alice",
4204 "ssn": "123-45-6789"
4205 });
4206
4207 let blocked: HashSet<String> = ["ssn"].iter().map(|s| s.to_string()).collect();
4208 let violations = find_blocked_fields_in_output(&value, &blocked);
4209
4210 assert_eq!(violations.len(), 1);
4211 assert_eq!(violations[0], "ssn");
4212 }
4213
4214 #[test]
4215 fn test_find_blocked_fields_in_output_nested() {
4216 let value = serde_json::json!({
4217 "user": {
4218 "profile": {
4219 "name": "Alice",
4220 "salary": 100000
4221 }
4222 }
4223 });
4224
4225 let blocked: HashSet<String> = ["salary"].iter().map(|s| s.to_string()).collect();
4226 let violations = find_blocked_fields_in_output(&value, &blocked);
4227
4228 assert_eq!(violations.len(), 1);
4229 assert!(violations[0].contains("salary"));
4230 }
4231
4232 #[test]
4233 fn test_find_blocked_fields_in_output_array() {
4234 let value = serde_json::json!([
4235 { "id": 1, "ssn": "111" },
4236 { "id": 2, "ssn": "222" }
4237 ]);
4238
4239 let blocked: HashSet<String> = ["ssn"].iter().map(|s| s.to_string()).collect();
4240 let violations = find_blocked_fields_in_output(&value, &blocked);
4241
4242 assert_eq!(violations.len(), 2);
4244 }
4245
4246 #[test]
4247 fn test_find_blocked_fields_in_output_empty_blocklist() {
4248 let value = serde_json::json!({
4249 "id": 1,
4250 "ssn": "123-45-6789"
4251 });
4252
4253 let blocked: HashSet<String> = HashSet::new();
4254 let violations = find_blocked_fields_in_output(&value, &blocked);
4255
4256 assert!(violations.is_empty());
4257 }
4258
4259 #[test]
4260 fn test_find_blocked_fields_in_output_no_violations() {
4261 let value = serde_json::json!({
4262 "id": 1,
4263 "name": "Alice"
4264 });
4265
4266 let blocked: HashSet<String> = ["ssn", "salary"].iter().map(|s| s.to_string()).collect();
4267 let violations = find_blocked_fields_in_output(&value, &blocked);
4268
4269 assert!(violations.is_empty());
4270 }
4271
4272 #[tokio::test]
4273 async fn test_execute_output_blocked_fields_rejected() {
4274 let code = r#"
4275 const user = await api.get('/users/1');
4276 return { name: user.name, ssn: user.ssn };
4277 "#;
4278
4279 let mut compiler = PlanCompiler::new();
4280 let plan = compiler.compile_code(code).expect("Should compile");
4281
4282 let mut mock_http = MockHttpExecutor::new();
4283 mock_http.add_response(
4284 "/users/1",
4285 serde_json::json!({
4286 "id": 1,
4287 "name": "Alice",
4288 "ssn": "123-45-6789"
4289 }),
4290 );
4291
4292 let config = ExecutionConfig::default().with_output_blocked_fields(["ssn"]);
4295
4296 let mut executor = PlanExecutor::new(mock_http, config);
4297 let result = executor.execute(&plan).await;
4298
4299 assert!(result.is_err());
4301 let err = result.unwrap_err();
4302 assert!(format!("{:?}", err).contains("ssn"));
4303 }
4304
4305 #[tokio::test]
4306 async fn test_execute_output_blocked_fields_internal_use_allowed() {
4307 let code = r#"
4309 const user = await api.get('/users/1');
4310 return { id: user.id, name: user.name };
4311 "#;
4312
4313 let mut compiler = PlanCompiler::new();
4314 let plan = compiler.compile_code(code).expect("Should compile");
4315
4316 let mut mock_http = MockHttpExecutor::new();
4317 mock_http.add_response(
4318 "/users/1",
4319 serde_json::json!({
4320 "id": 1,
4321 "name": "Alice",
4322 "ssn": "123-45-6789"
4323 }),
4324 );
4325
4326 let config = ExecutionConfig::default().with_output_blocked_fields(["ssn"]);
4329
4330 let mut executor = PlanExecutor::new(mock_http, config);
4331 let result = executor.execute(&plan).await.expect("Should succeed");
4332
4333 assert_eq!(result.value["id"], 1);
4335 assert_eq!(result.value["name"], "Alice");
4336 assert!(result.value.get("ssn").is_none());
4337 }
4338
4339 #[tokio::test]
4340 async fn test_execute_both_blocklists() {
4341 let code = r#"
4343 const user = await api.get('/users/1');
4344 return { name: user.name, dateOfBirth: user.dateOfBirth };
4345 "#;
4346
4347 let mut compiler = PlanCompiler::new();
4348 let plan = compiler.compile_code(code).expect("Should compile");
4349
4350 let mut mock_http = MockHttpExecutor::new();
4351 mock_http.add_response(
4352 "/users/1",
4353 serde_json::json!({
4354 "id": 1,
4355 "name": "Alice",
4356 "password": "secret123",
4357 "dateOfBirth": "1990-01-01"
4358 }),
4359 );
4360
4361 let config = ExecutionConfig::default()
4364 .with_blocked_fields(["password"])
4365 .with_output_blocked_fields(["dateOfBirth"]);
4366
4367 let mut executor = PlanExecutor::new(mock_http, config);
4368 let result = executor.execute(&plan).await;
4369
4370 assert!(result.is_err());
4372 let err = result.unwrap_err();
4373 assert!(format!("{:?}", err).contains("dateOfBirth"));
4374 }
4375
4376 #[tokio::test]
4381 async fn test_prebound_args_comparison() {
4382 let code = r#"
4384 if (args.k > args.n) {
4385 return { error: 'k must be <= n' };
4386 }
4387 return { ok: true };
4388 "#;
4389
4390 let mut compiler = PlanCompiler::new();
4391 let plan = compiler.compile_code(code).expect("Should compile");
4392
4393 let mock_http = MockHttpExecutor::new();
4394 let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4395 executor.set_variable("args", serde_json::json!({"n": 3, "k": 5}));
4396
4397 let result = executor.execute(&plan).await.expect("Should execute");
4398 assert_eq!(
4399 result.value["error"], "k must be <= n",
4400 "Expected error for k > n, got: {:?}",
4401 result.value
4402 );
4403 }
4404
4405 #[tokio::test]
4406 async fn test_prebound_args_strict_equality() {
4407 let code = r#"
4409 if (args.k === 0) {
4410 return { result: 1 };
4411 }
4412 return { result: 'not zero' };
4413 "#;
4414
4415 let mut compiler = PlanCompiler::new();
4416 let plan = compiler.compile_code(code).expect("Should compile");
4417
4418 let mock_http = MockHttpExecutor::new();
4419 let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4420 executor.set_variable("args", serde_json::json!({"k": 0}));
4421
4422 let result = executor.execute(&plan).await.expect("Should execute");
4423 assert_eq!(result.value["result"], 1);
4424 }
4425
4426 #[tokio::test]
4427 async fn test_assignment_expression_in_statement() {
4428 let code = r#"
4430 let k = 5;
4431 k = 2;
4432 return { k: k };
4433 "#;
4434
4435 let mut compiler = PlanCompiler::new();
4436 let plan = compiler.compile_code(code).expect("Should compile");
4437
4438 let mock_http = MockHttpExecutor::new();
4439 let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4440
4441 let result = executor.execute(&plan).await.expect("Should execute");
4442 assert_eq!(result.value["k"], 2);
4443 }
4444
4445 #[tokio::test]
4446 async fn test_assignment_swap_variables() {
4447 let code = r#"
4449 let a = 3;
4450 let b = 7;
4451 if (a < b) {
4452 const old_a = a;
4453 a = b;
4454 b = old_a;
4455 }
4456 return { a: a, b: b };
4457 "#;
4458
4459 let mut compiler = PlanCompiler::new();
4460 let plan = compiler.compile_code(code).expect("Should compile");
4461
4462 let mock_http = MockHttpExecutor::new();
4463 let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4464
4465 let result = executor.execute(&plan).await.expect("Should execute");
4466 assert_eq!(result.value["a"], 7);
4467 assert_eq!(result.value["b"], 3);
4468 }
4469
4470 #[cfg(feature = "mcp-code-mode")]
4475 mod mcp_tests {
4476 use super::*;
4477
4478 struct MockCalculatorExecutor;
4480
4481 #[async_trait::async_trait]
4482 impl McpExecutor for MockCalculatorExecutor {
4483 async fn call_tool(
4484 &self,
4485 _server_id: &str,
4486 tool_name: &str,
4487 args: JsonValue,
4488 ) -> Result<JsonValue, ExecutionError> {
4489 match tool_name {
4490 "add" => {
4491 let a = args["a"].as_f64().unwrap_or(0.0);
4492 let b = args["b"].as_f64().unwrap_or(0.0);
4493 Ok(serde_json::json!({"result": a + b}))
4494 },
4495 "subtract" => {
4496 let a = args["a"].as_f64().unwrap_or(0.0);
4497 let b = args["b"].as_f64().unwrap_or(0.0);
4498 Ok(serde_json::json!({"result": a - b}))
4499 },
4500 "multiply" => {
4501 let a = args["a"].as_f64().unwrap_or(0.0);
4502 let b = args["b"].as_f64().unwrap_or(0.0);
4503 Ok(serde_json::json!({"result": a * b}))
4504 },
4505 "divide" => {
4506 let a = args["a"].as_f64().unwrap_or(0.0);
4507 let b = args["b"].as_f64().unwrap_or(1.0);
4508 Ok(serde_json::json!({"result": a / b}))
4509 },
4510 "power" => {
4511 let base = args["base"].as_f64().unwrap_or(0.0);
4512 let exponent = args["exponent"].as_f64().unwrap_or(1.0);
4513 Ok(serde_json::json!({"result": base.powf(exponent)}))
4514 },
4515 "sqrt" => {
4516 let n = args["n"].as_f64().unwrap_or(0.0);
4517 Ok(serde_json::json!({"result": n.sqrt()}))
4518 },
4519 _ => Err(ExecutionError::RuntimeError {
4520 message: format!("Unknown tool: {}", tool_name),
4521 }),
4522 }
4523 }
4524 }
4525
4526 #[tokio::test]
4527 async fn test_mcp_call_simple() {
4528 let code = r#"
4529 const result = await mcp.call('calculator', 'add', { a: 5, b: 3 });
4530 return result;
4531 "#;
4532
4533 let mut compiler = PlanCompiler::new();
4534 let plan = compiler.compile_code(code).expect("Should compile");
4535
4536 let mock_http = MockHttpExecutor::new();
4537 let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4538 executor.set_mcp_executor(MockCalculatorExecutor);
4539
4540 let result = executor.execute(&plan).await.expect("Should execute");
4541 assert_eq!(result.value["result"], 8.0);
4542 }
4543
4544 #[tokio::test]
4545 async fn test_mcp_call_with_args() {
4546 let code = r#"
4548 const result = await mcp.call('calculator', 'add', { a: args.x, b: args.y });
4549 return { sum: result.result };
4550 "#;
4551
4552 let mut compiler = PlanCompiler::new();
4553 let plan = compiler.compile_code(code).expect("Should compile");
4554
4555 let mock_http = MockHttpExecutor::new();
4556 let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4557 executor.set_mcp_executor(MockCalculatorExecutor);
4558 executor.set_variable("args", serde_json::json!({"x": 10, "y": 20}));
4559
4560 let result = executor.execute(&plan).await.expect("Should execute");
4561 assert_eq!(result.value["sum"], 30.0);
4562 }
4563
4564 #[tokio::test]
4565 async fn test_mcp_assignment_in_loop() {
4566 let code = r#"
4568 let result = { result: 1 };
4569 for (const i of [2, 3, 4, 5]) {
4570 const mul = await mcp.call('calculator', 'multiply', { a: result.result, b: i });
4571 result = mul;
4572 }
4573 return { factorial: result.result };
4574 "#;
4575
4576 let mut compiler = PlanCompiler::new();
4577 let plan = compiler.compile_code(code).expect("Should compile");
4578
4579 let mock_http = MockHttpExecutor::new();
4580 let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4581 executor.set_mcp_executor(MockCalculatorExecutor);
4582
4583 let result = executor.execute(&plan).await.expect("Should execute");
4584 assert_eq!(result.value["factorial"], 120.0);
4586 }
4587
4588 #[tokio::test]
4589 async fn test_combinations_c_5_3() {
4590 let code = r#"
4592if (args.k > args.n) {
4593 return { error: 'k must be <= n', n: args.n, k: args.k };
4594}
4595if (args.k === 0 || args.k === args.n) {
4596 return { n: args.n, k: args.k, result: 1 };
4597}
4598let k = args.k;
4599const complement = await mcp.call('calculator', 'subtract', { a: args.n, b: args.k });
4600let nmk = complement.result;
4601if (nmk < k) {
4602 const old_k = k;
4603 k = nmk;
4604 nmk = old_k;
4605}
4606let result = { result: 1 };
4607for (const i of [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]) {
4608 if (i > k) { break; }
4609 const nki = await mcp.call('calculator', 'add', { a: nmk, b: i });
4610 const num = await mcp.call('calculator', 'multiply', { a: result.result, b: nki.result });
4611 result = await mcp.call('calculator', 'divide', { a: num.result, b: i });
4612}
4613return { n: args.n, k: args.k, result: result.result };
4614 "#;
4615
4616 let mut compiler = PlanCompiler::new();
4617 let plan = compiler.compile_code(code).expect("Should compile");
4618
4619 let mock_http = MockHttpExecutor::new();
4620 let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4621 executor.set_mcp_executor(MockCalculatorExecutor);
4622 executor.set_variable("args", serde_json::json!({"n": 5, "k": 3}));
4623
4624 let result = executor.execute(&plan).await.expect("Should execute");
4625 assert_eq!(
4626 result.value["result"], 10.0,
4627 "C(5,3) should be 10, got: {:?}",
4628 result.value
4629 );
4630 }
4631
4632 #[tokio::test]
4633 async fn test_combinations_k_greater_than_n() {
4634 let code = r#"
4636if (args.k > args.n) {
4637 return { error: 'k must be <= n', n: args.n, k: args.k };
4638}
4639return { result: 'should not reach here' };
4640 "#;
4641
4642 let mut compiler = PlanCompiler::new();
4643 let plan = compiler.compile_code(code).expect("Should compile");
4644
4645 let mock_http = MockHttpExecutor::new();
4646 let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4647 executor.set_mcp_executor(MockCalculatorExecutor);
4648 executor.set_variable("args", serde_json::json!({"n": 3, "k": 5}));
4649
4650 let result = executor.execute(&plan).await.expect("Should execute");
4651 assert_eq!(
4652 result.value["error"], "k must be <= n",
4653 "C(3,5) should return error, got: {:?}",
4654 result.value
4655 );
4656 }
4657
4658 #[tokio::test]
4659 async fn test_combinations_c_5_2() {
4660 let code = r#"
4662if (args.k > args.n) {
4663 return { error: 'k must be <= n', n: args.n, k: args.k };
4664}
4665if (args.k === 0 || args.k === args.n) {
4666 return { n: args.n, k: args.k, result: 1 };
4667}
4668let k = args.k;
4669const complement = await mcp.call('calculator', 'subtract', { a: args.n, b: args.k });
4670let nmk = complement.result;
4671if (nmk < k) {
4672 const old_k = k;
4673 k = nmk;
4674 nmk = old_k;
4675}
4676let result = { result: 1 };
4677for (const i of [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]) {
4678 if (i > k) { break; }
4679 const nki = await mcp.call('calculator', 'add', { a: nmk, b: i });
4680 const num = await mcp.call('calculator', 'multiply', { a: result.result, b: nki.result });
4681 result = await mcp.call('calculator', 'divide', { a: num.result, b: i });
4682}
4683return { n: args.n, k: args.k, result: result.result };
4684 "#;
4685
4686 let mut compiler = PlanCompiler::new();
4687 let plan = compiler.compile_code(code).expect("Should compile");
4688
4689 let mock_http = MockHttpExecutor::new();
4690 let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4691 executor.set_mcp_executor(MockCalculatorExecutor);
4692 executor.set_variable("args", serde_json::json!({"n": 5, "k": 2}));
4693
4694 let result = executor.execute(&plan).await.expect("Should execute");
4695 assert_eq!(
4696 result.value["result"], 10.0,
4697 "C(5,2) should be 10, got: {:?}",
4698 result.value
4699 );
4700 }
4701
4702 #[tokio::test]
4703 async fn test_combinations_edge_cases() {
4704 let code = r#"
4705if (args.k === 0 || args.k === args.n) {
4706 return { result: 1 };
4707}
4708return { result: 'not edge case' };
4709 "#;
4710
4711 let mut compiler = PlanCompiler::new();
4712 let plan = compiler.compile_code(code).expect("Should compile");
4713
4714 let mock_http = MockHttpExecutor::new();
4716 let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4717 executor.set_variable("args", serde_json::json!({"n": 5, "k": 0}));
4718 let result = executor.execute(&plan).await.expect("Should execute");
4719 assert_eq!(result.value["result"], 1, "C(5,0) should be 1");
4720
4721 let mock_http = MockHttpExecutor::new();
4723 let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4724 executor.set_variable("args", serde_json::json!({"n": 5, "k": 5}));
4725 let result = executor.execute(&plan).await.expect("Should execute");
4726 assert_eq!(result.value["result"], 1, "C(5,5) should be 1");
4727 }
4728
4729 #[tokio::test]
4730 async fn test_solve_quadratic() {
4731 let code = r#"
4733const b_sq = await mcp.call('calculator', 'power', { base: args.b, exponent: 2 });
4734const four_a = await mcp.call('calculator', 'multiply', { a: 4, b: args.a });
4735const four_ac = await mcp.call('calculator', 'multiply', { a: four_a.result, b: args.c });
4736const discriminant = await mcp.call('calculator', 'subtract', { a: b_sq.result, b: four_ac.result });
4737const root_type = discriminant.result > 0 ? 'two_real'
4738 : discriminant.result === 0 ? 'one_real' : 'complex';
4739if (discriminant.result < 0) {
4740 return { discriminant: discriminant.result, root_type: root_type, roots: [] };
4741}
4742const sqrt_disc = await mcp.call('calculator', 'sqrt', { n: discriminant.result });
4743const neg_b = await mcp.call('calculator', 'multiply', { a: -1, b: args.b });
4744const two_a = await mcp.call('calculator', 'multiply', { a: 2, b: args.a });
4745const x1_num = await mcp.call('calculator', 'add', { a: neg_b.result, b: sqrt_disc.result });
4746const x2_num = await mcp.call('calculator', 'subtract', { a: neg_b.result, b: sqrt_disc.result });
4747const x1 = await mcp.call('calculator', 'divide', { a: x1_num.result, b: two_a.result });
4748const x2 = await mcp.call('calculator', 'divide', { a: x2_num.result, b: two_a.result });
4749return { discriminant: discriminant.result, root_type: root_type, roots: [x1.result, x2.result] };
4750 "#;
4751
4752 let mut compiler = PlanCompiler::new();
4753 let plan = compiler.compile_code(code).expect("Should compile");
4754
4755 let mock_http = MockHttpExecutor::new();
4756 let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4757 executor.set_mcp_executor(MockCalculatorExecutor);
4758 executor.set_variable("args", serde_json::json!({"a": 1, "b": -3, "c": 2}));
4759
4760 let result = executor.execute(&plan).await.expect("Should execute");
4761 assert_eq!(result.value["root_type"], "two_real");
4762 assert_eq!(result.value["discriminant"], 1.0);
4763 let roots = result.value["roots"]
4764 .as_array()
4765 .expect("roots should be array");
4766 assert_eq!(roots.len(), 2);
4767 assert_eq!(roots[0], 2.0);
4768 assert_eq!(roots[1], 1.0);
4769 }
4770 }
4771
4772 #[tokio::test]
4777 async fn test_string_includes() {
4778 let code = r#"
4779 const text = "hello world";
4780 return { found: text.includes("world"), miss: text.includes("xyz") };
4781 "#;
4782
4783 let mut compiler = PlanCompiler::new();
4784 let plan = compiler.compile_code(code).expect("Should compile");
4785
4786 let mock_http = MockHttpExecutor::new();
4787 let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4788 let result = executor.execute(&plan).await.expect("Should execute");
4789
4790 assert_eq!(result.value["found"], true);
4791 assert_eq!(result.value["miss"], false);
4792 }
4793
4794 #[tokio::test]
4795 async fn test_string_index_of() {
4796 let code = r#"
4797 const text = "abcdef";
4798 return { idx: text.indexOf("cd"), miss: text.indexOf("xyz") };
4799 "#;
4800
4801 let mut compiler = PlanCompiler::new();
4802 let plan = compiler.compile_code(code).expect("Should compile");
4803
4804 let mock_http = MockHttpExecutor::new();
4805 let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4806 let result = executor.execute(&plan).await.expect("Should execute");
4807
4808 assert_eq!(result.value["idx"], 2);
4809 assert_eq!(result.value["miss"], -1);
4810 }
4811
4812 #[tokio::test]
4813 async fn test_string_length() {
4814 let code = r#"
4815 const text = "hello";
4816 return { len: text.length };
4817 "#;
4818
4819 let mut compiler = PlanCompiler::new();
4820 let plan = compiler.compile_code(code).expect("Should compile");
4821
4822 let mock_http = MockHttpExecutor::new();
4823 let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4824 let result = executor.execute(&plan).await.expect("Should execute");
4825
4826 assert_eq!(result.value["len"], 5);
4827 }
4828
4829 #[tokio::test]
4830 async fn test_string_slice() {
4831 let code = r#"
4832 const text = "hello world";
4833 return { first: text.slice(0, 5), rest: text.slice(6, 11) };
4834 "#;
4835
4836 let mut compiler = PlanCompiler::new();
4837 let plan = compiler.compile_code(code).expect("Should compile");
4838
4839 let mock_http = MockHttpExecutor::new();
4840 let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4841 let result = executor.execute(&plan).await.expect("Should execute");
4842
4843 assert_eq!(result.value["first"], "hello");
4844 assert_eq!(result.value["rest"], "world");
4845 }
4846
4847 #[tokio::test]
4848 async fn test_string_concat() {
4849 let code = r#"
4850 const greeting = "hello";
4851 return { result: greeting.concat(" world") };
4852 "#;
4853
4854 let mut compiler = PlanCompiler::new();
4855 let plan = compiler.compile_code(code).expect("Should compile");
4856
4857 let mock_http = MockHttpExecutor::new();
4858 let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4859 let result = executor.execute(&plan).await.expect("Should execute");
4860
4861 assert_eq!(result.value["result"], "hello world");
4862 }
4863
4864 #[tokio::test]
4865 async fn test_string_includes_in_filter() {
4866 let code = r#"
4868 const items = [
4869 { name: "TIMESTAMP_2024", desc: "A timestamped record" },
4870 { name: "PERSON_1", desc: "A person entity" },
4871 { name: "TIMESTAMP_2025", desc: "Another timestamped record" }
4872 ];
4873 const timestamped = items.filter(item => item.name.includes("TIMESTAMP"));
4874 return { count: timestamped.length, names: timestamped.map(t => t.name) };
4875 "#;
4876
4877 let mut compiler = PlanCompiler::new();
4878 let plan = compiler.compile_code(code).expect("Should compile");
4879
4880 let mock_http = MockHttpExecutor::new();
4881 let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4882 let result = executor.execute(&plan).await.expect("Should execute");
4883
4884 assert_eq!(result.value["count"], 2);
4885 let names = result.value["names"].as_array().unwrap();
4886 assert_eq!(names[0], "TIMESTAMP_2024");
4887 assert_eq!(names[1], "TIMESTAMP_2025");
4888 }
4889
4890 #[tokio::test]
4891 async fn test_array_includes_still_works() {
4892 let code = r#"
4894 const ids = ["alice", "bob", "charlie"];
4895 return { has_bob: ids.includes("bob"), has_dave: ids.includes("dave") };
4896 "#;
4897
4898 let mut compiler = PlanCompiler::new();
4899 let plan = compiler.compile_code(code).expect("Should compile");
4900
4901 let mock_http = MockHttpExecutor::new();
4902 let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4903 let result = executor.execute(&plan).await.expect("Should execute");
4904
4905 assert_eq!(result.value["has_bob"], true);
4906 assert_eq!(result.value["has_dave"], false);
4907 }
4908
4909 #[test]
4914 fn test_compile_parse_float() {
4915 let code = r#"
4916 const x = parseFloat("3.14");
4917 return x;
4918 "#;
4919 let mut compiler = PlanCompiler::new();
4920 let plan = compiler
4921 .compile_code(code)
4922 .expect("parseFloat should compile");
4923 assert_eq!(plan.steps.len(), 2); }
4925
4926 #[test]
4927 fn test_compile_parse_int() {
4928 let code = r#"
4929 const x = parseInt("42");
4930 return x;
4931 "#;
4932 let mut compiler = PlanCompiler::new();
4933 compiler
4934 .compile_code(code)
4935 .expect("parseInt should compile");
4936 }
4937
4938 #[test]
4939 fn test_compile_math_abs() {
4940 let code = r#"
4941 const x = Math.abs(-5);
4942 return x;
4943 "#;
4944 let mut compiler = PlanCompiler::new();
4945 compiler
4946 .compile_code(code)
4947 .expect("Math.abs should compile");
4948 }
4949
4950 #[test]
4951 fn test_compile_math_max() {
4952 let code = r#"
4953 const x = Math.max(1, 2, 3);
4954 return x;
4955 "#;
4956 let mut compiler = PlanCompiler::new();
4957 compiler
4958 .compile_code(code)
4959 .expect("Math.max should compile");
4960 }
4961
4962 #[test]
4963 fn test_compile_object_keys() {
4964 let code = r#"
4965 const obj = { a: 1, b: 2 };
4966 const keys = Object.keys(obj);
4967 return keys;
4968 "#;
4969 let mut compiler = PlanCompiler::new();
4970 compiler
4971 .compile_code(code)
4972 .expect("Object.keys should compile");
4973 }
4974
4975 #[test]
4976 fn test_compile_object_entries() {
4977 let code = r#"
4978 const obj = { x: 10 };
4979 const entries = Object.entries(obj);
4980 return entries;
4981 "#;
4982 let mut compiler = PlanCompiler::new();
4983 compiler
4984 .compile_code(code)
4985 .expect("Object.entries should compile");
4986 }
4987
4988 #[test]
4989 fn test_compile_unary_plus() {
4990 let code = r#"
4991 const x = +"42";
4992 return x;
4993 "#;
4994 let mut compiler = PlanCompiler::new();
4995 compiler.compile_code(code).expect("unary + should compile");
4996 }
4997
4998 #[test]
4999 fn test_compile_sort_with_comparator() {
5000 let code = r#"
5001 const arr = [3, 1, 2];
5002 const sorted = arr.sort((a, b) => a - b);
5003 return sorted;
5004 "#;
5005 let mut compiler = PlanCompiler::new();
5006 compiler
5007 .compile_code(code)
5008 .expect("sort with comparator should compile");
5009 }
5010
5011 #[test]
5012 fn test_compile_sort_without_comparator() {
5013 let code = r#"
5014 const arr = ["b", "a", "c"];
5015 const sorted = arr.sort();
5016 return sorted;
5017 "#;
5018 let mut compiler = PlanCompiler::new();
5019 compiler
5020 .compile_code(code)
5021 .expect("sort without comparator should compile");
5022 }
5023
5024 #[tokio::test]
5029 async fn test_execute_parse_float() {
5030 let code = r#"
5031 const x = parseFloat("3.14");
5032 return x;
5033 "#;
5034 let mut compiler = PlanCompiler::new();
5035 let plan = compiler.compile_code(code).unwrap();
5036 let mock_http = MockHttpExecutor::new();
5037 let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
5038 let result = executor.execute(&plan).await.unwrap();
5039 #[allow(clippy::approx_constant)]
5042 let expected = serde_json::json!(3.14);
5043 assert_eq!(result.value, expected);
5044 }
5045
5046 #[tokio::test]
5047 async fn test_execute_math_abs_and_sort() {
5048 let code = r#"
5049 const items = [
5050 { name: "a", val: -5 },
5051 { name: "b", val: 3 },
5052 { name: "c", val: -1 }
5053 ];
5054 const sorted = items.sort((a, b) => Math.abs(b.val) - Math.abs(a.val));
5055 return sorted.map(x => x.name);
5056 "#;
5057 let mut compiler = PlanCompiler::new();
5058 let plan = compiler.compile_code(code).unwrap();
5059 let mock_http = MockHttpExecutor::new();
5060 let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
5061 let result = executor.execute(&plan).await.unwrap();
5062 assert_eq!(result.value, serde_json::json!(["a", "b", "c"]));
5063 }
5064
5065 #[tokio::test]
5066 async fn test_execute_object_keys() {
5067 let code = r#"
5068 const obj = { x: 1, y: 2, z: 3 };
5069 return Object.keys(obj).length;
5070 "#;
5071 let mut compiler = PlanCompiler::new();
5072 let plan = compiler.compile_code(code).unwrap();
5073 let mock_http = MockHttpExecutor::new();
5074 let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
5075 let result = executor.execute(&plan).await.unwrap();
5076 assert_eq!(result.value, serde_json::json!(3));
5077 }
5078
5079 #[tokio::test]
5080 async fn test_execute_unary_plus() {
5081 let code = r#"
5082 const x = +"42";
5083 return x;
5084 "#;
5085 let mut compiler = PlanCompiler::new();
5086 let plan = compiler.compile_code(code).unwrap();
5087 let mock_http = MockHttpExecutor::new();
5088 let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
5089 let result = executor.execute(&plan).await.unwrap();
5090 assert_eq!(result.value, serde_json::json!(42.0));
5091 }
5092
5093 #[tokio::test]
5094 async fn test_execute_number_cast() {
5095 let code = r#"
5096 const x = Number("99.5");
5097 return x;
5098 "#;
5099 let mut compiler = PlanCompiler::new();
5100 let plan = compiler.compile_code(code).unwrap();
5101 let mock_http = MockHttpExecutor::new();
5102 let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
5103 let result = executor.execute(&plan).await.unwrap();
5104 assert_eq!(result.value, serde_json::json!(99.5));
5105 }
5106
5107 #[tokio::test]
5108 async fn test_execute_math_round_floor_ceil() {
5109 let code = r#"
5110 return {
5111 round: Math.round(3.7),
5112 floor: Math.floor(3.7),
5113 ceil: Math.ceil(3.2)
5114 };
5115 "#;
5116 let mut compiler = PlanCompiler::new();
5117 let plan = compiler.compile_code(code).unwrap();
5118 let mock_http = MockHttpExecutor::new();
5119 let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
5120 let result = executor.execute(&plan).await.unwrap();
5121 assert_eq!(result.value["round"], serde_json::json!(4.0));
5122 assert_eq!(result.value["floor"], serde_json::json!(3.0));
5123 assert_eq!(result.value["ceil"], serde_json::json!(4.0));
5124 }
5125
5126 #[test]
5131 fn test_compile_object_spread_basic() {
5132 let code = r#"
5133 const base = { id: 1, name: "Alice" };
5134 const extended = { ...base, age: 30 };
5135 return extended;
5136 "#;
5137 let mut compiler = PlanCompiler::new();
5138 let plan = compiler
5139 .compile_code(code)
5140 .expect("Object spread should compile");
5141 assert!(plan.steps.len() >= 2);
5142 }
5143
5144 #[tokio::test]
5145 async fn test_execute_object_spread_basic() {
5146 let code = r#"
5147 const base = { id: 1, name: "Alice" };
5148 const extended = { ...base, age: 30 };
5149 return extended;
5150 "#;
5151 let mut compiler = PlanCompiler::new();
5152 let plan = compiler.compile_code(code).unwrap();
5153 let mock_http = MockHttpExecutor::new();
5154 let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
5155 let result = executor.execute(&plan).await.unwrap();
5156 assert_eq!(result.value["id"], serde_json::json!(1));
5157 assert_eq!(result.value["name"], serde_json::json!("Alice"));
5158 assert_eq!(result.value["age"], serde_json::json!(30));
5159 }
5160
5161 #[tokio::test]
5162 async fn test_execute_object_spread_override() {
5163 let code = r#"
5165 const obj = { id: 1, name: "old" };
5166 const updated = { ...obj, name: "new" };
5167 return updated;
5168 "#;
5169 let mut compiler = PlanCompiler::new();
5170 let plan = compiler.compile_code(code).unwrap();
5171 let mock_http = MockHttpExecutor::new();
5172 let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
5173 let result = executor.execute(&plan).await.unwrap();
5174 assert_eq!(result.value["id"], serde_json::json!(1));
5175 assert_eq!(result.value["name"], serde_json::json!("new"));
5176 }
5177
5178 #[tokio::test]
5179 async fn test_execute_object_spread_multiple() {
5180 let code = r#"
5181 const a = { x: 1 };
5182 const b = { y: 2 };
5183 const merged = { ...a, ...b, z: 3 };
5184 return merged;
5185 "#;
5186 let mut compiler = PlanCompiler::new();
5187 let plan = compiler.compile_code(code).unwrap();
5188 let mock_http = MockHttpExecutor::new();
5189 let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
5190 let result = executor.execute(&plan).await.unwrap();
5191 assert_eq!(result.value["x"], serde_json::json!(1));
5192 assert_eq!(result.value["y"], serde_json::json!(2));
5193 assert_eq!(result.value["z"], serde_json::json!(3));
5194 }
5195
5196 #[tokio::test]
5197 async fn test_execute_object_spread_with_api_result() {
5198 let code = r#"
5200 const config = await api.get('/config');
5201 const result = { ...config, extra: "added" };
5202 return result;
5203 "#;
5204 let mut compiler = PlanCompiler::new();
5205 let plan = compiler.compile_code(code).unwrap();
5206 let mut mock_http = MockHttpExecutor::new();
5207 mock_http.add_response(
5208 "/config",
5209 serde_json::json!({ "key": "value", "enabled": true }),
5210 );
5211 let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
5212 let result = executor.execute(&plan).await.unwrap();
5213 assert_eq!(result.value["key"], serde_json::json!("value"));
5214 assert_eq!(result.value["enabled"], serde_json::json!(true));
5215 assert_eq!(result.value["extra"], serde_json::json!("added"));
5216 }
5217
5218 #[tokio::test]
5219 async fn test_execute_object_spread_non_object_noop() {
5220 let code = r#"
5222 const x = 42;
5223 const obj = { ...x, name: "test" };
5224 return obj;
5225 "#;
5226 let mut compiler = PlanCompiler::new();
5227 let plan = compiler.compile_code(code).unwrap();
5228 let mock_http = MockHttpExecutor::new();
5229 let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
5230 let result = executor.execute(&plan).await.unwrap();
5231 assert_eq!(result.value["name"], serde_json::json!("test"));
5232 assert!(result.value.as_object().unwrap().len() == 1);
5234 }
5235
5236 #[tokio::test]
5237 async fn test_execute_object_spread_preserves_order() {
5238 let code = r#"
5241 const obj = { a: 1, b: 2 };
5242 const result = { b: 99, ...obj, a: 100 };
5243 return result;
5244 "#;
5245 let mut compiler = PlanCompiler::new();
5246 let plan = compiler.compile_code(code).unwrap();
5247 let mock_http = MockHttpExecutor::new();
5248 let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
5249 let result = executor.execute(&plan).await.unwrap();
5250 assert_eq!(result.value["a"], serde_json::json!(100));
5252 assert_eq!(result.value["b"], serde_json::json!(2));
5253 }
5254
5255 #[test]
5260 fn test_compile_object_destructuring_simple() {
5261 let code = r#"
5262 const obj = { id: 1, name: "Alice" };
5263 const { id, name } = obj;
5264 return { id, name };
5265 "#;
5266 let mut compiler = PlanCompiler::new();
5267 let plan = compiler
5268 .compile_code(code)
5269 .expect("Object destructuring should compile");
5270 assert!(plan.steps.len() >= 4);
5272 }
5273
5274 #[tokio::test]
5275 async fn test_execute_object_destructuring_simple() {
5276 let code = r#"
5277 const obj = { id: 1, name: "Alice", extra: "ignored" };
5278 const { id, name } = obj;
5279 return { id, name };
5280 "#;
5281 let mut compiler = PlanCompiler::new();
5282 let plan = compiler.compile_code(code).unwrap();
5283 let mock_http = MockHttpExecutor::new();
5284 let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
5285 let result = executor.execute(&plan).await.unwrap();
5286 assert_eq!(result.value["id"], serde_json::json!(1));
5287 assert_eq!(result.value["name"], serde_json::json!("Alice"));
5288 assert!(result.value.get("extra").is_none());
5290 }
5291
5292 #[tokio::test]
5293 async fn test_execute_object_destructuring_renamed() {
5294 let code = r#"
5295 const user = { id: 1, name: "Alice" };
5296 const { id: userId, name: userName } = user;
5297 return { userId, userName };
5298 "#;
5299 let mut compiler = PlanCompiler::new();
5300 let plan = compiler.compile_code(code).unwrap();
5301 let mock_http = MockHttpExecutor::new();
5302 let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
5303 let result = executor.execute(&plan).await.unwrap();
5304 assert_eq!(result.value["userId"], serde_json::json!(1));
5305 assert_eq!(result.value["userName"], serde_json::json!("Alice"));
5306 }
5307
5308 #[tokio::test]
5309 async fn test_execute_object_destructuring_with_api_call() {
5310 let code = r#"
5312 const { data, status } = await api.get('/users');
5313 return { data, status };
5314 "#;
5315 let mut compiler = PlanCompiler::new();
5316 let plan = compiler.compile_code(code).unwrap();
5317 let mut mock_http = MockHttpExecutor::new();
5318 mock_http.add_response(
5319 "/users",
5320 serde_json::json!({ "data": [{"id": 1}], "status": "ok", "meta": "hidden" }),
5321 );
5322 let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
5323 let result = executor.execute(&plan).await.unwrap();
5324 assert_eq!(result.value["data"], serde_json::json!([{"id": 1}]));
5325 assert_eq!(result.value["status"], serde_json::json!("ok"));
5326 }
5327
5328 #[tokio::test]
5329 async fn test_execute_object_destructuring_missing_property() {
5330 let code = r#"
5332 const obj = { id: 1 };
5333 const { id, name } = obj;
5334 return { id, name };
5335 "#;
5336 let mut compiler = PlanCompiler::new();
5337 let plan = compiler.compile_code(code).unwrap();
5338 let mock_http = MockHttpExecutor::new();
5339 let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
5340 let result = executor.execute(&plan).await.unwrap();
5341 assert_eq!(result.value["id"], serde_json::json!(1));
5342 assert_eq!(result.value["name"], serde_json::json!(null));
5343 }
5344
5345 #[tokio::test]
5350 async fn test_execute_array_destructuring_simple() {
5351 let code = r#"
5352 const arr = [10, 20, 30];
5353 const [a, b] = arr;
5354 return { a, b };
5355 "#;
5356 let mut compiler = PlanCompiler::new();
5357 let plan = compiler.compile_code(code).unwrap();
5358 let mock_http = MockHttpExecutor::new();
5359 let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
5360 let result = executor.execute(&plan).await.unwrap();
5361 assert_eq!(result.value["a"], serde_json::json!(10));
5362 assert_eq!(result.value["b"], serde_json::json!(20));
5363 }
5364
5365 #[tokio::test]
5366 async fn test_execute_array_destructuring_with_promise_all() {
5367 let code = r#"
5369 const [users, products] = await Promise.all([
5370 api.get('/users'),
5371 api.get('/products')
5372 ]);
5373 return { users, products };
5374 "#;
5375 let mut compiler = PlanCompiler::new();
5376 let plan = compiler.compile_code(code).unwrap();
5377 let mut mock_http = MockHttpExecutor::new();
5378 mock_http.add_response("/users", serde_json::json!([{"id": 1}]));
5379 mock_http.add_response("/products", serde_json::json!([{"sku": "A"}]));
5380 let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
5381 let result = executor.execute(&plan).await.unwrap();
5382 assert_eq!(result.value["users"], serde_json::json!([{"id": 1}]));
5383 assert_eq!(result.value["products"], serde_json::json!([{"sku": "A"}]));
5384 }
5385
5386 #[test]
5391 fn test_compile_for_of_destructuring() {
5392 let code = r#"
5393 const items = [{ id: 1, name: "A" }, { id: 2, name: "B" }];
5394 const results = [];
5395 for (const { id, name } of items.slice(0, 10)) {
5396 results.push({ id, name });
5397 }
5398 return results;
5399 "#;
5400 let mut compiler = PlanCompiler::new();
5401 compiler
5402 .compile_code(code)
5403 .expect("For-of with destructuring should compile");
5404 }
5405
5406 #[tokio::test]
5407 async fn test_execute_for_of_destructuring() {
5408 let code = r#"
5409 const items = [{ id: 1, name: "A" }, { id: 2, name: "B" }];
5410 const results = [];
5411 for (const { id, name } of items.slice(0, 10)) {
5412 results.push({ label: name, num: id });
5413 }
5414 return results;
5415 "#;
5416 let mut compiler = PlanCompiler::new();
5417 let plan = compiler.compile_code(code).unwrap();
5418 let mock_http = MockHttpExecutor::new();
5419 let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
5420 let result = executor.execute(&plan).await.unwrap();
5421 let arr = result.value.as_array().unwrap();
5422 assert_eq!(arr.len(), 2);
5423 assert_eq!(arr[0]["label"], serde_json::json!("A"));
5424 assert_eq!(arr[0]["num"], serde_json::json!(1));
5425 assert_eq!(arr[1]["label"], serde_json::json!("B"));
5426 assert_eq!(arr[1]["num"], serde_json::json!(2));
5427 }
5428
5429 #[tokio::test]
5430 async fn test_execute_for_of_destructuring_with_api_calls() {
5431 let code = r#"
5433 const users = [{ id: 1, role: "admin" }, { id: 2, role: "user" }];
5434 const results = [];
5435 for (const { id, role } of users.slice(0, 10)) {
5436 const detail = await api.get(`/users/${id}`);
5437 results.push({ role, detail });
5438 }
5439 return results;
5440 "#;
5441 let mut compiler = PlanCompiler::new();
5442 let plan = compiler.compile_code(code).unwrap();
5443 let mut mock_http = MockHttpExecutor::new();
5444 mock_http.add_response("/users/1", serde_json::json!({ "name": "Alice" }));
5445 mock_http.add_response("/users/2", serde_json::json!({ "name": "Bob" }));
5446 let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
5447 let result = executor.execute(&plan).await.unwrap();
5448 let arr = result.value.as_array().unwrap();
5449 assert_eq!(arr.len(), 2);
5450 assert_eq!(arr[0]["role"], serde_json::json!("admin"));
5451 assert_eq!(arr[0]["detail"]["name"], serde_json::json!("Alice"));
5452 assert_eq!(arr[1]["role"], serde_json::json!("user"));
5453 assert_eq!(arr[1]["detail"]["name"], serde_json::json!("Bob"));
5454 }
5455
5456 #[tokio::test]
5461 async fn test_execute_spread_and_destructuring_combined() {
5462 let code = r#"
5464 const { data, token } = await api.get('/auth');
5465 const result = await api.post('/action', { ...data, token });
5466 return result;
5467 "#;
5468 let mut compiler = PlanCompiler::new();
5469 let plan = compiler.compile_code(code).unwrap();
5470 let mut mock_http = MockHttpExecutor::new();
5471 mock_http.add_response(
5472 "/auth",
5473 serde_json::json!({ "data": { "user": "alice" }, "token": "abc123" }),
5474 );
5475 mock_http.add_response("/action", serde_json::json!({ "success": true }));
5476 let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
5477 let result = executor.execute(&plan).await.unwrap();
5478 assert_eq!(result.value["success"], serde_json::json!(true));
5479 }
5480}
5481
5482#[cfg(test)]
5496mod d09_support {
5497 use super::*;
5498 use std::sync::{Arc, Mutex};
5499
5500 #[derive(Debug, Clone)]
5502 pub(super) struct Seen {
5503 pub(super) method: String,
5504 pub(super) path: String,
5505 pub(super) body: Option<JsonValue>,
5506 }
5507
5508 pub(super) struct RecordingHttp {
5511 seen: Arc<Mutex<Vec<Seen>>>,
5512 response: JsonValue,
5513 }
5514
5515 impl RecordingHttp {
5516 pub(super) fn new() -> (Self, Arc<Mutex<Vec<Seen>>>) {
5518 let seen = Arc::new(Mutex::new(Vec::new()));
5519 (
5520 Self {
5521 seen: Arc::clone(&seen),
5522 response: JsonValue::Object(serde_json::Map::new()),
5523 },
5524 seen,
5525 )
5526 }
5527 }
5528
5529 #[async_trait::async_trait]
5530 impl HttpExecutor for RecordingHttp {
5531 async fn execute_request(
5532 &self,
5533 method: &str,
5534 path: ResolvedPath<'_>,
5535 body: Option<JsonValue>,
5536 ) -> Result<JsonValue, ExecutionError> {
5537 self.seen.lock().unwrap().push(Seen {
5538 method: method.to_string(),
5539 path: path.as_str().to_string(),
5540 body,
5541 });
5542 Ok(self.response.clone())
5543 }
5544 }
5545
5546 pub(super) struct FailingHttp;
5548
5549 #[async_trait::async_trait]
5550 impl HttpExecutor for FailingHttp {
5551 async fn execute_request(
5552 &self,
5553 _method: &str,
5554 _path: ResolvedPath<'_>,
5555 _body: Option<JsonValue>,
5556 ) -> Result<JsonValue, ExecutionError> {
5557 Err(ExecutionError::RuntimeError {
5558 message: "simulated transport failure".to_string(),
5559 })
5560 }
5561 }
5562
5563 pub(super) fn plan(steps: Vec<PlanStep>) -> ExecutionPlan {
5564 ExecutionPlan {
5565 steps,
5566 metadata: PlanMetadata {
5567 api_call_count: 0,
5568 has_mutations: false,
5569 endpoints: Vec::new(),
5570 methods_used: Vec::new(),
5571 },
5572 }
5573 }
5574
5575 pub(super) fn get_step(parts: Vec<PathPart>, body: Option<JsonValue>) -> PlanStep {
5577 PlanStep::ApiCall {
5578 result_var: "out".to_string(),
5579 method: "GET".to_string(),
5580 path: PathTemplate { parts },
5581 body: body.map(ValueExpr::Literal),
5582 }
5583 }
5584
5585 pub(super) fn literal(path: &str) -> Vec<PathPart> {
5587 vec![PathPart::Literal(path.to_string())]
5588 }
5589}
5590
5591#[cfg(test)]
5598mod layer_one {
5599 use super::d09_support::{get_step, literal, plan, RecordingHttp};
5600 use super::*;
5601
5602 async fn run_with_var(
5603 var: &str,
5604 value: JsonValue,
5605 ) -> (Result<ExecutionResult, ExecutionError>, usize) {
5606 let (http, seen) = RecordingHttp::new();
5607 let step = get_step(
5608 vec![
5609 PathPart::Literal("/search/".to_string()),
5610 PathPart::Variable(var.to_string()),
5611 ],
5612 None,
5613 );
5614 let mut executor = PlanExecutor::new(http, ExecutionConfig::default());
5615 executor.set_variable(var, value);
5616 let result = executor.execute(&plan(vec![step])).await;
5617 let count = seen.lock().unwrap().len();
5618 (result, count)
5619 }
5620
5621 #[tokio::test]
5622 async fn layer_one_refuses_a_query_separator_in_a_variable_part() {
5623 let payload = format!("2026AA?string={}", "z".repeat(60));
5624 let (result, calls) = run_with_var("lookupKey", JsonValue::String(payload.clone())).await;
5625 let err = result.expect_err("a query separator in a ${var} part must be refused");
5626 let rendered = err.to_string();
5627 assert_eq!(
5628 calls, 0,
5629 "no upstream request may be dispatched: {rendered}"
5630 );
5631 assert!(
5632 rendered.contains("lookupKey"),
5633 "a Variable part's refusal names its identifier, which is what proves \
5634 the refusal came from LAYER 1 and not from the composed check: {rendered}"
5635 );
5636 assert!(
5637 !rendered.contains("2026AA") && !rendered.contains('?'),
5638 "the refusal must carry no byte of the value: {rendered}"
5639 );
5640 }
5641
5642 #[tokio::test]
5643 async fn layer_one_refuses_parent_traversal_in_a_variable_part() {
5644 let (result, calls) =
5645 run_with_var("lookupKey", JsonValue::String("../etc".to_string())).await;
5646 let rendered = result
5647 .expect_err("parent traversal in a ${var} part must be refused")
5648 .to_string();
5649 assert_eq!(
5650 calls, 0,
5651 "no upstream request may be dispatched: {rendered}"
5652 );
5653 assert!(rendered.contains("lookupKey"), "{rendered}");
5654 }
5655
5656 #[tokio::test]
5657 async fn layer_one_refuses_an_over_cap_variable_part() {
5658 let over = "a".repeat(crate::PLACEHOLDER_MAX_LENGTH + 1);
5659 let (result, calls) = run_with_var("lookupKey", JsonValue::String(over)).await;
5660 let rendered = result
5661 .expect_err("an over-cap ${var} part must be refused")
5662 .to_string();
5663 assert_eq!(
5664 calls, 0,
5665 "no upstream request may be dispatched: {rendered}"
5666 );
5667 assert!(rendered.contains("lookupKey"), "{rendered}");
5668 }
5669
5670 #[tokio::test]
5671 async fn layer_one_refuses_a_query_separator_in_an_expression_part() {
5672 let (http, seen) = RecordingHttp::new();
5676 let step = get_step(
5677 vec![
5678 PathPart::Literal("/search/".to_string()),
5679 PathPart::Expression(ValueExpr::PropertyAccess {
5680 object: Box::new(ValueExpr::Variable("holder".to_string())),
5681 property: "secretField".to_string(),
5682 }),
5683 ],
5684 None,
5685 );
5686 let mut executor = PlanExecutor::new(http, ExecutionConfig::default());
5687 executor.set_variable(
5688 "holder",
5689 serde_json::json!({ "secretField": "2026AA?string=payload" }),
5690 );
5691 let rendered = executor
5692 .execute(&plan(vec![step]))
5693 .await
5694 .expect_err("a query separator in an ${expr} part must be refused")
5695 .to_string();
5696 assert_eq!(seen.lock().unwrap().len(), 0, "{rendered}");
5697 assert!(
5698 rendered.contains("path expression"),
5699 "an Expression part uses a fixed positional descriptor: {rendered}"
5700 );
5701 for forbidden in ["2026AA", "payload", "secretField", "holder", "?"] {
5702 assert!(
5703 !rendered.contains(forbidden),
5704 "the refusal must carry neither the evaluated value nor a rendering \
5705 of the expression body; found {forbidden:?} in {rendered:?}"
5706 );
5707 }
5708 }
5709
5710 #[tokio::test]
5711 async fn layer_one_accepts_a_literal_only_template_including_slashes() {
5712 let (http, seen) = RecordingHttp::new();
5715 let step = get_step(literal("/Line/Mode/tube/Status"), None);
5716 let mut executor = PlanExecutor::new(http, ExecutionConfig::default());
5717 executor
5718 .execute(&plan(vec![step]))
5719 .await
5720 .expect("a literal-only template with slashes must still resolve");
5721 let seen = seen.lock().unwrap();
5722 assert_eq!(seen.len(), 1);
5723 assert_eq!(seen[0].path, "/Line/Mode/tube/Status");
5724 }
5725}
5726
5727#[cfg(test)]
5729mod layer_two {
5730 use super::d09_support::{get_step, literal, plan, RecordingHttp};
5731 use super::*;
5732
5733 async fn run(
5734 path: &str,
5735 body: JsonValue,
5736 ) -> (
5737 Result<ExecutionResult, ExecutionError>,
5738 Vec<super::d09_support::Seen>,
5739 ) {
5740 let (http, seen) = RecordingHttp::new();
5741 let step = get_step(literal(path), Some(body));
5742 let mut executor = PlanExecutor::new(http, ExecutionConfig::default());
5743 let result = executor.execute(&plan(vec![step])).await;
5744 let seen = seen.lock().unwrap().clone();
5745 (result, seen)
5746 }
5747
5748 #[tokio::test]
5749 async fn layer_two_substitutes_and_removes_the_consumed_key_from_the_body() {
5750 let (result, seen) = run("/users/{v}", serde_json::json!({"v": "7", "q": "keep"})).await;
5751 result.expect("a conforming placeholder must resolve");
5752 assert_eq!(seen.len(), 1);
5753 assert_eq!(seen[0].path, "/users/7");
5754 assert_eq!(
5755 seen[0].body,
5756 Some(serde_json::json!({"q": "keep"})),
5757 "the path-consumed key must not also reach the body"
5758 );
5759 }
5760
5761 #[tokio::test]
5773 async fn layer_two_value_cannot_manufacture_a_placeholder_for_a_later_key() {
5774 let (result, seen) = run(
5775 "/p/{a}/q/{b}",
5776 serde_json::json!({"a": "x{b}y", "b": "zzz"}),
5777 )
5778 .await;
5779 let rendered = result
5780 .expect_err("a value's literal `{b}` must never be expanded as a placeholder")
5781 .to_string();
5782 assert!(
5783 seen.is_empty(),
5784 "no upstream request may be dispatched: {rendered}"
5785 );
5786 assert!(
5793 !rendered.contains("zzz"),
5794 "the refusal must carry no byte of any value: {rendered}"
5795 );
5796 }
5797
5798 #[tokio::test]
5799 async fn layer_two_conforming_call_produces_exactly_one_request() {
5800 let (result, seen) = run("/users/{v}", serde_json::json!({"v": "ada"})).await;
5801 result.expect("a conforming placeholder must resolve");
5802 assert_eq!(seen.len(), 1, "exactly one upstream request");
5803 assert_eq!(seen[0].method, "GET");
5804 assert_eq!(seen[0].path, "/users/ada");
5805 }
5806
5807 #[tokio::test]
5808 async fn layer_two_refuses_a_query_separator_in_a_placeholder_value() {
5809 let payload = format!("2026AA?string={}", "z".repeat(60));
5810 let (result, seen) = run("/search/{v}", serde_json::json!({"v": payload})).await;
5811 let rendered = result
5812 .expect_err("a query separator in a {key} value must be refused")
5813 .to_string();
5814 assert!(
5815 seen.is_empty(),
5816 "no upstream request may be dispatched: {rendered}"
5817 );
5818 assert!(
5819 !rendered.contains("2026AA") && !rendered.contains('?'),
5820 "the refusal must carry no byte of the value: {rendered}"
5821 );
5822 }
5823
5824 #[tokio::test]
5825 async fn layer_two_refuses_parent_traversal_in_a_placeholder_value() {
5826 let (result, seen) = run("/files/{v}", serde_json::json!({"v": "../../etc/passwd"})).await;
5827 let rendered = result
5828 .expect_err("parent traversal in a {key} value must be refused")
5829 .to_string();
5830 assert!(seen.is_empty(), "{rendered}");
5831 assert!(!rendered.contains("passwd"), "{rendered}");
5832 }
5833
5834 #[tokio::test]
5835 async fn layer_two_refuses_an_object_valued_placeholder_naming_the_key_only() {
5836 let (result, seen) = run(
5839 "/users/{userId}",
5840 serde_json::json!({"userId": {"nested": [1, 2]}}),
5841 )
5842 .await;
5843 let rendered = result
5844 .expect_err("a non-scalar {key} value must be refused")
5845 .to_string();
5846 assert!(seen.is_empty(), "{rendered}");
5847 assert!(rendered.contains("userId"), "must name the key: {rendered}");
5848 for forbidden in ['{', '[', '"'] {
5849 assert!(
5850 !rendered.contains(forbidden),
5851 "must not echo JSON: {rendered}"
5852 );
5853 }
5854 }
5855
5856 #[tokio::test]
5857 async fn layer_two_refuses_an_unsubstituted_placeholder_before_dispatch() {
5858 let (result, seen) = run("/users/{missing}", serde_json::json!({"other": "x"})).await;
5861 let rendered = result
5862 .expect_err("an unsubstituted placeholder must be refused")
5863 .to_string();
5864 assert!(
5865 seen.is_empty(),
5866 "literal braces must never reach the wire: {rendered}"
5867 );
5868 }
5869
5870 #[tokio::test]
5871 async fn layer_two_aborts_before_any_substitution_reaches_a_request() {
5872 let (result, seen) = run(
5875 "/a/{good}/b/{bad}",
5876 serde_json::json!({"good": "ok", "bad": "../escape"}),
5877 )
5878 .await;
5879 let rendered = result
5880 .expect_err("a refusal on any placeholder aborts the step")
5881 .to_string();
5882 assert!(seen.is_empty(), "{rendered}");
5883 }
5884}
5885
5886#[cfg(test)]
5892mod composition {
5893 use super::d09_support::{get_step, plan, RecordingHttp};
5894 use super::*;
5895
5896 #[tokio::test]
5897 async fn composition_refuses_two_adjacent_values_over_the_cap_that_each_pass_alone() {
5898 let first = "a".repeat(180);
5899 let second = "b".repeat(200);
5900
5901 let rules = crate::PlaceholderRules::default();
5905 crate::validate_path_placeholder("a", &first, &rules)
5906 .expect("180 code points is under the cap and must pass alone");
5907 crate::validate_path_placeholder("b", &second, &rules)
5908 .expect("200 code points is under the cap and must pass alone");
5909
5910 let (http, seen) = RecordingHttp::new();
5912 let step = get_step(
5913 super::d09_support::literal("/search/{a}{b}"),
5914 Some(serde_json::json!({"a": first, "b": second})),
5915 );
5916 let mut executor = PlanExecutor::new(http, ExecutionConfig::default());
5917 let rendered = executor
5918 .execute(&plan(vec![step]))
5919 .await
5920 .expect_err("380 composed code points exceed the cap and must be refused")
5921 .to_string();
5922 assert!(seen.lock().unwrap().is_empty(), "{rendered}");
5923 assert!(
5924 !rendered.contains("aaaa") && !rendered.contains("bbbb"),
5925 "the composed refusal must carry no byte of either value: {rendered}"
5926 );
5927 }
5928
5929 #[tokio::test]
5930 async fn composition_refuses_traversal_assembled_from_unchecked_literal_parts() {
5931 let (http, seen) = RecordingHttp::new();
5936 let step = get_step(
5937 vec![
5938 PathPart::Literal("/a/.".to_string()),
5939 PathPart::Literal(".".to_string()),
5940 PathPart::Literal("/b".to_string()),
5941 ],
5942 None,
5943 );
5944 let mut executor = PlanExecutor::new(http, ExecutionConfig::default());
5945 let rendered = executor
5946 .execute(&plan(vec![step]))
5947 .await
5948 .expect_err("a composed `..` segment must be refused")
5949 .to_string();
5950 assert!(seen.lock().unwrap().is_empty(), "{rendered}");
5951 }
5952
5953 #[tokio::test]
5954 async fn composition_refuses_two_adjacent_single_dot_placeholders() {
5955 let (http, seen) = RecordingHttp::new();
5959 let step = get_step(
5960 super::d09_support::literal("/a/{x}{y}/b"),
5961 Some(serde_json::json!({"x": ".", "y": "."})),
5962 );
5963 let mut executor = PlanExecutor::new(http, ExecutionConfig::default());
5964 let rendered = executor
5965 .execute(&plan(vec![step]))
5966 .await
5967 .expect_err("`.` + `.` composes to traversal and must be refused")
5968 .to_string();
5969 assert!(seen.lock().unwrap().is_empty(), "{rendered}");
5970 }
5971
5972 #[tokio::test]
5973 async fn composition_refuses_a_mixed_layer_one_and_layer_two_over_cap_segment() {
5974 let first = "a".repeat(180);
5977 let second = "b".repeat(200);
5978 let rules = crate::PlaceholderRules::default();
5979 crate::validate_path_placeholder("lookupKey", &first, &rules)
5980 .expect("the layer-1 contribution passes alone");
5981 crate::validate_path_placeholder("b", &second, &rules)
5982 .expect("the layer-2 contribution passes alone");
5983
5984 let (http, seen) = RecordingHttp::new();
5985 let step = get_step(
5986 vec![
5987 PathPart::Literal("/search/".to_string()),
5988 PathPart::Variable("lookupKey".to_string()),
5989 PathPart::Literal("{b}".to_string()),
5990 ],
5991 Some(serde_json::json!({"b": second})),
5992 );
5993 let mut executor = PlanExecutor::new(http, ExecutionConfig::default());
5994 executor.set_variable("lookupKey", JsonValue::String(first));
5995 let rendered = executor
5996 .execute(&plan(vec![step]))
5997 .await
5998 .expect_err("a mixed-layer composed segment over the cap must be refused")
5999 .to_string();
6000 assert!(seen.lock().unwrap().is_empty(), "{rendered}");
6001 }
6002}
6003
6004#[cfg(test)]
6009mod error_does_not_echo_path {
6010 use super::d09_support::{get_step, literal, plan, FailingHttp};
6011 use super::*;
6012
6013 #[tokio::test]
6014 async fn error_does_not_echo_path_when_the_executor_itself_fails() {
6015 let step = get_step(literal("/secret/inventory/endpoint"), None);
6016 let mut executor = PlanExecutor::new(FailingHttp, ExecutionConfig::default());
6017 let rendered = executor
6018 .execute(&plan(vec![step]))
6019 .await
6020 .expect_err("the failing executor must surface an error")
6021 .to_string();
6022 assert!(
6023 rendered.contains("GET"),
6024 "the wrap must still name the method: {rendered}"
6025 );
6026 assert!(
6027 rendered.contains("simulated transport failure"),
6028 "the wrap must still carry the underlying cause: {rendered}"
6029 );
6030 assert!(
6031 !rendered.contains("/secret/inventory/endpoint"),
6032 "the wrap must NOT re-attach the resolved path: {rendered}"
6033 );
6034 }
6035
6036 #[tokio::test]
6037 async fn error_does_not_echo_path_in_the_parallel_arm() {
6038 let step = PlanStep::ParallelApiCalls {
6039 result_var: "out".to_string(),
6040 calls: vec![(
6041 "t0".to_string(),
6042 "GET".to_string(),
6043 PathTemplate {
6044 parts: literal("/secret/inventory/endpoint"),
6045 },
6046 None,
6047 )],
6048 };
6049 let mut executor = PlanExecutor::new(FailingHttp, ExecutionConfig::default());
6050 let rendered = executor
6051 .execute(&plan(vec![step]))
6052 .await
6053 .expect_err("the failing executor must surface an error")
6054 .to_string();
6055 assert!(rendered.contains("GET"), "{rendered}");
6056 assert!(
6057 !rendered.contains("/secret/inventory/endpoint"),
6058 "the parallel arm's wrap must NOT re-attach the resolved path: {rendered}"
6059 );
6060 }
6061
6062 #[tokio::test]
6063 async fn error_does_not_echo_path_on_a_refused_placeholder() {
6064 let (http, seen) = super::d09_support::RecordingHttp::new();
6065 let step = get_step(
6066 literal("/inventory/{sku}"),
6067 Some(serde_json::json!({"sku": "../../etc/shadow"})),
6068 );
6069 let mut executor = PlanExecutor::new(http, ExecutionConfig::default());
6070 let rendered = executor
6071 .execute(&plan(vec![step]))
6072 .await
6073 .expect_err("a refused placeholder must error")
6074 .to_string();
6075 assert!(seen.lock().unwrap().is_empty(), "{rendered}");
6076 for forbidden in ["shadow", "/inventory/", ".."] {
6077 assert!(
6078 !rendered.contains(forbidden),
6079 "a refusal carries neither the value nor the resolved path; \
6080 found {forbidden:?} in {rendered:?}"
6081 );
6082 }
6083 }
6084}
6085
6086#[cfg(test)]
6094mod query_separator {
6095 use super::d09_support::{get_step, literal, plan, RecordingHttp};
6096 use super::*;
6097
6098 async fn run(
6099 parts: Vec<PathPart>,
6100 body: Option<JsonValue>,
6101 ) -> (
6102 Result<ExecutionResult, ExecutionError>,
6103 Vec<super::d09_support::Seen>,
6104 ) {
6105 let (http, seen) = RecordingHttp::new();
6106 let mut executor = PlanExecutor::new(http, ExecutionConfig::default());
6107 let result = executor.execute(&plan(vec![get_step(parts, body)])).await;
6108 let seen = seen.lock().unwrap().clone();
6109 (result, seen)
6110 }
6111
6112 #[tokio::test]
6115 async fn query_separator_accepts_an_author_written_query_string() {
6116 let (result, seen) = run(literal("/Line/Mode/tube/Status?detail=true"), None).await;
6119 result.expect("an author-written query string must be accepted");
6120 assert_eq!(seen.len(), 1);
6121 assert_eq!(seen[0].path, "/Line/Mode/tube/Status?detail=true");
6122 }
6123
6124 #[tokio::test]
6125 async fn query_separator_accepts_a_literal_query_alongside_a_floored_placeholder() {
6126 let (result, seen) = run(
6129 literal("/content/{version}/CUI?string=headache"),
6130 Some(serde_json::json!({"version": "current"})),
6131 )
6132 .await;
6133 result.expect("an author query plus a conforming placeholder must be accepted");
6134 assert_eq!(seen.len(), 1);
6135 assert_eq!(seen[0].path, "/content/current/CUI?string=headache");
6136 }
6137
6138 #[tokio::test]
6139 async fn query_separator_accepts_a_graph_style_dollar_projection() {
6140 let (result, seen) = run(
6142 literal("/drives/D/items/I/workbook/worksheets/Customers/range(address='A2:D7')?$select=values"),
6143 None,
6144 )
6145 .await;
6146 result.expect("a Graph $select projection in the path must be accepted");
6147 assert_eq!(seen.len(), 1);
6148 assert!(
6149 seen[0].path.ends_with("?$select=values"),
6150 "{:?}",
6151 seen[0].path
6152 );
6153 }
6154
6155 #[tokio::test]
6158 async fn query_separator_still_refuses_traversal_in_the_path_portion() {
6159 let (result, seen) = run(literal("/a/../b?x=1"), None).await;
6162 let rendered = result
6163 .expect_err("traversal must still be refused when a query follows it")
6164 .to_string();
6165 assert!(seen.is_empty(), "{rendered}");
6166 }
6167
6168 #[tokio::test]
6169 async fn query_separator_still_refuses_traversal_in_the_query_portion() {
6170 let (result, seen) = run(literal("/search?next=../../etc/passwd"), None).await;
6171 let rendered = result
6172 .expect_err("traversal inside the query portion must still be refused")
6173 .to_string();
6174 assert!(seen.is_empty(), "{rendered}");
6175 assert!(!rendered.contains("passwd"), "{rendered}");
6176 }
6177
6178 #[tokio::test]
6179 async fn query_separator_still_refuses_a_control_byte_in_the_query_portion() {
6180 let (result, seen) = run(literal("/search?x=a%00b"), None).await;
6182 assert!(
6183 result.is_err(),
6184 "a control byte in the query portion must still be refused"
6185 );
6186 assert!(seen.is_empty());
6187 }
6188
6189 #[tokio::test]
6190 async fn query_separator_still_refuses_an_over_cap_query_portion() {
6191 let long = "z".repeat(crate::PLACEHOLDER_MAX_LENGTH + 1);
6192 let (result, seen) = run(literal(&format!("/search?q={long}")), None).await;
6193 assert!(
6194 result.is_err(),
6195 "an over-cap query portion must still be refused"
6196 );
6197 assert!(seen.is_empty());
6198 }
6199
6200 #[tokio::test]
6201 async fn query_separator_still_refuses_a_second_question_mark() {
6202 let (result, seen) = run(literal("/search?a=1?b=2"), None).await;
6205 assert!(result.is_err(), "a second `?` must still be refused");
6206 assert!(seen.is_empty());
6207 }
6208
6209 #[tokio::test]
6210 async fn query_separator_still_refuses_an_empty_query_portion() {
6211 let (result, seen) = run(literal("/search?"), None).await;
6214 assert!(result.is_err(), "a dangling `?` must still be refused");
6215 assert!(seen.is_empty());
6216 }
6217
6218 #[tokio::test]
6219 async fn query_separator_still_refuses_a_fragment_marker() {
6220 let (result, seen) = run(literal("/search#frag"), None).await;
6222 assert!(result.is_err(), "a fragment marker must still be refused");
6223 assert!(seen.is_empty());
6224 }
6225
6226 #[tokio::test]
6227 async fn query_separator_still_refuses_an_injected_separator_from_a_value() {
6228 let payload = format!("2026AA?string={}", "z".repeat(60));
6233 let (result, seen) = run(
6234 literal("/search/{v}?detail=true"),
6235 Some(serde_json::json!({"v": payload})),
6236 )
6237 .await;
6238 let rendered = result
6239 .expect_err("an injected `?` in a VALUE must still be refused")
6240 .to_string();
6241 assert!(seen.is_empty(), "{rendered}");
6242 assert!(
6243 !rendered.contains("2026AA") && !rendered.contains('?'),
6244 "the refusal must still carry no byte of the value: {rendered}"
6245 );
6246 }
6247
6248 #[tokio::test]
6249 async fn query_separator_still_refuses_an_injected_separator_from_a_layer_one_variable() {
6250 let (http, seen) = RecordingHttp::new();
6252 let mut executor = PlanExecutor::new(http, ExecutionConfig::default());
6253 executor.set_variable(
6254 "lookupKey",
6255 JsonValue::String("2026AA?string=x".to_string()),
6256 );
6257 let rendered = executor
6258 .execute(&plan(vec![get_step(
6259 vec![
6260 PathPart::Literal("/search/".to_string()),
6261 PathPart::Variable("lookupKey".to_string()),
6262 PathPart::Literal("?detail=true".to_string()),
6263 ],
6264 None,
6265 )]))
6266 .await
6267 .expect_err("an injected `?` in a ${var} part must still be refused")
6268 .to_string();
6269 assert!(seen.lock().unwrap().is_empty(), "{rendered}");
6270 assert!(rendered.contains("lookupKey"), "{rendered}");
6271 }
6272}