1use anyhow::{Context, Result};
7use serde_json::json;
8use std::path::PathBuf;
9use tracing::{debug, info, warn};
10
11use crate::config::RuntimeDaemonConfig;
12
13pub struct AutoGenerator {
15 config: RuntimeDaemonConfig,
17 management_api_url: String,
19}
20
21impl AutoGenerator {
22 pub fn new(config: RuntimeDaemonConfig, management_api_url: String) -> Self {
24 Self {
25 config,
26 management_api_url,
27 }
28 }
29
30 pub async fn generate_mock_from_404(&self, method: &str, path: &str) -> Result<()> {
40 info!("Generating mock for {} {}", method, path);
41
42 let mock_id = self.create_mock_endpoint(method, path).await?;
44 debug!("Created mock endpoint with ID: {}", mock_id);
45
46 if self.config.generate_types {
48 if let Err(e) = self.generate_type(method, path).await {
49 warn!("Failed to generate type: {}", e);
50 }
51 }
52
53 if self.config.generate_client_stubs {
55 if let Err(e) = self.generate_client_stub(method, path).await {
56 warn!("Failed to generate client stub: {}", e);
57 }
58 }
59
60 if self.config.update_openapi {
62 if let Err(e) = self.update_openapi_schema(method, path).await {
63 warn!("Failed to update OpenAPI schema: {}", e);
64 }
65 }
66
67 if self.config.create_scenario {
69 if let Err(e) = self.create_scenario(method, path, &mock_id).await {
70 warn!("Failed to create scenario: {}", e);
71 }
72 }
73
74 info!("Completed mock generation for {} {}", method, path);
75 Ok(())
76 }
77
78 async fn create_mock_endpoint(&self, method: &str, path: &str) -> Result<String> {
80 let response_body = self.generate_intelligent_response(method, path).await?;
82
83 let mock_config = json!({
85 "method": method,
86 "path": path,
87 "status_code": 200,
88 "body": response_body,
89 "name": format!("Auto-generated: {} {}", method, path),
90 "enabled": true,
91 });
92
93 let client = reqwest::Client::new();
95 let url = format!("{}/__mockforge/api/mocks", self.management_api_url);
96
97 let response = client
98 .post(&url)
99 .json(&mock_config)
100 .send()
101 .await
102 .context("Failed to send request to management API")?;
103
104 if !response.status().is_success() {
105 let status = response.status();
106 let text = response.text().await.unwrap_or_default();
107 anyhow::bail!("Management API returned {}: {}", status, text);
108 }
109
110 let created_mock: serde_json::Value =
111 response.json().await.context("Failed to parse response from management API")?;
112
113 let mock_id = created_mock
114 .get("id")
115 .and_then(|v| v.as_str())
116 .ok_or_else(|| anyhow::anyhow!("Response missing 'id' field"))?
117 .to_string();
118
119 Ok(mock_id)
120 }
121
122 async fn generate_intelligent_response(
124 &self,
125 method: &str,
126 path: &str,
127 ) -> Result<serde_json::Value> {
128 #[cfg(feature = "ai")]
130 if self.config.ai_generation {
131 return self.generate_ai_response(method, path).await;
132 }
133
134 let entity_type = self.infer_entity_type(path);
138
139 let response = match method.to_uppercase().as_str() {
141 "GET" => {
142 if path.ends_with('/')
144 || path.split('/').next_back().unwrap_or("").parse::<u64>().is_err()
145 {
146 json!([{
148 "id": "{{uuid}}",
149 "name": format!("Sample {}", entity_type),
150 "created_at": "{{now}}",
151 }])
152 } else {
153 json!({
155 "id": path.split('/').next_back().unwrap_or("123"),
156 "name": format!("Sample {}", entity_type),
157 "created_at": "{{now}}",
158 })
159 }
160 }
161 "POST" => {
162 json!({
164 "id": "{{uuid}}",
165 "name": format!("New {}", entity_type),
166 "created_at": "{{now}}",
167 "status": "created",
168 })
169 }
170 "PUT" | "PATCH" => {
171 json!({
173 "id": path.split('/').next_back().unwrap_or("123"),
174 "name": format!("Updated {}", entity_type),
175 "updated_at": "{{now}}",
176 })
177 }
178 "DELETE" => {
179 json!({
181 "success": true,
182 "message": "Resource deleted",
183 })
184 }
185 _ => {
186 json!({
188 "message": "Auto-generated response",
189 "method": method,
190 "path": path,
191 })
192 }
193 };
194
195 Ok(response)
196 }
197
198 #[cfg(feature = "ai")]
200 async fn generate_ai_response(&self, method: &str, path: &str) -> Result<serde_json::Value> {
201 use mockforge_data::{IntelligentMockConfig, IntelligentMockGenerator, ResponseMode};
202 use std::collections::HashMap;
203
204 let entity_type = self.infer_entity_type(path);
206 let prompt = format!(
207 "Generate a realistic {} API response for {} {} endpoint. \
208 The response should be appropriate for a {} operation and include realistic data \
209 for a {} entity.",
210 entity_type, method, path, method, entity_type
211 );
212
213 let schema = self.build_schema_for_entity(&entity_type, method);
215
216 let mut ai_config = IntelligentMockConfig::new(ResponseMode::Intelligent)
218 .with_prompt(prompt)
219 .with_schema(schema)
220 .with_count(1);
221
222 if let Ok(rag_config) = self.load_rag_config_from_env() {
224 ai_config = ai_config.with_rag_config(rag_config);
225 }
226
227 let mut generator = IntelligentMockGenerator::new(ai_config)
229 .context("Failed to create intelligent mock generator")?;
230
231 let response = generator.generate().await.context("Failed to generate AI response")?;
232
233 info!("Generated AI-powered response for {} {}", method, path);
234 Ok(response)
235 }
236
237 #[cfg(feature = "ai")]
239 fn load_rag_config_from_env(&self) -> Result<mockforge_data::RagConfig> {
240 use mockforge_data::{EmbeddingProvider, LlmProvider, RagConfig};
241
242 let provider = std::env::var("MOCKFORGE_RAG_PROVIDER")
244 .unwrap_or_else(|_| "openai".to_string())
245 .to_lowercase();
246
247 let provider = match provider.as_str() {
248 "openai" => LlmProvider::OpenAI,
249 "anthropic" => LlmProvider::Anthropic,
250 "ollama" => LlmProvider::Ollama,
251 _ => LlmProvider::OpenAI,
252 };
253
254 let model =
255 std::env::var("MOCKFORGE_RAG_MODEL").unwrap_or_else(|_| "gpt-3.5-turbo".to_string());
256
257 let api_key = std::env::var("MOCKFORGE_RAG_API_KEY").ok();
258
259 let api_endpoint = std::env::var("MOCKFORGE_RAG_API_ENDPOINT")
260 .unwrap_or_else(|_| "https://api.openai.com/v1/chat/completions".to_string());
261
262 let mut config = RagConfig::default();
263 config.provider = provider;
264 config.model = model;
265 config.api_key = api_key;
266 config.api_endpoint = api_endpoint;
267
268 config.embedding_provider = match config.provider {
270 LlmProvider::OpenAI => EmbeddingProvider::OpenAI,
271 LlmProvider::Anthropic => EmbeddingProvider::OpenAI, LlmProvider::Ollama => EmbeddingProvider::OpenAI, LlmProvider::OpenAICompatible => EmbeddingProvider::OpenAI,
274 };
275
276 Ok(config)
277 }
278
279 fn build_schema_for_entity(&self, entity_type: &str, method: &str) -> serde_json::Value {
281 let base_schema = json!({
282 "type": "object",
283 "properties": {
284 "id": {
285 "type": "string",
286 "format": "uuid"
287 },
288 "name": {
289 "type": "string"
290 },
291 "created_at": {
292 "type": "string",
293 "format": "date-time"
294 }
295 },
296 "required": ["id", "name"]
297 });
298
299 match method.to_uppercase().as_str() {
301 "GET" => {
302 if entity_type.ends_with('s') {
304 json!({
305 "type": "array",
306 "items": base_schema
307 })
308 } else {
309 base_schema
310 }
311 }
312 "POST" => {
313 let mut schema = base_schema.clone();
315 if let Some(props) = schema.get_mut("properties").and_then(|p| p.as_object_mut()) {
316 props.insert(
317 "status".to_string(),
318 json!({
319 "type": "string",
320 "enum": ["created", "pending", "active"]
321 }),
322 );
323 }
324 schema
325 }
326 "PUT" | "PATCH" => {
327 let mut schema = base_schema.clone();
329 if let Some(props) = schema.get_mut("properties").and_then(|p| p.as_object_mut()) {
330 props.insert(
331 "updated_at".to_string(),
332 json!({
333 "type": "string",
334 "format": "date-time"
335 }),
336 );
337 }
338 schema
339 }
340 _ => base_schema,
341 }
342 }
343
344 fn infer_entity_type(&self, path: &str) -> String {
346 let parts: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
348
349 let skip_prefixes = ["api", "v1", "v2", "v3", "v4", "v5"];
351 let meaningful_parts: Vec<&str> = parts
352 .iter()
353 .skip_while(|part| skip_prefixes.contains(&part.to_lowercase().as_str()))
354 .copied()
355 .collect();
356
357 if meaningful_parts.is_empty() {
358 return "resource".to_string();
359 }
360
361 let candidate = if let Some(last_part) = meaningful_parts.last() {
363 if last_part.parse::<u64>().is_ok() || last_part.parse::<i64>().is_ok() {
365 meaningful_parts.get(meaningful_parts.len().saturating_sub(2))
367 } else {
368 Some(last_part)
369 }
370 } else {
371 None
372 };
373
374 if let Some(part) = candidate {
375 let entity = part
377 .trim_end_matches('s') .to_lowercase();
379
380 if !entity.is_empty() {
381 return entity;
382 }
383 }
384
385 "resource".to_string()
386 }
387
388 async fn generate_type(&self, method: &str, path: &str) -> Result<()> {
390 use std::path::PathBuf;
391
392 let output_dir = if let Some(ref workspace_dir) = self.config.workspace_dir {
394 PathBuf::from(workspace_dir)
395 } else {
396 PathBuf::from(".")
397 };
398
399 let types_dir = output_dir.join("types");
401 if !types_dir.exists() {
402 std::fs::create_dir_all(&types_dir).context("Failed to create types directory")?;
403 }
404
405 let entity_type = self.infer_entity_type(path);
407 let type_name = self.sanitize_type_name(&entity_type);
408
409 let schema = self.build_schema_for_entity(&entity_type, method);
411
412 let ts_type = self.generate_typescript_interface(&type_name, &schema, method)?;
414
415 let ts_file = types_dir.join(format!("{}.ts", type_name.to_lowercase()));
417 std::fs::write(&ts_file, ts_type).context("Failed to write TypeScript type file")?;
418
419 let json_schema = self.generate_json_schema(&type_name, &schema)?;
421 let json_file = types_dir.join(format!("{}.schema.json", type_name.to_lowercase()));
422 std::fs::write(&json_file, serde_json::to_string_pretty(&json_schema)?)
423 .context("Failed to write JSON schema file")?;
424
425 info!(
426 "Generated types for {} {}: {} and {}.schema.json",
427 method,
428 path,
429 ts_file.display(),
430 json_file.display()
431 );
432
433 Ok(())
434 }
435
436 fn generate_typescript_interface(
438 &self,
439 type_name: &str,
440 schema: &serde_json::Value,
441 method: &str,
442 ) -> Result<String> {
443 let mut code = String::new();
444 code.push_str(&format!("// Generated TypeScript type for {} {}\n", method, type_name));
445 code.push_str("// Auto-generated by MockForge Runtime Daemon\n\n");
446
447 let schema_type = schema.get("type").and_then(|v| v.as_str()).unwrap_or("object");
449
450 if schema_type == "array" {
451 if let Some(items) = schema.get("items") {
453 let item_type_name = format!("{}Item", type_name);
454 code.push_str(&self.generate_typescript_interface(
455 &item_type_name,
456 items,
457 method,
458 )?);
459 code.push_str(&format!("export type {} = {}[];\n", type_name, item_type_name));
460 } else {
461 code.push_str(&format!("export type {} = any[];\n", type_name));
462 }
463 } else {
464 code.push_str(&format!("export interface {} {{\n", type_name));
466
467 if let Some(properties) = schema.get("properties").and_then(|p| p.as_object()) {
468 let required = schema
469 .get("required")
470 .and_then(|r| r.as_array())
471 .map(|arr| arr.iter().filter_map(|v| v.as_str()).collect::<Vec<_>>())
472 .unwrap_or_default();
473
474 for (prop_name, prop_schema) in properties {
475 let prop_type = self.schema_value_to_typescript_type(prop_schema)?;
476 let is_optional = !required.contains(&prop_name.as_str());
477 let optional_marker = if is_optional { "?" } else { "" };
478
479 code.push_str(&format!(" {}{}: {};\n", prop_name, optional_marker, prop_type));
480 }
481 }
482
483 code.push_str("}\n");
484 }
485
486 Ok(code)
487 }
488
489 fn schema_value_to_typescript_type(&self, schema: &serde_json::Value) -> Result<String> {
491 let schema_type = schema.get("type").and_then(|v| v.as_str()).unwrap_or("any");
492
493 match schema_type {
494 "string" => {
495 if let Some(format) = schema.get("format").and_then(|f| f.as_str()) {
497 match format {
498 "date-time" | "date" => Ok("string".to_string()),
499 "uuid" => Ok("string".to_string()),
500 _ => Ok("string".to_string()),
501 }
502 } else {
503 Ok("string".to_string())
504 }
505 }
506 "integer" | "number" => Ok("number".to_string()),
507 "boolean" => Ok("boolean".to_string()),
508 "array" => {
509 if let Some(items) = schema.get("items") {
510 let item_type = self.schema_value_to_typescript_type(items)?;
511 Ok(format!("{}[]", item_type))
512 } else {
513 Ok("any[]".to_string())
514 }
515 }
516 "object" => {
517 if schema.get("properties").is_some() {
518 Ok("Record<string, any>".to_string())
520 } else {
521 Ok("Record<string, any>".to_string())
522 }
523 }
524 _ => Ok("any".to_string()),
525 }
526 }
527
528 fn generate_json_schema(
530 &self,
531 type_name: &str,
532 schema: &serde_json::Value,
533 ) -> Result<serde_json::Value> {
534 let mut json_schema = json!({
535 "$schema": "http://json-schema.org/draft-07/schema#",
536 "title": type_name,
537 "type": schema.get("type").unwrap_or(&json!("object")),
538 });
539
540 if let Some(properties) = schema.get("properties") {
541 json_schema["properties"] = properties.clone();
542 }
543
544 if let Some(required) = schema.get("required") {
545 json_schema["required"] = required.clone();
546 }
547
548 Ok(json_schema)
549 }
550
551 fn sanitize_type_name(&self, name: &str) -> String {
553 let mut result = String::new();
554 let mut capitalize_next = true;
555
556 for ch in name.chars() {
557 match ch {
558 '-' | '_' | ' ' => capitalize_next = true,
559 ch if ch.is_alphanumeric() => {
560 if capitalize_next {
561 result.push(ch.to_uppercase().next().unwrap_or(ch));
562 capitalize_next = false;
563 } else {
564 result.push(ch);
565 }
566 }
567 _ => {}
568 }
569 }
570
571 if result.is_empty() {
572 "Resource".to_string()
573 } else {
574 let mut chars = result.chars();
576 if let Some(first) = chars.next() {
577 format!("{}{}", first.to_uppercase(), chars.as_str())
578 } else {
579 "Resource".to_string()
580 }
581 }
582 }
583
584 async fn generate_client_stub(&self, method: &str, path: &str) -> Result<()> {
586 use std::path::PathBuf;
587 use tokio::fs;
588
589 let output_dir = if let Some(ref workspace_dir) = self.config.workspace_dir {
591 PathBuf::from(workspace_dir)
592 } else {
593 PathBuf::from(".")
594 };
595
596 let stubs_dir = output_dir.join("client-stubs");
598 if !stubs_dir.exists() {
599 fs::create_dir_all(&stubs_dir)
600 .await
601 .context("Failed to create client-stubs directory")?;
602 }
603
604 let entity_type = self.infer_entity_type(path);
606 let function_name = self.generate_function_name(method, path);
607 let stub_code =
608 self.generate_client_stub_code(method, path, &function_name, &entity_type)?;
609
610 let stub_file = stubs_dir.join(format!("{}.ts", function_name.to_lowercase()));
612 fs::write(&stub_file, stub_code)
613 .await
614 .context("Failed to write client stub file")?;
615
616 info!("Generated client stub for {} {}: {}", method, path, stub_file.display());
617 Ok(())
618 }
619
620 fn generate_function_name(&self, method: &str, path: &str) -> String {
622 let entity_type = self.infer_entity_type(path);
623 let method_prefix = match method.to_uppercase().as_str() {
624 "GET" => "get",
625 "POST" => "create",
626 "PUT" => "update",
627 "PATCH" => "patch",
628 "DELETE" => "delete",
629 _ => "call",
630 };
631
632 let has_id = path
634 .split('/')
635 .any(|segment| segment.starts_with('{') && segment.ends_with('}'));
636
637 if has_id && method.to_uppercase() == "GET" {
638 format!("{}{}", method_prefix, self.sanitize_type_name(&entity_type))
639 } else if method.to_uppercase() == "GET" {
640 format!("list{}s", self.sanitize_type_name(&entity_type))
641 } else {
642 format!("{}{}", method_prefix, self.sanitize_type_name(&entity_type))
643 }
644 }
645
646 fn generate_client_stub_code(
648 &self,
649 method: &str,
650 path: &str,
651 function_name: &str,
652 entity_type: &str,
653 ) -> Result<String> {
654 let method_upper = method.to_uppercase();
655 let type_name = self.sanitize_type_name(entity_type);
656
657 let path_params: Vec<String> = path
659 .split('/')
660 .filter_map(|segment| {
661 if segment.starts_with('{') && segment.ends_with('}') {
662 Some(segment.trim_matches(|c| c == '{' || c == '}').to_string())
663 } else {
664 None
665 }
666 })
667 .collect();
668
669 let mut params = String::new();
671 if !path_params.is_empty() {
672 for param in &path_params {
673 params.push_str(&format!("{}: string", param));
674 params.push_str(", ");
675 }
676 }
677
678 if matches!(method_upper.as_str(), "POST" | "PUT" | "PATCH") {
680 params.push_str(&format!("data?: Partial<{}>", type_name));
681 }
682
683 if method_upper == "GET" {
685 if !params.is_empty() {
686 params.push_str(", ");
687 }
688 params.push_str("queryParams?: Record<string, any>");
689 }
690
691 let mut endpoint_path = path.to_string();
693 for param in &path_params {
694 endpoint_path =
695 endpoint_path.replace(&format!("{{{}}}", param), &format!("${{{}}}", param));
696 }
697
698 let stub = format!(
700 r#"// Auto-generated client stub for {} {}
701// Generated by MockForge Runtime Daemon
702
703import type {{ {} }} from '../types/{}';
704
705/**
706 * {} {} endpoint
707 *
708 * @param {} - Request parameters
709 * @returns Promise resolving to {} response
710 */
711export async function {}({}): Promise<{}> {{
712 const endpoint = `{}`;
713 const url = `${{baseUrl}}${{endpoint}}`;
714
715 const response = await fetch(url, {{
716 method: '{}',
717 headers: {{
718 'Content-Type': 'application/json',
719 ...(headers || {{}}),
720 }},
721 {}{}
722 }});
723
724 if (!response.ok) {{
725 throw new Error(`Request failed: ${{response.status}} ${{response.statusText}}`);
726 }}
727
728 return response.json();
729}}
730
731/**
732 * Base URL configuration
733 * Override this to point to your API server
734 */
735export let baseUrl = 'http://localhost:3000';
736"#,
737 method,
738 path,
739 type_name,
740 entity_type.to_lowercase(),
741 method,
742 path,
743 if params.is_empty() {
744 "headers?: Record<string, string>"
745 } else {
746 ¶ms
747 },
748 type_name,
749 function_name,
750 if params.is_empty() {
751 "headers?: Record<string, string>"
752 } else {
753 ¶ms
754 },
755 type_name,
756 endpoint_path,
757 method_upper,
758 if matches!(method_upper.as_str(), "POST" | "PUT" | "PATCH") {
759 "body: JSON.stringify(data || {}),\n ".to_string()
760 } else if method_upper == "GET" && !path_params.is_empty() {
761 format!("{}const queryString = queryParams ? '?' + new URLSearchParams(queryParams).toString() : '';\n const urlWithQuery = url + queryString;\n ",
762 if !path_params.is_empty() { "" } else { "" })
763 } else {
764 String::new()
765 },
766 if method_upper == "GET" && !path_params.is_empty() {
767 "url: urlWithQuery,\n ".to_string()
768 } else {
769 String::new()
770 }
771 );
772
773 Ok(stub)
774 }
775
776 async fn update_openapi_schema(&self, method: &str, path: &str) -> Result<()> {
778 use mockforge_core::openapi::OpenApiSpec;
779
780 let spec_path = self.find_or_create_openapi_spec_path().await?;
782
783 let mut spec = if spec_path.exists() {
785 OpenApiSpec::from_file(&spec_path)
786 .await
787 .context("Failed to load existing OpenAPI spec")?
788 } else {
789 self.create_new_openapi_spec().await?
791 };
792
793 self.add_endpoint_to_spec(&mut spec, method, path).await?;
795
796 self.save_openapi_spec(&spec, &spec_path).await?;
798
799 info!("Updated OpenAPI schema at {} with {} {}", spec_path.display(), method, path);
800 Ok(())
801 }
802
803 async fn find_or_create_openapi_spec_path(&self) -> Result<PathBuf> {
805 use std::path::PathBuf;
806
807 let possible_paths = vec![
809 PathBuf::from("openapi.yaml"),
810 PathBuf::from("openapi.yml"),
811 PathBuf::from("openapi.json"),
812 PathBuf::from("api.yaml"),
813 PathBuf::from("api.yml"),
814 PathBuf::from("api.json"),
815 ];
816
817 let mut all_paths = possible_paths.clone();
819 if let Some(ref workspace_dir) = self.config.workspace_dir {
820 for path in possible_paths {
821 all_paths.push(PathBuf::from(workspace_dir).join(path));
822 }
823 }
824
825 for path in &all_paths {
827 if path.exists() {
828 return Ok(path.clone());
829 }
830 }
831
832 let default_path = if let Some(ref workspace_dir) = self.config.workspace_dir {
834 PathBuf::from(workspace_dir).join("openapi.yaml")
835 } else {
836 PathBuf::from("openapi.yaml")
837 };
838
839 Ok(default_path)
840 }
841
842 async fn create_new_openapi_spec(&self) -> Result<mockforge_core::openapi::OpenApiSpec> {
844 use mockforge_core::openapi::OpenApiSpec;
845 use serde_json::json;
846
847 let spec_json = json!({
848 "openapi": "3.0.3",
849 "info": {
850 "title": "Auto-generated API",
851 "version": "1.0.0",
852 "description": "API specification auto-generated by MockForge Runtime Daemon"
853 },
854 "paths": {},
855 "components": {
856 "schemas": {}
857 }
858 });
859
860 OpenApiSpec::from_json(spec_json).context("Failed to create new OpenAPI spec")
861 }
862
863 async fn add_endpoint_to_spec(
865 &self,
866 spec: &mut mockforge_core::openapi::OpenApiSpec,
867 method: &str,
868 path: &str,
869 ) -> Result<()> {
870 let mut spec_json = spec
872 .raw_document
873 .clone()
874 .ok_or_else(|| anyhow::anyhow!("OpenAPI spec missing raw document"))?;
875
876 if spec_json.get("paths").is_none() {
878 spec_json["paths"] = json!({});
879 }
880
881 let paths = spec_json
882 .get_mut("paths")
883 .and_then(|p| p.as_object_mut())
884 .ok_or_else(|| anyhow::anyhow!("Failed to get paths object"))?;
885
886 let path_entry = paths.entry(path.to_string()).or_insert_with(|| json!({}));
888
889 let method_lower = method.to_lowercase();
891
892 let operation = json!({
894 "summary": format!("Auto-generated {} endpoint", method),
895 "description": format!("Endpoint auto-generated by MockForge Runtime Daemon for {} {}", method, path),
896 "operationId": self.generate_operation_id(method, path),
897 "responses": {
898 "200": {
899 "description": "Successful response",
900 "content": {
901 "application/json": {
902 "schema": self.build_schema_for_entity(&self.infer_entity_type(path), method)
903 }
904 }
905 }
906 }
907 });
908
909 path_entry[method_lower] = operation;
911
912 *spec = mockforge_core::openapi::OpenApiSpec::from_json(spec_json)
914 .context("Failed to reload OpenAPI spec after update")?;
915
916 Ok(())
917 }
918
919 fn generate_operation_id(&self, method: &str, path: &str) -> String {
921 let entity_type = self.infer_entity_type(path);
922 let method_lower = method.to_lowercase();
923
924 let path_parts: Vec<&str> =
926 path.split('/').filter(|s| !s.is_empty() && !s.starts_with('{')).collect();
927
928 if path_parts.is_empty() {
929 format!("{}_{}", method_lower, entity_type)
930 } else {
931 let mut op_id = String::new();
932 op_id.push_str(&method_lower);
933 for part in path_parts {
934 let mut chars = part.chars();
935 if let Some(first) = chars.next() {
936 op_id.push(first.to_uppercase().next().unwrap_or(first));
937 op_id.push_str(chars.as_str());
938 }
939 }
940 op_id
941 }
942 }
943
944 async fn save_openapi_spec(
946 &self,
947 spec: &mockforge_core::openapi::OpenApiSpec,
948 path: &PathBuf,
949 ) -> Result<()> {
950 use tokio::fs;
951
952 let spec_json = spec
953 .raw_document
954 .clone()
955 .ok_or_else(|| anyhow::anyhow!("OpenAPI spec missing raw document"))?;
956
957 let is_yaml = path
959 .extension()
960 .and_then(|s| s.to_str())
961 .map(|s| s == "yaml" || s == "yml")
962 .unwrap_or(false);
963
964 let content = if is_yaml {
965 serde_yaml::to_string(&spec_json).context("Failed to serialize OpenAPI spec to YAML")?
966 } else {
967 serde_json::to_string_pretty(&spec_json)
968 .context("Failed to serialize OpenAPI spec to JSON")?
969 };
970
971 if let Some(parent) = path.parent() {
973 fs::create_dir_all(parent)
974 .await
975 .context("Failed to create OpenAPI spec directory")?;
976 }
977
978 fs::write(path, content).await.context("Failed to write OpenAPI spec file")?;
979
980 Ok(())
981 }
982
983 async fn create_scenario(&self, method: &str, path: &str, mock_id: &str) -> Result<()> {
985 use std::path::PathBuf;
986 use tokio::fs;
987
988 let output_dir = if let Some(ref workspace_dir) = self.config.workspace_dir {
990 PathBuf::from(workspace_dir)
991 } else {
992 PathBuf::from(".")
993 };
994
995 let scenarios_dir = output_dir.join("scenarios");
997 if !scenarios_dir.exists() {
998 fs::create_dir_all(&scenarios_dir)
999 .await
1000 .context("Failed to create scenarios directory")?;
1001 }
1002
1003 let entity_type = self.infer_entity_type(path);
1005 let scenario_name = format!("auto-{}-{}", entity_type, method.to_lowercase());
1006 let scenario_dir = scenarios_dir.join(&scenario_name);
1007
1008 if !scenario_dir.exists() {
1010 fs::create_dir_all(&scenario_dir)
1011 .await
1012 .context("Failed to create scenario directory")?;
1013 }
1014
1015 let manifest = self.generate_scenario_manifest(&scenario_name, method, path, mock_id)?;
1017
1018 let manifest_path = scenario_dir.join("scenario.yaml");
1020 let manifest_yaml =
1021 serde_yaml::to_string(&manifest).context("Failed to serialize scenario manifest")?;
1022 fs::write(&manifest_path, manifest_yaml)
1023 .await
1024 .context("Failed to write scenario manifest")?;
1025
1026 let config = self.generate_scenario_config(method, path, mock_id)?;
1028 let config_path = scenario_dir.join("config.yaml");
1029 let config_yaml =
1030 serde_yaml::to_string(&config).context("Failed to serialize scenario config")?;
1031 fs::write(&config_path, config_yaml)
1032 .await
1033 .context("Failed to write scenario config")?;
1034
1035 info!("Created scenario '{}' at {}", scenario_name, scenario_dir.display());
1036 Ok(())
1037 }
1038
1039 fn generate_scenario_manifest(
1041 &self,
1042 scenario_name: &str,
1043 method: &str,
1044 path: &str,
1045 _mock_id: &str,
1046 ) -> Result<serde_json::Value> {
1047 use chrono::Utc;
1048
1049 let entity_type = self.infer_entity_type(path);
1050 let title = format!("Auto-generated {} {} Scenario", method, entity_type);
1051
1052 let manifest = json!({
1053 "manifest_version": "1.0",
1054 "name": scenario_name,
1055 "version": "1.0.0",
1056 "title": title,
1057 "description": format!(
1058 "Auto-generated scenario for {} {} endpoint. Created by MockForge Runtime Daemon.",
1059 method, path
1060 ),
1061 "author": "MockForge Runtime Daemon",
1062 "author_email": None::<String>,
1063 "category": "other",
1064 "tags": ["auto-generated", "runtime-daemon", entity_type],
1065 "compatibility": {
1066 "min_version": "0.3.0",
1067 "max_version": null,
1068 "required_features": [],
1069 "protocols": ["http"]
1070 },
1071 "files": [
1072 "scenario.yaml",
1073 "config.yaml"
1074 ],
1075 "readme": None::<String>,
1076 "example_usage": format!(
1077 "# Use this scenario\nmockforge scenario use {}\n\n# Start server\nmockforge serve --config config.yaml",
1078 scenario_name
1079 ),
1080 "required_features": [],
1081 "plugin_dependencies": [],
1082 "metadata": {
1083 "auto_generated": true,
1084 "endpoint": path,
1085 "method": method,
1086 "entity_type": entity_type
1087 },
1088 "created_at": Utc::now().to_rfc3339(),
1089 "updated_at": Utc::now().to_rfc3339()
1090 });
1091
1092 Ok(manifest)
1093 }
1094
1095 fn generate_scenario_config(
1097 &self,
1098 method: &str,
1099 path: &str,
1100 mock_id: &str,
1101 ) -> Result<serde_json::Value> {
1102 let entity_type = self.infer_entity_type(path);
1103 let response_body =
1104 serde_json::to_value(self.build_schema_for_entity(&entity_type, method))?;
1105
1106 let config = json!({
1107 "http": {
1108 "enabled": true,
1109 "port": 3000,
1110 "mocks": [
1111 {
1112 "id": mock_id,
1113 "method": method,
1114 "path": path,
1115 "status_code": 200,
1116 "body": response_body,
1117 "name": format!("Auto-generated: {} {}", method, path),
1118 "enabled": true
1119 }
1120 ]
1121 }
1122 });
1123
1124 Ok(config)
1125 }
1126}
1127
1128#[cfg(test)]
1129mod tests {
1130 use super::*;
1131
1132 fn create_test_generator() -> AutoGenerator {
1133 let config = RuntimeDaemonConfig::default();
1134 AutoGenerator::new(config, "http://localhost:3000".to_string())
1135 }
1136
1137 #[test]
1139 fn test_infer_entity_type() {
1140 let generator = create_test_generator();
1141
1142 assert_eq!(generator.infer_entity_type("/api/users"), "user");
1143 assert_eq!(generator.infer_entity_type("/api/products"), "product");
1144 assert_eq!(generator.infer_entity_type("/api/orders/123"), "order");
1145 assert_eq!(generator.infer_entity_type("/api"), "resource");
1146 }
1147
1148 #[test]
1149 fn test_infer_entity_type_with_versions() {
1150 let generator = create_test_generator();
1151
1152 assert_eq!(generator.infer_entity_type("/v1/users"), "user");
1153 assert_eq!(generator.infer_entity_type("/v2/products"), "product");
1154 assert_eq!(generator.infer_entity_type("/api/v1/orders"), "order");
1155 assert_eq!(generator.infer_entity_type("/api/v3/items"), "item");
1156 }
1157
1158 #[test]
1159 fn test_infer_entity_type_nested_paths() {
1160 let generator = create_test_generator();
1161
1162 assert_eq!(generator.infer_entity_type("/api/users/123/orders"), "order");
1163 assert_eq!(generator.infer_entity_type("/api/stores/456/products"), "product");
1164 }
1165
1166 #[test]
1167 fn test_infer_entity_type_numeric_id() {
1168 let generator = create_test_generator();
1169
1170 assert_eq!(generator.infer_entity_type("/api/users/123"), "user");
1171 assert_eq!(generator.infer_entity_type("/api/products/99999"), "product");
1172 }
1173
1174 #[test]
1175 fn test_infer_entity_type_empty_path() {
1176 let generator = create_test_generator();
1177
1178 assert_eq!(generator.infer_entity_type("/"), "resource");
1179 assert_eq!(generator.infer_entity_type(""), "resource");
1180 }
1181
1182 #[test]
1184 fn test_sanitize_type_name_basic() {
1185 let generator = create_test_generator();
1186
1187 assert_eq!(generator.sanitize_type_name("user"), "User");
1188 assert_eq!(generator.sanitize_type_name("product"), "Product");
1189 }
1190
1191 #[test]
1192 fn test_sanitize_type_name_with_hyphens() {
1193 let generator = create_test_generator();
1194
1195 assert_eq!(generator.sanitize_type_name("user-account"), "UserAccount");
1196 assert_eq!(generator.sanitize_type_name("product-item"), "ProductItem");
1197 }
1198
1199 #[test]
1200 fn test_sanitize_type_name_with_underscores() {
1201 let generator = create_test_generator();
1202
1203 assert_eq!(generator.sanitize_type_name("user_account"), "UserAccount");
1204 assert_eq!(generator.sanitize_type_name("product_item"), "ProductItem");
1205 }
1206
1207 #[test]
1208 fn test_sanitize_type_name_with_spaces() {
1209 let generator = create_test_generator();
1210
1211 assert_eq!(generator.sanitize_type_name("user account"), "UserAccount");
1212 assert_eq!(generator.sanitize_type_name("product item"), "ProductItem");
1213 }
1214
1215 #[test]
1216 fn test_sanitize_type_name_empty() {
1217 let generator = create_test_generator();
1218
1219 assert_eq!(generator.sanitize_type_name(""), "Resource");
1220 }
1221
1222 #[test]
1223 fn test_sanitize_type_name_special_chars() {
1224 let generator = create_test_generator();
1225
1226 assert_eq!(generator.sanitize_type_name("user@123"), "User123");
1229 assert_eq!(generator.sanitize_type_name("product!item"), "Productitem");
1230 assert_eq!(generator.sanitize_type_name("product-item"), "ProductItem");
1232 }
1233
1234 #[test]
1236 fn test_generate_function_name_get_list() {
1237 let generator = create_test_generator();
1238
1239 assert_eq!(generator.generate_function_name("GET", "/api/users"), "listUsers");
1240 assert_eq!(generator.generate_function_name("GET", "/api/products"), "listProducts");
1241 }
1242
1243 #[test]
1244 fn test_generate_function_name_get_single() {
1245 let generator = create_test_generator();
1246
1247 assert_eq!(generator.generate_function_name("GET", "/api/users/{id}"), "getId");
1250 assert_eq!(
1251 generator.generate_function_name("GET", "/api/products/{productId}"),
1252 "getProductid"
1253 );
1254 }
1255
1256 #[test]
1257 fn test_generate_function_name_post() {
1258 let generator = create_test_generator();
1259
1260 assert_eq!(generator.generate_function_name("POST", "/api/users"), "createUser");
1261 assert_eq!(generator.generate_function_name("POST", "/api/products"), "createProduct");
1262 }
1263
1264 #[test]
1265 fn test_generate_function_name_put() {
1266 let generator = create_test_generator();
1267
1268 assert_eq!(generator.generate_function_name("PUT", "/api/users/{id}"), "updateId");
1270 assert_eq!(generator.generate_function_name("PUT", "/api/users"), "updateUser");
1272 }
1273
1274 #[test]
1275 fn test_generate_function_name_patch() {
1276 let generator = create_test_generator();
1277
1278 assert_eq!(generator.generate_function_name("PATCH", "/api/users/{id}"), "patchId");
1280 assert_eq!(generator.generate_function_name("PATCH", "/api/users"), "patchUser");
1281 }
1282
1283 #[test]
1284 fn test_generate_function_name_delete() {
1285 let generator = create_test_generator();
1286
1287 assert_eq!(generator.generate_function_name("DELETE", "/api/users/{id}"), "deleteId");
1289 assert_eq!(generator.generate_function_name("DELETE", "/api/users"), "deleteUser");
1290 }
1291
1292 #[test]
1293 fn test_generate_function_name_unknown_method() {
1294 let generator = create_test_generator();
1295
1296 assert_eq!(generator.generate_function_name("OPTIONS", "/api/users"), "callUser");
1297 }
1298
1299 #[test]
1301 fn test_generate_operation_id_basic() {
1302 let generator = create_test_generator();
1303
1304 let op_id = generator.generate_operation_id("GET", "/api/users");
1305 assert!(op_id.starts_with("get"));
1306 assert!(op_id.contains("Api"));
1307 assert!(op_id.contains("Users"));
1308 }
1309
1310 #[test]
1311 fn test_generate_operation_id_post() {
1312 let generator = create_test_generator();
1313
1314 let op_id = generator.generate_operation_id("POST", "/api/users");
1315 assert!(op_id.starts_with("post"));
1316 }
1317
1318 #[test]
1319 fn test_generate_operation_id_with_id_param() {
1320 let generator = create_test_generator();
1321
1322 let op_id = generator.generate_operation_id("GET", "/api/users/{id}");
1324 assert!(!op_id.contains("{"));
1325 assert!(!op_id.contains("}"));
1326 }
1327
1328 #[test]
1330 fn test_schema_value_to_typescript_type_string() {
1331 let generator = create_test_generator();
1332
1333 let schema = serde_json::json!({"type": "string"});
1334 assert_eq!(generator.schema_value_to_typescript_type(&schema).unwrap(), "string");
1335 }
1336
1337 #[test]
1338 fn test_schema_value_to_typescript_type_integer() {
1339 let generator = create_test_generator();
1340
1341 let schema = serde_json::json!({"type": "integer"});
1342 assert_eq!(generator.schema_value_to_typescript_type(&schema).unwrap(), "number");
1343 }
1344
1345 #[test]
1346 fn test_schema_value_to_typescript_type_number() {
1347 let generator = create_test_generator();
1348
1349 let schema = serde_json::json!({"type": "number"});
1350 assert_eq!(generator.schema_value_to_typescript_type(&schema).unwrap(), "number");
1351 }
1352
1353 #[test]
1354 fn test_schema_value_to_typescript_type_boolean() {
1355 let generator = create_test_generator();
1356
1357 let schema = serde_json::json!({"type": "boolean"});
1358 assert_eq!(generator.schema_value_to_typescript_type(&schema).unwrap(), "boolean");
1359 }
1360
1361 #[test]
1362 fn test_schema_value_to_typescript_type_array() {
1363 let generator = create_test_generator();
1364
1365 let schema = serde_json::json!({
1366 "type": "array",
1367 "items": {"type": "string"}
1368 });
1369 assert_eq!(generator.schema_value_to_typescript_type(&schema).unwrap(), "string[]");
1370 }
1371
1372 #[test]
1373 fn test_schema_value_to_typescript_type_array_no_items() {
1374 let generator = create_test_generator();
1375
1376 let schema = serde_json::json!({"type": "array"});
1377 assert_eq!(generator.schema_value_to_typescript_type(&schema).unwrap(), "any[]");
1378 }
1379
1380 #[test]
1381 fn test_schema_value_to_typescript_type_object() {
1382 let generator = create_test_generator();
1383
1384 let schema = serde_json::json!({"type": "object"});
1385 assert_eq!(
1386 generator.schema_value_to_typescript_type(&schema).unwrap(),
1387 "Record<string, any>"
1388 );
1389 }
1390
1391 #[test]
1392 fn test_schema_value_to_typescript_type_unknown() {
1393 let generator = create_test_generator();
1394
1395 let schema = serde_json::json!({"type": "unknown_type"});
1396 assert_eq!(generator.schema_value_to_typescript_type(&schema).unwrap(), "any");
1397 }
1398
1399 #[test]
1400 fn test_schema_value_to_typescript_type_date_format() {
1401 let generator = create_test_generator();
1402
1403 let schema = serde_json::json!({"type": "string", "format": "date-time"});
1404 assert_eq!(generator.schema_value_to_typescript_type(&schema).unwrap(), "string");
1405 }
1406
1407 #[test]
1409 fn test_build_schema_for_entity_get_single() {
1410 let generator = create_test_generator();
1411
1412 let schema = generator.build_schema_for_entity("user", "GET");
1413 assert_eq!(schema["type"], "object");
1414 assert!(schema["properties"]["id"].is_object());
1415 assert!(schema["properties"]["name"].is_object());
1416 }
1417
1418 #[test]
1419 fn test_build_schema_for_entity_get_plural() {
1420 let generator = create_test_generator();
1421
1422 let schema = generator.build_schema_for_entity("users", "GET");
1423 assert_eq!(schema["type"], "array");
1424 assert!(schema["items"].is_object());
1425 }
1426
1427 #[test]
1428 fn test_build_schema_for_entity_post() {
1429 let generator = create_test_generator();
1430
1431 let schema = generator.build_schema_for_entity("user", "POST");
1432 assert_eq!(schema["type"], "object");
1433 assert!(schema["properties"]["status"].is_object());
1434 }
1435
1436 #[test]
1437 fn test_build_schema_for_entity_put() {
1438 let generator = create_test_generator();
1439
1440 let schema = generator.build_schema_for_entity("user", "PUT");
1441 assert_eq!(schema["type"], "object");
1442 assert!(schema["properties"]["updated_at"].is_object());
1443 }
1444
1445 #[test]
1446 fn test_build_schema_for_entity_patch() {
1447 let generator = create_test_generator();
1448
1449 let schema = generator.build_schema_for_entity("user", "PATCH");
1450 assert_eq!(schema["type"], "object");
1451 assert!(schema["properties"]["updated_at"].is_object());
1452 }
1453
1454 #[test]
1455 fn test_build_schema_for_entity_delete() {
1456 let generator = create_test_generator();
1457
1458 let schema = generator.build_schema_for_entity("user", "DELETE");
1459 assert_eq!(schema["type"], "object");
1460 }
1462
1463 #[tokio::test]
1465 async fn test_generate_intelligent_response_get_collection() {
1466 let generator = create_test_generator();
1467
1468 let response = generator.generate_intelligent_response("GET", "/api/users/").await.unwrap();
1469 assert!(response.is_array());
1470 }
1471
1472 #[tokio::test]
1473 async fn test_generate_intelligent_response_get_single() {
1474 let generator = create_test_generator();
1475
1476 let response =
1477 generator.generate_intelligent_response("GET", "/api/users/123").await.unwrap();
1478 assert!(response.is_object());
1479 assert_eq!(response["id"], "123");
1480 }
1481
1482 #[tokio::test]
1483 async fn test_generate_intelligent_response_post() {
1484 let generator = create_test_generator();
1485
1486 let response = generator.generate_intelligent_response("POST", "/api/users").await.unwrap();
1487 assert!(response.is_object());
1488 assert_eq!(response["status"], "created");
1489 }
1490
1491 #[tokio::test]
1492 async fn test_generate_intelligent_response_put() {
1493 let generator = create_test_generator();
1494
1495 let response =
1496 generator.generate_intelligent_response("PUT", "/api/users/123").await.unwrap();
1497 assert!(response.is_object());
1498 assert!(response["name"].as_str().unwrap().contains("Updated"));
1499 }
1500
1501 #[tokio::test]
1502 async fn test_generate_intelligent_response_delete() {
1503 let generator = create_test_generator();
1504
1505 let response = generator
1506 .generate_intelligent_response("DELETE", "/api/users/123")
1507 .await
1508 .unwrap();
1509 assert!(response.is_object());
1510 assert_eq!(response["success"], true);
1511 }
1512
1513 #[tokio::test]
1514 async fn test_generate_intelligent_response_unknown_method() {
1515 let generator = create_test_generator();
1516
1517 let response =
1518 generator.generate_intelligent_response("OPTIONS", "/api/users").await.unwrap();
1519 assert!(response.is_object());
1520 assert!(response["message"].is_string());
1521 }
1522
1523 #[test]
1525 fn test_generate_typescript_interface_object() {
1526 let generator = create_test_generator();
1527
1528 let schema = serde_json::json!({
1529 "type": "object",
1530 "properties": {
1531 "id": {"type": "string"},
1532 "name": {"type": "string"}
1533 },
1534 "required": ["id"]
1535 });
1536
1537 let ts_code = generator.generate_typescript_interface("User", &schema, "GET").unwrap();
1538 assert!(ts_code.contains("export interface User"));
1539 assert!(ts_code.contains("id: string"));
1540 assert!(ts_code.contains("name?: string")); }
1542
1543 #[test]
1544 fn test_generate_typescript_interface_array() {
1545 let generator = create_test_generator();
1546
1547 let schema = serde_json::json!({
1548 "type": "array",
1549 "items": {
1550 "type": "object",
1551 "properties": {
1552 "id": {"type": "string"}
1553 }
1554 }
1555 });
1556
1557 let ts_code = generator.generate_typescript_interface("Users", &schema, "GET").unwrap();
1558 assert!(ts_code.contains("export type Users = UsersItem[]"));
1559 }
1560
1561 #[test]
1563 fn test_generate_json_schema() {
1564 let generator = create_test_generator();
1565
1566 let schema = serde_json::json!({
1567 "type": "object",
1568 "properties": {
1569 "id": {"type": "string"}
1570 },
1571 "required": ["id"]
1572 });
1573
1574 let json_schema = generator.generate_json_schema("User", &schema).unwrap();
1575 assert_eq!(json_schema["$schema"], "http://json-schema.org/draft-07/schema#");
1576 assert_eq!(json_schema["title"], "User");
1577 assert_eq!(json_schema["type"], "object");
1578 }
1579
1580 #[test]
1582 fn test_generate_scenario_manifest() {
1583 let generator = create_test_generator();
1584
1585 let manifest = generator
1586 .generate_scenario_manifest("test-scenario", "GET", "/api/users", "mock-123")
1587 .unwrap();
1588
1589 assert_eq!(manifest["name"], "test-scenario");
1590 assert_eq!(manifest["manifest_version"], "1.0");
1591 assert!(manifest["metadata"]["auto_generated"].as_bool().unwrap());
1592 }
1593
1594 #[test]
1596 fn test_generate_scenario_config() {
1597 let generator = create_test_generator();
1598
1599 let config = generator.generate_scenario_config("GET", "/api/users", "mock-123").unwrap();
1600
1601 assert!(config["http"]["enabled"].as_bool().unwrap());
1602 assert!(config["http"]["mocks"].is_array());
1603 }
1604
1605 #[test]
1607 fn test_auto_generator_new() {
1608 let config = RuntimeDaemonConfig::default();
1609 let generator = AutoGenerator::new(config, "http://localhost:3000".to_string());
1610 assert!(!generator.config.enabled);
1611 assert_eq!(generator.management_api_url, "http://localhost:3000");
1612 }
1613
1614 #[test]
1615 fn test_auto_generator_with_custom_config() {
1616 let config = RuntimeDaemonConfig {
1617 enabled: true,
1618 ai_generation: true,
1619 generate_types: true,
1620 generate_client_stubs: true,
1621 workspace_dir: Some("/tmp/workspace".to_string()),
1622 ..Default::default()
1623 };
1624 let generator = AutoGenerator::new(config, "http://api.example.com".to_string());
1625
1626 assert!(generator.config.enabled);
1627 assert!(generator.config.ai_generation);
1628 assert!(generator.config.generate_types);
1629 assert!(generator.config.generate_client_stubs);
1630 assert_eq!(generator.config.workspace_dir, Some("/tmp/workspace".to_string()));
1631 }
1632}