use crate::cli::InitCommand;
use crate::core::parser::UnifiedSpec;
use anyhow::{Context, Result};
use colored::*;
use std::fs;
use std::path::Path;
pub fn init_project(cmd: InitCommand) -> Result<()> {
let project_path = Path::new(&cmd.name);
println!(
"{} Initializing MicroRapid project: {}",
"🚀".bright_blue(),
cmd.name.bright_yellow()
);
if project_path.exists() && project_path.read_dir()?.count() > 0 && !cmd.force {
println!(
"{} Directory '{}' is not empty. Use --force to overwrite.",
"❌".red(),
project_path.display()
);
return Ok(());
}
if !project_path.exists() {
fs::create_dir_all(&project_path)?;
}
create_flat_structure(project_path)?;
if let Some(url) = &cmd.from_url {
download_and_setup_spec(project_path, url, cmd.allow_insecure)?;
} else if let Some(file) = &cmd.from_file {
copy_and_setup_spec(project_path, file)?;
}
println!("\n{} Project initialized successfully!", "✅".green());
print_next_steps(&cmd.name);
Ok(())
}
fn create_flat_structure(base: &Path) -> Result<()> {
fs::create_dir_all(base.join("specs"))?;
fs::create_dir_all(base.join("config"))?;
fs::create_dir_all(base.join("env"))?;
fs::create_dir_all(base.join("collections"))?;
fs::create_dir_all(base.join("tests"))?;
fs::create_dir_all(base.join("scripts"))?;
fs::create_dir_all(base.join("docs"))?;
create_manifest_file(base)?;
create_config_files(base)?;
create_env_files(base)?;
create_default_spec(base)?;
create_gitignore(base)?;
create_readme(base)?;
create_smoke_test(base)?;
Ok(())
}
fn create_manifest_file(base: &Path) -> Result<()> {
let content = r#"# MicroRapid Project Manifest
name: my-api
version: 1.0.0
description: My API project
default_spec: specs/api.yaml
default_env: development
# Project metadata
author: Your Name
repository: https://github.com/yourusername/my-api
"#;
fs::write(base.join("mrapids.yaml"), content)?;
println!(" {} Created mrapids.yaml", "✓".green());
Ok(())
}
fn create_config_files(base: &Path) -> Result<()> {
let default_config = r#"# Shared configuration for all environments
timeout_ms: 30000
retries: 2
# Common headers
headers:
User-Agent: mrapids/1.0
Accept: application/json
Content-Type: application/json
"#;
fs::write(base.join("config/default.yaml"), default_config)?;
println!(" {} Created config/default.yaml", "✓".green());
let dev_config = r#"# Development environment configuration
base_url: http://localhost:3000/api/v1
timeout_ms: 60000 # Longer timeout for debugging
headers:
X-Environment: development
X-Debug: true
# Add auth headers as needed (uncomment one):
# Authorization: Bearer ${DEV_API_TOKEN}
# Authorization: Basic ${DEV_BASIC_AUTH} # base64(username:password)
# X-API-Key: ${DEV_API_KEY}
"#;
fs::write(base.join("config/development.yaml"), dev_config)?;
println!(" {} Created config/development.yaml", "✓".green());
let staging_config = r#"# Staging environment configuration
base_url: https://staging.api.example.com/v1
headers:
X-Environment: staging
# Add auth headers as needed (uncomment one):
# Authorization: Bearer ${STAGING_API_TOKEN}
# Authorization: Basic ${STAGING_BASIC_AUTH} # base64(username:password)
# X-API-Key: ${STAGING_API_KEY}
"#;
fs::write(base.join("config/staging.yaml"), staging_config)?;
println!(" {} Created config/staging.yaml", "✓".green());
let prod_config = r#"# Production environment configuration
base_url: https://api.example.com/v1
timeout_ms: 10000 # Strict timeout for production
retries: 3
headers:
X-Environment: production
X-Request-ID: ${REQUEST_ID:-auto}
# Add auth headers as needed (uncomment one):
# Authorization: Bearer ${PROD_API_TOKEN}
# Authorization: Basic ${PROD_BASIC_AUTH} # base64(username:password)
# X-API-Key: ${PROD_API_KEY}
# Stricter rate limiting for production
rate_limit:
requests_per_second: 100
burst: 200
# Production proxy (optional)
proxy: ${HTTPS_PROXY:-}
"#;
fs::write(base.join("config/production.yaml"), prod_config)?;
println!(" {} Created config/production.yaml", "✓".green());
Ok(())
}
fn create_env_files(base: &Path) -> Result<()> {
let dev_env = r#"# Development environment variables
# Uncomment and set the auth method you need:
# For Bearer token:
# DEV_API_TOKEN=your-bearer-token-here
# For Basic auth (base64 encode username:password):
# DEV_BASIC_AUTH=dXNlcm5hbWU6cGFzc3dvcmQ=
# For API key:
# DEV_API_KEY=your-api-key-here
LOG_LEVEL=debug
DEBUG=true
"#;
fs::write(base.join("env/.env.development"), dev_env)?;
println!(" {} Created env/.env.development", "✓".green());
let staging_env = r#"# Staging environment variables
# Uncomment and set the auth method you need:
# For Bearer token:
# STAGING_API_TOKEN=your-staging-token-here
# For Basic auth (base64 encode username:password):
# STAGING_BASIC_AUTH=dXNlcm5hbWU6cGFzc3dvcmQ=
# For API key:
# STAGING_API_KEY=your-staging-api-key-here
LOG_LEVEL=info
"#;
fs::write(base.join("env/.env.staging"), staging_env)?;
println!(" {} Created env/.env.staging", "✓".green());
let prod_env = r#"# Production environment variables
# NOTE: Real secrets should be injected by CI/CD, not committed
# For Bearer token:
# PROD_API_TOKEN= # Set by CI/CD
# For Basic auth (base64 encode username:password):
# PROD_BASIC_AUTH= # Set by CI/CD
# For API key:
# PROD_API_KEY= # Set by CI/CD
LOG_LEVEL=error
"#;
fs::write(base.join("env/.env.production"), prod_env)?;
println!(" {} Created env/.env.production", "✓".green());
Ok(())
}
fn create_default_spec(base: &Path) -> Result<()> {
let content = r#"openapi: 3.0.3
info:
title: Sample API
version: 1.0.0
description: |
This is a sample API specification.
Replace this with your actual API specification.
servers:
- url: http://localhost:3000/api/v1
description: Development server
- url: https://api.example.com/v1
description: Production server
paths:
/health:
get:
operationId: healthCheck
summary: Health check endpoint
tags:
- System
responses:
'200':
description: Service is healthy
content:
application/json:
schema:
type: object
properties:
status:
type: string
example: healthy
timestamp:
type: string
format: date-time
/users:
get:
operationId: listUsers
summary: List all users
tags:
- Users
security:
- bearerAuth: []
- apiKey: []
parameters:
- name: limit
in: query
schema:
type: integer
default: 10
- name: offset
in: query
schema:
type: integer
default: 0
responses:
'200':
description: List of users
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/User'
components:
schemas:
User:
type: object
properties:
id:
type: string
format: uuid
name:
type: string
email:
type: string
format: email
createdAt:
type: string
format: date-time
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
apiKey:
type: apiKey
in: header
name: X-API-Key
"#;
fs::write(base.join("specs/api.yaml"), content)?;
println!(" {} Created specs/api.yaml", "✓".green());
Ok(())
}
fn create_gitignore(base: &Path) -> Result<()> {
let content = r#"# Environment files (keep templates, ignore actual values)
env/.env.local
env/.env.*.local
# Test outputs
tests/results/
tests/.coverage/
# Generated files
scripts/generated/
*.generated.*
# IDE
.vscode/
.idea/
*.swp
*.swo
.DS_Store
# Dependencies
node_modules/
vendor/
# Logs
*.log
logs/
# Temporary files
tmp/
temp/
"#;
fs::write(base.join(".gitignore"), content)?;
println!(" {} Created .gitignore", "✓".green());
Ok(())
}
fn create_readme(base: &Path) -> Result<()> {
let content = r#"# My API Project
Generated with [MicroRapid](https://github.com/microrapid/mrapids)
## Project Structure
```
.
├── mrapids.yaml # Project manifest
├── config/ # Environment configurations
│ ├── default.yaml # Shared defaults
│ ├── development.yaml # Development
│ ├── staging.yaml # Staging
│ └── production.yaml # Production
├── env/ # Environment variables (gitignored)
│ ├── .env.development
│ ├── .env.staging
│ └── .env.production
├── specs/ # API specifications
│ └── api.yaml
├── tests/ # Test files
│ └── smoke.test.js # Smoke tests
├── scripts/ # Generated scripts
└── docs/ # Documentation
└── README.md # This file
```
## Quick Start
```bash
# Install MicroRapid
npm install -g mrapids
# Run in development
mrapids run --env development
# Run specific operation
mrapids run users/list --env staging
# Generate SDK
mrapids gen sdk --language typescript
# Run tests
mrapids test
```
## Configuration
Configuration follows this precedence (highest to lowest):
1. CLI arguments
2. Environment variables
3. `env/.env.{environment}`
4. `config/{environment}.yaml`
5. `config/default.yaml`
6. `mrapids.yaml` defaults
## Authentication
Authentication is handled via headers in the config files.
Example:
```yaml
# config/development.yaml
headers:
X-Environment: development
Authorization: Bearer ${DEV_API_TOKEN} # From env/.env.development
X-API-Key: ${DEV_API_KEY} # From env/.env.development
```
## Environments
- **development**: Local development
- **staging**: Pre-production
- **production**: Production
Switch environments:
```bash
mrapids run --env production
# or
export MRAPIDS_ENV=production
mrapids run
```
"#;
fs::write(base.join("docs/README.md"), content)?;
println!(" {} Created docs/README.md", "✓".green());
Ok(())
}
fn download_and_setup_spec(base: &Path, url: &str, allow_insecure: bool) -> Result<()> {
use crate::utils::security::enforce_https;
if !allow_insecure {
enforce_https(url, false)?;
}
println!("{} Downloading spec from: {}", "📥".cyan(), url);
let client = reqwest::blocking::Client::new();
let response = client.get(url).send().context("Failed to download spec")?;
let content = response.text().context("Failed to read spec content")?;
fs::write(base.join("specs/api.yaml"), &content)?;
if let Ok(spec) = crate::core::parser::parse_spec(&content) {
update_configs_from_spec(base, &spec)?;
}
Ok(())
}
fn copy_and_setup_spec(base: &Path, file: &str) -> Result<()> {
println!("{} Copying spec from: {}", "📄".cyan(), file);
let content = fs::read_to_string(file).context("Failed to read spec file")?;
fs::write(base.join("specs/api.yaml"), &content)?;
if let Ok(spec) = crate::core::parser::parse_spec(&content) {
update_configs_from_spec(base, &spec)?;
}
Ok(())
}
fn update_configs_from_spec(base: &Path, spec: &UnifiedSpec) -> Result<()> {
let base_url = spec.get_base_url();
let dev_config_path = base.join("config/development.yaml");
if dev_config_path.exists() {
let content = fs::read_to_string(&dev_config_path)?;
let updated = content.replace(
"base_url: http://localhost:3000/api/v1",
&format!("base_url: {}", base_url),
);
fs::write(dev_config_path, updated)?;
}
println!(" {} Updated configs with spec information", "✓".green());
Ok(())
}
fn create_smoke_test(base: &Path) -> Result<()> {
let content = r#"// Smoke tests for API endpoints
// Run with: mrapids test
const assert = require('assert');
describe('API Smoke Tests', () => {
it('should check health endpoint', async () => {
const response = await mrapids.run('healthCheck');
assert.equal(response.status, 200);
assert.equal(response.data.status, 'healthy');
});
it('should authenticate successfully', async () => {
const response = await mrapids.auth.validate();
assert.equal(response.success, true);
});
it('should list users with authentication', async () => {
const response = await mrapids.run('listUsers', {
params: { limit: 10 }
});
assert.equal(response.status, 200);
assert(Array.isArray(response.data));
});
it('should handle errors gracefully', async () => {
try {
await mrapids.run('nonExistentEndpoint');
assert.fail('Should have thrown an error');
} catch (error) {
assert(error.message.includes('not found'));
}
});
});
// Environment-specific tests
if (process.env.MRAPIDS_ENV === 'production') {
describe('Production Tests', () => {
it('should enforce rate limits', async () => {
// Production-specific tests
});
});
}
"#;
fs::write(base.join("tests/smoke.test.js"), content)?;
println!(" {} Created tests/smoke.test.js", "✓".green());
Ok(())
}
fn print_next_steps(project_name: &str) {
println!("\n{}", "Next steps:".bold().cyan());
println!(" 1. cd {}", project_name);
println!(" 2. Edit config/development.yaml with your API settings");
println!(" 3. Update env/.env.development with your credentials");
println!(" 4. Run: mrapids run --env development");
println!(
"\n{} Tip: Use 'mrapids doctor' to check your setup",
"💡".yellow()
);
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::TempDir;
#[test]
fn test_create_flat_structure_creates_all_directories() {
let temp_dir = TempDir::new().unwrap();
let base = temp_dir.path();
let result = create_flat_structure(base);
assert!(result.is_ok());
assert!(base.join("specs").exists());
assert!(base.join("config").exists());
assert!(base.join("env").exists());
assert!(base.join("collections").exists());
assert!(base.join("tests").exists());
assert!(base.join("scripts").exists());
assert!(base.join("docs").exists());
}
#[test]
fn test_create_flat_structure_creates_manifest() {
let temp_dir = TempDir::new().unwrap();
let base = temp_dir.path();
create_flat_structure(base).unwrap();
let manifest_path = base.join("mrapids.yaml");
assert!(manifest_path.exists());
let content = fs::read_to_string(manifest_path).unwrap();
assert!(content.contains("name:"));
assert!(content.contains("version:"));
assert!(content.contains("default_spec:"));
}
#[test]
fn test_create_flat_structure_creates_config_files() {
let temp_dir = TempDir::new().unwrap();
let base = temp_dir.path();
create_flat_structure(base).unwrap();
assert!(base.join("config/default.yaml").exists());
assert!(base.join("config/development.yaml").exists());
assert!(base.join("config/staging.yaml").exists());
assert!(base.join("config/production.yaml").exists());
}
#[test]
fn test_create_flat_structure_creates_env_files() {
let temp_dir = TempDir::new().unwrap();
let base = temp_dir.path();
create_flat_structure(base).unwrap();
assert!(base.join("env/.env.development").exists());
assert!(base.join("env/.env.staging").exists());
assert!(base.join("env/.env.production").exists());
}
#[test]
fn test_create_flat_structure_creates_gitignore() {
let temp_dir = TempDir::new().unwrap();
let base = temp_dir.path();
create_flat_structure(base).unwrap();
let gitignore_path = base.join(".gitignore");
assert!(gitignore_path.exists());
let content = fs::read_to_string(gitignore_path).unwrap();
assert!(content.contains(".env.local"));
assert!(content.contains("node_modules/"));
}
#[test]
fn test_create_flat_structure_creates_default_spec() {
let temp_dir = TempDir::new().unwrap();
let base = temp_dir.path();
create_flat_structure(base).unwrap();
let spec_path = base.join("specs/api.yaml");
assert!(spec_path.exists());
let content = fs::read_to_string(spec_path).unwrap();
assert!(content.contains("openapi: 3.0.3"));
assert!(content.contains("healthCheck"));
}
#[test]
fn test_development_config_has_localhost_base_url() {
let temp_dir = TempDir::new().unwrap();
let base = temp_dir.path();
fs::create_dir_all(base.join("config")).unwrap();
create_config_files(base).unwrap();
let content = fs::read_to_string(base.join("config/development.yaml")).unwrap();
assert!(content.contains("localhost"));
assert!(content.contains("X-Debug: true"));
}
#[test]
fn test_production_config_has_stricter_settings() {
let temp_dir = TempDir::new().unwrap();
let base = temp_dir.path();
fs::create_dir_all(base.join("config")).unwrap();
create_config_files(base).unwrap();
let content = fs::read_to_string(base.join("config/production.yaml")).unwrap();
assert!(content.contains("timeout_ms: 10000")); assert!(content.contains("retries: 3"));
assert!(content.contains("rate_limit:"));
}
#[test]
fn test_manifest_has_required_fields() {
let temp_dir = TempDir::new().unwrap();
let base = temp_dir.path();
create_manifest_file(base).unwrap();
let content = fs::read_to_string(base.join("mrapids.yaml")).unwrap();
assert!(content.contains("name:"));
assert!(content.contains("version:"));
assert!(content.contains("default_spec: specs/api.yaml"));
assert!(content.contains("default_env: development"));
}
#[test]
fn test_smoke_test_file_created() {
let temp_dir = TempDir::new().unwrap();
let base = temp_dir.path();
fs::create_dir_all(base.join("tests")).unwrap();
create_smoke_test(base).unwrap();
let smoke_path = base.join("tests/smoke.test.js");
assert!(smoke_path.exists());
let content = fs::read_to_string(smoke_path).unwrap();
assert!(content.contains("healthCheck"));
assert!(content.contains("describe"));
assert!(content.contains("assert"));
}
#[test]
fn test_readme_created_in_docs() {
let temp_dir = TempDir::new().unwrap();
let base = temp_dir.path();
fs::create_dir_all(base.join("docs")).unwrap();
create_readme(base).unwrap();
let readme_path = base.join("docs/README.md");
assert!(readme_path.exists());
let content = fs::read_to_string(readme_path).unwrap();
assert!(content.contains("MicroRapid"));
assert!(content.contains("Quick Start"));
assert!(content.contains("Project Structure"));
}
#[test]
fn test_update_configs_from_spec() {
let temp_dir = TempDir::new().unwrap();
let base = temp_dir.path();
fs::create_dir_all(base.join("config")).unwrap();
create_config_files(base).unwrap();
let spec = UnifiedSpec {
info: crate::core::parser::ApiInfo {
title: "Test API".to_string(),
version: "1.0.0".to_string(),
description: None,
},
base_url: "https://api.test.com/v1".to_string(),
operations: vec![],
security_schemes: std::collections::HashMap::new(),
};
let result = update_configs_from_spec(base, &spec);
assert!(result.is_ok());
let content = fs::read_to_string(base.join("config/development.yaml")).unwrap();
assert!(content.contains("https://api.test.com/v1"));
}
#[test]
fn test_copy_and_setup_spec() {
let temp_dir = TempDir::new().unwrap();
let base = temp_dir.path();
create_flat_structure(base).unwrap();
let source_spec = r#"
openapi: "3.0.0"
info:
title: Test API
version: "1.0.0"
servers:
- url: https://api.example.com
paths:
/test:
get:
operationId: test
responses:
"200":
description: OK
"#;
let source_path = temp_dir.path().join("source_spec.yaml");
fs::write(&source_path, source_spec).unwrap();
let result = copy_and_setup_spec(base, source_path.to_str().unwrap());
assert!(result.is_ok());
let copied_spec = fs::read_to_string(base.join("specs/api.yaml")).unwrap();
assert!(copied_spec.contains("Test API"));
}
}