use crate::core::parser::{UnifiedOperation, UnifiedSpec};
use anyhow::Result;
use std::fs;
use std::path::Path;
pub fn generate(
spec: &UnifiedSpec,
output_dir: &Path,
with_tests: bool,
with_validation: bool,
) -> Result<()> {
fs::create_dir_all(output_dir.join("src"))?;
fs::create_dir_all(output_dir.join("src/routes"))?;
fs::create_dir_all(output_dir.join("src/middleware"))?;
fs::create_dir_all(output_dir.join("src/controllers"))?;
if with_tests {
fs::create_dir_all(output_dir.join("tests"))?;
}
generate_app(spec, output_dir)?;
generate_routes(spec, output_dir)?;
generate_middleware(spec, output_dir, with_validation)?;
generate_package_json(spec, output_dir)?;
generate_tsconfig(output_dir)?;
if with_tests {
generate_tests(spec, output_dir)?;
}
Ok(())
}
fn generate_app(spec: &UnifiedSpec, output_dir: &Path) -> Result<()> {
let content = format!(
r#"/**
* {} Express Server
* Generated by MicroRapid
*/
import express from 'express';
import cors from 'cors';
import helmet from 'helmet';
import {{ errorHandler }} from './middleware/errorHandler';
{}
const app = express();
// Middleware
app.use(helmet());
app.use(cors());
app.use(express.json());
app.use(express.urlencoded({{ extended: true }}));
// Routes
{}
// Health check
app.get('/health', (req, res) => {{
res.json({{ status: 'healthy', version: '{}' }});
}});
// Error handling
app.use(errorHandler);
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {{
console.log(`Server running on port ${{PORT}}`);
}});
export {{ app }};
"#,
spec.info.title,
generate_route_imports(&spec),
generate_route_uses(&spec),
spec.info.version
);
fs::write(output_dir.join("src/app.ts"), content)?;
Ok(())
}
fn generate_routes(spec: &UnifiedSpec, output_dir: &Path) -> Result<()> {
let mut route_groups: std::collections::HashMap<String, Vec<&UnifiedOperation>> =
std::collections::HashMap::new();
for operation in &spec.operations {
let group = operation
.path
.split('/')
.nth(1)
.unwrap_or("default")
.to_string();
route_groups
.entry(group)
.or_insert_with(Vec::new)
.push(operation);
}
for (group_name, operations) in route_groups {
generate_router(&group_name, &operations, output_dir)?;
}
Ok(())
}
fn generate_router(name: &str, operations: &[&UnifiedOperation], output_dir: &Path) -> Result<()> {
let mut route_handlers = String::new();
for op in operations {
route_handlers.push_str(&format!(
r#"
router.{}('{}', async (req: Request, res: Response, next: NextFunction) => {{
try {{
// TODO: Implement {} logic
res.json({{ message: 'Not implemented: {}' }});
}} catch (error) {{
next(error);
}}
}});
"#,
op.method.to_lowercase(),
op.path.replace(&format!("/{}", name), ""),
op.operation_id,
op.operation_id
));
}
let content = format!(
r#"/**
* {} Routes
* Generated by MicroRapid
*/
import {{ Router, Request, Response, NextFunction }} from 'express';
const router = Router();
{}
export default router;
"#,
to_title_case(name),
route_handlers
);
fs::write(output_dir.join(format!("src/routes/{}.ts", name)), content)?;
Ok(())
}
fn generate_middleware(
_spec: &UnifiedSpec,
output_dir: &Path,
with_validation: bool,
) -> Result<()> {
let error_handler = r#"import { Request, Response, NextFunction } from 'express';
export const errorHandler = (
err: Error,
req: Request,
res: Response,
next: NextFunction
) => {
console.error(err.stack);
const status = (err as any).status || 500;
const message = err.message || 'Internal Server Error';
res.status(status).json({
error: {
message,
status,
},
});
};
"#;
fs::write(
output_dir.join("src/middleware/errorHandler.ts"),
error_handler,
)?;
if with_validation {
let validator = r#"import { Request, Response, NextFunction } from 'express';
import { validationResult } from 'express-validator';
export const validate = (req: Request, res: Response, next: NextFunction) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
next();
};
"#;
fs::write(output_dir.join("src/middleware/validator.ts"), validator)?;
}
Ok(())
}
fn generate_package_json(spec: &UnifiedSpec, output_dir: &Path) -> Result<()> {
let content = format!(
r#"{{
"name": "{}",
"version": "{}",
"description": "{}",
"main": "dist/app.js",
"scripts": {{
"dev": "ts-node-dev --respawn --transpile-only src/app.ts",
"build": "tsc",
"start": "node dist/app.js",
"test": "jest"
}},
"dependencies": {{
"express": "^4.18.2",
"cors": "^2.8.5",
"helmet": "^7.1.0"
}},
"devDependencies": {{
"@types/express": "^4.17.21",
"@types/node": "^20.10.0",
"@types/cors": "^2.8.17",
"typescript": "^5.3.0",
"ts-node-dev": "^2.0.0",
"@types/jest": "^29.5.0",
"jest": "^29.7.0",
"ts-jest": "^29.1.0",
"supertest": "^6.3.0",
"@types/supertest": "^2.0.16"
}}
}}
"#,
to_kebab_case(&spec.info.title),
spec.info.version,
spec.info.description.as_deref().unwrap_or("")
);
fs::write(output_dir.join("package.json"), content)?;
Ok(())
}
fn generate_tsconfig(output_dir: &Path) -> Result<()> {
let content = r#"{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"lib": ["ES2020"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"moduleResolution": "node",
"allowSyntheticDefaultImports": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "tests"]
}
"#;
fs::write(output_dir.join("tsconfig.json"), content)?;
Ok(())
}
fn generate_tests(_spec: &UnifiedSpec, output_dir: &Path) -> Result<()> {
let content = r#"// Test setup
import { app } from '../src/app';
import request from 'supertest';
describe('API Tests', () => {
test('GET /health', async () => {
const res = await request(app).get('/health');
expect(res.statusCode).toBe(200);
expect(res.body).toHaveProperty('status', 'healthy');
});
});
"#;
fs::write(output_dir.join("tests/api.test.ts"), content)?;
Ok(())
}
fn generate_route_imports(spec: &UnifiedSpec) -> String {
let mut imports = vec![];
let mut seen = std::collections::HashSet::new();
for op in &spec.operations {
let group = op.path.split('/').nth(1).unwrap_or("default").to_string();
if seen.insert(group.clone()) {
imports.push(format!("import {} from './routes/{}'", group, group));
}
}
imports.join(";\n")
}
fn generate_route_uses(spec: &UnifiedSpec) -> String {
let mut uses = vec![];
let mut seen = std::collections::HashSet::new();
for op in &spec.operations {
let group = op.path.split('/').nth(1).unwrap_or("default").to_string();
if seen.insert(group.clone()) {
uses.push(format!("app.use('/{0}', {0})", group));
}
}
uses.join(";\n")
}
fn to_title_case(s: &str) -> String {
s.split('_')
.map(|word| {
let mut chars = word.chars();
match chars.next() {
None => String::new(),
Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
}
})
.collect::<Vec<_>>()
.join(" ")
}
fn to_kebab_case(s: &str) -> String {
let mut result = String::new();
let mut prev_is_lower = false;
for ch in s.chars() {
if ch.is_uppercase() && prev_is_lower {
result.push('-');
}
result.push(ch.to_lowercase().next().unwrap());
prev_is_lower = ch.is_lowercase();
}
result
}