use anyhow::Result;
use clap::Parser;
use serde::Serialize;
use crate::cli::drive::format::{
output_as, sanitize_for_terminal, write_scalar_jsonl, JsonlSerialize, OutputFormat,
};
use crate::drive::client::DriveClient;
use crate::drive::files_api::FilesApi;
use crate::drive::rename::{self, RenameOutcome};
#[derive(Parser)]
pub struct RenameCommand {
pub file_id: String,
pub new_name: String,
#[arg(long)]
pub dry_run: bool,
#[arg(short = 'o', long, value_enum, default_value_t = OutputFormat::Table)]
pub output: OutputFormat,
}
impl RenameCommand {
pub async fn execute(self, client: &DriveClient) -> Result<()> {
run_rename(
client,
&self.file_id,
&self.new_name,
self.dry_run,
&self.output,
)
.await
}
}
#[derive(Debug, Clone, Serialize)]
struct DryRunReport {
file_id: String,
old_name: String,
new_name: String,
}
impl JsonlSerialize for DryRunReport {
fn write_jsonl(&self, out: &mut dyn std::io::Write) -> Result<()> {
write_scalar_jsonl(self, out)
}
}
async fn run_rename(
client: &DriveClient,
file_id: &str,
new_name: &str,
dry_run: bool,
output: &OutputFormat,
) -> Result<()> {
if dry_run {
let existing = FilesApi::new(client).get_metadata(file_id).await?;
let report = DryRunReport {
file_id: file_id.to_string(),
old_name: existing.name,
new_name: new_name.to_string(),
};
if output_as(&report, output)? {
return Ok(());
}
println!(
"Would rename: {} -> {} ({})",
sanitize_for_terminal(&report.old_name),
sanitize_for_terminal(&report.new_name),
sanitize_for_terminal(&report.file_id)
);
return Ok(());
}
let outcome = rename::rename(client, file_id, new_name).await?;
print_outcome(&outcome, output)
}
fn print_outcome(outcome: &RenameOutcome, output: &OutputFormat) -> Result<()> {
if output_as(outcome, output)? {
return Ok(());
}
println!(
"Renamed: {} -> {} ({})",
sanitize_for_terminal(&outcome.old_name),
sanitize_for_terminal(&outcome.new_name),
sanitize_for_terminal(&outcome.file_id)
);
Ok(())
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
use crate::drive::auth::{DriveCredentials, DriveScope};
use crate::utils::secret::Secret;
fn test_credentials() -> DriveCredentials {
DriveCredentials {
client_id: "client-1".to_string(),
client_secret: Secret::new("secret-1"),
refresh_token: Secret::new("refresh-1"),
scope: DriveScope::Metadata,
}
}
async fn client_with_bootstrapped_token(server: &wiremock::MockServer) -> DriveClient {
wiremock::Mock::given(wiremock::matchers::method("POST"))
.and(wiremock::matchers::path("/token"))
.respond_with(
wiremock::ResponseTemplate::new(200).set_body_json(serde_json::json!({
"access_token": "test-token",
"expires_in": 3600,
})),
)
.mount(server)
.await;
let mut client = DriveClient::new(&server.uri(), &test_credentials()).unwrap();
crate::drive::client::test_support::replace_session(
&mut client,
&test_credentials(),
&format!("{}/token", server.uri()),
);
client
}
#[tokio::test]
async fn dry_run_reports_current_name_without_renaming() {
let server = wiremock::MockServer::start().await;
let client = client_with_bootstrapped_token(&server).await;
wiremock::Mock::given(wiremock::matchers::method("GET"))
.and(wiremock::matchers::path("/drive/v3/files/f1"))
.respond_with(
wiremock::ResponseTemplate::new(200).set_body_json(serde_json::json!({
"id": "f1", "name": "Old Name",
})),
)
.mount(&server)
.await;
run_rename(&client, "f1", "New Name", true, &OutputFormat::Table)
.await
.unwrap();
}
#[tokio::test]
async fn run_rename_renames_and_propagates_errors() {
let server = wiremock::MockServer::start().await;
let client = client_with_bootstrapped_token(&server).await;
wiremock::Mock::given(wiremock::matchers::method("GET"))
.and(wiremock::matchers::path("/drive/v3/files/f1"))
.respond_with(wiremock::ResponseTemplate::new(404).set_body_string("not found"))
.mount(&server)
.await;
let err = run_rename(&client, "f1", "New Name", false, &OutputFormat::Table)
.await
.unwrap_err();
assert!(err.to_string().contains("404"));
}
}