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: u64,
10 #[serde(default)]
11 pub text: String,
12 #[serde(default)]
13 pub count: u64,
14 #[serde(default)]
15 pub pm_count: u64,
16 #[serde(default)]
17 pub description: Option<String>,
18}
19
20#[derive(Debug, Deserialize)]
21struct TagsResponse {
22 #[serde(default)]
23 tags: Vec<TagInfo>,
24}
25
26#[derive(Debug, Deserialize, Serialize, Clone)]
28pub struct TagGroupInfo {
29 pub id: u64,
30 pub name: String,
31 #[serde(default)]
32 pub tag_names: Vec<String>,
33 #[serde(default)]
34 pub one_per_topic: bool,
35 #[serde(default)]
36 pub parent_tag_name: Option<String>,
37 #[serde(default)]
38 pub permissions: Option<Value>,
39}
40
41#[derive(Debug, Deserialize)]
42struct TagGroupsResponse {
43 #[serde(default)]
44 tag_groups: Vec<TagGroupInfo>,
45}
46
47impl DiscourseClient {
48 pub fn list_tags(&self) -> Result<Vec<TagInfo>> {
50 let response = self.get("/tags.json")?;
51 let status = response.status();
52 let text = response.text().context("reading tags response body")?;
53 if !status.is_success() {
54 return Err(http_error("tags request", status, &text));
55 }
56 let body: TagsResponse =
57 serde_json::from_str(&text).context("parsing tags response json")?;
58 Ok(body.tags)
59 }
60
61 pub fn get_tag_description(&self, tag_name: &str) -> Result<Option<String>> {
63 let path = format!("/tag/{}.json", tag_name);
64 let response = self.get(&path)?;
65 let status = response.status();
66 let text = response.text().context("reading tag detail response")?;
67 if !status.is_success() {
68 return Ok(None);
69 }
70 let value: Value = serde_json::from_str(&text).unwrap_or_default();
71 let desc = value
72 .pointer("/topic_list/tags/0/description")
73 .or_else(|| value.pointer("/tag/description"))
74 .and_then(|v| v.as_str())
75 .filter(|s| !s.is_empty())
76 .map(|s| s.to_string());
77 Ok(desc)
78 }
79
80 pub fn list_tag_groups(&self) -> Result<Option<Vec<TagGroupInfo>>> {
83 let response = self.get("/tag_groups.json")?;
84 let status = response.status();
85 if status.as_u16() == 403 {
86 return Ok(None);
87 }
88 let text = response.text().context("reading tag groups response body")?;
89 if !status.is_success() {
90 return Err(http_error("tag groups request", status, &text));
91 }
92 let body: TagGroupsResponse =
93 serde_json::from_str(&text).context("parsing tag groups response json")?;
94 Ok(Some(body.tag_groups))
95 }
96
97 pub fn create_tag_group(&self, payload: &Value) -> Result<u64> {
99 let response = self.send_retrying(|| {
100 Ok(self.post("/tag_groups.json")?.json(payload))
101 })?;
102 let status = response.status();
103 let text = response.text().context("reading create tag group response")?;
104 if !status.is_success() {
105 return Err(http_error("create tag group", status, &text));
106 }
107 let value: Value = serde_json::from_str(&text)
108 .context("parsing create tag group response")?;
109 let id = value
110 .pointer("/tag_group/id")
111 .and_then(|v| v.as_u64())
112 .ok_or_else(|| anyhow::anyhow!("tag group creation response missing id"))?;
113 Ok(id)
114 }
115
116 pub fn update_tag_group(&self, group_id: u64, payload: &Value) -> Result<()> {
118 let path = format!("/tag_groups/{}.json", group_id);
119 let response = self.send_retrying(|| {
120 Ok(self.put(&path)?.json(payload))
121 })?;
122 let status = response.status();
123 let text = response.text().context("reading update tag group response")?;
124 if !status.is_success() {
125 return Err(http_error("update tag group", status, &text));
126 }
127 Ok(())
128 }
129
130 pub fn delete_tag_group(&self, group_id: u64) -> Result<()> {
132 let path = format!("/tag_groups/{}.json", group_id);
133 let response = self.delete(&path)?;
134 let status = response.status();
135 if !status.is_success() {
136 let text = response.text().unwrap_or_default();
137 return Err(http_error("delete tag group", status, &text));
138 }
139 Ok(())
140 }
141
142 pub fn update_tag(&self, tag_name: &str, description: Option<&str>) -> Result<()> {
144 let path = format!("/tag/{}.json", tag_name);
145 let mut payload = serde_json::Map::new();
146 if let Some(desc) = description {
147 payload.insert("tag".to_string(), serde_json::json!({"description": desc}));
148 }
149 let response = self.send_retrying(|| {
150 Ok(self.put(&path)?.json(&payload))
151 })?;
152 let status = response.status();
153 let text = response.text().context("reading update tag response")?;
154 if !status.is_success() {
155 return Err(http_error("update tag", status, &text));
156 }
157 Ok(())
158 }
159
160 pub fn rename_tag(&self, old_name: &str, new_name: &str) -> Result<()> {
164 let path = format!("/tag/{}.json", old_name);
165 let payload = serde_json::json!({ "tag": { "id": new_name } });
166 let response = self.send_retrying(|| Ok(self.put(&path)?.json(&payload)))?;
167 let status = response.status();
168 let text = response.text().context("reading rename tag response")?;
169 if !status.is_success() {
170 return Err(http_error("rename tag", status, &text));
171 }
172 Ok(())
173 }
174
175 pub fn delete_tag(&self, tag_name: &str) -> Result<()> {
177 let path = format!("/tags/{}.json", tag_name);
178 let response = self.delete(&path)?;
179 let status = response.status();
180 if !status.is_success() {
181 let text = response.text().unwrap_or_default();
182 return Err(http_error("delete tag", status, &text));
183 }
184 Ok(())
185 }
186
187 pub fn fetch_topic_tags(&self, topic_id: u64) -> Result<Vec<String>> {
189 let path = format!("/t/{}.json", topic_id);
190 let response = self.get(&path)?;
191 let status = response.status();
192 let text = response.text().context("reading topic response body")?;
193 if !status.is_success() {
194 return Err(http_error("topic request", status, &text));
195 }
196 let value: Value = serde_json::from_str(&text).context("parsing topic response json")?;
197 let tags = value
198 .get("tags")
199 .and_then(|v| v.as_array())
200 .map(|arr| {
201 arr.iter()
202 .filter_map(|v| v.as_str().map(|s| s.to_string()))
203 .collect()
204 })
205 .unwrap_or_default();
206 Ok(tags)
207 }
208
209 pub fn set_topic_tags(&self, topic_id: u64, tags: &[String]) -> Result<Vec<String>> {
211 let path = format!("/t/{}.json", topic_id);
212 let payload: Vec<(&str, &str)> = if tags.is_empty() {
213 vec![("tags[]", "")]
216 } else {
217 tags.iter().map(|t| ("tags[]", t.as_str())).collect()
218 };
219 let response = self.send_retrying(|| Ok(self.put(&path)?.form(&payload)))?;
220 let status = response.status();
221 let text = response.text().context("reading set-tags response body")?;
222 if !status.is_success() {
223 return Err(http_error("set tags request", status, &text));
224 }
225 self.fetch_topic_tags(topic_id)
228 }
229}