Skip to main content

finance/
tag.rs

1use crate::error::{FinanceError, TagError};
2use sqlx::{Connection, types::Uuid};
3use supp_macro::Builder;
4
5#[derive(Debug, sqlx::FromRow, Builder)]
6#[builder(error_kind = "TagError")]
7pub struct Tag {
8    pub id: Uuid,
9    pub tag_name: String,
10    pub tag_value: String,
11    pub description: Option<String>,
12}
13
14impl Tag {
15    /// Upsert this tag and return the canonical row id.
16    ///
17    /// Tag identity is `(tag_name, tag_value)`. When a row with the same pair
18    /// already exists, its id is returned and the description is refreshed
19    /// only if a non-NULL one was supplied. The returned id may differ from
20    /// `self.id`; callers linking the tag to an entity must use the returned
21    /// canonical id.
22    pub async fn commit<E>(&self, conn: &mut E) -> Result<Uuid, FinanceError>
23    where
24        E: Connection<Database = sqlx::Postgres>,
25    {
26        let mut tr = conn.begin().await?;
27
28        let row = sqlx::query_file!(
29            "sql/tag_insert.sql",
30            &self.id,
31            &self.tag_name,
32            &self.tag_value,
33            self.description
34        )
35        .fetch_one(&mut *tr)
36        .await?;
37        tr.commit().await?;
38
39        Ok(row.id)
40    }
41}
42
43#[cfg(test)]
44mod tag_tests {
45    use super::*;
46    #[cfg(feature = "testlog")]
47    use env_logger;
48    #[cfg(feature = "testlog")]
49    use log;
50    use sqlx::PgPool;
51    use tokio::sync::OnceCell;
52
53    /// Context for keeping environment intact
54    static CONTEXT: OnceCell<()> = OnceCell::const_new();
55
56    async fn setup() {
57        CONTEXT
58            .get_or_init(|| async {
59                #[cfg(feature = "testlog")]
60                let _ = env_logger::builder()
61                    .is_test(true)
62                    .filter_level(log::LevelFilter::Trace)
63                    .try_init();
64            })
65            .await;
66    }
67
68    #[sqlx::test(migrations = "../migrations")]
69    async fn test_tag_store(pool: PgPool) -> anyhow::Result<()> {
70        setup().await;
71        let mut conn = pool.acquire().await?;
72
73        let tag = Tag {
74            id: Uuid::new_v4(),
75            tag_name: "Category".to_string(),
76            tag_value: "test".to_string(),
77            description: None,
78        };
79
80        sqlx::query!(
81            "INSERT INTO tags (id, tag_name, tag_value, description) \
82		      VALUES ($1, $2, $3, $4)",
83            &tag.id,
84            &tag.tag_name,
85            &tag.tag_value,
86            tag.description
87        )
88        .execute(&mut *conn)
89        .await?;
90
91        let result = sqlx::query!("SELECT id, tag_value FROM tags WHERE tag_name = 'Category'")
92            .fetch_one(&mut *conn)
93            .await?;
94
95        assert_eq!(tag.id, result.id);
96        assert_eq!(tag.tag_value, "test".to_string());
97
98        let tag2 = Tag {
99            id: Uuid::new_v4(),
100            tag_name: "Cat2".to_string(),
101            ..tag
102        };
103        let mut conn = pool.acquire().await?;
104        tag2.commit(&mut *conn).await?;
105        let mut conn = pool.acquire().await?;
106        let result = sqlx::query!("SELECT id, tag_value FROM tags WHERE tag_name = 'Cat2'")
107            .fetch_one(&mut *conn)
108            .await?;
109
110        assert_eq!(tag2.id, result.id);
111        assert_eq!(tag2.tag_value, "test".to_string());
112
113        Ok(())
114    }
115
116    #[tokio::test]
117    async fn test_tag_builder() -> anyhow::Result<()> {
118        setup().await;
119        let build = Tag::builder().id(Uuid::new_v4()).build();
120        assert!(build.is_err());
121        let build = Tag::builder()
122            .id(Uuid::new_v4())
123            .tag_name("name")
124            .tag_value("type")
125            .build();
126
127        assert!(build.is_ok());
128        Ok(())
129    }
130}