use std::{fmt, io::IsTerminal as _, path::PathBuf};
use clap::{Args, Parser};
use console::style;
use ocpi_tariffs::{
json::{self, PathRef},
tariff, warning,
};
use crate::{
load_object_file, load_object_from_stdin,
opts::{FilePath, Version},
Error, ObjectKind,
};
const STDIN_NAME: &str = "<stdin>";
#[derive(Parser)]
pub struct Command {
#[command(flatten)]
args: Arguments,
}
#[derive(Args)]
struct Arguments {
#[arg(short = 'o', long, value_enum)]
ocpi_version: Version,
#[arg(long)]
pretty: bool,
#[arg(short = 'f', long)]
file: Option<PathBuf>,
}
impl Command {
pub fn run(self) -> Result<(), Error> {
let Self {
args:
Arguments {
ocpi_version,
pretty,
file: tariff,
},
} = self;
let ocpi_version = ocpi_version.into();
let tariff_file_path = tariff.as_deref().map(FilePath::from_path).transpose()?;
let tariff_name = tariff_file_path
.as_ref()
.map(FilePath::file_name)
.unwrap_or_else(|| STDIN_NAME);
eprintln!(
"{} tariff `{}` as version `{}`",
style("Linting").green().bold(),
style(&tariff_name).blue(),
style(ocpi_version).blue()
);
let tariff_json = if let Some(path) = tariff_file_path.as_ref() {
load_object_file(path, ObjectKind::Tariff)?
} else if std::io::stdin().is_terminal() {
return Err(Error::TariffRequired);
} else {
load_object_from_stdin(ObjectKind::Tariff)?
};
let report = tariff::parse_with_version(&tariff_json, ocpi_version)?;
let tariff::ParseReport {
tariff,
unexpected_fields,
} = report;
if pretty {
eprintln!("{}", style("# Source Tariff:").green());
eprintln!("{}\n", json::write::Pretty::new(tariff.as_element()));
}
let report = tariff::lint(&tariff);
eprintln!("Completed {} tariff\n", style("linting").green().bold());
print_unexpected_fields(unexpected_fields);
print_report(tariff_file_path.as_ref(), &tariff_json, report);
Ok(())
}
}
fn print_report(
file_path: Option<&FilePath>,
tariff_json: &str,
report: ocpi_tariffs::lint::tariff::Report,
) {
const INDENT: &str = " ";
let tariff_name = file_path
.as_ref()
.map(|path| path.dir_and_name())
.unwrap_or_else(|| STDIN_NAME.into());
let ocpi_tariffs::lint::tariff::Report { warnings } = report;
if !warnings.is_empty() {
eprintln!("{}\n", style("# Warnings").yellow());
}
let list_item = style("*").yellow();
let block = style("|").yellow();
let report_iter = warning::SourceReportIter::new(tariff_json, warnings.iter());
for warning::ElementReport {
element_path,
warnings,
json,
location,
} in report_iter
{
eprintln!(
"{} Warnings for `{}`\n",
style("##").yellow(),
style(element_path).yellow()
);
eprintln!(
"{INDENT} {} {}:{}\n{INDENT} {}",
style("|").green(),
style(&tariff_name).green(),
style(location).green(),
style("|").green(),
);
let val_lines = json.lines().collect::<Vec<_>>();
let trim_ws = val_lines.get(1..).and_then(|lines| {
lines
.iter()
.map(|s| {
let trimmed = s.trim_start();
s.len().saturating_sub(trimmed.len())
})
.min()
});
if let Some(trim_ws) = trim_ws {
let mut val_lines = val_lines.into_iter();
if let Some(first_line) = val_lines.next() {
eprint!("{INDENT} {block} {}", style(first_line).yellow());
for line in val_lines {
let (_, line) = line.split_at(trim_ws);
eprint!("\n{INDENT} {block} {}", style(line).yellow());
}
eprintln!();
}
} else {
eprintln!("{INDENT} {block} {}", style(json).yellow());
}
eprintln!();
for warning in warnings {
eprintln!("{INDENT} {list_item} {warning}");
}
eprintln!();
}
}
fn print_unexpected_fields(fields: json::UnexpectedFields<'_>) {
if !fields.is_empty() {
eprintln!("{}\n", style("# Unexpected fields").yellow());
}
let list_marker = style("*").yellow();
for path in fields {
eprintln!(" {list_marker} {}", PathDisplay(path));
}
eprintln!();
}
struct PathDisplay<'buf>(PathRef<'buf>);
impl fmt::Display for PathDisplay<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let components = self.0.components();
let mut components = components.collect::<Vec<_>>().into_iter().rev();
let dot = style('.').yellow();
if let Some(comp) = components.next() {
match comp.kind() {
json::PathNodeKind::Root | json::PathNodeKind::Array => write!(f, "{comp}")?,
json::PathNodeKind::Object => write!(f, "{}", style(comp).green())?,
}
}
for comp in components {
match comp.kind() {
json::PathNodeKind::Root | json::PathNodeKind::Array => write!(f, "{dot}{comp}")?,
json::PathNodeKind::Object => write!(f, "{dot}{}", style(comp).green())?,
}
}
Ok(())
}
}