use std::io::{Read, Write};
use serde::{Deserialize, Serialize};
pub const DOC_VERSION: u32 = 1;
#[derive(Debug, Clone, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum Doc {
Nil,
Src(u32, u32),
Lit(String),
Line,
Soft,
Hard,
Blank,
IfBreak(Box<Doc>, Box<Doc>),
Group(Box<Doc>),
Indent(Box<Doc>),
Concat(Vec<Doc>),
Host {
start: u32,
end: u32,
parse: HostParse,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum HostParse {
Block,
Expr,
}
impl Doc {
pub fn src(start: u32, end: u32) -> Self {
Self::Src(start, end)
}
pub fn lit(s: impl Into<String>) -> Self {
Self::Lit(s.into())
}
pub fn group(inner: Doc) -> Self {
Self::Group(Box::new(inner))
}
pub fn indent(inner: Doc) -> Self {
Self::Indent(Box::new(inner))
}
pub fn if_break(flat: Doc, broken: Doc) -> Self {
Self::IfBreak(Box::new(flat), Box::new(broken))
}
pub fn concat(parts: impl IntoIterator<Item = Doc>) -> Self {
Self::Concat(parts.into_iter().collect())
}
pub fn join(sep: Doc, parts: impl IntoIterator<Item = Doc>) -> Self {
let mut out = Vec::new();
for (i, part) in parts.into_iter().enumerate() {
if i > 0 {
out.push(sep.clone());
}
out.push(part);
}
Self::Concat(out)
}
pub fn host(start: u32, end: u32) -> Self {
Self::Host {
start,
end,
parse: HostParse::Block,
}
}
pub fn host_expr(start: u32, end: u32) -> Self {
Self::Host {
start,
end,
parse: HostParse::Expr,
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct Finding {
pub span: (u32, u32),
pub lint: String,
pub message: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub help: Option<String>,
}
impl Finding {
pub fn new(lint: impl Into<String>, span: (u32, u32), message: impl Into<String>) -> Self {
Self {
span,
lint: lint.into(),
message: message.into(),
help: None,
}
}
pub fn with_help(mut self, help: impl Into<String>) -> Self {
self.help = Some(help.into());
self
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Format {
pub document: Option<Doc>,
pub spans: Vec<(u32, u32)>,
pub comments: Vec<(u32, u32)>,
}
impl Format {
pub fn document(document: Doc) -> Self {
Self {
document: Some(document),
spans: Vec::new(),
comments: Vec::new(),
}
}
pub fn spans(spans: Vec<(u32, u32)>) -> Self {
Self {
document: None,
spans,
comments: Vec::new(),
}
}
pub fn with_comments(mut self, comments: Vec<(u32, u32)>) -> Self {
self.comments = comments;
self
}
}
#[derive(Debug, Clone, PartialEq, Default)]
pub struct Lint {
pub findings: Vec<Finding>,
pub luau: Option<String>,
pub comments: Vec<(u32, u32)>,
}
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())
}
}
#[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,
},
}
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,
})
}),
};
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}");
}
}