use anyhow::{Context, Result};
use serde_json::json;
use std::path::PathBuf;
use tracing::{debug, info, warn};
use crate::config::RuntimeDaemonConfig;
pub struct AutoGenerator {
config: RuntimeDaemonConfig,
management_api_url: String,
}
impl AutoGenerator {
pub fn new(config: RuntimeDaemonConfig, management_api_url: String) -> Self {
Self {
config,
management_api_url,
}
}
pub async fn generate_mock_from_404(&self, method: &str, path: &str) -> Result<()> {
info!("Generating mock for {} {}", method, path);
let mock_id = self.create_mock_endpoint(method, path).await?;
debug!("Created mock endpoint with ID: {}", mock_id);
if self.config.generate_types {
if let Err(e) = self.generate_type(method, path).await {
warn!("Failed to generate type: {}", e);
}
}
if self.config.generate_client_stubs {
if let Err(e) = self.generate_client_stub(method, path).await {
warn!("Failed to generate client stub: {}", e);
}
}
if self.config.update_openapi {
if let Err(e) = self.update_openapi_schema(method, path).await {
warn!("Failed to update OpenAPI schema: {}", e);
}
}
if self.config.create_scenario {
if let Err(e) = self.create_scenario(method, path, &mock_id).await {
warn!("Failed to create scenario: {}", e);
}
}
info!("Completed mock generation for {} {}", method, path);
Ok(())
}
async fn create_mock_endpoint(&self, method: &str, path: &str) -> Result<String> {
let response_body = self.generate_intelligent_response(method, path).await?;
let mock_config = json!({
"method": method,
"path": path,
"status_code": 200,
"body": response_body,
"name": format!("Auto-generated: {} {}", method, path),
"enabled": true,
});
let client = reqwest::Client::new();
let url = format!("{}/__mockforge/api/mocks", self.management_api_url);
let response = client
.post(&url)
.json(&mock_config)
.send()
.await
.context("Failed to send request to management API")?;
if !response.status().is_success() {
let status = response.status();
let text = response.text().await.unwrap_or_default();
anyhow::bail!("Management API returned {}: {}", status, text);
}
let created_mock: serde_json::Value =
response.json().await.context("Failed to parse response from management API")?;
let mock_id = created_mock
.get("id")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("Response missing 'id' field"))?
.to_string();
Ok(mock_id)
}
async fn generate_intelligent_response(
&self,
method: &str,
path: &str,
) -> Result<serde_json::Value> {
#[cfg(feature = "ai")]
if self.config.ai_generation {
return self.generate_ai_response(method, path).await;
}
let entity_type = self.infer_entity_type(path);
let response = match method.to_uppercase().as_str() {
"GET" => {
if path.ends_with('/')
|| path.split('/').next_back().unwrap_or("").parse::<u64>().is_err()
{
json!([{
"id": "{{uuid}}",
"name": format!("Sample {}", entity_type),
"created_at": "{{now}}",
}])
} else {
json!({
"id": path.split('/').next_back().unwrap_or("123"),
"name": format!("Sample {}", entity_type),
"created_at": "{{now}}",
})
}
}
"POST" => {
json!({
"id": "{{uuid}}",
"name": format!("New {}", entity_type),
"created_at": "{{now}}",
"status": "created",
})
}
"PUT" | "PATCH" => {
json!({
"id": path.split('/').next_back().unwrap_or("123"),
"name": format!("Updated {}", entity_type),
"updated_at": "{{now}}",
})
}
"DELETE" => {
json!({
"success": true,
"message": "Resource deleted",
})
}
_ => {
json!({
"message": "Auto-generated response",
"method": method,
"path": path,
})
}
};
Ok(response)
}
#[cfg(feature = "ai")]
async fn generate_ai_response(&self, method: &str, path: &str) -> Result<serde_json::Value> {
use mockforge_data::{IntelligentMockConfig, IntelligentMockGenerator, ResponseMode};
let entity_type = self.infer_entity_type(path);
let prompt = format!(
"Generate a realistic {} API response for {} {} endpoint. \
The response should be appropriate for a {} operation and include realistic data \
for a {} entity.",
entity_type, method, path, method, entity_type
);
let schema = self.build_schema_for_entity(&entity_type, method);
let mut ai_config = IntelligentMockConfig::new(ResponseMode::Intelligent)
.with_prompt(prompt)
.with_schema(schema)
.with_count(1);
if let Ok(rag_config) = self.load_rag_config_from_env() {
ai_config = ai_config.with_rag_config(rag_config);
}
let mut generator = IntelligentMockGenerator::new(ai_config)
.context("Failed to create intelligent mock generator")?;
let response = generator.generate().await.context("Failed to generate AI response")?;
info!("Generated AI-powered response for {} {}", method, path);
Ok(response)
}
#[cfg(feature = "ai")]
fn load_rag_config_from_env(&self) -> Result<mockforge_data::RagConfig> {
use mockforge_data::{EmbeddingProvider, LlmProvider, RagConfig};
let provider = std::env::var("MOCKFORGE_RAG_PROVIDER")
.unwrap_or_else(|_| "openai".to_string())
.to_lowercase();
let provider = match provider.as_str() {
"openai" => LlmProvider::OpenAI,
"anthropic" => LlmProvider::Anthropic,
"ollama" => LlmProvider::Ollama,
_ => LlmProvider::OpenAI,
};
let model =
std::env::var("MOCKFORGE_RAG_MODEL").unwrap_or_else(|_| "gpt-3.5-turbo".to_string());
let api_key = std::env::var("MOCKFORGE_RAG_API_KEY").ok();
let api_endpoint = std::env::var("MOCKFORGE_RAG_API_ENDPOINT")
.unwrap_or_else(|_| "https://api.openai.com/v1/chat/completions".to_string());
let mut config = RagConfig::default();
config.provider = provider;
config.model = model;
config.api_key = api_key;
config.api_endpoint = api_endpoint;
config.embedding_provider = match config.provider {
LlmProvider::OpenAI => EmbeddingProvider::OpenAI,
LlmProvider::Anthropic => EmbeddingProvider::OpenAI, LlmProvider::Ollama => EmbeddingProvider::Ollama,
LlmProvider::OpenAICompatible => EmbeddingProvider::OpenAI,
};
Ok(config)
}
fn build_schema_for_entity(&self, entity_type: &str, method: &str) -> serde_json::Value {
let base_schema = json!({
"type": "object",
"properties": {
"id": {
"type": "string",
"format": "uuid"
},
"name": {
"type": "string"
},
"created_at": {
"type": "string",
"format": "date-time"
}
},
"required": ["id", "name"]
});
match method.to_uppercase().as_str() {
"GET" => {
if entity_type.ends_with('s') {
json!({
"type": "array",
"items": base_schema
})
} else {
base_schema
}
}
"POST" => {
let mut schema = base_schema.clone();
if let Some(props) = schema.get_mut("properties").and_then(|p| p.as_object_mut()) {
props.insert(
"status".to_string(),
json!({
"type": "string",
"enum": ["created", "pending", "active"]
}),
);
}
schema
}
"PUT" | "PATCH" => {
let mut schema = base_schema.clone();
if let Some(props) = schema.get_mut("properties").and_then(|p| p.as_object_mut()) {
props.insert(
"updated_at".to_string(),
json!({
"type": "string",
"format": "date-time"
}),
);
}
schema
}
_ => base_schema,
}
}
fn infer_entity_type(&self, path: &str) -> String {
let parts: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
let skip_prefixes = ["api", "v1", "v2", "v3", "v4", "v5"];
let meaningful_parts: Vec<&str> = parts
.iter()
.skip_while(|part| skip_prefixes.contains(&part.to_lowercase().as_str()))
.copied()
.collect();
if meaningful_parts.is_empty() {
return "resource".to_string();
}
let candidate = if let Some(last_part) = meaningful_parts.last() {
if last_part.parse::<u64>().is_ok() || last_part.parse::<i64>().is_ok() {
meaningful_parts.get(meaningful_parts.len().saturating_sub(2))
} else {
Some(last_part)
}
} else {
None
};
if let Some(part) = candidate {
let entity = part
.trim_end_matches('s') .to_lowercase();
if !entity.is_empty() {
return entity;
}
}
"resource".to_string()
}
async fn generate_type(&self, method: &str, path: &str) -> Result<()> {
use std::path::PathBuf;
let output_dir = if let Some(ref workspace_dir) = self.config.workspace_dir {
PathBuf::from(workspace_dir)
} else {
PathBuf::from(".")
};
let types_dir = output_dir.join("types");
if !types_dir.exists() {
std::fs::create_dir_all(&types_dir).context("Failed to create types directory")?;
}
let entity_type = self.infer_entity_type(path);
let type_name = self.sanitize_type_name(&entity_type);
let schema = self.build_schema_for_entity(&entity_type, method);
let ts_type = Self::generate_typescript_interface(&type_name, &schema, method)?;
let ts_file = types_dir.join(format!("{}.ts", type_name.to_lowercase()));
std::fs::write(&ts_file, ts_type).context("Failed to write TypeScript type file")?;
let json_schema = self.generate_json_schema(&type_name, &schema)?;
let json_file = types_dir.join(format!("{}.schema.json", type_name.to_lowercase()));
std::fs::write(&json_file, serde_json::to_string_pretty(&json_schema)?)
.context("Failed to write JSON schema file")?;
info!(
"Generated types for {} {}: {} and {}.schema.json",
method,
path,
ts_file.display(),
json_file.display()
);
Ok(())
}
fn generate_typescript_interface(
type_name: &str,
schema: &serde_json::Value,
method: &str,
) -> Result<String> {
let mut code = String::new();
code.push_str(&format!("// Generated TypeScript type for {} {}\n", method, type_name));
code.push_str("// Auto-generated by MockForge Runtime Daemon\n\n");
let schema_type = schema.get("type").and_then(|v| v.as_str()).unwrap_or("object");
if schema_type == "array" {
if let Some(items) = schema.get("items") {
let item_type_name = format!("{}Item", type_name);
code.push_str(&Self::generate_typescript_interface(
&item_type_name,
items,
method,
)?);
code.push_str(&format!("export type {} = {}[];\n", type_name, item_type_name));
} else {
code.push_str(&format!("export type {} = any[];\n", type_name));
}
} else {
code.push_str(&format!("export interface {} {{\n", type_name));
if let Some(properties) = schema.get("properties").and_then(|p| p.as_object()) {
let required = schema
.get("required")
.and_then(|r| r.as_array())
.map(|arr| arr.iter().filter_map(|v| v.as_str()).collect::<Vec<_>>())
.unwrap_or_default();
for (prop_name, prop_schema) in properties {
let prop_type = Self::schema_value_to_typescript_type(prop_schema)?;
let is_optional = !required.contains(&prop_name.as_str());
let optional_marker = if is_optional { "?" } else { "" };
code.push_str(&format!(" {}{}: {};\n", prop_name, optional_marker, prop_type));
}
}
code.push_str("}\n");
}
Ok(code)
}
fn schema_value_to_typescript_type(schema: &serde_json::Value) -> Result<String> {
let schema_type = schema.get("type").and_then(|v| v.as_str()).unwrap_or("any");
match schema_type {
"string" => {
if let Some(format) = schema.get("format").and_then(|f| f.as_str()) {
match format {
"date-time" | "date" => Ok("string".to_string()),
"uuid" => Ok("string".to_string()),
_ => Ok("string".to_string()),
}
} else {
Ok("string".to_string())
}
}
"integer" | "number" => Ok("number".to_string()),
"boolean" => Ok("boolean".to_string()),
"array" => {
if let Some(items) = schema.get("items") {
let item_type = Self::schema_value_to_typescript_type(items)?;
Ok(format!("{}[]", item_type))
} else {
Ok("any[]".to_string())
}
}
"object" => {
if schema.get("properties").is_some() {
Ok("Record<string, any>".to_string())
} else {
Ok("Record<string, any>".to_string())
}
}
_ => Ok("any".to_string()),
}
}
fn generate_json_schema(
&self,
type_name: &str,
schema: &serde_json::Value,
) -> Result<serde_json::Value> {
let mut json_schema = json!({
"$schema": "http://json-schema.org/draft-07/schema#",
"title": type_name,
"type": schema.get("type").unwrap_or(&json!("object")),
});
if let Some(properties) = schema.get("properties") {
json_schema["properties"] = properties.clone();
}
if let Some(required) = schema.get("required") {
json_schema["required"] = required.clone();
}
Ok(json_schema)
}
fn sanitize_type_name(&self, name: &str) -> String {
let mut result = String::new();
let mut capitalize_next = true;
for ch in name.chars() {
match ch {
'-' | '_' | ' ' => capitalize_next = true,
ch if ch.is_alphanumeric() => {
if capitalize_next {
result.push(ch.to_uppercase().next().unwrap_or(ch));
capitalize_next = false;
} else {
result.push(ch);
}
}
_ => {}
}
}
if result.is_empty() {
"Resource".to_string()
} else {
let mut chars = result.chars();
if let Some(first) = chars.next() {
format!("{}{}", first.to_uppercase(), chars.as_str())
} else {
"Resource".to_string()
}
}
}
async fn generate_client_stub(&self, method: &str, path: &str) -> Result<()> {
use std::path::PathBuf;
use tokio::fs;
let output_dir = if let Some(ref workspace_dir) = self.config.workspace_dir {
PathBuf::from(workspace_dir)
} else {
PathBuf::from(".")
};
let stubs_dir = output_dir.join("client-stubs");
if !stubs_dir.exists() {
fs::create_dir_all(&stubs_dir)
.await
.context("Failed to create client-stubs directory")?;
}
let entity_type = self.infer_entity_type(path);
let function_name = self.generate_function_name(method, path);
let stub_code =
self.generate_client_stub_code(method, path, &function_name, &entity_type)?;
let stub_file = stubs_dir.join(format!("{}.ts", function_name.to_lowercase()));
fs::write(&stub_file, stub_code)
.await
.context("Failed to write client stub file")?;
info!("Generated client stub for {} {}: {}", method, path, stub_file.display());
Ok(())
}
fn generate_function_name(&self, method: &str, path: &str) -> String {
let entity_type = self.infer_entity_type(path);
let method_prefix = match method.to_uppercase().as_str() {
"GET" => "get",
"POST" => "create",
"PUT" => "update",
"PATCH" => "patch",
"DELETE" => "delete",
_ => "call",
};
let has_id = path
.split('/')
.any(|segment| segment.starts_with('{') && segment.ends_with('}'));
if has_id && method.to_uppercase() == "GET" {
format!("{}{}", method_prefix, self.sanitize_type_name(&entity_type))
} else if method.to_uppercase() == "GET" {
format!("list{}s", self.sanitize_type_name(&entity_type))
} else {
format!("{}{}", method_prefix, self.sanitize_type_name(&entity_type))
}
}
fn generate_client_stub_code(
&self,
method: &str,
path: &str,
function_name: &str,
entity_type: &str,
) -> Result<String> {
let method_upper = method.to_uppercase();
let type_name = self.sanitize_type_name(entity_type);
let path_params: Vec<String> = path
.split('/')
.filter_map(|segment| {
if segment.starts_with('{') && segment.ends_with('}') {
Some(segment.trim_matches(|c| c == '{' || c == '}').to_string())
} else {
None
}
})
.collect();
let mut params = String::new();
if !path_params.is_empty() {
for param in &path_params {
params.push_str(&format!("{}: string", param));
params.push_str(", ");
}
}
if matches!(method_upper.as_str(), "POST" | "PUT" | "PATCH") {
params.push_str(&format!("data?: Partial<{}>", type_name));
}
if method_upper == "GET" {
if !params.is_empty() {
params.push_str(", ");
}
params.push_str("queryParams?: Record<string, any>");
}
let mut endpoint_path = path.to_string();
for param in &path_params {
endpoint_path =
endpoint_path.replace(&format!("{{{}}}", param), &format!("${{{}}}", param));
}
let stub = format!(
r#"// Auto-generated client stub for {} {}
// Generated by MockForge Runtime Daemon
import type {{ {} }} from '../types/{}';
/**
* {} {} endpoint
*
* @param {} - Request parameters
* @returns Promise resolving to {} response
*/
export async function {}({}): Promise<{}> {{
const endpoint = `{}`;
const url = `${{baseUrl}}${{endpoint}}`;
const response = await fetch(url, {{
method: '{}',
headers: {{
'Content-Type': 'application/json',
...(headers || {{}}),
}},
{}{}
}});
if (!response.ok) {{
throw new Error(`Request failed: ${{response.status}} ${{response.statusText}}`);
}}
return response.json();
}}
/**
* Base URL configuration
* Override this to point to your API server
*/
export let baseUrl = 'http://localhost:3000';
"#,
method,
path,
type_name,
entity_type.to_lowercase(),
method,
path,
if params.is_empty() {
"headers?: Record<string, string>"
} else {
¶ms
},
type_name,
function_name,
if params.is_empty() {
"headers?: Record<string, string>"
} else {
¶ms
},
type_name,
endpoint_path,
method_upper,
if matches!(method_upper.as_str(), "POST" | "PUT" | "PATCH") {
"body: JSON.stringify(data || {}),\n ".to_string()
} else if method_upper == "GET" && !path_params.is_empty() {
"const queryString = queryParams ? '?' + new URLSearchParams(queryParams).toString() : '';\n const urlWithQuery = url + queryString;\n ".to_string()
} else {
String::new()
},
if method_upper == "GET" && !path_params.is_empty() {
"url: urlWithQuery,\n ".to_string()
} else {
String::new()
}
);
Ok(stub)
}
async fn update_openapi_schema(&self, method: &str, path: &str) -> Result<()> {
use mockforge_core::openapi::OpenApiSpec;
let spec_path = self.find_or_create_openapi_spec_path().await?;
let mut spec = if spec_path.exists() {
OpenApiSpec::from_file(&spec_path)
.await
.context("Failed to load existing OpenAPI spec")?
} else {
self.create_new_openapi_spec().await?
};
self.add_endpoint_to_spec(&mut spec, method, path).await?;
self.save_openapi_spec(&spec, &spec_path).await?;
info!("Updated OpenAPI schema at {} with {} {}", spec_path.display(), method, path);
Ok(())
}
async fn find_or_create_openapi_spec_path(&self) -> Result<PathBuf> {
use std::path::PathBuf;
let possible_paths = vec![
PathBuf::from("openapi.yaml"),
PathBuf::from("openapi.yml"),
PathBuf::from("openapi.json"),
PathBuf::from("api.yaml"),
PathBuf::from("api.yml"),
PathBuf::from("api.json"),
];
let mut all_paths = possible_paths.clone();
if let Some(ref workspace_dir) = self.config.workspace_dir {
for path in possible_paths {
all_paths.push(PathBuf::from(workspace_dir).join(path));
}
}
for path in &all_paths {
if path.exists() {
return Ok(path.clone());
}
}
let default_path = if let Some(ref workspace_dir) = self.config.workspace_dir {
PathBuf::from(workspace_dir).join("openapi.yaml")
} else {
PathBuf::from("openapi.yaml")
};
Ok(default_path)
}
async fn create_new_openapi_spec(&self) -> Result<mockforge_core::openapi::OpenApiSpec> {
use mockforge_core::openapi::OpenApiSpec;
use serde_json::json;
let spec_json = json!({
"openapi": "3.0.3",
"info": {
"title": "Auto-generated API",
"version": "1.0.0",
"description": "API specification auto-generated by MockForge Runtime Daemon"
},
"paths": {},
"components": {
"schemas": {}
}
});
OpenApiSpec::from_json(spec_json).context("Failed to create new OpenAPI spec")
}
async fn add_endpoint_to_spec(
&self,
spec: &mut mockforge_core::openapi::OpenApiSpec,
method: &str,
path: &str,
) -> Result<()> {
let mut spec_json = spec
.raw_document
.clone()
.ok_or_else(|| anyhow::anyhow!("OpenAPI spec missing raw document"))?;
if spec_json.get("paths").is_none() {
spec_json["paths"] = json!({});
}
let paths = spec_json
.get_mut("paths")
.and_then(|p| p.as_object_mut())
.ok_or_else(|| anyhow::anyhow!("Failed to get paths object"))?;
let path_entry = paths.entry(path.to_string()).or_insert_with(|| json!({}));
let method_lower = method.to_lowercase();
let operation = json!({
"summary": format!("Auto-generated {} endpoint", method),
"description": format!("Endpoint auto-generated by MockForge Runtime Daemon for {} {}", method, path),
"operationId": self.generate_operation_id(method, path),
"responses": {
"200": {
"description": "Successful response",
"content": {
"application/json": {
"schema": self.build_schema_for_entity(&self.infer_entity_type(path), method)
}
}
}
}
});
path_entry[method_lower] = operation;
*spec = mockforge_core::openapi::OpenApiSpec::from_json(spec_json)
.context("Failed to reload OpenAPI spec after update")?;
Ok(())
}
fn generate_operation_id(&self, method: &str, path: &str) -> String {
let entity_type = self.infer_entity_type(path);
let method_lower = method.to_lowercase();
let path_parts: Vec<&str> =
path.split('/').filter(|s| !s.is_empty() && !s.starts_with('{')).collect();
if path_parts.is_empty() {
format!("{}_{}", method_lower, entity_type)
} else {
let mut op_id = String::new();
op_id.push_str(&method_lower);
for part in path_parts {
let mut chars = part.chars();
if let Some(first) = chars.next() {
op_id.push(first.to_uppercase().next().unwrap_or(first));
op_id.push_str(chars.as_str());
}
}
op_id
}
}
async fn save_openapi_spec(
&self,
spec: &mockforge_core::openapi::OpenApiSpec,
path: &PathBuf,
) -> Result<()> {
use tokio::fs;
let spec_json = spec
.raw_document
.clone()
.ok_or_else(|| anyhow::anyhow!("OpenAPI spec missing raw document"))?;
let is_yaml = path
.extension()
.and_then(|s| s.to_str())
.map(|s| s == "yaml" || s == "yml")
.unwrap_or(false);
let content = if is_yaml {
serde_yaml::to_string(&spec_json).context("Failed to serialize OpenAPI spec to YAML")?
} else {
serde_json::to_string_pretty(&spec_json)
.context("Failed to serialize OpenAPI spec to JSON")?
};
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)
.await
.context("Failed to create OpenAPI spec directory")?;
}
fs::write(path, content).await.context("Failed to write OpenAPI spec file")?;
Ok(())
}
async fn create_scenario(&self, method: &str, path: &str, mock_id: &str) -> Result<()> {
use std::path::PathBuf;
use tokio::fs;
let output_dir = if let Some(ref workspace_dir) = self.config.workspace_dir {
PathBuf::from(workspace_dir)
} else {
PathBuf::from(".")
};
let scenarios_dir = output_dir.join("scenarios");
if !scenarios_dir.exists() {
fs::create_dir_all(&scenarios_dir)
.await
.context("Failed to create scenarios directory")?;
}
let entity_type = self.infer_entity_type(path);
let scenario_name = format!("auto-{}-{}", entity_type, method.to_lowercase());
let scenario_dir = scenarios_dir.join(&scenario_name);
if !scenario_dir.exists() {
fs::create_dir_all(&scenario_dir)
.await
.context("Failed to create scenario directory")?;
}
let manifest = self.generate_scenario_manifest(&scenario_name, method, path, mock_id)?;
let manifest_path = scenario_dir.join("scenario.yaml");
let manifest_yaml =
serde_yaml::to_string(&manifest).context("Failed to serialize scenario manifest")?;
fs::write(&manifest_path, manifest_yaml)
.await
.context("Failed to write scenario manifest")?;
let config = self.generate_scenario_config(method, path, mock_id)?;
let config_path = scenario_dir.join("config.yaml");
let config_yaml =
serde_yaml::to_string(&config).context("Failed to serialize scenario config")?;
fs::write(&config_path, config_yaml)
.await
.context("Failed to write scenario config")?;
info!("Created scenario '{}' at {}", scenario_name, scenario_dir.display());
Ok(())
}
fn generate_scenario_manifest(
&self,
scenario_name: &str,
method: &str,
path: &str,
_mock_id: &str,
) -> Result<serde_json::Value> {
use chrono::Utc;
let entity_type = self.infer_entity_type(path);
let title = format!("Auto-generated {} {} Scenario", method, entity_type);
let manifest = json!({
"manifest_version": "1.0",
"name": scenario_name,
"version": "1.0.0",
"title": title,
"description": format!(
"Auto-generated scenario for {} {} endpoint. Created by MockForge Runtime Daemon.",
method, path
),
"author": "MockForge Runtime Daemon",
"author_email": None::<String>,
"category": "other",
"tags": ["auto-generated", "runtime-daemon", entity_type],
"compatibility": {
"min_version": "0.3.0",
"max_version": null,
"required_features": [],
"protocols": ["http"]
},
"files": [
"scenario.yaml",
"config.yaml"
],
"readme": None::<String>,
"example_usage": format!(
"# Use this scenario\nmockforge scenario use {}\n\n# Start server\nmockforge serve --config config.yaml",
scenario_name
),
"required_features": [],
"plugin_dependencies": [],
"metadata": {
"auto_generated": true,
"endpoint": path,
"method": method,
"entity_type": entity_type
},
"created_at": Utc::now().to_rfc3339(),
"updated_at": Utc::now().to_rfc3339()
});
Ok(manifest)
}
fn generate_scenario_config(
&self,
method: &str,
path: &str,
mock_id: &str,
) -> Result<serde_json::Value> {
let entity_type = self.infer_entity_type(path);
let response_body =
serde_json::to_value(self.build_schema_for_entity(&entity_type, method))?;
let config = json!({
"http": {
"enabled": true,
"port": 3000,
"mocks": [
{
"id": mock_id,
"method": method,
"path": path,
"status_code": 200,
"body": response_body,
"name": format!("Auto-generated: {} {}", method, path),
"enabled": true
}
]
}
});
Ok(config)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn create_test_generator() -> AutoGenerator {
let config = RuntimeDaemonConfig::default();
AutoGenerator::new(config, "http://localhost:3000".to_string())
}
#[test]
fn test_infer_entity_type() {
let generator = create_test_generator();
assert_eq!(generator.infer_entity_type("/api/users"), "user");
assert_eq!(generator.infer_entity_type("/api/products"), "product");
assert_eq!(generator.infer_entity_type("/api/orders/123"), "order");
assert_eq!(generator.infer_entity_type("/api"), "resource");
}
#[test]
fn test_infer_entity_type_with_versions() {
let generator = create_test_generator();
assert_eq!(generator.infer_entity_type("/v1/users"), "user");
assert_eq!(generator.infer_entity_type("/v2/products"), "product");
assert_eq!(generator.infer_entity_type("/api/v1/orders"), "order");
assert_eq!(generator.infer_entity_type("/api/v3/items"), "item");
}
#[test]
fn test_infer_entity_type_nested_paths() {
let generator = create_test_generator();
assert_eq!(generator.infer_entity_type("/api/users/123/orders"), "order");
assert_eq!(generator.infer_entity_type("/api/stores/456/products"), "product");
}
#[test]
fn test_infer_entity_type_numeric_id() {
let generator = create_test_generator();
assert_eq!(generator.infer_entity_type("/api/users/123"), "user");
assert_eq!(generator.infer_entity_type("/api/products/99999"), "product");
}
#[test]
fn test_infer_entity_type_empty_path() {
let generator = create_test_generator();
assert_eq!(generator.infer_entity_type("/"), "resource");
assert_eq!(generator.infer_entity_type(""), "resource");
}
#[test]
fn test_sanitize_type_name_basic() {
let generator = create_test_generator();
assert_eq!(generator.sanitize_type_name("user"), "User");
assert_eq!(generator.sanitize_type_name("product"), "Product");
}
#[test]
fn test_sanitize_type_name_with_hyphens() {
let generator = create_test_generator();
assert_eq!(generator.sanitize_type_name("user-account"), "UserAccount");
assert_eq!(generator.sanitize_type_name("product-item"), "ProductItem");
}
#[test]
fn test_sanitize_type_name_with_underscores() {
let generator = create_test_generator();
assert_eq!(generator.sanitize_type_name("user_account"), "UserAccount");
assert_eq!(generator.sanitize_type_name("product_item"), "ProductItem");
}
#[test]
fn test_sanitize_type_name_with_spaces() {
let generator = create_test_generator();
assert_eq!(generator.sanitize_type_name("user account"), "UserAccount");
assert_eq!(generator.sanitize_type_name("product item"), "ProductItem");
}
#[test]
fn test_sanitize_type_name_empty() {
let generator = create_test_generator();
assert_eq!(generator.sanitize_type_name(""), "Resource");
}
#[test]
fn test_sanitize_type_name_special_chars() {
let generator = create_test_generator();
assert_eq!(generator.sanitize_type_name("user@123"), "User123");
assert_eq!(generator.sanitize_type_name("product!item"), "Productitem");
assert_eq!(generator.sanitize_type_name("product-item"), "ProductItem");
}
#[test]
fn test_generate_function_name_get_list() {
let generator = create_test_generator();
assert_eq!(generator.generate_function_name("GET", "/api/users"), "listUsers");
assert_eq!(generator.generate_function_name("GET", "/api/products"), "listProducts");
}
#[test]
fn test_generate_function_name_get_single() {
let generator = create_test_generator();
assert_eq!(generator.generate_function_name("GET", "/api/users/{id}"), "getId");
assert_eq!(
generator.generate_function_name("GET", "/api/products/{productId}"),
"getProductid"
);
}
#[test]
fn test_generate_function_name_post() {
let generator = create_test_generator();
assert_eq!(generator.generate_function_name("POST", "/api/users"), "createUser");
assert_eq!(generator.generate_function_name("POST", "/api/products"), "createProduct");
}
#[test]
fn test_generate_function_name_put() {
let generator = create_test_generator();
assert_eq!(generator.generate_function_name("PUT", "/api/users/{id}"), "updateId");
assert_eq!(generator.generate_function_name("PUT", "/api/users"), "updateUser");
}
#[test]
fn test_generate_function_name_patch() {
let generator = create_test_generator();
assert_eq!(generator.generate_function_name("PATCH", "/api/users/{id}"), "patchId");
assert_eq!(generator.generate_function_name("PATCH", "/api/users"), "patchUser");
}
#[test]
fn test_generate_function_name_delete() {
let generator = create_test_generator();
assert_eq!(generator.generate_function_name("DELETE", "/api/users/{id}"), "deleteId");
assert_eq!(generator.generate_function_name("DELETE", "/api/users"), "deleteUser");
}
#[test]
fn test_generate_function_name_unknown_method() {
let generator = create_test_generator();
assert_eq!(generator.generate_function_name("OPTIONS", "/api/users"), "callUser");
}
#[test]
fn test_generate_operation_id_basic() {
let generator = create_test_generator();
let op_id = generator.generate_operation_id("GET", "/api/users");
assert!(op_id.starts_with("get"));
assert!(op_id.contains("Api"));
assert!(op_id.contains("Users"));
}
#[test]
fn test_generate_operation_id_post() {
let generator = create_test_generator();
let op_id = generator.generate_operation_id("POST", "/api/users");
assert!(op_id.starts_with("post"));
}
#[test]
fn test_generate_operation_id_with_id_param() {
let generator = create_test_generator();
let op_id = generator.generate_operation_id("GET", "/api/users/{id}");
assert!(!op_id.contains("{"));
assert!(!op_id.contains("}"));
}
#[test]
fn test_schema_value_to_typescript_type_string() {
let schema = serde_json::json!({"type": "string"});
assert_eq!(AutoGenerator::schema_value_to_typescript_type(&schema).unwrap(), "string");
}
#[test]
fn test_schema_value_to_typescript_type_integer() {
let schema = serde_json::json!({"type": "integer"});
assert_eq!(AutoGenerator::schema_value_to_typescript_type(&schema).unwrap(), "number");
}
#[test]
fn test_schema_value_to_typescript_type_number() {
let schema = serde_json::json!({"type": "number"});
assert_eq!(AutoGenerator::schema_value_to_typescript_type(&schema).unwrap(), "number");
}
#[test]
fn test_schema_value_to_typescript_type_boolean() {
let schema = serde_json::json!({"type": "boolean"});
assert_eq!(AutoGenerator::schema_value_to_typescript_type(&schema).unwrap(), "boolean");
}
#[test]
fn test_schema_value_to_typescript_type_array() {
let schema = serde_json::json!({
"type": "array",
"items": {"type": "string"}
});
assert_eq!(AutoGenerator::schema_value_to_typescript_type(&schema).unwrap(), "string[]");
}
#[test]
fn test_schema_value_to_typescript_type_array_no_items() {
let schema = serde_json::json!({"type": "array"});
assert_eq!(AutoGenerator::schema_value_to_typescript_type(&schema).unwrap(), "any[]");
}
#[test]
fn test_schema_value_to_typescript_type_object() {
let schema = serde_json::json!({"type": "object"});
assert_eq!(
AutoGenerator::schema_value_to_typescript_type(&schema).unwrap(),
"Record<string, any>"
);
}
#[test]
fn test_schema_value_to_typescript_type_unknown() {
let schema = serde_json::json!({"type": "unknown_type"});
assert_eq!(AutoGenerator::schema_value_to_typescript_type(&schema).unwrap(), "any");
}
#[test]
fn test_schema_value_to_typescript_type_date_format() {
let schema = serde_json::json!({"type": "string", "format": "date-time"});
assert_eq!(AutoGenerator::schema_value_to_typescript_type(&schema).unwrap(), "string");
}
#[test]
fn test_build_schema_for_entity_get_single() {
let generator = create_test_generator();
let schema = generator.build_schema_for_entity("user", "GET");
assert_eq!(schema["type"], "object");
assert!(schema["properties"]["id"].is_object());
assert!(schema["properties"]["name"].is_object());
}
#[test]
fn test_build_schema_for_entity_get_plural() {
let generator = create_test_generator();
let schema = generator.build_schema_for_entity("users", "GET");
assert_eq!(schema["type"], "array");
assert!(schema["items"].is_object());
}
#[test]
fn test_build_schema_for_entity_post() {
let generator = create_test_generator();
let schema = generator.build_schema_for_entity("user", "POST");
assert_eq!(schema["type"], "object");
assert!(schema["properties"]["status"].is_object());
}
#[test]
fn test_build_schema_for_entity_put() {
let generator = create_test_generator();
let schema = generator.build_schema_for_entity("user", "PUT");
assert_eq!(schema["type"], "object");
assert!(schema["properties"]["updated_at"].is_object());
}
#[test]
fn test_build_schema_for_entity_patch() {
let generator = create_test_generator();
let schema = generator.build_schema_for_entity("user", "PATCH");
assert_eq!(schema["type"], "object");
assert!(schema["properties"]["updated_at"].is_object());
}
#[test]
fn test_build_schema_for_entity_delete() {
let generator = create_test_generator();
let schema = generator.build_schema_for_entity("user", "DELETE");
assert_eq!(schema["type"], "object");
}
#[tokio::test]
async fn test_generate_intelligent_response_get_collection() {
let generator = create_test_generator();
let response = generator.generate_intelligent_response("GET", "/api/users/").await.unwrap();
assert!(response.is_array());
}
#[tokio::test]
async fn test_generate_intelligent_response_get_single() {
let generator = create_test_generator();
let response =
generator.generate_intelligent_response("GET", "/api/users/123").await.unwrap();
assert!(response.is_object());
assert_eq!(response["id"], "123");
}
#[tokio::test]
async fn test_generate_intelligent_response_post() {
let generator = create_test_generator();
let response = generator.generate_intelligent_response("POST", "/api/users").await.unwrap();
assert!(response.is_object());
assert_eq!(response["status"], "created");
}
#[tokio::test]
async fn test_generate_intelligent_response_put() {
let generator = create_test_generator();
let response =
generator.generate_intelligent_response("PUT", "/api/users/123").await.unwrap();
assert!(response.is_object());
assert!(response["name"].as_str().unwrap().contains("Updated"));
}
#[tokio::test]
async fn test_generate_intelligent_response_delete() {
let generator = create_test_generator();
let response = generator
.generate_intelligent_response("DELETE", "/api/users/123")
.await
.unwrap();
assert!(response.is_object());
assert_eq!(response["success"], true);
}
#[tokio::test]
async fn test_generate_intelligent_response_unknown_method() {
let generator = create_test_generator();
let response =
generator.generate_intelligent_response("OPTIONS", "/api/users").await.unwrap();
assert!(response.is_object());
assert!(response["message"].is_string());
}
#[test]
fn test_generate_typescript_interface_object() {
let schema = serde_json::json!({
"type": "object",
"properties": {
"id": {"type": "string"},
"name": {"type": "string"}
},
"required": ["id"]
});
let ts_code = AutoGenerator::generate_typescript_interface("User", &schema, "GET").unwrap();
assert!(ts_code.contains("export interface User"));
assert!(ts_code.contains("id: string"));
assert!(ts_code.contains("name?: string")); }
#[test]
fn test_generate_typescript_interface_array() {
let schema = serde_json::json!({
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {"type": "string"}
}
}
});
let ts_code =
AutoGenerator::generate_typescript_interface("Users", &schema, "GET").unwrap();
assert!(ts_code.contains("export type Users = UsersItem[]"));
}
#[test]
fn test_generate_json_schema() {
let generator = create_test_generator();
let schema = serde_json::json!({
"type": "object",
"properties": {
"id": {"type": "string"}
},
"required": ["id"]
});
let json_schema = generator.generate_json_schema("User", &schema).unwrap();
assert_eq!(json_schema["$schema"], "http://json-schema.org/draft-07/schema#");
assert_eq!(json_schema["title"], "User");
assert_eq!(json_schema["type"], "object");
}
#[test]
fn test_generate_scenario_manifest() {
let generator = create_test_generator();
let manifest = generator
.generate_scenario_manifest("test-scenario", "GET", "/api/users", "mock-123")
.unwrap();
assert_eq!(manifest["name"], "test-scenario");
assert_eq!(manifest["manifest_version"], "1.0");
assert!(manifest["metadata"]["auto_generated"].as_bool().unwrap());
}
#[test]
fn test_generate_scenario_config() {
let generator = create_test_generator();
let config = generator.generate_scenario_config("GET", "/api/users", "mock-123").unwrap();
assert!(config["http"]["enabled"].as_bool().unwrap());
assert!(config["http"]["mocks"].is_array());
}
#[test]
fn test_auto_generator_new() {
let config = RuntimeDaemonConfig::default();
let generator = AutoGenerator::new(config, "http://localhost:3000".to_string());
assert!(!generator.config.enabled);
assert_eq!(generator.management_api_url, "http://localhost:3000");
}
#[test]
fn test_auto_generator_with_custom_config() {
let config = RuntimeDaemonConfig {
enabled: true,
ai_generation: true,
generate_types: true,
generate_client_stubs: true,
workspace_dir: Some("/tmp/workspace".to_string()),
..Default::default()
};
let generator = AutoGenerator::new(config, "http://api.example.com".to_string());
assert!(generator.config.enabled);
assert!(generator.config.ai_generation);
assert!(generator.config.generate_types);
assert!(generator.config.generate_client_stubs);
assert_eq!(generator.config.workspace_dir, Some("/tmp/workspace".to_string()));
}
}