use clap::Parser;
mod cli;
mod config;
mod generator;
mod introspection;
mod parser;
use cli::{Cli, Commands};
use config::Config;
use generator::create_generator;
use parser::GraphQLParser;
use fs_err as fs;
struct Logger {
verbosity: u8,
}
impl Logger {
fn new(verbosity: u8) -> Self {
Self { verbosity }
}
fn info(&self, message: &str) {
if self.verbosity >= 1 {
println!("{}", message);
}
}
fn debug(&self, message: &str) {
if self.verbosity >= 2 {
eprintln!("DEBUG: {}", message);
}
}
fn trace(&self, message: &str) {
if self.verbosity >= 3 {
eprintln!("TRACE: {}", message);
}
}
fn success(&self, message: &str) {
println!("✅ {}", message);
}
#[allow(dead_code)]
fn warning(&self, message: &str) {
eprintln!("⚠️ {}", message);
}
#[allow(dead_code)]
fn error(&self, message: &str) {
eprintln!("❌ {}", message);
}
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let cli = Cli::parse();
let logger = Logger::new(cli.verbose);
match cli.command {
Some(Commands::Init {
url,
orm,
db,
output,
headers,
}) => {
logger.info("Initializing GraphQL codegen...");
logger.debug(&format!("URL: {}", url));
logger.debug(&format!("ORM: {:?}", orm));
logger.debug(&format!("Database: {:?}", db));
logger.debug(&format!("Output directory: {:?}", output));
logger.trace("Creating output directory...");
fs::create_dir_all(&output)?;
let config = Config::from(&Commands::Init {
url,
orm,
db,
output,
headers,
});
logger.info("Fetching GraphQL schema via introspection...");
let parser = GraphQLParser::new();
let schema = parser
.parse_from_introspection(&config.url, &config.headers)
.await?;
let config_path = Config::config_path(&config.output_dir);
logger.trace(&format!("Saving config to: {:?}", config_path));
config.save_to_file(&config_path)?;
logger.info("Generating Rust code...");
let generator = create_generator(&config.orm);
generate_all_code(&schema, &config, &*generator, &logger).await?;
logger.success("Initialization complete!");
logger.info(&format!("Config saved to: {:?}", config_path));
}
Some(Commands::Generate { config, output }) => {
logger.info("Generating code...");
logger.trace("Locating config file...");
let config_path = if let Some(path) = config {
logger.debug(&format!("Using specified config: {:?}", path));
path
} else {
logger.trace("Auto-detecting config file...");
Config::auto_detect_config()?
};
logger.debug(&format!("Loading config from: {:?}", config_path));
let mut config = Config::from_file(&config_path)?;
if let Some(output_dir) = output {
logger.debug(&format!("Overriding output directory: {:?}", output_dir));
config.output_dir = output_dir;
}
logger.info("Fetching GraphQL schema via introspection...");
let parser = GraphQLParser::new();
let schema = parser
.parse_from_introspection(&config.url, &config.headers)
.await?;
logger.info("Generating Rust code...");
let generator = create_generator(&config.orm);
generate_all_code(&schema, &config, &*generator, &logger).await?;
logger.success("Code generation complete!");
}
None => {
logger.info("Generating code from auto-detected config...");
logger.trace("Auto-detecting config file...");
let config_path = Config::auto_detect_config()?;
logger.debug(&format!("Loading config from: {:?}", config_path));
let config = Config::from_file(&config_path)?;
logger.info("Fetching GraphQL schema via introspection...");
let parser = GraphQLParser::new();
let schema = parser
.parse_from_introspection(&config.url, &config.headers)
.await?;
logger.info("Generating Rust code...");
let generator = create_generator(&config.orm);
generate_all_code(&schema, &config, &*generator, &logger).await?;
logger.success("Code generation complete!");
}
}
Ok(())
}
async fn generate_all_code(
schema: &parser::ParsedSchema,
config: &Config,
generator: &dyn generator::CodeGenerator,
logger: &Logger,
) -> anyhow::Result<()> {
logger.trace("Creating output directory structure...");
fs::create_dir_all(&config.output_dir)?;
let src_dir = config.output_dir.join("src");
fs::create_dir_all(&src_dir)?;
logger.trace("Generating schema file...");
let schema_code = generator.generate_schema(schema, config)?;
if config.orm == cli::OrmType::Diesel {
let schema_path = src_dir.join("schema.rs");
fs::write(schema_path, schema_code)?;
logger.info("Generated schema.rs");
} else if config.orm == cli::OrmType::SeaOrm {
let mod_path = config.output_dir.join("mod.rs");
fs::write(mod_path, schema_code)?;
logger.info("Generated mod.rs");
}
logger.trace("Generating entity files...");
let entities = generator.generate_entities(schema, config)?;
let entities_dir = src_dir.join("entities");
fs::create_dir_all(&entities_dir)?;
let entity_count = entities.len();
for (filename, code) in entities {
let entity_path = entities_dir.join(filename);
fs::write(entity_path, code)?;
}
logger.info(&format!("Generated {} entity files", entity_count));
logger.trace("Generating migration files...");
let migrations = generator.generate_migrations(schema, config)?;
let migrations_dir = config.output_dir.join("migrations");
fs::create_dir_all(&migrations_dir)?;
let migration_count = migrations.len();
for migration in migrations {
let migration_dir = migrations_dir.join(&migration.name);
fs::create_dir_all(&migration_dir)?;
let up_path = migration_dir.join("up.sql");
let down_path = migration_dir.join("down.sql");
fs::write(up_path, migration.up_sql)?;
fs::write(down_path, migration.down_sql)?;
}
logger.info(&format!("Generated {} migrations", migration_count));
Ok(())
}