use itertools::Itertools;
use std::{
io::{self, Write},
process::{Command, Output, Stdio},
};
const DEFAULT_MAX_ERROR_LINES: usize = 10;
pub(crate) struct ThirdPartyTools;
impl ThirdPartyTools {
pub(crate) fn pandoc(args: &[&str]) -> Tool {
Tool::new("pandoc", args)
}
pub(crate) fn typst(args: &[&str]) -> Tool {
Tool::new("typst", args)
}
pub(crate) fn mermaid(args: &[&str]) -> Tool {
let mmdc = if cfg!(windows) { "mmdc.cmd" } else { "mmdc" };
Tool::new(mmdc, args)
}
pub(crate) fn d2(args: &[&str]) -> Tool {
Tool::new("d2", args)
}
pub(crate) fn weasyprint(args: &[&str]) -> Tool {
Tool::new("weasyprint", args).inherit_stdout().max_error_lines(100)
}
}
pub(crate) struct Tool {
command_name: &'static str,
command: Command,
stdin: Option<Vec<u8>>,
max_error_lines: usize,
}
impl Tool {
fn new(command_name: &'static str, args: &[&str]) -> Self {
let mut command = Command::new(command_name);
command.args(args).stdin(Stdio::null()).stdout(Stdio::null()).stderr(Stdio::piped());
Self { command_name, command, stdin: None, max_error_lines: DEFAULT_MAX_ERROR_LINES }
}
pub(crate) fn stdin(mut self, stdin: Vec<u8>) -> Self {
self.stdin = Some(stdin);
self
}
pub(crate) fn inherit_stdout(mut self) -> Self {
self.command.stdout(Stdio::inherit());
self
}
pub(crate) fn max_error_lines(mut self, value: usize) -> Self {
self.max_error_lines = value;
self
}
pub(crate) fn run(self) -> Result<(), ExecutionError> {
self.spawn()?;
Ok(())
}
pub(crate) fn run_and_capture_stdout(mut self) -> Result<Vec<u8>, ExecutionError> {
self.command.stdout(Stdio::piped());
let output = self.spawn()?;
Ok(output.stdout)
}
fn spawn(mut self) -> Result<Output, ExecutionError> {
use ExecutionError::*;
if self.stdin.is_some() {
self.command.stdin(Stdio::piped());
}
let mut child = match self.command.spawn() {
Ok(child) => child,
Err(e) if e.kind() == io::ErrorKind::NotFound => return Err(SpawnNotFound { command: self.command_name }),
Err(error) => return Err(Spawn { command: self.command_name, error }),
};
if let Some(data) = &self.stdin {
let mut stdin = child.stdin.take().expect("no stdin");
stdin
.write_all(data)
.and_then(|_| stdin.flush())
.map_err(|error| Communication { command: self.command_name, error })?;
}
let output = child.wait_with_output().map_err(|error| Communication { command: self.command_name, error })?;
self.validate_output(&output)?;
Ok(output)
}
fn validate_output(self, output: &Output) -> Result<(), ExecutionError> {
if output.status.success() {
Ok(())
} else {
let stderr = String::from_utf8_lossy(&output.stderr).lines().take(self.max_error_lines).join("\n");
Err(ExecutionError::Execution { command: self.command_name, stderr })
}
}
}
#[derive(Debug, thiserror::Error)]
pub enum ExecutionError {
#[error("spawning '{command}' failed: {error}")]
Spawn { command: &'static str, error: io::Error },
#[error("spawning '{command}' failed (is '{command}' installed?)")]
SpawnNotFound { command: &'static str },
#[error("communicating with '{command}' failed: {error}")]
Communication { command: &'static str, error: io::Error },
#[error("'{command}' execution failed: \n{stderr}")]
Execution { command: &'static str, stderr: String },
}