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
682impl CompileError {
683 #[must_use]
692 pub fn caller_message(&self) -> String {
693 match self {
694 Self::ParseError(_) => {
695 "the script has a syntax error and could not be parsed".to_string()
696 },
697 other => other.to_string(),
698 }
699 }
700}
701
702enum ExtractedCall {
707 Http {
709 method: String,
710 path: PathTemplate,
711 body: Option<ValueExpr>,
712 },
713 Sdk {
715 operation: String,
716 args: Option<ValueExpr>,
717 },
718}
719
720pub struct PlanCompiler {
722 api_call_count: usize,
723 endpoints: Vec<String>,
724 methods_used: Vec<String>,
725 has_mutations: bool,
726 sdk_operations: HashSet<String>,
728 destructure_counter: usize,
730}
731
732impl PlanCompiler {
733 pub fn new() -> Self {
735 Self::with_config(&ExecutionConfig::default())
736 }
737
738 pub fn with_config(_config: &ExecutionConfig) -> Self {
740 Self {
741 api_call_count: 0,
742 endpoints: Vec::new(),
743 methods_used: Vec::new(),
744 has_mutations: false,
745 sdk_operations: HashSet::new(),
746 destructure_counter: 0,
747 }
748 }
749
750 pub fn with_sdk_operations(mut self, operations: HashSet<String>) -> Self {
753 self.sdk_operations = operations;
754 self
755 }
756
757 pub fn compile_code(&mut self, code: &str) -> Result<ExecutionPlan, CompileError> {
761 let cm = SourceMap::default();
763 let fm = cm.new_source_file(FileName::Anon.into(), code.to_string());
764
765 let lexer = Lexer::new(
766 Syntax::Es(Default::default()),
767 EsVersion::Es2022,
768 StringInput::from(&*fm),
769 None,
770 );
771
772 let mut parser = Parser::new_from(lexer);
773
774 let module = parser.parse_module().map_err(|e| {
775 CompileError::ParseError(format!("JavaScript parse error: {:?}", e.into_kind()))
776 })?;
777
778 self.compile(&module)
780 }
781
782 pub fn compile(&mut self, module: &Module) -> Result<ExecutionPlan, CompileError> {
784 let mut steps = Vec::new();
785
786 for item in &module.body {
787 match item {
788 ModuleItem::Stmt(stmt) => {
789 self.compile_statement(stmt, &mut steps)?;
790 },
791 _ => {
792 return Err(CompileError::UnsupportedStatement(
793 "import/export not allowed".into(),
794 ));
795 },
796 }
797 }
798
799 self.methods_used.sort();
801 self.methods_used.dedup();
802
803 Ok(ExecutionPlan {
804 steps,
805 metadata: PlanMetadata {
806 api_call_count: self.api_call_count,
807 has_mutations: self.has_mutations,
808 endpoints: self.endpoints.clone(),
809 methods_used: self.methods_used.clone(),
810 },
811 })
812 }
813
814 fn compile_statement(
815 &mut self,
816 stmt: &Stmt,
817 steps: &mut Vec<PlanStep>,
818 ) -> Result<(), CompileError> {
819 match stmt {
820 Stmt::Decl(Decl::Var(var_decl)) => {
822 for decl in &var_decl.decls {
823 if let Some(init) = &decl.init {
824 match &decl.name {
825 Pat::Ident(ident) => {
826 let var_name = ident.id.sym.to_string();
827 self.compile_var_init(&var_name, init, steps)?;
828 },
829 Pat::Object(obj_pat) => {
830 self.compile_object_destructuring(obj_pat, init, steps)?;
831 },
832 Pat::Array(arr_pat) => {
833 self.compile_array_destructuring(arr_pat, init, steps)?;
834 },
835 _ => {
836 return Err(CompileError::UnsupportedExpression(
837 "complex destructuring pattern".into(),
838 ));
839 },
840 }
841 }
842 }
843 },
844
845 Stmt::Expr(expr_stmt) => {
847 if let Expr::Assign(assign) = expr_stmt.expr.as_ref() {
849 if assign.op == swc_ecma_ast::AssignOp::Assign {
850 if let Some(ident) = assign.left.as_ident() {
851 let var_name = ident.sym.to_string();
852 self.compile_var_init(&var_name, &assign.right, steps)?;
853 return Ok(());
854 }
855 }
856 }
857
858 let expr = self.compile_expr(&expr_stmt.expr)?;
859 match expr {
860 ValueExpr::ApiCall { method, path, body } => {
862 self.record_api_call(&method, &path);
863 steps.push(PlanStep::ApiCall {
864 result_var: "_".into(), method,
866 path,
867 body: body.map(|b| *b),
868 });
869 },
870 ValueExpr::SdkCall { operation, args } => {
872 steps.push(PlanStep::SdkCall {
873 result_var: "_".into(), operation,
875 args: args.map(|a| *a),
876 });
877 },
878 ValueExpr::ArrayMethod {
881 ref array,
882 ref method,
883 } if matches!(
884 method,
885 ArrayMethodCall::Push { .. } | ArrayMethodCall::Concat { .. }
886 ) =>
887 {
888 if let ValueExpr::Variable(var_name) = array.as_ref() {
889 steps.push(PlanStep::Assign {
890 var: var_name.clone(),
891 expr,
892 });
893 }
894 },
897 _ => {},
899 }
900 },
901
902 Stmt::If(if_stmt) => {
904 let condition = self.compile_expr(&if_stmt.test)?;
905 let mut then_steps = Vec::new();
906 self.compile_statement(&if_stmt.cons, &mut then_steps)?;
907
908 let mut else_steps = Vec::new();
909 if let Some(alt) = &if_stmt.alt {
910 self.compile_statement(alt, &mut else_steps)?;
911 }
912
913 steps.push(PlanStep::Conditional {
914 condition,
915 then_steps,
916 else_steps,
917 });
918 },
919
920 Stmt::ForOf(for_of) => {
922 let (item_var, destructure_bindings) = match &for_of.left {
923 ForHead::VarDecl(decl) => {
924 if let Some(first) = decl.decls.first() {
925 self.extract_loop_var(&first.name)?
926 } else {
927 return Err(CompileError::MissingVariableName);
928 }
929 },
930 ForHead::Pat(pat) => self.extract_loop_var(pat)?,
931 _ => return Err(CompileError::MissingVariableName),
932 };
933
934 let collection = self.compile_expr(&for_of.right)?;
935
936 let max_iterations = self.extract_bound(&collection).unwrap_or(100);
938
939 let mut body = destructure_bindings;
940 self.compile_statement(&for_of.body, &mut body)?;
941
942 steps.push(PlanStep::BoundedLoop {
943 item_var,
944 collection,
945 max_iterations,
946 body,
947 });
948 },
949
950 Stmt::Block(block) => {
952 for stmt in &block.stmts {
953 self.compile_statement(stmt, steps)?;
954 }
955 },
956
957 Stmt::Return(ret) => {
959 let value = if let Some(arg) = &ret.arg {
960 self.compile_expr(arg)?
961 } else {
962 ValueExpr::Literal(JsonValue::Null)
963 };
964 steps.push(PlanStep::Return { value });
965 },
966
967 Stmt::Empty(_) => {},
969
970 Stmt::Try(try_stmt) => {
972 let mut try_steps = Vec::new();
973 for stmt in &try_stmt.block.stmts {
974 self.compile_statement(stmt, &mut try_steps)?;
975 }
976
977 let (catch_var, catch_steps) = if let Some(handler) = &try_stmt.handler {
978 let var_name = handler.param.as_ref().map(|p| match p {
979 swc_ecma_ast::Pat::Ident(ident) => ident.sym.to_string(),
980 _ => "error".to_string(),
981 });
982 let mut catch_stmts = Vec::new();
983 for stmt in &handler.body.stmts {
984 self.compile_statement(stmt, &mut catch_stmts)?;
985 }
986 (var_name, catch_stmts)
987 } else {
988 (None, Vec::new())
989 };
990
991 let finally_steps = if let Some(finalizer) = &try_stmt.finalizer {
992 let mut finally_stmts = Vec::new();
993 for stmt in &finalizer.stmts {
994 self.compile_statement(stmt, &mut finally_stmts)?;
995 }
996 finally_stmts
997 } else {
998 Vec::new()
999 };
1000
1001 steps.push(PlanStep::TryCatch {
1002 try_steps,
1003 catch_var,
1004 catch_steps,
1005 finally_steps,
1006 });
1007 },
1008
1009 Stmt::Continue(_) => {
1011 steps.push(PlanStep::Continue);
1012 },
1013
1014 Stmt::Break(_) => {
1016 steps.push(PlanStep::Break);
1017 },
1018
1019 Stmt::Decl(decl) => {
1020 let msg = match decl {
1021 Decl::Fn(_) => "Function declarations are not supported. Use arrow functions inside array methods (.map, .filter) instead",
1022 Decl::Class(_) => "Class declarations are not supported",
1023 _ => "This declaration type is not supported",
1024 };
1025 return Err(CompileError::UnsupportedStatement(msg.into()));
1026 },
1027 Stmt::Switch(_) => {
1028 return Err(CompileError::UnsupportedStatement(
1029 "'switch' statements are not supported. Use if/else if/else instead".into(),
1030 ));
1031 },
1032 Stmt::Throw(_) => {
1033 return Err(CompileError::UnsupportedStatement(
1034 "'throw' statements are not supported. Use try/catch for error handling".into(),
1035 ));
1036 },
1037 Stmt::While(_) => {
1038 return Err(CompileError::UnsupportedStatement(
1039 "'while' loops are not supported. Use for-of with .slice() instead: for (const item of array.slice(0, N)) { }".into(),
1040 ));
1041 },
1042 Stmt::DoWhile(_) => {
1043 return Err(CompileError::UnsupportedStatement(
1044 "'do-while' loops are not supported. Use for-of with .slice() instead: for (const item of array.slice(0, N)) { }".into(),
1045 ));
1046 },
1047 Stmt::For(_) => {
1048 return Err(CompileError::UnsupportedStatement(
1049 "'for(;;)' loops are not supported. Use for-of with .slice() instead: for (const item of array.slice(0, N)) { }".into(),
1050 ));
1051 },
1052 Stmt::ForIn(_) => {
1053 return Err(CompileError::UnsupportedStatement(
1054 "'for-in' loops are not supported. Use for-of with .slice() instead".into(),
1055 ));
1056 },
1057 Stmt::Labeled(_) => {
1058 return Err(CompileError::UnsupportedStatement(
1059 "Labeled statements are not supported".into(),
1060 ));
1061 },
1062 Stmt::With(_) => {
1063 return Err(CompileError::UnsupportedStatement(
1064 "'with' statements are not supported".into(),
1065 ));
1066 },
1067 Stmt::Debugger(_) => {
1068 return Err(CompileError::UnsupportedStatement(
1069 "'debugger' statements are not supported".into(),
1070 ));
1071 },
1072 }
1073
1074 Ok(())
1075 }
1076
1077 fn compile_var_init(
1078 &mut self,
1079 var_name: &str,
1080 init: &Expr,
1081 steps: &mut Vec<PlanStep>,
1082 ) -> Result<(), CompileError> {
1083 if let Expr::Await(await_expr) = init {
1085 if let Some(extracted) = self.try_extract_api_call(&await_expr.arg)? {
1087 match extracted {
1088 ExtractedCall::Http { method, path, body } => {
1089 self.record_api_call(&method, &path);
1090 steps.push(PlanStep::ApiCall {
1091 result_var: var_name.into(),
1092 method,
1093 path,
1094 body,
1095 });
1096 },
1097 ExtractedCall::Sdk { operation, args } => {
1098 steps.push(PlanStep::SdkCall {
1099 result_var: var_name.into(),
1100 operation,
1101 args,
1102 });
1103 },
1104 }
1105 return Ok(());
1106 }
1107
1108 #[cfg(feature = "mcp-code-mode")]
1110 if let Some((server_id, tool_name, args)) =
1111 self.try_extract_mcp_call(&await_expr.arg)?
1112 {
1113 steps.push(PlanStep::McpCall {
1114 result_var: var_name.into(),
1115 server_id,
1116 tool_name,
1117 args,
1118 });
1119 return Ok(());
1120 }
1121
1122 if let Expr::Call(call) = await_expr.arg.as_ref() {
1124 let inner = self.compile_call(call)?;
1125 if let ValueExpr::PromiseAll { items } = inner {
1126 return self.compile_promise_all(var_name, items, steps);
1127 }
1128 }
1129 }
1130
1131 let expr = self.compile_expr(init)?;
1133 steps.push(PlanStep::Assign {
1134 var: var_name.into(),
1135 expr,
1136 });
1137 Ok(())
1138 }
1139
1140 fn compile_promise_all(
1143 &mut self,
1144 result_var: &str,
1145 items: Vec<ValueExpr>,
1146 steps: &mut Vec<PlanStep>,
1147 ) -> Result<(), CompileError> {
1148 let mut calls = Vec::new();
1149 let mut all_api_calls = true;
1150
1151 for (i, item) in items.iter().enumerate() {
1152 match item {
1153 ValueExpr::ApiCall { method, path, body } => {
1154 let temp_var = format!("__promise_all_{}_{}", result_var, i);
1155 calls.push((
1156 temp_var,
1157 method.clone(),
1158 path.clone(),
1159 body.as_ref().map(|b| *b.clone()),
1160 ));
1161 },
1162 _ => {
1163 all_api_calls = false;
1164 break;
1165 },
1166 }
1167 }
1168
1169 if all_api_calls && !calls.is_empty() {
1170 for (_, method, path, _) in &calls {
1172 self.record_api_call(method, path);
1173 }
1174 steps.push(PlanStep::ParallelApiCalls {
1175 result_var: result_var.into(),
1176 calls,
1177 });
1178 Ok(())
1179 } else {
1180 Err(CompileError::UnsupportedExpression(
1183 "Promise.all with non-API-call expressions".into(),
1184 ))
1185 }
1186 }
1187
1188 fn compile_expr(&mut self, expr: &Expr) -> Result<ValueExpr, CompileError> {
1189 match expr {
1190 Expr::Lit(lit) => Ok(ValueExpr::Literal(self.lit_to_json(lit))),
1192
1193 Expr::Ident(ident) => Ok(ValueExpr::Variable(ident.sym.to_string())),
1195
1196 Expr::Member(member) => {
1198 let object = Box::new(self.compile_expr(&member.obj)?);
1199
1200 if let MemberProp::Ident(prop) = &member.prop {
1202 let prop_name = prop.sym.to_string();
1203 if prop_name == "length" {
1204 return Ok(ValueExpr::ArrayMethod {
1205 array: object,
1206 method: ArrayMethodCall::Length,
1207 });
1208 }
1209 }
1210
1211 match &member.prop {
1212 MemberProp::Ident(ident) => Ok(ValueExpr::PropertyAccess {
1213 object,
1214 property: ident.sym.to_string(),
1215 }),
1216 MemberProp::Computed(computed) => {
1217 let index = Box::new(self.compile_expr(&computed.expr)?);
1218 Ok(ValueExpr::ArrayIndex {
1219 array: object,
1220 index,
1221 })
1222 }
1223 _ => Err(CompileError::UnsupportedExpression("private property".into())),
1224 }
1225 }
1226
1227 Expr::Call(call) => self.compile_call(call),
1229
1230 Expr::Object(obj) => {
1232 let mut fields = Vec::new();
1233 for prop in &obj.props {
1234 match prop {
1235 PropOrSpread::Prop(prop) => {
1236 if let Prop::KeyValue(kv) = prop.as_ref() {
1237 let key = self.prop_name_to_string(&kv.key)?;
1238 let value = self.compile_expr(&kv.value)?;
1239 fields.push(ObjectField::KeyValue { key, value });
1240 } else if let Prop::Shorthand(ident) = prop.as_ref() {
1241 let name = ident.sym.to_string();
1242 fields.push(ObjectField::KeyValue {
1243 key: name.clone(),
1244 value: ValueExpr::Variable(name),
1245 });
1246 }
1247 }
1248 PropOrSpread::Spread(spread) => {
1249 let expr = self.compile_expr(&spread.expr)?;
1250 fields.push(ObjectField::Spread { expr });
1251 }
1252 }
1253 }
1254 Ok(ValueExpr::ObjectLiteral { fields })
1255 }
1256
1257 Expr::Array(arr) => {
1259 let mut items = Vec::new();
1260 for elem in arr.elems.iter().flatten() {
1261 if elem.spread.is_some() {
1262 return Err(CompileError::UnsupportedExpression("spread".into()));
1263 }
1264 items.push(self.compile_expr(&elem.expr)?);
1265 }
1266 Ok(ValueExpr::ArrayLiteral { items })
1267 }
1268
1269 Expr::Tpl(tpl) => {
1271 let mut parts = Vec::new();
1274 for (i, quasi) in tpl.quasis.iter().enumerate() {
1275 let raw = quasi.raw.to_string();
1276 if !raw.is_empty() {
1277 parts.push(ValueExpr::Literal(JsonValue::String(raw)));
1278 }
1279 if i < tpl.exprs.len() {
1280 parts.push(self.compile_expr(&tpl.exprs[i])?);
1281 }
1282 }
1283
1284 if parts.len() == 1 {
1286 return Ok(parts.remove(0));
1287 }
1288
1289 let mut result = parts.remove(0);
1291 for part in parts {
1292 result = ValueExpr::BinaryOp {
1293 left: Box::new(result),
1294 op: BinaryOperator::Concat,
1295 right: Box::new(part),
1296 };
1297 }
1298 Ok(result)
1299 }
1300
1301 Expr::Bin(bin) => {
1303 let left = Box::new(self.compile_expr(&bin.left)?);
1304 let right = Box::new(self.compile_expr(&bin.right)?);
1305 let op = self.compile_bin_op(bin.op)?;
1306 Ok(ValueExpr::BinaryOp { left, op, right })
1307 }
1308
1309 Expr::Unary(unary) => {
1311 let operand = Box::new(self.compile_expr(&unary.arg)?);
1312 let op = match unary.op {
1313 UnaryOp::Bang => UnaryOperator::Not,
1314 UnaryOp::Minus => UnaryOperator::Neg,
1315 UnaryOp::TypeOf => UnaryOperator::TypeOf,
1316 UnaryOp::Plus => UnaryOperator::Plus,
1317 _ => return Err(CompileError::UnsupportedExpression("unary op".into())),
1318 };
1319 Ok(ValueExpr::UnaryOp { op, operand })
1320 }
1321
1322 Expr::Cond(cond) => {
1324 let condition = Box::new(self.compile_expr(&cond.test)?);
1325 let consequent = Box::new(self.compile_expr(&cond.cons)?);
1326 let alternate = Box::new(self.compile_expr(&cond.alt)?);
1327 Ok(ValueExpr::Ternary {
1328 condition,
1329 consequent,
1330 alternate,
1331 })
1332 }
1333
1334 Expr::Await(await_expr) => {
1336 if let Some(extracted) = self.try_extract_api_call(&await_expr.arg)? {
1338 return match extracted {
1339 ExtractedCall::Http { method, path, body } => {
1340 self.record_api_call(&method, &path);
1341 Ok(ValueExpr::ApiCall {
1342 method,
1343 path,
1344 body: body.map(Box::new),
1345 })
1346 }
1347 ExtractedCall::Sdk { operation, args } => {
1348 Ok(ValueExpr::SdkCall {
1349 operation,
1350 args: args.map(Box::new),
1351 })
1352 }
1353 };
1354 }
1355 #[cfg(feature = "mcp-code-mode")]
1357 if let Some((server_id, tool_name, args)) = self.try_extract_mcp_call(&await_expr.arg)? {
1358 return Ok(ValueExpr::McpCall {
1359 server_id,
1360 tool_name,
1361 args: args.map(Box::new),
1362 });
1363 }
1364 let inner = self.compile_expr(&await_expr.arg)?;
1366 Ok(ValueExpr::Await {
1367 expr: Box::new(inner),
1368 })
1369 }
1370
1371 Expr::Arrow(_) => {
1373 Err(CompileError::UnsupportedExpression(
1375 "arrow function outside array method".into(),
1376 ))
1377 }
1378
1379 Expr::Paren(paren) => self.compile_expr(&paren.expr),
1381
1382 Expr::OptChain(opt) => {
1384 match opt.base.as_ref() {
1385 OptChainBase::Member(member) => {
1386 let object = Box::new(self.compile_expr(&member.obj)?);
1387 if let MemberProp::Ident(ident) = &member.prop {
1388 Ok(ValueExpr::OptionalChain {
1389 object,
1390 property: ident.sym.to_string(),
1391 })
1392 } else {
1393 Err(CompileError::UnsupportedExpression("computed optional chain".into()))
1394 }
1395 }
1396 _ => Err(CompileError::UnsupportedExpression("optional call".into())),
1397 }
1398 }
1399
1400 Expr::This(_) => Err(CompileError::UnsupportedExpression(
1401 "'this' keyword is not supported".into(),
1402 )),
1403 Expr::Fn(_) => Err(CompileError::UnsupportedExpression(
1404 "Function expressions are not supported. Use arrow functions inside array methods (.map, .filter) instead".into(),
1405 )),
1406 Expr::Update(_) => Err(CompileError::UnsupportedExpression(
1407 "Increment/decrement operators (++, --) are not supported. Use 'x = x + 1' instead".into(),
1408 )),
1409 Expr::New(_) => Err(CompileError::UnsupportedExpression(
1410 "'new' keyword is not supported".into(),
1411 )),
1412 Expr::Seq(_) => Err(CompileError::UnsupportedExpression(
1413 "Sequence expressions (comma operator) are not supported. Use separate statements instead".into(),
1414 )),
1415 Expr::TaggedTpl(_) => Err(CompileError::UnsupportedExpression(
1416 "Tagged template literals are not supported. Use regular template literals instead".into(),
1417 )),
1418 Expr::Class(_) => Err(CompileError::UnsupportedExpression(
1419 "Class expressions are not supported".into(),
1420 )),
1421 Expr::Yield(_) => Err(CompileError::UnsupportedExpression(
1422 "Generator yield is not supported".into(),
1423 )),
1424 Expr::SuperProp(_) => Err(CompileError::UnsupportedExpression(
1425 "'super' is not supported".into(),
1426 )),
1427 Expr::Assign(_) => Err(CompileError::UnsupportedExpression(
1428 "Assignment expressions are not supported here. Use a separate variable declaration instead".into(),
1429 )),
1430 _ => Err(CompileError::UnsupportedExpression(
1431 "This expression type is not supported in the JavaScript subset".into(),
1432 )),
1433 }
1434 }
1435
1436 fn compile_call(&mut self, call: &CallExpr) -> Result<ValueExpr, CompileError> {
1437 if let Some(extracted) = self.try_extract_api_call(&Expr::Call(call.clone()))? {
1439 return match extracted {
1440 ExtractedCall::Http { method, path, body } => {
1441 self.record_api_call(&method, &path);
1442 Ok(ValueExpr::ApiCall {
1443 method,
1444 path,
1445 body: body.map(Box::new),
1446 })
1447 },
1448 ExtractedCall::Sdk { operation, args } => Ok(ValueExpr::SdkCall {
1449 operation,
1450 args: args.map(Box::new),
1451 }),
1452 };
1453 }
1454
1455 #[cfg(feature = "mcp-code-mode")]
1457 if let Some((server_id, tool_name, args)) =
1458 self.try_extract_mcp_call(&Expr::Call(call.clone()))?
1459 {
1460 return Ok(ValueExpr::McpCall {
1461 server_id,
1462 tool_name,
1463 args: args.map(Box::new),
1464 });
1465 }
1466
1467 if let Callee::Expr(callee) = &call.callee {
1469 if let Expr::Member(member) = callee.as_ref() {
1470 if let Expr::Ident(obj) = member.obj.as_ref() {
1471 if obj.sym.as_ref() == "Promise" {
1472 if let MemberProp::Ident(prop) = &member.prop {
1473 if prop.sym.as_ref() == "all" {
1474 if let Some(arg) = call.args.first() {
1475 if let Expr::Array(arr) = arg.expr.as_ref() {
1476 let mut items = Vec::new();
1477 for elem in arr.elems.iter().flatten() {
1478 items.push(self.compile_expr(&elem.expr)?);
1479 }
1480 return Ok(ValueExpr::PromiseAll { items });
1481 }
1482 }
1483 }
1484 }
1485 }
1486 }
1487 }
1488 }
1489
1490 if let Callee::Expr(callee) = &call.callee {
1492 if let Expr::Member(member) = callee.as_ref() {
1493 let array = Box::new(self.compile_expr(&member.obj)?);
1494
1495 if let MemberProp::Ident(method_ident) = &member.prop {
1496 let method_name = method_ident.sym.as_ref();
1497
1498 match method_name {
1499 "map" => {
1500 let (item_var, body) = self.extract_arrow_callback(call)?;
1501 return Ok(ValueExpr::ArrayMethod {
1502 array,
1503 method: ArrayMethodCall::Map {
1504 item_var,
1505 body: Box::new(body),
1506 },
1507 });
1508 },
1509 "filter" => {
1510 let (item_var, predicate) = self.extract_arrow_callback(call)?;
1511 return Ok(ValueExpr::ArrayMethod {
1512 array,
1513 method: ArrayMethodCall::Filter {
1514 item_var,
1515 predicate: Box::new(predicate),
1516 },
1517 });
1518 },
1519 "find" => {
1520 let (item_var, predicate) = self.extract_arrow_callback(call)?;
1521 return Ok(ValueExpr::ArrayMethod {
1522 array,
1523 method: ArrayMethodCall::Find {
1524 item_var,
1525 predicate: Box::new(predicate),
1526 },
1527 });
1528 },
1529 "some" => {
1530 let (item_var, predicate) = self.extract_arrow_callback(call)?;
1531 return Ok(ValueExpr::ArrayMethod {
1532 array,
1533 method: ArrayMethodCall::Some {
1534 item_var,
1535 predicate: Box::new(predicate),
1536 },
1537 });
1538 },
1539 "every" => {
1540 let (item_var, predicate) = self.extract_arrow_callback(call)?;
1541 return Ok(ValueExpr::ArrayMethod {
1542 array,
1543 method: ArrayMethodCall::Every {
1544 item_var,
1545 predicate: Box::new(predicate),
1546 },
1547 });
1548 },
1549 "flatMap" => {
1550 let (item_var, body) = self.extract_arrow_callback(call)?;
1551 return Ok(ValueExpr::ArrayMethod {
1552 array,
1553 method: ArrayMethodCall::FlatMap {
1554 item_var,
1555 body: Box::new(body),
1556 },
1557 });
1558 },
1559 "slice" => {
1560 let start = self.extract_number_arg(call, 0)?.unwrap_or(0) as usize;
1561 let end = self.extract_number_arg(call, 1)?.map(|n| n as usize);
1562 return Ok(ValueExpr::ArrayMethod {
1563 array,
1564 method: ArrayMethodCall::Slice { start, end },
1565 });
1566 },
1567 "push" => {
1568 if let Some(arg) = call.args.first() {
1569 let item = Box::new(self.compile_expr(&arg.expr)?);
1570 return Ok(ValueExpr::ArrayMethod {
1571 array,
1572 method: ArrayMethodCall::Push { item },
1573 });
1574 }
1575 },
1576 "concat" => {
1577 if let Some(arg) = call.args.first() {
1578 let other = Box::new(self.compile_expr(&arg.expr)?);
1579 return Ok(ValueExpr::ArrayMethod {
1580 array,
1581 method: ArrayMethodCall::Concat { other },
1582 });
1583 }
1584 },
1585 "includes" => {
1586 if let Some(arg) = call.args.first() {
1587 let item = Box::new(self.compile_expr(&arg.expr)?);
1588 return Ok(ValueExpr::ArrayMethod {
1589 array,
1590 method: ArrayMethodCall::Includes { item },
1591 });
1592 }
1593 },
1594 "indexOf" => {
1595 if let Some(arg) = call.args.first() {
1596 let item = Box::new(self.compile_expr(&arg.expr)?);
1597 return Ok(ValueExpr::ArrayMethod {
1598 array,
1599 method: ArrayMethodCall::IndexOf { item },
1600 });
1601 }
1602 },
1603 "join" => {
1604 let separator = if let Some(arg) = call.args.first() {
1605 if let Expr::Lit(Lit::Str(s)) = arg.expr.as_ref() {
1606 Some(s.value.to_string_lossy().into_owned())
1607 } else {
1608 None
1609 }
1610 } else {
1611 None
1612 };
1613 return Ok(ValueExpr::ArrayMethod {
1614 array,
1615 method: ArrayMethodCall::Join { separator },
1616 });
1617 },
1618 "reverse" => {
1619 return Ok(ValueExpr::ArrayMethod {
1620 array,
1621 method: ArrayMethodCall::Reverse,
1622 });
1623 },
1624 "sort" => {
1625 let comparator = if !call.args.is_empty() {
1626 let (a_var, b_var, body) = self.extract_reduce_callback(call)?;
1627 Some((a_var, b_var, Box::new(body)))
1628 } else {
1629 None
1630 };
1631 return Ok(ValueExpr::ArrayMethod {
1632 array,
1633 method: ArrayMethodCall::Sort { comparator },
1634 });
1635 },
1636 "flat" => {
1637 return Ok(ValueExpr::ArrayMethod {
1638 array,
1639 method: ArrayMethodCall::Flat,
1640 });
1641 },
1642 "at" => {
1643 if let Some(n) = self.extract_number_arg(call, 0)? {
1644 if n == 0 {
1645 return Ok(ValueExpr::ArrayMethod {
1646 array,
1647 method: ArrayMethodCall::First,
1648 });
1649 } else if n == -1 {
1650 return Ok(ValueExpr::ArrayMethod {
1651 array,
1652 method: ArrayMethodCall::Last,
1653 });
1654 }
1655 }
1656 },
1657 "reduce" if call.args.len() >= 2 => {
1659 let (acc_var, item_var, body) = self.extract_reduce_callback(call)?;
1660 let initial = Box::new(self.compile_expr(&call.args[1].expr)?);
1661 return Ok(ValueExpr::ArrayMethod {
1662 array,
1663 method: ArrayMethodCall::Reduce {
1664 acc_var,
1665 item_var,
1666 body: Box::new(body),
1667 initial,
1668 },
1669 });
1670 },
1671 "toFixed" => {
1672 let digits = self.extract_number_arg(call, 0)?.unwrap_or(0) as usize;
1674 return Ok(ValueExpr::NumberMethod {
1675 number: array, method: NumberMethodCall::ToFixed { digits },
1677 });
1678 },
1679 "toLowerCase" => {
1680 return Ok(ValueExpr::ArrayMethod {
1681 array,
1682 method: ArrayMethodCall::ToLowerCase,
1683 });
1684 },
1685 "toUpperCase" => {
1686 return Ok(ValueExpr::ArrayMethod {
1687 array,
1688 method: ArrayMethodCall::ToUpperCase,
1689 });
1690 },
1691 "startsWith" => {
1692 let arg = call.args.first().ok_or_else(|| {
1693 CompileError::UnsupportedExpression(
1694 "startsWith() requires a search argument".into(),
1695 )
1696 })?;
1697 let search = Box::new(self.compile_expr(&arg.expr)?);
1698 return Ok(ValueExpr::ArrayMethod {
1699 array,
1700 method: ArrayMethodCall::StartsWith { search },
1701 });
1702 },
1703 "endsWith" => {
1704 let arg = call.args.first().ok_or_else(|| {
1705 CompileError::UnsupportedExpression(
1706 "endsWith() requires a search argument".into(),
1707 )
1708 })?;
1709 let search = Box::new(self.compile_expr(&arg.expr)?);
1710 return Ok(ValueExpr::ArrayMethod {
1711 array,
1712 method: ArrayMethodCall::EndsWith { search },
1713 });
1714 },
1715 "trim" => {
1716 return Ok(ValueExpr::ArrayMethod {
1717 array,
1718 method: ArrayMethodCall::Trim,
1719 });
1720 },
1721 "replace" => {
1722 if call.args.len() < 2 {
1723 return Err(CompileError::UnsupportedExpression(
1724 "replace() requires search and replacement arguments".into(),
1725 ));
1726 }
1727 let search = Box::new(self.compile_expr(&call.args[0].expr)?);
1728 let replacement = Box::new(self.compile_expr(&call.args[1].expr)?);
1729 return Ok(ValueExpr::ArrayMethod {
1730 array,
1731 method: ArrayMethodCall::Replace {
1732 search,
1733 replacement,
1734 },
1735 });
1736 },
1737 "split" => {
1738 let arg = call.args.first().ok_or_else(|| {
1739 CompileError::UnsupportedExpression(
1740 "split() requires a separator argument".into(),
1741 )
1742 })?;
1743 let separator = Box::new(self.compile_expr(&arg.expr)?);
1744 return Ok(ValueExpr::ArrayMethod {
1745 array,
1746 method: ArrayMethodCall::Split { separator },
1747 });
1748 },
1749 "substring" => {
1750 let arg = call.args.first().ok_or_else(|| {
1751 CompileError::UnsupportedExpression(
1752 "substring() requires a start argument".into(),
1753 )
1754 })?;
1755 let start = Box::new(self.compile_expr(&arg.expr)?);
1756 let end = if call.args.len() >= 2 {
1757 Some(Box::new(self.compile_expr(&call.args[1].expr)?))
1758 } else {
1759 None
1760 };
1761 return Ok(ValueExpr::ArrayMethod {
1762 array,
1763 method: ArrayMethodCall::Substring { start, end },
1764 });
1765 },
1766 "toString" => {
1767 return Ok(ValueExpr::ArrayMethod {
1768 array,
1769 method: ArrayMethodCall::ToString,
1770 });
1771 },
1772 _ => {},
1773 }
1774 }
1775 }
1776 }
1777
1778 if let Callee::Expr(callee) = &call.callee {
1780 if let Expr::Ident(ident) = callee.as_ref() {
1781 let func = match ident.sym.as_ref() {
1782 "parseFloat" => Some(BuiltinFunction::ParseFloat),
1783 "parseInt" => Some(BuiltinFunction::ParseInt),
1784 "Number" => Some(BuiltinFunction::NumberCast),
1785 _ => None,
1786 };
1787 if let Some(func) = func {
1788 let args = call
1789 .args
1790 .iter()
1791 .map(|a| self.compile_expr(&a.expr))
1792 .collect::<Result<Vec<_>, _>>()?;
1793 return Ok(ValueExpr::BuiltinCall { func, args });
1794 }
1795 }
1796
1797 if let Expr::Member(member) = callee.as_ref() {
1799 if let Expr::Ident(obj) = member.obj.as_ref() {
1800 if let MemberProp::Ident(prop) = &member.prop {
1801 let func = match (obj.sym.as_ref(), prop.sym.as_ref()) {
1802 ("Math", "abs") => Some(BuiltinFunction::MathAbs),
1803 ("Math", "max") => Some(BuiltinFunction::MathMax),
1804 ("Math", "min") => Some(BuiltinFunction::MathMin),
1805 ("Math", "round") => Some(BuiltinFunction::MathRound),
1806 ("Math", "floor") => Some(BuiltinFunction::MathFloor),
1807 ("Math", "ceil") => Some(BuiltinFunction::MathCeil),
1808 ("Object", "keys") => Some(BuiltinFunction::ObjectKeys),
1809 ("Object", "values") => Some(BuiltinFunction::ObjectValues),
1810 ("Object", "entries") => Some(BuiltinFunction::ObjectEntries),
1811 _ => None,
1812 };
1813 if let Some(func) = func {
1814 let args = call
1815 .args
1816 .iter()
1817 .map(|a| self.compile_expr(&a.expr))
1818 .collect::<Result<Vec<_>, _>>()?;
1819 return Ok(ValueExpr::BuiltinCall { func, args });
1820 }
1821 }
1822 }
1823 }
1824 }
1825
1826 Err(CompileError::UnsupportedExpression("function call".into()))
1827 }
1828
1829 fn try_extract_api_call(&mut self, expr: &Expr) -> Result<Option<ExtractedCall>, CompileError> {
1830 let call = match expr {
1831 Expr::Call(c) => c,
1832 _ => return Ok(None),
1833 };
1834
1835 if let Callee::Expr(callee) = &call.callee {
1836 if let Expr::Member(member) = callee.as_ref() {
1837 if let Expr::Ident(obj) = member.obj.as_ref() {
1838 if obj.sym.as_ref() == "api" {
1839 if let MemberProp::Ident(method_ident) = &member.prop {
1840 let method_name = method_ident.sym.as_ref();
1841
1842 if !self.sdk_operations.is_empty() {
1843 if !self.sdk_operations.contains(method_name) {
1845 return Err(CompileError::InvalidApiCall(format!(
1846 "Unknown SDK operation: api.{}(). Check the code mode schema resource for available operations.",
1847 method_name
1848 )));
1849 }
1850 let args = if let Some(arg) = call.args.first() {
1851 Some(self.compile_expr(&arg.expr)?)
1852 } else {
1853 None
1854 };
1855 self.api_call_count += 1;
1856 let op_endpoint = format!("sdk:{}", method_name);
1857 if !self.endpoints.contains(&op_endpoint) {
1858 self.endpoints.push(op_endpoint);
1859 }
1860 if !self.methods_used.contains(&method_name.to_string()) {
1861 self.methods_used.push(method_name.to_string());
1862 }
1863 return Ok(Some(ExtractedCall::Sdk {
1864 operation: method_name.to_string(),
1865 args,
1866 }));
1867 }
1868
1869 if HttpMethod::from_str(method_name).is_none() {
1871 return Err(CompileError::InvalidApiCall(format!(
1872 "Unknown method: api.{}",
1873 method_name
1874 )));
1875 }
1876
1877 let path = if let Some(arg) = call.args.first() {
1879 self.extract_path_template(&arg.expr)?
1880 } else {
1881 return Err(CompileError::InvalidApiCall(
1882 "API call requires path".into(),
1883 ));
1884 };
1885
1886 let body = if let Some(arg) = call.args.get(1) {
1888 Some(self.compile_expr(&arg.expr)?)
1889 } else {
1890 None
1891 };
1892
1893 return Ok(Some(ExtractedCall::Http {
1894 method: method_name.to_uppercase(),
1895 path,
1896 body,
1897 }));
1898 }
1899 }
1900 }
1901 }
1902 }
1903
1904 Ok(None)
1905 }
1906
1907 #[cfg(feature = "mcp-code-mode")]
1911 fn try_extract_mcp_call(
1912 &mut self,
1913 expr: &Expr,
1914 ) -> Result<Option<(String, String, Option<ValueExpr>)>, CompileError> {
1915 let call = match expr {
1916 Expr::Call(c) => c,
1917 _ => return Ok(None),
1918 };
1919
1920 if let Callee::Expr(callee) = &call.callee {
1921 if let Expr::Member(member) = callee.as_ref() {
1922 if let Expr::Ident(obj) = member.obj.as_ref() {
1923 if obj.sym.as_ref() == "mcp" {
1924 if let MemberProp::Ident(method_ident) = &member.prop {
1925 if method_ident.sym.as_ref() == "call" {
1926 let server_id = call.args.first()
1928 .and_then(|a| {
1929 if let Expr::Lit(Lit::Str(s)) = a.expr.as_ref() {
1930 Some(s.value.to_string_lossy().into_owned())
1931 } else {
1932 None
1933 }
1934 })
1935 .ok_or_else(|| CompileError::UnsupportedExpression(
1936 "mcp.call() first argument must be a string literal (server_id)".into(),
1937 ))?;
1938
1939 let tool_name = call.args.get(1)
1941 .and_then(|a| {
1942 if let Expr::Lit(Lit::Str(s)) = a.expr.as_ref() {
1943 Some(s.value.to_string_lossy().into_owned())
1944 } else {
1945 None
1946 }
1947 })
1948 .ok_or_else(|| CompileError::UnsupportedExpression(
1949 "mcp.call() second argument must be a string literal (tool_name)".into(),
1950 ))?;
1951
1952 let args = call
1954 .args
1955 .get(2)
1956 .map(|a| self.compile_expr(&a.expr))
1957 .transpose()?;
1958
1959 return Ok(Some((server_id, tool_name, args)));
1960 }
1961 }
1962 }
1963 }
1964 }
1965 }
1966
1967 Ok(None)
1968 }
1969
1970 fn extract_path_template(&mut self, expr: &Expr) -> Result<PathTemplate, CompileError> {
1971 match expr {
1972 Expr::Lit(Lit::Str(s)) => Ok(PathTemplate::static_path(
1974 s.value.to_string_lossy().into_owned(),
1975 )),
1976
1977 Expr::Tpl(tpl) => {
1979 let mut parts = Vec::new();
1980 for (i, quasi) in tpl.quasis.iter().enumerate() {
1981 let raw = quasi.raw.to_string();
1982 if !raw.is_empty() {
1983 parts.push(PathPart::Literal(raw));
1984 }
1985 if i < tpl.exprs.len() {
1986 if let Expr::Ident(ident) = tpl.exprs[i].as_ref() {
1988 parts.push(PathPart::Variable(ident.sym.to_string()));
1989 } else {
1990 let expr = self.compile_expr(&tpl.exprs[i])?;
1992 parts.push(PathPart::Expression(expr));
1993 }
1994 }
1995 }
1996 Ok(PathTemplate { parts })
1997 },
1998
1999 _ => Err(CompileError::InvalidPath(
2000 "Path must be a string or template literal".into(),
2001 )),
2002 }
2003 }
2004
2005 fn extract_arrow_callback(
2006 &mut self,
2007 call: &CallExpr,
2008 ) -> Result<(String, ValueExpr), CompileError> {
2009 let arg = call
2010 .args
2011 .first()
2012 .ok_or_else(|| CompileError::UnsupportedExpression("missing callback".into()))?;
2013
2014 if let Expr::Arrow(arrow) = arg.expr.as_ref() {
2015 let param_name = if let Some(Pat::Ident(ident)) = arrow.params.first() {
2017 ident.id.sym.to_string()
2018 } else {
2019 return Err(CompileError::UnsupportedExpression(
2020 "complex callback parameter".into(),
2021 ));
2022 };
2023
2024 let body = match &*arrow.body {
2026 BlockStmtOrExpr::Expr(expr) => self.compile_expr(expr)?,
2027 BlockStmtOrExpr::BlockStmt(block) => {
2028 let mut bindings: Vec<(String, ValueExpr)> = Vec::new();
2030 let mut return_expr: Option<ValueExpr> = None;
2031
2032 for stmt in &block.stmts {
2033 match stmt {
2034 Stmt::Decl(Decl::Var(var_decl)) => {
2036 for decl in &var_decl.decls {
2037 let var_name = self.get_var_name(&decl.name)?;
2038 if let Some(init) = &decl.init {
2039 let expr = self.compile_expr(init)?;
2040 bindings.push((var_name, expr));
2041 }
2042 }
2043 },
2044 Stmt::Return(ret) => {
2046 if let Some(arg) = &ret.arg {
2047 return_expr = Some(self.compile_expr(arg)?);
2048 }
2049 break; },
2051 Stmt::Expr(_) => {
2053 },
2055 _ => {},
2056 }
2057 }
2058
2059 match return_expr {
2060 Some(result) if bindings.is_empty() => result,
2061 Some(result) => ValueExpr::Block {
2062 bindings,
2063 result: Box::new(result),
2064 },
2065 None => {
2066 return Err(CompileError::UnsupportedExpression(
2067 "callback block without return".into(),
2068 ));
2069 },
2070 }
2071 },
2072 };
2073
2074 Ok((param_name, body))
2075 } else {
2076 Err(CompileError::UnsupportedExpression(
2077 "callback must be arrow function".into(),
2078 ))
2079 }
2080 }
2081
2082 fn extract_reduce_callback(
2084 &mut self,
2085 call: &CallExpr,
2086 ) -> Result<(String, String, ValueExpr), CompileError> {
2087 let arg = call
2088 .args
2089 .first()
2090 .ok_or_else(|| CompileError::UnsupportedExpression("missing callback".into()))?;
2091
2092 if let Expr::Arrow(arrow) = arg.expr.as_ref() {
2093 if arrow.params.len() < 2 {
2095 return Err(CompileError::UnsupportedExpression(
2096 "reduce callback must have 2 parameters".into(),
2097 ));
2098 }
2099
2100 let acc_name = if let Pat::Ident(ident) = &arrow.params[0] {
2101 ident.id.sym.to_string()
2102 } else {
2103 return Err(CompileError::UnsupportedExpression(
2104 "complex callback parameter".into(),
2105 ));
2106 };
2107
2108 let item_name = if let Pat::Ident(ident) = &arrow.params[1] {
2109 ident.id.sym.to_string()
2110 } else {
2111 return Err(CompileError::UnsupportedExpression(
2112 "complex callback parameter".into(),
2113 ));
2114 };
2115
2116 let body = match &*arrow.body {
2118 BlockStmtOrExpr::Expr(expr) => self.compile_expr(expr)?,
2119 BlockStmtOrExpr::BlockStmt(block) => {
2120 for stmt in &block.stmts {
2122 if let Stmt::Return(ret) = stmt {
2123 if let Some(arg) = &ret.arg {
2124 return Ok((acc_name, item_name, self.compile_expr(arg)?));
2125 }
2126 }
2127 }
2128 return Err(CompileError::UnsupportedExpression(
2129 "callback block without return".into(),
2130 ));
2131 },
2132 };
2133
2134 Ok((acc_name, item_name, body))
2135 } else {
2136 Err(CompileError::UnsupportedExpression(
2137 "callback must be arrow function".into(),
2138 ))
2139 }
2140 }
2141
2142 fn extract_number_arg(
2143 &self,
2144 call: &CallExpr,
2145 index: usize,
2146 ) -> Result<Option<i64>, CompileError> {
2147 if let Some(arg) = call.args.get(index) {
2148 if let Expr::Lit(Lit::Num(n)) = arg.expr.as_ref() {
2149 return Ok(Some(n.value as i64));
2150 }
2151 if let Expr::Unary(unary) = arg.expr.as_ref() {
2152 if unary.op == UnaryOp::Minus {
2153 if let Expr::Lit(Lit::Num(n)) = unary.arg.as_ref() {
2154 return Ok(Some(-(n.value as i64)));
2155 }
2156 }
2157 }
2158 }
2159 Ok(None)
2160 }
2161
2162 fn extract_bound(&self, expr: &ValueExpr) -> Option<usize> {
2163 if let ValueExpr::ArrayMethod {
2164 method: ArrayMethodCall::Slice { end, .. },
2165 ..
2166 } = expr
2167 {
2168 return *end;
2169 }
2170 None
2171 }
2172
2173 fn get_var_name(&self, pat: &Pat) -> Result<String, CompileError> {
2174 match pat {
2175 Pat::Ident(ident) => Ok(ident.id.sym.to_string()),
2176 _ => Err(CompileError::UnsupportedExpression(
2177 "complex destructuring".into(),
2178 )),
2179 }
2180 }
2181
2182 fn next_temp_var(&mut self) -> String {
2184 let name = format!("__destructure_{}", self.destructure_counter);
2185 self.destructure_counter += 1;
2186 name
2187 }
2188
2189 fn extract_loop_var(&mut self, pat: &Pat) -> Result<(String, Vec<PlanStep>), CompileError> {
2192 match pat {
2193 Pat::Ident(ident) => Ok((ident.id.sym.to_string(), Vec::new())),
2194 Pat::Object(obj_pat) => {
2195 let temp_var = self.next_temp_var();
2196 let bindings = Self::extract_object_bindings(obj_pat)?;
2197 let steps = bindings
2198 .into_iter()
2199 .map(|(var_name, property)| PlanStep::Assign {
2200 var: var_name,
2201 expr: ValueExpr::PropertyAccess {
2202 object: Box::new(ValueExpr::Variable(temp_var.clone())),
2203 property,
2204 },
2205 })
2206 .collect();
2207 Ok((temp_var, steps))
2208 },
2209 Pat::Array(arr_pat) => {
2210 let temp_var = self.next_temp_var();
2211 let mut steps = Vec::new();
2212 for (i, elem) in arr_pat.elems.iter().enumerate() {
2213 if let Some(p) = elem {
2214 let var_name = self.get_var_name(p)?;
2215 steps.push(PlanStep::Assign {
2216 var: var_name,
2217 expr: ValueExpr::ArrayIndex {
2218 array: Box::new(ValueExpr::Variable(temp_var.clone())),
2219 index: Box::new(ValueExpr::Literal(JsonValue::Number(
2220 (i as i64).into(),
2221 ))),
2222 },
2223 });
2224 }
2225 }
2226 Ok((temp_var, steps))
2227 },
2228 _ => Err(CompileError::UnsupportedExpression(
2229 "complex loop variable pattern".into(),
2230 )),
2231 }
2232 }
2233
2234 fn extract_object_bindings(obj_pat: &ObjectPat) -> Result<Vec<(String, String)>, CompileError> {
2238 let mut bindings = Vec::new();
2239 for prop in &obj_pat.props {
2240 match prop {
2241 ObjectPatProp::Assign(assign) => {
2242 if assign.value.is_some() {
2244 return Err(CompileError::UnsupportedExpression(
2245 "default values in destructuring".into(),
2246 ));
2247 }
2248 let name = assign.key.sym.to_string();
2249 bindings.push((name.clone(), name));
2250 },
2251 ObjectPatProp::KeyValue(kv) => {
2252 let key = match &kv.key {
2254 PropName::Ident(ident) => ident.sym.to_string(),
2255 PropName::Str(s) => s.value.to_string_lossy().into_owned(),
2256 _ => {
2257 return Err(CompileError::UnsupportedExpression(
2258 "computed destructuring key".into(),
2259 ));
2260 },
2261 };
2262 let var_name = match kv.value.as_ref() {
2263 Pat::Ident(ident) => ident.id.sym.to_string(),
2264 _ => {
2265 return Err(CompileError::UnsupportedExpression(
2266 "nested destructuring".into(),
2267 ));
2268 },
2269 };
2270 bindings.push((var_name, key));
2271 },
2272 ObjectPatProp::Rest(_) => {
2273 return Err(CompileError::UnsupportedExpression(
2274 "rest pattern in destructuring".into(),
2275 ));
2276 },
2277 }
2278 }
2279 Ok(bindings)
2280 }
2281
2282 fn compile_object_destructuring(
2285 &mut self,
2286 obj_pat: &ObjectPat,
2287 init: &Expr,
2288 steps: &mut Vec<PlanStep>,
2289 ) -> Result<(), CompileError> {
2290 let bindings = Self::extract_object_bindings(obj_pat)?;
2291 let temp_var = self.next_temp_var();
2292
2293 self.compile_var_init(&temp_var, init, steps)?;
2295
2296 for (var_name, property) in bindings {
2298 steps.push(PlanStep::Assign {
2299 var: var_name,
2300 expr: ValueExpr::PropertyAccess {
2301 object: Box::new(ValueExpr::Variable(temp_var.clone())),
2302 property,
2303 },
2304 });
2305 }
2306 Ok(())
2307 }
2308
2309 fn compile_array_destructuring(
2312 &mut self,
2313 arr_pat: &ArrayPat,
2314 init: &Expr,
2315 steps: &mut Vec<PlanStep>,
2316 ) -> Result<(), CompileError> {
2317 let temp_var = self.next_temp_var();
2318
2319 self.compile_var_init(&temp_var, init, steps)?;
2321
2322 for (i, elem) in arr_pat.elems.iter().enumerate() {
2324 if let Some(pat) = elem {
2325 let var_name = self.get_var_name(pat)?;
2326 steps.push(PlanStep::Assign {
2327 var: var_name,
2328 expr: ValueExpr::ArrayIndex {
2329 array: Box::new(ValueExpr::Variable(temp_var.clone())),
2330 index: Box::new(ValueExpr::Literal(JsonValue::Number((i as i64).into()))),
2331 },
2332 });
2333 }
2334 }
2336 Ok(())
2337 }
2338
2339 fn lit_to_json(&self, lit: &Lit) -> JsonValue {
2340 match lit {
2341 Lit::Str(s) => JsonValue::String(s.value.to_string_lossy().into_owned()),
2342 Lit::Num(n) => {
2343 if n.value.fract() == 0.0 {
2344 JsonValue::Number((n.value as i64).into())
2345 } else {
2346 serde_json::Number::from_f64(n.value)
2347 .map(JsonValue::Number)
2348 .unwrap_or(JsonValue::Null)
2349 }
2350 },
2351 Lit::Bool(b) => JsonValue::Bool(b.value),
2352 Lit::Null(_) => JsonValue::Null,
2353 _ => JsonValue::Null,
2354 }
2355 }
2356
2357 fn prop_name_to_string(&self, prop: &PropName) -> Result<String, CompileError> {
2358 match prop {
2359 PropName::Ident(ident) => Ok(ident.sym.to_string()),
2360 PropName::Str(s) => Ok(s.value.to_string_lossy().into_owned()),
2361 PropName::Num(n) => Ok(n.value.to_string()),
2362 _ => Err(CompileError::UnsupportedExpression(
2363 "computed property".into(),
2364 )),
2365 }
2366 }
2367
2368 fn compile_bin_op(&self, op: BinaryOp) -> Result<BinaryOperator, CompileError> {
2369 match op {
2370 BinaryOp::Add => Ok(BinaryOperator::Add),
2371 BinaryOp::Sub => Ok(BinaryOperator::Sub),
2372 BinaryOp::Mul => Ok(BinaryOperator::Mul),
2373 BinaryOp::Div => Ok(BinaryOperator::Div),
2374 BinaryOp::Mod => Ok(BinaryOperator::Mod),
2375 BinaryOp::BitOr => Ok(BinaryOperator::BitwiseOr),
2376 BinaryOp::EqEq => Ok(BinaryOperator::Eq),
2377 BinaryOp::NotEq => Ok(BinaryOperator::NotEq),
2378 BinaryOp::EqEqEq => Ok(BinaryOperator::StrictEq),
2379 BinaryOp::NotEqEq => Ok(BinaryOperator::StrictNotEq),
2380 BinaryOp::Lt => Ok(BinaryOperator::Lt),
2381 BinaryOp::LtEq => Ok(BinaryOperator::Lte),
2382 BinaryOp::Gt => Ok(BinaryOperator::Gt),
2383 BinaryOp::GtEq => Ok(BinaryOperator::Gte),
2384 BinaryOp::LogicalAnd => Ok(BinaryOperator::And),
2385 BinaryOp::LogicalOr => Ok(BinaryOperator::Or),
2386 BinaryOp::NullishCoalescing => {
2387 Err(CompileError::UnsupportedExpression(
2389 "nullish coalescing".into(),
2390 ))
2391 },
2392 _ => Err(CompileError::UnsupportedExpression(format!(
2393 "binary operator {:?}",
2394 op
2395 ))),
2396 }
2397 }
2398
2399 fn record_api_call(&mut self, method: &str, path: &PathTemplate) {
2400 self.api_call_count += 1;
2401
2402 if !self.methods_used.contains(&method.to_string()) {
2404 self.methods_used.push(method.to_string());
2405 }
2406
2407 if method != "GET" && method != "HEAD" && method != "OPTIONS" {
2409 self.has_mutations = true;
2410 }
2411
2412 let endpoint = if !path.is_dynamic() {
2414 path.parts
2415 .iter()
2416 .filter_map(|p| match p {
2417 PathPart::Literal(s) => Some(s.clone()),
2418 _ => None,
2419 })
2420 .collect::<String>()
2421 } else {
2422 "{dynamic}".to_string()
2423 };
2424 if !self.endpoints.contains(&endpoint) {
2425 self.endpoints.push(endpoint);
2426 }
2427 }
2428}
2429
2430impl Default for PlanCompiler {
2431 fn default() -> Self {
2432 Self::new()
2433 }
2434}
2435
2436#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2483pub struct ResolvedPath<'a>(&'a str);
2484
2485impl<'a> ResolvedPath<'a> {
2486 pub fn from_checked(path: &'a str) -> Result<Self, crate::PlaceholderRefusal> {
2536 crate::validate_resolved_target(path)?;
2537 Ok(Self(path))
2538 }
2539
2540 #[must_use]
2542 pub fn as_str(&self) -> &'a str {
2543 self.0
2544 }
2545}
2546
2547impl std::fmt::Display for ResolvedPath<'_> {
2548 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2549 f.write_str(self.0)
2550 }
2551}
2552
2553#[async_trait::async_trait]
2574pub trait HttpExecutor: Send + Sync {
2575 async fn execute_request(
2581 &self,
2582 method: &str,
2583 path: ResolvedPath<'_>,
2584 body: Option<JsonValue>,
2585 ) -> Result<JsonValue, ExecutionError>;
2586
2587 fn placeholder_rules(
2613 &self,
2614 method: &str,
2615 path_template: &str,
2616 param: &str,
2617 ) -> crate::PlaceholderRules<'_> {
2618 let _ = (method, path_template, param);
2619 crate::PlaceholderRules::default()
2620 }
2621}
2622
2623#[cfg(feature = "mcp-code-mode")]
2629#[async_trait::async_trait]
2630pub trait McpExecutor: Send + Sync {
2631 async fn call_tool(
2633 &self,
2634 server_id: &str,
2635 tool_name: &str,
2636 args: JsonValue,
2637 ) -> Result<JsonValue, ExecutionError>;
2638}
2639
2640#[async_trait::async_trait]
2646pub trait SdkExecutor: Send + Sync {
2647 async fn execute_operation(
2652 &self,
2653 operation: &str,
2654 args: Option<JsonValue>,
2655 ) -> Result<JsonValue, ExecutionError>;
2656}
2657
2658#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
2664pub enum MockExecutionMode {
2665 #[default]
2667 DryRun,
2668 Testing,
2670 Record,
2672}
2673
2674pub struct MockHttpExecutor {
2712 responses: std::sync::RwLock<HashMap<String, JsonValue>>,
2714 default_response: JsonValue,
2716 recorded_calls: std::sync::RwLock<Vec<MockedCall>>,
2718}
2719
2720#[derive(Debug, Clone, Serialize)]
2722pub struct MockedCall {
2723 pub method: String,
2725 pub path: String,
2727 pub body: Option<JsonValue>,
2729 pub response: JsonValue,
2731}
2732
2733impl MockHttpExecutor {
2734 pub fn new_dry_run() -> Self {
2737 Self {
2738 responses: std::sync::RwLock::new(HashMap::new()),
2739 default_response: JsonValue::Object(serde_json::Map::new()),
2740 recorded_calls: std::sync::RwLock::new(Vec::new()),
2741 }
2742 }
2743
2744 pub fn new_testing() -> Self {
2747 Self {
2748 responses: std::sync::RwLock::new(HashMap::new()),
2749 default_response: JsonValue::Object(serde_json::Map::new()),
2750 recorded_calls: std::sync::RwLock::new(Vec::new()),
2751 }
2752 }
2753
2754 pub fn with_default_response(mut self, response: JsonValue) -> Self {
2756 self.default_response = response;
2757 self
2758 }
2759
2760 pub fn with_response(self, path_pattern: &str, response: JsonValue) -> Self {
2771 self.responses
2772 .write()
2773 .unwrap()
2774 .insert(path_pattern.to_string(), response);
2775 self
2776 }
2777
2778 pub fn add_response(&self, path_pattern: &str, response: JsonValue) {
2780 self.responses
2781 .write()
2782 .unwrap()
2783 .insert(path_pattern.to_string(), response);
2784 }
2785
2786 pub fn recorded_calls(&self) -> Vec<MockedCall> {
2788 self.recorded_calls.read().unwrap().clone()
2789 }
2790
2791 pub fn clear_calls(&self) {
2793 self.recorded_calls.write().unwrap().clear();
2794 }
2795
2796 pub fn call_count(&self) -> usize {
2798 self.recorded_calls.read().unwrap().len()
2799 }
2800
2801 pub fn was_called(&self, path: &str) -> bool {
2803 self.recorded_calls
2804 .read()
2805 .unwrap()
2806 .iter()
2807 .any(|c| c.path == path)
2808 }
2809
2810 pub fn was_called_with_method(&self, method: &str, path: &str) -> bool {
2812 self.recorded_calls
2813 .read()
2814 .unwrap()
2815 .iter()
2816 .any(|c| c.method == method && c.path == path)
2817 }
2818
2819 fn find_response(&self, path: &str) -> JsonValue {
2821 let responses = self.responses.read().unwrap();
2822
2823 if let Some(response) = responses.get(path) {
2825 return response.clone();
2826 }
2827
2828 for (pattern, response) in responses.iter() {
2830 if Self::matches_pattern(pattern, path) {
2831 return response.clone();
2832 }
2833 }
2834
2835 self.default_response.clone()
2837 }
2838
2839 fn matches_pattern(pattern: &str, path: &str) -> bool {
2841 if !pattern.contains('*') {
2842 return pattern == path;
2843 }
2844
2845 let pattern_parts: Vec<&str> = pattern.split('/').collect();
2846 let path_parts: Vec<&str> = path.split('/').collect();
2847
2848 if pattern_parts.len() != path_parts.len() {
2849 if pattern.ends_with("*") && path_parts.len() >= pattern_parts.len() - 1 {
2851 } else {
2853 return false;
2854 }
2855 }
2856
2857 for (p, s) in pattern_parts.iter().zip(path_parts.iter()) {
2858 if *p != "*" && *p != *s {
2859 return false;
2860 }
2861 }
2862
2863 true
2864 }
2865}
2866
2867#[async_trait::async_trait]
2868impl HttpExecutor for MockHttpExecutor {
2869 async fn execute_request(
2870 &self,
2871 method: &str,
2872 path: ResolvedPath<'_>,
2873 body: Option<JsonValue>,
2874 ) -> Result<JsonValue, ExecutionError> {
2875 let path = path.as_str();
2876 let response = self.find_response(path);
2877
2878 let call = MockedCall {
2880 method: method.to_string(),
2881 path: path.to_string(),
2882 body,
2883 response: response.clone(),
2884 };
2885 self.recorded_calls.write().unwrap().push(call);
2886
2887 Ok(response)
2888 }
2889}
2890
2891unsafe impl Send for MockHttpExecutor {}
2893unsafe impl Sync for MockHttpExecutor {}
2894
2895#[derive(Debug, Clone, Serialize)]
2897pub struct ExecutionResult {
2898 pub value: JsonValue,
2900 pub api_calls: Vec<ApiCallLog>,
2902 pub execution_time_ms: u64,
2904}
2905
2906#[derive(Debug, Clone, Serialize)]
2908pub struct ApiCallLog {
2909 pub method: String,
2911 pub path: String,
2921 pub body: Option<JsonValue>,
2923 pub response: JsonValue,
2925 pub duration_ms: u64,
2927}
2928
2929fn refusal_to_execution_error(refusal: crate::PlaceholderRefusal) -> ExecutionError {
2937 ExecutionError::RequestRefused {
2938 message: refusal.to_string(),
2939 }
2940}
2941
2942fn api_call_error(method: &str, result_var: &str, error: ExecutionError) -> ExecutionError {
2954 match error {
2955 ExecutionError::RequestRefused { message } => ExecutionError::RequestRefused {
2956 message: format!("{method} api call '{result_var}' was refused: {message}"),
2957 },
2958 other => ExecutionError::RuntimeError {
2959 message: format!("{method} api call '{result_var}' failed: {other}"),
2960 },
2961 }
2962}
2963
2964fn floor_layer_one_contribution(param: &str, rendered: &str) -> Result<(), ExecutionError> {
2974 crate::validate_path_placeholder(param, rendered, &crate::PlaceholderRules::default())
2975 .map_err(refusal_to_execution_error)
2976}
2977
2978fn render_path_scalar(key: &str, value: JsonValue) -> Result<String, ExecutionError> {
2993 match value {
2994 JsonValue::String(s) => Ok(s),
2995 JsonValue::Null => Ok("null".to_string()),
2996 JsonValue::Number(n) => Ok(n.to_string()),
2997 JsonValue::Bool(b) => Ok(b.to_string()),
2998 JsonValue::Object(_) | JsonValue::Array(_) => Err(ExecutionError::RequestRefused {
2999 message: format!("path/query param '{key}' must be a scalar"),
3000 }),
3001 }
3002}
3003
3004fn apply_substitutions(template: &str, substitutions: &[(String, String)]) -> String {
3025 let mut order: Vec<(&str, &str)> = substitutions
3026 .iter()
3027 .map(|(placeholder, rendered)| (placeholder.as_str(), rendered.as_str()))
3028 .collect();
3029 order.sort_by_key(|(placeholder, _)| std::cmp::Reverse(placeholder.len()));
3030
3031 let mut resolved = String::with_capacity(template.len());
3032 let mut rest = template;
3033 while let Some(ch) = rest.chars().next() {
3034 match order.iter().copied().find(|(p, _)| rest.starts_with(*p)) {
3035 Some((placeholder, rendered)) => {
3036 resolved.push_str(rendered);
3037 rest = &rest[placeholder.len()..];
3038 },
3039 None => {
3040 resolved.push(ch);
3041 rest = &rest[ch.len_utf8()..];
3042 },
3043 }
3044 }
3045 resolved
3046}
3047
3048fn resolve_layer_two_placeholders<H: HttpExecutor + ?Sized>(
3073 http: &H,
3074 method: &str,
3075 template: &str,
3076 body: Option<JsonValue>,
3077) -> Result<(String, Option<JsonValue>), ExecutionError> {
3078 let obj = match body {
3083 Some(JsonValue::Object(obj)) => obj,
3084 other => return Ok((template.to_string(), other)),
3085 };
3086
3087 let mut substitutions: Vec<(String, String)> = Vec::new();
3089 let mut remaining = serde_json::Map::new();
3090 let mut probe = String::new();
3095 for (key, value) in obj {
3096 probe.clear();
3097 probe.push('{');
3098 probe.push_str(&key);
3099 probe.push('}');
3100 if template.contains(probe.as_str()) {
3101 let rules = http.placeholder_rules(method, template, &key);
3102 let rendered = render_path_scalar(&key, value)?;
3105 crate::validate_path_placeholder(&key, &rendered, &rules)
3106 .map_err(refusal_to_execution_error)?;
3107 substitutions.push((probe.clone(), rendered));
3108 } else {
3109 remaining.insert(key, value);
3110 }
3111 }
3112
3113 let resolved = apply_substitutions(template, &substitutions);
3115
3116 let remaining = if remaining.is_empty() {
3117 None
3118 } else {
3119 Some(JsonValue::Object(remaining))
3120 };
3121 Ok((resolved, remaining))
3122}
3123
3124pub struct PlanExecutor<H: HttpExecutor> {
3126 http: H,
3127 config: ExecutionConfig,
3128 variables: HashMap<String, JsonValue>,
3129 api_calls: Vec<ApiCallLog>,
3130 api_call_count: usize,
3131 #[cfg(feature = "mcp-code-mode")]
3132 mcp: Option<Box<dyn McpExecutor>>,
3133 sdk: Option<Box<dyn SdkExecutor>>,
3135}
3136
3137impl<H: HttpExecutor> PlanExecutor<H> {
3138 pub fn new(http: H, config: ExecutionConfig) -> Self {
3140 Self {
3141 http,
3142 config,
3143 variables: HashMap::new(),
3144 api_calls: Vec::new(),
3145 api_call_count: 0,
3146 #[cfg(feature = "mcp-code-mode")]
3147 mcp: None,
3148 sdk: None,
3149 }
3150 }
3151
3152 #[cfg(feature = "mcp-code-mode")]
3154 pub fn set_mcp_executor(&mut self, executor: impl McpExecutor + 'static) {
3155 self.mcp = Some(Box::new(executor));
3156 }
3157
3158 pub fn set_sdk_executor(&mut self, executor: impl SdkExecutor + 'static) {
3160 self.sdk = Some(Box::new(executor));
3161 }
3162
3163 pub fn set_variable(&mut self, name: impl Into<String>, value: JsonValue) {
3165 self.variables.insert(name.into(), value);
3166 }
3167
3168 pub async fn execute(
3170 &mut self,
3171 plan: &ExecutionPlan,
3172 ) -> Result<ExecutionResult, ExecutionError> {
3173 let start = std::time::Instant::now();
3174
3175 let mut return_value = JsonValue::Null;
3176
3177 for step in &plan.steps {
3178 match self.execute_step(step).await? {
3179 StepOutcome::Return(value) => {
3180 return_value = value;
3181 break; },
3183 StepOutcome::None | StepOutcome::Continue | StepOutcome::Break => {},
3184 }
3185 }
3186
3187 let blocked_in_output =
3190 find_blocked_fields_in_output(&return_value, &self.config.output_blocked_fields);
3191
3192 if !blocked_in_output.is_empty() {
3193 return Err(ExecutionError::RuntimeError {
3194 message: format!(
3195 "Script output contains blocked fields: {}",
3196 blocked_in_output.join(", ")
3197 ),
3198 });
3199 }
3200
3201 Ok(ExecutionResult {
3202 value: return_value,
3203 api_calls: std::mem::take(&mut self.api_calls),
3204 execution_time_ms: start.elapsed().as_millis() as u64,
3205 })
3206 }
3207
3208 fn execute_step<'a>(
3211 &'a mut self,
3212 step: &'a PlanStep,
3213 ) -> std::pin::Pin<
3214 Box<dyn std::future::Future<Output = Result<StepOutcome, ExecutionError>> + Send + 'a>,
3215 > {
3216 Box::pin(async move {
3217 match step {
3218 PlanStep::ApiCall {
3219 result_var,
3220 method,
3221 path,
3222 body,
3223 } => {
3224 self.api_call_count += 1;
3225 if self.api_call_count > self.config.max_api_calls {
3226 return Err(ExecutionError::RuntimeError {
3227 message: format!(
3228 "Too many API calls: {} (max: {})",
3229 self.api_call_count, self.config.max_api_calls
3230 ),
3231 });
3232 }
3233
3234 let templated_path = self.resolve_path(path)?;
3237 let evaluated_body = match body {
3238 Some(expr) => Some(self.evaluate(expr)?),
3239 None => None,
3240 };
3241 let (resolved_path, resolved_body) = resolve_layer_two_placeholders(
3243 &self.http,
3244 method,
3245 &templated_path,
3246 evaluated_body,
3247 )?;
3248 let checked_path = ResolvedPath::from_checked(&resolved_path)
3254 .map_err(refusal_to_execution_error)?;
3255
3256 let call_start = std::time::Instant::now();
3257 let raw_response = self
3258 .http
3259 .execute_request(method, checked_path, resolved_body.clone())
3260 .await
3261 .map_err(|e| api_call_error(method, &result_var, e))?;
3262 let duration_ms = call_start.elapsed().as_millis() as u64;
3263
3264 let response = filter_blocked_fields(raw_response, &self.config.blocked_fields);
3267
3268 self.api_calls.push(ApiCallLog {
3269 method: method.clone(),
3270 path: resolved_path,
3271 body: resolved_body,
3272 response: response.clone(),
3273 duration_ms,
3274 });
3275
3276 if result_var != "_" {
3277 self.variables.insert(result_var.clone(), response);
3278 }
3279 Ok(StepOutcome::None)
3280 },
3281
3282 PlanStep::Assign { var, expr } => {
3283 let value = self.evaluate(expr)?;
3284 self.variables.insert(var.clone(), value);
3285 Ok(StepOutcome::None)
3286 },
3287
3288 PlanStep::Conditional {
3289 condition,
3290 then_steps,
3291 else_steps,
3292 } => {
3293 let cond_value = self.evaluate(condition)?;
3294 let steps = if shared_is_truthy(&cond_value) {
3295 then_steps
3296 } else {
3297 else_steps
3298 };
3299
3300 for step in steps {
3301 match self.execute_step(step).await? {
3302 StepOutcome::None => {},
3303 outcome => return Ok(outcome),
3304 }
3305 }
3306 Ok(StepOutcome::None)
3307 },
3308
3309 PlanStep::BoundedLoop {
3310 item_var,
3311 collection,
3312 max_iterations,
3313 body,
3314 } => {
3315 let collection_value = self.evaluate(collection)?;
3316 let items = match collection_value {
3317 JsonValue::Array(arr) => arr,
3318 _ => {
3319 return Err(ExecutionError::RuntimeError {
3320 message: "Loop collection must be an array".into(),
3321 })
3322 },
3323 };
3324
3325 let limit = (*max_iterations).min(self.config.max_loop_iterations);
3326 'outer: for item in items.into_iter().take(limit) {
3327 self.variables.insert(item_var.clone(), item);
3328
3329 for step in body {
3330 match self.execute_step(step).await? {
3331 StepOutcome::Return(value) => {
3332 return Ok(StepOutcome::Return(value))
3333 },
3334 StepOutcome::None => {},
3335 StepOutcome::Continue => continue 'outer,
3336 StepOutcome::Break => break 'outer,
3337 }
3338 }
3339 }
3340 Ok(StepOutcome::None)
3341 },
3342
3343 PlanStep::Return { value } => {
3344 let result = self.evaluate(value)?;
3345 Ok(StepOutcome::Return(result))
3346 },
3347
3348 PlanStep::TryCatch {
3349 try_steps,
3350 catch_var,
3351 catch_steps,
3352 finally_steps,
3353 } => {
3354 let try_result = async {
3356 for step in try_steps {
3357 match self.execute_step(step).await? {
3358 StepOutcome::None => {},
3359 outcome => return Ok::<StepOutcome, ExecutionError>(outcome),
3360 }
3361 }
3362 Ok(StepOutcome::None)
3363 }
3364 .await;
3365
3366 let result = match try_result {
3368 Ok(outcome) => {
3369 outcome
3371 },
3372 Err(error) => {
3373 if let Some(var) = catch_var {
3375 let error_obj = JsonValue::Object(serde_json::Map::from_iter([(
3377 "message".to_string(),
3378 JsonValue::String(format!("{}", error)),
3379 )]));
3380 self.variables.insert(var.clone(), error_obj);
3381 }
3382
3383 let mut catch_outcome = StepOutcome::None;
3385 for step in catch_steps {
3386 match self.execute_step(step).await? {
3387 StepOutcome::None => {},
3388 outcome => {
3389 catch_outcome = outcome;
3390 break;
3391 },
3392 }
3393 }
3394 catch_outcome
3395 },
3396 };
3397
3398 for step in finally_steps {
3400 match self.execute_step(step).await? {
3401 StepOutcome::None => {},
3402 outcome => return Ok(outcome),
3403 }
3404 }
3405
3406 Ok(result)
3407 },
3408
3409 PlanStep::ParallelApiCalls { result_var, calls } => {
3413 let mut results = Vec::with_capacity(calls.len());
3414 for (temp_var, method, path, body) in calls {
3415 self.api_call_count += 1;
3416 if self.api_call_count > self.config.max_api_calls {
3417 return Err(ExecutionError::RuntimeError {
3418 message: format!(
3419 "Maximum API calls exceeded ({})",
3420 self.config.max_api_calls
3421 ),
3422 });
3423 }
3424
3425 let templated_path = self.resolve_path(path)?;
3430 let evaluated_body = body.as_ref().map(|b| self.evaluate(b)).transpose()?;
3431 let (resolved_path, resolved_body) = resolve_layer_two_placeholders(
3432 &self.http,
3433 method,
3434 &templated_path,
3435 evaluated_body,
3436 )?;
3437 let checked_path = ResolvedPath::from_checked(&resolved_path)
3438 .map_err(refusal_to_execution_error)?;
3439 let call_start = std::time::Instant::now();
3440 let raw_response = self
3441 .http
3442 .execute_request(method, checked_path, resolved_body.clone())
3443 .await
3444 .map_err(|e| api_call_error(method, &temp_var, e))?;
3445 let duration_ms = call_start.elapsed().as_millis() as u64;
3446 let response =
3447 filter_blocked_fields(raw_response, &self.config.blocked_fields);
3448
3449 self.api_calls.push(ApiCallLog {
3450 method: method.clone(),
3451 path: resolved_path,
3452 body: resolved_body,
3453 response: response.clone(),
3454 duration_ms,
3455 });
3456
3457 results.push(response);
3458 }
3459 self.variables
3460 .insert(result_var.clone(), JsonValue::Array(results));
3461 Ok(StepOutcome::None)
3462 },
3463
3464 PlanStep::Continue => Ok(StepOutcome::Continue),
3466
3467 PlanStep::Break => Ok(StepOutcome::Break),
3469
3470 #[cfg(feature = "mcp-code-mode")]
3472 PlanStep::McpCall {
3473 result_var,
3474 server_id,
3475 tool_name,
3476 args,
3477 } => {
3478 self.api_call_count += 1;
3479 if self.api_call_count > self.config.max_api_calls {
3480 return Err(ExecutionError::RuntimeError {
3481 message: format!(
3482 "Too many calls: {} (max: {})",
3483 self.api_call_count, self.config.max_api_calls
3484 ),
3485 });
3486 }
3487
3488 let resolved_args = match args {
3489 Some(expr) => self.evaluate(expr)?,
3490 None => JsonValue::Object(Default::default()),
3491 };
3492
3493 let mcp_executor =
3494 self.mcp
3495 .as_ref()
3496 .ok_or_else(|| ExecutionError::RuntimeError {
3497 message: "MCP executor not configured".into(),
3498 })?;
3499
3500 let call_start = std::time::Instant::now();
3501 let result = mcp_executor
3502 .call_tool(server_id, tool_name, resolved_args.clone())
3503 .await?;
3504 let duration_ms = call_start.elapsed().as_millis() as u64;
3505
3506 self.api_calls.push(ApiCallLog {
3507 method: format!("MCP:{}.{}", server_id, tool_name),
3508 path: format!("{}/{}", server_id, tool_name),
3509 body: Some(resolved_args),
3510 response: result.clone(),
3511 duration_ms,
3512 });
3513
3514 if result_var != "_" {
3515 self.variables.insert(result_var.clone(), result);
3516 }
3517 Ok(StepOutcome::None)
3518 },
3519
3520 PlanStep::SdkCall {
3522 result_var,
3523 operation,
3524 args,
3525 } => {
3526 self.api_call_count += 1;
3527 if self.api_call_count > self.config.max_api_calls {
3528 return Err(ExecutionError::RuntimeError {
3529 message: format!(
3530 "Too many calls: {} (max: {})",
3531 self.api_call_count, self.config.max_api_calls
3532 ),
3533 });
3534 }
3535
3536 let resolved_args =
3537 args.as_ref().map(|expr| self.evaluate(expr)).transpose()?;
3538
3539 let sdk_executor =
3540 self.sdk
3541 .as_ref()
3542 .ok_or_else(|| ExecutionError::RuntimeError {
3543 message: "SDK executor not configured".into(),
3544 })?;
3545
3546 let call_start = std::time::Instant::now();
3547 let result = sdk_executor
3548 .execute_operation(operation, resolved_args.clone())
3549 .await?;
3550 let duration_ms = call_start.elapsed().as_millis() as u64;
3551
3552 self.api_calls.push(ApiCallLog {
3553 method: operation.clone(),
3554 path: format!("sdk:{}", operation),
3555 body: resolved_args,
3556 response: result.clone(),
3557 duration_ms,
3558 });
3559
3560 if result_var != "_" {
3561 self.variables.insert(result_var.clone(), result);
3562 }
3563 Ok(StepOutcome::None)
3564 },
3565 }
3566 })
3567 }
3568
3569 fn resolve_path(&self, path: &PathTemplate) -> Result<String, ExecutionError> {
3592 let mut result = String::new();
3593 for (index, part) in path.parts.iter().enumerate() {
3594 match part {
3595 PathPart::Literal(s) => result.push_str(s),
3596 PathPart::Variable(var) => {
3597 let value =
3598 self.variables
3599 .get(var)
3600 .ok_or_else(|| ExecutionError::RuntimeError {
3601 message: format!("Undefined variable in path: {}", var),
3602 })?;
3603 let rendered = shared_json_to_string_with_mode(value, JsonStringMode::Json);
3604 floor_layer_one_contribution(var, &rendered)?;
3630 result.push_str(&rendered);
3631 },
3632 PathPart::Expression(expr) => {
3633 let value = self.evaluate(expr)?;
3634 let rendered = shared_json_to_string_with_mode(&value, JsonStringMode::Json);
3635 floor_layer_one_contribution(&format!("path expression #{index}"), &rendered)?;
3640 result.push_str(&rendered);
3641 },
3642 }
3643 }
3644 Ok(result)
3645 }
3646
3647 fn evaluate(&self, expr: &ValueExpr) -> Result<JsonValue, ExecutionError> {
3650 shared_evaluate(expr, &self.variables)
3651 }
3652}
3653
3654pub type JsExecutor = PlanCompiler;
3660
3661#[cfg(test)]
3662mod tests {
3663 use super::*;
3664
3665 #[test]
3666 fn test_execution_config_default() {
3667 let config = ExecutionConfig::default();
3668 assert_eq!(config.max_api_calls, 50);
3669 assert_eq!(config.timeout_seconds, 30);
3670 assert_eq!(config.max_loop_iterations, 100);
3671 }
3672
3673 #[test]
3674 fn test_path_template_static() {
3675 let path = PathTemplate::static_path("/users".into());
3676 assert!(!path.is_dynamic());
3677 }
3678
3679 #[test]
3680 fn test_path_template_dynamic() {
3681 let path = PathTemplate {
3682 parts: vec![
3683 PathPart::Literal("/users/".into()),
3684 PathPart::Variable("id".into()),
3685 ],
3686 };
3687 assert!(path.is_dynamic());
3688 }
3689
3690 #[test]
3691 fn test_plan_metadata() {
3692 let metadata = PlanMetadata {
3693 api_call_count: 2,
3694 has_mutations: false,
3695 endpoints: vec!["/users".into(), "/products".into()],
3696 methods_used: vec!["GET".into()],
3697 };
3698 assert_eq!(metadata.api_call_count, 2);
3699 assert!(!metadata.has_mutations);
3700 }
3701
3702 #[test]
3703 fn test_compile_simple_api_call() {
3704 let code = r#"
3705 const user = await api.get('/users/1');
3706 return user;
3707 "#;
3708
3709 let mut compiler = PlanCompiler::new();
3710 let plan = compiler.compile_code(code).expect("Should compile");
3711
3712 assert_eq!(plan.metadata.api_call_count, 1);
3713 assert!(!plan.metadata.has_mutations);
3714 assert_eq!(plan.steps.len(), 2); }
3716
3717 #[test]
3718 fn test_compile_multiple_api_calls() {
3719 let code = r#"
3720 const users = await api.get('/users');
3721 const products = await api.get('/products');
3722 return { users, products };
3723 "#;
3724
3725 let mut compiler = PlanCompiler::new();
3726 let plan = compiler.compile_code(code).expect("Should compile");
3727
3728 assert_eq!(plan.metadata.api_call_count, 2);
3729 assert!(!plan.metadata.has_mutations);
3730 }
3731
3732 #[test]
3733 fn test_compile_mutation() {
3734 let code = r#"
3735 const result = await api.post('/users', { name: 'Test' });
3736 return result;
3737 "#;
3738
3739 let mut compiler = PlanCompiler::new();
3740 let plan = compiler.compile_code(code).expect("Should compile");
3741
3742 assert_eq!(plan.metadata.api_call_count, 1);
3743 assert!(plan.metadata.has_mutations);
3744 }
3745
3746 #[test]
3747 fn test_compile_dynamic_path() {
3748 let code = r#"
3749 const id = 123;
3750 const user = await api.get(`/users/${id}`);
3751 return user;
3752 "#;
3753
3754 let mut compiler = PlanCompiler::new();
3755 let plan = compiler.compile_code(code).expect("Should compile");
3756
3757 assert_eq!(plan.metadata.api_call_count, 1);
3758 }
3759
3760 #[test]
3761 fn test_compile_bounded_loop() {
3762 let code = r#"
3763 const items = [];
3764 const users = [{ id: 1 }, { id: 2 }, { id: 3 }];
3765 for (const user of users.slice(0, 2)) {
3766 const detail = await api.get(`/users/${user.id}`);
3767 items.push(detail);
3768 }
3769 return items;
3770 "#;
3771
3772 let mut compiler = PlanCompiler::new();
3773 let plan = compiler.compile_code(code).expect("Should compile");
3774
3775 assert!(plan
3777 .steps
3778 .iter()
3779 .any(|s| matches!(s, PlanStep::BoundedLoop { .. })));
3780 }
3781
3782 #[test]
3783 fn test_compile_unbounded_loop_detection() {
3784 let code = r#"
3788 const users = [{ id: 1 }, { id: 2 }, { id: 3 }];
3789 for (const user of users) {
3790 const detail = await api.get(`/users/${user.id}`);
3791 }
3792 return users;
3793 "#;
3794
3795 let mut compiler = PlanCompiler::new();
3796 let result = compiler.compile_code(code);
3797
3798 assert!(result.is_ok(), "Loop compiled: {:?}", result);
3801 }
3802
3803 #[test]
3804 fn test_compile_conditional() {
3805 let code = r#"
3806 const user = await api.get('/users/1');
3807 if (user.active) {
3808 const orders = await api.get(`/users/${user.id}/orders`);
3809 return orders;
3810 } else {
3811 return [];
3812 }
3813 "#;
3814
3815 let mut compiler = PlanCompiler::new();
3816 let plan = compiler.compile_code(code).expect("Should compile");
3817
3818 assert!(plan
3819 .steps
3820 .iter()
3821 .any(|s| matches!(s, PlanStep::Conditional { .. })));
3822 }
3823
3824 struct MockHttpExecutor {
3826 responses: std::collections::HashMap<String, JsonValue>,
3827 }
3828
3829 impl MockHttpExecutor {
3830 fn new() -> Self {
3831 Self {
3832 responses: std::collections::HashMap::new(),
3833 }
3834 }
3835
3836 fn add_response(&mut self, path: &str, response: JsonValue) {
3837 self.responses.insert(path.to_string(), response);
3838 }
3839 }
3840
3841 #[async_trait::async_trait]
3842 impl HttpExecutor for MockHttpExecutor {
3843 async fn execute_request(
3844 &self,
3845 _method: &str,
3846 path: ResolvedPath<'_>,
3847 _body: Option<JsonValue>,
3848 ) -> Result<JsonValue, ExecutionError> {
3849 let path = path.as_str();
3850 self.responses
3851 .get(path)
3852 .cloned()
3853 .ok_or_else(|| ExecutionError::RuntimeError {
3854 message: format!("No mock response for path: {}", path),
3855 })
3856 }
3857 }
3858
3859 #[tokio::test]
3860 async fn test_execute_simple_api_call() {
3861 let code = r#"
3862 const user = await api.get('/users/1');
3863 return user;
3864 "#;
3865
3866 let mut compiler = PlanCompiler::new();
3867 let plan = compiler.compile_code(code).expect("Should compile");
3868
3869 let mut mock_http = MockHttpExecutor::new();
3870 mock_http.add_response("/users/1", serde_json::json!({ "id": 1, "name": "Alice" }));
3871
3872 let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
3873 let result = executor.execute(&plan).await.expect("Should execute");
3874
3875 assert_eq!(result.value["id"], 1);
3876 assert_eq!(result.value["name"], "Alice");
3877 assert_eq!(result.api_calls.len(), 1);
3878 }
3879
3880 #[tokio::test]
3881 async fn test_execute_multiple_api_calls() {
3882 let code = r#"
3883 const users = await api.get('/users');
3884 const products = await api.get('/products');
3885 return { users, products };
3886 "#;
3887
3888 let mut compiler = PlanCompiler::new();
3889 let plan = compiler.compile_code(code).expect("Should compile");
3890
3891 let mut mock_http = MockHttpExecutor::new();
3892 mock_http.add_response("/users", serde_json::json!([{ "id": 1, "name": "Alice" }]));
3893 mock_http.add_response(
3894 "/products",
3895 serde_json::json!([{ "id": 100, "name": "Widget" }]),
3896 );
3897
3898 let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
3899 let result = executor.execute(&plan).await.expect("Should execute");
3900
3901 assert!(result.value["users"].is_array());
3902 assert!(result.value["products"].is_array());
3903 assert_eq!(result.api_calls.len(), 2);
3904 }
3905
3906 #[tokio::test]
3907 async fn test_execute_with_template_path() {
3908 let code = r#"
3909 const userId = 42;
3910 const user = await api.get(`/users/${userId}`);
3911 return user;
3912 "#;
3913
3914 let mut compiler = PlanCompiler::new();
3915 let plan = compiler.compile_code(code).expect("Should compile");
3916
3917 let mut mock_http = MockHttpExecutor::new();
3918 mock_http.add_response("/users/42", serde_json::json!({ "id": 42, "name": "Bob" }));
3919
3920 let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
3921 let result = executor.execute(&plan).await.expect("Should execute");
3922
3923 assert_eq!(result.value["id"], 42);
3924 assert_eq!(result.value["name"], "Bob");
3925 }
3926
3927 #[tokio::test]
3928 async fn test_execute_conditional_true_branch() {
3929 let code = r#"
3930 const user = await api.get('/users/1');
3931 if (user.active) {
3932 return { status: "active", user: user };
3933 } else {
3934 return { status: "inactive" };
3935 }
3936 "#;
3937
3938 let mut compiler = PlanCompiler::new();
3939 let plan = compiler.compile_code(code).expect("Should compile");
3940
3941 let mut mock_http = MockHttpExecutor::new();
3942 mock_http.add_response("/users/1", serde_json::json!({ "id": 1, "active": true }));
3943
3944 let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
3945 let result = executor.execute(&plan).await.expect("Should execute");
3946
3947 assert_eq!(result.value["status"], "active");
3948 }
3949
3950 #[tokio::test]
3951 async fn test_execute_conditional_false_branch() {
3952 let code = r#"
3953 const user = await api.get('/users/1');
3954 if (user.active) {
3955 return { status: "active" };
3956 } else {
3957 return { status: "inactive", user: user };
3958 }
3959 "#;
3960
3961 let mut compiler = PlanCompiler::new();
3962 let plan = compiler.compile_code(code).expect("Should compile");
3963
3964 let mut mock_http = MockHttpExecutor::new();
3965 mock_http.add_response("/users/1", serde_json::json!({ "id": 1, "active": false }));
3966
3967 let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
3968 let result = executor.execute(&plan).await.expect("Should execute");
3969
3970 assert_eq!(result.value["status"], "inactive");
3971 }
3972
3973 #[tokio::test]
3974 async fn test_compile_and_execute_reduce() {
3975 let code = r#"
3976 const products = await api.get('/products');
3977 const totalPrice = products.reduce((sum, p) => sum + p.price, 0);
3978 return { total: totalPrice };
3979 "#;
3980
3981 let mut compiler = PlanCompiler::new();
3982 let plan = compiler.compile_code(code).expect("Should compile reduce");
3983
3984 let mut mock_http = MockHttpExecutor::new();
3985 mock_http.add_response(
3986 "/products",
3987 serde_json::json!([
3988 { "id": 1, "name": "Widget", "price": 10 },
3989 { "id": 2, "name": "Gadget", "price": 25 },
3990 { "id": 3, "name": "Gizmo", "price": 15 }
3991 ]),
3992 );
3993
3994 let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
3995 let result = executor.execute(&plan).await.expect("Should execute");
3996
3997 assert_eq!(result.value["total"].as_f64().unwrap(), 50.0);
3999 }
4000
4001 #[tokio::test]
4002 async fn test_compile_and_execute_to_fixed() {
4003 let code = r#"
4004 const products = await api.get('/products');
4005 const totalPrice = products.reduce((sum, p) => sum + p.price, 0);
4006 const averagePrice = products.length > 0 ? totalPrice / products.length : 0;
4007 return { averagePrice: averagePrice.toFixed(2) };
4008 "#;
4009
4010 let mut compiler = PlanCompiler::new();
4011 let plan = compiler.compile_code(code).expect("Should compile toFixed");
4012
4013 let mut mock_http = MockHttpExecutor::new();
4014 mock_http.add_response(
4015 "/products",
4016 serde_json::json!([
4017 { "id": 1, "name": "Widget", "price": 10 },
4018 { "id": 2, "name": "Gadget", "price": 25 },
4019 { "id": 3, "name": "Gizmo", "price": 15 }
4020 ]),
4021 );
4022
4023 let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4024 let result = executor.execute(&plan).await.expect("Should execute");
4025
4026 assert_eq!(result.value["averagePrice"], "16.67");
4028 }
4029
4030 #[test]
4035 fn test_filter_blocked_fields_simple() {
4036 let value = serde_json::json!({
4037 "id": 1,
4038 "name": "Alice",
4039 "password": "secret123",
4040 "email": "alice@example.com"
4041 });
4042
4043 let blocked: HashSet<String> = ["password"].iter().map(|s| s.to_string()).collect();
4044 let filtered = filter_blocked_fields(value, &blocked);
4045
4046 assert_eq!(filtered["id"], 1);
4047 assert_eq!(filtered["name"], "Alice");
4048 assert_eq!(filtered["email"], "alice@example.com");
4049 assert!(filtered.get("password").is_none());
4050 }
4051
4052 #[test]
4053 fn test_filter_blocked_fields_multiple() {
4054 let value = serde_json::json!({
4055 "id": 1,
4056 "name": "Alice",
4057 "password": "secret123",
4058 "ssn": "123-45-6789",
4059 "apiKey": "key-abc123"
4060 });
4061
4062 let blocked: HashSet<String> = ["password", "ssn", "apiKey"]
4063 .iter()
4064 .map(|s| s.to_string())
4065 .collect();
4066 let filtered = filter_blocked_fields(value, &blocked);
4067
4068 assert_eq!(filtered["id"], 1);
4069 assert_eq!(filtered["name"], "Alice");
4070 assert!(filtered.get("password").is_none());
4071 assert!(filtered.get("ssn").is_none());
4072 assert!(filtered.get("apiKey").is_none());
4073 }
4074
4075 #[test]
4076 fn test_filter_blocked_fields_nested() {
4077 let value = serde_json::json!({
4078 "user": {
4079 "id": 1,
4080 "profile": {
4081 "name": "Alice",
4082 "password": "secret123"
4083 }
4084 }
4085 });
4086
4087 let blocked: HashSet<String> = ["password"].iter().map(|s| s.to_string()).collect();
4088 let filtered = filter_blocked_fields(value, &blocked);
4089
4090 assert_eq!(filtered["user"]["id"], 1);
4091 assert_eq!(filtered["user"]["profile"]["name"], "Alice");
4092 assert!(filtered["user"]["profile"].get("password").is_none());
4093 }
4094
4095 #[test]
4096 fn test_filter_blocked_fields_in_array() {
4097 let value = serde_json::json!([
4098 { "id": 1, "name": "Alice", "password": "secret1" },
4099 { "id": 2, "name": "Bob", "password": "secret2" }
4100 ]);
4101
4102 let blocked: HashSet<String> = ["password"].iter().map(|s| s.to_string()).collect();
4103 let filtered = filter_blocked_fields(value, &blocked);
4104
4105 let arr = filtered.as_array().unwrap();
4106 assert_eq!(arr.len(), 2);
4107 assert_eq!(arr[0]["id"], 1);
4108 assert_eq!(arr[0]["name"], "Alice");
4109 assert!(arr[0].get("password").is_none());
4110 assert_eq!(arr[1]["id"], 2);
4111 assert_eq!(arr[1]["name"], "Bob");
4112 assert!(arr[1].get("password").is_none());
4113 }
4114
4115 #[test]
4116 fn test_filter_blocked_fields_empty_blocklist() {
4117 let value = serde_json::json!({
4118 "id": 1,
4119 "password": "secret123"
4120 });
4121
4122 let blocked: HashSet<String> = HashSet::new();
4123 let filtered = filter_blocked_fields(value.clone(), &blocked);
4124
4125 assert_eq!(filtered, value);
4127 }
4128
4129 #[test]
4130 fn test_filter_blocked_fields_primitive_values() {
4131 let blocked: HashSet<String> = ["password"].iter().map(|s| s.to_string()).collect();
4133
4134 assert_eq!(
4135 filter_blocked_fields(JsonValue::String("test".into()), &blocked),
4136 JsonValue::String("test".into())
4137 );
4138 assert_eq!(
4139 filter_blocked_fields(JsonValue::Number(42.into()), &blocked),
4140 JsonValue::Number(42.into())
4141 );
4142 assert_eq!(
4143 filter_blocked_fields(JsonValue::Bool(true), &blocked),
4144 JsonValue::Bool(true)
4145 );
4146 assert_eq!(
4147 filter_blocked_fields(JsonValue::Null, &blocked),
4148 JsonValue::Null
4149 );
4150 }
4151
4152 #[tokio::test]
4153 async fn test_execute_with_blocked_fields() {
4154 let code = r#"
4155 const user = await api.get('/users/1');
4156 return user;
4157 "#;
4158
4159 let mut compiler = PlanCompiler::new();
4160 let plan = compiler.compile_code(code).expect("Should compile");
4161
4162 let mut mock_http = MockHttpExecutor::new();
4163 mock_http.add_response(
4164 "/users/1",
4165 serde_json::json!({
4166 "id": 1,
4167 "name": "Alice",
4168 "password": "secret123",
4169 "apiKey": "key-abc"
4170 }),
4171 );
4172
4173 let config = ExecutionConfig::default().with_blocked_fields(["password", "apiKey"]);
4175
4176 let mut executor = PlanExecutor::new(mock_http, config);
4177 let result = executor.execute(&plan).await.expect("Should execute");
4178
4179 assert_eq!(result.value["id"], 1);
4181 assert_eq!(result.value["name"], "Alice");
4182 assert!(result.value.get("password").is_none());
4183 assert!(result.value.get("apiKey").is_none());
4184 }
4185
4186 #[tokio::test]
4187 async fn test_execute_nested_blocked_fields() {
4188 let code = r#"
4189 const data = await api.get('/data');
4190 return data;
4191 "#;
4192
4193 let mut compiler = PlanCompiler::new();
4194 let plan = compiler.compile_code(code).expect("Should compile");
4195
4196 let mut mock_http = MockHttpExecutor::new();
4197 mock_http.add_response(
4198 "/data",
4199 serde_json::json!({
4200 "users": [
4201 { "id": 1, "name": "Alice", "secret": "hidden1" },
4202 { "id": 2, "name": "Bob", "secret": "hidden2" }
4203 ],
4204 "config": {
4205 "setting": "value",
4206 "secret": "also-hidden"
4207 }
4208 }),
4209 );
4210
4211 let config = ExecutionConfig::default().with_blocked_fields(["secret"]);
4213
4214 let mut executor = PlanExecutor::new(mock_http, config);
4215 let result = executor.execute(&plan).await.expect("Should execute");
4216
4217 let users = result.value["users"].as_array().unwrap();
4219 assert_eq!(users[0]["name"], "Alice");
4220 assert!(users[0].get("secret").is_none());
4221 assert_eq!(users[1]["name"], "Bob");
4222 assert!(users[1].get("secret").is_none());
4223
4224 assert_eq!(result.value["config"]["setting"], "value");
4225 assert!(result.value["config"].get("secret").is_none());
4226 }
4227
4228 #[test]
4233 fn test_find_blocked_fields_in_output_simple() {
4234 let value = serde_json::json!({
4235 "id": 1,
4236 "name": "Alice",
4237 "ssn": "123-45-6789"
4238 });
4239
4240 let blocked: HashSet<String> = ["ssn"].iter().map(|s| s.to_string()).collect();
4241 let violations = find_blocked_fields_in_output(&value, &blocked);
4242
4243 assert_eq!(violations.len(), 1);
4244 assert_eq!(violations[0], "ssn");
4245 }
4246
4247 #[test]
4248 fn test_find_blocked_fields_in_output_nested() {
4249 let value = serde_json::json!({
4250 "user": {
4251 "profile": {
4252 "name": "Alice",
4253 "salary": 100000
4254 }
4255 }
4256 });
4257
4258 let blocked: HashSet<String> = ["salary"].iter().map(|s| s.to_string()).collect();
4259 let violations = find_blocked_fields_in_output(&value, &blocked);
4260
4261 assert_eq!(violations.len(), 1);
4262 assert!(violations[0].contains("salary"));
4263 }
4264
4265 #[test]
4266 fn test_find_blocked_fields_in_output_array() {
4267 let value = serde_json::json!([
4268 { "id": 1, "ssn": "111" },
4269 { "id": 2, "ssn": "222" }
4270 ]);
4271
4272 let blocked: HashSet<String> = ["ssn"].iter().map(|s| s.to_string()).collect();
4273 let violations = find_blocked_fields_in_output(&value, &blocked);
4274
4275 assert_eq!(violations.len(), 2);
4277 }
4278
4279 #[test]
4280 fn test_find_blocked_fields_in_output_empty_blocklist() {
4281 let value = serde_json::json!({
4282 "id": 1,
4283 "ssn": "123-45-6789"
4284 });
4285
4286 let blocked: HashSet<String> = HashSet::new();
4287 let violations = find_blocked_fields_in_output(&value, &blocked);
4288
4289 assert!(violations.is_empty());
4290 }
4291
4292 #[test]
4293 fn test_find_blocked_fields_in_output_no_violations() {
4294 let value = serde_json::json!({
4295 "id": 1,
4296 "name": "Alice"
4297 });
4298
4299 let blocked: HashSet<String> = ["ssn", "salary"].iter().map(|s| s.to_string()).collect();
4300 let violations = find_blocked_fields_in_output(&value, &blocked);
4301
4302 assert!(violations.is_empty());
4303 }
4304
4305 #[tokio::test]
4306 async fn test_execute_output_blocked_fields_rejected() {
4307 let code = r#"
4308 const user = await api.get('/users/1');
4309 return { name: user.name, ssn: user.ssn };
4310 "#;
4311
4312 let mut compiler = PlanCompiler::new();
4313 let plan = compiler.compile_code(code).expect("Should compile");
4314
4315 let mut mock_http = MockHttpExecutor::new();
4316 mock_http.add_response(
4317 "/users/1",
4318 serde_json::json!({
4319 "id": 1,
4320 "name": "Alice",
4321 "ssn": "123-45-6789"
4322 }),
4323 );
4324
4325 let config = ExecutionConfig::default().with_output_blocked_fields(["ssn"]);
4328
4329 let mut executor = PlanExecutor::new(mock_http, config);
4330 let result = executor.execute(&plan).await;
4331
4332 assert!(result.is_err());
4334 let err = result.unwrap_err();
4335 assert!(format!("{:?}", err).contains("ssn"));
4336 }
4337
4338 #[tokio::test]
4339 async fn test_execute_output_blocked_fields_internal_use_allowed() {
4340 let code = r#"
4342 const user = await api.get('/users/1');
4343 return { id: user.id, name: user.name };
4344 "#;
4345
4346 let mut compiler = PlanCompiler::new();
4347 let plan = compiler.compile_code(code).expect("Should compile");
4348
4349 let mut mock_http = MockHttpExecutor::new();
4350 mock_http.add_response(
4351 "/users/1",
4352 serde_json::json!({
4353 "id": 1,
4354 "name": "Alice",
4355 "ssn": "123-45-6789"
4356 }),
4357 );
4358
4359 let config = ExecutionConfig::default().with_output_blocked_fields(["ssn"]);
4362
4363 let mut executor = PlanExecutor::new(mock_http, config);
4364 let result = executor.execute(&plan).await.expect("Should succeed");
4365
4366 assert_eq!(result.value["id"], 1);
4368 assert_eq!(result.value["name"], "Alice");
4369 assert!(result.value.get("ssn").is_none());
4370 }
4371
4372 #[tokio::test]
4373 async fn test_execute_both_blocklists() {
4374 let code = r#"
4376 const user = await api.get('/users/1');
4377 return { name: user.name, dateOfBirth: user.dateOfBirth };
4378 "#;
4379
4380 let mut compiler = PlanCompiler::new();
4381 let plan = compiler.compile_code(code).expect("Should compile");
4382
4383 let mut mock_http = MockHttpExecutor::new();
4384 mock_http.add_response(
4385 "/users/1",
4386 serde_json::json!({
4387 "id": 1,
4388 "name": "Alice",
4389 "password": "secret123",
4390 "dateOfBirth": "1990-01-01"
4391 }),
4392 );
4393
4394 let config = ExecutionConfig::default()
4397 .with_blocked_fields(["password"])
4398 .with_output_blocked_fields(["dateOfBirth"]);
4399
4400 let mut executor = PlanExecutor::new(mock_http, config);
4401 let result = executor.execute(&plan).await;
4402
4403 assert!(result.is_err());
4405 let err = result.unwrap_err();
4406 assert!(format!("{:?}", err).contains("dateOfBirth"));
4407 }
4408
4409 #[tokio::test]
4414 async fn test_prebound_args_comparison() {
4415 let code = r#"
4417 if (args.k > args.n) {
4418 return { error: 'k must be <= n' };
4419 }
4420 return { ok: true };
4421 "#;
4422
4423 let mut compiler = PlanCompiler::new();
4424 let plan = compiler.compile_code(code).expect("Should compile");
4425
4426 let mock_http = MockHttpExecutor::new();
4427 let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4428 executor.set_variable("args", serde_json::json!({"n": 3, "k": 5}));
4429
4430 let result = executor.execute(&plan).await.expect("Should execute");
4431 assert_eq!(
4432 result.value["error"], "k must be <= n",
4433 "Expected error for k > n, got: {:?}",
4434 result.value
4435 );
4436 }
4437
4438 #[tokio::test]
4439 async fn test_prebound_args_strict_equality() {
4440 let code = r#"
4442 if (args.k === 0) {
4443 return { result: 1 };
4444 }
4445 return { result: 'not zero' };
4446 "#;
4447
4448 let mut compiler = PlanCompiler::new();
4449 let plan = compiler.compile_code(code).expect("Should compile");
4450
4451 let mock_http = MockHttpExecutor::new();
4452 let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4453 executor.set_variable("args", serde_json::json!({"k": 0}));
4454
4455 let result = executor.execute(&plan).await.expect("Should execute");
4456 assert_eq!(result.value["result"], 1);
4457 }
4458
4459 #[tokio::test]
4460 async fn test_assignment_expression_in_statement() {
4461 let code = r#"
4463 let k = 5;
4464 k = 2;
4465 return { k: k };
4466 "#;
4467
4468 let mut compiler = PlanCompiler::new();
4469 let plan = compiler.compile_code(code).expect("Should compile");
4470
4471 let mock_http = MockHttpExecutor::new();
4472 let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4473
4474 let result = executor.execute(&plan).await.expect("Should execute");
4475 assert_eq!(result.value["k"], 2);
4476 }
4477
4478 #[tokio::test]
4479 async fn test_assignment_swap_variables() {
4480 let code = r#"
4482 let a = 3;
4483 let b = 7;
4484 if (a < b) {
4485 const old_a = a;
4486 a = b;
4487 b = old_a;
4488 }
4489 return { a: a, b: b };
4490 "#;
4491
4492 let mut compiler = PlanCompiler::new();
4493 let plan = compiler.compile_code(code).expect("Should compile");
4494
4495 let mock_http = MockHttpExecutor::new();
4496 let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4497
4498 let result = executor.execute(&plan).await.expect("Should execute");
4499 assert_eq!(result.value["a"], 7);
4500 assert_eq!(result.value["b"], 3);
4501 }
4502
4503 #[cfg(feature = "mcp-code-mode")]
4508 mod mcp_tests {
4509 use super::*;
4510
4511 struct MockCalculatorExecutor;
4513
4514 #[async_trait::async_trait]
4515 impl McpExecutor for MockCalculatorExecutor {
4516 async fn call_tool(
4517 &self,
4518 _server_id: &str,
4519 tool_name: &str,
4520 args: JsonValue,
4521 ) -> Result<JsonValue, ExecutionError> {
4522 match tool_name {
4523 "add" => {
4524 let a = args["a"].as_f64().unwrap_or(0.0);
4525 let b = args["b"].as_f64().unwrap_or(0.0);
4526 Ok(serde_json::json!({"result": a + b}))
4527 },
4528 "subtract" => {
4529 let a = args["a"].as_f64().unwrap_or(0.0);
4530 let b = args["b"].as_f64().unwrap_or(0.0);
4531 Ok(serde_json::json!({"result": a - b}))
4532 },
4533 "multiply" => {
4534 let a = args["a"].as_f64().unwrap_or(0.0);
4535 let b = args["b"].as_f64().unwrap_or(0.0);
4536 Ok(serde_json::json!({"result": a * b}))
4537 },
4538 "divide" => {
4539 let a = args["a"].as_f64().unwrap_or(0.0);
4540 let b = args["b"].as_f64().unwrap_or(1.0);
4541 Ok(serde_json::json!({"result": a / b}))
4542 },
4543 "power" => {
4544 let base = args["base"].as_f64().unwrap_or(0.0);
4545 let exponent = args["exponent"].as_f64().unwrap_or(1.0);
4546 Ok(serde_json::json!({"result": base.powf(exponent)}))
4547 },
4548 "sqrt" => {
4549 let n = args["n"].as_f64().unwrap_or(0.0);
4550 Ok(serde_json::json!({"result": n.sqrt()}))
4551 },
4552 _ => Err(ExecutionError::RuntimeError {
4553 message: format!("Unknown tool: {}", tool_name),
4554 }),
4555 }
4556 }
4557 }
4558
4559 #[tokio::test]
4560 async fn test_mcp_call_simple() {
4561 let code = r#"
4562 const result = await mcp.call('calculator', 'add', { a: 5, b: 3 });
4563 return result;
4564 "#;
4565
4566 let mut compiler = PlanCompiler::new();
4567 let plan = compiler.compile_code(code).expect("Should compile");
4568
4569 let mock_http = MockHttpExecutor::new();
4570 let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4571 executor.set_mcp_executor(MockCalculatorExecutor);
4572
4573 let result = executor.execute(&plan).await.expect("Should execute");
4574 assert_eq!(result.value["result"], 8.0);
4575 }
4576
4577 #[tokio::test]
4578 async fn test_mcp_call_with_args() {
4579 let code = r#"
4581 const result = await mcp.call('calculator', 'add', { a: args.x, b: args.y });
4582 return { sum: result.result };
4583 "#;
4584
4585 let mut compiler = PlanCompiler::new();
4586 let plan = compiler.compile_code(code).expect("Should compile");
4587
4588 let mock_http = MockHttpExecutor::new();
4589 let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4590 executor.set_mcp_executor(MockCalculatorExecutor);
4591 executor.set_variable("args", serde_json::json!({"x": 10, "y": 20}));
4592
4593 let result = executor.execute(&plan).await.expect("Should execute");
4594 assert_eq!(result.value["sum"], 30.0);
4595 }
4596
4597 #[tokio::test]
4598 async fn test_mcp_assignment_in_loop() {
4599 let code = r#"
4601 let result = { result: 1 };
4602 for (const i of [2, 3, 4, 5]) {
4603 const mul = await mcp.call('calculator', 'multiply', { a: result.result, b: i });
4604 result = mul;
4605 }
4606 return { factorial: result.result };
4607 "#;
4608
4609 let mut compiler = PlanCompiler::new();
4610 let plan = compiler.compile_code(code).expect("Should compile");
4611
4612 let mock_http = MockHttpExecutor::new();
4613 let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4614 executor.set_mcp_executor(MockCalculatorExecutor);
4615
4616 let result = executor.execute(&plan).await.expect("Should execute");
4617 assert_eq!(result.value["factorial"], 120.0);
4619 }
4620
4621 #[tokio::test]
4622 async fn test_combinations_c_5_3() {
4623 let code = r#"
4625if (args.k > args.n) {
4626 return { error: 'k must be <= n', n: args.n, k: args.k };
4627}
4628if (args.k === 0 || args.k === args.n) {
4629 return { n: args.n, k: args.k, result: 1 };
4630}
4631let k = args.k;
4632const complement = await mcp.call('calculator', 'subtract', { a: args.n, b: args.k });
4633let nmk = complement.result;
4634if (nmk < k) {
4635 const old_k = k;
4636 k = nmk;
4637 nmk = old_k;
4638}
4639let result = { result: 1 };
4640for (const i of [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]) {
4641 if (i > k) { break; }
4642 const nki = await mcp.call('calculator', 'add', { a: nmk, b: i });
4643 const num = await mcp.call('calculator', 'multiply', { a: result.result, b: nki.result });
4644 result = await mcp.call('calculator', 'divide', { a: num.result, b: i });
4645}
4646return { n: args.n, k: args.k, result: result.result };
4647 "#;
4648
4649 let mut compiler = PlanCompiler::new();
4650 let plan = compiler.compile_code(code).expect("Should compile");
4651
4652 let mock_http = MockHttpExecutor::new();
4653 let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4654 executor.set_mcp_executor(MockCalculatorExecutor);
4655 executor.set_variable("args", serde_json::json!({"n": 5, "k": 3}));
4656
4657 let result = executor.execute(&plan).await.expect("Should execute");
4658 assert_eq!(
4659 result.value["result"], 10.0,
4660 "C(5,3) should be 10, got: {:?}",
4661 result.value
4662 );
4663 }
4664
4665 #[tokio::test]
4666 async fn test_combinations_k_greater_than_n() {
4667 let code = r#"
4669if (args.k > args.n) {
4670 return { error: 'k must be <= n', n: args.n, k: args.k };
4671}
4672return { result: 'should not reach here' };
4673 "#;
4674
4675 let mut compiler = PlanCompiler::new();
4676 let plan = compiler.compile_code(code).expect("Should compile");
4677
4678 let mock_http = MockHttpExecutor::new();
4679 let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4680 executor.set_mcp_executor(MockCalculatorExecutor);
4681 executor.set_variable("args", serde_json::json!({"n": 3, "k": 5}));
4682
4683 let result = executor.execute(&plan).await.expect("Should execute");
4684 assert_eq!(
4685 result.value["error"], "k must be <= n",
4686 "C(3,5) should return error, got: {:?}",
4687 result.value
4688 );
4689 }
4690
4691 #[tokio::test]
4692 async fn test_combinations_c_5_2() {
4693 let code = r#"
4695if (args.k > args.n) {
4696 return { error: 'k must be <= n', n: args.n, k: args.k };
4697}
4698if (args.k === 0 || args.k === args.n) {
4699 return { n: args.n, k: args.k, result: 1 };
4700}
4701let k = args.k;
4702const complement = await mcp.call('calculator', 'subtract', { a: args.n, b: args.k });
4703let nmk = complement.result;
4704if (nmk < k) {
4705 const old_k = k;
4706 k = nmk;
4707 nmk = old_k;
4708}
4709let result = { result: 1 };
4710for (const i of [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]) {
4711 if (i > k) { break; }
4712 const nki = await mcp.call('calculator', 'add', { a: nmk, b: i });
4713 const num = await mcp.call('calculator', 'multiply', { a: result.result, b: nki.result });
4714 result = await mcp.call('calculator', 'divide', { a: num.result, b: i });
4715}
4716return { n: args.n, k: args.k, result: result.result };
4717 "#;
4718
4719 let mut compiler = PlanCompiler::new();
4720 let plan = compiler.compile_code(code).expect("Should compile");
4721
4722 let mock_http = MockHttpExecutor::new();
4723 let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4724 executor.set_mcp_executor(MockCalculatorExecutor);
4725 executor.set_variable("args", serde_json::json!({"n": 5, "k": 2}));
4726
4727 let result = executor.execute(&plan).await.expect("Should execute");
4728 assert_eq!(
4729 result.value["result"], 10.0,
4730 "C(5,2) should be 10, got: {:?}",
4731 result.value
4732 );
4733 }
4734
4735 #[tokio::test]
4736 async fn test_combinations_edge_cases() {
4737 let code = r#"
4738if (args.k === 0 || args.k === args.n) {
4739 return { result: 1 };
4740}
4741return { result: 'not edge case' };
4742 "#;
4743
4744 let mut compiler = PlanCompiler::new();
4745 let plan = compiler.compile_code(code).expect("Should compile");
4746
4747 let mock_http = MockHttpExecutor::new();
4749 let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4750 executor.set_variable("args", serde_json::json!({"n": 5, "k": 0}));
4751 let result = executor.execute(&plan).await.expect("Should execute");
4752 assert_eq!(result.value["result"], 1, "C(5,0) should be 1");
4753
4754 let mock_http = MockHttpExecutor::new();
4756 let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4757 executor.set_variable("args", serde_json::json!({"n": 5, "k": 5}));
4758 let result = executor.execute(&plan).await.expect("Should execute");
4759 assert_eq!(result.value["result"], 1, "C(5,5) should be 1");
4760 }
4761
4762 #[tokio::test]
4763 async fn test_solve_quadratic() {
4764 let code = r#"
4766const b_sq = await mcp.call('calculator', 'power', { base: args.b, exponent: 2 });
4767const four_a = await mcp.call('calculator', 'multiply', { a: 4, b: args.a });
4768const four_ac = await mcp.call('calculator', 'multiply', { a: four_a.result, b: args.c });
4769const discriminant = await mcp.call('calculator', 'subtract', { a: b_sq.result, b: four_ac.result });
4770const root_type = discriminant.result > 0 ? 'two_real'
4771 : discriminant.result === 0 ? 'one_real' : 'complex';
4772if (discriminant.result < 0) {
4773 return { discriminant: discriminant.result, root_type: root_type, roots: [] };
4774}
4775const sqrt_disc = await mcp.call('calculator', 'sqrt', { n: discriminant.result });
4776const neg_b = await mcp.call('calculator', 'multiply', { a: -1, b: args.b });
4777const two_a = await mcp.call('calculator', 'multiply', { a: 2, b: args.a });
4778const x1_num = await mcp.call('calculator', 'add', { a: neg_b.result, b: sqrt_disc.result });
4779const x2_num = await mcp.call('calculator', 'subtract', { a: neg_b.result, b: sqrt_disc.result });
4780const x1 = await mcp.call('calculator', 'divide', { a: x1_num.result, b: two_a.result });
4781const x2 = await mcp.call('calculator', 'divide', { a: x2_num.result, b: two_a.result });
4782return { discriminant: discriminant.result, root_type: root_type, roots: [x1.result, x2.result] };
4783 "#;
4784
4785 let mut compiler = PlanCompiler::new();
4786 let plan = compiler.compile_code(code).expect("Should compile");
4787
4788 let mock_http = MockHttpExecutor::new();
4789 let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4790 executor.set_mcp_executor(MockCalculatorExecutor);
4791 executor.set_variable("args", serde_json::json!({"a": 1, "b": -3, "c": 2}));
4792
4793 let result = executor.execute(&plan).await.expect("Should execute");
4794 assert_eq!(result.value["root_type"], "two_real");
4795 assert_eq!(result.value["discriminant"], 1.0);
4796 let roots = result.value["roots"]
4797 .as_array()
4798 .expect("roots should be array");
4799 assert_eq!(roots.len(), 2);
4800 assert_eq!(roots[0], 2.0);
4801 assert_eq!(roots[1], 1.0);
4802 }
4803 }
4804
4805 #[tokio::test]
4810 async fn test_string_includes() {
4811 let code = r#"
4812 const text = "hello world";
4813 return { found: text.includes("world"), miss: text.includes("xyz") };
4814 "#;
4815
4816 let mut compiler = PlanCompiler::new();
4817 let plan = compiler.compile_code(code).expect("Should compile");
4818
4819 let mock_http = MockHttpExecutor::new();
4820 let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4821 let result = executor.execute(&plan).await.expect("Should execute");
4822
4823 assert_eq!(result.value["found"], true);
4824 assert_eq!(result.value["miss"], false);
4825 }
4826
4827 #[tokio::test]
4828 async fn test_string_index_of() {
4829 let code = r#"
4830 const text = "abcdef";
4831 return { idx: text.indexOf("cd"), miss: text.indexOf("xyz") };
4832 "#;
4833
4834 let mut compiler = PlanCompiler::new();
4835 let plan = compiler.compile_code(code).expect("Should compile");
4836
4837 let mock_http = MockHttpExecutor::new();
4838 let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4839 let result = executor.execute(&plan).await.expect("Should execute");
4840
4841 assert_eq!(result.value["idx"], 2);
4842 assert_eq!(result.value["miss"], -1);
4843 }
4844
4845 #[tokio::test]
4846 async fn test_string_length() {
4847 let code = r#"
4848 const text = "hello";
4849 return { len: text.length };
4850 "#;
4851
4852 let mut compiler = PlanCompiler::new();
4853 let plan = compiler.compile_code(code).expect("Should compile");
4854
4855 let mock_http = MockHttpExecutor::new();
4856 let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4857 let result = executor.execute(&plan).await.expect("Should execute");
4858
4859 assert_eq!(result.value["len"], 5);
4860 }
4861
4862 #[tokio::test]
4863 async fn test_string_slice() {
4864 let code = r#"
4865 const text = "hello world";
4866 return { first: text.slice(0, 5), rest: text.slice(6, 11) };
4867 "#;
4868
4869 let mut compiler = PlanCompiler::new();
4870 let plan = compiler.compile_code(code).expect("Should compile");
4871
4872 let mock_http = MockHttpExecutor::new();
4873 let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4874 let result = executor.execute(&plan).await.expect("Should execute");
4875
4876 assert_eq!(result.value["first"], "hello");
4877 assert_eq!(result.value["rest"], "world");
4878 }
4879
4880 #[tokio::test]
4881 async fn test_string_concat() {
4882 let code = r#"
4883 const greeting = "hello";
4884 return { result: greeting.concat(" world") };
4885 "#;
4886
4887 let mut compiler = PlanCompiler::new();
4888 let plan = compiler.compile_code(code).expect("Should compile");
4889
4890 let mock_http = MockHttpExecutor::new();
4891 let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4892 let result = executor.execute(&plan).await.expect("Should execute");
4893
4894 assert_eq!(result.value["result"], "hello world");
4895 }
4896
4897 #[tokio::test]
4898 async fn test_string_includes_in_filter() {
4899 let code = r#"
4901 const items = [
4902 { name: "TIMESTAMP_2024", desc: "A timestamped record" },
4903 { name: "PERSON_1", desc: "A person entity" },
4904 { name: "TIMESTAMP_2025", desc: "Another timestamped record" }
4905 ];
4906 const timestamped = items.filter(item => item.name.includes("TIMESTAMP"));
4907 return { count: timestamped.length, names: timestamped.map(t => t.name) };
4908 "#;
4909
4910 let mut compiler = PlanCompiler::new();
4911 let plan = compiler.compile_code(code).expect("Should compile");
4912
4913 let mock_http = MockHttpExecutor::new();
4914 let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4915 let result = executor.execute(&plan).await.expect("Should execute");
4916
4917 assert_eq!(result.value["count"], 2);
4918 let names = result.value["names"].as_array().unwrap();
4919 assert_eq!(names[0], "TIMESTAMP_2024");
4920 assert_eq!(names[1], "TIMESTAMP_2025");
4921 }
4922
4923 #[tokio::test]
4924 async fn test_array_includes_still_works() {
4925 let code = r#"
4927 const ids = ["alice", "bob", "charlie"];
4928 return { has_bob: ids.includes("bob"), has_dave: ids.includes("dave") };
4929 "#;
4930
4931 let mut compiler = PlanCompiler::new();
4932 let plan = compiler.compile_code(code).expect("Should compile");
4933
4934 let mock_http = MockHttpExecutor::new();
4935 let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4936 let result = executor.execute(&plan).await.expect("Should execute");
4937
4938 assert_eq!(result.value["has_bob"], true);
4939 assert_eq!(result.value["has_dave"], false);
4940 }
4941
4942 #[test]
4947 fn test_compile_parse_float() {
4948 let code = r#"
4949 const x = parseFloat("3.14");
4950 return x;
4951 "#;
4952 let mut compiler = PlanCompiler::new();
4953 let plan = compiler
4954 .compile_code(code)
4955 .expect("parseFloat should compile");
4956 assert_eq!(plan.steps.len(), 2); }
4958
4959 #[test]
4960 fn test_compile_parse_int() {
4961 let code = r#"
4962 const x = parseInt("42");
4963 return x;
4964 "#;
4965 let mut compiler = PlanCompiler::new();
4966 compiler
4967 .compile_code(code)
4968 .expect("parseInt should compile");
4969 }
4970
4971 #[test]
4972 fn test_compile_math_abs() {
4973 let code = r#"
4974 const x = Math.abs(-5);
4975 return x;
4976 "#;
4977 let mut compiler = PlanCompiler::new();
4978 compiler
4979 .compile_code(code)
4980 .expect("Math.abs should compile");
4981 }
4982
4983 #[test]
4984 fn test_compile_math_max() {
4985 let code = r#"
4986 const x = Math.max(1, 2, 3);
4987 return x;
4988 "#;
4989 let mut compiler = PlanCompiler::new();
4990 compiler
4991 .compile_code(code)
4992 .expect("Math.max should compile");
4993 }
4994
4995 #[test]
4996 fn test_compile_object_keys() {
4997 let code = r#"
4998 const obj = { a: 1, b: 2 };
4999 const keys = Object.keys(obj);
5000 return keys;
5001 "#;
5002 let mut compiler = PlanCompiler::new();
5003 compiler
5004 .compile_code(code)
5005 .expect("Object.keys should compile");
5006 }
5007
5008 #[test]
5009 fn test_compile_object_entries() {
5010 let code = r#"
5011 const obj = { x: 10 };
5012 const entries = Object.entries(obj);
5013 return entries;
5014 "#;
5015 let mut compiler = PlanCompiler::new();
5016 compiler
5017 .compile_code(code)
5018 .expect("Object.entries should compile");
5019 }
5020
5021 #[test]
5022 fn test_compile_unary_plus() {
5023 let code = r#"
5024 const x = +"42";
5025 return x;
5026 "#;
5027 let mut compiler = PlanCompiler::new();
5028 compiler.compile_code(code).expect("unary + should compile");
5029 }
5030
5031 #[test]
5032 fn test_compile_sort_with_comparator() {
5033 let code = r#"
5034 const arr = [3, 1, 2];
5035 const sorted = arr.sort((a, b) => a - b);
5036 return sorted;
5037 "#;
5038 let mut compiler = PlanCompiler::new();
5039 compiler
5040 .compile_code(code)
5041 .expect("sort with comparator should compile");
5042 }
5043
5044 #[test]
5045 fn test_compile_sort_without_comparator() {
5046 let code = r#"
5047 const arr = ["b", "a", "c"];
5048 const sorted = arr.sort();
5049 return sorted;
5050 "#;
5051 let mut compiler = PlanCompiler::new();
5052 compiler
5053 .compile_code(code)
5054 .expect("sort without comparator should compile");
5055 }
5056
5057 #[tokio::test]
5062 async fn test_execute_parse_float() {
5063 let code = r#"
5064 const x = parseFloat("3.14");
5065 return x;
5066 "#;
5067 let mut compiler = PlanCompiler::new();
5068 let plan = compiler.compile_code(code).unwrap();
5069 let mock_http = MockHttpExecutor::new();
5070 let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
5071 let result = executor.execute(&plan).await.unwrap();
5072 #[allow(clippy::approx_constant)]
5075 let expected = serde_json::json!(3.14);
5076 assert_eq!(result.value, expected);
5077 }
5078
5079 #[tokio::test]
5080 async fn test_execute_math_abs_and_sort() {
5081 let code = r#"
5082 const items = [
5083 { name: "a", val: -5 },
5084 { name: "b", val: 3 },
5085 { name: "c", val: -1 }
5086 ];
5087 const sorted = items.sort((a, b) => Math.abs(b.val) - Math.abs(a.val));
5088 return sorted.map(x => x.name);
5089 "#;
5090 let mut compiler = PlanCompiler::new();
5091 let plan = compiler.compile_code(code).unwrap();
5092 let mock_http = MockHttpExecutor::new();
5093 let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
5094 let result = executor.execute(&plan).await.unwrap();
5095 assert_eq!(result.value, serde_json::json!(["a", "b", "c"]));
5096 }
5097
5098 #[tokio::test]
5099 async fn test_execute_object_keys() {
5100 let code = r#"
5101 const obj = { x: 1, y: 2, z: 3 };
5102 return Object.keys(obj).length;
5103 "#;
5104 let mut compiler = PlanCompiler::new();
5105 let plan = compiler.compile_code(code).unwrap();
5106 let mock_http = MockHttpExecutor::new();
5107 let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
5108 let result = executor.execute(&plan).await.unwrap();
5109 assert_eq!(result.value, serde_json::json!(3));
5110 }
5111
5112 #[tokio::test]
5113 async fn test_execute_unary_plus() {
5114 let code = r#"
5115 const x = +"42";
5116 return x;
5117 "#;
5118 let mut compiler = PlanCompiler::new();
5119 let plan = compiler.compile_code(code).unwrap();
5120 let mock_http = MockHttpExecutor::new();
5121 let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
5122 let result = executor.execute(&plan).await.unwrap();
5123 assert_eq!(result.value, serde_json::json!(42.0));
5124 }
5125
5126 #[tokio::test]
5127 async fn test_execute_number_cast() {
5128 let code = r#"
5129 const x = Number("99.5");
5130 return x;
5131 "#;
5132 let mut compiler = PlanCompiler::new();
5133 let plan = compiler.compile_code(code).unwrap();
5134 let mock_http = MockHttpExecutor::new();
5135 let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
5136 let result = executor.execute(&plan).await.unwrap();
5137 assert_eq!(result.value, serde_json::json!(99.5));
5138 }
5139
5140 #[tokio::test]
5141 async fn test_execute_math_round_floor_ceil() {
5142 let code = r#"
5143 return {
5144 round: Math.round(3.7),
5145 floor: Math.floor(3.7),
5146 ceil: Math.ceil(3.2)
5147 };
5148 "#;
5149 let mut compiler = PlanCompiler::new();
5150 let plan = compiler.compile_code(code).unwrap();
5151 let mock_http = MockHttpExecutor::new();
5152 let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
5153 let result = executor.execute(&plan).await.unwrap();
5154 assert_eq!(result.value["round"], serde_json::json!(4.0));
5155 assert_eq!(result.value["floor"], serde_json::json!(3.0));
5156 assert_eq!(result.value["ceil"], serde_json::json!(4.0));
5157 }
5158
5159 #[test]
5164 fn test_compile_object_spread_basic() {
5165 let code = r#"
5166 const base = { id: 1, name: "Alice" };
5167 const extended = { ...base, age: 30 };
5168 return extended;
5169 "#;
5170 let mut compiler = PlanCompiler::new();
5171 let plan = compiler
5172 .compile_code(code)
5173 .expect("Object spread should compile");
5174 assert!(plan.steps.len() >= 2);
5175 }
5176
5177 #[tokio::test]
5178 async fn test_execute_object_spread_basic() {
5179 let code = r#"
5180 const base = { id: 1, name: "Alice" };
5181 const extended = { ...base, age: 30 };
5182 return extended;
5183 "#;
5184 let mut compiler = PlanCompiler::new();
5185 let plan = compiler.compile_code(code).unwrap();
5186 let mock_http = MockHttpExecutor::new();
5187 let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
5188 let result = executor.execute(&plan).await.unwrap();
5189 assert_eq!(result.value["id"], serde_json::json!(1));
5190 assert_eq!(result.value["name"], serde_json::json!("Alice"));
5191 assert_eq!(result.value["age"], serde_json::json!(30));
5192 }
5193
5194 #[tokio::test]
5195 async fn test_execute_object_spread_override() {
5196 let code = r#"
5198 const obj = { id: 1, name: "old" };
5199 const updated = { ...obj, name: "new" };
5200 return updated;
5201 "#;
5202 let mut compiler = PlanCompiler::new();
5203 let plan = compiler.compile_code(code).unwrap();
5204 let mock_http = MockHttpExecutor::new();
5205 let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
5206 let result = executor.execute(&plan).await.unwrap();
5207 assert_eq!(result.value["id"], serde_json::json!(1));
5208 assert_eq!(result.value["name"], serde_json::json!("new"));
5209 }
5210
5211 #[tokio::test]
5212 async fn test_execute_object_spread_multiple() {
5213 let code = r#"
5214 const a = { x: 1 };
5215 const b = { y: 2 };
5216 const merged = { ...a, ...b, z: 3 };
5217 return merged;
5218 "#;
5219 let mut compiler = PlanCompiler::new();
5220 let plan = compiler.compile_code(code).unwrap();
5221 let mock_http = MockHttpExecutor::new();
5222 let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
5223 let result = executor.execute(&plan).await.unwrap();
5224 assert_eq!(result.value["x"], serde_json::json!(1));
5225 assert_eq!(result.value["y"], serde_json::json!(2));
5226 assert_eq!(result.value["z"], serde_json::json!(3));
5227 }
5228
5229 #[tokio::test]
5230 async fn test_execute_object_spread_with_api_result() {
5231 let code = r#"
5233 const config = await api.get('/config');
5234 const result = { ...config, extra: "added" };
5235 return result;
5236 "#;
5237 let mut compiler = PlanCompiler::new();
5238 let plan = compiler.compile_code(code).unwrap();
5239 let mut mock_http = MockHttpExecutor::new();
5240 mock_http.add_response(
5241 "/config",
5242 serde_json::json!({ "key": "value", "enabled": true }),
5243 );
5244 let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
5245 let result = executor.execute(&plan).await.unwrap();
5246 assert_eq!(result.value["key"], serde_json::json!("value"));
5247 assert_eq!(result.value["enabled"], serde_json::json!(true));
5248 assert_eq!(result.value["extra"], serde_json::json!("added"));
5249 }
5250
5251 #[tokio::test]
5252 async fn test_execute_object_spread_non_object_noop() {
5253 let code = r#"
5255 const x = 42;
5256 const obj = { ...x, name: "test" };
5257 return obj;
5258 "#;
5259 let mut compiler = PlanCompiler::new();
5260 let plan = compiler.compile_code(code).unwrap();
5261 let mock_http = MockHttpExecutor::new();
5262 let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
5263 let result = executor.execute(&plan).await.unwrap();
5264 assert_eq!(result.value["name"], serde_json::json!("test"));
5265 assert!(result.value.as_object().unwrap().len() == 1);
5267 }
5268
5269 #[tokio::test]
5270 async fn test_execute_object_spread_preserves_order() {
5271 let code = r#"
5274 const obj = { a: 1, b: 2 };
5275 const result = { b: 99, ...obj, a: 100 };
5276 return result;
5277 "#;
5278 let mut compiler = PlanCompiler::new();
5279 let plan = compiler.compile_code(code).unwrap();
5280 let mock_http = MockHttpExecutor::new();
5281 let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
5282 let result = executor.execute(&plan).await.unwrap();
5283 assert_eq!(result.value["a"], serde_json::json!(100));
5285 assert_eq!(result.value["b"], serde_json::json!(2));
5286 }
5287
5288 #[test]
5293 fn test_compile_object_destructuring_simple() {
5294 let code = r#"
5295 const obj = { id: 1, name: "Alice" };
5296 const { id, name } = obj;
5297 return { id, name };
5298 "#;
5299 let mut compiler = PlanCompiler::new();
5300 let plan = compiler
5301 .compile_code(code)
5302 .expect("Object destructuring should compile");
5303 assert!(plan.steps.len() >= 4);
5305 }
5306
5307 #[tokio::test]
5308 async fn test_execute_object_destructuring_simple() {
5309 let code = r#"
5310 const obj = { id: 1, name: "Alice", extra: "ignored" };
5311 const { id, name } = obj;
5312 return { id, name };
5313 "#;
5314 let mut compiler = PlanCompiler::new();
5315 let plan = compiler.compile_code(code).unwrap();
5316 let mock_http = MockHttpExecutor::new();
5317 let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
5318 let result = executor.execute(&plan).await.unwrap();
5319 assert_eq!(result.value["id"], serde_json::json!(1));
5320 assert_eq!(result.value["name"], serde_json::json!("Alice"));
5321 assert!(result.value.get("extra").is_none());
5323 }
5324
5325 #[tokio::test]
5326 async fn test_execute_object_destructuring_renamed() {
5327 let code = r#"
5328 const user = { id: 1, name: "Alice" };
5329 const { id: userId, name: userName } = user;
5330 return { userId, userName };
5331 "#;
5332 let mut compiler = PlanCompiler::new();
5333 let plan = compiler.compile_code(code).unwrap();
5334 let mock_http = MockHttpExecutor::new();
5335 let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
5336 let result = executor.execute(&plan).await.unwrap();
5337 assert_eq!(result.value["userId"], serde_json::json!(1));
5338 assert_eq!(result.value["userName"], serde_json::json!("Alice"));
5339 }
5340
5341 #[tokio::test]
5342 async fn test_execute_object_destructuring_with_api_call() {
5343 let code = r#"
5345 const { data, status } = await api.get('/users');
5346 return { data, status };
5347 "#;
5348 let mut compiler = PlanCompiler::new();
5349 let plan = compiler.compile_code(code).unwrap();
5350 let mut mock_http = MockHttpExecutor::new();
5351 mock_http.add_response(
5352 "/users",
5353 serde_json::json!({ "data": [{"id": 1}], "status": "ok", "meta": "hidden" }),
5354 );
5355 let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
5356 let result = executor.execute(&plan).await.unwrap();
5357 assert_eq!(result.value["data"], serde_json::json!([{"id": 1}]));
5358 assert_eq!(result.value["status"], serde_json::json!("ok"));
5359 }
5360
5361 #[tokio::test]
5362 async fn test_execute_object_destructuring_missing_property() {
5363 let code = r#"
5365 const obj = { id: 1 };
5366 const { id, name } = obj;
5367 return { id, name };
5368 "#;
5369 let mut compiler = PlanCompiler::new();
5370 let plan = compiler.compile_code(code).unwrap();
5371 let mock_http = MockHttpExecutor::new();
5372 let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
5373 let result = executor.execute(&plan).await.unwrap();
5374 assert_eq!(result.value["id"], serde_json::json!(1));
5375 assert_eq!(result.value["name"], serde_json::json!(null));
5376 }
5377
5378 #[tokio::test]
5383 async fn test_execute_array_destructuring_simple() {
5384 let code = r#"
5385 const arr = [10, 20, 30];
5386 const [a, b] = arr;
5387 return { a, b };
5388 "#;
5389 let mut compiler = PlanCompiler::new();
5390 let plan = compiler.compile_code(code).unwrap();
5391 let mock_http = MockHttpExecutor::new();
5392 let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
5393 let result = executor.execute(&plan).await.unwrap();
5394 assert_eq!(result.value["a"], serde_json::json!(10));
5395 assert_eq!(result.value["b"], serde_json::json!(20));
5396 }
5397
5398 #[tokio::test]
5399 async fn test_execute_array_destructuring_with_promise_all() {
5400 let code = r#"
5402 const [users, products] = await Promise.all([
5403 api.get('/users'),
5404 api.get('/products')
5405 ]);
5406 return { users, products };
5407 "#;
5408 let mut compiler = PlanCompiler::new();
5409 let plan = compiler.compile_code(code).unwrap();
5410 let mut mock_http = MockHttpExecutor::new();
5411 mock_http.add_response("/users", serde_json::json!([{"id": 1}]));
5412 mock_http.add_response("/products", serde_json::json!([{"sku": "A"}]));
5413 let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
5414 let result = executor.execute(&plan).await.unwrap();
5415 assert_eq!(result.value["users"], serde_json::json!([{"id": 1}]));
5416 assert_eq!(result.value["products"], serde_json::json!([{"sku": "A"}]));
5417 }
5418
5419 #[test]
5424 fn test_compile_for_of_destructuring() {
5425 let code = r#"
5426 const items = [{ id: 1, name: "A" }, { id: 2, name: "B" }];
5427 const results = [];
5428 for (const { id, name } of items.slice(0, 10)) {
5429 results.push({ id, name });
5430 }
5431 return results;
5432 "#;
5433 let mut compiler = PlanCompiler::new();
5434 compiler
5435 .compile_code(code)
5436 .expect("For-of with destructuring should compile");
5437 }
5438
5439 #[tokio::test]
5440 async fn test_execute_for_of_destructuring() {
5441 let code = r#"
5442 const items = [{ id: 1, name: "A" }, { id: 2, name: "B" }];
5443 const results = [];
5444 for (const { id, name } of items.slice(0, 10)) {
5445 results.push({ label: name, num: id });
5446 }
5447 return results;
5448 "#;
5449 let mut compiler = PlanCompiler::new();
5450 let plan = compiler.compile_code(code).unwrap();
5451 let mock_http = MockHttpExecutor::new();
5452 let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
5453 let result = executor.execute(&plan).await.unwrap();
5454 let arr = result.value.as_array().unwrap();
5455 assert_eq!(arr.len(), 2);
5456 assert_eq!(arr[0]["label"], serde_json::json!("A"));
5457 assert_eq!(arr[0]["num"], serde_json::json!(1));
5458 assert_eq!(arr[1]["label"], serde_json::json!("B"));
5459 assert_eq!(arr[1]["num"], serde_json::json!(2));
5460 }
5461
5462 #[tokio::test]
5463 async fn test_execute_for_of_destructuring_with_api_calls() {
5464 let code = r#"
5466 const users = [{ id: 1, role: "admin" }, { id: 2, role: "user" }];
5467 const results = [];
5468 for (const { id, role } of users.slice(0, 10)) {
5469 const detail = await api.get(`/users/${id}`);
5470 results.push({ role, detail });
5471 }
5472 return results;
5473 "#;
5474 let mut compiler = PlanCompiler::new();
5475 let plan = compiler.compile_code(code).unwrap();
5476 let mut mock_http = MockHttpExecutor::new();
5477 mock_http.add_response("/users/1", serde_json::json!({ "name": "Alice" }));
5478 mock_http.add_response("/users/2", serde_json::json!({ "name": "Bob" }));
5479 let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
5480 let result = executor.execute(&plan).await.unwrap();
5481 let arr = result.value.as_array().unwrap();
5482 assert_eq!(arr.len(), 2);
5483 assert_eq!(arr[0]["role"], serde_json::json!("admin"));
5484 assert_eq!(arr[0]["detail"]["name"], serde_json::json!("Alice"));
5485 assert_eq!(arr[1]["role"], serde_json::json!("user"));
5486 assert_eq!(arr[1]["detail"]["name"], serde_json::json!("Bob"));
5487 }
5488
5489 #[tokio::test]
5494 async fn test_execute_spread_and_destructuring_combined() {
5495 let code = r#"
5497 const { data, token } = await api.get('/auth');
5498 const result = await api.post('/action', { ...data, token });
5499 return result;
5500 "#;
5501 let mut compiler = PlanCompiler::new();
5502 let plan = compiler.compile_code(code).unwrap();
5503 let mut mock_http = MockHttpExecutor::new();
5504 mock_http.add_response(
5505 "/auth",
5506 serde_json::json!({ "data": { "user": "alice" }, "token": "abc123" }),
5507 );
5508 mock_http.add_response("/action", serde_json::json!({ "success": true }));
5509 let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
5510 let result = executor.execute(&plan).await.unwrap();
5511 assert_eq!(result.value["success"], serde_json::json!(true));
5512 }
5513}
5514
5515#[cfg(test)]
5529mod d09_support {
5530 use super::*;
5531 use std::sync::{Arc, Mutex};
5532
5533 #[derive(Debug, Clone)]
5535 pub(super) struct Seen {
5536 pub(super) method: String,
5537 pub(super) path: String,
5538 pub(super) body: Option<JsonValue>,
5539 }
5540
5541 pub(super) struct RecordingHttp {
5544 seen: Arc<Mutex<Vec<Seen>>>,
5545 response: JsonValue,
5546 }
5547
5548 impl RecordingHttp {
5549 pub(super) fn new() -> (Self, Arc<Mutex<Vec<Seen>>>) {
5551 let seen = Arc::new(Mutex::new(Vec::new()));
5552 (
5553 Self {
5554 seen: Arc::clone(&seen),
5555 response: JsonValue::Object(serde_json::Map::new()),
5556 },
5557 seen,
5558 )
5559 }
5560 }
5561
5562 #[async_trait::async_trait]
5563 impl HttpExecutor for RecordingHttp {
5564 async fn execute_request(
5565 &self,
5566 method: &str,
5567 path: ResolvedPath<'_>,
5568 body: Option<JsonValue>,
5569 ) -> Result<JsonValue, ExecutionError> {
5570 self.seen.lock().unwrap().push(Seen {
5571 method: method.to_string(),
5572 path: path.as_str().to_string(),
5573 body,
5574 });
5575 Ok(self.response.clone())
5576 }
5577 }
5578
5579 pub(super) struct FailingHttp;
5581
5582 #[async_trait::async_trait]
5583 impl HttpExecutor for FailingHttp {
5584 async fn execute_request(
5585 &self,
5586 _method: &str,
5587 _path: ResolvedPath<'_>,
5588 _body: Option<JsonValue>,
5589 ) -> Result<JsonValue, ExecutionError> {
5590 Err(ExecutionError::RuntimeError {
5591 message: "simulated transport failure".to_string(),
5592 })
5593 }
5594 }
5595
5596 pub(super) struct RefusingHttp;
5600
5601 pub(super) const REFUSAL_MESSAGE: &str = "outbound request refused by policy: fixed text";
5602
5603 #[async_trait::async_trait]
5604 impl HttpExecutor for RefusingHttp {
5605 async fn execute_request(
5606 &self,
5607 _method: &str,
5608 _path: ResolvedPath<'_>,
5609 _body: Option<JsonValue>,
5610 ) -> Result<JsonValue, ExecutionError> {
5611 Err(ExecutionError::RequestRefused {
5612 message: REFUSAL_MESSAGE.to_string(),
5613 })
5614 }
5615 }
5616
5617 pub(super) fn plan(steps: Vec<PlanStep>) -> ExecutionPlan {
5618 ExecutionPlan {
5619 steps,
5620 metadata: PlanMetadata {
5621 api_call_count: 0,
5622 has_mutations: false,
5623 endpoints: Vec::new(),
5624 methods_used: Vec::new(),
5625 },
5626 }
5627 }
5628
5629 pub(super) fn get_step(parts: Vec<PathPart>, body: Option<JsonValue>) -> PlanStep {
5631 PlanStep::ApiCall {
5632 result_var: "out".to_string(),
5633 method: "GET".to_string(),
5634 path: PathTemplate { parts },
5635 body: body.map(ValueExpr::Literal),
5636 }
5637 }
5638
5639 pub(super) fn literal(path: &str) -> Vec<PathPart> {
5641 vec![PathPart::Literal(path.to_string())]
5642 }
5643}
5644
5645#[cfg(test)]
5652mod layer_one {
5653 use super::d09_support::{get_step, literal, plan, RecordingHttp};
5654 use super::*;
5655
5656 async fn run_with_var(
5657 var: &str,
5658 value: JsonValue,
5659 ) -> (Result<ExecutionResult, ExecutionError>, usize) {
5660 let (http, seen) = RecordingHttp::new();
5661 let step = get_step(
5662 vec![
5663 PathPart::Literal("/search/".to_string()),
5664 PathPart::Variable(var.to_string()),
5665 ],
5666 None,
5667 );
5668 let mut executor = PlanExecutor::new(http, ExecutionConfig::default());
5669 executor.set_variable(var, value);
5670 let result = executor.execute(&plan(vec![step])).await;
5671 let count = seen.lock().unwrap().len();
5672 (result, count)
5673 }
5674
5675 #[tokio::test]
5676 async fn layer_one_refuses_a_query_separator_in_a_variable_part() {
5677 let payload = format!("2026AA?string={}", "z".repeat(60));
5678 let (result, calls) = run_with_var("lookupKey", JsonValue::String(payload.clone())).await;
5679 let err = result.expect_err("a query separator in a ${var} part must be refused");
5680 let rendered = err.to_string();
5681 assert_eq!(
5682 calls, 0,
5683 "no upstream request may be dispatched: {rendered}"
5684 );
5685 assert!(
5686 rendered.contains("lookupKey"),
5687 "a Variable part's refusal names its identifier, which is what proves \
5688 the refusal came from LAYER 1 and not from the composed check: {rendered}"
5689 );
5690 assert!(
5691 !rendered.contains("2026AA") && !rendered.contains('?'),
5692 "the refusal must carry no byte of the value: {rendered}"
5693 );
5694 }
5695
5696 #[tokio::test]
5697 async fn layer_one_refuses_parent_traversal_in_a_variable_part() {
5698 let (result, calls) =
5699 run_with_var("lookupKey", JsonValue::String("../etc".to_string())).await;
5700 let rendered = result
5701 .expect_err("parent traversal in a ${var} part must be refused")
5702 .to_string();
5703 assert_eq!(
5704 calls, 0,
5705 "no upstream request may be dispatched: {rendered}"
5706 );
5707 assert!(rendered.contains("lookupKey"), "{rendered}");
5708 }
5709
5710 #[tokio::test]
5711 async fn layer_one_refuses_an_over_cap_variable_part() {
5712 let over = "a".repeat(crate::PLACEHOLDER_MAX_LENGTH + 1);
5713 let (result, calls) = run_with_var("lookupKey", JsonValue::String(over)).await;
5714 let rendered = result
5715 .expect_err("an over-cap ${var} part must be refused")
5716 .to_string();
5717 assert_eq!(
5718 calls, 0,
5719 "no upstream request may be dispatched: {rendered}"
5720 );
5721 assert!(rendered.contains("lookupKey"), "{rendered}");
5722 }
5723
5724 #[tokio::test]
5725 async fn layer_one_refuses_a_query_separator_in_an_expression_part() {
5726 let (http, seen) = RecordingHttp::new();
5730 let step = get_step(
5731 vec![
5732 PathPart::Literal("/search/".to_string()),
5733 PathPart::Expression(ValueExpr::PropertyAccess {
5734 object: Box::new(ValueExpr::Variable("holder".to_string())),
5735 property: "secretField".to_string(),
5736 }),
5737 ],
5738 None,
5739 );
5740 let mut executor = PlanExecutor::new(http, ExecutionConfig::default());
5741 executor.set_variable(
5742 "holder",
5743 serde_json::json!({ "secretField": "2026AA?string=payload" }),
5744 );
5745 let rendered = executor
5746 .execute(&plan(vec![step]))
5747 .await
5748 .expect_err("a query separator in an ${expr} part must be refused")
5749 .to_string();
5750 assert_eq!(seen.lock().unwrap().len(), 0, "{rendered}");
5751 assert!(
5752 rendered.contains("path expression"),
5753 "an Expression part uses a fixed positional descriptor: {rendered}"
5754 );
5755 for forbidden in ["2026AA", "payload", "secretField", "holder", "?"] {
5756 assert!(
5757 !rendered.contains(forbidden),
5758 "the refusal must carry neither the evaluated value nor a rendering \
5759 of the expression body; found {forbidden:?} in {rendered:?}"
5760 );
5761 }
5762 }
5763
5764 #[tokio::test]
5765 async fn layer_one_accepts_a_literal_only_template_including_slashes() {
5766 let (http, seen) = RecordingHttp::new();
5769 let step = get_step(literal("/Line/Mode/tube/Status"), None);
5770 let mut executor = PlanExecutor::new(http, ExecutionConfig::default());
5771 executor
5772 .execute(&plan(vec![step]))
5773 .await
5774 .expect("a literal-only template with slashes must still resolve");
5775 let seen = seen.lock().unwrap();
5776 assert_eq!(seen.len(), 1);
5777 assert_eq!(seen[0].path, "/Line/Mode/tube/Status");
5778 }
5779}
5780
5781#[cfg(test)]
5783mod layer_two {
5784 use super::d09_support::{get_step, literal, plan, RecordingHttp};
5785 use super::*;
5786
5787 async fn run(
5788 path: &str,
5789 body: JsonValue,
5790 ) -> (
5791 Result<ExecutionResult, ExecutionError>,
5792 Vec<super::d09_support::Seen>,
5793 ) {
5794 let (http, seen) = RecordingHttp::new();
5795 let step = get_step(literal(path), Some(body));
5796 let mut executor = PlanExecutor::new(http, ExecutionConfig::default());
5797 let result = executor.execute(&plan(vec![step])).await;
5798 let seen = seen.lock().unwrap().clone();
5799 (result, seen)
5800 }
5801
5802 #[tokio::test]
5809 async fn a_refused_placeholder_is_request_refused_not_runtime_error() {
5810 let (result, seen) =
5811 run("/search/{v}", serde_json::json!({"v": "2026AA?string=zzz"})).await;
5812 let err = result.expect_err("a query separator in a placeholder value is refused");
5813 assert!(
5814 matches!(err, ExecutionError::RequestRefused { .. }),
5815 "a placeholder-floor refusal must be RequestRefused, got: {err:?}"
5816 );
5817 assert!(seen.is_empty(), "a refused call dispatches nothing");
5818 }
5819
5820 #[tokio::test]
5821 async fn a_non_scalar_path_value_is_request_refused() {
5822 let (result, seen) = run("/x/{v}", serde_json::json!({"v": {"nested": 1}})).await;
5823 let err = result.expect_err("an object cannot be a path value");
5824 assert!(
5825 matches!(err, ExecutionError::RequestRefused { .. }),
5826 "a non-scalar path value is the script's to fix: {err:?}"
5827 );
5828 assert!(seen.is_empty());
5829 }
5830
5831 #[tokio::test]
5836 async fn an_embedder_refusal_survives_the_api_call_wrapper() {
5837 use super::d09_support::{RefusingHttp, REFUSAL_MESSAGE};
5838 let step = get_step(literal("/anything"), None);
5839 let mut executor = PlanExecutor::new(RefusingHttp, ExecutionConfig::default());
5840 let err = executor
5841 .execute(&plan(vec![step]))
5842 .await
5843 .expect_err("the executor refuses");
5844 let ExecutionError::RequestRefused { message } = &err else {
5845 panic!("a refusal must stay a RequestRefused through the wrapper, got: {err:?}");
5846 };
5847 assert!(
5848 message.contains(REFUSAL_MESSAGE),
5849 "the embedder's own message must be carried through: {message}"
5850 );
5851 assert!(
5852 message.contains("GET api call") && message.contains("was refused"),
5853 "the wrapper adds the method and result variable: {message}"
5854 );
5855 assert!(
5856 !message.contains("/anything"),
5857 "the resolved path must NOT be re-attached (Pitfall 7 / SC-7): {message}"
5858 );
5859 }
5860
5861 #[tokio::test]
5865 async fn a_transport_failure_is_still_a_runtime_error() {
5866 use super::d09_support::FailingHttp;
5867 let step = get_step(literal("/anything"), None);
5868 let mut executor = PlanExecutor::new(FailingHttp, ExecutionConfig::default());
5869 let err = executor
5870 .execute(&plan(vec![step]))
5871 .await
5872 .expect_err("the transport fails");
5873 assert!(
5874 matches!(err, ExecutionError::RuntimeError { .. }),
5875 "a real fault must stay a RuntimeError, got: {err:?}"
5876 );
5877 }
5878
5879 #[tokio::test]
5880 async fn layer_two_substitutes_and_removes_the_consumed_key_from_the_body() {
5881 let (result, seen) = run("/users/{v}", serde_json::json!({"v": "7", "q": "keep"})).await;
5882 result.expect("a conforming placeholder must resolve");
5883 assert_eq!(seen.len(), 1);
5884 assert_eq!(seen[0].path, "/users/7");
5885 assert_eq!(
5886 seen[0].body,
5887 Some(serde_json::json!({"q": "keep"})),
5888 "the path-consumed key must not also reach the body"
5889 );
5890 }
5891
5892 #[tokio::test]
5904 async fn layer_two_value_cannot_manufacture_a_placeholder_for_a_later_key() {
5905 let (result, seen) = run(
5906 "/p/{a}/q/{b}",
5907 serde_json::json!({"a": "x{b}y", "b": "zzz"}),
5908 )
5909 .await;
5910 let rendered = result
5911 .expect_err("a value's literal `{b}` must never be expanded as a placeholder")
5912 .to_string();
5913 assert!(
5914 seen.is_empty(),
5915 "no upstream request may be dispatched: {rendered}"
5916 );
5917 assert!(
5924 !rendered.contains("zzz"),
5925 "the refusal must carry no byte of any value: {rendered}"
5926 );
5927 }
5928
5929 #[tokio::test]
5930 async fn layer_two_conforming_call_produces_exactly_one_request() {
5931 let (result, seen) = run("/users/{v}", serde_json::json!({"v": "ada"})).await;
5932 result.expect("a conforming placeholder must resolve");
5933 assert_eq!(seen.len(), 1, "exactly one upstream request");
5934 assert_eq!(seen[0].method, "GET");
5935 assert_eq!(seen[0].path, "/users/ada");
5936 }
5937
5938 #[tokio::test]
5939 async fn layer_two_refuses_a_query_separator_in_a_placeholder_value() {
5940 let payload = format!("2026AA?string={}", "z".repeat(60));
5941 let (result, seen) = run("/search/{v}", serde_json::json!({"v": payload})).await;
5942 let rendered = result
5943 .expect_err("a query separator in a {key} value must be refused")
5944 .to_string();
5945 assert!(
5946 seen.is_empty(),
5947 "no upstream request may be dispatched: {rendered}"
5948 );
5949 assert!(
5950 !rendered.contains("2026AA") && !rendered.contains('?'),
5951 "the refusal must carry no byte of the value: {rendered}"
5952 );
5953 }
5954
5955 #[tokio::test]
5956 async fn layer_two_refuses_parent_traversal_in_a_placeholder_value() {
5957 let (result, seen) = run("/files/{v}", serde_json::json!({"v": "../../etc/passwd"})).await;
5958 let rendered = result
5959 .expect_err("parent traversal in a {key} value must be refused")
5960 .to_string();
5961 assert!(seen.is_empty(), "{rendered}");
5962 assert!(!rendered.contains("passwd"), "{rendered}");
5963 }
5964
5965 #[tokio::test]
5966 async fn layer_two_refuses_an_object_valued_placeholder_naming_the_key_only() {
5967 let (result, seen) = run(
5970 "/users/{userId}",
5971 serde_json::json!({"userId": {"nested": [1, 2]}}),
5972 )
5973 .await;
5974 let rendered = result
5975 .expect_err("a non-scalar {key} value must be refused")
5976 .to_string();
5977 assert!(seen.is_empty(), "{rendered}");
5978 assert!(rendered.contains("userId"), "must name the key: {rendered}");
5979 for forbidden in ['{', '[', '"'] {
5980 assert!(
5981 !rendered.contains(forbidden),
5982 "must not echo JSON: {rendered}"
5983 );
5984 }
5985 }
5986
5987 #[tokio::test]
5988 async fn layer_two_refuses_an_unsubstituted_placeholder_before_dispatch() {
5989 let (result, seen) = run("/users/{missing}", serde_json::json!({"other": "x"})).await;
5992 let rendered = result
5993 .expect_err("an unsubstituted placeholder must be refused")
5994 .to_string();
5995 assert!(
5996 seen.is_empty(),
5997 "literal braces must never reach the wire: {rendered}"
5998 );
5999 }
6000
6001 #[tokio::test]
6002 async fn layer_two_aborts_before_any_substitution_reaches_a_request() {
6003 let (result, seen) = run(
6006 "/a/{good}/b/{bad}",
6007 serde_json::json!({"good": "ok", "bad": "../escape"}),
6008 )
6009 .await;
6010 let rendered = result
6011 .expect_err("a refusal on any placeholder aborts the step")
6012 .to_string();
6013 assert!(seen.is_empty(), "{rendered}");
6014 }
6015}
6016
6017#[cfg(test)]
6023mod composition {
6024 use super::d09_support::{get_step, plan, RecordingHttp};
6025 use super::*;
6026
6027 #[tokio::test]
6028 async fn composition_refuses_two_adjacent_values_over_the_cap_that_each_pass_alone() {
6029 let first = "a".repeat(180);
6030 let second = "b".repeat(200);
6031
6032 let rules = crate::PlaceholderRules::default();
6036 crate::validate_path_placeholder("a", &first, &rules)
6037 .expect("180 code points is under the cap and must pass alone");
6038 crate::validate_path_placeholder("b", &second, &rules)
6039 .expect("200 code points is under the cap and must pass alone");
6040
6041 let (http, seen) = RecordingHttp::new();
6043 let step = get_step(
6044 super::d09_support::literal("/search/{a}{b}"),
6045 Some(serde_json::json!({"a": first, "b": second})),
6046 );
6047 let mut executor = PlanExecutor::new(http, ExecutionConfig::default());
6048 let rendered = executor
6049 .execute(&plan(vec![step]))
6050 .await
6051 .expect_err("380 composed code points exceed the cap and must be refused")
6052 .to_string();
6053 assert!(seen.lock().unwrap().is_empty(), "{rendered}");
6054 assert!(
6055 !rendered.contains("aaaa") && !rendered.contains("bbbb"),
6056 "the composed refusal must carry no byte of either value: {rendered}"
6057 );
6058 }
6059
6060 #[tokio::test]
6061 async fn composition_refuses_traversal_assembled_from_unchecked_literal_parts() {
6062 let (http, seen) = RecordingHttp::new();
6067 let step = get_step(
6068 vec![
6069 PathPart::Literal("/a/.".to_string()),
6070 PathPart::Literal(".".to_string()),
6071 PathPart::Literal("/b".to_string()),
6072 ],
6073 None,
6074 );
6075 let mut executor = PlanExecutor::new(http, ExecutionConfig::default());
6076 let rendered = executor
6077 .execute(&plan(vec![step]))
6078 .await
6079 .expect_err("a composed `..` segment must be refused")
6080 .to_string();
6081 assert!(seen.lock().unwrap().is_empty(), "{rendered}");
6082 }
6083
6084 #[tokio::test]
6085 async fn composition_refuses_two_adjacent_single_dot_placeholders() {
6086 let (http, seen) = RecordingHttp::new();
6090 let step = get_step(
6091 super::d09_support::literal("/a/{x}{y}/b"),
6092 Some(serde_json::json!({"x": ".", "y": "."})),
6093 );
6094 let mut executor = PlanExecutor::new(http, ExecutionConfig::default());
6095 let rendered = executor
6096 .execute(&plan(vec![step]))
6097 .await
6098 .expect_err("`.` + `.` composes to traversal and must be refused")
6099 .to_string();
6100 assert!(seen.lock().unwrap().is_empty(), "{rendered}");
6101 }
6102
6103 #[tokio::test]
6104 async fn composition_refuses_a_mixed_layer_one_and_layer_two_over_cap_segment() {
6105 let first = "a".repeat(180);
6108 let second = "b".repeat(200);
6109 let rules = crate::PlaceholderRules::default();
6110 crate::validate_path_placeholder("lookupKey", &first, &rules)
6111 .expect("the layer-1 contribution passes alone");
6112 crate::validate_path_placeholder("b", &second, &rules)
6113 .expect("the layer-2 contribution passes alone");
6114
6115 let (http, seen) = RecordingHttp::new();
6116 let step = get_step(
6117 vec![
6118 PathPart::Literal("/search/".to_string()),
6119 PathPart::Variable("lookupKey".to_string()),
6120 PathPart::Literal("{b}".to_string()),
6121 ],
6122 Some(serde_json::json!({"b": second})),
6123 );
6124 let mut executor = PlanExecutor::new(http, ExecutionConfig::default());
6125 executor.set_variable("lookupKey", JsonValue::String(first));
6126 let rendered = executor
6127 .execute(&plan(vec![step]))
6128 .await
6129 .expect_err("a mixed-layer composed segment over the cap must be refused")
6130 .to_string();
6131 assert!(seen.lock().unwrap().is_empty(), "{rendered}");
6132 }
6133}
6134
6135#[cfg(test)]
6140mod error_does_not_echo_path {
6141 use super::d09_support::{get_step, literal, plan, FailingHttp};
6142 use super::*;
6143
6144 #[tokio::test]
6145 async fn error_does_not_echo_path_when_the_executor_itself_fails() {
6146 let step = get_step(literal("/secret/inventory/endpoint"), None);
6147 let mut executor = PlanExecutor::new(FailingHttp, ExecutionConfig::default());
6148 let rendered = executor
6149 .execute(&plan(vec![step]))
6150 .await
6151 .expect_err("the failing executor must surface an error")
6152 .to_string();
6153 assert!(
6154 rendered.contains("GET"),
6155 "the wrap must still name the method: {rendered}"
6156 );
6157 assert!(
6158 rendered.contains("simulated transport failure"),
6159 "the wrap must still carry the underlying cause: {rendered}"
6160 );
6161 assert!(
6162 !rendered.contains("/secret/inventory/endpoint"),
6163 "the wrap must NOT re-attach the resolved path: {rendered}"
6164 );
6165 }
6166
6167 #[tokio::test]
6168 async fn error_does_not_echo_path_in_the_parallel_arm() {
6169 let step = PlanStep::ParallelApiCalls {
6170 result_var: "out".to_string(),
6171 calls: vec![(
6172 "t0".to_string(),
6173 "GET".to_string(),
6174 PathTemplate {
6175 parts: literal("/secret/inventory/endpoint"),
6176 },
6177 None,
6178 )],
6179 };
6180 let mut executor = PlanExecutor::new(FailingHttp, ExecutionConfig::default());
6181 let rendered = executor
6182 .execute(&plan(vec![step]))
6183 .await
6184 .expect_err("the failing executor must surface an error")
6185 .to_string();
6186 assert!(rendered.contains("GET"), "{rendered}");
6187 assert!(
6188 !rendered.contains("/secret/inventory/endpoint"),
6189 "the parallel arm's wrap must NOT re-attach the resolved path: {rendered}"
6190 );
6191 }
6192
6193 #[tokio::test]
6194 async fn error_does_not_echo_path_on_a_refused_placeholder() {
6195 let (http, seen) = super::d09_support::RecordingHttp::new();
6196 let step = get_step(
6197 literal("/inventory/{sku}"),
6198 Some(serde_json::json!({"sku": "../../etc/shadow"})),
6199 );
6200 let mut executor = PlanExecutor::new(http, ExecutionConfig::default());
6201 let rendered = executor
6202 .execute(&plan(vec![step]))
6203 .await
6204 .expect_err("a refused placeholder must error")
6205 .to_string();
6206 assert!(seen.lock().unwrap().is_empty(), "{rendered}");
6207 for forbidden in ["shadow", "/inventory/", ".."] {
6208 assert!(
6209 !rendered.contains(forbidden),
6210 "a refusal carries neither the value nor the resolved path; \
6211 found {forbidden:?} in {rendered:?}"
6212 );
6213 }
6214 }
6215}
6216
6217#[cfg(test)]
6225mod query_separator {
6226 use super::d09_support::{get_step, literal, plan, RecordingHttp};
6227 use super::*;
6228
6229 async fn run(
6230 parts: Vec<PathPart>,
6231 body: Option<JsonValue>,
6232 ) -> (
6233 Result<ExecutionResult, ExecutionError>,
6234 Vec<super::d09_support::Seen>,
6235 ) {
6236 let (http, seen) = RecordingHttp::new();
6237 let mut executor = PlanExecutor::new(http, ExecutionConfig::default());
6238 let result = executor.execute(&plan(vec![get_step(parts, body)])).await;
6239 let seen = seen.lock().unwrap().clone();
6240 (result, seen)
6241 }
6242
6243 #[tokio::test]
6246 async fn query_separator_accepts_an_author_written_query_string() {
6247 let (result, seen) = run(literal("/Line/Mode/tube/Status?detail=true"), None).await;
6250 result.expect("an author-written query string must be accepted");
6251 assert_eq!(seen.len(), 1);
6252 assert_eq!(seen[0].path, "/Line/Mode/tube/Status?detail=true");
6253 }
6254
6255 #[tokio::test]
6256 async fn query_separator_accepts_a_literal_query_alongside_a_floored_placeholder() {
6257 let (result, seen) = run(
6260 literal("/content/{version}/CUI?string=headache"),
6261 Some(serde_json::json!({"version": "current"})),
6262 )
6263 .await;
6264 result.expect("an author query plus a conforming placeholder must be accepted");
6265 assert_eq!(seen.len(), 1);
6266 assert_eq!(seen[0].path, "/content/current/CUI?string=headache");
6267 }
6268
6269 #[tokio::test]
6270 async fn query_separator_accepts_a_graph_style_dollar_projection() {
6271 let (result, seen) = run(
6273 literal("/drives/D/items/I/workbook/worksheets/Customers/range(address='A2:D7')?$select=values"),
6274 None,
6275 )
6276 .await;
6277 result.expect("a Graph $select projection in the path must be accepted");
6278 assert_eq!(seen.len(), 1);
6279 assert!(
6280 seen[0].path.ends_with("?$select=values"),
6281 "{:?}",
6282 seen[0].path
6283 );
6284 }
6285
6286 #[tokio::test]
6289 async fn query_separator_still_refuses_traversal_in_the_path_portion() {
6290 let (result, seen) = run(literal("/a/../b?x=1"), None).await;
6293 let rendered = result
6294 .expect_err("traversal must still be refused when a query follows it")
6295 .to_string();
6296 assert!(seen.is_empty(), "{rendered}");
6297 }
6298
6299 #[tokio::test]
6300 async fn query_separator_still_refuses_traversal_in_the_query_portion() {
6301 let (result, seen) = run(literal("/search?next=../../etc/passwd"), None).await;
6302 let rendered = result
6303 .expect_err("traversal inside the query portion must still be refused")
6304 .to_string();
6305 assert!(seen.is_empty(), "{rendered}");
6306 assert!(!rendered.contains("passwd"), "{rendered}");
6307 }
6308
6309 #[tokio::test]
6310 async fn query_separator_still_refuses_a_control_byte_in_the_query_portion() {
6311 let (result, seen) = run(literal("/search?x=a%00b"), None).await;
6313 assert!(
6314 result.is_err(),
6315 "a control byte in the query portion must still be refused"
6316 );
6317 assert!(seen.is_empty());
6318 }
6319
6320 #[tokio::test]
6321 async fn query_separator_still_refuses_an_over_cap_query_portion() {
6322 let long = "z".repeat(crate::PLACEHOLDER_MAX_LENGTH + 1);
6323 let (result, seen) = run(literal(&format!("/search?q={long}")), None).await;
6324 assert!(
6325 result.is_err(),
6326 "an over-cap query portion must still be refused"
6327 );
6328 assert!(seen.is_empty());
6329 }
6330
6331 #[tokio::test]
6332 async fn query_separator_still_refuses_a_second_question_mark() {
6333 let (result, seen) = run(literal("/search?a=1?b=2"), None).await;
6336 assert!(result.is_err(), "a second `?` must still be refused");
6337 assert!(seen.is_empty());
6338 }
6339
6340 #[tokio::test]
6341 async fn query_separator_still_refuses_an_empty_query_portion() {
6342 let (result, seen) = run(literal("/search?"), None).await;
6345 assert!(result.is_err(), "a dangling `?` must still be refused");
6346 assert!(seen.is_empty());
6347 }
6348
6349 #[tokio::test]
6350 async fn query_separator_still_refuses_a_fragment_marker() {
6351 let (result, seen) = run(literal("/search#frag"), None).await;
6353 assert!(result.is_err(), "a fragment marker must still be refused");
6354 assert!(seen.is_empty());
6355 }
6356
6357 #[tokio::test]
6358 async fn query_separator_still_refuses_an_injected_separator_from_a_value() {
6359 let payload = format!("2026AA?string={}", "z".repeat(60));
6364 let (result, seen) = run(
6365 literal("/search/{v}?detail=true"),
6366 Some(serde_json::json!({"v": payload})),
6367 )
6368 .await;
6369 let rendered = result
6370 .expect_err("an injected `?` in a VALUE must still be refused")
6371 .to_string();
6372 assert!(seen.is_empty(), "{rendered}");
6373 assert!(
6374 !rendered.contains("2026AA") && !rendered.contains('?'),
6375 "the refusal must still carry no byte of the value: {rendered}"
6376 );
6377 }
6378
6379 #[tokio::test]
6380 async fn query_separator_still_refuses_an_injected_separator_from_a_layer_one_variable() {
6381 let (http, seen) = RecordingHttp::new();
6383 let mut executor = PlanExecutor::new(http, ExecutionConfig::default());
6384 executor.set_variable(
6385 "lookupKey",
6386 JsonValue::String("2026AA?string=x".to_string()),
6387 );
6388 let rendered = executor
6389 .execute(&plan(vec![get_step(
6390 vec![
6391 PathPart::Literal("/search/".to_string()),
6392 PathPart::Variable("lookupKey".to_string()),
6393 PathPart::Literal("?detail=true".to_string()),
6394 ],
6395 None,
6396 )]))
6397 .await
6398 .expect_err("an injected `?` in a ${var} part must still be refused")
6399 .to_string();
6400 assert!(seen.lock().unwrap().is_empty(), "{rendered}");
6401 assert!(rendered.contains("lookupKey"), "{rendered}");
6402 }
6403}