Skip to main content

runx_cli/
parser.rs

1use std::collections::BTreeMap;
2use std::env;
3use std::fmt;
4use std::fs;
5use std::io::{self, Read};
6use std::path::{Path, PathBuf};
7use std::process::ExitCode;
8
9use runx_runtime::{ParserEvalError, ParserEvalOutput, evaluate_parser_document_str};
10use serde::Serialize;
11
12#[derive(Clone, Debug, Eq, PartialEq)]
13pub struct ParserPlan {
14    pub input: ParserInputSource,
15    pub json: bool,
16}
17
18#[derive(Clone, Debug, Eq, PartialEq)]
19pub enum ParserInputSource {
20    Path(PathBuf),
21    Stdin,
22}
23
24pub fn run_native_parser(plan: ParserPlan) -> ExitCode {
25    let cwd = match env::current_dir() {
26        Ok(cwd) => cwd,
27        Err(error) => {
28            let error = ParserCliError::CurrentDirectory(error);
29            return write_error(&error, plan.json);
30        }
31    };
32
33    match run_parser_command(&plan, &crate::cli_io::env_map(), &cwd) {
34        Ok(output) => crate::cli_io::write_stdout_code(&output.stdout, output.exit_code),
35        Err(error) => write_error(&error, plan.json),
36    }
37}
38
39pub fn run_parser_command(
40    plan: &ParserPlan,
41    env: &BTreeMap<String, String>,
42    cwd: &Path,
43) -> Result<ParserCliOutput, ParserCliError> {
44    if !plan.json {
45        return Err(ParserCliError::InvalidArgs(
46            "runx parser eval requires --json".to_owned(),
47        ));
48    }
49
50    let raw = read_parser_input(&plan.input, env, cwd)?;
51    let result = evaluate_parser_document_str(&raw)?;
52    let stdout = serde_json::to_string_pretty(&ParserJsonEnvelope {
53        status: "success",
54        result: &result,
55    })
56    .map(|json| format!("{json}\n"))
57    .map_err(ParserCliError::Serialize)?;
58    Ok(ParserCliOutput {
59        stdout,
60        exit_code: 0,
61    })
62}
63
64#[derive(Debug)]
65pub struct ParserCliOutput {
66    pub stdout: String,
67    pub exit_code: u8,
68}
69
70#[derive(Debug)]
71pub enum ParserCliError {
72    CurrentDirectory(io::Error),
73    InvalidArgs(String),
74    Read(PathBuf, io::Error),
75    ReadStdin(io::Error),
76    Eval(ParserEvalError),
77    Serialize(serde_json::Error),
78}
79
80impl ParserCliError {
81    fn code(&self) -> &'static str {
82        match self {
83            Self::CurrentDirectory(_) => "current_directory",
84            Self::InvalidArgs(_) => "invalid_args",
85            Self::Read(_, _) => "read_input",
86            Self::ReadStdin(_) => "read_stdin",
87            Self::Eval(error) => error.code(),
88            Self::Serialize(_) => "serialize_output",
89        }
90    }
91
92    fn exit_code(&self) -> u8 {
93        match self {
94            Self::InvalidArgs(_) => 64,
95            Self::CurrentDirectory(_)
96            | Self::Read(_, _)
97            | Self::ReadStdin(_)
98            | Self::Eval(_)
99            | Self::Serialize(_) => 1,
100        }
101    }
102}
103
104impl fmt::Display for ParserCliError {
105    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
106        match self {
107            Self::CurrentDirectory(error) => write!(formatter, "failed to resolve cwd: {error}"),
108            Self::InvalidArgs(message) => formatter.write_str(message),
109            Self::Read(path, error) => {
110                write!(
111                    formatter,
112                    "failed to read parser input {}: {error}",
113                    path.display()
114                )
115            }
116            Self::ReadStdin(error) => {
117                write!(formatter, "failed to read parser input stdin: {error}")
118            }
119            Self::Eval(error) => write!(formatter, "{error}"),
120            Self::Serialize(error) => {
121                write!(formatter, "failed to serialize parser result: {error}")
122            }
123        }
124    }
125}
126
127impl std::error::Error for ParserCliError {}
128
129impl From<ParserEvalError> for ParserCliError {
130    fn from(error: ParserEvalError) -> Self {
131        Self::Eval(error)
132    }
133}
134
135#[derive(Serialize)]
136struct ParserJsonEnvelope<'a> {
137    status: &'static str,
138    result: &'a ParserEvalOutput,
139}
140
141#[derive(Serialize)]
142struct ParserJsonError<'a> {
143    status: &'static str,
144    code: &'static str,
145    message: &'a str,
146}
147
148fn read_parser_input(
149    source: &ParserInputSource,
150    env: &BTreeMap<String, String>,
151    cwd: &Path,
152) -> Result<String, ParserCliError> {
153    match source {
154        ParserInputSource::Path(path) => {
155            let resolved = resolve_parser_path(path, env, cwd);
156            fs::read_to_string(&resolved).map_err(|error| ParserCliError::Read(resolved, error))
157        }
158        ParserInputSource::Stdin => {
159            let mut raw = String::new();
160            io::stdin()
161                .read_to_string(&mut raw)
162                .map_err(ParserCliError::ReadStdin)?;
163            Ok(raw)
164        }
165    }
166}
167
168fn resolve_parser_path(path: &Path, env: &BTreeMap<String, String>, cwd: &Path) -> PathBuf {
169    if path.is_absolute() {
170        return path.to_path_buf();
171    }
172    env.get("RUNX_CWD")
173        .map(PathBuf::from)
174        .or_else(|| env.get("INIT_CWD").map(PathBuf::from))
175        .unwrap_or_else(|| cwd.to_path_buf())
176        .join(path)
177}
178
179fn write_error(error: &ParserCliError, json: bool) -> ExitCode {
180    if json {
181        let message = error.to_string();
182        match serde_json::to_string_pretty(&ParserJsonError {
183            status: "error",
184            code: error.code(),
185            message: &message,
186        }) {
187            Ok(body) => {
188                return crate::cli_io::write_stdout_code(&format!("{body}\n"), error.exit_code());
189            }
190            Err(serialize_error) => {
191                let _ignored = crate::cli_io::write_stderr_code(&format!(
192                    "runx: failed to serialize parser error: {serialize_error}\n"
193                ));
194                return ExitCode::from(1);
195            }
196        }
197    }
198
199    let _ignored = crate::cli_io::write_stderr_code(&format!("runx: {error}\n"));
200    ExitCode::from(error.exit_code())
201}