fmtview 0.1.8

Fast terminal formatter and viewer for JSON, JSONL, XML-compatible markup, and formatted diffs
Documentation
use std::io::{BufReader, BufWriter, Cursor, Write};

use anyhow::{Context, Result, bail};
use tempfile::NamedTempFile;

use crate::input::InputSource;

use super::{
    detect::candidate_kinds,
    json::{format_json, format_json_value, format_jsonl, trim_line_end},
    types::{FormatKind, FormatOptions},
    xml::format_xml_reader,
};

pub fn format_source_to_temp(
    source: &InputSource,
    options: &FormatOptions,
) -> Result<NamedTempFile> {
    let candidates = candidate_kinds(source, options)?;
    let mut errors = Vec::new();

    for kind in candidates {
        match try_format_source_to_temp(source, kind, options) {
            Ok(temp) => return Ok(temp),
            Err(error) => errors.push(format!("{kind:?}: {error:#}")),
        }
    }

    bail!(
        "failed to format {} as JSON, JSONL, or XML:\n{}",
        source.label(),
        errors.join("\n")
    )
}

fn try_format_source_to_temp(
    source: &InputSource,
    kind: FormatKind,
    options: &FormatOptions,
) -> Result<NamedTempFile> {
    let mut temp = NamedTempFile::new().context("failed to create formatted temp file")?;
    {
        let mut output = BufWriter::new(temp.as_file_mut());
        match kind {
            FormatKind::Auto => unreachable!("auto is expanded before formatting"),
            FormatKind::Json => format_json(source, &mut output, options)
                .with_context(|| format!("failed to format {} as JSON", source.label()))?,
            FormatKind::Jsonl => format_jsonl(source, &mut output, options)
                .with_context(|| format!("failed to format {} as JSONL", source.label()))?,
            FormatKind::Xml => {
                format_xml_reader(BufReader::new(source.open()?), &mut output, options.indent)
                    .with_context(|| format!("failed to format {} as XML", source.label()))?
            }
        }
        output.flush().context("failed to flush formatted output")?;
    }
    Ok(temp)
}

pub fn format_record_to_string(input: &[u8], kind: FormatKind, indent: usize) -> Result<String> {
    let mut output = Vec::with_capacity(input.len().min(8192));
    match kind {
        FormatKind::Auto => unreachable!("auto must be resolved before formatting a record"),
        FormatKind::Json | FormatKind::Jsonl => {
            format_json_value(Cursor::new(input), &mut output, indent)
                .context("failed to parse JSON record")?
        }
        FormatKind::Xml => {
            format_xml_reader(Cursor::new(input), &mut output, indent)
                .context("failed to parse XML-compatible record")?;
            while output.ends_with(b"\n") || output.ends_with(b"\r") {
                output.pop();
            }
        }
    }
    String::from_utf8(output).context("formatted record was not valid UTF-8")
}

pub fn trim_record_line_end(line: &[u8]) -> &[u8] {
    trim_line_end(line)
}