1use super::client::DiscourseClient;
2use super::error::http_error;
3use anyhow::{Context, Result};
4use serde::{Deserialize, Serialize};
5
6#[derive(Debug, Deserialize, Serialize, Clone)]
9pub struct SearchHit {
10 pub id: u64,
11 pub title: String,
12 #[serde(default)]
13 pub slug: String,
14 #[serde(default)]
15 pub posts_count: u64,
16 #[serde(default)]
17 pub category_id: Option<u64>,
18 #[serde(default)]
19 pub tags: Option<Vec<String>>,
20}
21
22#[derive(Debug, Deserialize)]
23struct RawSearchResponse {
24 #[serde(default)]
25 topics: Vec<SearchHit>,
26}
27
28impl DiscourseClient {
29 pub fn search_topics(&self, query: &str) -> Result<Vec<SearchHit>> {
32 let path = format!(
33 "/search.json?q={}",
34 urlencode_form(query)
35 );
36 let response = self.get(&path)?;
37 let status = response.status();
38 let text = response.text().context("reading search response body")?;
39 if !status.is_success() {
40 return Err(http_error("search request", status, &text));
41 }
42 let body: RawSearchResponse =
43 serde_json::from_str(&text).context("parsing search response json")?;
44 Ok(body.topics)
45 }
46}
47
48fn urlencode_form(input: &str) -> String {
51 let mut out = String::with_capacity(input.len());
52 for byte in input.as_bytes() {
53 let b = *byte;
54 if b.is_ascii_alphanumeric() || matches!(b, b'-' | b'_' | b'.' | b'~') {
55 out.push(b as char);
56 } else if b == b' ' {
57 out.push('+');
58 } else {
59 out.push_str(&format!("%{:02X}", b));
60 }
61 }
62 out
63}
64
65#[cfg(test)]
66mod tests {
67 use super::urlencode_form;
68
69 #[test]
70 fn encodes_spaces_as_plus() {
71 assert_eq!(urlencode_form("hello world"), "hello+world");
72 }
73
74 #[test]
75 fn encodes_special_chars_percent() {
76 assert_eq!(urlencode_form("a&b=c"), "a%26b%3Dc");
77 }
78
79 #[test]
80 fn passes_alnum_unchanged() {
81 assert_eq!(urlencode_form("Topic42"), "Topic42");
82 }
83
84 #[test]
85 fn passes_unreserved_unchanged() {
86 assert_eq!(urlencode_form("a-b_c.d~e"), "a-b_c.d~e");
87 }
88
89 #[test]
90 fn encodes_discourse_filter_syntax() {
91 assert_eq!(
93 urlencode_form("hello category:foo @bob"),
94 "hello+category%3Afoo+%40bob"
95 );
96 }
97}