Skip to main content

aven_core/
labels.rs

1use crate::ids::WorkspaceId;
2use anyhow::{Result, bail};
3use sqlx::{Row, SqliteConnection};
4
5use crate::db::Database;
6use crate::matching::is_near;
7use crate::operations::LabelOutcome;
8use crate::projects::normalize_key;
9use crate::workspaces::Workspace;
10
11#[derive(Debug, Clone, PartialEq, Eq)]
12pub struct LabelUsage {
13    pub name: String,
14    pub task_count: usize,
15    pub series_count: usize,
16}
17
18#[derive(Debug, Clone)]
19pub(crate) struct CreatedLabel {
20    pub(crate) name: String,
21    pub(crate) change_id: String,
22}
23
24#[derive(Debug, Default)]
25pub(crate) struct LabelResolution {
26    pub(crate) names: Vec<String>,
27    pub(crate) created: Vec<CreatedLabel>,
28}
29
30impl Database {
31    pub async fn label_usage(&self, workspace_id: &WorkspaceId) -> Result<Vec<LabelUsage>> {
32        let mut conn = self.acquire().await?;
33        let rows = sqlx::query(
34            "SELECT l.name,
35                    (SELECT count(*) FROM task_labels tl
36                     WHERE tl.workspace_id = l.workspace_id AND tl.label = l.name) AS task_count,
37                    (SELECT count(*) FROM recurrence_series_labels sl
38                     WHERE sl.workspace_id = l.workspace_id AND sl.label = l.name) AS series_count
39             FROM labels l
40             WHERE l.workspace_id = ?
41             ORDER BY l.name",
42        )
43        .bind(workspace_id)
44        .fetch_all(&mut *conn)
45        .await?;
46        Ok(rows
47            .into_iter()
48            .map(|row| LabelUsage {
49                name: row.get("name"),
50                task_count: usize::try_from(row.get::<i64, _>("task_count")).unwrap_or(usize::MAX),
51                series_count: usize::try_from(row.get::<i64, _>("series_count"))
52                    .unwrap_or(usize::MAX),
53            })
54            .collect())
55    }
56
57    pub async fn list_labels(
58        &self,
59        workspace_id: &WorkspaceId,
60        search: Option<&str>,
61    ) -> Result<Vec<String>> {
62        let mut conn = self.acquire().await?;
63        list_labels_in_workspace(&mut conn, workspace_id, search).await
64    }
65
66    pub async fn resolve_labels(
67        &self,
68        workspace_id: &WorkspaceId,
69        labels: &[String],
70    ) -> Result<Vec<String>> {
71        let mut conn = self.acquire().await?;
72        resolve_labels_in_workspace(&mut conn, workspace_id, labels).await
73    }
74}
75
76pub fn normalize_label(input: &str) -> String {
77    normalize_key(input)
78}
79
80async fn near_labels(
81    conn: &mut SqliteConnection,
82    workspace_id: &WorkspaceId,
83    input: &str,
84) -> Result<Vec<String>> {
85    let needle = normalize_label(input);
86    Ok(list_labels_in_workspace(conn, workspace_id, None)
87        .await?
88        .into_iter()
89        .filter(|label| is_near(&needle, label))
90        .collect())
91}
92
93pub(crate) async fn list_labels_in_workspace(
94    conn: &mut SqliteConnection,
95    workspace_id: &WorkspaceId,
96    search: Option<&str>,
97) -> Result<Vec<String>> {
98    let search = search.map(normalize_label);
99    let labels = sqlx::query_scalar::<_, String>(
100        "SELECT name FROM labels WHERE workspace_id = ? ORDER BY name",
101    )
102    .bind(workspace_id)
103    .fetch_all(&mut *conn)
104    .await?;
105    Ok(labels
106        .into_iter()
107        .filter(|label| {
108            search
109                .as_deref()
110                .is_none_or(|search| label.contains(search))
111        })
112        .collect())
113}
114
115pub(crate) async fn ensure_label_exists_in_workspace(
116    conn: &mut SqliteConnection,
117    workspace_id: &WorkspaceId,
118    label: &str,
119) -> Result<String> {
120    let label = normalize_label(label);
121    if sqlx::query_scalar::<_, i64>(
122        "SELECT count(*) FROM labels WHERE workspace_id = ? AND name = ?",
123    )
124    .bind(workspace_id)
125    .bind(&label)
126    .fetch_one(&mut *conn)
127    .await?
128        > 0
129    {
130        Ok(label)
131    } else {
132        let choices = near_labels(conn, workspace_id, &label).await?;
133        eprintln!("error unknown-label input={}", label);
134        for choice in choices {
135            eprintln!("choice {choice}");
136        }
137        eprintln!("hint \"create the label explicitly\"");
138        bail!("unknown label");
139    }
140}
141
142pub(crate) async fn resolve_labels_in_workspace(
143    conn: &mut SqliteConnection,
144    workspace_id: &WorkspaceId,
145    labels: &[String],
146) -> Result<Vec<String>> {
147    let mut resolved = Vec::with_capacity(labels.len());
148    for label in labels {
149        resolved.push(ensure_label_exists_in_workspace(conn, workspace_id, label).await?);
150    }
151    Ok(resolved)
152}
153
154pub(crate) async fn resolve_or_create_labels_in_workspace(
155    conn: &mut SqliteConnection,
156    workspace: &Workspace,
157    labels: &[String],
158) -> Result<LabelResolution> {
159    let mut resolution = LabelResolution {
160        names: Vec::with_capacity(labels.len()),
161        created: Vec::new(),
162    };
163    for label in labels {
164        let outcome: LabelOutcome =
165            crate::operations::create_label_operation(conn, workspace, label).await?;
166        if outcome.created {
167            let change_id = outcome
168                .change_id
169                .ok_or_else(|| anyhow::anyhow!("created label missing change id"))?;
170            resolution.created.push(CreatedLabel {
171                name: outcome.name.clone(),
172                change_id,
173            });
174        }
175        resolution.names.push(outcome.name);
176    }
177    Ok(resolution)
178}