1use crate::{Error, Result};
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9use tokio::sync::RwLock;
10
11#[derive(Debug, Clone, Serialize, Deserialize)]
13#[serde(rename_all = "camelCase")]
14pub struct ChainConfig {
15 pub enabled: bool,
17 pub max_chain_length: usize,
19 pub global_timeout_secs: u64,
21 pub enable_parallel_execution: bool,
23}
24
25#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct ChainContext {
28 #[serde(default)]
30 pub responses: HashMap<String, ChainResponse>,
31 #[serde(default)]
33 pub variables: HashMap<String, serde_json::Value>,
34 #[serde(default)]
36 pub metadata: HashMap<String, String>,
37}
38
39impl ChainContext {
40 pub fn new() -> Self {
42 Self {
43 responses: HashMap::new(),
44 variables: HashMap::new(),
45 metadata: HashMap::new(),
46 }
47 }
48
49 pub fn store_response(&mut self, name: String, response: ChainResponse) {
51 self.responses.insert(name, response);
52 }
53
54 pub fn get_response(&self, name: &str) -> Option<&ChainResponse> {
56 self.responses.get(name)
57 }
58
59 pub fn set_variable(&mut self, name: String, value: serde_json::Value) {
61 self.variables.insert(name, value);
62 }
63
64 pub fn get_variable(&self, name: &str) -> Option<&serde_json::Value> {
66 self.variables.get(name)
67 }
68
69 pub fn set_metadata(&mut self, key: String, value: String) {
71 self.metadata.insert(key, value);
72 }
73
74 pub fn get_metadata(&self, key: &str) -> Option<&String> {
76 self.metadata.get(key)
77 }
78}
79
80impl Default for ChainContext {
81 fn default() -> Self {
82 Self::new()
83 }
84}
85
86#[derive(Debug, Clone, Serialize, Deserialize)]
88#[serde(rename_all = "camelCase")]
89pub struct RequestScripting {
90 pub pre_script: Option<String>,
92 pub post_script: Option<String>,
94 #[serde(default = "default_script_runtime")]
96 pub runtime: String,
97 #[serde(default = "default_script_timeout")]
99 pub timeout_ms: u64,
100}
101
102#[derive(Debug, Clone, Serialize, Deserialize)]
104#[serde(tag = "type", content = "data")]
105pub enum RequestBody {
106 #[serde(rename = "json")]
108 Json(serde_json::Value),
109 #[serde(rename = "binary_file")]
111 BinaryFile {
112 path: String,
114 content_type: Option<String>,
116 },
117}
118
119impl RequestBody {
120 pub fn json(value: serde_json::Value) -> Self {
122 Self::Json(value)
123 }
124
125 pub fn binary_file(path: String, content_type: Option<String>) -> Self {
127 Self::BinaryFile { path, content_type }
128 }
129
130 pub async fn to_bytes(&self) -> crate::Result<Vec<u8>> {
132 match self {
133 RequestBody::Json(value) => Ok(serde_json::to_vec(value)?),
134 RequestBody::BinaryFile { path, .. } => tokio::fs::read(path).await.map_err(|e| {
135 crate::Error::generic(format!("Failed to read binary file '{}': {}", path, e))
136 }),
137 }
138 }
139
140 pub fn content_type(&self) -> Option<&str> {
142 match self {
143 RequestBody::Json(_) => Some("application/json"),
144 RequestBody::BinaryFile { content_type, .. } => content_type.as_deref(),
145 }
146 }
147}
148
149#[derive(Debug, Clone, Serialize, Deserialize)]
151#[serde(rename_all = "camelCase")]
152pub struct ChainRequest {
153 pub id: String,
155 pub method: String,
157 pub url: String,
159 #[serde(default)]
161 pub headers: HashMap<String, String>,
162 pub body: Option<RequestBody>,
164 #[serde(default)]
166 pub depends_on: Vec<String>,
167 pub timeout_secs: Option<u64>,
169 pub expected_status: Option<Vec<u16>>,
171 #[serde(default)]
173 pub scripting: Option<RequestScripting>,
174}
175
176#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
178#[serde(rename_all = "camelCase")]
179pub struct ChainResponse {
180 pub status: u16,
182 pub headers: HashMap<String, String>,
184 pub body: Option<serde_json::Value>,
186 pub duration_ms: u64,
188 pub executed_at: String,
190 pub error: Option<String>,
192}
193
194#[derive(Debug, Clone, Serialize, Deserialize)]
196#[serde(rename_all = "camelCase")]
197pub struct ChainLink {
198 pub request: ChainRequest,
200 #[serde(default)]
202 pub extract: HashMap<String, String>,
203 pub store_as: Option<String>,
205}
206
207#[derive(Debug, Clone, Serialize, Deserialize)]
209#[serde(rename_all = "camelCase")]
210pub struct ChainDefinition {
211 pub id: String,
213 pub name: String,
215 pub description: Option<String>,
217 pub config: ChainConfig,
219 pub links: Vec<ChainLink>,
221 #[serde(default)]
223 pub variables: HashMap<String, serde_json::Value>,
224 #[serde(default)]
226 pub tags: Vec<String>,
227}
228
229#[derive(Debug, Clone, Serialize, Deserialize)]
231pub struct ScriptExecutionContext {
232 pub chain_context: ChainContext,
234 pub request_variables: HashMap<String, serde_json::Value>,
236 pub current_request: Option<ChainRequest>,
238 pub current_response: Option<ChainResponse>,
240}
241
242#[derive(Debug, Clone)]
244pub struct ChainTemplatingContext {
245 pub chain_context: ChainContext,
247 pub request_variables: HashMap<String, serde_json::Value>,
249 pub current_request: Option<ChainRequest>,
251}
252
253impl ChainTemplatingContext {
254 pub fn new(chain_context: ChainContext) -> Self {
256 Self {
257 chain_context,
258 request_variables: HashMap::new(),
259 current_request: None,
260 }
261 }
262
263 pub fn set_request_variable(&mut self, name: String, value: serde_json::Value) {
265 self.request_variables.insert(name, value);
266 }
267
268 pub fn set_current_request(&mut self, request: ChainRequest) {
270 self.current_request = Some(request);
271 }
272
273 pub fn extract_value(&self, path: &str) -> Option<serde_json::Value> {
275 let parts: Vec<&str> = path.split('.').collect();
277
278 if parts.is_empty() {
279 return None;
280 }
281
282 let root = parts[0];
283
284 let root_value = if let Some(resp) = self.chain_context.get_response(root) {
286 resp.body.clone()?
288 } else if let Some(var) = self.chain_context.get_variable(root) {
289 var.clone()
290 } else if let Some(var) = self.request_variables.get(root) {
291 var.clone()
292 } else {
293 return None;
294 };
295
296 self.navigate_json_path(&root_value, &parts[1..])
298 }
299
300 #[allow(clippy::only_used_in_recursion)]
302 fn navigate_json_path(
303 &self,
304 value: &serde_json::Value,
305 path: &[&str],
306 ) -> Option<serde_json::Value> {
307 if path.is_empty() {
308 return Some(value.clone());
309 }
310
311 match value {
312 serde_json::Value::Object(map) => {
313 if let Some(next_value) = map.get(path[0]) {
314 self.navigate_json_path(next_value, &path[1..])
315 } else {
316 None
317 }
318 }
319 serde_json::Value::Array(arr) => {
320 if path[0].starts_with('[') && path[0].ends_with(']') {
322 let index_str = &path[0][1..path[0].len() - 1];
323 if let Ok(index) = index_str.parse::<usize>() {
324 if let Some(item) = arr.get(index) {
325 self.navigate_json_path(item, &path[1..])
326 } else {
327 None
328 }
329 } else {
330 None
331 }
332 } else {
333 None
334 }
335 }
336 _ => None,
337 }
338 }
339}
340
341#[derive(Debug)]
343pub struct ChainStore {
344 chains: RwLock<HashMap<String, ChainDefinition>>,
346 config: ChainConfig,
348}
349
350impl ChainStore {
351 pub fn new(config: ChainConfig) -> Self {
353 Self {
354 chains: RwLock::new(HashMap::new()),
355 config,
356 }
357 }
358
359 pub async fn register_chain(&self, chain: ChainDefinition) -> Result<()> {
361 let mut chains = self.chains.write().await;
362 chains.insert(chain.id.clone(), chain);
363 Ok(())
364 }
365
366 pub async fn get_chain(&self, id: &str) -> Option<ChainDefinition> {
368 let chains = self.chains.read().await;
369 chains.get(id).cloned()
370 }
371
372 pub async fn list_chains(&self) -> Vec<String> {
374 let chains = self.chains.read().await;
375 chains.keys().cloned().collect()
376 }
377
378 pub async fn remove_chain(&self, id: &str) -> Result<()> {
380 let mut chains = self.chains.write().await;
381 chains.remove(id);
382 Ok(())
383 }
384
385 pub fn update_config(&mut self, config: ChainConfig) {
387 self.config = config;
388 }
389}
390
391#[derive(Debug)]
393pub struct ChainExecutionContext {
394 pub definition: ChainDefinition,
396 pub templating: ChainTemplatingContext,
398 pub start_time: std::time::Instant,
400 pub config: ChainConfig,
402}
403
404impl ChainExecutionContext {
405 pub fn new(definition: ChainDefinition) -> Self {
407 let chain_context = ChainContext::new();
408 let templating = ChainTemplatingContext::new(chain_context);
409 let config = definition.config.clone();
410
411 Self {
412 definition,
413 templating,
414 start_time: std::time::Instant::now(),
415 config,
416 }
417 }
418
419 pub fn elapsed_ms(&self) -> u128 {
421 self.start_time.elapsed().as_millis()
422 }
423}
424
425#[derive(Debug)]
427pub struct RequestChainRegistry {
428 store: ChainStore,
430}
431
432impl RequestChainRegistry {
433 pub fn new(config: ChainConfig) -> Self {
435 Self {
436 store: ChainStore::new(config),
437 }
438 }
439
440 pub async fn register_from_yaml(&self, yaml: &str) -> Result<String> {
442 let chain: ChainDefinition = serde_yaml::from_str(yaml)
443 .map_err(|e| Error::generic(format!("Failed to parse chain YAML: {}", e)))?;
444 self.store.register_chain(chain.clone()).await?;
445 Ok(chain.id.clone())
446 }
447
448 pub async fn register_from_json(&self, json: &str) -> Result<String> {
450 let chain: ChainDefinition = serde_json::from_str(json)
451 .map_err(|e| Error::generic(format!("Failed to parse chain JSON: {}", e)))?;
452 self.store.register_chain(chain.clone()).await?;
453 Ok(chain.id.clone())
454 }
455
456 pub async fn get_chain(&self, id: &str) -> Option<ChainDefinition> {
458 self.store.get_chain(id).await
459 }
460
461 pub async fn list_chains(&self) -> Vec<String> {
463 self.store.list_chains().await
464 }
465
466 pub async fn remove_chain(&self, id: &str) -> Result<()> {
468 self.store.remove_chain(id).await
469 }
470
471 pub async fn validate_chain(&self, chain: &ChainDefinition) -> Result<()> {
473 if chain.links.is_empty() {
474 return Err(Error::generic("Chain must have at least one link"));
475 }
476
477 if chain.links.len() > self.store.config.max_chain_length {
478 return Err(Error::generic(format!(
479 "Chain length {} exceeds maximum allowed length {}",
480 chain.links.len(),
481 self.store.config.max_chain_length
482 )));
483 }
484
485 let mut visited = std::collections::HashSet::new();
487 let mut rec_stack = std::collections::HashSet::new();
488
489 for link in &chain.links {
490 self.validate_link_dependencies(link, &mut visited, &mut rec_stack, chain)?;
491 }
492
493 let request_ids: std::collections::HashSet<_> =
495 chain.links.iter().map(|link| &link.request.id).collect();
496
497 if request_ids.len() != chain.links.len() {
498 return Err(Error::generic("Duplicate request IDs found in chain"));
499 }
500
501 Ok(())
502 }
503
504 #[allow(clippy::only_used_in_recursion)]
506 fn validate_link_dependencies(
507 &self,
508 link: &ChainLink,
509 visited: &mut std::collections::HashSet<String>,
510 rec_stack: &mut std::collections::HashSet<String>,
511 chain: &ChainDefinition,
512 ) -> Result<()> {
513 if rec_stack.contains(&link.request.id) {
514 return Err(Error::generic(format!(
515 "Circular dependency detected involving request '{}'",
516 link.request.id
517 )));
518 }
519
520 if visited.contains(&link.request.id) {
521 return Ok(());
522 }
523
524 visited.insert(link.request.id.clone());
525 rec_stack.insert(link.request.id.clone());
526
527 for dep in &link.request.depends_on {
528 if !chain.links.iter().any(|l| &l.request.id == dep) {
530 return Err(Error::generic(format!(
531 "Request '{}' depends on '{}' which does not exist in the chain",
532 link.request.id, dep
533 )));
534 }
535
536 if let Some(dep_link) = chain.links.iter().find(|l| &l.request.id == dep) {
538 self.validate_link_dependencies(dep_link, visited, rec_stack, chain)?;
539 }
540 }
541
542 rec_stack.remove(&link.request.id);
543 Ok(())
544 }
545
546 pub fn store(&self) -> &ChainStore {
548 &self.store
549 }
550}
551
552fn default_script_runtime() -> String {
553 "javascript".to_string()
554}
555
556fn default_script_timeout() -> u64 {
557 5000 }
559
560impl Default for ChainConfig {
561 fn default() -> Self {
562 Self {
563 enabled: false,
564 max_chain_length: 20,
565 global_timeout_secs: 300,
566 enable_parallel_execution: false,
567 }
568 }
569}
570
571#[cfg(test)]
572mod tests {
573 use super::*;
574 use serde_json::json;
575
576 #[test]
577 fn test_chain_context() {
578 let mut ctx = ChainContext::new();
579
580 ctx.set_variable("user_id".to_string(), json!("12345"));
582 assert_eq!(ctx.get_variable("user_id"), Some(&json!("12345")));
583
584 let response = ChainResponse {
586 status: 200,
587 headers: HashMap::new(),
588 body: Some(json!({"user": {"id": 123, "name": "John"}})),
589 duration_ms: 150,
590 executed_at: "2023-01-01T00:00:00Z".to_string(),
591 error: None,
592 };
593 ctx.store_response("login".to_string(), response.clone());
594 assert_eq!(ctx.get_response("login"), Some(&response));
595 }
596
597 #[test]
598 fn test_chain_context_comprehensive() {
599 let mut ctx = ChainContext::new();
600
601 ctx.set_variable("user_id".to_string(), json!("12345"));
603 ctx.set_variable("token".to_string(), json!("abc-def-ghi"));
604 ctx.set_variable("environment".to_string(), json!("production"));
605 ctx.set_variable("timeout".to_string(), json!(30));
606
607 assert_eq!(ctx.get_variable("user_id"), Some(&json!("12345")));
608 assert_eq!(ctx.get_variable("token"), Some(&json!("abc-def-ghi")));
609 assert_eq!(ctx.get_variable("environment"), Some(&json!("production")));
610 assert_eq!(ctx.get_variable("timeout"), Some(&json!(30)));
611
612 assert_eq!(ctx.get_variable("nonexistent"), None);
614
615 ctx.set_variable("user_id".to_string(), json!("67890"));
617 assert_eq!(ctx.get_variable("user_id"), Some(&json!("67890")));
618
619 ctx.set_metadata("chain_id".to_string(), "test-chain-123".to_string());
621 ctx.set_metadata("version".to_string(), "1.0.0".to_string());
622 assert_eq!(ctx.get_metadata("chain_id"), Some(&"test-chain-123".to_string()));
623 assert_eq!(ctx.get_metadata("version"), Some(&"1.0.0".to_string()));
624
625 let response1 = ChainResponse {
627 status: 200,
628 headers: vec![("Content-Type".to_string(), "application/json".to_string())]
629 .into_iter()
630 .collect(),
631 body: Some(json!({"message": "success1"})),
632 duration_ms: 100,
633 executed_at: "2023-01-01T00:00:00Z".to_string(),
634 error: None,
635 };
636
637 let response2 = ChainResponse {
638 status: 201,
639 headers: vec![("Location".to_string(), "/users/123".to_string())].into_iter().collect(),
640 body: Some(json!({"id": 123, "name": "John"})),
641 duration_ms: 150,
642 executed_at: "2023-01-01T00:00:01Z".to_string(),
643 error: None,
644 };
645
646 ctx.store_response("step1".to_string(), response1.clone());
647 ctx.store_response("step2".to_string(), response2.clone());
648
649 assert_eq!(ctx.get_response("step1"), Some(&response1));
650 assert_eq!(ctx.get_response("step2"), Some(&response2));
651 assert_eq!(ctx.get_response("nonexistent"), None);
652
653 let updated_response = ChainResponse {
655 status: 202,
656 headers: HashMap::new(),
657 body: Some(json!({"message": "updated"})),
658 duration_ms: 200,
659 executed_at: "2023-01-01T00:00:02Z".to_string(),
660 error: None,
661 };
662 ctx.store_response("step1".to_string(), updated_response.clone());
663 assert_eq!(ctx.get_response("step1"), Some(&updated_response));
664 }
665
666 #[test]
667 fn test_chain_context_serialization() {
668 let mut ctx = ChainContext::new();
669
670 ctx.set_variable("test_var".to_string(), json!("test_value"));
672 ctx.set_metadata("test_meta".to_string(), "test_value".to_string());
673
674 let response = ChainResponse {
675 status: 200,
676 headers: HashMap::new(),
677 body: Some(json!({"data": "test"})),
678 duration_ms: 100,
679 executed_at: "2023-01-01T00:00:00Z".to_string(),
680 error: None,
681 };
682 ctx.store_response("test_response".to_string(), response);
683
684 let json_str = serde_json::to_string(&ctx).unwrap();
686 assert!(json_str.contains("test_var"));
687 assert!(json_str.contains("test_value"));
688 assert!(json_str.contains("test_meta"));
689 assert!(json_str.contains("test_response"));
690
691 let deserialized: ChainContext = serde_json::from_str(&json_str).unwrap();
693 assert_eq!(deserialized.get_variable("test_var"), Some(&json!("test_value")));
694 assert_eq!(deserialized.get_metadata("test_meta"), Some(&"test_value".to_string()));
695 assert!(deserialized.get_response("test_response").is_some());
696 }
697
698 #[test]
699 fn test_chain_request_serialization() {
700 let request = ChainRequest {
701 id: "test-req".to_string(),
702 method: "POST".to_string(),
703 url: "https://api.example.com/test".to_string(),
704 headers: vec![("Content-Type".to_string(), "application/json".to_string())]
705 .into_iter()
706 .collect(),
707 body: Some(RequestBody::Json(json!({"key": "value"}))),
708 depends_on: vec!["req1".to_string(), "req2".to_string()],
709 timeout_secs: Some(30),
710 expected_status: Some(vec![200, 201, 202]),
711 scripting: Some(RequestScripting {
712 pre_script: Some("console.log('pre');".to_string()),
713 post_script: Some("console.log('post');".to_string()),
714 runtime: "javascript".to_string(),
715 timeout_ms: 5000,
716 }),
717 };
718
719 let json_str = serde_json::to_string(&request).unwrap();
720 assert!(json_str.contains("test-req"));
721 assert!(json_str.contains("POST"));
722 assert!(json_str.contains("Content-Type"));
723 assert!(json_str.contains("req1"));
724 assert!(json_str.contains("preScript"));
725
726 let deserialized: ChainRequest = serde_json::from_str(&json_str).unwrap();
727 assert_eq!(deserialized.id, request.id);
728 assert_eq!(deserialized.method, request.method);
729 assert_eq!(deserialized.depends_on, request.depends_on);
730 }
731
732 #[test]
733 fn test_chain_response_serialization() {
734 let response = ChainResponse {
735 status: 200,
736 headers: vec![
737 ("Content-Type".to_string(), "application/json".to_string()),
738 ("X-Request-ID".to_string(), "req-123".to_string()),
739 ]
740 .into_iter()
741 .collect(),
742 body: Some(json!({"result": "success", "data": [1, 2, 3]})),
743 duration_ms: 150,
744 executed_at: "2023-01-01T00:00:00Z".to_string(),
745 error: None,
746 };
747
748 let json_str = serde_json::to_string(&response).unwrap();
749 assert!(json_str.contains("200"));
750 assert!(json_str.contains("application/json"));
751 assert!(json_str.contains("success"));
752
753 let deserialized: ChainResponse = serde_json::from_str(&json_str).unwrap();
754 assert_eq!(deserialized.status, response.status);
755 assert_eq!(deserialized.duration_ms, response.duration_ms);
756 assert_eq!(deserialized.body, response.body);
757 }
758
759 #[test]
760 fn test_chain_response_with_error() {
761 let error_response = ChainResponse {
762 status: 500,
763 headers: HashMap::new(),
764 body: None,
765 duration_ms: 50,
766 executed_at: "2023-01-01T00:00:00Z".to_string(),
767 error: Some("Internal server error".to_string()),
768 };
769
770 let json_str = serde_json::to_string(&error_response).unwrap();
771 assert!(json_str.contains("500"));
772 assert!(json_str.contains("Internal server error"));
773
774 let deserialized: ChainResponse = serde_json::from_str(&json_str).unwrap();
775 assert_eq!(deserialized.error, Some("Internal server error".to_string()));
776 assert!(deserialized.body.is_none());
777 }
778
779 #[test]
780 fn test_request_body_types() {
781 let json_body = RequestBody::Json(json!({"key": "value", "number": 42}));
783 assert!(matches!(json_body, RequestBody::Json(_)));
784
785 let string_body =
787 RequestBody::Json(serde_json::Value::String("raw text content".to_string()));
788 assert!(matches!(string_body, RequestBody::Json(_)));
789
790 let binary_body = RequestBody::BinaryFile {
792 path: "/path/to/file.bin".to_string(),
793 content_type: Some("application/octet-stream".to_string()),
794 };
795 assert!(matches!(binary_body, RequestBody::BinaryFile { .. }));
796
797 let test_cases = vec![
799 RequestBody::Json(json!({"test": "json"})),
800 RequestBody::Json(serde_json::Value::String("test string".to_string())),
801 RequestBody::BinaryFile {
802 path: "/path/to/bytes.bin".to_string(),
803 content_type: None,
804 },
805 ];
806
807 for body in test_cases {
808 let json_str = serde_json::to_string(&body).unwrap();
809 let deserialized: RequestBody = serde_json::from_str(&json_str).unwrap();
810 assert_eq!(format!("{:?}", body), format!("{:?}", deserialized));
811 }
812 }
813
814 #[test]
815 fn test_chain_link_dependencies() {
816 let link1 = ChainLink {
817 request: ChainRequest {
818 id: "req1".to_string(),
819 method: "GET".to_string(),
820 url: "https://api.example.com/users".to_string(),
821 headers: HashMap::new(),
822 body: None,
823 depends_on: vec![], timeout_secs: None,
825 expected_status: None,
826 scripting: None,
827 },
828 extract: HashMap::new(),
829 store_as: Some("users".to_string()),
830 };
831
832 let link2 = ChainLink {
833 request: ChainRequest {
834 id: "req2".to_string(),
835 method: "POST".to_string(),
836 url: "https://api.example.com/posts".to_string(),
837 headers: HashMap::new(),
838 body: Some(RequestBody::Json(json!({"title": "Test"}))),
839 depends_on: vec!["req1".to_string()], timeout_secs: Some(30),
841 expected_status: Some(vec![200, 201]),
842 scripting: None,
843 },
844 extract: HashMap::new(),
845 store_as: Some("post".to_string()),
846 };
847
848 let link3 = ChainLink {
849 request: ChainRequest {
850 id: "req3".to_string(),
851 method: "PUT".to_string(),
852 url: "https://api.example.com/posts/{{chain.post.id}}".to_string(),
853 headers: HashMap::new(),
854 body: None,
855 depends_on: vec!["req1".to_string(), "req2".to_string()], timeout_secs: None,
857 expected_status: None,
858 scripting: None,
859 },
860 extract: HashMap::new(),
861 store_as: None,
862 };
863
864 assert!(link1.request.depends_on.is_empty());
865 assert_eq!(link2.request.depends_on, vec!["req1".to_string()]);
866 assert_eq!(link3.request.depends_on, vec!["req1".to_string(), "req2".to_string()]);
867 }
868
869 #[test]
870 fn test_chain_config_validation() {
871 let valid_config = ChainConfig {
873 enabled: true,
874 max_chain_length: 10,
875 global_timeout_secs: 300,
876 enable_parallel_execution: true,
877 };
878
879 let invalid_config = ChainConfig {
881 enabled: true,
882 max_chain_length: 0, global_timeout_secs: 300,
884 enable_parallel_execution: true,
885 };
886
887 assert!(valid_config.max_chain_length > 0);
888 assert!(invalid_config.max_chain_length == 0);
889
890 let edge_config = ChainConfig {
892 enabled: false,
893 max_chain_length: 1,
894 global_timeout_secs: 0,
895 enable_parallel_execution: false,
896 };
897 assert_eq!(edge_config.max_chain_length, 1);
898 assert_eq!(edge_config.global_timeout_secs, 0);
899 assert!(!edge_config.enabled);
900 }
901
902 #[test]
903 fn test_request_scripting_config() {
904 let scripting = RequestScripting {
905 pre_script: Some("console.log('Starting request');".to_string()),
906 post_script: Some("console.log('Request completed');".to_string()),
907 runtime: "javascript".to_string(),
908 timeout_ms: 5000,
909 };
910
911 assert_eq!(scripting.runtime, "javascript");
912 assert_eq!(scripting.timeout_ms, 5000);
913 assert!(scripting.pre_script.is_some());
914 assert!(scripting.post_script.is_some());
915
916 let json_str = serde_json::to_string(&scripting).unwrap();
918 assert!(json_str.contains("javascript"));
919 assert!(json_str.contains("Starting request"));
920 assert!(json_str.contains("Request completed"));
921
922 let deserialized: RequestScripting = serde_json::from_str(&json_str).unwrap();
923 assert_eq!(deserialized.runtime, scripting.runtime);
924 assert_eq!(deserialized.timeout_ms, scripting.timeout_ms);
925 }
926
927 #[test]
928 fn test_chain_definition_structure() {
929 let definition = ChainDefinition {
930 id: "test-chain".to_string(),
931 name: "Test Chain".to_string(),
932 description: Some("A comprehensive test chain".to_string()),
933 config: ChainConfig::default(),
934 links: vec![ChainLink {
935 request: ChainRequest {
936 id: "req1".to_string(),
937 method: "GET".to_string(),
938 url: "https://api.example.com/users".to_string(),
939 headers: HashMap::new(),
940 body: None,
941 depends_on: vec![],
942 timeout_secs: None,
943 expected_status: None,
944 scripting: None,
945 },
946 extract: vec![("user_id".to_string(), "$.users[0].id".to_string())]
947 .into_iter()
948 .collect(),
949 store_as: Some("users".to_string()),
950 }],
951 variables: vec![
952 ("api_key".to_string(), json!("test-key")),
953 ("base_url".to_string(), json!("https://api.example.com")),
954 ]
955 .into_iter()
956 .collect(),
957 tags: vec!["test".to_string(), "integration".to_string()],
958 };
959
960 assert_eq!(definition.id, "test-chain");
961 assert_eq!(definition.name, "Test Chain");
962 assert!(definition.description.is_some());
963 assert_eq!(definition.links.len(), 1);
964 assert_eq!(definition.variables.len(), 2);
965 assert_eq!(definition.tags.len(), 2);
966
967 let json_str = serde_json::to_string(&definition).unwrap();
969 assert!(json_str.contains("test-chain"));
970 assert!(json_str.contains("Test Chain"));
971 assert!(json_str.contains("comprehensive test chain"));
972 assert!(json_str.contains("api_key"));
973 assert!(json_str.contains("test-key"));
974 }
975
976 #[test]
977 fn test_chain_execution_context() {
978 let chain_def = ChainDefinition {
979 id: "test_chain".to_string(),
980 name: "Test Chain".to_string(),
981 description: Some("Test chain for unit tests".to_string()),
982 config: ChainConfig::default(),
983 links: vec![],
984 tags: vec![],
985 variables: HashMap::new(),
986 };
987 let exec_ctx = ChainExecutionContext::new(chain_def);
988
989 std::thread::sleep(std::time::Duration::from_millis(1));
991
992 assert!(exec_ctx.elapsed_ms() > 0);
994 }
995
996 #[tokio::test]
997 async fn test_chain_definition_validation() {
998 let registry = RequestChainRegistry::new(ChainConfig::default());
999
1000 let valid_chain = ChainDefinition {
1001 id: "test-chain".to_string(),
1002 name: "Test Chain".to_string(),
1003 description: Some("A test chain for validation".to_string()),
1004 config: ChainConfig::default(),
1005 links: vec![
1006 ChainLink {
1007 request: ChainRequest {
1008 id: "req1".to_string(),
1009 method: "GET".to_string(),
1010 url: "https://api.example.com/users".to_string(),
1011 headers: HashMap::new(),
1012 body: None,
1013 depends_on: vec![],
1014 timeout_secs: None,
1015 expected_status: None,
1016 scripting: None,
1017 },
1018 extract: HashMap::new(),
1019 store_as: Some("users".to_string()),
1020 },
1021 ChainLink {
1022 request: ChainRequest {
1023 id: "req2".to_string(),
1024 method: "POST".to_string(),
1025 url: "https://api.example.com/users/{{chain.users.body[0].id}}/posts"
1026 .to_string(),
1027 headers: HashMap::new(),
1028 body: Some(RequestBody::Json(json!({"title": "Hello World"}))),
1029 depends_on: vec!["req1".to_string()],
1030 timeout_secs: None,
1031 expected_status: None,
1032 scripting: None,
1033 },
1034 extract: HashMap::new(),
1035 store_as: Some("post".to_string()),
1036 },
1037 ],
1038 variables: {
1039 let mut vars = HashMap::new();
1040 vars.insert("api_key".to_string(), json!("test-key-123"));
1041 vars
1042 },
1043 tags: vec!["test".to_string()],
1044 };
1045
1046 assert!(registry.validate_chain(&valid_chain).await.is_ok());
1048
1049 let invalid_chain = ChainDefinition {
1051 id: "empty-chain".to_string(),
1052 name: "Empty Chain".to_string(),
1053 description: None,
1054 config: ChainConfig::default(),
1055 links: vec![],
1056 variables: HashMap::new(),
1057 tags: vec![],
1058 };
1059 assert!(registry.validate_chain(&invalid_chain).await.is_err());
1060
1061 let self_dep_chain = ChainDefinition {
1063 id: "self-dep-chain".to_string(),
1064 name: "Self Dependency Chain".to_string(),
1065 description: None,
1066 config: ChainConfig::default(),
1067 links: vec![ChainLink {
1068 request: ChainRequest {
1069 id: "req1".to_string(),
1070 method: "GET".to_string(),
1071 url: "https://api.example.com/users".to_string(),
1072 headers: HashMap::new(),
1073 body: None,
1074 depends_on: vec!["req1".to_string()], timeout_secs: None,
1076 expected_status: None,
1077 scripting: None,
1078 },
1079 extract: HashMap::new(),
1080 store_as: None,
1081 }],
1082 variables: HashMap::new(),
1083 tags: vec![],
1084 };
1085 assert!(registry.validate_chain(&self_dep_chain).await.is_err());
1086 }
1087}