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 #[allow(clippy::too_many_arguments)]
485 pub async fn process_stub_state(
486 &self,
487 method: &Method,
488 uri: &Uri,
489 headers: &HeaderMap,
490 body: Option<&[u8]>,
491 resource_type: &str,
492 resource_id_extract: &ResourceIdExtract,
493 initial_state: &str,
494 transitions: Option<&[TransitionTrigger]>,
495 ) -> Result<Option<StateInfo>> {
496 let resource_id = self.extract_resource_id(resource_id_extract, uri, headers, body)?;
498
499 let state_instance = self
501 .state_manager
502 .get_or_create_instance(
503 resource_id.clone(),
504 resource_type.to_string(),
505 initial_state.to_string(),
506 )
507 .await?;
508
509 let new_state = if let Some(transition_list) = transitions {
511 let path = uri.path();
512 let mut transitioned_state = None;
515
516 for transition in transition_list {
517 if transition.method != *method {
519 continue;
520 }
521
522 if !self.path_matches(&transition.path_pattern, path) {
523 continue;
524 }
525
526 if state_instance.current_state != transition.from_state {
528 continue;
529 }
530
531 if let Some(ref condition) = transition.condition {
533 if !self.evaluate_condition(condition, headers, body)? {
534 continue;
535 }
536 }
537
538 debug!(
540 "State transition triggered in stub processing: {} -> {} for resource {}",
541 transition.from_state, transition.to_state, resource_id
542 );
543
544 transitioned_state = Some(transition.to_state.clone());
545 break; }
547
548 transitioned_state
549 } else {
550 None
551 };
552
553 let final_state = if let Some(ref new_state) = new_state {
555 let mut updated_instance = state_instance.clone();
556 updated_instance.transition_to(new_state.clone());
557 self.state_manager
558 .update_instance(resource_id.clone(), updated_instance)
559 .await?;
560 new_state.clone()
561 } else {
562 state_instance.current_state.clone()
563 };
564
565 Ok(Some(StateInfo {
566 resource_id: resource_id.clone(),
567 current_state: final_state,
568 state_data: state_instance.state_data.clone(),
569 }))
570 }
571
572 pub async fn update_resource_state(
574 &self,
575 resource_id: &str,
576 resource_type: &str,
577 new_state: &str,
578 ) -> Result<()> {
579 let mut instances = self.state_manager.instances.write().await;
580 if let Some(instance) = instances.get_mut(resource_id) {
581 if instance.resource_type == resource_type {
582 instance.transition_to(new_state.to_string());
583 return Ok(());
584 }
585 }
586 Err(Error::generic(format!(
587 "Resource '{}' of type '{}' not found",
588 resource_id, resource_type
589 )))
590 }
591
592 pub async fn get_resource_state(
594 &self,
595 resource_id: &str,
596 resource_type: &str,
597 ) -> Result<Option<StateInfo>> {
598 let instances = self.state_manager.instances.read().await;
599 if let Some(instance) = instances.get(resource_id) {
600 if instance.resource_type == resource_type {
601 return Ok(Some(StateInfo {
602 resource_id: resource_id.to_string(),
603 current_state: instance.current_state.clone(),
604 state_data: instance.state_data.clone(),
605 }));
606 }
607 }
608 Ok(None)
609 }
610
611 fn path_matches(&self, pattern: &str, path: &str) -> bool {
613 let pattern_regex = pattern.replace("{", "(?P<").replace("}", ">[^/]+)").replace("*", ".*");
615 let regex = regex::Regex::new(&format!("^{}$", pattern_regex));
616 match regex {
617 Ok(re) => re.is_match(path),
618 Err(_) => pattern == path, }
620 }
621}
622
623#[derive(Debug, Clone)]
625pub struct StateInfo {
626 pub resource_id: String,
628 pub current_state: String,
630 pub state_data: HashMap<String, Value>,
632}
633
634#[derive(Debug, Clone)]
636pub struct StatefulResponse {
637 pub status_code: u16,
639 pub headers: HashMap<String, String>,
641 pub body: String,
643 pub content_type: String,
645 pub state: String,
647 pub resource_id: String,
649}
650
651#[cfg(test)]
652mod tests {
653 use super::*;
654
655 #[test]
660 fn test_state_instance_new() {
661 let instance =
662 StateInstance::new("order-123".to_string(), "order".to_string(), "pending".to_string());
663 assert_eq!(instance.resource_id, "order-123");
664 assert_eq!(instance.resource_type, "order");
665 assert_eq!(instance.current_state, "pending");
666 assert!(instance.state_data.is_empty());
667 }
668
669 #[test]
670 fn test_state_instance_transition_to() {
671 let mut instance =
672 StateInstance::new("order-123".to_string(), "order".to_string(), "pending".to_string());
673 instance.transition_to("confirmed".to_string());
674 assert_eq!(instance.current_state, "confirmed");
675
676 instance.transition_to("shipped".to_string());
677 assert_eq!(instance.current_state, "shipped");
678 }
679
680 #[test]
681 fn test_state_instance_clone() {
682 let instance =
683 StateInstance::new("order-123".to_string(), "order".to_string(), "pending".to_string());
684 let cloned = instance.clone();
685 assert_eq!(cloned.resource_id, instance.resource_id);
686 assert_eq!(cloned.current_state, instance.current_state);
687 }
688
689 #[test]
690 fn test_state_instance_debug() {
691 let instance =
692 StateInstance::new("order-123".to_string(), "order".to_string(), "pending".to_string());
693 let debug_str = format!("{:?}", instance);
694 assert!(debug_str.contains("order-123"));
695 assert!(debug_str.contains("pending"));
696 }
697
698 #[tokio::test]
703 async fn test_state_machine_manager_new() {
704 let manager = StateMachineManager::new();
705 let instances = manager.instances.read().await;
706 assert!(instances.is_empty());
707 }
708
709 #[tokio::test]
710 async fn test_state_machine_manager_get_or_create_new() {
711 let manager = StateMachineManager::new();
712 let instance = manager
713 .get_or_create_instance(
714 "order-123".to_string(),
715 "order".to_string(),
716 "pending".to_string(),
717 )
718 .await
719 .unwrap();
720 assert_eq!(instance.resource_id, "order-123");
721 assert_eq!(instance.current_state, "pending");
722 }
723
724 #[tokio::test]
725 async fn test_state_machine_manager_get_or_create_existing() {
726 let manager = StateMachineManager::new();
727
728 let instance1 = manager
730 .get_or_create_instance(
731 "order-123".to_string(),
732 "order".to_string(),
733 "pending".to_string(),
734 )
735 .await
736 .unwrap();
737 assert_eq!(instance1.current_state, "pending");
738
739 let instance2 = manager
741 .get_or_create_instance(
742 "order-123".to_string(),
743 "order".to_string(),
744 "confirmed".to_string(), )
746 .await
747 .unwrap();
748 assert_eq!(instance2.current_state, "pending"); }
750
751 #[tokio::test]
752 async fn test_state_machine_manager_update_instance() {
753 let manager = StateMachineManager::new();
754
755 let mut instance = manager
757 .get_or_create_instance(
758 "order-123".to_string(),
759 "order".to_string(),
760 "pending".to_string(),
761 )
762 .await
763 .unwrap();
764
765 instance.transition_to("confirmed".to_string());
767 manager.update_instance("order-123".to_string(), instance).await.unwrap();
768
769 let updated = manager
771 .get_or_create_instance(
772 "order-123".to_string(),
773 "order".to_string(),
774 "pending".to_string(),
775 )
776 .await
777 .unwrap();
778 assert_eq!(updated.current_state, "confirmed");
779 }
780
781 #[test]
786 fn test_stateful_config_serialize_deserialize() {
787 let config = StatefulConfig {
788 resource_id_extract: ResourceIdExtract::PathParam {
789 param: "order_id".to_string(),
790 },
791 resource_type: "order".to_string(),
792 state_responses: {
793 let mut map = HashMap::new();
794 map.insert(
795 "pending".to_string(),
796 StateResponse {
797 status_code: 200,
798 headers: HashMap::new(),
799 body_template: "{\"status\": \"pending\"}".to_string(),
800 content_type: "application/json".to_string(),
801 },
802 );
803 map
804 },
805 transitions: vec![],
806 };
807
808 let json = serde_json::to_string(&config).unwrap();
809 let deserialized: StatefulConfig = serde_json::from_str(&json).unwrap();
810 assert_eq!(deserialized.resource_type, "order");
811 }
812
813 #[test]
814 fn test_stateful_config_debug() {
815 let config = StatefulConfig {
816 resource_id_extract: ResourceIdExtract::PathParam {
817 param: "order_id".to_string(),
818 },
819 resource_type: "order".to_string(),
820 state_responses: HashMap::new(),
821 transitions: vec![],
822 };
823 let debug_str = format!("{:?}", config);
824 assert!(debug_str.contains("order"));
825 }
826
827 #[test]
828 fn test_stateful_config_clone() {
829 let config = StatefulConfig {
830 resource_id_extract: ResourceIdExtract::PathParam {
831 param: "id".to_string(),
832 },
833 resource_type: "user".to_string(),
834 state_responses: HashMap::new(),
835 transitions: vec![],
836 };
837 let cloned = config.clone();
838 assert_eq!(cloned.resource_type, "user");
839 }
840
841 #[test]
846 fn test_resource_id_extract_path_param() {
847 let extract = ResourceIdExtract::PathParam {
848 param: "order_id".to_string(),
849 };
850 let json = serde_json::to_string(&extract).unwrap();
851 assert!(json.contains("path_param"));
852 }
853
854 #[test]
855 fn test_resource_id_extract_json_path() {
856 let extract = ResourceIdExtract::JsonPath {
857 path: "$.order.id".to_string(),
858 };
859 let json = serde_json::to_string(&extract).unwrap();
860 assert!(json.contains("json_path"));
861 }
862
863 #[test]
864 fn test_resource_id_extract_header() {
865 let extract = ResourceIdExtract::Header {
866 name: "X-Order-ID".to_string(),
867 };
868 let json = serde_json::to_string(&extract).unwrap();
869 assert!(json.contains("header"));
870 }
871
872 #[test]
873 fn test_resource_id_extract_query_param() {
874 let extract = ResourceIdExtract::QueryParam {
875 param: "order_id".to_string(),
876 };
877 let json = serde_json::to_string(&extract).unwrap();
878 assert!(json.contains("query_param"));
879 }
880
881 #[test]
882 fn test_resource_id_extract_composite() {
883 let extract = ResourceIdExtract::Composite {
884 extractors: vec![
885 ResourceIdExtract::PathParam {
886 param: "id".to_string(),
887 },
888 ResourceIdExtract::Header {
889 name: "X-ID".to_string(),
890 },
891 ],
892 };
893 let json = serde_json::to_string(&extract).unwrap();
894 assert!(json.contains("composite"));
895 }
896
897 #[test]
902 fn test_state_response_serialize_deserialize() {
903 let response = StateResponse {
904 status_code: 200,
905 headers: {
906 let mut h = HashMap::new();
907 h.insert("X-State".to_string(), "pending".to_string());
908 h
909 },
910 body_template: "{\"state\": \"{{state}}\"}".to_string(),
911 content_type: "application/json".to_string(),
912 };
913 let json = serde_json::to_string(&response).unwrap();
914 let deserialized: StateResponse = serde_json::from_str(&json).unwrap();
915 assert_eq!(deserialized.status_code, 200);
916 assert_eq!(deserialized.content_type, "application/json");
917 }
918
919 #[test]
920 fn test_state_response_clone() {
921 let response = StateResponse {
922 status_code: 201,
923 headers: HashMap::new(),
924 body_template: "{}".to_string(),
925 content_type: "text/plain".to_string(),
926 };
927 let cloned = response.clone();
928 assert_eq!(cloned.status_code, 201);
929 }
930
931 #[test]
932 fn test_state_response_debug() {
933 let response = StateResponse {
934 status_code: 404,
935 headers: HashMap::new(),
936 body_template: "Not found".to_string(),
937 content_type: "text/plain".to_string(),
938 };
939 let debug_str = format!("{:?}", response);
940 assert!(debug_str.contains("404"));
941 }
942
943 #[test]
948 fn test_transition_trigger_serialize_deserialize() {
949 let trigger = TransitionTrigger {
950 method: Method::POST,
951 path_pattern: "/orders/{id}/confirm".to_string(),
952 from_state: "pending".to_string(),
953 to_state: "confirmed".to_string(),
954 condition: None,
955 };
956 let json = serde_json::to_string(&trigger).unwrap();
957 let deserialized: TransitionTrigger = serde_json::from_str(&json).unwrap();
958 assert_eq!(deserialized.from_state, "pending");
959 assert_eq!(deserialized.to_state, "confirmed");
960 }
961
962 #[test]
963 fn test_transition_trigger_with_condition() {
964 let trigger = TransitionTrigger {
965 method: Method::POST,
966 path_pattern: "/orders/{id}/ship".to_string(),
967 from_state: "confirmed".to_string(),
968 to_state: "shipped".to_string(),
969 condition: Some("$.payment.verified".to_string()),
970 };
971 let json = serde_json::to_string(&trigger).unwrap();
972 assert!(json.contains("payment.verified"));
973 }
974
975 #[test]
976 fn test_transition_trigger_clone() {
977 let trigger = TransitionTrigger {
978 method: Method::DELETE,
979 path_pattern: "/orders/{id}".to_string(),
980 from_state: "pending".to_string(),
981 to_state: "cancelled".to_string(),
982 condition: None,
983 };
984 let cloned = trigger.clone();
985 assert_eq!(cloned.method, Method::DELETE);
986 }
987
988 #[tokio::test]
993 async fn test_stateful_response_handler_new() {
994 let handler = StatefulResponseHandler::new().unwrap();
995 let configs = handler.configs.read().await;
996 assert!(configs.is_empty());
997 }
998
999 #[tokio::test]
1000 async fn test_stateful_response_handler_add_config() {
1001 let handler = StatefulResponseHandler::new().unwrap();
1002 let config = StatefulConfig {
1003 resource_id_extract: ResourceIdExtract::PathParam {
1004 param: "id".to_string(),
1005 },
1006 resource_type: "order".to_string(),
1007 state_responses: HashMap::new(),
1008 transitions: vec![],
1009 };
1010
1011 handler.add_config("/orders/{id}".to_string(), config).await;
1012
1013 let configs = handler.configs.read().await;
1014 assert!(configs.contains_key("/orders/{id}"));
1015 }
1016
1017 #[tokio::test]
1018 async fn test_stateful_response_handler_can_handle_true() {
1019 let handler = StatefulResponseHandler::new().unwrap();
1020 let config = StatefulConfig {
1021 resource_id_extract: ResourceIdExtract::PathParam {
1022 param: "id".to_string(),
1023 },
1024 resource_type: "order".to_string(),
1025 state_responses: HashMap::new(),
1026 transitions: vec![],
1027 };
1028
1029 handler.add_config("/orders/{id}".to_string(), config).await;
1030
1031 assert!(handler.can_handle(&Method::GET, "/orders/123").await);
1032 }
1033
1034 #[tokio::test]
1035 async fn test_stateful_response_handler_can_handle_false() {
1036 let handler = StatefulResponseHandler::new().unwrap();
1037 assert!(!handler.can_handle(&Method::GET, "/orders/123").await);
1038 }
1039
1040 #[test]
1045 fn test_path_matching() {
1046 let handler = StatefulResponseHandler::new().unwrap();
1047
1048 assert!(handler.path_matches("/orders/{id}", "/orders/123"));
1049 assert!(handler.path_matches("/api/*", "/api/users"));
1050 assert!(!handler.path_matches("/orders/{id}", "/orders/123/items"));
1051 }
1052
1053 #[test]
1054 fn test_path_matching_exact() {
1055 let handler = StatefulResponseHandler::new().unwrap();
1056 assert!(handler.path_matches("/api/health", "/api/health"));
1057 assert!(!handler.path_matches("/api/health", "/api/health/check"));
1058 }
1059
1060 #[test]
1061 fn test_path_matching_multiple_params() {
1062 let handler = StatefulResponseHandler::new().unwrap();
1063 assert!(handler.path_matches("/users/{user_id}/orders/{order_id}", "/users/1/orders/2"));
1064 }
1065
1066 #[test]
1067 fn test_path_matching_wildcard() {
1068 let handler = StatefulResponseHandler::new().unwrap();
1069 assert!(handler.path_matches("/api/*", "/api/anything"));
1070 assert!(handler.path_matches("/api/*", "/api/users/123"));
1071 }
1072
1073 #[tokio::test]
1078 async fn test_extract_resource_id_from_path() {
1079 let handler = StatefulResponseHandler::new().unwrap();
1080 let extract = ResourceIdExtract::PathParam {
1081 param: "order_id".to_string(),
1082 };
1083 let uri: Uri = "/orders/12345".parse().unwrap();
1084 let headers = HeaderMap::new();
1085
1086 let id = handler.extract_resource_id(&extract, &uri, &headers, None).unwrap();
1087 assert_eq!(id, "12345");
1088 }
1089
1090 #[tokio::test]
1091 async fn test_extract_resource_id_from_header() {
1092 let handler = StatefulResponseHandler::new().unwrap();
1093 let extract = ResourceIdExtract::Header {
1094 name: "x-order-id".to_string(),
1095 };
1096 let uri: Uri = "/orders".parse().unwrap();
1097 let mut headers = HeaderMap::new();
1098 headers.insert("x-order-id", "order-abc".parse().unwrap());
1099
1100 let id = handler.extract_resource_id(&extract, &uri, &headers, None).unwrap();
1101 assert_eq!(id, "order-abc");
1102 }
1103
1104 #[tokio::test]
1105 async fn test_extract_resource_id_from_query_param() {
1106 let handler = StatefulResponseHandler::new().unwrap();
1107 let extract = ResourceIdExtract::QueryParam {
1108 param: "id".to_string(),
1109 };
1110 let uri: Uri = "/orders?id=query-123".parse().unwrap();
1111 let headers = HeaderMap::new();
1112
1113 let id = handler.extract_resource_id(&extract, &uri, &headers, None).unwrap();
1114 assert_eq!(id, "query-123");
1115 }
1116
1117 #[tokio::test]
1118 async fn test_extract_resource_id_from_json_body() {
1119 let handler = StatefulResponseHandler::new().unwrap();
1120 let extract = ResourceIdExtract::JsonPath {
1121 path: "$.order.id".to_string(),
1122 };
1123 let uri: Uri = "/orders".parse().unwrap();
1124 let headers = HeaderMap::new();
1125 let body = br#"{"order": {"id": "json-456"}}"#;
1126
1127 let id = handler.extract_resource_id(&extract, &uri, &headers, Some(body)).unwrap();
1128 assert_eq!(id, "json-456");
1129 }
1130
1131 #[tokio::test]
1132 async fn test_extract_resource_id_composite() {
1133 let handler = StatefulResponseHandler::new().unwrap();
1134 let extract = ResourceIdExtract::Composite {
1135 extractors: vec![
1136 ResourceIdExtract::Header {
1137 name: "x-id".to_string(),
1138 },
1139 ResourceIdExtract::PathParam {
1140 param: "id".to_string(),
1141 },
1142 ],
1143 };
1144 let uri: Uri = "/orders/fallback-123".parse().unwrap();
1145 let headers = HeaderMap::new(); let id = handler.extract_resource_id(&extract, &uri, &headers, None).unwrap();
1148 assert_eq!(id, "fallback-123");
1149 }
1150
1151 #[tokio::test]
1152 async fn test_extract_resource_id_header_not_found() {
1153 let handler = StatefulResponseHandler::new().unwrap();
1154 let extract = ResourceIdExtract::Header {
1155 name: "x-missing".to_string(),
1156 };
1157 let uri: Uri = "/orders".parse().unwrap();
1158 let headers = HeaderMap::new();
1159
1160 let result = handler.extract_resource_id(&extract, &uri, &headers, None);
1161 assert!(result.is_err());
1162 }
1163
1164 #[test]
1169 fn test_extract_json_path_simple() {
1170 let handler = StatefulResponseHandler::new().unwrap();
1171 let json: Value = serde_json::json!({"id": "123"});
1172 let result = handler.extract_json_path(&json, "$.id").unwrap();
1173 assert_eq!(result, "123");
1174 }
1175
1176 #[test]
1177 fn test_extract_json_path_nested() {
1178 let handler = StatefulResponseHandler::new().unwrap();
1179 let json: Value = serde_json::json!({"order": {"details": {"id": "456"}}});
1180 let result = handler.extract_json_path(&json, "$.order.details.id").unwrap();
1181 assert_eq!(result, "456");
1182 }
1183
1184 #[test]
1185 fn test_extract_json_path_number() {
1186 let handler = StatefulResponseHandler::new().unwrap();
1187 let json: Value = serde_json::json!({"count": 42});
1188 let result = handler.extract_json_path(&json, "$.count").unwrap();
1189 assert_eq!(result, "42");
1190 }
1191
1192 #[test]
1193 fn test_extract_json_path_array_index() {
1194 let handler = StatefulResponseHandler::new().unwrap();
1195 let json: Value = serde_json::json!({"items": ["a", "b", "c"]});
1196 let result = handler.extract_json_path(&json, "$.items.1").unwrap();
1197 assert_eq!(result, "b");
1198 }
1199
1200 #[test]
1201 fn test_extract_json_path_not_found() {
1202 let handler = StatefulResponseHandler::new().unwrap();
1203 let json: Value = serde_json::json!({"other": "value"});
1204 let result = handler.extract_json_path(&json, "$.missing");
1205 assert!(result.is_err());
1206 }
1207
1208 #[test]
1213 fn test_render_body_template_state() {
1214 let handler = StatefulResponseHandler::new().unwrap();
1215 let instance =
1216 StateInstance::new("order-123".to_string(), "order".to_string(), "pending".to_string());
1217 let template = r#"{"status": "{{state}}"}"#;
1218 let result = handler.render_body_template(template, &instance).unwrap();
1219 assert_eq!(result, r#"{"status": "pending"}"#);
1220 }
1221
1222 #[test]
1223 fn test_render_body_template_resource_id() {
1224 let handler = StatefulResponseHandler::new().unwrap();
1225 let instance =
1226 StateInstance::new("order-456".to_string(), "order".to_string(), "shipped".to_string());
1227 let template = r#"{"id": "{{resource_id}}"}"#;
1228 let result = handler.render_body_template(template, &instance).unwrap();
1229 assert_eq!(result, r#"{"id": "order-456"}"#);
1230 }
1231
1232 #[test]
1233 fn test_render_body_template_state_data() {
1234 let handler = StatefulResponseHandler::new().unwrap();
1235 let mut instance =
1236 StateInstance::new("order-789".to_string(), "order".to_string(), "shipped".to_string());
1237 instance
1238 .state_data
1239 .insert("carrier".to_string(), Value::String("FedEx".to_string()));
1240 let template = r#"{"carrier": "{{state_data.carrier}}"}"#;
1241 let result = handler.render_body_template(template, &instance).unwrap();
1242 assert_eq!(result, r#"{"carrier": "FedEx"}"#);
1243 }
1244
1245 #[test]
1246 fn test_render_body_template_multiple_placeholders() {
1247 let handler = StatefulResponseHandler::new().unwrap();
1248 let instance = StateInstance::new(
1249 "order-abc".to_string(),
1250 "order".to_string(),
1251 "confirmed".to_string(),
1252 );
1253 let template = r#"{"id": "{{resource_id}}", "status": "{{state}}"}"#;
1254 let result = handler.render_body_template(template, &instance).unwrap();
1255 assert_eq!(result, r#"{"id": "order-abc", "status": "confirmed"}"#);
1256 }
1257
1258 #[tokio::test]
1263 async fn test_process_request_no_config() {
1264 let handler = StatefulResponseHandler::new().unwrap();
1265 let uri: Uri = "/orders/123".parse().unwrap();
1266 let headers = HeaderMap::new();
1267
1268 let result = handler.process_request(&Method::GET, &uri, &headers, None).await.unwrap();
1269 assert!(result.is_none());
1270 }
1271
1272 #[tokio::test]
1273 async fn test_process_request_with_config() {
1274 let handler = StatefulResponseHandler::new().unwrap();
1275 let mut state_responses = HashMap::new();
1276 state_responses.insert(
1277 "initial".to_string(),
1278 StateResponse {
1279 status_code: 200,
1280 headers: HashMap::new(),
1281 body_template: r#"{"state": "{{state}}", "id": "{{resource_id}}"}"#.to_string(),
1282 content_type: "application/json".to_string(),
1283 },
1284 );
1285
1286 let config = StatefulConfig {
1287 resource_id_extract: ResourceIdExtract::PathParam {
1288 param: "id".to_string(),
1289 },
1290 resource_type: "order".to_string(),
1291 state_responses,
1292 transitions: vec![],
1293 };
1294
1295 handler.add_config("/orders/{id}".to_string(), config).await;
1296
1297 let uri: Uri = "/orders/test-123".parse().unwrap();
1298 let headers = HeaderMap::new();
1299
1300 let result = handler.process_request(&Method::GET, &uri, &headers, None).await.unwrap();
1301 assert!(result.is_some());
1302
1303 let response = result.unwrap();
1304 assert_eq!(response.status_code, 200);
1305 assert_eq!(response.state, "initial");
1306 assert_eq!(response.resource_id, "test-123");
1307 assert!(response.body.contains("test-123"));
1308 }
1309
1310 #[tokio::test]
1311 async fn test_process_request_with_transition() {
1312 let handler = StatefulResponseHandler::new().unwrap();
1313 let mut state_responses = HashMap::new();
1314 state_responses.insert(
1315 "initial".to_string(),
1316 StateResponse {
1317 status_code: 200,
1318 headers: HashMap::new(),
1319 body_template: r#"{"state": "{{state}}"}"#.to_string(),
1320 content_type: "application/json".to_string(),
1321 },
1322 );
1323 state_responses.insert(
1324 "confirmed".to_string(),
1325 StateResponse {
1326 status_code: 200,
1327 headers: HashMap::new(),
1328 body_template: r#"{"state": "{{state}}"}"#.to_string(),
1329 content_type: "application/json".to_string(),
1330 },
1331 );
1332
1333 let config = StatefulConfig {
1334 resource_id_extract: ResourceIdExtract::PathParam {
1335 param: "id".to_string(),
1336 },
1337 resource_type: "order".to_string(),
1338 state_responses,
1339 transitions: vec![TransitionTrigger {
1340 method: Method::POST,
1341 path_pattern: "/orders/{id}".to_string(),
1342 from_state: "initial".to_string(),
1343 to_state: "confirmed".to_string(),
1344 condition: None,
1345 }],
1346 };
1347
1348 handler.add_config("/orders/{id}".to_string(), config).await;
1349
1350 let uri: Uri = "/orders/order-1".parse().unwrap();
1352 let headers = HeaderMap::new();
1353
1354 let result = handler.process_request(&Method::GET, &uri, &headers, None).await.unwrap();
1355 assert_eq!(result.unwrap().state, "initial");
1356
1357 let result = handler.process_request(&Method::POST, &uri, &headers, None).await.unwrap();
1359 assert_eq!(result.unwrap().state, "confirmed");
1360
1361 let result = handler.process_request(&Method::GET, &uri, &headers, None).await.unwrap();
1363 assert_eq!(result.unwrap().state, "confirmed");
1364 }
1365
1366 #[tokio::test]
1371 async fn test_process_stub_state() {
1372 let handler = StatefulResponseHandler::new().unwrap();
1373 let extract = ResourceIdExtract::PathParam {
1374 param: "id".to_string(),
1375 };
1376 let uri: Uri = "/users/user-123".parse().unwrap();
1377 let headers = HeaderMap::new();
1378
1379 let result = handler
1380 .process_stub_state(
1381 &Method::GET,
1382 &uri,
1383 &headers,
1384 None,
1385 "user",
1386 &extract,
1387 "active",
1388 None,
1389 )
1390 .await
1391 .unwrap();
1392
1393 assert!(result.is_some());
1394 let info = result.unwrap();
1395 assert_eq!(info.resource_id, "user-123");
1396 assert_eq!(info.current_state, "active");
1397 }
1398
1399 #[tokio::test]
1400 async fn test_process_stub_state_with_transition() {
1401 let handler = StatefulResponseHandler::new().unwrap();
1402 let extract = ResourceIdExtract::PathParam {
1403 param: "id".to_string(),
1404 };
1405 let uri: Uri = "/users/user-456".parse().unwrap();
1406 let headers = HeaderMap::new();
1407
1408 let _ = handler
1410 .process_stub_state(
1411 &Method::GET,
1412 &uri,
1413 &headers,
1414 None,
1415 "user",
1416 &extract,
1417 "active",
1418 None,
1419 )
1420 .await
1421 .unwrap();
1422
1423 let transitions = vec![TransitionTrigger {
1425 method: Method::DELETE,
1426 path_pattern: "/users/{id}".to_string(),
1427 from_state: "active".to_string(),
1428 to_state: "deleted".to_string(),
1429 condition: None,
1430 }];
1431
1432 let result = handler
1433 .process_stub_state(
1434 &Method::DELETE,
1435 &uri,
1436 &headers,
1437 None,
1438 "user",
1439 &extract,
1440 "active",
1441 Some(&transitions),
1442 )
1443 .await
1444 .unwrap();
1445
1446 let info = result.unwrap();
1447 assert_eq!(info.current_state, "deleted");
1448 }
1449
1450 #[tokio::test]
1455 async fn test_get_resource_state_not_found() {
1456 let handler = StatefulResponseHandler::new().unwrap();
1457 let result = handler.get_resource_state("nonexistent", "order").await.unwrap();
1458 assert!(result.is_none());
1459 }
1460
1461 #[tokio::test]
1462 async fn test_get_resource_state_exists() {
1463 let handler = StatefulResponseHandler::new().unwrap();
1464
1465 let extract = ResourceIdExtract::PathParam {
1467 param: "id".to_string(),
1468 };
1469 let uri: Uri = "/orders/order-999".parse().unwrap();
1470 let headers = HeaderMap::new();
1471
1472 handler
1473 .process_stub_state(
1474 &Method::GET,
1475 &uri,
1476 &headers,
1477 None,
1478 "order",
1479 &extract,
1480 "pending",
1481 None,
1482 )
1483 .await
1484 .unwrap();
1485
1486 let result = handler.get_resource_state("order-999", "order").await.unwrap();
1488 assert!(result.is_some());
1489 assert_eq!(result.unwrap().current_state, "pending");
1490 }
1491
1492 #[tokio::test]
1493 async fn test_update_resource_state() {
1494 let handler = StatefulResponseHandler::new().unwrap();
1495
1496 let extract = ResourceIdExtract::PathParam {
1498 param: "id".to_string(),
1499 };
1500 let uri: Uri = "/orders/order-update".parse().unwrap();
1501 let headers = HeaderMap::new();
1502
1503 handler
1504 .process_stub_state(
1505 &Method::GET,
1506 &uri,
1507 &headers,
1508 None,
1509 "order",
1510 &extract,
1511 "pending",
1512 None,
1513 )
1514 .await
1515 .unwrap();
1516
1517 handler.update_resource_state("order-update", "order", "shipped").await.unwrap();
1519
1520 let result = handler.get_resource_state("order-update", "order").await.unwrap();
1522 assert_eq!(result.unwrap().current_state, "shipped");
1523 }
1524
1525 #[tokio::test]
1526 async fn test_update_resource_state_not_found() {
1527 let handler = StatefulResponseHandler::new().unwrap();
1528 let result = handler.update_resource_state("nonexistent", "order", "shipped").await;
1529 assert!(result.is_err());
1530 }
1531
1532 #[tokio::test]
1533 async fn test_update_resource_state_wrong_type() {
1534 let handler = StatefulResponseHandler::new().unwrap();
1535
1536 let extract = ResourceIdExtract::PathParam {
1538 param: "id".to_string(),
1539 };
1540 let uri: Uri = "/orders/order-type-test".parse().unwrap();
1541 let headers = HeaderMap::new();
1542
1543 handler
1544 .process_stub_state(
1545 &Method::GET,
1546 &uri,
1547 &headers,
1548 None,
1549 "order",
1550 &extract,
1551 "pending",
1552 None,
1553 )
1554 .await
1555 .unwrap();
1556
1557 let result = handler.update_resource_state("order-type-test", "user", "active").await;
1559 assert!(result.is_err());
1560 }
1561
1562 #[test]
1567 fn test_evaluate_condition_json_path_truthy() {
1568 let handler = StatefulResponseHandler::new().unwrap();
1569 let headers = HeaderMap::new();
1570 let body = br#"{"verified": "true"}"#;
1571 let result = handler.evaluate_condition("$.verified", &headers, Some(body)).unwrap();
1572 assert!(result);
1573 }
1574
1575 #[test]
1576 fn test_evaluate_condition_json_path_falsy() {
1577 let handler = StatefulResponseHandler::new().unwrap();
1578 let headers = HeaderMap::new();
1579 let body = br#"{"verified": "false"}"#;
1580 let result = handler.evaluate_condition("$.verified", &headers, Some(body)).unwrap();
1581 assert!(!result);
1582 }
1583
1584 #[test]
1585 fn test_evaluate_condition_non_jsonpath() {
1586 let handler = StatefulResponseHandler::new().unwrap();
1587 let headers = HeaderMap::new();
1588 let result = handler.evaluate_condition("some_condition", &headers, None).unwrap();
1589 assert!(result); }
1591
1592 #[test]
1597 fn test_state_info_clone() {
1598 let info = StateInfo {
1599 resource_id: "res-1".to_string(),
1600 current_state: "active".to_string(),
1601 state_data: HashMap::new(),
1602 };
1603 let cloned = info.clone();
1604 assert_eq!(cloned.resource_id, "res-1");
1605 }
1606
1607 #[test]
1608 fn test_state_info_debug() {
1609 let info = StateInfo {
1610 resource_id: "res-2".to_string(),
1611 current_state: "inactive".to_string(),
1612 state_data: HashMap::new(),
1613 };
1614 let debug_str = format!("{:?}", info);
1615 assert!(debug_str.contains("res-2"));
1616 assert!(debug_str.contains("inactive"));
1617 }
1618
1619 #[test]
1624 fn test_stateful_response_clone() {
1625 let response = StatefulResponse {
1626 status_code: 200,
1627 headers: HashMap::new(),
1628 body: "{}".to_string(),
1629 content_type: "application/json".to_string(),
1630 state: "active".to_string(),
1631 resource_id: "res-3".to_string(),
1632 };
1633 let cloned = response.clone();
1634 assert_eq!(cloned.status_code, 200);
1635 assert_eq!(cloned.state, "active");
1636 }
1637
1638 #[test]
1639 fn test_stateful_response_debug() {
1640 let response = StatefulResponse {
1641 status_code: 404,
1642 headers: HashMap::new(),
1643 body: "Not found".to_string(),
1644 content_type: "text/plain".to_string(),
1645 state: "deleted".to_string(),
1646 resource_id: "res-4".to_string(),
1647 };
1648 let debug_str = format!("{:?}", response);
1649 assert!(debug_str.contains("404"));
1650 assert!(debug_str.contains("deleted"));
1651 }
1652}