dsync 0.0.17

Generate rust structs & query functions from diesel schema files.
Documentation
use clap::{CommandFactory, Parser, Subcommand};
use clap_complete::{generate, Shell};
use dsync::{error::IOErrorToError, GenerationConfig, TableOptions};
use std::collections::HashMap;
use std::io::{BufWriter, Write};
use std::path::PathBuf;

#[derive(Debug, Parser, Clone, PartialEq)]
#[command(author, version, about, long_about = None)]
#[command(bin_name("dsync"))]
#[command(disable_help_subcommand(true))] // Disable subcommand "help", only "-h" or "--help" should be used
#[command(subcommand_negates_reqs(true))]
#[command(infer_subcommands(true))]
pub struct CliDerive {
    // use extra struct, otherwise clap subcommands require all options
    #[clap(flatten)]
    pub args: Option<MainOptions>,

    #[command(subcommand)]
    pub subcommands: Option<SubCommands>,
}

#[derive(Debug, Subcommand, Clone, PartialEq)]
pub enum SubCommands {
    /// Generate shell completions
    Completions(CommandCompletions),
}

#[derive(Debug, Parser, Clone, PartialEq)]
pub struct CommandCompletions {
    /// Set which shell completions should be generated
    /// Supported are: Bash, Elvish, Fish, PowerShell, Zsh
    #[arg(short = 's', long = "shell", value_enum)]
    pub shell: Shell,
    /// Output path where to output the completions to
    /// Not specifying this will print to STDOUT
    #[arg(short = 'o', long = "out")]
    pub output_file_path: Option<PathBuf>,
}

#[derive(Debug, Parser, Clone, PartialEq)]
pub struct MainOptions {
    /// Input diesel schema file
    #[arg(short = 'i', long = "input")]
    pub input: PathBuf,

    /// Output file, stdout if not present
    #[arg(short = 'o', long = "output")]
    pub output: PathBuf,

    /// adds the #[tsync] attribute to all structs; see https://github.com/Wulf/tsync
    #[arg(long = "tsync")]
    #[cfg(feature = "tsync")]
    pub tsync: bool,

    /// uses diesel_async for generated functions; see https://github.com/weiznich/diesel_async
    #[arg(long = "async")]
    #[cfg(feature = "async")]
    pub use_async: bool,

    /// List of columns which are automatically generated but are not primary keys (for example: "created_at", "updated_at", etc.)
    #[arg(short = 'g', long = "autogenerated-columns")]
    pub autogenerated_columns: Option<Vec<String>>,

    /// rust type which describes a connection, for example: "diesel::r2d2::PooledConnection<diesel::r2d2::ConnectionManager<diesel::PgConnection>>"
    #[arg(short = 'c', long = "connection-type")]
    pub connection_type: String,

    /// Disable generating serde implementations
    #[arg(long = "no-serde")]
    pub no_serde: bool,

    /// Set custom schema use path
    #[arg(long = "schema-path", default_value = "crate::schema::")]
    pub schema_path: String,

    /// Set custom model use path
    #[arg(long = "model-path", default_value = "crate::models::")]
    pub model_path: String,

    /// Do not generate the CRUD (impl) functions for generated models
    #[arg(long = "no-crud")]
    pub no_crud: bool,
}

fn main() {
    let res = actual_main();

    if let Err(err) = res {
        eprintln!("Error:\n{err}");
        #[cfg(feature = "backtrace")]
        {
            let backtrace = err.backtrace().to_string();

            if backtrace == "disabled backtrace" {
                eprintln!(
                    "note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace"
                );
            } else {
                eprintln!("{}", backtrace);
            }
        }
        #[cfg(not(feature = "backtrace"))]
        {
            eprintln!("backtrace support is disabled, enable feature \"backtrace\"");
        }

        std::process::exit(1);
    }
}

fn actual_main() -> dsync::Result<()> {
    let cli = CliDerive::parse();

    if let Some(subcommand) = cli.subcommands {
        return match subcommand {
            SubCommands::Completions(subcommand) => command_completions(&subcommand),
        };
    }

    let args = cli
        .args
        .expect("cli.args should be defined if no subcommand is given");

    let cols = args.autogenerated_columns.unwrap_or_default();
    let mut default_table_options = TableOptions::default()
        .autogenerated_columns(cols.iter().map(|t| t.as_str()).collect::<Vec<&str>>());

    #[cfg(feature = "tsync")]
    if args.tsync {
        default_table_options = default_table_options.tsync();
    }

    #[cfg(feature = "async")]
    if args.use_async {
        default_table_options = default_table_options.use_async();
    }

    if args.no_serde {
        default_table_options = default_table_options.disable_serde();
    }

    if args.no_crud {
        default_table_options = default_table_options.disable_fns();
    }

    dsync::generate_files(
        args.input,
        args.output,
        GenerationConfig {
            default_table_options,
            table_options: HashMap::from([]),
            connection_type: args.connection_type,
            schema_path: args.schema_path,
            model_path: args.model_path,
        },
    )?;

    Ok(())
}

/// Handler function for the "completions" subcommand
/// This function is mainly to keep the code structured and sorted
#[inline]
pub fn command_completions(sub_args: &CommandCompletions) -> dsync::Result<()> {
    // if there is a output file path, use that path, otherwise use stdout
    let mut writer: BufWriter<Box<dyn Write>> = match &sub_args.output_file_path {
        Some(v) => {
            if v.exists() {
                return Err(dsync::Error::other("Output file already exists"));
            }
            let v_parent = v
                .parent()
                .expect("Expected input filename to have a parent");
            std::fs::create_dir_all(v_parent).attach_path_err(v_parent)?;
            BufWriter::new(Box::from(std::fs::File::create(v).attach_path_err(v)?))
        }
        None => BufWriter::new(Box::from(std::io::stdout())),
    };
    let mut parsed = CliDerive::command();
    let bin_name = parsed
        .get_bin_name()
        .expect("Expected binary to have a binary name")
        .to_string();
    generate(sub_args.shell, &mut parsed, bin_name, &mut writer);

    Ok(())
}