1use 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 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 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(""", "\"")
146 .replace("<", "<")
147 .replace(">", ">")
148 .replace("&", "&"))
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('&', "&")
225 .replace('<', "<")
226 .replace('>', ">")
227 .replace('"', """)
228}