Skip to main content

rlean_search/
protocol.rs

1//! JSONL and XML request/response protocol for the daemon and client.
2
3use crate::ast::RLEAN_NS;
4use crate::search::SearchHit;
5use serde::{Deserialize, Serialize};
6
7#[derive(Debug, Clone, Serialize, Deserialize)]
8#[serde(tag = "cmd", rename_all = "lowercase")]
9pub enum Request {
10    #[serde(rename = "search")]
11    Search {
12        pattern: String,
13        #[serde(default = "default_limit")]
14        limit: usize,
15    },
16    #[serde(rename = "stats")]
17    Stats {},
18    #[serde(rename = "reload")]
19    Reload {},
20    #[serde(rename = "ping")]
21    Ping {},
22}
23
24fn default_limit() -> usize {
25    50
26}
27
28#[derive(Debug, Clone, Serialize, Deserialize)]
29#[serde(tag = "type", rename_all = "lowercase")]
30pub enum Response {
31    #[serde(rename = "search")]
32    Search {
33        pattern: String,
34        count: usize,
35        hits: Vec<SearchHit>,
36    },
37    #[serde(rename = "stats")]
38    Stats {
39        declarations: usize,
40        packages: usize,
41        source_hash: String,
42    },
43    #[serde(rename = "ok")]
44    Ok { message: String },
45    #[serde(rename = "error")]
46    Error { message: String },
47    #[serde(rename = "pong")]
48    Pong {},
49}
50
51#[derive(Debug, Clone, Copy, PartialEq, Eq)]
52pub enum ProtocolKind {
53    Jsonl,
54    Xml,
55}
56
57impl ProtocolKind {
58    pub fn detect(line: &str) -> Self {
59        let t = line.trim_start();
60        if t.starts_with('<') {
61            ProtocolKind::Xml
62        } else {
63            ProtocolKind::Jsonl
64        }
65    }
66}
67
68pub fn parse_request(line: &str) -> Result<(ProtocolKind, Request), String> {
69    let kind = ProtocolKind::detect(line);
70    match kind {
71        ProtocolKind::Jsonl => {
72            let req: Request =
73                serde_json::from_str(line.trim()).map_err(|e| format!("json parse: {e}"))?;
74            Ok((kind, req))
75        }
76        ProtocolKind::Xml => {
77            let req = parse_xml_request(line)?;
78            Ok((kind, req))
79        }
80    }
81}
82
83pub fn format_response(kind: ProtocolKind, resp: &Response) -> String {
84    match kind {
85        ProtocolKind::Jsonl => {
86            let mut s = serde_json::to_string(resp).unwrap_or_else(|e| {
87                format!(
88                    r#"{{"type":"error","message":{}}}"#,
89                    serde_json::to_string(&e.to_string()).unwrap()
90                )
91            });
92            s.push('\n');
93            s
94        }
95        ProtocolKind::Xml => {
96            let mut s = response_to_xml(resp);
97            if !s.ends_with('\n') {
98                s.push('\n');
99            }
100            s
101        }
102    }
103}
104
105fn parse_xml_request(xml: &str) -> Result<Request, String> {
106    let xml = xml.trim();
107    // <search pattern="..." limit="50"/> or <request><search .../></request>
108    if let Some(pat) = xml_attr(xml, "pattern") {
109        let limit = xml_attr(xml, "limit")
110            .and_then(|s| s.parse().ok())
111            .unwrap_or(50);
112        if xml.contains("search") {
113            return Ok(Request::Search {
114                pattern: pat,
115                limit,
116            });
117        }
118    }
119    if xml.contains("<stats") || xml.contains("<rlean:stats") {
120        return Ok(Request::Stats {});
121    }
122    if xml.contains("<reload") || xml.contains("<rlean:reload") {
123        return Ok(Request::Reload {});
124    }
125    if xml.contains("<ping") || xml.contains("<rlean:ping") {
126        return Ok(Request::Ping {});
127    }
128    // <rlean:request>...<rlean:search pattern="..."/>
129    if let Some(inner) = extract_tag(xml, "rlean:search").or_else(|| extract_tag(xml, "search")) {
130        let pattern = xml_attr(&inner, "pattern").ok_or("missing pattern")?;
131        let limit = xml_attr(&inner, "limit")
132            .and_then(|s| s.parse().ok())
133            .unwrap_or(50);
134        return Ok(Request::Search { pattern, limit });
135    }
136    Err("unrecognized XML request".into())
137}
138
139fn xml_attr(xml: &str, name: &str) -> Option<String> {
140    let key = format!(r#"{name}=""#);
141    let idx = xml.find(&key)?;
142    let rest = &xml[idx + key.len()..];
143    let end = rest.find('"')?;
144    Some(rest[..end]
145        .replace("&quot;", "\"")
146        .replace("&lt;", "<")
147        .replace("&gt;", ">")
148        .replace("&amp;", "&"))
149}
150
151fn extract_tag<'a>(xml: &'a str, tag: &str) -> Option<String> {
152    let open = format!("<{tag}");
153    let idx = xml.find(&open)?;
154    let rest = &xml[idx..];
155    if let Some(end) = rest.find("/>") {
156        return Some(rest[..=end + 1].to_string());
157    }
158    let close = format!("</{tag}>");
159    let end = rest.find(&close)?;
160    Some(rest[..end + close.len()].to_string())
161}
162
163fn response_to_xml(resp: &Response) -> String {
164    match resp {
165        Response::Search {
166            pattern,
167            count,
168            hits,
169        } => {
170            let mut out = format!(
171                r#"<rlean:response xmlns:rlean="{ns}" type="search" pattern="{pat}" count="{count}">"#,
172                ns = RLEAN_NS,
173                pat = escape(pattern),
174                count = count,
175            );
176            out.push('\n');
177            for h in hits {
178                out.push_str(&format!(
179                    r#"  <rlean:hit name="{name}" full_name="{full}" kind="{kind}" file="{file}" line="{line}" score="{score}">
180    <rlean:typeSurface>{ty}</rlean:typeSurface>
181  </rlean:hit>
182"#,
183                    name = escape(&h.name),
184                    full = escape(&h.full_name),
185                    kind = escape(&h.kind),
186                    file = escape(&h.file),
187                    line = h.line,
188                    score = h.score,
189                    ty = escape(&h.type_surface),
190                ));
191            }
192            out.push_str("</rlean:response>");
193            out
194        }
195        Response::Stats {
196            declarations,
197            packages,
198            source_hash,
199        } => format!(
200            r#"<rlean:response xmlns:rlean="{ns}" type="stats" declarations="{d}" packages="{p}" source_hash="{h}"/>"#,
201            ns = RLEAN_NS,
202            d = declarations,
203            p = packages,
204            h = escape(source_hash),
205        ),
206        Response::Ok { message } => format!(
207            r#"<rlean:response xmlns:rlean="{ns}" type="ok" message="{m}"/>"#,
208            ns = RLEAN_NS,
209            m = escape(message),
210        ),
211        Response::Error { message } => format!(
212            r#"<rlean:response xmlns:rlean="{ns}" type="error" message="{m}"/>"#,
213            ns = RLEAN_NS,
214            m = escape(message),
215        ),
216        Response::Pong {} => format!(
217            r#"<rlean:response xmlns:rlean="{ns}" type="pong"/>"#,
218            ns = RLEAN_NS
219        ),
220    }
221}
222
223fn escape(s: &str) -> String {
224    s.replace('&', "&amp;")
225        .replace('<', "&lt;")
226        .replace('>', "&gt;")
227        .replace('"', "&quot;")
228}