use std::io::{Read, Write};
pub use crate::wire::{DOC_VERSION, Doc, Finding, Format, HostParse, Lint};
use serde::{Deserialize, Serialize};
pub trait Handler {
fn init(&mut self, config: &str, rules: &str, settings: &Settings) -> Result<(), String> {
let _ = (config, rules, settings);
Ok(())
}
fn transform(&mut self, source: &str) -> Result<String, String> {
let _ = source;
Err("this worm does not transform".into())
}
fn format(&mut self, source: &str) -> Result<Format, String> {
let _ = source;
Err("this worm does not format".into())
}
fn lint(&mut self, source: &str) -> Result<Lint, String> {
let _ = source;
Err("this worm does not lint".into())
}
fn manifest(&self) -> Option<&'static str> {
None
}
fn rules(&mut self, source: &str, rules: &[RuleCall]) -> Result<Vec<Edit>, String> {
let _ = (source, rules);
Err("this worm does not run rules".into())
}
}
pub type Edit = (u32, u32, String);
#[derive(Debug, Clone, Deserialize)]
pub struct RuleCall {
pub name: String,
pub nodes: Vec<WireNode>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct WireNode {
pub id: u32,
pub kind: String,
pub span: (u32, u32),
}
#[derive(Debug, Clone, Default)]
pub struct Settings {
pub fmt: String,
pub lint: String,
}
#[derive(Deserialize)]
#[serde(tag = "op", rename_all = "snake_case")]
enum Request {
Init {
#[serde(default)]
config: String,
#[serde(default)]
rules: String,
#[serde(default)]
doc_version: u32,
#[serde(default)]
fmt: String,
#[serde(default)]
lint: String,
},
Transform {
source: String,
},
Format {
source: String,
},
Lint {
source: String,
},
Rules {
source: String,
#[serde(default)]
rules: Vec<RuleCall>,
},
Manifest,
}
pub fn serve(mut handler: impl Handler) {
let stdin = std::io::stdin();
let stdout = std::io::stdout();
let mut input = stdin.lock();
let mut output = stdout.lock();
loop {
let Some(body) = read_frame(&mut input) else {
return;
};
let reply = match serde_json::from_slice::<Request>(&body) {
Ok(request) => answer(&mut handler, request),
Err(e) => error_reply(format!("cannot read the request, {e}")),
};
write_frame(&mut output, &reply);
}
}
fn answer(handler: &mut impl Handler, request: Request) -> Vec<u8> {
let reply = match request {
Request::Init {
config,
rules,
doc_version,
fmt,
lint,
} => {
if doc_version != 0 && doc_version != DOC_VERSION {
return error_reply(format!(
"this worm speaks doc v{DOC_VERSION}, larvae speaks v{doc_version}"
));
}
handler
.init(&config, &rules, &Settings { fmt, lint })
.map(|()| serde_json::json!({ "ok": true }))
}
Request::Transform { source } => handler
.transform(&source)
.map(|output| serde_json::json!({ "ok": true, "output": output })),
Request::Format { source } => handler.format(&source).map(|format| {
serde_json::json!({
"ok": true,
"doc": DOC_VERSION,
"document": format.document,
"spans": format.spans,
"comments": format.comments,
})
}),
Request::Lint { source } => handler.lint(&source).map(|lint| {
serde_json::json!({
"ok": true,
"findings": lint.findings,
"comments": lint.comments,
"luau": lint.luau,
})
}),
Request::Rules { source, rules } => handler
.rules(&source, &rules)
.map(|edits| serde_json::json!({ "ok": true, "edits": edits })),
Request::Manifest => match handler.manifest() {
Some(text) => Ok(serde_json::json!({ "ok": true, "manifest": text })),
None => Err("this worm does not carry its manifest".into()),
},
};
match reply {
Ok(value) => serde_json::to_vec(&value).expect("a reply always serialises"),
Err(why) => error_reply(why),
}
}
fn error_reply(why: String) -> Vec<u8> {
serde_json::to_vec(&serde_json::json!({ "ok": false, "error": why }))
.expect("a reply always serialises")
}
fn read_frame(input: &mut impl Read) -> Option<Vec<u8>> {
let mut len = [0u8; 4];
input.read_exact(&mut len).ok()?;
let mut body = vec![0u8; u32::from_le_bytes(len) as usize];
input.read_exact(&mut body).ok()?;
Some(body)
}
fn write_frame(output: &mut impl Write, body: &[u8]) {
let len = u32::try_from(body.len()).expect("a reply under 4GB");
let _ = output.write_all(&len.to_le_bytes());
let _ = output.write_all(body);
let _ = output.flush();
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_wire_doc_shape_matches_the_host() {
let doc = Doc::concat([
Doc::Nil,
Doc::src(0, 4),
Doc::lit("<"),
Doc::Line,
Doc::if_break(Doc::Nil, Doc::Hard),
Doc::group(Doc::indent(Doc::host_expr(8, 12))),
]);
assert_eq!(
serde_json::to_string(&doc).unwrap(),
r#"{"concat":["nil",{"src":[0,4]},{"lit":"<"},"line",{"if_break":["nil","hard"]},{"group":{"indent":{"host":{"start":8,"end":12,"parse":"expr"}}}}]}"#
);
}
#[test]
fn a_finding_serialises_without_a_null_help() {
let finding = Finding::new("tidy", (2, 7), "untidy");
assert_eq!(
serde_json::to_string(&finding).unwrap(),
r#"{"span":[2,7],"lint":"tidy","message":"untidy"}"#
);
let helped = finding.with_help("do less");
assert!(serde_json::to_string(&helped).unwrap().contains("do less"));
}
struct Echo;
impl Handler for Echo {
fn transform(&mut self, source: &str) -> Result<String, String> {
Ok(source.to_uppercase())
}
}
fn frame(json: &str) -> Vec<u8> {
let mut out = (json.len() as u32).to_le_bytes().to_vec();
out.extend_from_slice(json.as_bytes());
out
}
#[test]
fn a_transform_round_trips_through_answer() {
let body = frame(r#"{"op":"transform","source":"hi"}"#);
let request: Request = serde_json::from_slice(&body[4..]).unwrap();
let reply = answer(&mut Echo, request);
assert_eq!(
String::from_utf8(reply).unwrap(),
r#"{"ok":true,"output":"HI"}"#
);
}
#[test]
fn an_undeclared_op_refuses_rather_than_panics() {
let request: Request = serde_json::from_slice(br#"{"op":"format","source":"x"}"#).unwrap();
let reply = String::from_utf8(answer(&mut Echo, request)).unwrap();
assert!(reply.contains(r#""ok":false"#), "{reply}");
assert!(reply.contains("does not format"), "{reply}");
}
#[test]
fn a_doc_version_mismatch_is_refused_at_init() {
let request: Request =
serde_json::from_slice(br#"{"op":"init","config":"","rules":"","doc_version":9}"#)
.unwrap();
let reply = String::from_utf8(answer(&mut Echo, request)).unwrap();
assert!(reply.contains("doc v1"), "{reply}");
}
struct Shout;
impl Handler for Shout {
fn rules(&mut self, source: &str, rules: &[RuleCall]) -> Result<Vec<Edit>, String> {
Ok(rules
.iter()
.flat_map(|call| &call.nodes)
.map(|node| {
let (start, end) = node.span;
let text = source[start as usize..end as usize].to_uppercase();
(start, end, text)
})
.collect())
}
}
#[test]
fn a_rules_request_round_trips_through_answer() {
let request: Request = serde_json::from_slice(
br#"{"op":"rules","source":"hi there","rules":[{"name":"up","nodes":[{"id":1,"kind":"Name","span":[0,2]},{"id":2,"kind":"Name","span":[3,8]}]}]}"#,
)
.unwrap();
let reply = String::from_utf8(answer(&mut Shout, request)).unwrap();
assert_eq!(reply, r#"{"edits":[[0,2,"HI"],[3,8,"THERE"]],"ok":true}"#);
}
#[test]
fn an_undeclared_rules_op_refuses_rather_than_panics() {
let request: Request =
serde_json::from_slice(br#"{"op":"rules","source":"x","rules":[]}"#).unwrap();
let reply = String::from_utf8(answer(&mut Echo, request)).unwrap();
assert!(reply.contains(r#""ok":false"#), "{reply}");
assert!(reply.contains("does not run rules"), "{reply}");
}
}