Skip to main content

classy_sync/data_stores/
sync_requests.rs

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    /// column names are not sanitized by default so it is recommended to use the `verify_columns` method
51    /// when using column names in sql expressions
52    pub pk_fields: HashMap<String, Value>,
53    /// column names are not sanitized by default so it is recommended to use the `verify_columns` method
54    /// when using column names in sql expressions
55    pub relevant_fields: Option<HashMap<String, Value>>,
56}
57
58impl ClassDataSync {
59    /// This funciton should be used to verify columns in case of sql injection
60    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// TERM SYNCS - for getting information about specfic terms from classy
114#[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    // all of these setter methods are pretty picky so maybe just make they less so
149
150    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// ALL SYNCS - for getting all information from class
211
212#[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}