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
89 .text()
90 .context("reading tag groups response body")?;
91 if !status.is_success() {
92 return Err(http_error("tag groups request", status, &text));
93 }
94 let body: TagGroupsResponse =
95 serde_json::from_str(&text).context("parsing tag groups response json")?;
96 Ok(Some(body.tag_groups))
97 }
98
99 pub fn create_tag_group(&self, payload: &Value) -> Result<u64> {
101 let response = self.send_retrying(|| Ok(self.post("/tag_groups.json")?.json(payload)))?;
102 let status = response.status();
103 let text = response
104 .text()
105 .context("reading create tag group response")?;
106 if !status.is_success() {
107 return Err(http_error("create tag group", status, &text));
108 }
109 let value: Value =
110 serde_json::from_str(&text).context("parsing create tag group response")?;
111 let id = value
112 .pointer("/tag_group/id")
113 .and_then(|v| v.as_u64())
114 .ok_or_else(|| anyhow::anyhow!("tag group creation response missing id"))?;
115 Ok(id)
116 }
117
118 pub fn update_tag_group(&self, group_id: u64, payload: &Value) -> Result<()> {
120 let path = format!("/tag_groups/{}.json", group_id);
121 let response = self.send_retrying(|| Ok(self.put(&path)?.json(payload)))?;
122 let status = response.status();
123 let text = response
124 .text()
125 .context("reading update tag group response")?;
126 if !status.is_success() {
127 return Err(http_error("update tag group", status, &text));
128 }
129 Ok(())
130 }
131
132 pub fn delete_tag_group(&self, group_id: u64) -> Result<()> {
134 let path = format!("/tag_groups/{}.json", group_id);
135 let response = self.delete(&path)?;
136 let status = response.status();
137 if !status.is_success() {
138 let text = response.text().unwrap_or_default();
139 return Err(http_error("delete tag group", status, &text));
140 }
141 Ok(())
142 }
143
144 pub fn update_tag(&self, tag_name: &str, description: Option<&str>) -> Result<()> {
153 let path = format!("/tag/{}.json", tag_name);
154 let mut payload = serde_json::Map::new();
155 if let Some(desc) = description {
156 payload.insert("tag".to_string(), serde_json::json!({"description": desc}));
157 }
158 let response = self.send_retrying(|| Ok(self.put(&path)?.json(&payload)))?;
159 let status = response.status();
160 let text = response.text().context("reading update tag response")?;
161 if !status.is_success() {
162 return Err(http_error("update tag", status, &text));
163 }
164 Ok(())
165 }
166
167 pub fn rename_tag(&self, old_name: &str, new_name: &str) -> Result<()> {
171 let path = format!("/tag/{}.json", old_name);
172 let payload = serde_json::json!({ "tag": { "id": new_name } });
173 let response = self.send_retrying(|| Ok(self.put(&path)?.json(&payload)))?;
174 let status = response.status();
175 let text = response.text().context("reading rename tag response")?;
176 if !status.is_success() {
177 return Err(http_error("rename tag", status, &text));
178 }
179 Ok(())
180 }
181
182 pub fn delete_tag(&self, tag_name: &str) -> Result<()> {
188 let path = format!("/tag/{}.json", tag_name);
189 let response = self.delete(&path)?;
190 let status = response.status();
191 if !status.is_success() {
192 let text = response.text().unwrap_or_default();
193 return Err(http_error("delete tag", status, &text));
194 }
195 Ok(())
196 }
197
198 pub fn fetch_topic_tags(&self, topic_id: u64) -> Result<Vec<String>> {
200 let path = format!("/t/{}.json", topic_id);
201 let response = self.get(&path)?;
202 let status = response.status();
203 let text = response.text().context("reading topic response body")?;
204 if !status.is_success() {
205 return Err(http_error("topic request", status, &text));
206 }
207 let value: Value = serde_json::from_str(&text).context("parsing topic response json")?;
208 let tags = value
209 .get("tags")
210 .and_then(|v| v.as_array())
211 .map(|arr| {
212 arr.iter()
213 .filter_map(|v| v.as_str().map(|s| s.to_string()))
214 .collect()
215 })
216 .unwrap_or_default();
217 Ok(tags)
218 }
219
220 pub fn set_topic_tags(&self, topic_id: u64, tags: &[String]) -> Result<Vec<String>> {
222 let path = format!("/t/{}.json", topic_id);
223 let payload: Vec<(&str, &str)> = if tags.is_empty() {
224 vec![("tags[]", "")]
227 } else {
228 tags.iter().map(|t| ("tags[]", t.as_str())).collect()
229 };
230 let response = self.send_retrying(|| Ok(self.put(&path)?.form(&payload)))?;
231 let status = response.status();
232 let text = response.text().context("reading set-tags response body")?;
233 if !status.is_success() {
234 return Err(http_error("set tags request", status, &text));
235 }
236 self.fetch_topic_tags(topic_id)
239 }
240}