graphql_codegen_rust/
cli.rs

1use clap::{Parser, Subcommand};
2use std::path::PathBuf;
3
4#[derive(Parser)]
5#[command(name = "graphql-codegen-rust")]
6#[command(version = env!("CARGO_PKG_VERSION"))]
7#[command(about = "Generate Rust ORM code from GraphQL schemas")]
8pub struct Cli {
9    #[command(subcommand)]
10    pub command: Option<Commands>,
11
12    /// Increase verbosity level (-v, -vv, -vvv)
13    #[arg(short, long, action = clap::ArgAction::Count)]
14    pub verbose: u8,
15}
16
17#[derive(Subcommand)]
18pub enum Commands {
19    /// Initialize a new GraphQL codegen project
20    Init {
21        /// GraphQL endpoint URL
22        #[arg(short, long)]
23        url: String,
24
25        /// ORM to generate code for
26        #[arg(short, long, value_enum, default_value = "diesel")]
27        orm: OrmType,
28
29        /// Database backend
30        #[arg(short, long, value_enum, default_value = "sqlite")]
31        db: DatabaseType,
32
33        /// Output directory for generated code
34        #[arg(long, default_value = "./generated")]
35        output: PathBuf,
36
37        /// Additional headers for GraphQL requests (key:value pairs)
38        #[arg(short = 'H', long, value_parser = parse_header)]
39        headers: Vec<(String, String)>,
40    },
41
42    /// Generate code from existing configuration
43    Generate {
44        /// Config file path (auto-detects codegen.yml or TOML)
45        #[arg(short, long)]
46        config: Option<PathBuf>,
47
48        /// Output directory (overrides config)
49        #[arg(short, long)]
50        output: Option<PathBuf>,
51    },
52}
53
54/// Supported ORM frameworks for code generation.
55///
56/// Each ORM generates different code structures optimized for their respective ecosystems:
57/// - **Diesel**: Mature, compile-time SQL safety, macro-heavy approach
58/// - **Sea-ORM**: Async-first, runtime SQL building, entity relationships
59#[derive(
60    Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize, clap::ValueEnum, Default,
61)]
62pub enum OrmType {
63    /// Generates Diesel table schemas, Queryable structs, and Insertable structs.
64    /// Best for applications needing maximum compile-time safety and performance.
65    #[default]
66    Diesel,
67
68    /// Generates Sea-ORM Entity models, ActiveModel structs, and migration files.
69    /// Best for async applications with complex relationships and runtime flexibility.
70    SeaOrm,
71}
72
73/// Supported database backends.
74///
75/// Each database has different capabilities and type mappings:
76/// - **SQLite**: File-based, simple deployment, limited concurrent writes
77/// - **PostgreSQL**: Advanced features, JSON support, excellent concurrency
78/// - **MySQL**: High performance, wide adoption, good for large datasets
79#[derive(
80    Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize, clap::ValueEnum, Default,
81)]
82pub enum DatabaseType {
83    /// SQLite database - file-based, ACID compliant, no server required.
84    /// Uses INTEGER for IDs, TEXT for strings, REAL for floats.
85    #[default]
86    Sqlite,
87
88    /// PostgreSQL database - advanced open-source RDBMS.
89    /// Uses UUID for IDs, native JSON/JSONB, full-text search, advanced indexing.
90    Postgres,
91
92    /// MySQL database - high-performance, widely adopted RDBMS.
93    /// Uses INT/UNSIGNED for IDs, VARCHAR/TEXT for strings, various numeric types.
94    Mysql,
95}
96
97/// Parses a header string in "key:value" format for CLI arguments.
98///
99/// Used internally by clap to validate and parse header arguments.
100/// Headers must be provided as `key:value` pairs with no spaces around the colon.
101///
102/// # Examples
103/// ```rust
104/// # fn test_parse_header() {
105/// let result = graphql_codegen_rust::cli::parse_header("Authorization:Bearer token123");
106/// assert_eq!(result.unwrap(), ("Authorization".to_string(), "Bearer token123".to_string()));
107/// # }
108/// ```
109///
110/// # Errors
111/// Returns an error if the string doesn't contain exactly one colon separator,
112/// or if the key or value would be empty after trimming.
113pub fn parse_header(s: &str) -> Result<(String, String), String> {
114    let parts: Vec<&str> = s.splitn(2, ':').collect();
115    if parts.len() != 2 {
116        return Err(format!(
117            "Invalid header format '{}'. Headers must be in 'key:value' format.\nExample: --header 'Authorization:Bearer token123'",
118            s
119        ));
120    }
121    let key = parts[0].trim();
122    let value = parts[1].trim();
123
124    if key.is_empty() {
125        return Err("Header key cannot be empty. Format: 'key:value'".to_string());
126    }
127    if value.is_empty() {
128        return Err("Header value cannot be empty. Format: 'key:value'".to_string());
129    }
130
131    Ok((key.to_string(), value.to_string()))
132}