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!("/search.json?q={}", urlencode_form(query));
33 let response = self.get(&path)?;
34 let status = response.status();
35 let text = response.text().context("reading search response body")?;
36 if !status.is_success() {
37 return Err(http_error("search request", status, &text));
38 }
39 let body: RawSearchResponse =
40 serde_json::from_str(&text).context("parsing search response json")?;
41 Ok(body.topics)
42 }
43}
44
45fn urlencode_form(input: &str) -> String {
48 let mut out = String::with_capacity(input.len());
49 for byte in input.as_bytes() {
50 let b = *byte;
51 if b.is_ascii_alphanumeric() || matches!(b, b'-' | b'_' | b'.' | b'~') {
52 out.push(b as char);
53 } else if b == b' ' {
54 out.push('+');
55 } else {
56 out.push_str(&format!("%{:02X}", b));
57 }
58 }
59 out
60}
61
62#[cfg(test)]
63mod tests {
64 use super::urlencode_form;
65
66 #[test]
67 fn encodes_spaces_as_plus() {
68 assert_eq!(urlencode_form("hello world"), "hello+world");
69 }
70
71 #[test]
72 fn encodes_special_chars_percent() {
73 assert_eq!(urlencode_form("a&b=c"), "a%26b%3Dc");
74 }
75
76 #[test]
77 fn passes_alnum_unchanged() {
78 assert_eq!(urlencode_form("Topic42"), "Topic42");
79 }
80
81 #[test]
82 fn passes_unreserved_unchanged() {
83 assert_eq!(urlencode_form("a-b_c.d~e"), "a-b_c.d~e");
84 }
85
86 #[test]
87 fn encodes_discourse_filter_syntax() {
88 assert_eq!(
90 urlencode_form("hello category:foo @bob"),
91 "hello+category%3Afoo+%40bob"
92 );
93 }
94}