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