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 {
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 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 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 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}