shortener 0.2.0

A simple URL shortener.
Documentation
use std::io;
use std::process;

use clap::{CommandFactory, Parser, Subcommand};
use clap_complete::Shell;

use shortener::{Config, Logger, Shortener};

/// URL shortener server
#[derive(Parser)]
#[command(
  version,
  long_version = shortener::version_string(),
)]
struct Cli {
  /// Enable authentication for URL shortening
  #[arg(long)]
  auth: bool,

  /// Port to listen for HTTP requests
  #[arg(long, default_value_t = 8080)]
  listen_port: u16,

  /// Prefix to shortened URL, e.g. <https://example.com/>
  #[arg(long)]
  url_prefix: Option<String>,

  /// URL for main page of shortener; leave blank for default home page
  #[arg(long)]
  main_page: Option<String>,

  /// Length of shortened code
  #[arg(long, default_value_t = 6)]
  code_length: usize,

  /// Path to SQLite database for URL and API key storage
  #[arg(long, default_value = "shortener.db")]
  database: String,

  /// Path to access log file
  #[arg(long, default_value = "access.log")]
  log_file: String,

  /// Trust X-Forwarded-For header from reverse proxy
  #[arg(long)]
  trust_proxy: bool,

  #[command(subcommand)]
  command: Option<Command>,
}

#[derive(Subcommand)]
enum Command {
  /// Output a shell completion script to stdout
  Completions {
    /// Shell to generate completions for
    shell: Shell,
  },
}

impl From<Cli> for Config {
  fn from(cli: Cli) -> Config {
    let url_prefix = cli.url_prefix.unwrap_or_else(|| {
      let listen_port = cli.listen_port;
      if listen_port == 80 {
        "http://localhost/".to_owned()
      } else {
        format!("http://localhost:{listen_port}/")
      }
    });
    Config {
      auth: cli.auth,
      listen_port: cli.listen_port,
      url_prefix,
      main_page: cli.main_page,
      code_length: cli.code_length,
      database: cli.database,
      log_file: cli.log_file,
      trust_proxy: cli.trust_proxy,
    }
  }
}

#[tokio::main]
async fn main() {
  let cli = Cli::parse();

  if let Some(Command::Completions { shell }) = cli.command {
    clap_complete::generate(
      shell,
      &mut Cli::command(),
      "shortener",
      &mut io::stdout(),
    );
    return;
  }

  let config: Config = cli.into();

  if let Err(error) = Logger::init(&config.log_file) {
    eprintln!("Failed to open log file: {error}");
    process::exit(1);
  }

  log::info!("Config: {config:?}");

  let shortener = match Shortener::new(config) {
    Ok(shortener) => shortener,
    Err(error) => {
      log::error!("Failed to create shortener: {error}");
      process::exit(1);
    }
  };

  log::info!("Starting HTTP server");
  if let Err(error) = shortener.listen_and_serve().await {
    log::error!("{error}");
    process::exit(1);
  }
}