1use crate::{Error, Result};
7use axum::http::{HeaderMap, Method, Uri};
8use serde::{Deserialize, Serialize};
9use serde_json::Value;
10use std::collections::HashMap;
11use std::sync::Arc;
12use tokio::sync::RwLock;
13use tracing::debug;
14
15#[derive(Debug, Clone)]
17struct StateInstance {
18 resource_id: String,
20 current_state: String,
22 resource_type: String,
24 state_data: HashMap<String, Value>,
26}
27
28impl StateInstance {
29 fn new(resource_id: String, resource_type: String, initial_state: String) -> Self {
30 Self {
31 resource_id,
32 current_state: initial_state,
33 resource_type,
34 state_data: HashMap::new(),
35 }
36 }
37
38 fn transition_to(&mut self, new_state: String) {
39 self.current_state = new_state;
40 }
41}
42
43struct StateMachineManager {
45 instances: Arc<RwLock<HashMap<String, StateInstance>>>,
47}
48
49impl StateMachineManager {
50 fn new() -> Self {
51 Self {
52 instances: Arc::new(RwLock::new(HashMap::new())),
53 }
54 }
55
56 async fn get_or_create_instance(
57 &self,
58 resource_id: String,
59 resource_type: String,
60 initial_state: String,
61 ) -> Result<StateInstance> {
62 let mut instances = self.instances.write().await;
63 if let Some(instance) = instances.get(&resource_id) {
64 Ok(instance.clone())
65 } else {
66 let instance = StateInstance::new(resource_id.clone(), resource_type, initial_state);
67 instances.insert(resource_id, instance.clone());
68 Ok(instance)
69 }
70 }
71
72 async fn update_instance(&self, resource_id: String, instance: StateInstance) -> Result<()> {
73 let mut instances = self.instances.write().await;
74 instances.insert(resource_id, instance);
75 Ok(())
76 }
77}
78
79#[derive(Debug, Clone, Serialize, Deserialize)]
81pub struct StatefulConfig {
82 pub resource_id_extract: ResourceIdExtract,
84 pub resource_type: String,
86 pub state_responses: HashMap<String, StateResponse>,
88 pub transitions: Vec<TransitionTrigger>,
90}
91
92#[derive(Debug, Clone, Serialize, Deserialize)]
94#[serde(tag = "type", rename_all = "snake_case")]
95pub enum ResourceIdExtract {
96 PathParam {
98 param: String,
100 },
101 JsonPath {
103 path: String,
105 },
106 Header {
108 name: String,
110 },
111 QueryParam {
113 param: String,
115 },
116 Composite {
118 extractors: Vec<ResourceIdExtract>,
120 },
121}
122
123#[derive(Debug, Clone, Serialize, Deserialize)]
125pub struct StateResponse {
126 pub status_code: u16,
128 pub headers: HashMap<String, String>,
130 pub body_template: String,
132 pub content_type: String,
134}
135
136#[derive(Debug, Clone, Serialize, Deserialize)]
138pub struct TransitionTrigger {
139 #[serde(with = "method_serde")]
141 pub method: Method,
142 pub path_pattern: String,
144 pub from_state: String,
146 pub to_state: String,
148 pub condition: Option<String>,
150}
151
152mod method_serde {
153 use axum::http::Method;
154 use serde::{Deserialize, Deserializer, Serialize, Serializer};
155
156 pub fn serialize<S>(method: &Method, serializer: S) -> Result<S::Ok, S::Error>
157 where
158 S: Serializer,
159 {
160 method.as_str().serialize(serializer)
161 }
162
163 pub fn deserialize<'de, D>(deserializer: D) -> Result<Method, D::Error>
164 where
165 D: Deserializer<'de>,
166 {
167 let s = String::deserialize(deserializer)?;
168 Method::from_bytes(s.as_bytes()).map_err(serde::de::Error::custom)
169 }
170}
171
172pub struct StatefulResponseHandler {
174 state_manager: Arc<StateMachineManager>,
176 configs: Arc<RwLock<HashMap<String, StatefulConfig>>>,
178}
179
180impl StatefulResponseHandler {
181 pub fn new() -> Result<Self> {
183 Ok(Self {
184 state_manager: Arc::new(StateMachineManager::new()),
185 configs: Arc::new(RwLock::new(HashMap::new())),
186 })
187 }
188
189 pub async fn add_config(&self, path_pattern: String, config: StatefulConfig) {
191 let mut configs = self.configs.write().await;
192 configs.insert(path_pattern, config);
193 }
194
195 pub async fn can_handle(&self, _method: &Method, path: &str) -> bool {
197 let configs = self.configs.read().await;
198 for (pattern, _) in configs.iter() {
199 if self.path_matches(pattern, path) {
200 return true;
201 }
202 }
203 false
204 }
205
206 pub async fn process_request(
208 &self,
209 method: &Method,
210 uri: &Uri,
211 headers: &HeaderMap,
212 body: Option<&[u8]>,
213 ) -> Result<Option<StatefulResponse>> {
214 let path = uri.path();
215
216 let config = {
218 let configs = self.configs.read().await;
219 configs
220 .iter()
221 .find(|(pattern, _)| self.path_matches(pattern, path))
222 .map(|(_, config)| config.clone())
223 };
224
225 let config = match config {
226 Some(c) => c,
227 None => return Ok(None),
228 };
229
230 let resource_id =
232 self.extract_resource_id(&config.resource_id_extract, uri, headers, body)?;
233
234 let state_instance = self
236 .state_manager
237 .get_or_create_instance(
238 resource_id.clone(),
239 config.resource_type.clone(),
240 "initial".to_string(), )
242 .await?;
243
244 let new_state = self
246 .check_transitions(&config, method, path, &state_instance, headers, body)
247 .await?;
248
249 let current_state = if let Some(ref state) = new_state {
251 state.clone()
252 } else {
253 state_instance.current_state.clone()
254 };
255
256 let state_response = config.state_responses.get(¤t_state).ok_or_else(|| {
258 Error::generic(format!("No response configuration for state '{}'", current_state))
259 })?;
260
261 if let Some(ref new_state) = new_state {
263 let mut updated_instance = state_instance.clone();
264 updated_instance.transition_to(new_state.clone());
265 self.state_manager
266 .update_instance(resource_id.clone(), updated_instance)
267 .await?;
268 }
269
270 Ok(Some(StatefulResponse {
271 status_code: state_response.status_code,
272 headers: state_response.headers.clone(),
273 body: self.render_body_template(&state_response.body_template, &state_instance)?,
274 content_type: state_response.content_type.clone(),
275 state: current_state,
276 resource_id: resource_id.clone(),
277 }))
278 }
279
280 fn extract_resource_id(
282 &self,
283 extract: &ResourceIdExtract,
284 uri: &Uri,
285 headers: &HeaderMap,
286 body: Option<&[u8]>,
287 ) -> Result<String> {
288 let path = uri.path();
289 match extract {
290 ResourceIdExtract::PathParam { param } => {
291 let segments: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
294 if let Some(last) = segments.last() {
295 Ok(last.to_string())
296 } else {
297 Err(Error::generic(format!(
298 "Could not extract path parameter '{}' from path '{}'",
299 param, path
300 )))
301 }
302 }
303 ResourceIdExtract::Header { name } => headers
304 .get(name)
305 .and_then(|v| v.to_str().ok())
306 .map(|s| s.to_string())
307 .ok_or_else(|| Error::generic(format!("Header '{}' not found", name))),
308 ResourceIdExtract::QueryParam { param } => {
309 uri.query()
311 .and_then(|q| {
312 url::form_urlencoded::parse(q.as_bytes())
313 .find(|(k, _)| k == param)
314 .map(|(_, v)| v.to_string())
315 })
316 .ok_or_else(|| Error::generic(format!("Query parameter '{}' not found", param)))
317 }
318 ResourceIdExtract::JsonPath { path: json_path } => {
319 let body_str = body
320 .and_then(|b| std::str::from_utf8(b).ok())
321 .ok_or_else(|| Error::generic("Request body is not valid UTF-8".to_string()))?;
322
323 let json: Value = serde_json::from_str(body_str)
324 .map_err(|e| Error::generic(format!("Invalid JSON body: {}", e)))?;
325
326 self.extract_json_path(&json, json_path)
328 }
329 ResourceIdExtract::Composite { extractors } => {
330 for extract in extractors {
332 if let Ok(id) = self.extract_resource_id(extract, uri, headers, body) {
333 return Ok(id);
334 }
335 }
336 Err(Error::generic("Could not extract resource ID from any source".to_string()))
337 }
338 }
339 }
340
341 fn extract_json_path(&self, json: &Value, path: &str) -> Result<String> {
343 let path = path.trim_start_matches('$').trim_start_matches('.');
344 let parts: Vec<&str> = path.split('.').collect();
345
346 let mut current = json;
347 for part in parts {
348 match current {
349 Value::Object(map) => {
350 current = map
351 .get(part)
352 .ok_or_else(|| Error::generic(format!("Path '{}' not found", path)))?;
353 }
354 Value::Array(arr) => {
355 let idx: usize = part
356 .parse()
357 .map_err(|_| Error::generic(format!("Invalid array index: {}", part)))?;
358 current = arr.get(idx).ok_or_else(|| {
359 Error::generic(format!("Array index {} out of bounds", idx))
360 })?;
361 }
362 _ => {
363 return Err(Error::generic(format!(
364 "Cannot traverse path '{}' at '{}'",
365 path, part
366 )));
367 }
368 }
369 }
370
371 match current {
372 Value::String(s) => Ok(s.clone()),
373 Value::Number(n) => Ok(n.to_string()),
374 _ => {
375 Err(Error::generic(format!("Path '{}' does not point to a string or number", path)))
376 }
377 }
378 }
379
380 async fn check_transitions(
382 &self,
383 config: &StatefulConfig,
384 method: &Method,
385 path: &str,
386 instance: &StateInstance,
387 headers: &HeaderMap,
388 body: Option<&[u8]>,
389 ) -> Result<Option<String>> {
390 for transition in &config.transitions {
391 if transition.method != *method {
393 continue;
394 }
395
396 if !self.path_matches(&transition.path_pattern, path) {
397 continue;
398 }
399
400 if instance.current_state != transition.from_state {
402 continue;
403 }
404
405 if let Some(ref condition) = transition.condition {
407 if !self.evaluate_condition(condition, headers, body)? {
408 continue;
409 }
410 }
411
412 debug!(
414 "State transition triggered: {} -> {} for resource {}",
415 transition.from_state, transition.to_state, instance.resource_id
416 );
417
418 return Ok(Some(transition.to_state.clone()));
419 }
420
421 Ok(None)
422 }
423
424 fn evaluate_condition(
426 &self,
427 condition: &str,
428 _headers: &HeaderMap,
429 body: Option<&[u8]>,
430 ) -> Result<bool> {
431 if condition.starts_with("$.") {
434 let body_str = body
435 .and_then(|b| std::str::from_utf8(b).ok())
436 .ok_or_else(|| Error::generic("Request body is not valid UTF-8".to_string()))?;
437
438 let json: Value = serde_json::from_str(body_str)
439 .map_err(|e| Error::generic(format!("Invalid JSON body: {}", e)))?;
440
441 let value = self.extract_json_path(&json, condition)?;
443 Ok(!value.is_empty() && value != "false" && value != "0")
444 } else {
445 Ok(true)
447 }
448 }
449
450 fn render_body_template(&self, template: &str, instance: &StateInstance) -> Result<String> {
452 let mut result = template.to_string();
453
454 result = result.replace("{{state}}", &instance.current_state);
456
457 result = result.replace("{{resource_id}}", &instance.resource_id);
459
460 for (key, value) in &instance.state_data {
462 let placeholder = format!("{{{{state_data.{}}}}}", key);
463 let value_str = match value {
464 Value::String(s) => s.clone(),
465 Value::Number(n) => n.to_string(),
466 Value::Bool(b) => b.to_string(),
467 _ => serde_json::to_string(value).unwrap_or_default(),
468 };
469 result = result.replace(&placeholder, &value_str);
470 }
471
472 Ok(result)
473 }
474
475 pub async fn process_stub_state(
485 &self,
486 method: &Method,
487 uri: &Uri,
488 headers: &HeaderMap,
489 body: Option<&[u8]>,
490 resource_type: &str,
491 resource_id_extract: &ResourceIdExtract,
492 initial_state: &str,
493 transitions: Option<&[TransitionTrigger]>,
494 ) -> Result<Option<StateInfo>> {
495 let resource_id = self.extract_resource_id(resource_id_extract, uri, headers, body)?;
497
498 let state_instance = self
500 .state_manager
501 .get_or_create_instance(
502 resource_id.clone(),
503 resource_type.to_string(),
504 initial_state.to_string(),
505 )
506 .await?;
507
508 let new_state = if let Some(transition_list) = transitions {
510 let path = uri.path();
511 let mut transitioned_state = None;
514
515 for transition in transition_list {
516 if transition.method != *method {
518 continue;
519 }
520
521 if !self.path_matches(&transition.path_pattern, path) {
522 continue;
523 }
524
525 if state_instance.current_state != transition.from_state {
527 continue;
528 }
529
530 if let Some(ref condition) = transition.condition {
532 if !self.evaluate_condition(condition, headers, body)? {
533 continue;
534 }
535 }
536
537 debug!(
539 "State transition triggered in stub processing: {} -> {} for resource {}",
540 transition.from_state, transition.to_state, resource_id
541 );
542
543 transitioned_state = Some(transition.to_state.clone());
544 break; }
546
547 transitioned_state
548 } else {
549 None
550 };
551
552 let final_state = if let Some(ref new_state) = new_state {
554 let mut updated_instance = state_instance.clone();
555 updated_instance.transition_to(new_state.clone());
556 self.state_manager
557 .update_instance(resource_id.clone(), updated_instance)
558 .await?;
559 new_state.clone()
560 } else {
561 state_instance.current_state.clone()
562 };
563
564 Ok(Some(StateInfo {
565 resource_id: resource_id.clone(),
566 current_state: final_state,
567 state_data: state_instance.state_data.clone(),
568 }))
569 }
570
571 pub async fn update_resource_state(
573 &self,
574 resource_id: &str,
575 resource_type: &str,
576 new_state: &str,
577 ) -> Result<()> {
578 let mut instances = self.state_manager.instances.write().await;
579 if let Some(instance) = instances.get_mut(resource_id) {
580 if instance.resource_type == resource_type {
581 instance.transition_to(new_state.to_string());
582 return Ok(());
583 }
584 }
585 Err(Error::generic(format!(
586 "Resource '{}' of type '{}' not found",
587 resource_id, resource_type
588 )))
589 }
590
591 pub async fn get_resource_state(
593 &self,
594 resource_id: &str,
595 resource_type: &str,
596 ) -> Result<Option<StateInfo>> {
597 let instances = self.state_manager.instances.read().await;
598 if let Some(instance) = instances.get(resource_id) {
599 if instance.resource_type == resource_type {
600 return Ok(Some(StateInfo {
601 resource_id: resource_id.to_string(),
602 current_state: instance.current_state.clone(),
603 state_data: instance.state_data.clone(),
604 }));
605 }
606 }
607 Ok(None)
608 }
609
610 fn path_matches(&self, pattern: &str, path: &str) -> bool {
612 let pattern_regex = pattern.replace("{", "(?P<").replace("}", ">[^/]+)").replace("*", ".*");
614 let regex = regex::Regex::new(&format!("^{}$", pattern_regex));
615 match regex {
616 Ok(re) => re.is_match(path),
617 Err(_) => pattern == path, }
619 }
620}
621
622#[derive(Debug, Clone)]
624pub struct StateInfo {
625 pub resource_id: String,
627 pub current_state: String,
629 pub state_data: HashMap<String, Value>,
631}
632
633#[derive(Debug, Clone)]
635pub struct StatefulResponse {
636 pub status_code: u16,
638 pub headers: HashMap<String, String>,
640 pub body: String,
642 pub content_type: String,
644 pub state: String,
646 pub resource_id: String,
648}
649
650#[cfg(test)]
651mod tests {
652 use super::*;
653
654 #[test]
659 fn test_state_instance_new() {
660 let instance =
661 StateInstance::new("order-123".to_string(), "order".to_string(), "pending".to_string());
662 assert_eq!(instance.resource_id, "order-123");
663 assert_eq!(instance.resource_type, "order");
664 assert_eq!(instance.current_state, "pending");
665 assert!(instance.state_data.is_empty());
666 }
667
668 #[test]
669 fn test_state_instance_transition_to() {
670 let mut instance =
671 StateInstance::new("order-123".to_string(), "order".to_string(), "pending".to_string());
672 instance.transition_to("confirmed".to_string());
673 assert_eq!(instance.current_state, "confirmed");
674
675 instance.transition_to("shipped".to_string());
676 assert_eq!(instance.current_state, "shipped");
677 }
678
679 #[test]
680 fn test_state_instance_clone() {
681 let instance =
682 StateInstance::new("order-123".to_string(), "order".to_string(), "pending".to_string());
683 let cloned = instance.clone();
684 assert_eq!(cloned.resource_id, instance.resource_id);
685 assert_eq!(cloned.current_state, instance.current_state);
686 }
687
688 #[test]
689 fn test_state_instance_debug() {
690 let instance =
691 StateInstance::new("order-123".to_string(), "order".to_string(), "pending".to_string());
692 let debug_str = format!("{:?}", instance);
693 assert!(debug_str.contains("order-123"));
694 assert!(debug_str.contains("pending"));
695 }
696
697 #[tokio::test]
702 async fn test_state_machine_manager_new() {
703 let manager = StateMachineManager::new();
704 let instances = manager.instances.read().await;
705 assert!(instances.is_empty());
706 }
707
708 #[tokio::test]
709 async fn test_state_machine_manager_get_or_create_new() {
710 let manager = StateMachineManager::new();
711 let instance = manager
712 .get_or_create_instance(
713 "order-123".to_string(),
714 "order".to_string(),
715 "pending".to_string(),
716 )
717 .await
718 .unwrap();
719 assert_eq!(instance.resource_id, "order-123");
720 assert_eq!(instance.current_state, "pending");
721 }
722
723 #[tokio::test]
724 async fn test_state_machine_manager_get_or_create_existing() {
725 let manager = StateMachineManager::new();
726
727 let instance1 = manager
729 .get_or_create_instance(
730 "order-123".to_string(),
731 "order".to_string(),
732 "pending".to_string(),
733 )
734 .await
735 .unwrap();
736 assert_eq!(instance1.current_state, "pending");
737
738 let instance2 = manager
740 .get_or_create_instance(
741 "order-123".to_string(),
742 "order".to_string(),
743 "confirmed".to_string(), )
745 .await
746 .unwrap();
747 assert_eq!(instance2.current_state, "pending"); }
749
750 #[tokio::test]
751 async fn test_state_machine_manager_update_instance() {
752 let manager = StateMachineManager::new();
753
754 let mut instance = manager
756 .get_or_create_instance(
757 "order-123".to_string(),
758 "order".to_string(),
759 "pending".to_string(),
760 )
761 .await
762 .unwrap();
763
764 instance.transition_to("confirmed".to_string());
766 manager.update_instance("order-123".to_string(), instance).await.unwrap();
767
768 let updated = manager
770 .get_or_create_instance(
771 "order-123".to_string(),
772 "order".to_string(),
773 "pending".to_string(),
774 )
775 .await
776 .unwrap();
777 assert_eq!(updated.current_state, "confirmed");
778 }
779
780 #[test]
785 fn test_stateful_config_serialize_deserialize() {
786 let config = StatefulConfig {
787 resource_id_extract: ResourceIdExtract::PathParam {
788 param: "order_id".to_string(),
789 },
790 resource_type: "order".to_string(),
791 state_responses: {
792 let mut map = HashMap::new();
793 map.insert(
794 "pending".to_string(),
795 StateResponse {
796 status_code: 200,
797 headers: HashMap::new(),
798 body_template: "{\"status\": \"pending\"}".to_string(),
799 content_type: "application/json".to_string(),
800 },
801 );
802 map
803 },
804 transitions: vec![],
805 };
806
807 let json = serde_json::to_string(&config).unwrap();
808 let deserialized: StatefulConfig = serde_json::from_str(&json).unwrap();
809 assert_eq!(deserialized.resource_type, "order");
810 }
811
812 #[test]
813 fn test_stateful_config_debug() {
814 let config = StatefulConfig {
815 resource_id_extract: ResourceIdExtract::PathParam {
816 param: "order_id".to_string(),
817 },
818 resource_type: "order".to_string(),
819 state_responses: HashMap::new(),
820 transitions: vec![],
821 };
822 let debug_str = format!("{:?}", config);
823 assert!(debug_str.contains("order"));
824 }
825
826 #[test]
827 fn test_stateful_config_clone() {
828 let config = StatefulConfig {
829 resource_id_extract: ResourceIdExtract::PathParam {
830 param: "id".to_string(),
831 },
832 resource_type: "user".to_string(),
833 state_responses: HashMap::new(),
834 transitions: vec![],
835 };
836 let cloned = config.clone();
837 assert_eq!(cloned.resource_type, "user");
838 }
839
840 #[test]
845 fn test_resource_id_extract_path_param() {
846 let extract = ResourceIdExtract::PathParam {
847 param: "order_id".to_string(),
848 };
849 let json = serde_json::to_string(&extract).unwrap();
850 assert!(json.contains("path_param"));
851 }
852
853 #[test]
854 fn test_resource_id_extract_json_path() {
855 let extract = ResourceIdExtract::JsonPath {
856 path: "$.order.id".to_string(),
857 };
858 let json = serde_json::to_string(&extract).unwrap();
859 assert!(json.contains("json_path"));
860 }
861
862 #[test]
863 fn test_resource_id_extract_header() {
864 let extract = ResourceIdExtract::Header {
865 name: "X-Order-ID".to_string(),
866 };
867 let json = serde_json::to_string(&extract).unwrap();
868 assert!(json.contains("header"));
869 }
870
871 #[test]
872 fn test_resource_id_extract_query_param() {
873 let extract = ResourceIdExtract::QueryParam {
874 param: "order_id".to_string(),
875 };
876 let json = serde_json::to_string(&extract).unwrap();
877 assert!(json.contains("query_param"));
878 }
879
880 #[test]
881 fn test_resource_id_extract_composite() {
882 let extract = ResourceIdExtract::Composite {
883 extractors: vec![
884 ResourceIdExtract::PathParam {
885 param: "id".to_string(),
886 },
887 ResourceIdExtract::Header {
888 name: "X-ID".to_string(),
889 },
890 ],
891 };
892 let json = serde_json::to_string(&extract).unwrap();
893 assert!(json.contains("composite"));
894 }
895
896 #[test]
901 fn test_state_response_serialize_deserialize() {
902 let response = StateResponse {
903 status_code: 200,
904 headers: {
905 let mut h = HashMap::new();
906 h.insert("X-State".to_string(), "pending".to_string());
907 h
908 },
909 body_template: "{\"state\": \"{{state}}\"}".to_string(),
910 content_type: "application/json".to_string(),
911 };
912 let json = serde_json::to_string(&response).unwrap();
913 let deserialized: StateResponse = serde_json::from_str(&json).unwrap();
914 assert_eq!(deserialized.status_code, 200);
915 assert_eq!(deserialized.content_type, "application/json");
916 }
917
918 #[test]
919 fn test_state_response_clone() {
920 let response = StateResponse {
921 status_code: 201,
922 headers: HashMap::new(),
923 body_template: "{}".to_string(),
924 content_type: "text/plain".to_string(),
925 };
926 let cloned = response.clone();
927 assert_eq!(cloned.status_code, 201);
928 }
929
930 #[test]
931 fn test_state_response_debug() {
932 let response = StateResponse {
933 status_code: 404,
934 headers: HashMap::new(),
935 body_template: "Not found".to_string(),
936 content_type: "text/plain".to_string(),
937 };
938 let debug_str = format!("{:?}", response);
939 assert!(debug_str.contains("404"));
940 }
941
942 #[test]
947 fn test_transition_trigger_serialize_deserialize() {
948 let trigger = TransitionTrigger {
949 method: Method::POST,
950 path_pattern: "/orders/{id}/confirm".to_string(),
951 from_state: "pending".to_string(),
952 to_state: "confirmed".to_string(),
953 condition: None,
954 };
955 let json = serde_json::to_string(&trigger).unwrap();
956 let deserialized: TransitionTrigger = serde_json::from_str(&json).unwrap();
957 assert_eq!(deserialized.from_state, "pending");
958 assert_eq!(deserialized.to_state, "confirmed");
959 }
960
961 #[test]
962 fn test_transition_trigger_with_condition() {
963 let trigger = TransitionTrigger {
964 method: Method::POST,
965 path_pattern: "/orders/{id}/ship".to_string(),
966 from_state: "confirmed".to_string(),
967 to_state: "shipped".to_string(),
968 condition: Some("$.payment.verified".to_string()),
969 };
970 let json = serde_json::to_string(&trigger).unwrap();
971 assert!(json.contains("payment.verified"));
972 }
973
974 #[test]
975 fn test_transition_trigger_clone() {
976 let trigger = TransitionTrigger {
977 method: Method::DELETE,
978 path_pattern: "/orders/{id}".to_string(),
979 from_state: "pending".to_string(),
980 to_state: "cancelled".to_string(),
981 condition: None,
982 };
983 let cloned = trigger.clone();
984 assert_eq!(cloned.method, Method::DELETE);
985 }
986
987 #[tokio::test]
992 async fn test_stateful_response_handler_new() {
993 let handler = StatefulResponseHandler::new().unwrap();
994 let configs = handler.configs.read().await;
995 assert!(configs.is_empty());
996 }
997
998 #[tokio::test]
999 async fn test_stateful_response_handler_add_config() {
1000 let handler = StatefulResponseHandler::new().unwrap();
1001 let config = StatefulConfig {
1002 resource_id_extract: ResourceIdExtract::PathParam {
1003 param: "id".to_string(),
1004 },
1005 resource_type: "order".to_string(),
1006 state_responses: HashMap::new(),
1007 transitions: vec![],
1008 };
1009
1010 handler.add_config("/orders/{id}".to_string(), config).await;
1011
1012 let configs = handler.configs.read().await;
1013 assert!(configs.contains_key("/orders/{id}"));
1014 }
1015
1016 #[tokio::test]
1017 async fn test_stateful_response_handler_can_handle_true() {
1018 let handler = StatefulResponseHandler::new().unwrap();
1019 let config = StatefulConfig {
1020 resource_id_extract: ResourceIdExtract::PathParam {
1021 param: "id".to_string(),
1022 },
1023 resource_type: "order".to_string(),
1024 state_responses: HashMap::new(),
1025 transitions: vec![],
1026 };
1027
1028 handler.add_config("/orders/{id}".to_string(), config).await;
1029
1030 assert!(handler.can_handle(&Method::GET, "/orders/123").await);
1031 }
1032
1033 #[tokio::test]
1034 async fn test_stateful_response_handler_can_handle_false() {
1035 let handler = StatefulResponseHandler::new().unwrap();
1036 assert!(!handler.can_handle(&Method::GET, "/orders/123").await);
1037 }
1038
1039 #[test]
1044 fn test_path_matching() {
1045 let handler = StatefulResponseHandler::new().unwrap();
1046
1047 assert!(handler.path_matches("/orders/{id}", "/orders/123"));
1048 assert!(handler.path_matches("/api/*", "/api/users"));
1049 assert!(!handler.path_matches("/orders/{id}", "/orders/123/items"));
1050 }
1051
1052 #[test]
1053 fn test_path_matching_exact() {
1054 let handler = StatefulResponseHandler::new().unwrap();
1055 assert!(handler.path_matches("/api/health", "/api/health"));
1056 assert!(!handler.path_matches("/api/health", "/api/health/check"));
1057 }
1058
1059 #[test]
1060 fn test_path_matching_multiple_params() {
1061 let handler = StatefulResponseHandler::new().unwrap();
1062 assert!(handler.path_matches("/users/{user_id}/orders/{order_id}", "/users/1/orders/2"));
1063 }
1064
1065 #[test]
1066 fn test_path_matching_wildcard() {
1067 let handler = StatefulResponseHandler::new().unwrap();
1068 assert!(handler.path_matches("/api/*", "/api/anything"));
1069 assert!(handler.path_matches("/api/*", "/api/users/123"));
1070 }
1071
1072 #[tokio::test]
1077 async fn test_extract_resource_id_from_path() {
1078 let handler = StatefulResponseHandler::new().unwrap();
1079 let extract = ResourceIdExtract::PathParam {
1080 param: "order_id".to_string(),
1081 };
1082 let uri: Uri = "/orders/12345".parse().unwrap();
1083 let headers = HeaderMap::new();
1084
1085 let id = handler.extract_resource_id(&extract, &uri, &headers, None).unwrap();
1086 assert_eq!(id, "12345");
1087 }
1088
1089 #[tokio::test]
1090 async fn test_extract_resource_id_from_header() {
1091 let handler = StatefulResponseHandler::new().unwrap();
1092 let extract = ResourceIdExtract::Header {
1093 name: "x-order-id".to_string(),
1094 };
1095 let uri: Uri = "/orders".parse().unwrap();
1096 let mut headers = HeaderMap::new();
1097 headers.insert("x-order-id", "order-abc".parse().unwrap());
1098
1099 let id = handler.extract_resource_id(&extract, &uri, &headers, None).unwrap();
1100 assert_eq!(id, "order-abc");
1101 }
1102
1103 #[tokio::test]
1104 async fn test_extract_resource_id_from_query_param() {
1105 let handler = StatefulResponseHandler::new().unwrap();
1106 let extract = ResourceIdExtract::QueryParam {
1107 param: "id".to_string(),
1108 };
1109 let uri: Uri = "/orders?id=query-123".parse().unwrap();
1110 let headers = HeaderMap::new();
1111
1112 let id = handler.extract_resource_id(&extract, &uri, &headers, None).unwrap();
1113 assert_eq!(id, "query-123");
1114 }
1115
1116 #[tokio::test]
1117 async fn test_extract_resource_id_from_json_body() {
1118 let handler = StatefulResponseHandler::new().unwrap();
1119 let extract = ResourceIdExtract::JsonPath {
1120 path: "$.order.id".to_string(),
1121 };
1122 let uri: Uri = "/orders".parse().unwrap();
1123 let headers = HeaderMap::new();
1124 let body = br#"{"order": {"id": "json-456"}}"#;
1125
1126 let id = handler.extract_resource_id(&extract, &uri, &headers, Some(body)).unwrap();
1127 assert_eq!(id, "json-456");
1128 }
1129
1130 #[tokio::test]
1131 async fn test_extract_resource_id_composite() {
1132 let handler = StatefulResponseHandler::new().unwrap();
1133 let extract = ResourceIdExtract::Composite {
1134 extractors: vec![
1135 ResourceIdExtract::Header {
1136 name: "x-id".to_string(),
1137 },
1138 ResourceIdExtract::PathParam {
1139 param: "id".to_string(),
1140 },
1141 ],
1142 };
1143 let uri: Uri = "/orders/fallback-123".parse().unwrap();
1144 let headers = HeaderMap::new(); let id = handler.extract_resource_id(&extract, &uri, &headers, None).unwrap();
1147 assert_eq!(id, "fallback-123");
1148 }
1149
1150 #[tokio::test]
1151 async fn test_extract_resource_id_header_not_found() {
1152 let handler = StatefulResponseHandler::new().unwrap();
1153 let extract = ResourceIdExtract::Header {
1154 name: "x-missing".to_string(),
1155 };
1156 let uri: Uri = "/orders".parse().unwrap();
1157 let headers = HeaderMap::new();
1158
1159 let result = handler.extract_resource_id(&extract, &uri, &headers, None);
1160 assert!(result.is_err());
1161 }
1162
1163 #[test]
1168 fn test_extract_json_path_simple() {
1169 let handler = StatefulResponseHandler::new().unwrap();
1170 let json: Value = serde_json::json!({"id": "123"});
1171 let result = handler.extract_json_path(&json, "$.id").unwrap();
1172 assert_eq!(result, "123");
1173 }
1174
1175 #[test]
1176 fn test_extract_json_path_nested() {
1177 let handler = StatefulResponseHandler::new().unwrap();
1178 let json: Value = serde_json::json!({"order": {"details": {"id": "456"}}});
1179 let result = handler.extract_json_path(&json, "$.order.details.id").unwrap();
1180 assert_eq!(result, "456");
1181 }
1182
1183 #[test]
1184 fn test_extract_json_path_number() {
1185 let handler = StatefulResponseHandler::new().unwrap();
1186 let json: Value = serde_json::json!({"count": 42});
1187 let result = handler.extract_json_path(&json, "$.count").unwrap();
1188 assert_eq!(result, "42");
1189 }
1190
1191 #[test]
1192 fn test_extract_json_path_array_index() {
1193 let handler = StatefulResponseHandler::new().unwrap();
1194 let json: Value = serde_json::json!({"items": ["a", "b", "c"]});
1195 let result = handler.extract_json_path(&json, "$.items.1").unwrap();
1196 assert_eq!(result, "b");
1197 }
1198
1199 #[test]
1200 fn test_extract_json_path_not_found() {
1201 let handler = StatefulResponseHandler::new().unwrap();
1202 let json: Value = serde_json::json!({"other": "value"});
1203 let result = handler.extract_json_path(&json, "$.missing");
1204 assert!(result.is_err());
1205 }
1206
1207 #[test]
1212 fn test_render_body_template_state() {
1213 let handler = StatefulResponseHandler::new().unwrap();
1214 let instance =
1215 StateInstance::new("order-123".to_string(), "order".to_string(), "pending".to_string());
1216 let template = r#"{"status": "{{state}}"}"#;
1217 let result = handler.render_body_template(template, &instance).unwrap();
1218 assert_eq!(result, r#"{"status": "pending"}"#);
1219 }
1220
1221 #[test]
1222 fn test_render_body_template_resource_id() {
1223 let handler = StatefulResponseHandler::new().unwrap();
1224 let instance =
1225 StateInstance::new("order-456".to_string(), "order".to_string(), "shipped".to_string());
1226 let template = r#"{"id": "{{resource_id}}"}"#;
1227 let result = handler.render_body_template(template, &instance).unwrap();
1228 assert_eq!(result, r#"{"id": "order-456"}"#);
1229 }
1230
1231 #[test]
1232 fn test_render_body_template_state_data() {
1233 let handler = StatefulResponseHandler::new().unwrap();
1234 let mut instance =
1235 StateInstance::new("order-789".to_string(), "order".to_string(), "shipped".to_string());
1236 instance
1237 .state_data
1238 .insert("carrier".to_string(), Value::String("FedEx".to_string()));
1239 let template = r#"{"carrier": "{{state_data.carrier}}"}"#;
1240 let result = handler.render_body_template(template, &instance).unwrap();
1241 assert_eq!(result, r#"{"carrier": "FedEx"}"#);
1242 }
1243
1244 #[test]
1245 fn test_render_body_template_multiple_placeholders() {
1246 let handler = StatefulResponseHandler::new().unwrap();
1247 let instance = StateInstance::new(
1248 "order-abc".to_string(),
1249 "order".to_string(),
1250 "confirmed".to_string(),
1251 );
1252 let template = r#"{"id": "{{resource_id}}", "status": "{{state}}"}"#;
1253 let result = handler.render_body_template(template, &instance).unwrap();
1254 assert_eq!(result, r#"{"id": "order-abc", "status": "confirmed"}"#);
1255 }
1256
1257 #[tokio::test]
1262 async fn test_process_request_no_config() {
1263 let handler = StatefulResponseHandler::new().unwrap();
1264 let uri: Uri = "/orders/123".parse().unwrap();
1265 let headers = HeaderMap::new();
1266
1267 let result = handler.process_request(&Method::GET, &uri, &headers, None).await.unwrap();
1268 assert!(result.is_none());
1269 }
1270
1271 #[tokio::test]
1272 async fn test_process_request_with_config() {
1273 let handler = StatefulResponseHandler::new().unwrap();
1274 let mut state_responses = HashMap::new();
1275 state_responses.insert(
1276 "initial".to_string(),
1277 StateResponse {
1278 status_code: 200,
1279 headers: HashMap::new(),
1280 body_template: r#"{"state": "{{state}}", "id": "{{resource_id}}"}"#.to_string(),
1281 content_type: "application/json".to_string(),
1282 },
1283 );
1284
1285 let config = StatefulConfig {
1286 resource_id_extract: ResourceIdExtract::PathParam {
1287 param: "id".to_string(),
1288 },
1289 resource_type: "order".to_string(),
1290 state_responses,
1291 transitions: vec![],
1292 };
1293
1294 handler.add_config("/orders/{id}".to_string(), config).await;
1295
1296 let uri: Uri = "/orders/test-123".parse().unwrap();
1297 let headers = HeaderMap::new();
1298
1299 let result = handler.process_request(&Method::GET, &uri, &headers, None).await.unwrap();
1300 assert!(result.is_some());
1301
1302 let response = result.unwrap();
1303 assert_eq!(response.status_code, 200);
1304 assert_eq!(response.state, "initial");
1305 assert_eq!(response.resource_id, "test-123");
1306 assert!(response.body.contains("test-123"));
1307 }
1308
1309 #[tokio::test]
1310 async fn test_process_request_with_transition() {
1311 let handler = StatefulResponseHandler::new().unwrap();
1312 let mut state_responses = HashMap::new();
1313 state_responses.insert(
1314 "initial".to_string(),
1315 StateResponse {
1316 status_code: 200,
1317 headers: HashMap::new(),
1318 body_template: r#"{"state": "{{state}}"}"#.to_string(),
1319 content_type: "application/json".to_string(),
1320 },
1321 );
1322 state_responses.insert(
1323 "confirmed".to_string(),
1324 StateResponse {
1325 status_code: 200,
1326 headers: HashMap::new(),
1327 body_template: r#"{"state": "{{state}}"}"#.to_string(),
1328 content_type: "application/json".to_string(),
1329 },
1330 );
1331
1332 let config = StatefulConfig {
1333 resource_id_extract: ResourceIdExtract::PathParam {
1334 param: "id".to_string(),
1335 },
1336 resource_type: "order".to_string(),
1337 state_responses,
1338 transitions: vec![TransitionTrigger {
1339 method: Method::POST,
1340 path_pattern: "/orders/{id}".to_string(),
1341 from_state: "initial".to_string(),
1342 to_state: "confirmed".to_string(),
1343 condition: None,
1344 }],
1345 };
1346
1347 handler.add_config("/orders/{id}".to_string(), config).await;
1348
1349 let uri: Uri = "/orders/order-1".parse().unwrap();
1351 let headers = HeaderMap::new();
1352
1353 let result = handler.process_request(&Method::GET, &uri, &headers, None).await.unwrap();
1354 assert_eq!(result.unwrap().state, "initial");
1355
1356 let result = handler.process_request(&Method::POST, &uri, &headers, None).await.unwrap();
1358 assert_eq!(result.unwrap().state, "confirmed");
1359
1360 let result = handler.process_request(&Method::GET, &uri, &headers, None).await.unwrap();
1362 assert_eq!(result.unwrap().state, "confirmed");
1363 }
1364
1365 #[tokio::test]
1370 async fn test_process_stub_state() {
1371 let handler = StatefulResponseHandler::new().unwrap();
1372 let extract = ResourceIdExtract::PathParam {
1373 param: "id".to_string(),
1374 };
1375 let uri: Uri = "/users/user-123".parse().unwrap();
1376 let headers = HeaderMap::new();
1377
1378 let result = handler
1379 .process_stub_state(
1380 &Method::GET,
1381 &uri,
1382 &headers,
1383 None,
1384 "user",
1385 &extract,
1386 "active",
1387 None,
1388 )
1389 .await
1390 .unwrap();
1391
1392 assert!(result.is_some());
1393 let info = result.unwrap();
1394 assert_eq!(info.resource_id, "user-123");
1395 assert_eq!(info.current_state, "active");
1396 }
1397
1398 #[tokio::test]
1399 async fn test_process_stub_state_with_transition() {
1400 let handler = StatefulResponseHandler::new().unwrap();
1401 let extract = ResourceIdExtract::PathParam {
1402 param: "id".to_string(),
1403 };
1404 let uri: Uri = "/users/user-456".parse().unwrap();
1405 let headers = HeaderMap::new();
1406
1407 let _ = handler
1409 .process_stub_state(
1410 &Method::GET,
1411 &uri,
1412 &headers,
1413 None,
1414 "user",
1415 &extract,
1416 "active",
1417 None,
1418 )
1419 .await
1420 .unwrap();
1421
1422 let transitions = vec![TransitionTrigger {
1424 method: Method::DELETE,
1425 path_pattern: "/users/{id}".to_string(),
1426 from_state: "active".to_string(),
1427 to_state: "deleted".to_string(),
1428 condition: None,
1429 }];
1430
1431 let result = handler
1432 .process_stub_state(
1433 &Method::DELETE,
1434 &uri,
1435 &headers,
1436 None,
1437 "user",
1438 &extract,
1439 "active",
1440 Some(&transitions),
1441 )
1442 .await
1443 .unwrap();
1444
1445 let info = result.unwrap();
1446 assert_eq!(info.current_state, "deleted");
1447 }
1448
1449 #[tokio::test]
1454 async fn test_get_resource_state_not_found() {
1455 let handler = StatefulResponseHandler::new().unwrap();
1456 let result = handler.get_resource_state("nonexistent", "order").await.unwrap();
1457 assert!(result.is_none());
1458 }
1459
1460 #[tokio::test]
1461 async fn test_get_resource_state_exists() {
1462 let handler = StatefulResponseHandler::new().unwrap();
1463
1464 let extract = ResourceIdExtract::PathParam {
1466 param: "id".to_string(),
1467 };
1468 let uri: Uri = "/orders/order-999".parse().unwrap();
1469 let headers = HeaderMap::new();
1470
1471 handler
1472 .process_stub_state(
1473 &Method::GET,
1474 &uri,
1475 &headers,
1476 None,
1477 "order",
1478 &extract,
1479 "pending",
1480 None,
1481 )
1482 .await
1483 .unwrap();
1484
1485 let result = handler.get_resource_state("order-999", "order").await.unwrap();
1487 assert!(result.is_some());
1488 assert_eq!(result.unwrap().current_state, "pending");
1489 }
1490
1491 #[tokio::test]
1492 async fn test_update_resource_state() {
1493 let handler = StatefulResponseHandler::new().unwrap();
1494
1495 let extract = ResourceIdExtract::PathParam {
1497 param: "id".to_string(),
1498 };
1499 let uri: Uri = "/orders/order-update".parse().unwrap();
1500 let headers = HeaderMap::new();
1501
1502 handler
1503 .process_stub_state(
1504 &Method::GET,
1505 &uri,
1506 &headers,
1507 None,
1508 "order",
1509 &extract,
1510 "pending",
1511 None,
1512 )
1513 .await
1514 .unwrap();
1515
1516 handler.update_resource_state("order-update", "order", "shipped").await.unwrap();
1518
1519 let result = handler.get_resource_state("order-update", "order").await.unwrap();
1521 assert_eq!(result.unwrap().current_state, "shipped");
1522 }
1523
1524 #[tokio::test]
1525 async fn test_update_resource_state_not_found() {
1526 let handler = StatefulResponseHandler::new().unwrap();
1527 let result = handler.update_resource_state("nonexistent", "order", "shipped").await;
1528 assert!(result.is_err());
1529 }
1530
1531 #[tokio::test]
1532 async fn test_update_resource_state_wrong_type() {
1533 let handler = StatefulResponseHandler::new().unwrap();
1534
1535 let extract = ResourceIdExtract::PathParam {
1537 param: "id".to_string(),
1538 };
1539 let uri: Uri = "/orders/order-type-test".parse().unwrap();
1540 let headers = HeaderMap::new();
1541
1542 handler
1543 .process_stub_state(
1544 &Method::GET,
1545 &uri,
1546 &headers,
1547 None,
1548 "order",
1549 &extract,
1550 "pending",
1551 None,
1552 )
1553 .await
1554 .unwrap();
1555
1556 let result = handler.update_resource_state("order-type-test", "user", "active").await;
1558 assert!(result.is_err());
1559 }
1560
1561 #[test]
1566 fn test_evaluate_condition_json_path_truthy() {
1567 let handler = StatefulResponseHandler::new().unwrap();
1568 let headers = HeaderMap::new();
1569 let body = br#"{"verified": "true"}"#;
1570 let result = handler.evaluate_condition("$.verified", &headers, Some(body)).unwrap();
1571 assert!(result);
1572 }
1573
1574 #[test]
1575 fn test_evaluate_condition_json_path_falsy() {
1576 let handler = StatefulResponseHandler::new().unwrap();
1577 let headers = HeaderMap::new();
1578 let body = br#"{"verified": "false"}"#;
1579 let result = handler.evaluate_condition("$.verified", &headers, Some(body)).unwrap();
1580 assert!(!result);
1581 }
1582
1583 #[test]
1584 fn test_evaluate_condition_non_jsonpath() {
1585 let handler = StatefulResponseHandler::new().unwrap();
1586 let headers = HeaderMap::new();
1587 let result = handler.evaluate_condition("some_condition", &headers, None).unwrap();
1588 assert!(result); }
1590
1591 #[test]
1596 fn test_state_info_clone() {
1597 let info = StateInfo {
1598 resource_id: "res-1".to_string(),
1599 current_state: "active".to_string(),
1600 state_data: HashMap::new(),
1601 };
1602 let cloned = info.clone();
1603 assert_eq!(cloned.resource_id, "res-1");
1604 }
1605
1606 #[test]
1607 fn test_state_info_debug() {
1608 let info = StateInfo {
1609 resource_id: "res-2".to_string(),
1610 current_state: "inactive".to_string(),
1611 state_data: HashMap::new(),
1612 };
1613 let debug_str = format!("{:?}", info);
1614 assert!(debug_str.contains("res-2"));
1615 assert!(debug_str.contains("inactive"));
1616 }
1617
1618 #[test]
1623 fn test_stateful_response_clone() {
1624 let response = StatefulResponse {
1625 status_code: 200,
1626 headers: HashMap::new(),
1627 body: "{}".to_string(),
1628 content_type: "application/json".to_string(),
1629 state: "active".to_string(),
1630 resource_id: "res-3".to_string(),
1631 };
1632 let cloned = response.clone();
1633 assert_eq!(cloned.status_code, 200);
1634 assert_eq!(cloned.state, "active");
1635 }
1636
1637 #[test]
1638 fn test_stateful_response_debug() {
1639 let response = StatefulResponse {
1640 status_code: 404,
1641 headers: HashMap::new(),
1642 body: "Not found".to_string(),
1643 content_type: "text/plain".to_string(),
1644 state: "deleted".to_string(),
1645 resource_id: "res-4".to_string(),
1646 };
1647 let debug_str = format!("{:?}", response);
1648 assert!(debug_str.contains("404"));
1649 assert!(debug_str.contains("deleted"));
1650 }
1651}