use anyhow::{Context, Result};
use chrono::Utc;
use mockforge_core::codegen::backend_generator::{
extract_routes, extract_schemas, generate_handler_name, sanitize_name, schema_to_rust_type,
to_pascal_case, to_snake_case, RouteInfo,
};
use mockforge_core::openapi::spec::OpenApiSpec;
use mockforge_plugin_core::backend_generator::{
BackendGenerationMetadata, BackendGenerationResult, BackendGeneratorConfig,
BackendGeneratorPlugin, Complexity, TodoCategory, TodoItem,
};
use mockforge_plugin_core::types::{PluginError, PluginMetadata};
use mockforge_plugin_core::GeneratedFile;
use openapiv3::{ReferenceOr, Schema, SchemaKind, Type};
use std::collections::HashMap;
pub struct RustAxumGenerator;
impl RustAxumGenerator {
pub fn new() -> Self {
Self
}
}
#[async_trait::async_trait]
impl BackendGeneratorPlugin for RustAxumGenerator {
fn backend_type(&self) -> &str {
"rust-axum"
}
fn backend_name(&self) -> &str {
"Rust/Axum"
}
fn supported_spec_versions(&self) -> Vec<&str> {
vec!["3.0.0", "3.0.1", "3.0.2", "3.0.3", "3.1.0"]
}
fn supported_extensions(&self) -> Vec<&str> {
vec!["rs", "toml"]
}
fn default_port(&self) -> u16 {
3000
}
fn supports_database(&self, db_type: &str) -> bool {
matches!(db_type, "postgres" | "mysql" | "sqlite" | "mongodb")
}
async fn generate_backend(
&self,
_spec: &mockforge_plugin_core::client_generator::OpenApiSpec,
_config: &BackendGeneratorConfig,
) -> Result<BackendGenerationResult, PluginError> {
Err(PluginError::ConfigurationError {
message: "Use generate_rust_axum_backend with core OpenApiSpec".to_string(),
})
}
async fn get_metadata(&self) -> PluginMetadata {
PluginMetadata::new("Rust/Axum Backend Generator")
.with_capability("backend_generator")
.with_prefix("rust-axum")
}
async fn validate_config(&self, config: &BackendGeneratorConfig) -> Result<(), PluginError> {
if config.output_dir.is_empty() {
return Err(PluginError::ConfigurationError {
message: "output_dir cannot be empty".to_string(),
});
}
Ok(())
}
}
pub async fn generate_rust_axum_backend(
spec: &OpenApiSpec,
config: &BackendGeneratorConfig,
) -> Result<BackendGenerationResult> {
let routes = extract_routes(spec).context("Failed to extract routes from OpenAPI spec")?;
let schemas = extract_schemas(spec);
let mut files = Vec::new();
let mut todos = Vec::new();
let warnings = Vec::new();
let port = config.port.unwrap_or(3000);
files.push(generate_cargo_toml(spec, config, port)?);
let (main_rs, main_todos) = generate_main_rs(spec, config, port, &routes)?;
files.push(main_rs);
todos.extend(main_todos);
let (model_files, model_todos) = generate_models(&schemas, config)?;
files.extend(model_files);
todos.extend(model_todos);
let (handler_files, handler_todos) = generate_handlers(&routes, spec, config)?;
files.extend(handler_files);
todos.extend(handler_todos);
let (routes_file, routes_todos) = generate_routes_file(&routes, config)?;
files.push(routes_file);
todos.extend(routes_todos);
files.push(generate_errors_rs(config)?);
files.push(generate_env_example(config, port)?);
files.push(generate_readme(spec, config, port)?);
if config.generate_todo_md {
files.push(generate_todo_md(spec, config, &todos, &routes)?);
}
let metadata = BackendGenerationMetadata {
framework: "rust-axum".to_string(),
backend_name: sanitize_name(spec.spec.info.title.as_str()),
api_title: spec.spec.info.title.clone(),
api_version: spec.spec.info.version.clone(),
operation_count: routes.len(),
schema_count: schemas.len(),
default_port: port,
};
Ok(BackendGenerationResult {
files,
warnings,
metadata,
todos,
})
}
fn generate_cargo_toml(
spec: &OpenApiSpec,
config: &BackendGeneratorConfig,
_port: u16,
) -> Result<GeneratedFile> {
let project_name = sanitize_name(&spec.spec.info.title);
let version = &spec.spec.info.version;
let db_deps = if let Some(db_type) = &config.database {
match db_type.as_str() {
"postgres" => {
r#"
# Database (PostgreSQL)
sqlx = { version = "0.7", features = ["runtime-tokio-native-tls", "postgres", "chrono", "uuid"] }
diesel = { version = "2.1", features = ["postgres", "chrono", "uuid", "r2d2"] }
"#
}
"mysql" => {
r#"
# Database (MySQL)
sqlx = { version = "0.7", features = ["runtime-tokio-native-tls", "mysql", "chrono", "uuid"] }
diesel = { version = "2.1", features = ["mysql", "chrono", "uuid", "r2d2"] }
"#
}
"sqlite" => {
r#"
# Database (SQLite)
sqlx = { version = "0.7", features = ["runtime-tokio-native-tls", "sqlite", "chrono", "uuid"] }
diesel = { version = "2.1", features = ["sqlite", "chrono", "uuid"] }
"#
}
_ => "",
}
} else {
""
};
let content = format!(
r#"[package]
name = "{}"
version = "{}"
edition = "2021"
authors = ["Generated by MockForge"]
[dependencies]
# Web framework
axum = {{ version = "0.7", features = ["macros", "json"] }}
tokio = {{ version = "1.0", features = ["full"] }}
tower = "0.5"
tower-http = {{ version = "0.6", features = ["cors", "trace"] }}
# Serialization
serde = {{ version = "1.0", features = ["derive"] }}
serde_json = "1.0"
# Error handling
anyhow = "1.0"
thiserror = "2.0"
# Utilities
chrono = {{ version = "0.4", features = ["serde"] }}
uuid = {{ version = "1.0", features = ["v4", "serde"] }}
# Logging
tracing = "0.1"
tracing-subscriber = {{ version = "0.3", features = ["env-filter", "fmt"] }}
{}# Environment variables
dotenv = "0.15"
[dev-dependencies]
# Testing
tokio-test = "0.4"
"#,
project_name, version, db_deps
);
Ok(GeneratedFile {
path: "Cargo.toml".to_string(),
content,
file_type: "toml".to_string(),
})
}
fn generate_main_rs(
spec: &OpenApiSpec,
_config: &BackendGeneratorConfig,
port: u16,
routes: &[RouteInfo],
) -> Result<(GeneratedFile, Vec<TodoItem>)> {
let mut todos = Vec::new();
let app_name = sanitize_name(&spec.spec.info.title);
let handler_count = routes.len();
let addr_str = format!("http://0.0.0.0:{}", port);
let content = format!(
r#"//! {} Backend Server
//!
//! Generated by MockForge from OpenAPI specification
//! API: {} v{}
//!
//! This server was auto-generated. Implement the TODO items in handlers/ to add business logic.
use axum::{{extract::State, http::StatusCode, response::IntoResponse, routing::get, Router}};
use std::{{net::SocketAddr, sync::Arc}};
use tracing::info;
use tower_http::cors::CorsLayer;
pub mod errors;
pub mod handlers;
pub mod models;
pub mod routes;
/// Application state
#[derive(Clone)]
pub struct AppState {{
/// OpenAPI specification (for validation and documentation)
pub openapi_spec: Arc<serde_json::Value>,
// Add your application state here
// Example: database connection pool, cache, configuration, etc.
// pub db: Arc<Pool<Postgres>>,
// pub cache: Arc<RedisClient>,
}}
impl AppState {{
pub fn new() -> Self {{
// Initialize OpenAPI spec
// In a real application, you would embed the OpenAPI spec at compile time
// or load it from a file at runtime
// Build OpenAPI spec JSON with title and version
let mut spec_map = serde_json::Map::new();
spec_map.insert("openapi".to_string(), serde_json::Value::String("3.0.0".to_string()));
let mut info_map = serde_json::Map::new();
info_map.insert("title".to_string(), serde_json::Value::String("{}".to_string()));
info_map.insert("version".to_string(), serde_json::Value::String("{}".to_string()));
spec_map.insert("info".to_string(), serde_json::Value::Object(info_map));
spec_map.insert("paths".to_string(), serde_json::Value::Object(serde_json::Map::new()));
let spec_value = serde_json::Value::Object(spec_map);
Self {{
openapi_spec: Arc::new(spec_value),
// Initialize your application state here
// Example:
// db: Arc::new(create_db_pool().await?),
// cache: Arc::new(RedisClient::connect("redis://localhost").await?),
}}
}}
/// Get OpenAPI specification as JSON value
pub fn get_openapi_spec(&self) -> &serde_json::Value {{
&self.openapi_spec
}}
}}
#[tokio::main]
async fn main() -> anyhow::Result<()> {{
// Initialize logging
tracing_subscriber::fmt()
.with_env_filter("info")
.init();
// Load environment variables
dotenv::dotenv().ok();
// Create application state
let state = AppState::new();
// Build router with all routes
let app = create_router(state);
// Start server
let addr = SocketAddr::from(([0, 0, 0, 0], {}));
info!("🚀 Server starting on {{}}");
info!("📚 {} endpoints available", {});
let listener = tokio::net::TcpListener::bind(addr).await?;
axum::serve(listener, app).await?;
Ok(())
}}
/// Create the Axum router with all routes
fn create_router(state: AppState) -> Router {{
routes::create_routes()
.layer(CorsLayer::permissive())
.with_state(Arc::new(state))
}}
/// Health check endpoint
async fn health_check() -> impl IntoResponse {{
(StatusCode::OK, "OK")
}}
"#,
app_name,
spec.spec.info.title,
spec.spec.info.version,
port,
handler_count,
addr_str,
spec.spec.info.title,
spec.spec.info.version
);
todos.push(TodoItem {
description: "Initialize application state (database, cache, etc.)".to_string(),
file_path: "src/main.rs".to_string(),
line_number: 30,
related_operation: None,
category: TodoCategory::Config,
definition_of_done: vec![
"OpenAPI spec is already initialized in AppState".to_string(),
"Add database connection pool to AppState if needed".to_string(),
"Initialize database connection in AppState::new()".to_string(),
"Handle connection errors gracefully".to_string(),
"Add connection health checks".to_string(),
],
complexity: Complexity::Low,
dependencies: Vec::new(),
});
Ok((
GeneratedFile {
path: "src/main.rs".to_string(),
content,
file_type: "rust".to_string(),
},
todos,
))
}
fn generate_models(
schemas: &HashMap<String, Schema>,
config: &BackendGeneratorConfig,
) -> Result<(Vec<GeneratedFile>, Vec<TodoItem>)> {
let mut files = Vec::new();
let mut todos = Vec::new();
if schemas.is_empty() {
let content = r#"//! Data models
//!
//! This module contains data models generated from OpenAPI schemas.
//! Add your custom models here as needed.
use serde::{Deserialize, Serialize};
// TODO: Add models generated from OpenAPI schemas
"#
.to_string();
files.push(GeneratedFile {
path: "src/models/mod.rs".to_string(),
content,
file_type: "rust".to_string(),
});
return Ok((files, todos));
}
let mut mod_content =
String::from("//! Data models\n//!\n//! Generated from OpenAPI schemas\n\n");
mod_content.push_str("use serde::{Deserialize, Serialize};\n\n");
for (name, schema) in schemas {
let struct_name = to_pascal_case(name);
mod_content.push_str(&format!("pub mod {};\n", to_snake_case(name)));
mod_content.push_str(&format!("pub use {}::{};\n\n", to_snake_case(name), struct_name));
let (model_content, model_todos) = generate_schema_struct(name, schema, config)?;
files.push(GeneratedFile {
path: format!("src/models/{}.rs", to_snake_case(name)),
content: model_content,
file_type: "rust".to_string(),
});
todos.extend(model_todos);
}
files.push(GeneratedFile {
path: "src/models/mod.rs".to_string(),
content: mod_content,
file_type: "rust".to_string(),
});
Ok((files, todos))
}
fn generate_schema_struct(
name: &str,
schema: &Schema,
_config: &BackendGeneratorConfig,
) -> Result<(String, Vec<TodoItem>)> {
let mut todos = Vec::new();
let struct_name = to_pascal_case(name);
let mut content =
format!("//! {} model\n//!\n//! Generated from OpenAPI schema\n\n", struct_name);
content.push_str("use serde::{Deserialize, Serialize};\n\n");
if let SchemaKind::Type(Type::Object(obj)) = &schema.schema_kind {
content.push_str("#[derive(Debug, Clone, Serialize, Deserialize)]\n");
content.push_str(&format!("pub struct {} {{\n", struct_name));
for (prop_name, prop_schema_ref) in &obj.properties {
let is_required = obj.required.contains(prop_name);
let prop_type = match prop_schema_ref {
ReferenceOr::Item(prop_schema) => schema_to_rust_type(
prop_schema,
Some(&format!("{}_{}", struct_name, prop_name)),
),
ReferenceOr::Reference { reference } => {
if let Some(ref_name) = reference.strip_prefix("#/components/schemas/") {
to_pascal_case(ref_name)
} else {
"serde_json::Value".to_string()
}
}
};
let optional = if is_required { "" } else { "Option<" };
let optional_close = if is_required { "" } else { ">" };
content.push_str(&format!(" /// {}\n", prop_name));
content.push_str(&format!(
" pub {}: {}{}{},\n",
to_snake_case(prop_name),
optional,
prop_type,
optional_close
));
}
content.push_str("}\n");
} else {
let rust_type = schema_to_rust_type(schema, Some(name));
content.push_str(&format!("pub type {} = {};\n", struct_name, rust_type));
}
todos.push(TodoItem {
description: format!("Add database annotations and relationships for {}", struct_name),
file_path: format!("src/models/{}.rs", to_snake_case(name)),
line_number: 5,
related_operation: None,
category: TodoCategory::Model,
definition_of_done: vec![
format!("Add ORM table mapping for {}", struct_name),
"Define primary key constraint".to_string(),
"Add indexes for frequently queried fields".to_string(),
"Set up relationships if applicable".to_string(),
],
complexity: Complexity::Medium,
dependencies: Vec::new(),
});
Ok((content, todos))
}
fn generate_handlers(
routes: &[RouteInfo],
spec: &OpenApiSpec,
config: &BackendGeneratorConfig,
) -> Result<(Vec<GeneratedFile>, Vec<TodoItem>)> {
let mut files = Vec::new();
let mut todos = Vec::new();
let mut routes_by_tag: HashMap<String, Vec<&RouteInfo>> = HashMap::new();
for route in routes {
if route.tags.is_empty() {
routes_by_tag.entry("default".to_string()).or_default().push(route);
} else {
for tag in &route.tags {
routes_by_tag.entry(tag.clone()).or_default().push(route);
}
}
}
let mut mod_content =
String::from("//! Request handlers\n//!\n//! Generated from OpenAPI operations\n\n");
for (tag, tag_routes) in &routes_by_tag {
let file_name = to_snake_case(tag);
mod_content.push_str(&format!("pub mod {};\n", file_name));
mod_content.push_str(&format!("pub use {}::*;\n\n", file_name));
let mut handler_content =
format!("//! {} handlers\n//!\n//! Generated from OpenAPI operations\n\n", tag);
handler_content.push_str("use axum::{extract::{Path, Query, State}, http::StatusCode, response::IntoResponse, Json};\n");
handler_content.push_str("use serde_json::Value;\n");
handler_content.push_str("use std::sync::Arc;\n");
handler_content.push_str("use crate::errors::ApiError;\n");
handler_content.push_str("use crate::models::*;\n");
handler_content.push_str("use crate::AppState;\n\n");
for route in tag_routes {
let (handler_code, handler_todos) = generate_handler_function(route, spec, config)?;
handler_content.push_str(&handler_code);
handler_content.push_str("\n\n");
todos.extend(handler_todos);
}
files.push(GeneratedFile {
path: format!("src/handlers/{}.rs", file_name),
content: handler_content,
file_type: "rust".to_string(),
});
}
files.push(GeneratedFile {
path: "src/handlers/mod.rs".to_string(),
content: mod_content,
file_type: "rust".to_string(),
});
Ok((files, todos))
}
fn generate_handler_function(
route: &RouteInfo,
_spec: &OpenApiSpec,
_config: &BackendGeneratorConfig,
) -> Result<(String, Vec<TodoItem>)> {
let mut todos = Vec::new();
let handler_name = generate_handler_name(route);
let mut code = String::new();
code.push_str(&format!("/// Handler for {} {}\n", route.method, route.path));
if let Some(summary) = &route.summary {
code.push_str(&format!("/// \n/// {}\n", summary));
}
if let Some(desc) = &route.description {
code.push_str(&format!("/// \n/// {}\n", desc));
}
code.push_str(&format!("/// \n/// Tags: {}\n", route.tags.join(", ")));
code.push_str(&format!("pub async fn {}(\n", handler_name));
code.push_str(" State(state): State<Arc<AppState>>,\n");
if !route.path_params.is_empty() {
if route.path_params.len() == 1 {
let param = &route.path_params[0];
code.push_str(&format!(" Path({}): Path<String>,\n", to_snake_case(param)));
} else {
code.push_str(" Path(params): Path<std::collections::HashMap<String, String>>,\n");
}
}
if !route.query_params.is_empty() {
code.push_str(" Query(query): Query<std::collections::HashMap<String, String>>,\n");
}
if matches!(route.method.as_str(), "POST" | "PUT" | "PATCH")
&& route.request_body_schema.is_some()
{
code.push_str(" Json(body): Json<Value>,\n");
}
if code.ends_with(",\n") {
code.pop();
code.pop();
code.push('\n');
}
code.push_str(") -> Result<(StatusCode, Json<serde_json::Value>), ApiError> {\n");
code.push_str(" // TODO: Implement business logic\n");
match route.method.as_str() {
"GET" => {
if !route.path_params.is_empty() {
code.push_str(" // TODO: Query database for resource\n");
code.push_str(" // TODO: Handle case where resource not found (404)\n");
} else {
code.push_str(" // TODO: Query database for list of resources\n");
code.push_str(" // TODO: Apply pagination if needed\n");
code.push_str(" // TODO: Apply filtering/sorting if needed\n");
}
}
"POST" => {
code.push_str(" // TODO: Validate request body\n");
code.push_str(" // TODO: Create new resource in database\n");
code.push_str(" // TODO: Return created resource with 201 status\n");
}
"PUT" | "PATCH" => {
code.push_str(" // TODO: Validate request body\n");
code.push_str(" // TODO: Update resource in database\n");
code.push_str(" // TODO: Handle case where resource not found (404)\n");
code.push_str(" // TODO: Return updated resource\n");
}
"DELETE" => {
code.push_str(" // TODO: Delete resource from database\n");
code.push_str(" // TODO: Handle case where resource not found (404)\n");
code.push_str(" // TODO: Return 204 No Content or deleted resource\n");
}
_ => {}
}
code.push_str(" // TODO: Add authorization check if needed\n");
code.push_str(" // TODO: Add logging\n");
code.push_str(" // TODO: Handle errors properly\n");
code.push('\n');
code.push_str(" // Placeholder response (remove when implementing)\n");
let status_code = route.responses.keys().next().copied().unwrap_or(200);
code.push_str(&format!(
" Ok((StatusCode::from_u16({}).unwrap(), Json(serde_json::json!({{\n",
status_code
));
code.push_str(" \"message\": \"TODO: Implement this endpoint\",\n");
if !route.path_params.is_empty() {
if route.path_params.len() == 1 {
code.push_str(&format!(
" \"path_param\": {},\n",
to_snake_case(&route.path_params[0])
));
} else {
code.push_str(" \"path_params\": params,\n");
}
}
code.push_str(" }))))\n");
code.push_str("}\n");
todos.push(TodoItem {
description: format!("Implement {} handler", handler_name),
file_path: format!(
"src/handlers/{}.rs",
to_snake_case(route.tags.first().unwrap_or(&"default".to_string()))
),
line_number: 15,
related_operation: route.operation_id.clone(),
category: TodoCategory::Handler,
definition_of_done: vec![
"Implement business logic".to_string(),
format!("Query/update database for {}", route.path),
"Handle errors properly (404, 400, etc.)".to_string(),
"Add authorization if needed".to_string(),
"Add logging".to_string(),
"Write unit tests".to_string(),
],
complexity: match route.method.as_str() {
"GET" if route.path_params.is_empty() => Complexity::Medium,
"GET" => Complexity::Low,
"POST" => Complexity::Medium,
"PUT" | "PATCH" => Complexity::Medium,
"DELETE" => Complexity::Low,
_ => Complexity::Low,
},
dependencies: Vec::new(),
});
Ok((code, todos))
}
fn generate_routes_file(
routes: &[RouteInfo],
_config: &BackendGeneratorConfig,
) -> Result<(GeneratedFile, Vec<TodoItem>)> {
let todos = Vec::new();
let mut content =
String::from("//! Route definitions\n//!\n//! Generated from OpenAPI paths\n\n");
content.push_str("use axum::{routing::{get, post, put, patch, delete}, Router};\n");
content.push_str("use crate::handlers::*;\n\n");
content.push_str("/// Create router with all routes\n");
content.push_str("/// Note: State will be added in main.rs via .with_state()\n");
content.push_str("pub fn create_routes() -> Router {\n");
content.push_str(" Router::new()\n");
for route in routes {
let handler_name = generate_handler_name(route);
let method = route.method.to_lowercase();
let axum_path = format_axum_path(&route.path, &route.path_params);
content.push_str(&format!(
" .route(\"{}\", {}({}))\n",
axum_path, method, handler_name
));
}
content.push_str("}\n");
Ok((
GeneratedFile {
path: "src/routes.rs".to_string(),
content,
file_type: "rust".to_string(),
},
todos,
))
}
fn format_axum_path(path: &str, path_params: &[String]) -> String {
let mut axum_path = path.to_string();
for param in path_params {
axum_path = axum_path.replace(&format!("{{{}}}", param), &format!(":{}", param));
}
axum_path
}
fn generate_errors_rs(_config: &BackendGeneratorConfig) -> Result<GeneratedFile> {
let content = r#"//! API Error types
//!
//! Error handling for the API
use axum::{http::StatusCode, response::IntoResponse, Json};
use serde_json::json;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum ApiError {
#[error("Not Found: {0}")]
NotFound(String),
#[error("Bad Request: {0}")]
BadRequest(String),
#[error("Unauthorized: {0}")]
Unauthorized(String),
#[error("Internal Server Error: {0}")]
InternalServerError(String),
#[error("Validation Error: {0}")]
ValidationError(String),
}
impl IntoResponse for ApiError {
fn into_response(self) -> axum::response::Response {
let (status, error_message) = match self {
ApiError::NotFound(msg) => (StatusCode::NOT_FOUND, msg),
ApiError::BadRequest(msg) => (StatusCode::BAD_REQUEST, msg),
ApiError::Unauthorized(msg) => (StatusCode::UNAUTHORIZED, msg),
ApiError::InternalServerError(msg) => (StatusCode::INTERNAL_SERVER_ERROR, msg),
ApiError::ValidationError(msg) => (StatusCode::BAD_REQUEST, msg),
};
let body = Json(json!({
"error": error_message,
"status": status.as_u16(),
}));
(status, body).into_response()
}
}
"#
.to_string();
Ok(GeneratedFile {
path: "src/errors.rs".to_string(),
content,
file_type: "rust".to_string(),
})
}
fn generate_env_example(_config: &BackendGeneratorConfig, port: u16) -> Result<GeneratedFile> {
let db_config = if let Some(db_type) = &_config.database {
match db_type.as_str() {
"postgres" => "DATABASE_URL=postgres://user:password@localhost:5432/dbname\n",
"mysql" => "DATABASE_URL=mysql://user:password@localhost:3306/dbname\n",
"sqlite" => "DATABASE_URL=sqlite:./database.db\n",
_ => "",
}
} else {
"# DATABASE_URL=postgres://user:password@localhost:5432/dbname\n"
};
let content = format!(
r#"# Server Configuration
PORT={}
# Database Configuration
{}
# Logging
RUST_LOG=info
# Add your environment variables here
"#,
port, db_config
);
Ok(GeneratedFile {
path: ".env.example".to_string(),
content,
file_type: "env".to_string(),
})
}
fn generate_readme(
spec: &OpenApiSpec,
config: &BackendGeneratorConfig,
port: u16,
) -> Result<GeneratedFile> {
let project_name = sanitize_name(&spec.spec.info.title);
let description = spec.spec.info.description.as_deref().unwrap_or("Generated backend server");
let content = format!(
r#"# {}
{}
Generated by MockForge from OpenAPI specification.
## Quick Start
1. **Install dependencies:**
```bash
cargo build
```
2. **Set up environment:**
```bash
cp .env.example .env
# Edit .env with your configuration
```
3. **Run the server:**
```bash
cargo run
```
The server will start on `http://localhost:{}`
## Project Structure
```
src/
├── main.rs # Server entry point and setup
├── handlers/ # Request handlers (TODO: implement business logic)
├── models/ # Data models (generated from OpenAPI schemas)
├── routes.rs # Route definitions
└── errors.rs # Error types and handling
```
## Development
### Implementing Handlers
Each handler in `src/handlers/` contains TODO comments indicating what needs to be implemented:
- Database queries
- Business logic
- Authorization checks
- Error handling
### Adding Database Support
If you specified a database type during generation, uncomment the database dependencies in `Cargo.toml` and configure the connection in `src/main.rs`.
### Testing
```bash
cargo test
```
## API Endpoints
Generated from OpenAPI specification:
- **API Title:** {}
- **API Version:** {}
- **Total Endpoints:** {}
See the OpenAPI specification for detailed endpoint documentation.
## Next Steps
1. Review and implement TODOs in `TODO.md`
2. Set up your database connection
3. Implement business logic in handlers
4. Add authentication/authorization as needed
5. Write tests
6. Deploy!
## Generated By
MockForge - API Mocking and Testing Platform
"#,
project_name,
description,
port,
spec.spec.info.title,
spec.spec.info.version,
config.port.unwrap_or(0)
);
Ok(GeneratedFile {
path: "README.md".to_string(),
content,
file_type: "markdown".to_string(),
})
}
fn generate_todo_md(
spec: &OpenApiSpec,
_config: &BackendGeneratorConfig,
todos: &[TodoItem],
routes: &[RouteInfo],
) -> Result<GeneratedFile> {
let _project_name = sanitize_name(&spec.spec.info.title);
let date_str = Utc::now().format("%Y-%m-%d").to_string();
let mut content = format!(
r#"# Backend Implementation TODO List
Generated from: `{}`
Backend Type: Rust/Axum
Generated: {}
## Overview
This file contains all implementation TODOs extracted from generated code.
Each TODO includes:
- Location in codebase
- Description
- Related endpoints/operations
- Definition of Done (DoD) criteria
- Estimated complexity
---
## Endpoints
"#,
spec.spec.info.title, date_str
);
let mut todos_by_operation: HashMap<Option<String>, Vec<&TodoItem>> = HashMap::new();
for todo in todos {
todos_by_operation.entry(todo.related_operation.clone()).or_default().push(todo);
}
for route in routes {
let operation_id = route.operation_id.as_ref();
if let Some(op_todos) = todos_by_operation.get(&operation_id.cloned()) {
content.push_str(&format!(
"### {} {} - `{}`\n\n",
route.method,
route.path,
generate_handler_name(route)
));
if let Some(summary) = &route.summary {
content.push_str(&format!("**Summary:** {}\n\n", summary));
}
content.push_str(&format!("**File:** `{}`\n\n", op_todos[0].file_path));
content.push_str("**TODOs:**\n\n");
for (idx, todo) in op_todos.iter().enumerate() {
content.push_str(&format!("{}. **{}**\n", idx + 1, todo.description));
content.push_str(&format!(
" - Location: `{}:{}`\n",
todo.file_path, todo.line_number
));
content.push_str(&format!(" - Category: {}\n", todo.category));
content.push_str(&format!(" - Complexity: {}\n", todo.complexity));
content.push_str(" - DoD:\n");
for dod in &todo.definition_of_done {
content.push_str(&format!(" - [ ] {}\n", dod));
}
if !todo.dependencies.is_empty() {
content.push_str(&format!(
" - Dependencies: {}\n",
todo.dependencies.join(", ")
));
}
content.push('\n');
}
content.push_str("---\n\n");
}
}
let other_todos: Vec<_> = todos.iter().filter(|t| t.related_operation.is_none()).collect();
if !other_todos.is_empty() {
content.push_str("## Other Tasks\n\n");
for todo in &other_todos {
content.push_str(&format!("### {}\n\n", todo.description));
content.push_str(&format!("- **File:** `{}:{}`\n", todo.file_path, todo.line_number));
content.push_str(&format!("- **Category:** {}\n", todo.category));
content.push_str(&format!("- **Complexity:** {}\n", todo.complexity));
content.push_str("- **DoD:**\n");
for dod in &todo.definition_of_done {
content.push_str(&format!(" - [ ] {}\n", dod));
}
content.push('\n');
}
}
let handler_todos =
todos.iter().filter(|t| matches!(t.category, TodoCategory::Handler)).count();
let model_todos = todos.iter().filter(|t| matches!(t.category, TodoCategory::Model)).count();
let config_todos = todos.iter().filter(|t| matches!(t.category, TodoCategory::Config)).count();
let test_todos = todos.iter().filter(|t| matches!(t.category, TodoCategory::Test)).count();
content.push_str(&format!(
r#"## Summary
- Total TODOs: {}
- Endpoints: {} TODOs
- Models: {} TODOs
- Configuration: {} TODOs
- Testing: {} TODOs
## Priority Recommendations
1. **High Priority**: Database connection setup, basic CRUD operations
2. **Medium Priority**: Authorization, pagination, validation
3. **Low Priority**: Advanced features, optimization
## AI Agent Instructions
This TODO list is optimized for AI-assisted development. When implementing:
1. Work through TODOs in priority order
2. Mark items complete by checking [x] in source code
3. Update this file when TODOs are completed
4. Generate tests for each implemented feature
5. Ensure DoD criteria are met before marking complete
"#,
todos.len(),
handler_todos,
model_todos,
config_todos,
test_todos
));
Ok(GeneratedFile {
path: "TODO.md".to_string(),
content,
file_type: "markdown".to_string(),
})
}