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}