graphql_codegen_rust/config.rs
1use serde::{Deserialize, Serialize};
2use std::collections::HashMap;
3use std::path::PathBuf;
4
5use fs_err as fs;
6
7use crate::cli::{DatabaseType, OrmType};
8
9/// YAML configuration format compatible with GraphQL Code Generator
10#[cfg(feature = "yaml-codegen-config")]
11#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct YamlConfig {
13 /// Schema configuration (shared with GraphQL Code Generator)
14 pub schema: SchemaConfig,
15 /// Rust codegen specific configuration
16 pub rust_codegen: Option<RustCodegenConfig>,
17}
18
19/// Schema configuration (compatible with GraphQL Code Generator)
20#[cfg(feature = "yaml-codegen-config")]
21#[derive(Debug, Clone, Serialize, Deserialize)]
22#[serde(untagged)]
23pub enum SchemaConfig {
24 /// Simple URL string
25 Url(String),
26 /// Object with URL and headers
27 Object {
28 /// GraphQL endpoint URL
29 url: String,
30 /// Additional headers for requests
31 #[serde(default)]
32 headers: HashMap<String, String>,
33 },
34}
35
36/// Rust codegen specific configuration
37#[cfg(feature = "yaml-codegen-config")]
38#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct RustCodegenConfig {
40 /// ORM type
41 #[serde(default = "default_orm")]
42 pub orm: OrmType,
43 /// Database type
44 #[serde(default = "default_db")]
45 pub db: DatabaseType,
46 /// Output directory
47 #[serde(default = "default_output")]
48 pub output_dir: PathBuf,
49 /// Custom type mappings
50 #[serde(default)]
51 pub type_mappings: HashMap<String, String>,
52 /// Custom scalar mappings
53 #[serde(default)]
54 pub scalar_mappings: HashMap<String, String>,
55 /// Table naming convention
56 #[serde(default)]
57 pub table_naming: TableNamingConvention,
58 /// Generate migrations
59 #[serde(default = "default_true")]
60 pub generate_migrations: bool,
61 /// Generate entities
62 #[serde(default = "default_true")]
63 pub generate_entities: bool,
64}
65
66#[cfg(feature = "yaml-codegen-config")]
67fn default_orm() -> OrmType {
68 OrmType::Diesel
69}
70
71#[cfg(feature = "yaml-codegen-config")]
72fn default_db() -> DatabaseType {
73 DatabaseType::Sqlite
74}
75
76#[cfg(feature = "yaml-codegen-config")]
77fn default_output() -> PathBuf {
78 PathBuf::from("./generated")
79}
80
81#[cfg(feature = "yaml-codegen-config")]
82impl Default for RustCodegenConfig {
83 fn default() -> Self {
84 Self {
85 orm: default_orm(),
86 db: default_db(),
87 output_dir: default_output(),
88 type_mappings: HashMap::new(),
89 scalar_mappings: HashMap::new(),
90 table_naming: TableNamingConvention::default(),
91 generate_migrations: true,
92 generate_entities: true,
93 }
94 }
95}
96
97/// Configuration for GraphQL code generation.
98///
99/// The `Config` struct defines all parameters needed to generate Rust ORM code
100/// from a GraphQL schema. It supports both programmatic creation and loading
101/// from configuration files (TOML or YAML).
102///
103/// ## Required Fields
104///
105/// - `url`: GraphQL endpoint URL that supports introspection
106/// - `orm`: ORM to generate code for (Diesel or Sea-ORM)
107/// - `db`: Target database (SQLite, PostgreSQL, or MySQL)
108/// - `output_dir`: Directory where generated code will be written
109///
110/// ## Optional Fields
111///
112/// All other fields have sensible defaults and are typically configured
113/// through configuration files rather than programmatically.
114///
115/// ## Configuration Files
116///
117/// ### TOML Format (`graphql-codegen-rust.toml`)
118/// ```toml
119/// url = "https://api.example.com/graphql"
120/// orm = "Diesel"
121/// db = "Postgres"
122/// output_dir = "./generated"
123///
124/// [headers]
125/// Authorization = "Bearer token"
126///
127/// [type_mappings]
128/// "MyCustomType" = "String"
129/// ```
130///
131/// ### YAML Format (`codegen.yml`) - *Requires `yaml-codegen-config` feature*
132/// ```yaml
133/// schema:
134/// url: https://api.example.com/graphql
135/// headers:
136/// Authorization: Bearer token
137///
138/// rust_codegen:
139/// orm: Diesel
140/// db: Postgres
141/// output_dir: ./generated
142/// ```
143///
144/// ## Example
145///
146/// ```rust
147/// use graphql_codegen_rust::{Config, cli::{OrmType, DatabaseType}};
148/// use std::collections::HashMap;
149///
150/// let config = Config {
151/// url: "https://api.example.com/graphql".to_string(),
152/// orm: OrmType::Diesel,
153/// db: DatabaseType::Postgres,
154/// output_dir: "./generated".into(),
155/// headers: HashMap::from([
156/// ("Authorization".to_string(), "Bearer token".to_string())
157/// ]),
158/// ..Default::default()
159/// };
160/// ```
161#[derive(Debug, Clone, Serialize, Deserialize, Default)]
162pub struct Config {
163 /// URL of the GraphQL endpoint that supports introspection.
164 ///
165 /// This must be a GraphQL API that responds to introspection queries.
166 /// The endpoint should be accessible and may require authentication headers.
167 ///
168 /// # Examples
169 /// - `"https://api.github.com/graphql"` (GitHub's public API)
170 /// - `"https://api.example.com/graphql"` (your custom API)
171 /// - `"http://localhost:4000/graphql"` (local development)
172 pub url: String,
173
174 /// ORM framework to generate code for.
175 ///
176 /// Determines the structure and style of generated code:
177 /// - `OrmType::Diesel`: Generates table schemas and Queryable structs
178 /// - `OrmType::SeaOrm`: Generates Entity models and ActiveModel structs
179 pub orm: OrmType,
180
181 /// Target database backend.
182 ///
183 /// Affects type mappings and SQL generation:
184 /// - `DatabaseType::Sqlite`: Uses INTEGER for IDs, TEXT for strings
185 /// - `DatabaseType::Postgres`: Uses UUID for IDs, native JSON support
186 /// - `DatabaseType::Mysql`: Uses INT for IDs, MEDIUMTEXT for large content
187 pub db: DatabaseType,
188
189 /// Directory where generated code will be written.
190 ///
191 /// The directory will be created if it doesn't exist. Generated files include:
192 /// - `src/schema.rs` (Diesel table definitions)
193 /// - `src/entities/*.rs` (Entity structs)
194 /// - `src/mod.rs` (Sea-ORM module definitions)
195 /// - `migrations/` (Database migration files)
196 pub output_dir: PathBuf,
197
198 /// Additional HTTP headers to send with GraphQL requests.
199 ///
200 /// Common headers include authentication tokens, API keys, or content-type specifications.
201 /// Headers are sent with both introspection queries and any follow-up requests.
202 ///
203 /// # Examples
204 /// ```rust
205 /// use std::collections::HashMap;
206 ///
207 /// let mut headers = HashMap::new();
208 /// headers.insert("Authorization".to_string(), "Bearer token123".to_string());
209 /// headers.insert("X-API-Key".to_string(), "key456".to_string());
210 /// ```
211 #[serde(default)]
212 pub headers: HashMap<String, String>,
213
214 /// Custom type mappings for GraphQL types to Rust types.
215 ///
216 /// Maps GraphQL type names to custom Rust types. Useful for:
217 /// - Custom scalar types (DateTime, UUID, etc.)
218 /// - Domain-specific types
219 /// - Third-party library types
220 ///
221 /// If a GraphQL type is not found in this map, default mappings are used
222 /// based on the database type and built-in GraphQL scalars.
223 ///
224 /// # Examples
225 /// ```toml
226 /// [type_mappings]
227 /// "DateTime" = "chrono::DateTime<chrono::Utc>"
228 /// "UUID" = "uuid::Uuid"
229 /// "Email" = "String" # Simple string wrapper
230 /// ```
231 #[serde(default)]
232 pub type_mappings: HashMap<String, String>,
233
234 /// Custom scalar type mappings for GraphQL scalars.
235 ///
236 /// Similar to `type_mappings` but specifically for GraphQL scalar types.
237 /// These are applied before the built-in scalar mappings.
238 ///
239 /// # Examples
240 /// ```toml
241 /// [scalar_mappings]
242 /// "Date" = "chrono::NaiveDate"
243 /// "Timestamp" = "i64"
244 /// ```
245 #[serde(default)]
246 pub scalar_mappings: HashMap<String, String>,
247
248 /// Naming convention for database tables and columns.
249 ///
250 /// Controls how GraphQL type/field names are converted to database identifiers.
251 /// - `TableNamingConvention::SnakeCase`: `UserProfile` → `user_profile`
252 /// - `TableNamingConvention::PascalCase`: `UserProfile` → `UserProfile`
253 ///
254 /// SnakeCase is recommended for most databases.
255 #[serde(default)]
256 pub table_naming: TableNamingConvention,
257
258 /// Whether to generate database migration files.
259 ///
260 /// When enabled, creates SQL migration files in the `migrations/` directory
261 /// that can be applied to set up the database schema. Each GraphQL type
262 /// gets its own migration with CREATE TABLE statements.
263 ///
264 /// Default: `true`
265 #[serde(default = "default_true")]
266 pub generate_migrations: bool,
267
268 /// Whether to generate Rust entity/model structs.
269 ///
270 /// When enabled, creates Rust structs that represent the GraphQL types:
271 /// - Diesel: `Queryable` structs for reading data
272 /// - Sea-ORM: `Model` structs with relationships
273 ///
274 /// Default: `true`
275 #[serde(default = "default_true")]
276 pub generate_entities: bool,
277}
278
279fn default_true() -> bool {
280 true
281}
282
283#[derive(Debug, Clone, Serialize, Deserialize, Default)]
284pub enum TableNamingConvention {
285 /// Convert GraphQL type names to snake_case (default)
286 #[serde(rename = "snake_case")]
287 #[default]
288 SnakeCase,
289 /// Keep GraphQL type names as-is
290 #[serde(rename = "pascal_case")]
291 PascalCase,
292}
293
294impl Config {
295 /// Load config from a file (auto-detects YAML or TOML)
296 pub fn from_file(path: &PathBuf) -> anyhow::Result<Self> {
297 let contents = fs::read_to_string(path).map_err(|e| {
298 anyhow::anyhow!(
299 "Failed to read config file '{}': {}\n\nEnsure the file exists and you have read permissions.",
300 path.display(),
301 e
302 )
303 })?;
304
305 // Check if it's YAML (starts with schema: or has .yml/.yaml extension)
306 if path
307 .extension()
308 .is_some_and(|ext| ext == "yml" || ext == "yaml")
309 || contents.trim().starts_with("schema:")
310 {
311 #[cfg(feature = "yaml-codegen-config")]
312 {
313 Self::from_yaml_str(&contents)
314 }
315 #[cfg(not(feature = "yaml-codegen-config"))]
316 {
317 Err(anyhow::anyhow!(
318 "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'"
319 ))
320 }
321 } else {
322 Self::from_toml_str(&contents)
323 }
324 }
325
326 /// Load config from TOML string
327 pub fn from_toml_str(contents: &str) -> anyhow::Result<Self> {
328 let config: Config = toml::from_str(contents).map_err(|e| {
329 anyhow::anyhow!(
330 "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.",
331 e
332 )
333 })?;
334 Ok(config)
335 }
336
337 /// Load config from YAML string
338 #[cfg(feature = "yaml-codegen-config")]
339 pub fn from_yaml_str(contents: &str) -> anyhow::Result<Self> {
340 let yaml_config: YamlConfig = serde_yaml::from_str(contents).map_err(|e| {
341 anyhow::anyhow!(
342 "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.",
343 e
344 )
345 })?;
346
347 // Extract schema info
348 let (url, headers) = match yaml_config.schema {
349 SchemaConfig::Url(url) => (url, HashMap::new()),
350 SchemaConfig::Object { url, headers } => (url, headers),
351 };
352
353 // Use rust_codegen section if present, otherwise defaults
354 let rust_config = yaml_config.rust_codegen.unwrap_or_default();
355
356 Ok(Config {
357 url,
358 orm: rust_config.orm,
359 db: rust_config.db,
360 output_dir: rust_config.output_dir,
361 headers,
362 type_mappings: rust_config.type_mappings,
363 scalar_mappings: rust_config.scalar_mappings,
364 table_naming: rust_config.table_naming,
365 generate_migrations: rust_config.generate_migrations,
366 generate_entities: rust_config.generate_entities,
367 })
368 }
369
370 /// Save config to a TOML file
371 pub fn save_to_file(&self, path: &PathBuf) -> anyhow::Result<()> {
372 let toml = toml::to_string_pretty(self)?;
373 fs::write(path, toml)?;
374 Ok(())
375 }
376
377 /// Get the config file path for a given output directory
378 pub fn config_path(output_dir: &std::path::Path) -> PathBuf {
379 output_dir.join("graphql-codegen-rust.toml")
380 }
381
382 /// Auto-detect config file in current directory
383 pub fn auto_detect_config() -> anyhow::Result<PathBuf> {
384 // Try codegen.yml first
385 let yaml_path = PathBuf::from("codegen.yml");
386 if yaml_path.exists() {
387 return Ok(yaml_path);
388 }
389
390 // Try codegen.yaml
391 let yaml_path = PathBuf::from("codegen.yaml");
392 if yaml_path.exists() {
393 return Ok(yaml_path);
394 }
395
396 // Try TOML file
397 let toml_path = PathBuf::from("graphql-codegen-rust.toml");
398 if toml_path.exists() {
399 return Ok(toml_path);
400 }
401
402 Err(anyhow::anyhow!(
403 "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>"
404 ))
405 }
406}
407
408impl From<&crate::cli::Commands> for Config {
409 fn from(cmd: &crate::cli::Commands) -> Self {
410 match cmd {
411 crate::cli::Commands::Init {
412 url,
413 orm,
414 db,
415 output,
416 headers,
417 } => {
418 let headers_map = headers.iter().cloned().collect();
419
420 Config {
421 url: url.clone(),
422 orm: orm.clone(),
423 db: db.clone(),
424 output_dir: output.clone(),
425 headers: headers_map,
426 type_mappings: HashMap::new(),
427 scalar_mappings: HashMap::new(),
428 table_naming: TableNamingConvention::default(),
429 generate_migrations: true,
430 generate_entities: true,
431 }
432 }
433 _ => unreachable!("Config can only be created from Init command"),
434 }
435 }
436}