use std::io::{self, BufWriter, Read, Write};
use std::process::ExitCode;
use serde::Serialize;
use crate::eval::{Evaluator, LineResult, format_number};
use crate::lang::parse_all;
use crate::storage::split_lines;
#[derive(Debug, Serialize)]
struct Record<'a> {
line: usize,
value: Option<f64>,
text: &'a str,
error: Option<String>,
}
pub fn eval_json_lines(input: &str) -> Vec<String> {
let lines = split_lines(input);
let results = Evaluator::new().evaluate_all(&parse_all(&lines));
results
.iter()
.enumerate()
.map(|(i, r)| record_json(i + 1, r))
.collect()
}
fn record_json(line: usize, result: &LineResult) -> String {
let text = match (&result.error, result.value) {
(Some(e), _) => e.short(),
(None, Some(v)) => format_number(v),
(None, None) => String::new(),
};
let record = Record {
line,
value: result
.value
.filter(|v| v.is_finite() && result.error.is_none()),
text: &text,
error: result.error.as_ref().map(ToString::to_string),
};
serde_json::to_string(&record)
.unwrap_or_else(|_| format!(r#"{{"line":{line},"value":null,"text":"","error":null}}"#))
}
pub fn run() -> ExitCode {
let mut input = Vec::new();
if let Err(e) = io::stdin().lock().read_to_end(&mut input) {
eprintln!("nm eval: reading stdin: {e}");
return ExitCode::FAILURE;
}
let input = String::from_utf8_lossy(&input);
match write_lines(&mut io::stdout().lock(), &eval_json_lines(&input)) {
Ok(()) => ExitCode::SUCCESS,
Err(e) if e.kind() == io::ErrorKind::BrokenPipe => ExitCode::SUCCESS,
Err(e) => {
eprintln!("nm eval: writing stdout: {e}");
ExitCode::FAILURE
}
}
}
fn write_lines(out: &mut impl Write, lines: &[String]) -> io::Result<()> {
let mut out = BufWriter::new(out);
for line in lines {
writeln!(out, "{line}")?;
}
out.flush()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn value_line() {
assert_eq!(
eval_json_lines("income=5000\n"),
[r#"{"line":1,"value":5000.0,"text":"5,000","error":null}"#]
);
}
#[test]
fn error_line() {
assert_eq!(
eval_json_lines("1\n2\nx+1\n")[2],
r#"{"line":3,"value":null,"text":"undef: x","error":"undefined variable: x"}"#
);
assert_eq!(
eval_json_lines("1/0")[0],
r#"{"line":1,"value":null,"text":"div/0","error":"division by zero"}"#
);
}
#[test]
fn no_result_lines() {
let empty = r#"{"line":1,"value":null,"text":"","error":null}"#;
let comment = r#"{"line":2,"value":null,"text":"","error":null}"#;
assert_eq!(eval_json_lines("\n# just a note\n"), [empty, comment]);
}
#[test]
fn fractions_and_crlf() {
assert_eq!(
eval_json_lines("1.5\r\n0.1+0.2\r\n"),
[
r#"{"line":1,"value":1.5,"text":"1.5","error":null}"#,
r#"{"line":2,"value":0.30000000000000004,"text":"0.3","error":null}"#,
]
);
}
#[test]
fn empty_input_prints_nothing() {
assert!(eval_json_lines("").is_empty());
}
#[test]
fn strings_are_escaped() {
let out = eval_json_lines("\"q\\\n");
assert_eq!(out.len(), 1);
let v: serde_json::Value = serde_json::from_str(&out[0]).unwrap();
assert_eq!(v["line"], 1);
}
#[test]
fn write_lines_appends_newlines() {
let mut buf = Vec::new();
write_lines(&mut buf, &["a".into(), "b".into()]).unwrap();
assert_eq!(buf, b"a\nb\n");
}
}