use clap::Subcommand;
use mockforge_core::{
build_file_naming_context, process_generated_file, BarrelGenerator, GeneratedFile, OpenApiSpec,
};
use std::path::PathBuf;
use crate::progress;
#[derive(Subcommand, Debug)]
pub(crate) enum SchemaCommands {
Generate {
#[arg(short, long)]
output: Option<PathBuf>,
#[arg(short, long, default_value = "all")]
r#type: String,
},
Validate {
#[arg(short, long)]
file: Option<PathBuf>,
#[arg(short, long)]
directory: Option<PathBuf>,
#[arg(long)]
schema_type: Option<String>,
#[arg(long)]
schema_dir: Option<PathBuf>,
#[arg(long)]
strict: bool,
},
}
pub(crate) async fn handle_schema(
schema_command: Option<SchemaCommands>,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
use crate::schema::generate_all_schemas;
use std::fs;
use std::path::Path;
let command = schema_command.unwrap_or(SchemaCommands::Generate {
output: None,
r#type: "all".to_string(),
});
match command {
SchemaCommands::Generate { output, r#type } => {
let schemas = generate_all_schemas();
let types_to_generate: Vec<&str> = if r#type == "all" {
vec![
"mockforge-config",
"reality-config",
"persona-config",
"blueprint-config",
]
} else {
vec![&r#type]
};
if let Some(output_path) = output {
let output_path = Path::new(&output_path);
if output_path.is_dir()
|| !output_path.exists() && output_path.extension().is_none()
{
fs::create_dir_all(output_path)?;
for schema_type in &types_to_generate {
if let Some(schema) = schemas.get(*schema_type) {
let filename = format!("{}.schema.json", schema_type.replace("-", "_"));
let file_path = output_path.join(&filename);
let schema_json = serde_json::to_string_pretty(schema)?;
fs::write(&file_path, schema_json)?;
println!(" \u{2713} Generated: {}", file_path.display());
}
}
println!(
"\n\u{2705} Generated {} schema(s) in {}",
types_to_generate.len(),
output_path.display()
);
println!("\nTo use in your IDE:");
println!(" 1. Install a YAML schema extension (e.g., 'YAML' by Red Hat)");
println!(" 2. Add schema mapping to your VS Code settings.json:");
println!(" \"yaml.schemas\": {{");
for schema_type in &types_to_generate {
let filename = format!("{}.schema.json", schema_type.replace("-", "_"));
let schema_path = output_path.join(&filename);
let file_pattern = match *schema_type {
"mockforge-config" => "mockforge.yaml",
"reality-config" => "**/reality*.yaml",
"persona-config" => "**/personas/**/*.yaml",
"blueprint-config" => "**/blueprint.yaml",
_ => "*.yaml",
};
println!(
" \"{}\": \"{}\",",
schema_path.to_string_lossy(),
file_pattern
);
}
println!(" }}");
} else {
let schema_type = if r#type == "all" {
"mockforge-config"
} else {
&r#type
};
if let Some(schema) = schemas.get(schema_type) {
let schema_json = serde_json::to_string_pretty(schema)?;
fs::write(output_path, schema_json)?;
println!("\u{2705} JSON Schema generated: {}", output_path.display());
} else {
eprintln!("\u{274c} Unknown schema type: {}", schema_type);
eprintln!("Available types: mockforge-config, reality-config, persona-config, blueprint-config");
return Err("Invalid schema type".into());
}
}
} else {
if r#type == "all" {
println!("Generating all schemas...\n");
for schema_type in &types_to_generate {
if let Some(schema) = schemas.get(*schema_type) {
println!("=== {} ===", schema_type);
println!("{}", serde_json::to_string_pretty(schema)?);
println!();
}
}
} else if let Some(schema) = schemas.get(&r#type) {
println!("{}", serde_json::to_string_pretty(schema)?);
} else {
eprintln!("\u{274c} Unknown schema type: {}", r#type);
eprintln!("Available types: mockforge-config, reality-config, persona-config, blueprint-config");
return Err("Invalid schema type".into());
}
}
}
SchemaCommands::Validate {
file,
directory,
schema_type,
schema_dir,
strict,
} => {
use crate::schema::{detect_schema_type, generate_all_schemas, validate_config_file};
use std::fs;
let schemas = generate_all_schemas();
let mut validation_results = Vec::new();
let mut has_errors = false;
let schema_dir_path = schema_dir.or_else(|| {
let current_dir = std::env::current_dir().ok()?;
let schemas_dir = current_dir.join("schemas");
if schemas_dir.exists() {
Some(schemas_dir)
} else {
None
}
});
let files_to_validate: Vec<PathBuf> = if let Some(file_path) = file {
vec![file_path]
} else if let Some(dir_path) = directory {
let mut files = Vec::new();
if let Ok(entries) = fs::read_dir(&dir_path) {
for entry in entries.flatten() {
let path = entry.path();
if path.is_file() {
if let Some(ext) = path.extension() {
let ext_str = ext.to_string_lossy().to_lowercase();
if ext_str == "yaml" || ext_str == "yml" || ext_str == "json" {
files.push(path);
}
}
}
}
}
files
} else {
let current_dir = std::env::current_dir()?;
let default_file = current_dir.join("mockforge.yaml");
if default_file.exists() {
vec![default_file]
} else {
eprintln!("\u{274c} No config file specified and mockforge.yaml not found in current directory");
eprintln!(" Use --file or --directory to specify files to validate");
return Err("No files to validate".into());
}
};
for file_path in &files_to_validate {
let file_schema_type = schema_type.clone().unwrap_or_else(|| {
detect_schema_type(file_path).unwrap_or_else(|| "mockforge-config".to_string())
});
let schema = if let Some(ref schema_dir) = schema_dir_path {
let schema_file = schema_dir
.join(format!("{}.schema.json", file_schema_type.replace("-", "_")));
if schema_file.exists() {
match fs::read_to_string(&schema_file).and_then(|content| {
serde_json::from_str::<serde_json::Value>(&content).map_err(|e| {
std::io::Error::new(std::io::ErrorKind::InvalidData, e)
})
}) {
Ok(s) => s,
Err(e) => {
eprintln!(
"\u{26a0}\u{fe0f} Failed to load schema from {}: {}",
schema_file.display(),
e
);
schemas.get(&file_schema_type).cloned().unwrap_or_else(|| {
eprintln!(
"\u{274c} Schema type '{}' not found",
file_schema_type
);
has_errors = true;
serde_json::json!({})
})
}
}
} else {
schemas.get(&file_schema_type).cloned().unwrap_or_else(|| {
eprintln!(
"\u{26a0}\u{fe0f} Schema file not found: {}, using generated schema",
schema_file.display()
);
schemas.get(&file_schema_type).cloned().unwrap_or_else(|| {
eprintln!("\u{274c} Schema type '{}' not found", file_schema_type);
has_errors = true;
serde_json::json!({})
})
})
}
} else {
schemas.get(&file_schema_type).cloned().unwrap_or_else(|| {
eprintln!("\u{274c} Schema type '{}' not found", file_schema_type);
has_errors = true;
serde_json::json!({})
})
};
match validate_config_file(file_path, &file_schema_type, &schema) {
Ok(result) => {
validation_results.push(result);
}
Err(e) => {
eprintln!("\u{274c} Failed to validate {}: {}", file_path.display(), e);
has_errors = true;
}
}
}
println!("\n\u{1f4cb} Validation Results:\n");
for result in &validation_results {
if result.valid {
println!(" \u{2705} {} (schema: {})", result.file_path, result.schema_type);
} else {
println!(" \u{274c} {} (schema: {})", result.file_path, result.schema_type);
for error in &result.errors {
println!(" \u{2022} {}", error);
}
has_errors = true;
}
}
let valid_count = validation_results.iter().filter(|r| r.valid).count();
let total_count = validation_results.len();
println!(
"\n\u{1f4ca} Summary: {} of {} file(s) passed validation",
valid_count, total_count
);
if has_errors {
if strict {
return Err("Validation failed".into());
} else {
eprintln!("\n\u{26a0}\u{fe0f} Validation completed with errors (use --strict to exit with error code)");
}
} else if !validation_results.is_empty() {
println!("\n\u{2705} All files passed validation!");
}
}
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
pub(crate) async fn handle_generate(
config_path: Option<PathBuf>,
spec_path: Option<PathBuf>,
output_path: Option<PathBuf>,
verbose: bool,
dry_run: bool,
watch: bool,
watch_debounce: u64,
show_progress: bool,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
use mockforge_core::discover_config_file;
use progress::{CliError, ExitCode, LogLevel, ProgressManager};
let mut progress_mgr = ProgressManager::new(verbose);
if watch {
let files_to_watch = if let Some(spec) = &spec_path {
vec![spec.clone()]
} else if let Some(config) = &config_path {
vec![config.clone()]
} else {
match discover_config_file() {
Ok(path) => vec![path],
Err(_) => CliError::new(
"No configuration file found for watch mode".to_string(),
ExitCode::ConfigurationError,
)
.with_suggestion(
"Provide --config or --spec flag, or create mockforge.toml".to_string(),
)
.display_and_exit(),
}
};
progress_mgr.log(LogLevel::Info, "\u{1f504} Starting watch mode...");
progress_mgr.log(
LogLevel::Info,
&format!("\u{1f440} Watching {} file(s) for changes", files_to_watch.len()),
);
if let Err(e) = execute_generation(
&mut progress_mgr,
config_path.clone(),
spec_path.clone(),
output_path.clone(),
verbose,
dry_run,
show_progress,
)
.await
{
progress_mgr.log(LogLevel::Error, &format!("Initial generation failed: {}", e));
return Err(e);
}
let callback = move || {
let config_path = config_path.clone();
let spec_path = spec_path.clone();
let output_path = output_path.clone();
let verbose = verbose;
let dry_run = dry_run;
let progress = show_progress;
async move {
let mut progress_mgr = ProgressManager::new(verbose);
execute_generation(
&mut progress_mgr,
config_path,
spec_path,
output_path,
verbose,
dry_run,
progress,
)
.await
}
};
progress::watch::watch_files(files_to_watch, callback, watch_debounce).await?;
return Ok(());
}
execute_generation(
&mut progress_mgr,
config_path,
spec_path,
output_path,
verbose,
dry_run,
show_progress,
)
.await
}
async fn load_and_validate_config(
path: &PathBuf,
verbose: bool,
progress_mgr: &mut progress::ProgressManager,
) -> mockforge_core::GenerateConfig {
use crate::progress::{utils, LogLevel};
use mockforge_core::load_generate_config_with_fallback;
if verbose {
progress_mgr.log(
LogLevel::Debug,
&format!("\u{1f4c4} Loading configuration from: {}", path.display()),
);
}
if let Err(e) = utils::validate_file_path(path) {
e.display_and_exit();
}
load_generate_config_with_fallback(path).await
}
async fn execute_generation(
progress_mgr: &mut progress::ProgressManager,
config_path: Option<PathBuf>,
spec_path: Option<PathBuf>,
output_path: Option<PathBuf>,
verbose: bool,
dry_run: bool,
show_progress: bool,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
use mockforge_core::{discover_config_file, GenerateConfig};
use progress::{utils, CliError, ExitCode, LogLevel};
use std::time::Instant;
let start_time = Instant::now();
progress_mgr.log(LogLevel::Info, "\u{1f527} Generating mocks from configuration...");
let (_config_file, mut config) = if let Some(path) = &config_path {
let config = load_and_validate_config(path, verbose, progress_mgr).await;
(Some(path.clone()), config)
} else {
match discover_config_file() {
Ok(path) => {
let config = load_and_validate_config(&path, verbose, progress_mgr).await;
(Some(path), config)
}
Err(_) => {
if spec_path.is_none() {
progress_mgr.log(
LogLevel::Warning,
"\u{2139}\u{fe0f} No configuration file found, using defaults",
);
CliError::new(
"No configuration file found and no spec provided. Please create mockforge.toml, mockforge.yaml, or mockforge.json, or provide --spec flag.".to_string(),
ExitCode::ConfigurationError,
).with_suggestion(
"Create a configuration file or use --spec to specify an OpenAPI specification".to_string()
).display_and_exit();
}
progress_mgr.log(
LogLevel::Warning,
"\u{2139}\u{fe0f} No configuration file found, using defaults",
);
(None, GenerateConfig::default())
}
}
};
if let Some(spec) = &spec_path {
config.input.spec = Some(spec.clone());
}
if let Some(output) = &output_path {
config.output.path = output.clone();
}
let spec = progress::require_registry(&config.input.spec, "spec")?;
if !spec.exists() {
CliError::new(
format!("Specification file not found: {}", spec.display()),
ExitCode::FileNotFound,
)
.with_suggestion("Check the file path and ensure the specification file exists".to_string())
.display_and_exit();
}
if verbose {
progress_mgr.log(LogLevel::Debug, "\u{1f50d} Validating specification...");
}
let spec_content = match tokio::fs::read_to_string(spec).await {
Ok(content) => content,
Err(e) => CliError::new(
format!("Failed to read specification file: {}", e),
ExitCode::FileNotFound,
)
.display_and_exit(),
};
let format = match mockforge_core::spec_parser::SpecFormat::detect(&spec_content, Some(spec)) {
Ok(fmt) => fmt,
Err(e) => CliError::new(
format!("Failed to detect specification format: {}", e),
ExitCode::ConfigurationError,
)
.with_suggestion(
"Ensure your file is a valid OpenAPI, GraphQL, or protobuf specification".to_string(),
)
.display_and_exit(),
};
if verbose {
progress_mgr.log(
LogLevel::Debug,
&format!("\u{1f4cb} Detected format: {}", format.display_name()),
);
}
match format {
mockforge_core::spec_parser::SpecFormat::OpenApi20
| mockforge_core::spec_parser::SpecFormat::OpenApi30
| mockforge_core::spec_parser::SpecFormat::OpenApi31 => {
let json_value: serde_json::Value =
match serde_json::from_str::<serde_json::Value>(&spec_content) {
Ok(val) => val,
Err(_) => {
match serde_yaml::from_str(&spec_content) {
Ok(val) => val,
Err(e) => CliError::new(
format!("Invalid JSON or YAML in OpenAPI spec: {}", e),
ExitCode::ConfigurationError,
)
.display_and_exit(),
}
}
};
let validation =
mockforge_core::spec_parser::OpenApiValidator::validate(&json_value, format);
if !validation.is_valid {
let error_details: Vec<String> = validation
.errors
.iter()
.map(|e| {
let mut msg = e.message.clone();
if let Some(path) = &e.path {
msg.push_str(&format!(" (at {})", path));
}
if let Some(suggestion) = &e.suggestion {
msg.push_str(&format!(". Hint: {}", suggestion));
}
msg
})
.collect();
let error_msg = error_details.join("\n ");
CliError::new(
format!("Invalid OpenAPI specification:\n {}", error_msg),
ExitCode::ConfigurationError,
)
.with_suggestion("Fix the validation errors above and try again".to_string())
.display_and_exit();
}
if !validation.warnings.is_empty() && verbose {
progress_mgr.log(LogLevel::Warning, "\u{26a0}\u{fe0f} Validation warnings:");
for warning in &validation.warnings {
progress_mgr.log(LogLevel::Warning, &format!(" - {}", warning));
}
}
if verbose {
progress_mgr.log(LogLevel::Success, "\u{2705} OpenAPI specification is valid");
}
}
mockforge_core::spec_parser::SpecFormat::GraphQL => {
let validation = mockforge_core::spec_parser::GraphQLValidator::validate(&spec_content);
if !validation.is_valid {
let error_details: Vec<String> = validation
.errors
.iter()
.map(|e| {
let mut msg = e.message.clone();
if let Some(suggestion) = &e.suggestion {
msg.push_str(&format!(". Hint: {}", suggestion));
}
msg
})
.collect();
let error_msg = error_details.join("\n ");
CliError::new(
format!("Invalid GraphQL schema:\n {}", error_msg),
ExitCode::ConfigurationError,
)
.with_suggestion("Fix the validation errors above and try again".to_string())
.display_and_exit();
}
if !validation.warnings.is_empty() && verbose {
progress_mgr.log(LogLevel::Warning, "\u{26a0}\u{fe0f} Validation warnings:");
for warning in &validation.warnings {
progress_mgr.log(LogLevel::Warning, &format!(" - {}", warning));
}
}
if verbose {
progress_mgr.log(LogLevel::Success, "\u{2705} GraphQL schema is valid");
}
}
mockforge_core::spec_parser::SpecFormat::Protobuf => {
if verbose {
progress_mgr.log(
LogLevel::Info,
"\u{1f4cb} Protobuf validation will be performed during parsing",
);
}
}
}
if let Err(e) = utils::validate_output_dir(&config.output.path) {
e.display_and_exit();
}
if verbose {
progress_mgr.log(LogLevel::Debug, &format!("\u{1f4dd} Input spec: {}", spec.display()));
progress_mgr.log(
LogLevel::Debug,
&format!("\u{1f4c2} Output path: {}", config.output.path.display()),
);
if let Some(filename) = &config.output.filename {
progress_mgr.log(LogLevel::Debug, &format!("\u{1f4c4} Output filename: {}", filename));
}
if let Some(options) = &config.options {
progress_mgr
.log(LogLevel::Debug, &format!("\u{2699}\u{fe0f} Client: {:?}", options.client));
progress_mgr
.log(LogLevel::Debug, &format!("\u{2699}\u{fe0f} Mode: {:?}", options.mode));
progress_mgr
.log(LogLevel::Debug, &format!("\u{2699}\u{fe0f} Runtime: {:?}", options.runtime));
}
if !config.plugins.is_empty() {
progress_mgr.log(LogLevel::Debug, "\u{1f50c} Plugins:");
for (name, plugin) in &config.plugins {
match plugin {
mockforge_core::PluginConfig::Simple(pkg) => {
progress_mgr.log(LogLevel::Debug, &format!(" - {}: {}", name, pkg));
}
mockforge_core::PluginConfig::Advanced { package, options } => {
progress_mgr.log(
LogLevel::Debug,
&format!(" - {}: {} (with options)", name, package),
);
if !options.is_empty() {
for (k, v) in options {
progress_mgr.log(LogLevel::Debug, &format!(" - {}: {}", k, v));
}
}
}
}
}
}
}
if dry_run {
progress_mgr.log(LogLevel::Success, "\u{2705} Configuration is valid (dry run)");
return Ok(());
}
let total_steps = 5u64;
let progress_bar = if show_progress {
Some(progress_mgr.create_main_progress(total_steps, "Generating mocks"))
} else {
None
};
progress_mgr.log_step(1, total_steps as usize, "Preparing output directory");
if config.output.clean && config.output.path.exists() {
if verbose {
progress_mgr.log(
LogLevel::Debug,
&format!("\u{1f9f9} Cleaning output directory: {}", config.output.path.display()),
);
}
tokio::fs::remove_dir_all(&config.output.path).await?;
}
tokio::fs::create_dir_all(&config.output.path).await?;
if let Some(ref pb) = progress_bar {
pb.inc(1u64);
}
progress_mgr.log_step(2, total_steps as usize, "Loading OpenAPI specification");
let spec_content = tokio::fs::read_to_string(spec).await?;
let spec_size = utils::format_file_size(spec_content.len() as u64);
progress_mgr.log(
LogLevel::Info,
&format!("\u{1f4d6} Loaded OpenAPI specification ({})", spec_size),
);
let parsed_spec =
OpenApiSpec::from_string(&spec_content, spec.extension().and_then(|e| e.to_str()))
.map_err(|e| -> Box<dyn std::error::Error + Send + Sync> {
format!("Failed to parse OpenAPI specification: {}", e).into()
})?;
let naming_context = if config.output.file_naming_template.is_some() {
Some(build_file_naming_context(&parsed_spec))
} else {
None
};
if let Some(ref pb) = progress_bar {
pb.inc(1u64);
}
progress_mgr.log_step(3, total_steps as usize, "Generating mock server code");
let base_filename =
config.output.filename.clone().unwrap_or_else(|| "generated_mock".to_string());
let extension = config.output.extension.clone().unwrap_or_else(|| "rs".to_string());
let mut output_file = config.output.path.join(format!("{}.{}", base_filename, extension));
let codegen_config = mockforge_core::codegen::CodegenConfig {
mock_data_strategy: mockforge_core::codegen::MockDataStrategy::ExamplesOrRandom,
port: None, enable_cors: false,
default_delay_ms: None,
};
let raw_mock_code = mockforge_core::codegen::generate_mock_server_code(
&parsed_spec,
&extension,
&codegen_config,
)
.map_err(|e| -> Box<dyn std::error::Error + Send + Sync> {
format!("Failed to generate mock server code: {}", e).into()
})?;
let mut generated_file = GeneratedFile {
path: output_file
.strip_prefix(&config.output.path)
.unwrap_or(&output_file)
.to_path_buf(),
content: raw_mock_code,
extension: extension.clone(),
exportable: matches!(extension.as_str(), "ts" | "tsx" | "js" | "jsx" | "mjs"),
};
generated_file =
process_generated_file(generated_file, &config.output, Some(spec), naming_context.as_ref());
output_file = config.output.path.join(&generated_file.path);
tokio::fs::write(&output_file, generated_file.content.clone()).await?;
let all_generated_files = vec![generated_file];
if let Some(ref pb) = progress_bar {
pb.inc(1u64);
}
progress_mgr.log_step(4, total_steps as usize, "Generating additional files");
let readme_content = format!(
r#"# Generated Mock Server
This mock server was generated by MockForge from the OpenAPI specification:
- Source: {}
- Generated: {}
## Usage
```bash
# Start the mock server
cargo run
# Or use MockForge CLI
mockforge serve --spec {}
```
## Files Generated
- `{}` - Main mock server implementation
- `README.md` - This file
"#,
spec.display(),
chrono::Utc::now().format("%Y-%m-%d %H:%M:%S UTC"),
spec.display(),
{
use crate::progress::get_file_name;
get_file_name(&output_file).unwrap_or_else(|e| {
eprintln!("{}", e.message);
if let Some(suggestion) = e.suggestion {
eprintln!("\u{1f4a1} {}", suggestion);
}
std::process::exit(e.exit_code as i32);
})
}
);
let readme_file = config.output.path.join("README.md");
tokio::fs::write(&readme_file, readme_content).await?;
if let Some(ref pb) = progress_bar {
pb.inc(1u64);
}
if config.output.barrel_type != mockforge_core::BarrelType::None {
if verbose {
progress_mgr.log(
LogLevel::Debug,
&format!(
"\u{1f4e6} Generating barrel files (type: {:?})",
config.output.barrel_type
),
);
}
match BarrelGenerator::generate_barrel_files(
&config.output.path,
&all_generated_files,
config.output.barrel_type,
) {
Ok(barrel_files) => {
for (barrel_path, barrel_content) in barrel_files {
tokio::fs::write(&barrel_path, barrel_content).await?;
if verbose {
progress_mgr.log(
LogLevel::Debug,
&format!("\u{1f4c4} Generated barrel file: {}", barrel_path.display()),
);
}
}
}
Err(e) => {
progress_mgr.log(
LogLevel::Warning,
&format!("\u{26a0}\u{fe0f} Failed to generate barrel files: {}", e),
);
}
}
}
progress_mgr.log_step(5, total_steps as usize, "Finalizing generation");
let duration = start_time.elapsed();
let duration_str = utils::format_duration(duration);
let total_files = all_generated_files.len() + 1;
progress_mgr.log(
LogLevel::Success,
&format!("\u{2705} Mock generation completed in {}", duration_str),
);
progress_mgr.log(
LogLevel::Info,
&format!("\u{1f4c1} Output directory: {}", config.output.path.display()),
);
progress_mgr.log(LogLevel::Info, &format!("\u{1f4c4} Generated files: {} files", total_files));
if let Some(ref pb) = progress_bar {
pb.finish();
}
Ok(())
}
pub(crate) async fn handle_init(
name: String,
no_examples: bool,
blueprint: Option<String>,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
use std::fs;
if let Some(blueprint_id) = blueprint {
println!("\u{1f680} Creating project from blueprint '{}'...", blueprint_id);
let project_dir = if name == "." {
std::env::current_dir()?
} else {
PathBuf::from(&name)
};
use crate::blueprint_commands;
blueprint_commands::create_from_blueprint(
if name == "." {
project_dir
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("my-project")
.to_string()
} else {
name
},
blueprint_id,
Some(project_dir),
false, )?;
return Ok(());
}
println!("\u{1f680} Initializing MockForge project...");
let project_dir = if name == "." {
std::env::current_dir()?
} else {
PathBuf::from(&name)
};
if !project_dir.exists() {
fs::create_dir_all(&project_dir)?;
println!("\u{1f4c1} Created directory: {}", project_dir.display());
}
let config_path = project_dir.join("mockforge.yaml");
if config_path.exists() {
println!("\u{26a0}\u{fe0f} Configuration file already exists: {}", config_path.display());
} else {
let openapi_spec_line = if !no_examples {
" openapi_spec: \"./examples/openapi.json\"\n"
} else {
""
};
let config_content = format!(
r#"# MockForge Configuration
# Full configuration reference: https://docs.mockforge.dev/config
# HTTP Server
http:
port: 3000
host: "0.0.0.0"
cors_enabled: true
request_timeout_secs: 30
request_validation: "enforce"
aggregate_validation_errors: true
validate_responses: false
response_template_expand: false
validation_overrides: {{}}
skip_admin_validation: true
{}
# WebSocket Server
websocket:
port: 3001
host: "0.0.0.0"
connection_timeout_secs: 300
# gRPC Server
grpc:
port: 50051
host: "0.0.0.0"
# Admin UI
admin:
enabled: true
port: 9080
host: "127.0.0.1"
api_enabled: true
auth_required: false
prometheus_url: "http://localhost:9090"
# Core Features
core:
latency_enabled: true
failures_enabled: false
overrides_enabled: true
traffic_shaping_enabled: false
# Observability
observability:
prometheus:
enabled: true
port: 9090
host: "0.0.0.0"
path: "/metrics"
opentelemetry: null
recorder: null
chaos: null
# Data Generation
data:
default_rows: 100
default_format: "json"
locale: "en"
templates: {{}}
rag:
enabled: false
provider: "openai"
# Logging
logging:
level: "info"
json_format: false
max_file_size_mb: 10
max_files: 5
"#,
openapi_spec_line
);
fs::write(&config_path, config_content)?;
println!("\u{2705} Created mockforge.yaml");
}
if !no_examples {
let examples_dir = project_dir.join("examples");
fs::create_dir_all(&examples_dir)?;
println!("\u{1f4c1} Created examples directory");
let openapi_path = examples_dir.join("openapi.json");
let openapi_content = r#"{
"openapi": "3.0.0",
"info": {
"title": "Example API",
"version": "1.0.0"
},
"paths": {
"/health": {
"get": {
"summary": "Health check",
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"status": {
"type": "string"
}
}
}
}
}
}
}
}
},
"/users": {
"get": {
"summary": "List users",
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "integer"
},
"name": {
"type": "string"
},
"email": {
"type": "string"
}
}
}
}
}
}
}
}
}
}
}
}"#;
fs::write(&openapi_path, openapi_content)?;
println!("\u{2705} Created examples/openapi.json");
let fixtures_dir = project_dir.join("fixtures");
fs::create_dir_all(&fixtures_dir)?;
let fixture_path = fixtures_dir.join("users.json");
let fixture_content = r#"[
{
"id": 1,
"name": "Alice Johnson",
"email": "alice@example.com"
},
{
"id": 2,
"name": "Bob Smith",
"email": "bob@example.com"
}
]"#;
fs::write(&fixture_path, fixture_content)?;
println!("\u{2705} Created fixtures/users.json");
}
println!("\n\u{1f389} MockForge project initialized successfully!");
println!("\nNext steps:");
println!(" 1. cd {}", if name == "." { "." } else { &name });
println!(" 2. Edit mockforge.yaml to configure your mock servers");
if !no_examples {
println!(" 3. Review examples/openapi.json for API specifications");
}
println!(" 4. Run: mockforge serve --config mockforge.yaml");
println!("\nNote: CLI arguments override config file settings");
Ok(())
}