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}