1mod files;
7mod oauth;
8mod sheet;
9mod sheet_test_client;
10mod tiller;
11
12use crate::api::sheet::GoogleSheet;
13use crate::api::tiller::TillerImpl;
14use crate::model::TillerData;
15use crate::Config;
16use crate::Result;
17pub(super) use oauth::TokenProvider;
18pub(super) use sheet_test_client::TestSheet;
19use std::env::VarError;
20
21use crate::error::{ErrorType, IntoResult, Res};
22#[cfg(test)]
23pub(super) use sheet_test_client::{SheetCall, TestSheetState};
24
25const OAUTH_SCOPES: &[&str] = &[
29 "https://www.googleapis.com/auth/spreadsheets",
30 "https://www.googleapis.com/auth/drive",
31];
32
33pub(crate) const TRANSACTIONS: &str = "Transactions";
35pub(crate) const CATEGORIES: &str = "Categories";
36pub(crate) const AUTO_CAT: &str = "AutoCat";
37
38#[derive(Debug, Clone, PartialEq)]
40pub struct SheetRange {
41 pub range: String,
43 pub values: Vec<Vec<String>>,
45}
46
47pub(crate) const MODE_ENV: &str = "TILLER_SYNC_IN_TEST_MODE";
50
51#[derive(Default, Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash)]
53pub enum Mode {
54 #[default]
56 Google,
57 Testing,
59}
60
61impl Mode {
62 pub fn from_env() -> Self {
68 match std::env::var(MODE_ENV) {
69 Err(VarError::NotPresent) => Self::Google,
70 _ => Self::Testing,
71 }
72 }
73}
74
75pub async fn sheet(config: Config, mode: Mode) -> Result<Box<dyn Sheet>> {
79 let sheet_client: Box<dyn Sheet> = match mode {
80 Mode::Google => {
81 let token_provider =
82 TokenProvider::load(config.client_secret_path(), config.token_path())
83 .await
84 .pub_result(ErrorType::Config)?;
85 Box::new(
86 GoogleSheet::new(config.clone(), token_provider)
87 .await
88 .pub_result(ErrorType::Internal)?,
89 )
90 }
91 Mode::Testing => Box::new(TestSheet::new_with_seed_data(config.spreadsheet_id())),
92 };
93
94 Ok(sheet_client)
95}
96
97pub async fn tiller(sheet: Box<dyn Sheet>) -> Res<impl Tiller> {
100 TillerImpl::new(sheet).await
101}
102
103#[async_trait::async_trait]
104pub trait Sheet: Send {
105 async fn get(&mut self, sheet_name: &str) -> Res<Vec<Vec<String>>>;
107
108 async fn get_formulas(&mut self, sheet_name: &str) -> Res<Vec<Vec<String>>>;
110
111 async fn clear_ranges(&mut self, ranges: &[&str]) -> Res<()>;
114
115 async fn write_ranges(&mut self, data: &[SheetRange]) -> Res<()>;
118
119 async fn copy_spreadsheet(&mut self, new_name: &str) -> Res<String>;
122}
123
124#[async_trait::async_trait]
125pub trait Tiller {
126 async fn get_data(&mut self) -> Res<TillerData>;
128
129 async fn copy_spreadsheet(&mut self, new_name: &str) -> Res<String>;
132
133 async fn clear_and_write_data(&mut self, data: &TillerData) -> Res<()>;
136
137 async fn verify_write(&mut self, expected: &TillerData) -> Res<(usize, usize, usize)>;
140}
141
142#[tokio::test]
143async fn test_sync_down_behavior() {
144 use crate::model::{Amount, RowCol};
145 use std::str::FromStr as _;
146
147 let client = Box::new(TestSheet::new_with_seed_data("test_sync_down_behavior"));
148 let mut tiller = crate::api::tiller(client).await.unwrap();
149 let tiller_data = tiller.get_data().await.unwrap();
150
151 for (tix, transaction) in tiller_data.transactions.data().iter().enumerate() {
154 let amount = &transaction.amount;
155 let abs = Amount::from_str(transaction.other_fields.get("Custom Column").unwrap()).unwrap();
156 assert_eq!(amount.value().abs(), abs.value());
157 let formula = format!("=ABS(E{})", tix + 2);
158 let cix = tiller_data
159 .transactions
160 .mapping()
161 ._header_index("Custom Column")
162 .unwrap();
163 let formula_cell = RowCol::new(tix, cix);
164 let found_formula = tiller_data
165 .transactions
166 .formulas()
167 .get(&formula_cell)
168 .unwrap()
169 .to_owned();
170 assert_eq!(formula, found_formula);
171 }
172
173 let tiller_data_serialized = serde_json::to_string_pretty(&tiller_data).unwrap();
175 let tiller_data_deserialized: TillerData =
176 serde_json::from_str(&tiller_data_serialized).unwrap();
177 let tiller_data_serialized_again =
178 serde_json::to_string_pretty(&tiller_data_deserialized).unwrap();
179 assert_eq!(tiller_data, tiller_data_deserialized);
180 assert_eq!(tiller_data_serialized, tiller_data_serialized_again)
181}