use anyhow::Result;
use clap::Parser;
use crate::cli::drive::format::{output_as, sanitize_for_terminal, OutputFormat};
use crate::cli::drive::helpers;
use crate::cli::drive::sheets::format::DimensionArg;
use crate::drive::client::DriveClient;
use crate::drive::sheets::auto_fill::{auto_fill, describe_lines, AutoFillForm, AutoFillOptions};
use crate::drive::sheets::client::SheetsClient;
#[derive(Parser)]
#[command(group(clap::ArgGroup::new("input_form")
.args(["range", "source"])
.required(true)))]
pub struct AutoFillCommand {
pub spreadsheet_id: String,
#[arg(long, value_name = "NAME")]
pub sheet: Option<String>,
#[arg(long, value_name = "A1")]
pub range: Option<String>,
#[arg(long, value_name = "A1", requires_all = ["dimension", "fill_length"])]
pub source: Option<String>,
#[arg(long, value_enum, requires = "source")]
pub dimension: Option<DimensionArg>,
#[arg(
long,
value_name = "N",
requires = "source",
allow_hyphen_values = true
)]
pub fill_length: Option<i64>,
#[arg(long)]
pub alternate_series: bool,
#[arg(long)]
pub dry_run: bool,
#[command(flatten)]
pub lease: crate::cli::drive::helpers::LeaseTokenArg,
#[arg(short = 'o', long, value_enum, default_value_t = OutputFormat::Table)]
pub output: OutputFormat,
}
impl AutoFillCommand {
pub async fn execute(self, client: &DriveClient) -> Result<()> {
let form = match (self.source, self.dimension, self.fill_length) {
(Some(source), Some(dimension), Some(fill_length)) => {
AutoFillForm::SourceAndDestination {
sheet: self.sheet,
source: Some(source),
dimension: dimension.engine(),
fill_length,
}
}
_ => AutoFillForm::Range {
sheet: self.sheet,
range: self.range,
},
};
let opts = AutoFillOptions {
spreadsheet_id: self.spreadsheet_id,
form,
use_alternate_series: self.alternate_series,
dry_run: self.dry_run,
lease_token: self.lease.lease,
ledger_path: helpers::resolve_ledger_path(self.dry_run)?,
};
run_auto_fill(client, &opts, &self.output).await
}
}
async fn run_auto_fill(
client: &DriveClient,
opts: &AutoFillOptions,
output: &OutputFormat,
) -> Result<()> {
let sheets = SheetsClient::from_drive_client(client)?;
let rules = helpers::active_account_rules()?;
let outcome = auto_fill(client, &sheets, opts, &rules).await;
if output_as(&outcome, output)? {
return Ok(());
}
let lines: Vec<String> = describe_lines(&outcome)
.into_iter()
.map(|line| sanitize_for_terminal(&line))
.collect();
println!("{}", lines.join("\n"));
Ok(())
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
use crate::drive::auth::{DriveCredentials, DriveGrantedScopes};
use crate::utils::secret::Secret;
use clap::CommandFactory;
async fn client(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 credentials = DriveCredentials {
client_id: "client-1".into(),
client_secret: Secret::new("secret-1"),
refresh_token: Secret::new("refresh-1"),
scope: DriveGrantedScopes::READONLY,
};
let mut client = DriveClient::new(&server.uri(), &credentials).unwrap();
crate::drive::client::test_support::replace_session(
&mut client,
&credentials,
&format!("{}/token", server.uri()),
);
client
}
#[tokio::test]
async fn both_cli_forms_reach_the_gate_and_render_table_and_json() {
let guard = crate::drive::test_support::EnvGuard::take();
let _dir = guard.clear_credentials();
let server = wiremock::MockServer::start().await;
let client = client(&server).await;
std::env::set_var(crate::drive::sheets::client::SHEETS_API_URL, server.uri());
wiremock::Mock::given(wiremock::matchers::method("GET"))
.and(wiremock::matchers::path("/drive/v3/files/sheet-1"))
.respond_with(
wiremock::ResponseTemplate::new(200).set_body_json(serde_json::json!({
"id": "sheet-1", "name": "Budget",
"mimeType": crate::drive::types::GOOGLE_SHEET_MIME_TYPE,
"parents": ["folder-1"],
})),
)
.mount(&server)
.await;
wiremock::Mock::given(wiremock::matchers::method("GET"))
.and(wiremock::matchers::path("/drive/v3/files/folder-1"))
.respond_with(
wiremock::ResponseTemplate::new(200).set_body_json(serde_json::json!({
"id": "folder-1", "name": "folder-1",
"mimeType": "application/vnd.google-apps.folder", "parents": [],
})),
)
.mount(&server)
.await;
let range = AutoFillCommand::try_parse_from([
"auto-fill",
"sheet-1",
"--sheet",
"Q1",
"--range",
"A1:A3",
"--dry-run",
"--output",
"table",
])
.unwrap();
range.execute(&client).await.unwrap();
let source = AutoFillCommand::try_parse_from([
"auto-fill",
"sheet-1",
"--source",
"Q1!A1:A3",
"--dimension",
"rows",
"--fill-length",
"-1",
"--alternate-series",
"--dry-run",
"--output",
"json",
])
.unwrap();
source.execute(&client).await.unwrap();
assert_eq!(
server
.received_requests()
.await
.unwrap()
.iter()
.filter(|r| r.url.path() == "/drive/v3/files/sheet-1")
.count(),
2
);
}
#[test]
fn range_and_source_are_mutually_exclusive() {
let err = AutoFillCommand::command()
.try_get_matches_from([
"auto-fill",
"sheet-1",
"--range",
"A1:A2",
"--source",
"A1:A2",
"--dimension",
"rows",
"--fill-length",
"1",
])
.unwrap_err();
assert_eq!(err.kind(), clap::error::ErrorKind::ArgumentConflict);
}
#[test]
fn one_of_range_or_source_is_required() {
let err = AutoFillCommand::command()
.try_get_matches_from(["auto-fill", "sheet-1"])
.unwrap_err();
assert_eq!(err.kind(), clap::error::ErrorKind::MissingRequiredArgument);
}
#[test]
fn source_without_dimension_or_fill_length_is_refused() {
let err = AutoFillCommand::command()
.try_get_matches_from(["auto-fill", "sheet-1", "--source", "A1:A2"])
.unwrap_err();
assert_eq!(err.kind(), clap::error::ErrorKind::MissingRequiredArgument);
}
#[test]
fn a_negative_fill_length_parses() {
let matches = AutoFillCommand::command()
.try_get_matches_from([
"auto-fill",
"sheet-1",
"--source",
"A4:A5",
"--dimension",
"rows",
"--fill-length",
"-3",
])
.unwrap();
assert_eq!(matches.get_one::<i64>("fill_length").copied(), Some(-3));
}
#[test]
fn range_form_parses_with_no_dimension_or_fill_length() {
AutoFillCommand::command()
.try_get_matches_from(["auto-fill", "sheet-1", "--range", "A1:A10"])
.unwrap();
}
}