Skip to main content

farmap/
user_collection.rs

1use crate::fetch::DataReadError;
2use crate::has_tag::HasTag;
3use crate::user::InvalidInputError;
4use crate::user::User;
5use crate::user::UserError;
6use serde::Deserialize;
7use serde::Serialize;
8use std::collections::hash_map::Entry::Vacant;
9use std::collections::HashMap;
10use std::error::Error;
11use std::fs::File;
12use std::io::Read;
13use std::io::Write;
14use std::path::Path;
15use thiserror::Error;
16
17#[derive(Default, Debug, PartialEq, Serialize, Deserialize)]
18pub struct UserCollection {
19    map: HashMap<usize, User>,
20}
21
22pub type CreateResult = Result<(UserCollection, Vec<DataCreationError>), DataCreationError>;
23
24impl UserCollection {
25    /// Returns a vec of all the collision errors, if there are any.
26    pub fn add_user_value_iter(
27        &mut self,
28        values: impl IntoIterator<Item = impl HasTag<u64>>,
29    ) -> Option<Vec<CollectionError>> {
30        let mut errors: Option<Vec<CollectionError>> = None;
31        for value in values {
32            if let Some(user) = self.user_mut(value.tag() as usize) {
33                let user_add_result = user
34                    .add_user_value(value.untag().0)
35                    .map_err(|_| CollectionError::UserValueCollisionError);
36                if let Err(add_result) = user_add_result {
37                    if let Some(errors) = &mut errors {
38                        errors.push(add_result);
39                    } else {
40                        errors = Some(vec![add_result])
41                    }
42                }
43            } else {
44                let mut user = User::new_without_labels(value.tag() as usize);
45                user.add_user_value(value.untag().0)
46                    .expect("new user cannot collide");
47                self.add_user(user).expect("new user cannot collide");
48            }
49        }
50        errors
51    }
52
53    pub fn user_mut(&mut self, fid: usize) -> Option<&mut User> {
54        self.map.get_mut(&fid)
55    }
56
57    #[allow(unused)]
58    pub(crate) fn user_mut_unchecked(&mut self, fid: usize) -> &mut User {
59        self.map
60            .get_mut(&fid)
61            .expect("fid {fid} should exist in collection")
62    }
63
64    pub fn user(&self, fid: usize) -> Option<&User> {
65        self.map.get(&fid)
66    }
67
68    pub fn user_count(&self) -> usize {
69        self.map.len()
70    }
71
72    // the problem with this is that when the file does not exist the program will fail because
73    // there isn't really a way for the caller to anticipate this...
74    pub fn create_from_db(db: &Path) -> Result<Self, DbReadError> {
75        let collection = serde_json::from_str(&std::fs::read_to_string(db)?)?;
76
77        Ok(collection)
78    }
79
80    pub fn create_from_file(file: &mut std::fs::File) -> Result<Self, DbReadError> {
81        let mut result = String::new();
82        file.read_to_string(&mut result)?;
83        Ok(serde_json::from_str(&result)?)
84    }
85
86    pub fn save_to_db(&self, db: &Path) -> Result<(), Box<dyn Error>> {
87        let mut file = File::create(db)?;
88        let json_text = serde_json::to_string(self)?;
89        file.write_all(json_text.as_bytes())?;
90        Ok(())
91    }
92
93    /// Applies a filter to the user data. Use with caution since the data is removed from the
94    /// struct. For most situations it is preferred to create a subset of the data.
95    pub fn apply_filter<F>(&mut self, filter: F)
96    where
97        F: Fn(&User) -> bool,
98    {
99        let old_map = std::mem::take(&mut self.map);
100        let new_map = old_map
101            .into_values()
102            .filter(|user| filter(user))
103            .map(|user| (user.fid(), user))
104            .collect::<HashMap<usize, User>>();
105        self.map = new_map;
106    }
107
108    pub fn iter(&self) -> impl Iterator<Item = &User> {
109        self.map.values()
110    }
111
112    pub fn data(&self) -> &HashMap<usize, User> {
113        &self.map
114    }
115
116    pub fn add_user(&mut self, user: User) -> Result<(), CollectionError> {
117        let fid = user.fid();
118        if let Vacant(v) = self.map.entry(fid) {
119            v.insert(user);
120            Ok(())
121        } else {
122            Err(CollectionError::DuplicateUserError)
123        }
124    }
125}
126
127#[derive(Error, Debug, PartialEq, Clone, Hash)]
128#[non_exhaustive]
129pub enum CollectionError {
130    #[error("Tried to add colliding value")]
131    UserValueCollisionError,
132
133    #[error("user already exists in collection")]
134    DuplicateUserError,
135}
136
137#[derive(Error, Debug, PartialEq)]
138pub enum DataCreationError {
139    #[error("Input data is invalid.")]
140    InvalidInputError(#[from] InvalidInputError),
141
142    #[error("UserError")]
143    UserError(#[from] UserError),
144
145    #[error("Input is not readable or accessible")]
146    DataReadError(#[from] DataReadError),
147
148    #[error("DuplicateUserError")]
149    DuplicateUserError,
150}
151
152#[derive(Error, Debug)]
153pub enum DbReadError {
154    #[error("fs error")]
155    FSError(#[from] std::io::Error),
156
157    #[error("json error")]
158    JSONError(#[from] serde_json::Error),
159}
160
161#[cfg(test)]
162pub mod tests {
163    use super::*;
164    use crate::user_with_spam_data::tests::create_user_with_m_spam_scores;
165    use crate::UserValue;
166    use chrono::NaiveDate;
167    use std::path::PathBuf;
168
169    #[test]
170    pub fn test_user_count_on_dir_with_new() {
171        assert_eq!(dummy_data().user_count(), 2);
172    }
173
174    impl<T> HasTag<u64> for TestUserValue<T>
175    where
176        T: UserValue,
177    {
178        fn tag(&self) -> u64 {
179            self.fid
180        }
181
182        #[allow(refining_impl_trait)]
183        fn untag(self) -> (T, u64) {
184            (self.value, self.fid)
185        }
186    }
187
188    struct TestUserValue<T: UserValue> {
189        pub value: T,
190        pub fid: u64,
191    }
192
193    #[track_caller]
194    pub fn collection_from_fidded<T: HasTag<u64>>(
195        values: impl IntoIterator<Item = T>,
196    ) -> UserCollection {
197        let mut collection = UserCollection::default();
198        collection.add_user_value_iter(values);
199        collection
200    }
201
202    pub fn new_collection_from_user_value_iter<T>(
203        values: impl IntoIterator<Item = T>,
204    ) -> UserCollection
205    where
206        T: UserValue,
207    {
208        let mut collection = UserCollection::default();
209        let res = collection.add_user_value_iter(values.into_iter().enumerate().map(|(n, x)| {
210            TestUserValue {
211                value: x,
212                fid: n as u64 + 1,
213            }
214        }));
215        assert!(res.is_none()); // There is one entry per fid so there should be no collisions
216        collection
217    }
218
219    pub fn empty_collection() -> UserCollection {
220        UserCollection::default()
221    }
222
223    pub fn basic_single_user_test_collection_with_n_spam_updates(n: u64) -> UserCollection {
224        let date = NaiveDate::parse_from_str("2020-1-1", "%Y-%m-%d").unwrap();
225        let user = create_user_with_m_spam_scores(1, n, date);
226
227        let mut user_collection = UserCollection::default();
228        user_collection
229            .add_user(user)
230            .expect("only one user in collection - cannot collide");
231        user_collection
232    }
233
234    pub fn dummy_data() -> UserCollection {
235        let db_path = PathBuf::from("data/dummy-data_db.json");
236        UserCollection::create_from_db(&db_path).unwrap()
237    }
238
239    mod add_user {
240
241        use super::*;
242
243        #[track_caller]
244        pub fn check_add_user(collection: &mut UserCollection, user: User) {
245            collection.add_user(user).unwrap()
246        }
247    }
248
249    mod user_count {
250        use super::add_user::check_add_user;
251        use crate::user::tests::create_user;
252
253        use super::*;
254
255        #[track_caller]
256        fn check_user_count(collection: &UserCollection, n: usize) {
257            assert_eq!(collection.user_count(), n)
258        }
259
260        #[test]
261        fn test_empty_user_count() {
262            let collection = empty_collection();
263            check_user_count(&collection, 0);
264        }
265
266        #[test]
267        fn test_non_empty_user_count() {
268            let mut collection = empty_collection();
269            check_add_user(&mut collection, create_user(1));
270            check_add_user(&mut collection, create_user(2));
271            check_user_count(&collection, 2);
272        }
273    }
274}