Skip to main content

dsc/api/
categories.rs

1use super::client::DiscourseClient;
2use super::error::http_error;
3use super::models::{CategoriesResponse, CategoryInfo, CategoryResponse, CreateCategoryResponse};
4use anyhow::{Context, Result, anyhow};
5use reqwest::StatusCode;
6use serde_json::Value;
7use std::collections::HashMap;
8
9impl DiscourseClient {
10    /// Fetch a category by ID (topics list included).
11    pub fn fetch_category(&self, category_id: u64) -> Result<CategoryResponse> {
12        let path = format!("/c/{}.json", category_id);
13        let response = self.get(&path)?;
14        let status = response.status();
15        let text = response.text().context("reading category response body")?;
16        if !status.is_success() {
17            if status == StatusCode::NOT_FOUND {
18                return Err(anyhow!("category not found: {}", category_id));
19            }
20            return Err(anyhow!("category request failed with {}", status));
21        }
22        let body: CategoryResponse =
23            serde_json::from_str(&text).context("reading category json")?;
24        Ok(body)
25    }
26
27    /// Fetch all categories.
28    pub fn fetch_categories(&self) -> Result<Vec<CategoryInfo>> {
29        let response = self.get("/categories.json?include_subcategories=true")?;
30        let status = response.status();
31        let text = response
32            .text()
33            .context("reading categories response body")?;
34        if !status.is_success() {
35            return Err(anyhow!("categories request failed with {}", status));
36        }
37        let body: CategoriesResponse =
38            serde_json::from_str(&text).context("reading categories json")?;
39        let mut categories = body.category_list.categories;
40        if let Ok(site_categories) = self.fetch_site_categories() {
41            let mut seen = HashMap::new();
42            for (idx, cat) in categories.iter().enumerate() {
43                if let Some(id) = cat.id {
44                    seen.insert(id, idx);
45                }
46            }
47            for cat in site_categories {
48                if let Some(id) = cat.id {
49                    if !seen.contains_key(&id) {
50                        categories.push(cat);
51                    }
52                }
53            }
54        }
55        Ok(categories)
56    }
57
58    /// Create a category with basic fields copied from a source category.
59    pub fn create_category(&self, category: &CategoryInfo) -> Result<u64> {
60        let mut payload = vec![("name", category.name.clone())];
61        if !category.slug.is_empty() {
62            payload.push(("slug", category.slug.clone()));
63        }
64        if let Some(color) = category.color.clone() {
65            payload.push(("color", color));
66        }
67        if let Some(text_color) = category.text_color.clone() {
68            payload.push(("text_color", text_color));
69        }
70        let response = self
71            .post("/categories")?
72            .form(&payload)
73            .send()
74            .context("creating category")?;
75        let status = response.status();
76        let text = response.text().context("reading category response body")?;
77        if !status.is_success() {
78            return Err(anyhow!("create category failed with {}", status));
79        }
80        let body: CreateCategoryResponse =
81            serde_json::from_str(&text).context("reading category response")?;
82        Ok(body.category.id)
83    }
84
85    fn fetch_site_categories(&self) -> Result<Vec<CategoryInfo>> {
86        let response = self.get("/site.json")?;
87        let status = response.status();
88        let text = response.text().context("reading site.json response body")?;
89        if !status.is_success() {
90            return Err(http_error("site.json request", status, &text));
91        }
92        let value: Value = serde_json::from_str(&text).context("parsing site.json")?;
93        let array = value
94            .get("categories")
95            .and_then(|v| v.as_array())
96            .or_else(|| {
97                value
98                    .get("site")
99                    .and_then(|v| v.get("categories"))
100                    .and_then(|v| v.as_array())
101            })
102            .ok_or_else(|| anyhow!("site.json missing categories list"))?;
103        let mut categories = Vec::new();
104        for item in array {
105            if let Ok(cat) = serde_json::from_value::<CategoryInfo>(item.clone()) {
106                categories.push(cat);
107            }
108        }
109        Ok(categories)
110    }
111}