1use serde::Deserialize;
2use std::io::Write;
3use std::process::{Command, Stdio};
4use thiserror::Error;
5
6use crate::sync::find_julie_extract_binary;
7
8#[derive(Debug, Error, PartialEq)]
9pub enum SyntaxError {
10 #[error("Syntax error in {0}: {1}")]
11 ParseError(String, String),
12 #[error("Syntax check could not run: {0}")]
13 CheckFailed(String),
14}
15
16#[derive(Deserialize)]
17struct CheckReport {
18 status: String,
19 #[serde(default)]
20 errors: Vec<CheckDiagnostic>,
21}
22
23#[derive(Deserialize)]
24struct CheckDiagnostic {
25 message: String,
26}
27
28pub fn validate_syntax(file_path: &str, content: &str) -> Result<bool, SyntaxError> {
32 let bin = find_julie_extract_binary()
33 .ok_or_else(|| SyntaxError::CheckFailed("julie-extract binary not found".to_string()))?;
34 let mut child = Command::new(bin)
35 .args(["check", "--path", file_path, "--json"])
36 .stdin(Stdio::piped())
37 .stdout(Stdio::piped())
38 .stderr(Stdio::null())
39 .spawn()
40 .map_err(|e| SyntaxError::CheckFailed(e.to_string()))?;
41 let written = child
42 .stdin
43 .take()
44 .expect("stdin was piped")
45 .write_all(content.as_bytes());
46 let output = child
47 .wait_with_output()
48 .map_err(|e| SyntaxError::CheckFailed(e.to_string()))?;
49 written.map_err(|e| SyntaxError::CheckFailed(format!("could not send content: {e}")))?;
50 let report: CheckReport = serde_json::from_slice(&output.stdout)
51 .map_err(|e| SyntaxError::CheckFailed(format!("unreadable check report: {e}")))?;
52
53 match report.status.as_str() {
54 "ok" => Ok(true),
55 "unsupported" => Ok(false),
56 _ => {
57 let first = report
58 .errors
59 .first()
60 .map(|e| e.message.clone())
61 .unwrap_or_else(|| "syntax error".to_string());
62 let more = report.errors.len().saturating_sub(1);
63 let detail = if more > 0 {
64 format!("{first} (+{more} more)")
65 } else {
66 first
67 };
68 Err(SyntaxError::ParseError(file_path.to_string(), detail))
69 }
70 }
71}