ocpi-tariffs-cli 0.49.0

CLI application for OCPI tariff calculation
Documentation
//! Lint a tariff and print a report.

use std::{io::IsTerminal as _, path::PathBuf};

use clap::{Args, Parser};
use console::style;
use ocpi_tariffs::{json, tariff};

use crate::{
    load_object_file, load_object_from_stdin,
    opts::{FilePath, Version},
    print, Error, ObjectKind,
};

/// The name of the tariff source when using `stdin`.
const STDIN_NAME: &str = "<stdin>";

/// The arguments and logic for the `lint::Command`.
#[derive(Parser)]
pub struct Command {
    /// Arguments for the `lint::Command`.
    #[command(flatten)]
    args: Arguments,
}

/// Argument for the lint command.
#[derive(Args)]
struct Arguments {
    /// The OCPI version that should be used for the input structures.
    #[arg(short = 'o', long, value_enum)]
    ocpi_version: Version,

    /// Pretty print the tariff JSON for human consumption.
    #[arg(long)]
    pretty: bool,

    /// A path to the tariff structure in JSON format.
    #[arg(short = 'f', long)]
    file: Option<PathBuf>,
}

impl Command {
    /// Run the `lint` 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 tariff_doc = json::parse_object(&tariff_json)?;
        let (tariff, warnings) = tariff::build(tariff_doc, ocpi_version).into_parts();

        if pretty {
            eprintln!("{}", style("# Source Tariff:").green());
            eprintln!("{}\n", json::write::Pretty::from_doc(tariff.as_doc()));
        }

        let report = tariff::lint(&tariff);

        eprintln!("Completed {} tariff\n", style("linting").green().bold());

        print::warning_set("linting", &warnings);
        print_report(tariff_file_path.as_ref(), &tariff, report);

        Ok(())
    }
}

/// Print the lint report cross referencing the warning with the source JSON so the report can
/// display the source JSON for each [`json::Element`] that has warnings.
fn print_report(
    file_path: Option<&FilePath>,
    tariff: &tariff::Versioned<'_>,
    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 source = tariff.as_doc().source();
    let list_item = style("*").yellow();
    let block = style("|").yellow();

    for group in warnings {
        let (element, warnings) = group.into_parts();
        eprintln!(
            "{} Warnings for `{}`\n",
            style("##").yellow(),
            style(element.path).yellow()
        );

        eprintln!(
            "{INDENT} {} {}:{}\n{INDENT} {}",
            style("|").green(),
            style(&tariff_name).green(),
            style(element.location).green(),
            style("|").green(),
        );

        let val_lines = source.lines().collect::<Vec<_>>();

        // Calculate the largest amount of whitespace we can trim from a multiline value.
        // `trim_ws` will be `None` if the value is not multiline.
        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(source).yellow());
        }

        eprintln!();

        for warning in warnings {
            eprintln!("{INDENT} {list_item} {warning}");
        }

        eprintln!();
    }
}