Skip to main content

dsc/api/
tags.rs

1use super::client::DiscourseClient;
2use super::error::http_error;
3use anyhow::{Context, Result};
4use serde::{Deserialize, Serialize};
5use serde_json::Value;
6
7#[derive(Debug, Deserialize, Serialize, Clone)]
8pub struct TagInfo {
9    pub id: String,
10    #[serde(default)]
11    pub text: String,
12    #[serde(default)]
13    pub count: u64,
14    #[serde(default)]
15    pub pm_count: u64,
16}
17
18#[derive(Debug, Deserialize)]
19struct TagsResponse {
20    #[serde(default)]
21    tags: Vec<TagInfo>,
22}
23
24impl DiscourseClient {
25    /// List every tag visible to the authenticated user.
26    pub fn list_tags(&self) -> Result<Vec<TagInfo>> {
27        let response = self.get("/tags.json")?;
28        let status = response.status();
29        let text = response.text().context("reading tags response body")?;
30        if !status.is_success() {
31            return Err(http_error("tags request", status, &text));
32        }
33        let body: TagsResponse =
34            serde_json::from_str(&text).context("parsing tags response json")?;
35        Ok(body.tags)
36    }
37
38    /// Fetch the current tag list for a topic.
39    pub fn fetch_topic_tags(&self, topic_id: u64) -> Result<Vec<String>> {
40        let path = format!("/t/{}.json", topic_id);
41        let response = self.get(&path)?;
42        let status = response.status();
43        let text = response.text().context("reading topic response body")?;
44        if !status.is_success() {
45            return Err(http_error("topic request", status, &text));
46        }
47        let value: Value = serde_json::from_str(&text).context("parsing topic response json")?;
48        let tags = value
49            .get("tags")
50            .and_then(|v| v.as_array())
51            .map(|arr| {
52                arr.iter()
53                    .filter_map(|v| v.as_str().map(|s| s.to_string()))
54                    .collect()
55            })
56            .unwrap_or_default();
57        Ok(tags)
58    }
59
60    /// Replace the full tag list on a topic. Returns the resulting tag set.
61    pub fn set_topic_tags(&self, topic_id: u64, tags: &[String]) -> Result<Vec<String>> {
62        let path = format!("/t/{}.json", topic_id);
63        let payload: Vec<(&str, &str)> = if tags.is_empty() {
64            // Discourse needs at least one form field to clear tags;
65            // sending an empty `tags[]` removes them all.
66            vec![("tags[]", "")]
67        } else {
68            tags.iter().map(|t| ("tags[]", t.as_str())).collect()
69        };
70        let response = self.send_retrying(|| Ok(self.put(&path)?.form(&payload)))?;
71        let status = response.status();
72        let text = response.text().context("reading set-tags response body")?;
73        if !status.is_success() {
74            return Err(http_error("set tags request", status, &text));
75        }
76        // Confirm the post-update state by re-reading the topic; the PUT response
77        // shape is awkward to depend on across versions.
78        self.fetch_topic_tags(topic_id)
79    }
80}