Skip to main content

server/
commodity.rs

1pub mod user {
2    use crate::db::DBError;
3    use crate::error::ServerError;
4    use crate::user::User;
5    #[cfg(feature = "scripting")]
6    use finance::commodity::CommodityBuilder;
7    use finance::{commodity::Commodity, tag::Tag};
8    #[cfg(feature = "scripting")]
9    use scripting::commodity::apply_commodity_hook;
10    use sqlx::types::Uuid;
11    #[cfg(feature = "scripting")]
12    use std::collections::HashMap;
13    #[cfg(feature = "scripting")]
14    use std::sync::{Arc, Mutex};
15
16    impl User {
17        #[cfg(feature = "scripting")]
18        pub async fn add_commodity(&self, script: &[u8]) -> Result<Commodity, ServerError> {
19            let c = CommodityBuilder::new().id(Uuid::new_v4()).build()?;
20
21            let mut tags = self.get_commodity_tags(&c).await?;
22            let tagdb: Arc<Mutex<HashMap<String, String>>> = Arc::new(Mutex::new(HashMap::new()));
23            {
24                let mut tagdb_lock = tagdb.lock().map_err(|err| {
25                    log::error!("{}", t!("Mutex error: %{err}", err = err : {:?}));
26                    ServerError::Lock
27                })?;
28                for t in &tags {
29                    tagdb_lock.insert(t.tag_name.clone(), t.tag_value.clone());
30                }
31            }
32
33            let commodity = if script.is_empty() {
34                c
35            } else {
36                apply_commodity_hook(c, tagdb.clone(), script).await?
37            };
38
39            // Update the tags with the new values from `tagdb`
40            {
41                let tagdb_lock = tagdb.lock().map_err(|err| {
42                    log::error!("{}", t!("Mutex error: %{err}", err = err : {:?}));
43                    ServerError::Lock
44                })?;
45                for t in &mut tags {
46                    if let Some(value) = tagdb_lock.get(&t.tag_name) {
47                        t.tag_value = value.to_owned();
48                    }
49                }
50            }
51
52            // Call `update_commodity_tags` to apply the updates to the database
53            self.update_commodity_tags(&commodity, &tags).await?;
54
55            Ok(commodity)
56        }
57
58        pub async fn create_commodity(
59            &self,
60            symbol: String,
61            name: String,
62        ) -> Result<Commodity, ServerError> {
63            let c = Commodity { id: Uuid::new_v4() };
64
65            let mut conn = self.get_connection().await.map_err(|err| {
66                log::error!("{}", t!("Database error: %{err}", err = err : {:?}));
67                ServerError::DB(err)
68            })?;
69
70            c.commit(&mut *conn).await?;
71            let tags: Vec<Tag> = vec![
72                Tag {
73                    id: Uuid::new_v4(),
74                    tag_name: "symbol".to_string(),
75                    tag_value: symbol,
76                    description: None,
77                },
78                Tag {
79                    id: Uuid::new_v4(),
80                    tag_name: "name".to_string(),
81                    tag_value: name,
82                    description: None,
83                },
84            ];
85            self.update_commodity_tags(&c, &tags).await?;
86
87            Ok(c)
88        }
89
90        pub async fn update_commodity_tags(
91            &self,
92            c: &Commodity,
93            tags: &[Tag],
94        ) -> Result<(), ServerError> {
95            for tag in tags {
96                self.set_commodity_tag(c, tag).await?;
97            }
98            Ok(())
99        }
100
101        pub async fn set_commodity_tag(&self, c: &Commodity, t: &Tag) -> Result<(), ServerError> {
102            if t.tag_name.trim().is_empty() || t.tag_value.trim().is_empty() {
103                return Err(ServerError::Creation);
104            }
105            let mut conn = self.get_connection().await.map_err(|err| {
106                log::error!("{}", t!("Database error: %{err}", err = err : {:?}));
107                ServerError::DB(err)
108            })?;
109
110            sqlx::query_file!(
111                "sql/set/commodities/tag.sql",
112                &c.id,
113                &t.tag_name,
114                &t.tag_value,
115                t.description
116            )
117            .execute(&mut *conn)
118            .await
119            .map_err(|err| {
120                log::error!("{}", t!("Database error: %{err}", err = err : {:?}));
121                ServerError::DB(DBError::Sqlx(err))
122            })?;
123
124            Ok(())
125        }
126
127        pub async fn get_commodity_tags(&self, c: &Commodity) -> Result<Vec<Tag>, ServerError> {
128            let mut conn = self.get_connection().await.map_err(|err| {
129                log::error!("{}", t!("Database error: %{err}", err = err : {:?}));
130                ServerError::DB(err)
131            })?;
132
133            let tags = sqlx::query_file_as!(Tag, "sql/select/commodities/tags.sql", &c.id)
134                .fetch_all(&mut *conn)
135                .await
136                .map_err(|err| {
137                    log::error!("Database error: {err:?}");
138                    ServerError::DB(DBError::Sqlx(err))
139                })?;
140
141            Ok(tags)
142        }
143
144        pub async fn get_commodity_tag(
145            &self,
146            c: &Commodity,
147            tag: &String,
148        ) -> Result<Tag, ServerError> {
149            let mut conn = self.get_connection().await.map_err(|err| {
150                log::error!("{}", t!("Database error: %{err}", err = err : {:?}));
151                ServerError::DB(err)
152            })?;
153
154            let tag = sqlx::query_file_as!(Tag, "sql/select/commodities/tag.sql", &c.id, tag)
155                .fetch_one(&mut *conn)
156                .await
157                .map_err(|err| {
158                    log::error!("Database error: {err:?}");
159                    ServerError::DB(DBError::Sqlx(err))
160                })?;
161
162            Ok(tag)
163        }
164    }
165
166    #[cfg(test)]
167    mod commodity_tests {
168        use super::*;
169        use crate::db::DB_POOL;
170        #[cfg(feature = "testlog")]
171        use env_logger;
172        #[cfg(feature = "testlog")]
173        use log;
174        use sqlx::PgPool;
175        #[cfg(feature = "scripting")]
176        use std::collections::HashMap;
177        #[cfg(feature = "scripting")]
178        use std::fs;
179        #[cfg(feature = "scripting")]
180        use std::sync::{Arc, Mutex};
181        use supp_macro::local_db_sqlx_test;
182        use tokio::sync::OnceCell;
183
184        /// Context for keeping environment intact
185        static CONTEXT: OnceCell<()> = OnceCell::const_new();
186        static USER: OnceCell<User> = OnceCell::const_new();
187
188        async fn setup() {
189            CONTEXT
190                .get_or_init(|| async {
191                    #[cfg(feature = "testlog")]
192                    let _ = env_logger::builder()
193                        .is_test(true)
194                        .filter_level(log::LevelFilter::Trace)
195                        .try_init();
196                })
197                .await;
198
199            USER.get_or_init(|| async { User { id: Uuid::new_v4() } })
200                .await;
201        }
202
203        #[cfg(feature = "scripting")]
204        #[local_db_sqlx_test]
205        async fn test_commodity_creation(pool: PgPool) -> Result<(), anyhow::Error> {
206            USER.get()
207                .unwrap()
208                .commit()
209                .await
210                .expect("Failed to commit user to database");
211
212            let mut conn = pool.acquire().await.unwrap();
213
214            let commodity = Commodity { id: Uuid::new_v4() };
215            let user = USER.get().unwrap();
216
217            sqlx::query!("INSERT INTO commodities (id) VALUES ($1)", &commodity.id,)
218                .execute(&mut *conn)
219                .await
220                .unwrap();
221
222            let script = fs::read("../target/commodity.wasm")?;
223            let comm = user.add_commodity(&script).await?;
224            assert_eq!(comm.id, comm.id);
225        }
226
227        #[local_db_sqlx_test]
228        async fn test_create_commodity(pool: PgPool) -> Result<(), anyhow::Error> {
229            let user = USER.get().unwrap();
230            user.commit()
231                .await
232                .expect("Failed to commit user to database");
233
234            let c = user
235                .create_commodity("JPY".to_string(), "Japanese Yen".to_string())
236                .await?;
237            let mut conn = pool.acquire().await.unwrap();
238            let res = sqlx::query!("SELECT id FROM commodities WHERE id = $1", c.id)
239                .fetch_one(&mut *conn)
240                .await?;
241
242            assert_eq!(res.id, c.id);
243
244            let tag = user.get_commodity_tag(&c, &"symbol".to_string()).await?;
245
246            assert_eq!(tag.tag_value, "JPY");
247
248            let tag = user.get_commodity_tag(&c, &"name".to_string()).await?;
249            assert_eq!(tag.tag_value, "Japanese Yen");
250        }
251
252        #[local_db_sqlx_test]
253        async fn test_commodity_tag(pool: PgPool) {
254            let user = USER.get().unwrap();
255            user.commit()
256                .await
257                .expect("Failed to commit user to database");
258
259            let mut conn = user.get_connection().await?;
260            let commodity = Commodity { id: Uuid::new_v4() };
261            commodity.commit(&mut *conn).await?;
262            user.set_commodity_tag(
263                &commodity,
264                &Tag::builder()
265                    .id(Uuid::new_v4())
266                    .tag_name("test")
267                    .tag_value("testval")
268                    .build()?,
269            )
270            .await?;
271
272            let res = sqlx::query_file!("testdata/query_tag.sql", &commodity.id)
273                .fetch_one(&mut *conn)
274                .await?;
275            assert_eq!(res.tag_name_result, "test".to_string());
276            assert_eq!(res.tag_value_result, "testval".to_string());
277            user.set_commodity_tag(
278                &commodity,
279                &Tag::builder()
280                    .id(Uuid::new_v4())
281                    .tag_name("test")
282                    .tag_value("testval2")
283                    .build()?,
284            )
285            .await?;
286            let res = sqlx::query_file!("testdata/query_tag.sql", &commodity.id)
287                .fetch_one(&mut *conn)
288                .await?;
289            assert_eq!(res.tag_name_result, "test".to_string());
290            assert_eq!(res.tag_value_result, "testval2".to_string());
291        }
292
293        #[cfg(feature = "scripting")]
294        #[local_db_sqlx_test]
295        async fn test_get_commodity_tags(pool: PgPool) {
296            let user = USER.get().unwrap();
297            user.commit()
298                .await
299                .expect("Failed to commit user to database");
300
301            let commodity = Commodity { id: Uuid::new_v4() };
302            {
303                let mut conn = user.get_connection().await?;
304                commodity.commit(&mut *conn).await?;
305            }
306            user.set_commodity_tag(
307                &commodity,
308                &Tag::builder()
309                    .id(Uuid::new_v4())
310                    .tag_name("test")
311                    .tag_value("testval")
312                    .build()?,
313            )
314            .await?;
315            let tags = user.get_commodity_tags(&commodity).await?;
316            assert_eq!(tags.len(), 1);
317            assert_eq!(tags.first().unwrap().tag_name, "test".to_string());
318            user.set_commodity_tag(
319                &commodity,
320                &Tag::builder()
321                    .id(Uuid::new_v4())
322                    .tag_name("test2")
323                    .tag_value("testval2")
324                    .build()?,
325            )
326            .await?;
327
328            let tags = user.get_commodity_tags(&commodity).await?;
329            assert_eq!(tags.len(), 2);
330            assert_eq!(tags.last().unwrap().tag_name, "test2".to_string());
331
332            user.set_commodity_tag(
333                &commodity,
334                &Tag::builder()
335                    .id(Uuid::new_v4())
336                    .tag_name("newname")
337                    .tag_value("the new full name of the Yen")
338                    .build()?,
339            )
340            .await?;
341
342            let tags = user.get_commodity_tags(&commodity).await?;
343            let script = fs::read("../target/commodity.wasm")?;
344            let tagdb: Arc<Mutex<HashMap<String, String>>> = Arc::new(Mutex::new(HashMap::new()));
345            {
346                let mut tagdb_lock = tagdb
347                    .lock()
348                    .map_err(|e| anyhow::anyhow!("Mutex is poisoned: {e}"))?;
349                for t in &tags {
350                    tagdb_lock.insert(t.tag_name.clone(), t.tag_value.clone());
351                }
352            }
353            let comm = apply_commodity_hook(commodity, tagdb, &script).await?;
354            // assert_eq!(
355            //     comm.fullname,
356            //     Some("the new full name of the Yen".to_string())
357            // );
358
359            let mut newtags = user.get_commodity_tags(&comm).await?;
360            let name = newtags[0].tag_name.clone();
361            newtags[0].tag_value = "thenewval".to_string();
362            user.update_commodity_tags(&comm, &newtags).await?;
363            let updated_tags = user.get_commodity_tags(&comm).await?;
364
365            if let Some(tag) = updated_tags.iter().find(|t| t.tag_name == *name) {
366                assert_eq!(
367                    tag.tag_value, "thenewval",
368                    "The tag value was not updated correctly"
369                );
370            } else {
371                panic!("Tag with name '{name}' not found in updated tags");
372            }
373        }
374    }
375}