1use crate::error::{BenchError, Result};
8use crate::spec_parser::ApiOperation;
9use serde::{Deserialize, Deserializer, Serialize};
10use std::collections::{HashMap, HashSet};
11use std::path::Path;
12
13#[derive(Debug, Clone, Serialize, PartialEq)]
15pub struct ExtractField {
16 pub field: String,
18 pub store_as: String,
21}
22
23impl ExtractField {
24 pub fn simple(field: String) -> Self {
26 Self {
27 store_as: field.clone(),
28 field,
29 }
30 }
31
32 pub fn aliased(field: String, store_as: String) -> Self {
34 Self { field, store_as }
35 }
36}
37
38impl<'de> Deserialize<'de> for ExtractField {
39 fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
40 where
41 D: Deserializer<'de>,
42 {
43 use serde::de::{self, MapAccess, Visitor};
44
45 struct ExtractFieldVisitor;
46
47 impl<'de> Visitor<'de> for ExtractFieldVisitor {
48 type Value = ExtractField;
49
50 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
51 formatter.write_str("a string or an object with 'field' and optional 'as' keys")
52 }
53
54 fn visit_str<E>(self, value: &str) -> std::result::Result<Self::Value, E>
56 where
57 E: de::Error,
58 {
59 Ok(ExtractField::simple(value.to_string()))
60 }
61
62 fn visit_map<M>(self, mut map: M) -> std::result::Result<Self::Value, M::Error>
64 where
65 M: MapAccess<'de>,
66 {
67 let mut field: Option<String> = None;
68 let mut store_as: Option<String> = None;
69
70 while let Some(key) = map.next_key::<String>()? {
71 match key.as_str() {
72 "field" => {
73 field = Some(map.next_value()?);
74 }
75 "as" => {
76 store_as = Some(map.next_value()?);
77 }
78 _ => {
79 let _: serde::de::IgnoredAny = map.next_value()?;
80 }
81 }
82 }
83
84 let field = field.ok_or_else(|| de::Error::missing_field("field"))?;
85 let store_as = store_as.unwrap_or_else(|| field.clone());
86
87 Ok(ExtractField { field, store_as })
88 }
89 }
90
91 deserializer.deserialize_any(ExtractFieldVisitor)
92 }
93}
94
95#[derive(Debug, Clone, Serialize, Deserialize)]
97pub struct FlowStep {
98 pub operation: String,
100 #[serde(default)]
105 pub extract: Vec<ExtractField>,
106 #[serde(default)]
109 pub use_values: HashMap<String, String>,
110 #[serde(skip_serializing_if = "Option::is_none")]
112 pub description: Option<String>,
113}
114
115impl FlowStep {
116 pub fn new(operation: String) -> Self {
118 Self {
119 operation,
120 extract: Vec::new(),
121 use_values: HashMap::new(),
122 description: None,
123 }
124 }
125
126 pub fn with_extract(mut self, fields: Vec<String>) -> Self {
128 self.extract = fields.into_iter().map(ExtractField::simple).collect();
129 self
130 }
131
132 pub fn with_extract_fields(mut self, fields: Vec<ExtractField>) -> Self {
134 self.extract = fields;
135 self
136 }
137
138 pub fn with_values(mut self, values: HashMap<String, String>) -> Self {
140 self.use_values = values;
141 self
142 }
143
144 pub fn with_description(mut self, description: String) -> Self {
146 self.description = Some(description);
147 self
148 }
149}
150
151#[derive(Debug, Clone, Serialize, Deserialize)]
153pub struct CrudFlow {
154 pub name: String,
156 #[serde(skip_serializing_if = "Option::is_none")]
158 pub base_path: Option<String>,
159 pub steps: Vec<FlowStep>,
161}
162
163impl CrudFlow {
164 pub fn new(name: String) -> Self {
166 Self {
167 name,
168 base_path: None,
169 steps: Vec::new(),
170 }
171 }
172
173 pub fn with_base_path(mut self, path: String) -> Self {
175 self.base_path = Some(path);
176 self
177 }
178
179 pub fn add_step(&mut self, step: FlowStep) {
181 self.steps.push(step);
182 }
183
184 pub fn get_all_extract_fields(&self) -> HashSet<String> {
186 self.steps
187 .iter()
188 .flat_map(|step| step.extract.iter().map(|e| e.field.clone()))
189 .collect()
190 }
191
192 pub fn get_all_extract_configs(&self) -> Vec<&ExtractField> {
194 self.steps.iter().flat_map(|step| step.extract.iter()).collect()
195 }
196}
197
198#[derive(Debug, Clone, Serialize, Deserialize)]
200pub struct CrudFlowConfig {
201 pub flows: Vec<CrudFlow>,
203 #[serde(default)]
205 pub default_extract_fields: Vec<String>,
206}
207
208impl Default for CrudFlowConfig {
209 fn default() -> Self {
210 Self {
211 flows: Vec::new(),
212 default_extract_fields: vec!["id".to_string(), "uuid".to_string()],
213 }
214 }
215}
216
217impl CrudFlowConfig {
218 pub fn from_file(path: &Path) -> Result<Self> {
220 let content = std::fs::read_to_string(path)
221 .map_err(|e| BenchError::Other(format!("Failed to read flow config: {}", e)))?;
222
223 Self::from_yaml(&content)
224 }
225
226 pub fn from_yaml(yaml: &str) -> Result<Self> {
228 serde_yaml::from_str(yaml)
229 .map_err(|e| BenchError::Other(format!("Failed to parse flow config: {}", e)))
230 }
231
232 pub fn single_flow(flow: CrudFlow) -> Self {
234 Self {
235 flows: vec![flow],
236 ..Default::default()
237 }
238 }
239}
240
241#[derive(Debug, Clone)]
243pub struct ResourceOperations {
244 pub base_path: String,
246 pub create: Option<ApiOperation>,
248 pub read: Option<ApiOperation>,
250 pub update: Option<ApiOperation>,
252 pub delete: Option<ApiOperation>,
254 pub list: Option<ApiOperation>,
256}
257
258impl ResourceOperations {
259 pub fn new(base_path: String) -> Self {
261 Self {
262 base_path,
263 create: None,
264 read: None,
265 update: None,
266 delete: None,
267 list: None,
268 }
269 }
270
271 pub fn has_crud_operations(&self) -> bool {
273 self.create.is_some()
274 && (self.read.is_some() || self.update.is_some() || self.delete.is_some())
275 }
276
277 pub fn get_id_param_name(&self) -> Option<String> {
279 let path = self
281 .read
282 .as_ref()
283 .or(self.update.as_ref())
284 .or(self.delete.as_ref())
285 .map(|op| &op.path)?;
286
287 extract_id_param_from_path(path)
289 }
290}
291
292fn extract_id_param_from_path(path: &str) -> Option<String> {
294 for segment in path.split('/').rev() {
296 if segment.starts_with('{') && segment.ends_with('}') {
297 return Some(segment[1..segment.len() - 1].to_string());
298 }
299 }
300 None
301}
302
303fn get_base_path(path: &str) -> String {
306 let segments: Vec<&str> = path.split('/').collect();
307 let mut base_segments = Vec::new();
308
309 for segment in segments {
310 if segment.starts_with('{') {
311 break;
312 }
313 if !segment.is_empty() {
314 base_segments.push(segment);
315 }
316 }
317
318 if base_segments.is_empty() {
319 "/".to_string()
320 } else {
321 format!("/{}", base_segments.join("/"))
322 }
323}
324
325fn is_detail_path(path: &str) -> bool {
327 path.contains('{') && path.contains('}')
328}
329
330pub struct CrudFlowDetector;
332
333impl CrudFlowDetector {
334 pub fn detect_flows(operations: &[ApiOperation]) -> Vec<CrudFlow> {
338 let mut resources: HashMap<String, ResourceOperations> = HashMap::new();
340
341 for op in operations {
342 let base_path = get_base_path(&op.path);
343 let is_detail = is_detail_path(&op.path);
344 let method = op.method.to_lowercase();
345
346 let resource = resources
347 .entry(base_path.clone())
348 .or_insert_with(|| ResourceOperations::new(base_path));
349
350 match (method.as_str(), is_detail) {
351 ("post", false) => resource.create = Some(op.clone()),
352 ("get", false) => resource.list = Some(op.clone()),
353 ("get", true) => resource.read = Some(op.clone()),
354 ("put", true) | ("patch", true) => resource.update = Some(op.clone()),
355 ("delete", true) => resource.delete = Some(op.clone()),
356 _ => {}
357 }
358 }
359
360 resources
362 .into_values()
363 .filter(|r| r.has_crud_operations())
364 .map(|r| Self::build_flow_from_resource(&r))
365 .collect()
366 }
367
368 fn build_flow_from_resource(resource: &ResourceOperations) -> CrudFlow {
370 let name = resource.base_path.trim_start_matches('/').replace('/', "_").to_string();
371
372 let mut flow =
373 CrudFlow::new(format!("{} CRUD", name)).with_base_path(resource.base_path.clone());
374
375 let id_param = resource.get_id_param_name().unwrap_or_else(|| "id".to_string());
376
377 if let Some(create_op) = &resource.create {
379 let step =
380 FlowStep::new(format!("{} {}", create_op.method.to_uppercase(), create_op.path))
381 .with_extract(vec!["id".to_string(), "uuid".to_string()])
382 .with_description("Create resource".to_string());
383 flow.add_step(step);
384 }
385
386 if let Some(read_op) = &resource.read {
388 let mut values = HashMap::new();
389 values.insert(id_param.clone(), "id".to_string());
390
391 let step = FlowStep::new(format!("{} {}", read_op.method.to_uppercase(), read_op.path))
392 .with_values(values)
393 .with_description("Read created resource".to_string());
394 flow.add_step(step);
395 }
396
397 if let Some(update_op) = &resource.update {
399 let mut values = HashMap::new();
400 values.insert(id_param.clone(), "id".to_string());
401
402 let step =
403 FlowStep::new(format!("{} {}", update_op.method.to_uppercase(), update_op.path))
404 .with_values(values)
405 .with_description("Update resource".to_string());
406 flow.add_step(step);
407 }
408
409 if let Some(read_op) = &resource.read {
411 let mut values = HashMap::new();
412 values.insert(id_param.clone(), "id".to_string());
413
414 let step = FlowStep::new(format!("{} {}", read_op.method.to_uppercase(), read_op.path))
415 .with_values(values)
416 .with_description("Verify update".to_string());
417 flow.add_step(step);
418 }
419
420 if let Some(delete_op) = &resource.delete {
422 let mut values = HashMap::new();
423 values.insert(id_param.clone(), "id".to_string());
424
425 let step =
426 FlowStep::new(format!("{} {}", delete_op.method.to_uppercase(), delete_op.path))
427 .with_values(values)
428 .with_description("Delete resource".to_string());
429 flow.add_step(step);
430 }
431
432 flow
433 }
434
435 pub fn merge_with_config(detected: Vec<CrudFlow>, config: &CrudFlowConfig) -> Vec<CrudFlow> {
439 if !config.flows.is_empty() {
440 config.flows.clone()
442 } else {
443 detected
445 }
446 }
447}
448
449#[derive(Debug, Clone, Default)]
451pub struct FlowExecutionContext {
452 pub extracted_values: HashMap<String, String>,
455 pub current_step: usize,
457 pub errors: Vec<String>,
459}
460
461impl FlowExecutionContext {
462 pub fn new() -> Self {
464 Self::default()
465 }
466
467 pub fn store_value(&mut self, key: String, value: String) {
469 self.extracted_values.insert(key, value);
470 }
471
472 pub fn get_value(&self, key: &str) -> Option<&String> {
474 self.extracted_values.get(key)
475 }
476
477 pub fn next_step(&mut self) {
479 self.current_step += 1;
480 }
481
482 pub fn record_error(&mut self, error: String) {
484 self.errors.push(error);
485 }
486
487 pub fn has_errors(&self) -> bool {
489 !self.errors.is_empty()
490 }
491}
492
493#[cfg(test)]
494mod tests {
495 use super::*;
496 use openapiv3::Operation;
497
498 fn create_test_operation(method: &str, path: &str, operation_id: Option<&str>) -> ApiOperation {
499 ApiOperation {
500 method: method.to_string(),
501 path: path.to_string(),
502 operation: Operation::default(),
503 operation_id: operation_id.map(|s| s.to_string()),
504 }
505 }
506
507 #[test]
508 fn test_get_base_path() {
509 assert_eq!(get_base_path("/users"), "/users");
510 assert_eq!(get_base_path("/users/{id}"), "/users");
511 assert_eq!(get_base_path("/api/v1/users/{userId}"), "/api/v1/users");
512 assert_eq!(get_base_path("/resources/{resourceId}/items/{itemId}"), "/resources");
513 assert_eq!(get_base_path("/"), "/");
514 }
515
516 #[test]
517 fn test_is_detail_path() {
518 assert!(!is_detail_path("/users"));
519 assert!(is_detail_path("/users/{id}"));
520 assert!(is_detail_path("/users/{userId}"));
521 assert!(!is_detail_path("/users/list"));
522 }
523
524 #[test]
525 fn test_extract_id_param_from_path() {
526 assert_eq!(extract_id_param_from_path("/users/{id}"), Some("id".to_string()));
527 assert_eq!(extract_id_param_from_path("/users/{userId}"), Some("userId".to_string()));
528 assert_eq!(extract_id_param_from_path("/a/{b}/{c}"), Some("c".to_string()));
529 assert_eq!(extract_id_param_from_path("/users"), None);
530 }
531
532 #[test]
533 fn test_crud_flow_detection() {
534 let operations = vec![
535 create_test_operation("post", "/users", Some("createUser")),
536 create_test_operation("get", "/users", Some("listUsers")),
537 create_test_operation("get", "/users/{id}", Some("getUser")),
538 create_test_operation("put", "/users/{id}", Some("updateUser")),
539 create_test_operation("delete", "/users/{id}", Some("deleteUser")),
540 ];
541
542 let flows = CrudFlowDetector::detect_flows(&operations);
543 assert_eq!(flows.len(), 1);
544
545 let flow = &flows[0];
546 assert!(flow.name.contains("users"));
547 assert_eq!(flow.steps.len(), 5); assert!(flow.steps[0].operation.starts_with("POST"));
551 assert!(flow.steps[0].extract.iter().any(|e| e.field == "id"));
553 }
554
555 #[test]
556 fn test_multiple_resources_detection() {
557 let operations = vec![
558 create_test_operation("post", "/users", Some("createUser")),
560 create_test_operation("get", "/users/{id}", Some("getUser")),
561 create_test_operation("delete", "/users/{id}", Some("deleteUser")),
562 create_test_operation("post", "/posts", Some("createPost")),
564 create_test_operation("get", "/posts/{id}", Some("getPost")),
565 create_test_operation("put", "/posts/{id}", Some("updatePost")),
566 ];
567
568 let flows = CrudFlowDetector::detect_flows(&operations);
569 assert_eq!(flows.len(), 2);
570 }
571
572 #[test]
573 fn test_no_crud_without_create() {
574 let operations = vec![
575 create_test_operation("get", "/users", Some("listUsers")),
577 create_test_operation("get", "/users/{id}", Some("getUser")),
578 ];
579
580 let flows = CrudFlowDetector::detect_flows(&operations);
581 assert!(flows.is_empty());
582 }
583
584 #[test]
585 fn test_flow_step_builder() {
586 let step = FlowStep::new("POST /users".to_string())
587 .with_extract(vec!["id".to_string(), "uuid".to_string()])
588 .with_description("Create a new user".to_string());
589
590 assert_eq!(step.operation, "POST /users");
591 assert_eq!(step.extract.len(), 2);
592 assert_eq!(step.description, Some("Create a new user".to_string()));
593 }
594
595 #[test]
596 fn test_flow_step_use_values() {
597 let mut values = HashMap::new();
598 values.insert("id".to_string(), "user_id".to_string());
599
600 let step = FlowStep::new("GET /users/{id}".to_string()).with_values(values);
601
602 assert_eq!(step.use_values.get("id"), Some(&"user_id".to_string()));
603 }
604
605 #[test]
606 fn test_crud_flow_config_from_yaml() {
607 let yaml = r#"
608flows:
609 - name: "User CRUD"
610 base_path: "/users"
611 steps:
612 - operation: "POST /users"
613 extract: ["id"]
614 description: "Create user"
615 - operation: "GET /users/{id}"
616 use_values:
617 id: "id"
618 description: "Get user"
619default_extract_fields:
620 - id
621 - uuid
622"#;
623
624 let config = CrudFlowConfig::from_yaml(yaml).expect("Should parse YAML");
625 assert_eq!(config.flows.len(), 1);
626 assert_eq!(config.flows[0].name, "User CRUD");
627 assert_eq!(config.flows[0].steps.len(), 2);
628 assert_eq!(config.default_extract_fields.len(), 2);
629 }
630
631 #[test]
632 fn test_execution_context() {
633 let mut ctx = FlowExecutionContext::new();
634
635 ctx.store_value("id".to_string(), "12345".to_string());
636 assert_eq!(ctx.get_value("id"), Some(&"12345".to_string()));
637
638 ctx.next_step();
639 assert_eq!(ctx.current_step, 1);
640
641 ctx.record_error("Something went wrong".to_string());
642 assert!(ctx.has_errors());
643 }
644
645 #[test]
646 fn test_resource_operations_has_crud() {
647 let mut resource = ResourceOperations::new("/users".to_string());
648 assert!(!resource.has_crud_operations());
649
650 resource.create = Some(create_test_operation("post", "/users", Some("createUser")));
651 assert!(!resource.has_crud_operations()); resource.read = Some(create_test_operation("get", "/users/{id}", Some("getUser")));
654 assert!(resource.has_crud_operations());
655 }
656
657 #[test]
658 fn test_get_id_param_name() {
659 let mut resource = ResourceOperations::new("/users".to_string());
660 resource.read = Some(create_test_operation("get", "/users/{userId}", Some("getUser")));
661
662 assert_eq!(resource.get_id_param_name(), Some("userId".to_string()));
663 }
664
665 #[test]
666 fn test_merge_with_config_user_provided() {
667 let detected = vec![CrudFlow::new("detected_flow".to_string())];
668
669 let mut config = CrudFlowConfig::default();
670 config.flows.push(CrudFlow::new("user_flow".to_string()));
671
672 let result = CrudFlowDetector::merge_with_config(detected, &config);
673 assert_eq!(result.len(), 1);
674 assert_eq!(result[0].name, "user_flow");
675 }
676
677 #[test]
678 fn test_merge_with_config_auto_detected() {
679 let detected = vec![CrudFlow::new("detected_flow".to_string())];
680
681 let config = CrudFlowConfig::default();
682
683 let result = CrudFlowDetector::merge_with_config(detected, &config);
684 assert_eq!(result.len(), 1);
685 assert_eq!(result[0].name, "detected_flow");
686 }
687
688 #[test]
689 fn test_crud_flow_get_all_extract_fields() {
690 let mut flow = CrudFlow::new("test".to_string());
691 flow.add_step(FlowStep::new("POST /test".to_string()).with_extract(vec!["id".to_string()]));
692 flow.add_step(
693 FlowStep::new("GET /test/{id}".to_string()).with_extract(vec!["uuid".to_string()]),
694 );
695
696 let fields = flow.get_all_extract_fields();
697 assert!(fields.contains(&"id".to_string()));
698 assert!(fields.contains(&"uuid".to_string()));
699 assert_eq!(fields.len(), 2);
700 }
701}