mod batch_update_builder;
mod compilation_target;
mod credentials;
mod google_sheets_modifier;
use super::{ExistingCell, ExistingValues};
use crate::{Error, Result, Runtime, Template};
use batch_update_builder::BatchUpdateBuilder;
use credentials::Credentials;
use google_sheets4::hyper;
use google_sheets4::hyper_rustls;
use google_sheets4::oauth2;
type SheetsHub = google_sheets4::Sheets<hyper_rustls::HttpsConnector<hyper::client::HttpConnector>>;
type SheetsValue = google_sheets4::api::CellData;
pub(crate) struct GoogleSheets<'a> {
async_runtime: tokio::runtime::Runtime,
credentials: Credentials,
runtime: &'a Runtime,
pub(crate) sheet_id: String,
}
macro_rules! unwrap_or_empty {
($to_unwrap:expr) => {{
match $to_unwrap {
Some(s) => s,
None => return Ok(ExistingValues::default()),
}
}};
}
impl<'a> GoogleSheets<'a> {
pub(crate) fn new<S: Into<String>>(runtime: &'a Runtime, sheet_id: S) -> Result<Self> {
let credentials = runtime.try_into()?;
let async_runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.map_err(|e| {
Error::InitError(format!(
"Error starting async runtime to write Google Sheets: {e}"
))
})?;
Ok(Self {
async_runtime,
credentials,
sheet_id: sheet_id.into(),
runtime,
})
}
async fn read_existing_cells(&self, hub: &SheetsHub) -> Result<ExistingValues<SheetsValue>> {
let spreadsheet = match hub
.spreadsheets()
.get(&self.sheet_id)
.include_grid_data(true)
.doit()
.await
{
Ok((_, s)) => s,
Err(e) => {
match e {
google_sheets4::Error::BadRequest(obj)
if obj["error"]["code"].as_u64().is_some_and(|c| c == 404) =>
{
return Ok(ExistingValues::default());
}
_ => {
self.runtime
.warn(format!("Google Sheets API error response: {e}"));
return Err(Error::GoogleSetupError(format!(
"Error reading existing sheet: {e}",
)));
}
}
}
};
let sheets = unwrap_or_empty!(spreadsheet.sheets); let sheet = unwrap_or_empty!(sheets.get(0)); let data = unwrap_or_empty!(&sheet.data); let grid_data = unwrap_or_empty!(data.get(0)); let row_data = unwrap_or_empty!(&grid_data.row_data);
let mut existing_cells = vec![];
for row in row_data.iter() {
if let Some(v) = &row.values {
existing_cells.push(
v.iter()
.map(|cell| ExistingCell::Value(cell.clone()))
.collect(),
);
} else {
existing_cells.push(vec![]);
}
}
Ok(ExistingValues {
cells: existing_cells,
})
}
async fn sheets_hub(&self) -> Result<SheetsHub> {
let auth = if self.credentials.is_authorized_user()? {
let secret = oauth2::read_authorized_user_secret(&self.credentials.file)
.await
.map_err(|e| {
Error::GoogleSetupError(format!("Error reading application secret: {e}"))
})?;
oauth2::AuthorizedUserAuthenticator::builder(secret)
.build()
.await
.map_err(|e| {
Error::GoogleSetupError(format!(
"Error requesting access to the spreadsheet: {e}"
))
})?
} else if self.credentials.is_service_account()? {
let secret = oauth2::read_service_account_key(&self.credentials.file)
.await
.map_err(|e| {
Error::GoogleSetupError(format!(
"Error reading sevice account credentials: {e}"
))
})?;
oauth2::ServiceAccountAuthenticator::builder(secret)
.build()
.await
.map_err(|e| {
Error::GoogleSetupError(format!(
"Error building service account authenticator: {e}"
))
})?
} else {
return Err(Error::GoogleSetupError(
"Credentials file must be a service or user account".to_string(),
));
};
Ok(google_sheets4::Sheets::new(
hyper::Client::builder().build(
hyper_rustls::HttpsConnectorBuilder::new()
.with_native_roots()
.https_or_http()
.enable_http1()
.enable_http2()
.build(),
),
auth,
))
}
async fn write_sheet(&self, template: &Template<'a>) -> Result<()> {
let hub = self.sheets_hub().await?;
let existing_values = self.read_existing_cells(&hub).await?;
let batch_update_request =
BatchUpdateBuilder::new(self.runtime, template, &existing_values).build();
hub.spreadsheets()
.batch_update(batch_update_request, &self.sheet_id)
.doit()
.await
.map(|_i| ())
.map_err(|e| {
self.runtime.error(format!("{e:?}"));
self.runtime
.output
.clone()
.into_error(format!("Error writing to Google Sheets: {e}"))
})
}
}
#[cfg(test)]
mod tests {
}