use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::PathBuf;
use fs_err as fs;
use crate::cli::{DatabaseType, OrmType};
#[cfg(feature = "yaml-codegen-config")]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct YamlConfig {
pub schema: SchemaConfig,
pub rust_codegen: Option<RustCodegenConfig>,
}
#[cfg(feature = "yaml-codegen-config")]
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum SchemaConfig {
Url(String),
Object {
url: String,
#[serde(default)]
headers: HashMap<String, String>,
},
}
#[cfg(feature = "yaml-codegen-config")]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RustCodegenConfig {
#[serde(default = "default_orm")]
pub orm: OrmType,
#[serde(default = "default_db")]
pub db: DatabaseType,
#[serde(default = "default_output")]
pub output_dir: PathBuf,
#[serde(default)]
pub type_mappings: HashMap<String, String>,
#[serde(default)]
pub scalar_mappings: HashMap<String, String>,
#[serde(default)]
pub table_naming: TableNamingConvention,
#[serde(default = "default_true")]
pub generate_migrations: bool,
#[serde(default = "default_true")]
pub generate_entities: bool,
}
#[cfg(feature = "yaml-codegen-config")]
fn default_orm() -> OrmType {
OrmType::Diesel
}
#[cfg(feature = "yaml-codegen-config")]
fn default_db() -> DatabaseType {
DatabaseType::Sqlite
}
#[cfg(feature = "yaml-codegen-config")]
fn default_output() -> PathBuf {
PathBuf::from("./generated")
}
#[cfg(feature = "yaml-codegen-config")]
impl Default for RustCodegenConfig {
fn default() -> Self {
Self {
orm: default_orm(),
db: default_db(),
output_dir: default_output(),
type_mappings: HashMap::new(),
scalar_mappings: HashMap::new(),
table_naming: TableNamingConvention::default(),
generate_migrations: true,
generate_entities: true,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct Config {
pub url: String,
pub orm: OrmType,
pub db: DatabaseType,
pub output_dir: PathBuf,
#[serde(default)]
pub headers: HashMap<String, String>,
#[serde(default)]
pub type_mappings: HashMap<String, String>,
#[serde(default)]
pub scalar_mappings: HashMap<String, String>,
#[serde(default)]
pub table_naming: TableNamingConvention,
#[serde(default = "default_true")]
pub generate_migrations: bool,
#[serde(default = "default_true")]
pub generate_entities: bool,
}
fn default_true() -> bool {
true
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub enum TableNamingConvention {
#[serde(rename = "snake_case")]
#[default]
SnakeCase,
#[serde(rename = "pascal_case")]
PascalCase,
}
impl Config {
pub fn from_file(path: &PathBuf) -> anyhow::Result<Self> {
let contents = fs::read_to_string(path).map_err(|e| {
anyhow::anyhow!(
"Failed to read config file '{}': {}\n\nEnsure the file exists and you have read permissions.",
path.display(),
e
)
})?;
if path
.extension()
.is_some_and(|ext| ext == "yml" || ext == "yaml")
|| contents.trim().starts_with("schema:")
{
#[cfg(feature = "yaml-codegen-config")]
{
Self::from_yaml_str(&contents)
}
#[cfg(not(feature = "yaml-codegen-config"))]
{
Err(anyhow::anyhow!(
"YAML config support not enabled.\n\nTo use YAML config files, rebuild with:\n cargo build --features yaml-codegen-config\n\nAlternatively, use TOML format with 'graphql-codegen-rust.toml'"
))
}
} else {
Self::from_toml_str(&contents)
}
}
pub fn from_toml_str(contents: &str) -> anyhow::Result<Self> {
let config: Config = toml::from_str(contents).map_err(|e| {
anyhow::anyhow!(
"Invalid TOML config format: {}\n\nExpected format:\n url = \"https://api.example.com/graphql\"\n orm = \"Diesel\"\n db = \"Sqlite\"\n output_dir = \"./generated\"\n [headers]\n Authorization = \"Bearer <token>\"\n\nSee documentation for complete configuration options.",
e
)
})?;
Ok(config)
}
#[cfg(feature = "yaml-codegen-config")]
pub fn from_yaml_str(contents: &str) -> anyhow::Result<Self> {
let yaml_config: YamlConfig = serde_yaml::from_str(contents).map_err(|e| {
anyhow::anyhow!(
"Invalid YAML config format: {}\n\nExpected format:\n schema:\n url: https://api.example.com/graphql\n headers:\n Authorization: Bearer <token>\n rust_codegen:\n orm: Diesel\n db: Sqlite\n output_dir: ./generated\n\nSee documentation for complete configuration options.",
e
)
})?;
let (url, headers) = match yaml_config.schema {
SchemaConfig::Url(url) => (url, HashMap::new()),
SchemaConfig::Object { url, headers } => (url, headers),
};
let rust_config = yaml_config.rust_codegen.unwrap_or_default();
Ok(Config {
url,
orm: rust_config.orm,
db: rust_config.db,
output_dir: rust_config.output_dir,
headers,
type_mappings: rust_config.type_mappings,
scalar_mappings: rust_config.scalar_mappings,
table_naming: rust_config.table_naming,
generate_migrations: rust_config.generate_migrations,
generate_entities: rust_config.generate_entities,
})
}
pub fn save_to_file(&self, path: &PathBuf) -> anyhow::Result<()> {
let toml = toml::to_string_pretty(self)?;
fs::write(path, toml)?;
Ok(())
}
pub fn config_path(output_dir: &std::path::Path) -> PathBuf {
output_dir.join("graphql-codegen-rust.toml")
}
pub fn auto_detect_config() -> anyhow::Result<PathBuf> {
let yaml_path = PathBuf::from("codegen.yml");
if yaml_path.exists() {
return Ok(yaml_path);
}
let yaml_path = PathBuf::from("codegen.yaml");
if yaml_path.exists() {
return Ok(yaml_path);
}
let toml_path = PathBuf::from("graphql-codegen-rust.toml");
if toml_path.exists() {
return Ok(toml_path);
}
Err(anyhow::anyhow!(
"No config file found in current directory.\n\nExpected one of:\n - codegen.yml\n - codegen.yaml\n - graphql-codegen-rust.toml\n\nTo create a new project, run:\n graphql-codegen-rust init --url <your-graphql-endpoint>\n\nTo specify a config file explicitly, run:\n graphql-codegen-rust generate --config <path-to-config>"
))
}
}
impl From<&crate::cli::Commands> for Config {
fn from(cmd: &crate::cli::Commands) -> Self {
match cmd {
crate::cli::Commands::Init {
url,
orm,
db,
output,
headers,
} => {
let headers_map = headers.iter().cloned().collect();
Config {
url: url.clone(),
orm: orm.clone(),
db: db.clone(),
output_dir: output.clone(),
headers: headers_map,
type_mappings: HashMap::new(),
scalar_mappings: HashMap::new(),
table_naming: TableNamingConvention::default(),
generate_migrations: true,
generate_entities: true,
}
}
_ => unreachable!("Config can only be created from Init command"),
}
}
}