Skip to content

Commit e333438

Browse files
committed
Move CLI parsing to separate module
fix potential cli parsing crash
1 parent 7f7549e commit e333438

File tree

6 files changed

+131
-111
lines changed

6 files changed

+131
-111
lines changed

src/app_config.rs

Lines changed: 3 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use crate::webserver::content_security_policy::ContentSecurityPolicyTemplate;
22
use crate::webserver::routing::RoutingConfig;
33
use anyhow::Context;
4-
use clap::Parser;
4+
use crate::cli::arguments::{Cli, parse_cli};
55
use config::Config;
66
use openidconnect::IssuerUrl;
77
use percent_encoding::AsciiSet;
@@ -10,34 +10,6 @@ use serde::{Deserialize, Deserializer, Serialize};
1010
use std::net::{SocketAddr, ToSocketAddrs};
1111
use std::path::{Path, PathBuf};
1212

13-
#[derive(Parser)]
14-
#[clap(author, version, about, long_about = None)]
15-
pub struct Cli {
16-
/// The directory where the .sql files are located.
17-
#[clap(short, long)]
18-
pub web_root: Option<PathBuf>,
19-
/// The directory where the sqlpage.json configuration, the templates, and the migrations are located.
20-
#[clap(short = 'd', long)]
21-
pub config_dir: Option<PathBuf>,
22-
/// The path to the configuration file.
23-
#[clap(short = 'c', long)]
24-
pub config_file: Option<PathBuf>,
25-
26-
/// Subcommands for additional functionality.
27-
#[clap(subcommand)]
28-
pub command: Option<Commands>,
29-
}
30-
31-
/// Enum for subcommands.
32-
#[derive(Parser)]
33-
pub enum Commands {
34-
/// Create a new migration file.
35-
CreateMigration {
36-
/// Name of the migration.
37-
migration_name: String,
38-
},
39-
}
40-
4113
#[cfg(not(feature = "lambda-web"))]
4214
const DEFAULT_DATABASE_FILE: &str = "sqlpage.db";
4315

@@ -154,8 +126,8 @@ impl AppConfig {
154126
}
155127
}
156128

157-
pub fn load_from_cli() -> anyhow::Result<AppConfig> {
158-
let cli = Cli::parse();
129+
pub fn load_config() -> anyhow::Result<AppConfig> {
130+
let cli = parse_cli()?;
159131
AppConfig::from_cli(&cli)
160132
}
161133

@@ -654,23 +626,6 @@ mod test {
654626
);
655627
}
656628

657-
#[test]
658-
fn test_cli_argument_parsing() {
659-
let cli = Cli::parse_from([
660-
"sqlpage",
661-
"--web-root",
662-
"/path/to/web",
663-
"--config-dir",
664-
"/path/to/config",
665-
"--config-file",
666-
"/path/to/config.json",
667-
]);
668-
669-
assert_eq!(cli.web_root, Some(PathBuf::from("/path/to/web")));
670-
assert_eq!(cli.config_dir, Some(PathBuf::from("/path/to/config")));
671-
assert_eq!(cli.config_file, Some(PathBuf::from("/path/to/config.json")));
672-
}
673-
674629
#[test]
675630
fn test_sqlpage_prefixed_env_variable_parsing() {
676631
let _lock = ENV_LOCK

src/cli/arguments.rs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
use clap::Parser;
2+
use std::path::PathBuf;
3+
use super::commands::SubCommand;
4+
5+
#[derive(Parser)]
6+
#[clap(author, version, about, long_about = None)]
7+
pub struct Cli {
8+
/// The directory where the .sql files are located.
9+
#[clap(short, long)]
10+
pub web_root: Option<PathBuf>,
11+
/// The directory where the sqlpage.json configuration, the templates, and the migrations are located.
12+
#[clap(short = 'd', long)]
13+
pub config_dir: Option<PathBuf>,
14+
/// The path to the configuration file.
15+
#[clap(short = 'c', long)]
16+
pub config_file: Option<PathBuf>,
17+
18+
/// Subcommands for additional functionality.
19+
#[clap(subcommand)]
20+
pub command: Option<SubCommand>,
21+
}
22+
23+
pub fn parse_cli() -> anyhow::Result<Cli> {
24+
let cli = Cli::parse();
25+
Ok(cli)
26+
}
27+
28+
#[test]
29+
fn test_cli_argument_parsing() {
30+
let cli = Cli::parse_from([
31+
"sqlpage",
32+
"--web-root",
33+
"/path/to/web",
34+
"--config-dir",
35+
"/path/to/config",
36+
"--config-file",
37+
"/path/to/config.json",
38+
]);
39+
40+
assert_eq!(cli.web_root, Some(PathBuf::from("/path/to/web")));
41+
assert_eq!(cli.config_dir, Some(PathBuf::from("/path/to/config")));
42+
assert_eq!(cli.config_file, Some(PathBuf::from("/path/to/config.json")));
43+
}

src/cli/commands.rs

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
use clap::Parser;
2+
use std::path::Path;
3+
use chrono::Utc;
4+
5+
use crate::app_config::AppConfig;
6+
7+
/// Sub-commands for the sqlpage CLI.
8+
/// Each subcommand can be executed using the `sqlpage <subcommand name>` from the command line.
9+
#[derive(Parser)]
10+
pub enum SubCommand {
11+
/// Create a new migration file.
12+
CreateMigration {
13+
/// Name of the migration.
14+
migration_name: String,
15+
},
16+
}
17+
18+
impl SubCommand {
19+
/// Execute the subcommand.
20+
pub async fn execute(&self, app_config: AppConfig) -> anyhow::Result<()> {
21+
match self {
22+
SubCommand::CreateMigration { migration_name } => {
23+
// Pass configuration_directory from app_config
24+
create_migration_file(
25+
migration_name,
26+
&app_config.configuration_directory,
27+
).await?;
28+
Ok(())
29+
}
30+
}
31+
}
32+
}
33+
34+
async fn create_migration_file(
35+
migration_name: &str,
36+
configuration_directory: &Path,
37+
) -> anyhow::Result<()> {
38+
let timestamp = Utc::now().format("%Y%m%d%H%M%S").to_string();
39+
let snake_case_name = migration_name
40+
.replace(|c: char| !c.is_alphanumeric(), "_")
41+
.to_lowercase();
42+
let file_name = format!("{timestamp}_{snake_case_name}.sql");
43+
let migrations_dir = Path::new(configuration_directory).join("migrations");
44+
45+
if !migrations_dir.exists() {
46+
tokio::fs::create_dir_all(&migrations_dir).await?;
47+
}
48+
49+
let mut unique_file_name = file_name.clone();
50+
let mut counter = 1;
51+
52+
while migrations_dir.join(&unique_file_name).exists() {
53+
unique_file_name = format!("{timestamp}_{snake_case_name}_{counter}.sql");
54+
counter += 1;
55+
}
56+
57+
let file_path = migrations_dir.join(unique_file_name);
58+
tokio::fs::write(&file_path, "-- Write your migration here\n").await?;
59+
60+
// the following code cleans up the display path to show where the migration was created
61+
// relative to the current working directory, and then outputs the path to the migration
62+
let file_path_canon = file_path.canonicalize().unwrap_or(file_path.clone());
63+
let cwd_canon = std::env::current_dir()?
64+
.canonicalize()
65+
.unwrap_or(std::env::current_dir()?);
66+
let rel_path = match file_path_canon.strip_prefix(&cwd_canon) {
67+
Ok(p) => p,
68+
Err(_) => file_path_canon.as_path(),
69+
};
70+
let mut display_path_str = rel_path.display().to_string();
71+
if display_path_str.starts_with("\\\\?\\") {
72+
display_path_str = display_path_str.trim_start_matches("\\\\?\\").to_string();
73+
}
74+
display_path_str = display_path_str.replace('\\', "/");
75+
println!("Migration file created: {display_path_str}");
76+
Ok(())
77+
}

src/cli/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
pub mod arguments;
2+
pub mod commands;

src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ pub mod template_helpers;
7979
pub mod templates;
8080
pub mod utils;
8181
pub mod webserver;
82+
pub mod cli;
8283

8384
use crate::app_config::AppConfig;
8485
use crate::filesystem::FileSystem;

src/main.rs

Lines changed: 5 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
use clap::Parser;
21
use sqlpage::{
3-
app_config,
2+
app_config::AppConfig,
3+
cli,
44
webserver::{self, Database},
55
AppState,
66
};
@@ -15,20 +15,11 @@ async fn main() {
1515
}
1616

1717
async fn start() -> anyhow::Result<()> {
18-
let app_config = app_config::load_from_cli()?;
19-
let cli = app_config::Cli::parse();
18+
let cli = cli::arguments::parse_cli()?;
19+
let app_config = AppConfig::from_cli(&cli)?;
2020

2121
if let Some(command) = cli.command {
22-
match command {
23-
app_config::Commands::CreateMigration { migration_name } => {
24-
// Pass configuration_directory from app_config
25-
create_migration_file(
26-
&migration_name,
27-
app_config.configuration_directory.to_str().unwrap(),
28-
)?;
29-
return Ok(());
30-
}
31-
}
22+
return command.execute(app_config).await;
3223
}
3324

3425
let db = Database::init(&app_config).await?;
@@ -58,52 +49,3 @@ fn init_logging() {
5849
Err(e) => log::error!("Error loading .env file: {e}"),
5950
}
6051
}
61-
62-
fn create_migration_file(
63-
migration_name: &str,
64-
configuration_directory: &str,
65-
) -> anyhow::Result<()> {
66-
use chrono::Utc;
67-
use std::fs;
68-
use std::path::Path;
69-
70-
let timestamp = Utc::now().format("%Y%m%d%H%M%S").to_string();
71-
let snake_case_name = migration_name
72-
.replace(|c: char| !c.is_alphanumeric(), "_")
73-
.to_lowercase();
74-
let file_name = format!("{timestamp}_{snake_case_name}.sql");
75-
let migrations_dir = Path::new(configuration_directory).join("migrations");
76-
77-
if !migrations_dir.exists() {
78-
fs::create_dir_all(&migrations_dir)?;
79-
}
80-
81-
let mut unique_file_name = file_name.clone();
82-
let mut counter = 1;
83-
84-
while migrations_dir.join(&unique_file_name).exists() {
85-
unique_file_name = format!("{timestamp}_{snake_case_name}_{counter}.sql");
86-
counter += 1;
87-
}
88-
89-
let file_path = migrations_dir.join(unique_file_name);
90-
fs::write(&file_path, "-- Write your migration here\n")?;
91-
92-
// the following code cleans up the display path to show where the migration was created
93-
// relative to the current working directory, and then outputs the path to the migration
94-
let file_path_canon = file_path.canonicalize().unwrap_or(file_path.clone());
95-
let cwd_canon = std::env::current_dir()?
96-
.canonicalize()
97-
.unwrap_or(std::env::current_dir()?);
98-
let rel_path = match file_path_canon.strip_prefix(&cwd_canon) {
99-
Ok(p) => p,
100-
Err(_) => file_path_canon.as_path(),
101-
};
102-
let mut display_path_str = rel_path.display().to_string();
103-
if display_path_str.starts_with("\\\\?\\") {
104-
display_path_str = display_path_str.trim_start_matches("\\\\?\\").to_string();
105-
}
106-
display_path_str = display_path_str.replace('\\', "/");
107-
println!("Migration file created: {display_path_str}");
108-
Ok(())
109-
}

0 commit comments

Comments
 (0)