1#![cfg(not(target_arch = "wasm32"))]
2
3use std::io::{BufRead, BufReader, BufWriter, Read, Write};
8use std::net::{TcpListener, TcpStream};
9use std::sync::atomic::{AtomicBool, Ordering};
10use std::sync::Arc;
11use std::thread::JoinHandle;
12use std::time::Duration;
13
14use crate::core::ExceptionSite;
15use crate::native_cli::{
16 Documentation, DocumentationValue, RuntimeBroker, RuntimeDiagnostic, RuntimeException,
17};
18
19const MAX_LINE: usize = 64 * 1024;
20const MAX_BULK: usize = 64 * 1024 * 1024;
21const MAX_NESTING: usize = 64;
22const MAX_DIAGNOSTIC_DATA_BYTES: usize = 16 * 1024;
23
24#[derive(Clone, Debug)]
25struct RespFailure {
26 code: &'static str,
27 message: String,
28 diagnostic: Option<RespValue>,
29}
30
31impl RespFailure {
32 fn new(code: &'static str, message: impl Into<String>) -> Self {
33 Self {
34 code,
35 message: message.into(),
36 diagnostic: None,
37 }
38 }
39
40 fn evaluation(diagnostic: RuntimeDiagnostic, origin: Option<SourceOrigin>) -> Self {
41 let message = diagnostic.message.clone();
42 Self {
43 code: "EVAL_ERROR",
44 message,
45 diagnostic: Some(diagnostic_payload(&diagnostic, origin.as_ref())),
46 }
47 }
48}
49
50#[derive(Clone, Debug)]
51struct SourceOrigin {
52 file: String,
53 line: usize,
54 column: usize,
55 source: String,
56}
57
58#[derive(Clone, Debug, PartialEq, Eq)]
59pub enum RespValue {
60 Simple(String),
61 Error(String),
62 Integer(i64),
63 Bulk(Option<Vec<u8>>),
64 Array(Option<Vec<RespValue>>),
65}
66
67impl RespValue {
68 pub fn text(&self) -> Option<String> {
69 match self {
70 Self::Simple(value) | Self::Error(value) => Some(value.clone()),
71 Self::Integer(value) => Some(value.to_string()),
72 Self::Bulk(Some(value)) => String::from_utf8(value.clone()).ok(),
73 _ => None,
74 }
75 }
76
77 pub fn bulk(value: impl Into<String>) -> Self {
78 Self::Bulk(Some(value.into().into_bytes()))
79 }
80
81 pub fn array(values: impl IntoIterator<Item = impl Into<String>>) -> Self {
82 Self::Array(Some(
83 values
84 .into_iter()
85 .map(|value| Self::bulk(value.into()))
86 .collect(),
87 ))
88 }
89}
90
91pub struct RespConnection {
92 input: BufReader<TcpStream>,
93 output: BufWriter<TcpStream>,
94}
95
96impl RespConnection {
97 pub fn new(stream: TcpStream) -> Result<Self, String> {
98 let output = stream
99 .try_clone()
100 .map(BufWriter::new)
101 .map_err(|error| format!("RESP socket clone failed: {error}"))?;
102 Ok(Self {
103 input: BufReader::new(stream),
104 output,
105 })
106 }
107
108 pub fn read(&mut self) -> Result<Option<RespValue>, String> {
109 let mut prefix = [0_u8; 1];
110 match self.input.read_exact(&mut prefix) {
111 Ok(()) => self.read_after_prefix(prefix[0], 0).map(Some),
112 Err(error) if error.kind() == std::io::ErrorKind::UnexpectedEof => Ok(None),
113 Err(error) => Err(format!("RESP read failed: {error}")),
114 }
115 }
116
117 fn read_after_prefix(&mut self, prefix: u8, depth: usize) -> Result<RespValue, String> {
118 if depth > MAX_NESTING {
119 return Err("RESP nesting limit exceeded".into());
120 }
121 match prefix {
122 b'+' => Ok(RespValue::Simple(self.line()?)),
123 b'-' => Ok(RespValue::Error(self.line()?)),
124 b':' => self
125 .line()?
126 .parse::<i64>()
127 .map(RespValue::Integer)
128 .map_err(|_| "Invalid RESP integer".into()),
129 b'$' => {
130 let length = self.length()?;
131 if length < 0 {
132 return Ok(RespValue::Bulk(None));
133 }
134 let length = usize::try_from(length).map_err(|_| "Invalid RESP length")?;
135 if length > MAX_BULK {
136 return Err("RESP bulk limit exceeded".into());
137 }
138 let mut bytes = vec![0; length];
139 self.input
140 .read_exact(&mut bytes)
141 .map_err(|error| format!("RESP read failed: {error}"))?;
142 self.crlf()?;
143 Ok(RespValue::Bulk(Some(bytes)))
144 }
145 b'*' => {
146 let length = self.length()?;
147 if length < 0 {
148 return Ok(RespValue::Array(None));
149 }
150 let length = usize::try_from(length).map_err(|_| "Invalid RESP length")?;
151 if length > MAX_LINE {
152 return Err("RESP array limit exceeded".into());
153 }
154 let mut values = Vec::with_capacity(length);
155 for _ in 0..length {
156 let mut prefix = [0_u8; 1];
157 self.input
158 .read_exact(&mut prefix)
159 .map_err(|error| format!("RESP read failed: {error}"))?;
160 values.push(self.read_after_prefix(prefix[0], depth + 1)?);
161 }
162 Ok(RespValue::Array(Some(values)))
163 }
164 _ => Err("Unknown RESP type".into()),
165 }
166 }
167
168 fn length(&mut self) -> Result<i64, String> {
169 self.line()?
170 .parse()
171 .map_err(|_| "Invalid RESP length".into())
172 }
173
174 fn line(&mut self) -> Result<String, String> {
175 let mut bytes = Vec::new();
176 let read = self
177 .input
178 .read_until(b'\n', &mut bytes)
179 .map_err(|error| format!("RESP read failed: {error}"))?;
180 if read < 2 || bytes[read - 2..] != *b"\r\n" {
181 return Err("Invalid RESP line ending".into());
182 }
183 if bytes.len() > MAX_LINE {
184 return Err("RESP line limit exceeded".into());
185 }
186 bytes.truncate(read - 2);
187 String::from_utf8(bytes).map_err(|_| "RESP line is not UTF-8".into())
188 }
189
190 fn crlf(&mut self) -> Result<(), String> {
191 let mut ending = [0_u8; 2];
192 self.input
193 .read_exact(&mut ending)
194 .map_err(|error| format!("RESP read failed: {error}"))?;
195 if ending != *b"\r\n" {
196 return Err("Invalid RESP bulk ending".into());
197 }
198 Ok(())
199 }
200
201 pub fn write(&mut self, value: &RespValue) -> Result<(), String> {
202 write_value(&mut self.output, value)?;
203 self.output
204 .flush()
205 .map_err(|error| format!("RESP write failed: {error}"))
206 }
207}
208
209fn write_value(output: &mut impl Write, value: &RespValue) -> Result<(), String> {
210 match value {
211 RespValue::Simple(value) => line_value(output, b'+', value),
212 RespValue::Error(value) => line_value(output, b'-', value),
213 RespValue::Integer(value) => line_value(output, b':', &value.to_string()),
214 RespValue::Bulk(None) => output
215 .write_all(b"$-1\r\n")
216 .map_err(|error| format!("RESP write failed: {error}")),
217 RespValue::Bulk(Some(bytes)) => output
218 .write_all(format!("${}\r\n", bytes.len()).as_bytes())
219 .and_then(|_| output.write_all(bytes))
220 .and_then(|_| output.write_all(b"\r\n"))
221 .map_err(|error| format!("RESP write failed: {error}")),
222 RespValue::Array(None) => output
223 .write_all(b"*-1\r\n")
224 .map_err(|error| format!("RESP write failed: {error}")),
225 RespValue::Array(Some(values)) => {
226 output
227 .write_all(format!("*{}\r\n", values.len()).as_bytes())
228 .map_err(|error| format!("RESP write failed: {error}"))?;
229 for value in values {
230 write_value(output, value)?;
231 }
232 Ok(())
233 }
234 }
235}
236
237fn line_value(output: &mut impl Write, prefix: u8, value: &str) -> Result<(), String> {
238 if value.contains(['\r', '\n']) {
239 return Err("RESP line values cannot contain CR or LF".into());
240 }
241 output
242 .write_all(&[prefix])
243 .and_then(|_| output.write_all(value.as_bytes()))
244 .and_then(|_| output.write_all(b"\r\n"))
245 .map_err(|error| format!("RESP write failed: {error}"))
246}
247
248pub struct RespServer {
249 host: String,
250 port: u16,
251 running: Arc<AtomicBool>,
252 thread: Option<JoinHandle<()>>,
253}
254
255impl RespServer {
256 pub fn start(host: &str, port: u16, broker: RuntimeBroker) -> Result<Self, String> {
257 let listener = TcpListener::bind((host, port))
258 .map_err(|error| format!("RESP bind {host}:{port} failed: {error}"))?;
259 let address = listener
260 .local_addr()
261 .map_err(|error| format!("RESP address failed: {error}"))?;
262 listener
263 .set_nonblocking(true)
264 .map_err(|error| format!("RESP listener setup failed: {error}"))?;
265 let running = Arc::new(AtomicBool::new(true));
266 let active = running.clone();
267 let instance = format!("RUST-{}-{}", std::process::id(), address.port());
268 let root = std::env::current_dir()
269 .unwrap_or_default()
270 .display()
271 .to_string();
272 let thread = std::thread::Builder::new()
273 .name("hara-resp-listener".into())
274 .spawn(move || {
275 while active.load(Ordering::Acquire) {
276 match listener.accept() {
277 Ok((stream, _)) => {
278 if stream.set_nonblocking(false).is_err() {
279 continue;
280 }
281 let broker = broker.clone();
282 let instance = instance.clone();
283 let root = root.clone();
284 let _ = std::thread::Builder::new()
285 .name("hara-resp-client".into())
286 .spawn(move || serve(stream, broker, &instance, &root));
287 }
288 Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
289 std::thread::sleep(Duration::from_millis(10));
290 }
291 Err(_) => break,
292 }
293 }
294 })
295 .map_err(|error| format!("RESP listener thread failed: {error}"))?;
296 Ok(Self {
297 host: host.into(),
298 port: address.port(),
299 running,
300 thread: Some(thread),
301 })
302 }
303
304 pub fn endpoint(&self) -> String {
305 format!("{}:{}", self.host, self.port)
306 }
307
308 pub fn stop(&mut self) {
309 self.running.store(false, Ordering::Release);
310 if let Some(thread) = self.thread.take() {
311 let _ = thread.join();
312 }
313 }
314}
315
316impl Drop for RespServer {
317 fn drop(&mut self) {
318 self.stop();
319 }
320}
321
322fn serve(stream: TcpStream, broker: RuntimeBroker, instance: &str, root: &str) {
323 let Ok(mut connection) = RespConnection::new(stream) else {
324 return;
325 };
326 let mut protocol = 3_u8;
327 let mut attached = "ROOT".to_owned();
328 loop {
329 let request = match connection.read() {
330 Ok(Some(RespValue::Array(Some(values)))) => values,
331 Ok(Some(_)) => {
332 let _ = connection.write(&RespValue::Error("BAD_REQUEST expected array".into()));
333 continue;
334 }
335 Ok(None) => return,
336 Err(error) => {
337 let _ = connection.write(&RespValue::Error(format!("BAD_REQUEST {error}")));
338 continue;
339 }
340 };
341 let words = request
342 .iter()
343 .map(RespValue::text)
344 .collect::<Option<Vec<_>>>();
345 let Some(words) = words else {
346 let _ = connection.write(&RespValue::Error(
347 "BAD_REQUEST textual arguments required".into(),
348 ));
349 continue;
350 };
351 if words.is_empty() {
352 continue;
353 }
354 let operation = words[0].to_ascii_uppercase();
355 if operation == "QUIT" {
356 let _ = connection.write(&RespValue::Simple("OK".into()));
357 return;
358 }
359 if operation == "HELLO" {
360 protocol = words
361 .get(1)
362 .and_then(|value| value.parse().ok())
363 .unwrap_or(3);
364 let hello = RespValue::array([
365 "SERVER",
366 "HARA",
367 "INSTANCE",
368 instance,
369 "PROTOCOL",
370 &protocol.to_string(),
371 "ROOT",
372 root,
373 ]);
374 let _ = connection.write(&hello);
375 continue;
376 }
377 if protocol >= 4 {
378 let id = words.get(1).cloned().unwrap_or_else(|| "?".into());
379 handle_v4(
380 &mut connection,
381 &broker,
382 &mut attached,
383 &operation,
384 &id,
385 &words[2..],
386 );
387 } else {
388 handle_legacy(
389 &mut connection,
390 &broker,
391 &mut attached,
392 &operation,
393 &words[1..],
394 );
395 }
396 }
397}
398
399fn handle_v4(
400 connection: &mut RespConnection,
401 broker: &RuntimeBroker,
402 attached: &mut String,
403 operation: &str,
404 id: &str,
405 arguments: &[String],
406) {
407 let result = operation_result(broker, attached, operation, arguments);
408 match result {
409 Ok(value) => {
410 let _ = connection.write(&RespValue::Array(Some(vec![
411 RespValue::bulk("RESULT"),
412 RespValue::bulk(id),
413 value,
414 ])));
415 let _ = connection.write(&RespValue::array(["DONE", id, "OK"]));
416 }
417 Err(failure) => {
418 let mut frame = vec![
419 RespValue::bulk("ERROR"),
420 RespValue::bulk(id),
421 RespValue::bulk(failure.code),
422 RespValue::bulk(failure.message),
423 ];
424 if let Some(diagnostic) = failure.diagnostic {
425 frame.push(diagnostic);
426 }
427 let _ = connection.write(&RespValue::Array(Some(frame)));
428 let _ = connection.write(&RespValue::array(["DONE", id, "ERROR"]));
429 }
430 }
431}
432
433fn handle_legacy(
434 connection: &mut RespConnection,
435 broker: &RuntimeBroker,
436 attached: &mut String,
437 operation: &str,
438 arguments: &[String],
439) {
440 let result = if operation == "EVAL" && arguments.len() >= 2 {
441 broker
442 .eval(&arguments[0], &arguments[1])
443 .map(RespValue::bulk)
444 .map_err(|message| RespFailure::new("EVAL_ERROR", message))
445 } else {
446 operation_result(broker, attached, operation, arguments)
447 };
448 let response = match result {
449 Ok(value) => legacy_value(value),
450 Err(failure) => RespValue::Error(format!("{} {}", failure.code, failure.message)),
451 };
452 let _ = connection.write(&response);
453}
454
455fn operation_result(
456 broker: &RuntimeBroker,
457 attached: &mut String,
458 operation: &str,
459 arguments: &[String],
460) -> Result<RespValue, RespFailure> {
461 match operation {
462 "EVAL" => {
463 let source = arguments
464 .first()
465 .ok_or_else(|| RespFailure::new("BAD_REQUEST", "EVAL requires source"))?;
466 broker
467 .eval_diagnostic(attached, source)
468 .map(RespValue::bulk)
469 .map_err(|diagnostic| RespFailure::evaluation(diagnostic, eval_origin(arguments)))
470 }
471 "COMPLETE" => {
472 let prefix = arguments.first().map_or("", String::as_str);
473 broker
474 .complete(attached, prefix)
475 .map(RespValue::array)
476 .map_err(|error| RespFailure::new("NO_SESSION", error))
477 }
478 "DOC" => {
479 let symbol = arguments
480 .first()
481 .ok_or_else(|| RespFailure::new("BAD_REQUEST", "DOC requires symbol"))?;
482 broker
483 .documentation(attached, symbol)
484 .map(documentation_value)
485 .map_err(|error| {
486 if error.starts_with("No session:") {
487 RespFailure::new("NO_SESSION", error)
488 } else {
489 RespFailure::new("DOC_NOT_FOUND", error)
490 }
491 })
492 }
493 "SESSION" => session_operation(broker, attached, arguments),
494 "COMMANDS" => Ok(RespValue::bulk(
495 "HELLO EVAL COMPLETE DOC SESSION COMMANDS INFO QUIT",
496 )),
497 "INFO" => broker
498 .info(attached)
499 .map(RespValue::bulk)
500 .map_err(|error| RespFailure::new("NO_SESSION", error)),
501 _ => Err(RespFailure::new(
502 "UNKNOWN_OP",
503 format!("Unknown operation: {operation}"),
504 )),
505 }
506}
507
508fn session_operation(
509 broker: &RuntimeBroker,
510 attached: &mut String,
511 arguments: &[String],
512) -> Result<RespValue, RespFailure> {
513 let action = arguments
514 .first()
515 .map(|value| value.to_ascii_uppercase())
516 .ok_or_else(|| RespFailure::new("BAD_REQUEST", "SESSION requires an action"))?;
517 match action.as_str() {
518 "NEW" => broker
519 .create(
520 arguments
521 .get(1)
522 .ok_or_else(|| RespFailure::new("BAD_REQUEST", "SESSION NEW requires name"))?,
523 )
524 .map(RespValue::bulk)
525 .map_err(|error| RespFailure::new("BAD_REQUEST", error)),
526 "LIST" => broker
527 .list()
528 .map(RespValue::array)
529 .map_err(|error| RespFailure::new("INTERNAL_ERROR", error)),
530 "ATTACH" => {
531 let name = arguments
532 .get(1)
533 .ok_or_else(|| RespFailure::new("BAD_REQUEST", "SESSION ATTACH requires name"))?;
534 broker
535 .info(name)
536 .map_err(|error| RespFailure::new("NO_SESSION", error))?;
537 *attached = name.clone();
538 Ok(RespValue::bulk(name))
539 }
540 "DETACH" => {
541 *attached = "ROOT".into();
542 Ok(RespValue::bulk("ROOT"))
543 }
544 "INFO" => broker
545 .info(attached)
546 .map(RespValue::bulk)
547 .map_err(|error| RespFailure::new("NO_SESSION", error)),
548 "CLOSE" => {
549 broker
550 .close(arguments.get(1).ok_or_else(|| {
551 RespFailure::new("BAD_REQUEST", "SESSION CLOSE requires name")
552 })?)
553 .map(RespValue::bulk)
554 .map_err(|error| RespFailure::new("BAD_REQUEST", error))
555 }
556 _ => Err(RespFailure::new(
557 "BAD_REQUEST",
558 format!("Unknown SESSION action: {action}"),
559 )),
560 }
561}
562
563fn eval_origin(arguments: &[String]) -> Option<SourceOrigin> {
564 let source = arguments.first()?.clone();
565 let mut file = None;
566 let mut line = None;
567 let mut column = None;
568 for pair in arguments[1..].chunks_exact(2) {
569 match pair[0].to_ascii_uppercase().as_str() {
570 "FILE" => file = Some(pair[1].clone()),
571 "LINE" => line = pair[1].parse::<usize>().ok().filter(|value| *value > 0),
572 "COLUMN" => column = pair[1].parse::<usize>().ok().filter(|value| *value > 0),
573 _ => {}
574 }
575 }
576 Some(SourceOrigin {
577 file: file?,
578 line: line?,
579 column: column.unwrap_or(1),
580 source,
581 })
582}
583
584fn truncated_text(value: String) -> String {
585 if value.len() <= MAX_DIAGNOSTIC_DATA_BYTES {
586 return value;
587 }
588 let mut end = MAX_DIAGNOSTIC_DATA_BYTES.saturating_sub(3);
589 while end > 0 && !value.is_char_boundary(end) {
590 end -= 1;
591 }
592 format!("{}...", &value[..end])
593}
594
595fn optional_bulk(value: Option<String>) -> RespValue {
596 value.map_or(RespValue::Bulk(None), RespValue::bulk)
597}
598
599fn optional_integer(value: Option<usize>) -> RespValue {
600 value.map_or(RespValue::Bulk(None), |value| {
601 RespValue::Integer(value as i64)
602 })
603}
604
605fn exception_site(exception: &RuntimeException) -> Option<ExceptionSite> {
606 exception.throws.last().cloned()
607}
608
609fn site_location(
610 site: Option<&ExceptionSite>,
611 origin: Option<&SourceOrigin>,
612 use_origin: bool,
613) -> (Option<String>, Option<usize>, Option<usize>) {
614 let Some(site) = site else {
615 return origin
616 .filter(|_| use_origin)
617 .map(|origin| {
618 (
619 Some(origin.file.clone()),
620 Some(origin.line),
621 Some(origin.column),
622 )
623 })
624 .unwrap_or((None, None, None));
625 };
626 if let Some(resource) = &site.resource {
627 return (
628 Some(resource.clone()),
629 (site.line > 0).then_some(site.line),
630 (site.column > 0).then_some(site.column),
631 );
632 }
633 if use_origin {
634 if let Some(origin) = origin {
635 let line = (site.line > 0).then(|| origin.line + site.line - 1);
636 let column = if site.line <= 1 {
637 (site.column > 0).then(|| origin.column + site.column - 1)
638 } else {
639 (site.column > 0).then_some(site.column)
640 };
641 return (Some(origin.file.clone()), line, column);
642 }
643 }
644 (
645 None,
646 (site.line > 0).then_some(site.line),
647 (site.column > 0).then_some(site.column),
648 )
649}
650
651fn location_payload(site: Option<&ExceptionSite>, origin: Option<&SourceOrigin>) -> RespValue {
652 let use_origin = site.is_none_or(|site| site.resource.is_none());
653 let (file, line, column) = site_location(site, origin, use_origin);
654 RespValue::Array(Some(vec![
655 RespValue::bulk("FILE"),
656 optional_bulk(file),
657 RespValue::bulk("LINE"),
658 optional_integer(line),
659 RespValue::bulk("COLUMN"),
660 optional_integer(column),
661 ]))
662}
663
664fn exception_payload(exception: &RuntimeException) -> RespValue {
665 let class = exception.class.clone().map(truncated_text);
666 let code = exception.code.clone().map(truncated_text);
667 let cause = exception.cause.as_deref().map(exception_payload);
668 let throws = exception
669 .throws
670 .iter()
671 .map(|site| location_payload(Some(site), None))
672 .collect::<Vec<_>>();
673 RespValue::Array(Some(vec![
674 RespValue::bulk("MESSAGE"),
675 RespValue::bulk(truncated_text(exception.message.clone())),
676 RespValue::bulk("CLASS"),
677 optional_bulk(class),
678 RespValue::bulk("CODE"),
679 optional_bulk(code),
680 RespValue::bulk("DATA"),
681 RespValue::bulk(truncated_text(exception.data.clone())),
682 RespValue::bulk("CAUSE"),
683 cause.unwrap_or(RespValue::Bulk(None)),
684 RespValue::bulk("THROWS"),
685 RespValue::Array(Some(throws)),
686 ]))
687}
688
689fn frame_payload(frame: &crate::core::TraceFrame, origin: Option<&SourceOrigin>) -> RespValue {
690 let use_origin = frame.namespace.is_none()
691 && frame
692 .site
693 .as_ref()
694 .is_none_or(|site| site.resource.is_none());
695 let (file, line, column) = site_location(frame.site.as_ref(), origin, use_origin);
696 RespValue::Array(Some(vec![
697 RespValue::bulk("FUNCTION"),
698 RespValue::bulk(frame.name.clone()),
699 RespValue::bulk("NAMESPACE"),
700 optional_bulk(frame.namespace.clone()),
701 RespValue::bulk("FILE"),
702 optional_bulk(file),
703 RespValue::bulk("LINE"),
704 optional_integer(line),
705 RespValue::bulk("COLUMN"),
706 optional_integer(column),
707 ]))
708}
709
710fn evaluation_frame_payload(origin: &SourceOrigin) -> RespValue {
711 RespValue::Array(Some(vec![
712 RespValue::bulk("FUNCTION"),
713 RespValue::bulk("<eval>"),
714 RespValue::bulk("NAMESPACE"),
715 RespValue::Bulk(None),
716 RespValue::bulk("FILE"),
717 RespValue::bulk(origin.file.clone()),
718 RespValue::bulk("LINE"),
719 RespValue::Integer(origin.line as i64),
720 RespValue::bulk("COLUMN"),
721 RespValue::Integer(origin.column as i64),
722 ]))
723}
724
725fn source_excerpt(origin: Option<&SourceOrigin>, line: Option<usize>) -> RespValue {
726 let Some(origin) = origin else {
727 return RespValue::Bulk(None);
728 };
729 let Some(line) = line else {
730 return RespValue::Bulk(None);
731 };
732 let local_line = line.checked_sub(origin.line).map_or(0, |offset| offset + 1);
733 if local_line == 0 {
734 return RespValue::Bulk(None);
735 }
736 let lines = origin.source.lines().collect::<Vec<_>>();
737 if local_line > lines.len() {
738 return RespValue::Bulk(None);
739 }
740 let start = local_line.saturating_sub(3);
741 let end = usize::min(lines.len(), local_line + 2);
742 let text = lines[start..end].join("\n");
743 RespValue::Array(Some(vec![
744 RespValue::bulk("START-LINE"),
745 RespValue::Integer((origin.line + start) as i64),
746 RespValue::bulk("TEXT"),
747 RespValue::bulk(truncated_text(text)),
748 ]))
749}
750
751fn diagnostic_payload(diagnostic: &RuntimeDiagnostic, origin: Option<&SourceOrigin>) -> RespValue {
752 let exception = diagnostic.exception.as_ref();
753 let primary_site = exception.and_then(exception_site).or_else(|| {
754 diagnostic
755 .frames
756 .iter()
757 .rev()
758 .find_map(|frame| frame.site.clone())
759 });
760 let (_, primary_line, _) = site_location(primary_site.as_ref(), origin, true);
761 let mut frames = diagnostic
762 .frames
763 .iter()
764 .rev()
765 .map(|frame| frame_payload(frame, origin))
766 .collect::<Vec<_>>();
767 if frames.is_empty() {
768 if let Some(origin) = origin {
769 frames.push(evaluation_frame_payload(origin));
770 }
771 }
772 RespValue::Array(Some(vec![
773 RespValue::bulk("VERSION"),
774 RespValue::Integer(1),
775 RespValue::bulk("MESSAGE"),
776 RespValue::bulk(truncated_text(diagnostic.message.clone())),
777 RespValue::bulk("EXCEPTION"),
778 exception.map_or(RespValue::Bulk(None), exception_payload),
779 RespValue::bulk("PRIMARY"),
780 location_payload(primary_site.as_ref(), origin),
781 RespValue::bulk("EXCERPT"),
782 source_excerpt(origin, primary_line),
783 RespValue::bulk("FRAMES"),
784 RespValue::Array(Some(frames)),
785 ]))
786}
787
788fn documentation_part(value: DocumentationValue) -> RespValue {
789 match value {
790 DocumentationValue::Nil => RespValue::Bulk(None),
791 DocumentationValue::Boolean(value) => RespValue::bulk(value.to_string()),
792 DocumentationValue::Integer(value) => RespValue::Integer(value),
793 DocumentationValue::String(value) => RespValue::bulk(value),
794 DocumentationValue::Array(values) => {
795 RespValue::Array(Some(values.into_iter().map(documentation_part).collect()))
796 }
797 }
798}
799
800fn documentation_value(documentation: Documentation) -> RespValue {
801 RespValue::Array(Some(vec![
802 RespValue::bulk("SYMBOL"),
803 RespValue::bulk(documentation.symbol),
804 RespValue::bulk("DOC"),
805 documentation
806 .doc
807 .map_or(RespValue::Bulk(None), RespValue::bulk),
808 RespValue::bulk("ARGLISTS"),
809 documentation_part(documentation.arglists),
810 RespValue::bulk("FILE"),
811 documentation
812 .file
813 .map_or(RespValue::Bulk(None), RespValue::bulk),
814 RespValue::bulk("LINE"),
815 documentation
816 .line
817 .map_or(RespValue::Bulk(None), RespValue::Integer),
818 RespValue::bulk("COLUMN"),
819 documentation
820 .column
821 .map_or(RespValue::Bulk(None), RespValue::Integer),
822 ]))
823}
824
825fn legacy_value(value: RespValue) -> RespValue {
826 match value {
827 RespValue::Array(Some(values)) => RespValue::bulk(
828 values
829 .into_iter()
830 .filter_map(|value| value.text())
831 .collect::<Vec<_>>()
832 .join("\n"),
833 ),
834 value => value,
835 }
836}
837
838#[cfg(test)]
839mod tests {
840 use super::{RespConnection, RespValue};
841 use std::net::{TcpListener, TcpStream};
842
843 fn array(value: &RespValue) -> &[RespValue] {
844 match value {
845 RespValue::Array(Some(values)) => values,
846 value => panic!("expected RESP array, got {value:?}"),
847 }
848 }
849
850 fn field<'a>(values: &'a [RespValue], name: &str) -> &'a RespValue {
851 values
852 .chunks_exact(2)
853 .find_map(|pair| (pair[0].text().as_deref() == Some(name)).then_some(&pair[1]))
854 .unwrap_or_else(|| panic!("missing {name} in {values:?}"))
855 }
856
857 #[test]
858 fn resp2_values_round_trip() {
859 let listener = TcpListener::bind("127.0.0.1:0").unwrap();
860 let address = listener.local_addr().unwrap();
861 let writer = std::thread::spawn(move || {
862 let mut connection = RespConnection::new(TcpStream::connect(address).unwrap()).unwrap();
863 connection
864 .write(&RespValue::Array(Some(vec![
865 RespValue::Simple("OK".into()),
866 RespValue::Integer(42),
867 RespValue::Bulk(None),
868 RespValue::bulk("hello"),
869 ])))
870 .unwrap();
871 });
872 let (stream, _) = listener.accept().unwrap();
873 let mut connection = RespConnection::new(stream).unwrap();
874 assert_eq!(
875 connection.read().unwrap().unwrap(),
876 RespValue::Array(Some(vec![
877 RespValue::Simple("OK".into()),
878 RespValue::Integer(42),
879 RespValue::Bulk(None),
880 RespValue::bulk("hello"),
881 ]))
882 );
883 writer.join().unwrap();
884 }
885 #[test]
886 fn server_streams_protocol_four_and_shares_root_with_legacy_clients() {
887 let broker = crate::native_cli::RuntimeBroker::start().unwrap();
888 broker.eval("ROOT", "(def answer 41)").unwrap();
889 let mut server = super::RespServer::start("127.0.0.1", 0, broker).unwrap();
890 let endpoint = server.endpoint();
891
892 let mut legacy = RespConnection::new(TcpStream::connect(&endpoint).unwrap()).unwrap();
893 legacy
894 .write(&RespValue::array(["EVAL", "ROOT", "(+ answer 1)"]))
895 .unwrap();
896 assert_eq!(legacy.read().unwrap().unwrap().text().unwrap(), "42");
897 legacy
898 .write(&RespValue::array([
899 "EVAL",
900 "ROOT",
901 "(throw (ex :test/failed {:value 41}))",
902 ]))
903 .unwrap();
904 assert!(matches!(
905 legacy.read().unwrap().unwrap(),
906 RespValue::Error(_)
907 ));
908
909 let mut modern = RespConnection::new(TcpStream::connect(&endpoint).unwrap()).unwrap();
910 modern.write(&RespValue::array(["HELLO", "4"])).unwrap();
911 let hello = modern.read().unwrap().unwrap();
912 assert!(matches!(hello, RespValue::Array(Some(_))));
913 modern
914 .write(&RespValue::array(["EVAL", "REQ-1", "answer"]))
915 .unwrap();
916 assert_eq!(
917 modern.read().unwrap().unwrap(),
918 RespValue::array(["RESULT", "REQ-1", "41"])
919 );
920 assert_eq!(
921 modern.read().unwrap().unwrap(),
922 RespValue::array(["DONE", "REQ-1", "OK"])
923 );
924 modern
925 .write(&RespValue::array(["COMPLETE", "REQ-2", "ans"]))
926 .unwrap();
927 assert_eq!(
928 modern.read().unwrap().unwrap(),
929 RespValue::Array(Some(vec![
930 RespValue::bulk("RESULT"),
931 RespValue::bulk("REQ-2"),
932 RespValue::array(["answer"]),
933 ]))
934 );
935 assert_eq!(
936 modern.read().unwrap().unwrap(),
937 RespValue::array(["DONE", "REQ-2", "OK"])
938 );
939 modern
940 .write(&RespValue::array([
941 "EVAL",
942 "REQ-3",
943 concat!(
944 "(defn ^{:file \"/tmp/sample.hal\" :line 12 :column 3} located ",
945 "\"A located function.\" [value] value)"
946 ),
947 ]))
948 .unwrap();
949 modern.read().unwrap().unwrap();
950 modern.read().unwrap().unwrap();
951 modern
952 .write(&RespValue::array(["DOC", "REQ-4", "located"]))
953 .unwrap();
954 assert_eq!(
955 modern.read().unwrap().unwrap(),
956 RespValue::Array(Some(vec![
957 RespValue::bulk("RESULT"),
958 RespValue::bulk("REQ-4"),
959 RespValue::Array(Some(vec![
960 RespValue::bulk("SYMBOL"),
961 RespValue::bulk("located"),
962 RespValue::bulk("DOC"),
963 RespValue::bulk("A located function."),
964 RespValue::bulk("ARGLISTS"),
965 RespValue::Array(Some(vec![RespValue::array(["value"])])),
966 RespValue::bulk("FILE"),
967 RespValue::bulk("/tmp/sample.hal"),
968 RespValue::bulk("LINE"),
969 RespValue::Integer(12),
970 RespValue::bulk("COLUMN"),
971 RespValue::Integer(3),
972 ])),
973 ]))
974 );
975 assert_eq!(
976 modern.read().unwrap().unwrap(),
977 RespValue::array(["DONE", "REQ-4", "OK"])
978 );
979 server.stop();
980 }
981
982 #[test]
983 fn server_v4_error_carries_a_structured_evaluation_diagnostic() {
984 let broker = crate::native_cli::RuntimeBroker::start().unwrap();
985 let mut server = super::RespServer::start("127.0.0.1", 0, broker).unwrap();
986 let endpoint = server.endpoint();
987 let mut client = RespConnection::new(TcpStream::connect(&endpoint).unwrap()).unwrap();
988 client.write(&RespValue::array(["HELLO", "4"])).unwrap();
989 client.read().unwrap().unwrap();
990
991 let source = "(defn boom [] (throw (ex :test/failed {:value 41})))\n(boom)";
992 client
993 .write(&RespValue::array([
994 "EVAL",
995 "REQ-ERROR",
996 source,
997 "FILE",
998 "/tmp/request.hal",
999 "LINE",
1000 "10",
1001 "COLUMN",
1002 "5",
1003 ]))
1004 .unwrap();
1005 let error = client.read().unwrap().unwrap();
1006 let error_values = array(&error);
1007 assert_eq!(error_values.len(), 5);
1008 assert_eq!(error_values[0].text().as_deref(), Some("ERROR"));
1009 assert_eq!(error_values[1].text().as_deref(), Some("REQ-ERROR"));
1010 assert_eq!(error_values[2].text().as_deref(), Some("EVAL_ERROR"));
1011
1012 let diagnostic = array(&error_values[4]);
1013 assert_eq!(field(diagnostic, "VERSION"), &RespValue::Integer(1));
1014 let exception = array(field(diagnostic, "EXCEPTION"));
1015 assert_eq!(
1016 field(exception, "CODE").text().as_deref(),
1017 Some(":test/failed")
1018 );
1019 assert!(field(exception, "DATA")
1020 .text()
1021 .is_some_and(|data| data.contains(":value 41")));
1022 let primary = array(field(diagnostic, "PRIMARY"));
1023 assert_eq!(
1024 field(primary, "FILE").text().as_deref(),
1025 Some("/tmp/request.hal")
1026 );
1027 assert_eq!(field(primary, "LINE"), &RespValue::Integer(10));
1028 let excerpt = array(field(diagnostic, "EXCERPT"));
1029 assert_eq!(field(excerpt, "START-LINE"), &RespValue::Integer(10));
1030 assert!(field(excerpt, "TEXT")
1031 .text()
1032 .is_some_and(|text| text.contains("(boom)")));
1033 assert!(!array(field(diagnostic, "FRAMES")).is_empty());
1034 assert_eq!(
1035 client.read().unwrap().unwrap(),
1036 RespValue::array(["DONE", "REQ-ERROR", "ERROR"])
1037 );
1038 server.stop();
1039 }
1040
1041 #[test]
1042 fn server_v4_validation_errors_carry_a_clickable_evaluation_location() {
1043 let broker = crate::native_cli::RuntimeBroker::start().unwrap();
1044 let mut server = super::RespServer::start("127.0.0.1", 0, broker).unwrap();
1045 let endpoint = server.endpoint();
1046 let mut client = RespConnection::new(TcpStream::connect(&endpoint).unwrap()).unwrap();
1047 client.write(&RespValue::array(["HELLO", "4"])).unwrap();
1048 client.read().unwrap().unwrap();
1049
1050 client
1051 .write(&RespValue::array([
1052 "EVAL",
1053 "REQ-VALIDATION-ERROR",
1054 "(ex :unknown {})",
1055 "FILE",
1056 "/tmp/validation.hal",
1057 "LINE",
1058 "12",
1059 "COLUMN",
1060 "3",
1061 ]))
1062 .unwrap();
1063 let error = client.read().unwrap().unwrap();
1064 let error_values = array(&error);
1065 assert_eq!(error_values.len(), 5);
1066 assert_eq!(error_values[2].text().as_deref(), Some("EVAL_ERROR"));
1067 let diagnostic = array(&error_values[4]);
1068 let primary = array(field(diagnostic, "PRIMARY"));
1069 assert_eq!(
1070 field(primary, "FILE").text().as_deref(),
1071 Some("/tmp/validation.hal")
1072 );
1073 assert_eq!(field(primary, "LINE"), &RespValue::Integer(12));
1074 assert_eq!(field(primary, "COLUMN"), &RespValue::Integer(3));
1075 assert!(field(array(field(diagnostic, "EXCERPT")), "TEXT")
1076 .text()
1077 .is_some_and(|text| text.contains("(ex :unknown {})")));
1078 assert!(!array(field(diagnostic, "FRAMES")).is_empty());
1079 assert_eq!(
1080 client.read().unwrap().unwrap(),
1081 RespValue::array(["DONE", "REQ-VALIDATION-ERROR", "ERROR"])
1082 );
1083 server.stop();
1084 }
1085}