1use crate::errors::{Error, SyncError};
2use regex::Regex;
3use serde::{Deserialize, Serialize};
4use serde_json::Value;
5use std::collections::HashMap;
6use strum_macros::Display;
7
8const DEFUALT_MAX_RECORDS: u16 = 10_000;
9
10#[derive(Serialize, Deserialize, Debug)]
11#[serde(rename_all = "snake_case")]
12pub enum SyncAction {
13 Update,
14 Delete,
15 Insert,
16}
17
18#[derive(Serialize, Display, Debug, Deserialize)]
19#[serde(rename_all = "snake_case")]
20pub enum TableName {
21 #[strum(serialize = "meeting_times")]
22 MeetingTimes,
23 #[strum(serialize = "sections")]
24 Sections,
25 #[strum(serialize = "professors")]
26 Professors,
27 #[strum(serialize = "courses")]
28 Courses,
29 #[strum(serialize = "term_collections")]
30 TermCollections,
31 #[strum(serialize = "schools")]
32 Schools,
33}
34
35#[derive(Serialize, Display, Debug, Deserialize)]
36#[serde(rename_all = "snake_case")]
37pub enum CommonTable {
38 #[strum(serialize = "professors")]
39 Professors,
40 #[strum(serialize = "courses")]
41 Courses,
42 #[strum(serialize = "term_collections")]
43 TermCollections,
44}
45
46#[derive(Serialize, Debug, Deserialize)]
47pub struct ClassDataSync {
48 pub table_name: TableName,
49 pub sync_action: SyncAction,
50 pub pk_fields: HashMap<String, Value>,
53 pub relevant_fields: Option<HashMap<String, Value>>,
56}
57
58impl ClassDataSync {
59 pub fn verify_columns(&self) -> Result<(), Error> {
61 let is_column = Regex::new(r"\b[a-zA-Z_]\b").unwrap();
62 let invalid_cols: Vec<_> = self
63 .relevant_fields
64 .as_ref()
65 .unwrap_or(&HashMap::new())
66 .iter()
67 .filter_map(|(col, _)| {
68 if is_column.is_match(col) {
69 Some(col.to_string())
70 } else {
71 None
72 }
73 })
74 .collect();
75
76 if !invalid_cols.is_empty() {
77 return Err(SyncError::new(format!(
78 "`{:?}` There is are invalid column(s) in relevant fields: {}",
79 self.relevant_fields,
80 invalid_cols.join(", ")
81 )));
82 }
83
84 let invalid_cols: Vec<_> = self
85 .pk_fields
86 .iter()
87 .filter_map(|(col, _)| {
88 if is_column.is_match(col) {
89 Some(col.to_string())
90 } else {
91 None
92 }
93 })
94 .collect();
95 if !invalid_cols.is_empty() {
96 return Err(SyncError::new(format!(
97 "`{:?}` There is an invalid column in pk fields: {}",
98 self.pk_fields,
99 invalid_cols.join(", ")
100 )));
101 }
102
103 Ok(())
104 }
105}
106
107#[derive(Debug)]
108pub enum SyncOptions {
109 All(AllSync),
110 Select(SelectSync),
111}
112
113#[derive(Debug, Serialize, Deserialize)]
115#[serde(untagged)]
116pub enum SchoolEntry {
117 TermToSequence(HashMap<String, u64>),
118 Sequence(u64),
119}
120
121#[derive(Debug, Serialize, Deserialize, Default)]
122pub struct SelectSync {
123 exclude: HashMap<String, HashMap<String, u64>>,
124 max_records_per_request: Option<u16>,
125 schools: HashMap<String, SchoolEntry>,
126}
127
128impl SelectSync {
129 pub fn new() -> SelectSync {
130 SelectSync {
131 max_records_per_request: Some(DEFUALT_MAX_RECORDS),
132 ..Default::default()
133 }
134 }
135
136 pub fn get_exclusions(&self) -> &HashMap<String, HashMap<String, u64>> {
137 &self.exclude
138 }
139
140 pub fn get_max_records(&self) -> Option<u16> {
141 self.max_records_per_request
142 }
143
144 pub fn get_schools(&self) -> &HashMap<String, SchoolEntry> {
145 &self.schools
146 }
147
148 pub fn add_school_sync(&mut self, school_id: String, synced_at: u64) -> Result<(), Error> {
151 if self.schools.contains_key(&school_id) {
152 return Err(SyncError::new(format!(
153 "school_id `{school_id}` is already set"
154 )));
155 }
156 self.schools
157 .insert(school_id, SchoolEntry::Sequence(synced_at));
158 Ok(())
159 }
160
161 pub fn add_term_sync(
162 &mut self,
163 school_id: String,
164 term_collection_id: String,
165 synced_at: u64,
166 ) -> Result<(), Error> {
167 let school_entry = self
168 .schools
169 .entry(school_id)
170 .or_insert(SchoolEntry::TermToSequence(HashMap::new()));
171 match school_entry {
172 SchoolEntry::TermToSequence(terms) => {
173 if let Some(old_sync) = terms.insert(term_collection_id, synced_at) {
174 return Err(SyncError::new(format!(
175 "This term already was set to sync with {old_sync}"
176 )));
177 }
178 }
179 SchoolEntry::Sequence(sequence) => {
180 return Err(SyncError::new(format!(
181 "school id already being synced with {sequence}",
182 )));
183 }
184 };
185 Ok(())
186 }
187
188 pub fn add_exclusion(
189 &mut self,
190 school_id: String,
191 term_collection_id: String,
192 synced_at: u64,
193 ) -> Result<(), Error> {
194 let terms = self.exclude.entry(school_id).or_default();
195 if let Some(old_sync) = terms.insert(term_collection_id, synced_at) {
196 return Err(SyncError::new(format!(
197 "This term already was set as an exclusion with {old_sync}"
198 )));
199 }
200 Ok(())
201 }
202}
203
204#[derive(Debug, Serialize, Deserialize)]
205pub struct TermSyncResult {
206 pub new_sync_term_sequences: HashMap<String, SchoolEntry>,
207 pub sync_data: Vec<ClassDataSync>,
208}
209
210#[derive(Debug, Serialize, Deserialize)]
213pub struct AllSync {
214 pub last_sync: u64,
215 pub max_records_count: Option<u16>,
216}
217
218#[derive(Debug, Serialize, Deserialize)]
219pub struct AllSyncResult {
220 pub new_latest_sync: u64,
221 pub sync_data: Vec<ClassDataSync>,
222}