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