use std::fs;
use std::io::{self, Read, Write};
use std::path::{Path, PathBuf};
use std::process::ExitCode;
use clap::Parser;
use miette::{NamedSource, Report};
use noya_cli::NoyavalidateCli;
fn read_input(path: Option<&Path>) -> io::Result<(String, String)> {
match path {
None => {
let mut buf = String::new();
let _ = io::stdin().read_to_string(&mut buf)?;
Ok(("<stdin>".to_string(), buf))
}
Some(p) => {
let source = fs::read_to_string(p)?;
Ok((p.display().to_string(), source))
}
}
}
fn read_schema(path: &Path) -> io::Result<String> {
fs::read_to_string(path)
}
fn run_schema_validation(
docs: &[noyalib::Value],
schema_text: &str,
schema_path_label: &str,
source_label: &str,
full_source: &str,
) -> usize {
let schema: noyalib::Value = match noyalib::from_str(schema_text) {
Ok(v) => v,
Err(e) => {
let report = Report::new(e)
.with_source_code(NamedSource::new(schema_path_label, schema_text.to_owned()));
eprintln!("error: parsing schema:");
eprintln!("{report:?}");
return 1;
}
};
let mut violations = 0;
for (i, doc) in docs.iter().enumerate() {
if let Err(e) = noyalib::validate_against_schema(doc, &schema) {
violations += 1;
if docs.len() > 1 {
eprintln!("[document {}]", i + 1);
}
let report = Report::new(e)
.with_source_code(NamedSource::new(source_label, full_source.to_owned()));
eprintln!("{report:?}");
}
}
violations
}
fn run_fix(path: Option<&Path>, source: &str) -> io::Result<()> {
let formatted = match noyalib::cst::format(source) {
Ok(s) => s,
Err(e) => {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!("--fix: formatter rejected the input: {e}"),
));
}
};
match path {
None => {
let mut stdout = io::stdout().lock();
stdout.write_all(formatted.as_bytes())?;
}
Some(p) => fs::write(p, formatted.as_bytes())?,
}
Ok(())
}
struct FixOutcome {
applied: usize,
wrote: bool,
}
fn run_fix_with_schema(
path: Option<&Path>,
source: &str,
schema: &noyalib::Value,
) -> io::Result<FixOutcome> {
let mut docs = match noyalib::cst::parse_stream(source) {
Ok(d) => d,
Err(e) => {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!("--fix: parse stream: {e}"),
));
}
};
let mut applied = 0usize;
for cst_doc in docs.iter_mut() {
match noyalib::cst::coerce_to_schema(cst_doc, schema) {
Ok(n) => applied += n,
Err(e) => {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!("--fix: cst::coerce_to_schema failed: {e}"),
));
}
}
}
let still_invalid = docs.iter().any(|cst_doc| {
match noyalib::from_str::<noyalib::Value>(&cst_doc.to_string()) {
Ok(v) => noyalib::validate_against_schema(&v, schema).is_err(),
Err(_) => true,
}
});
if still_invalid {
return Ok(FixOutcome {
applied,
wrote: false,
});
}
let mut output = String::with_capacity(source.len());
for cst_doc in &docs {
output.push_str(&cst_doc.to_string());
}
let final_output = noyalib::cst::format(&output).unwrap_or(output);
match path {
None => {
let mut stdout = io::stdout().lock();
stdout.write_all(final_output.as_bytes())?;
}
Some(p) => fs::write(p, final_output.as_bytes())?,
}
Ok(FixOutcome {
applied,
wrote: true,
})
}
fn run() -> ExitCode {
let args = NoyavalidateCli::parse();
let path: Option<PathBuf> = match args.file {
Some(ref p) if p.as_os_str() == "-" => None,
other => other,
};
let (name, source) = match read_input(path.as_deref()) {
Ok(pair) => pair,
Err(e) => {
eprintln!("error: reading input: {e}");
return ExitCode::from(3);
}
};
let docs = match noyalib::load_all_as::<noyalib::Value>(&source) {
Ok(d) => d,
Err(e) => {
let report = Report::new(e).with_source_code(NamedSource::new(name, source.clone()));
eprintln!("{report:?}");
return ExitCode::from(1);
}
};
let mut total_fixes_via_coerce: usize = 0;
let mut fix_handled_via_schema_path = false;
if let Some(schema_path) = args.schema.as_deref() {
let schema_text = match read_schema(schema_path) {
Ok(t) => t,
Err(e) => {
eprintln!("error: reading schema {}: {e}", schema_path.display());
return ExitCode::from(3);
}
};
let schema: noyalib::Value = match noyalib::from_str(&schema_text) {
Ok(s) => s,
Err(e) => {
let report = Report::new(e).with_source_code(NamedSource::new(
schema_path.display().to_string(),
schema_text.clone(),
));
eprintln!("error: parsing schema:");
eprintln!("{report:?}");
return ExitCode::from(1);
}
};
if args.fix {
let outcome = match run_fix_with_schema(path.as_deref(), &source, &schema) {
Ok(o) => o,
Err(e) => {
eprintln!("error: applying --fix: {e}");
let code = if e.kind() == io::ErrorKind::InvalidData {
1
} else {
3
};
return ExitCode::from(code);
}
};
total_fixes_via_coerce = outcome.applied;
fix_handled_via_schema_path = true;
if !outcome.wrote {
let label = schema_path.display().to_string();
let _ = run_schema_validation(&docs, &schema_text, &label, &name, &source);
return ExitCode::from(1);
}
} else {
let label = schema_path.display().to_string();
let violations = run_schema_validation(&docs, &schema_text, &label, &name, &source);
if violations > 0 {
return ExitCode::from(1);
}
}
}
if args.fix && !fix_handled_via_schema_path {
if let Err(e) = run_fix(path.as_deref(), &source) {
eprintln!("error: applying --fix: {e}");
let code = if e.kind() == io::ErrorKind::InvalidData {
1
} else {
3
};
return ExitCode::from(code);
}
}
let stdin_fix = args.fix && path.is_none();
if !args.quiet && !stdin_fix {
let n = docs.len();
let plural = if n == 1 { "document" } else { "documents" };
let suffix = match (args.schema.is_some(), args.fix) {
(true, true) => {
if total_fixes_via_coerce == 0 {
" (schema-checked, no fixes needed)".to_string()
} else {
format!(" (schema-checked, {total_fixes_via_coerce} fix(es) applied)")
}
}
(true, false) => " (schema-checked)".to_string(),
(false, true) => " (fixed)".to_string(),
(false, false) => String::new(),
};
println!("ok: {n} {plural} valid ({name}){suffix}");
}
ExitCode::from(0)
}
fn main() -> ExitCode {
run()
}