paperless-cli 2.1.0

Rust TUI and LLM-friendly client for Paperless-ngx
Documentation
use anyhow::Result;
use clap::ValueEnum;
use serde::Serialize;
use serde_json::{Map, Value};

#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, ValueEnum)]
pub enum OutputFormat {
    Json,
    #[default]
    Markdown,
}

pub fn render<T: Serialize>(value: &T, format: OutputFormat) -> Result<String> {
    let json_value = serde_json::to_value(value)?;
    match format {
        OutputFormat::Json => Ok(serde_json::to_string_pretty(&json_value)?),
        OutputFormat::Markdown => Ok(render_markdown(&json_value)),
    }
}

fn render_markdown(value: &Value) -> String {
    let mut lines = Vec::new();
    render_value(value, 1, None, &mut lines);
    lines.join("\n").trim().to_string() + "\n"
}

fn render_value(value: &Value, depth: usize, label: Option<&str>, lines: &mut Vec<String>) {
    match value {
        Value::Object(object) => render_object(object, depth, label, lines),
        Value::Array(items) => render_array(items, depth, label, lines),
        scalar => {
            if let Some(key) = label {
                lines.push(format!("- {}: {}", key, scalar_to_string(scalar)));
            } else {
                lines.push(scalar_to_string(scalar));
            }
        }
    }
}

fn render_object(
    object: &Map<String, Value>,
    depth: usize,
    label: Option<&str>,
    lines: &mut Vec<String>,
) {
    if let Some(title) = label {
        lines.push(format!("{} {}", "#".repeat(depth), title));
    }

    for (key, value) in object {
        match value {
            Value::Object(_) | Value::Array(_) => {
                render_value(value, depth + 1, Some(key), lines);
            }
            scalar => {
                lines.push(format!("- {}: {}", key, scalar_to_string(scalar)));
            }
        }
    }
}

fn render_array(items: &[Value], depth: usize, label: Option<&str>, lines: &mut Vec<String>) {
    if let Some(title) = label {
        lines.push(format!("{} {}", "#".repeat(depth), title));
    }

    for (index, item) in items.iter().enumerate() {
        match item {
            Value::Object(object) => {
                let summary = object
                    .get("title")
                    .or_else(|| object.get("name"))
                    .or_else(|| object.get("status"))
                    .map(scalar_to_string)
                    .unwrap_or_else(|| format!("item {}", index + 1));
                lines.push(format!("## {}", summary));
                for (key, value) in object {
                    if matches!(value, Value::Object(_) | Value::Array(_)) {
                        render_value(value, depth + 2, Some(key), lines);
                    } else {
                        lines.push(format!("- {}: {}", key, scalar_to_string(value)));
                    }
                }
            }
            scalar => lines.push(format!("- {}", scalar_to_string(scalar))),
        }
    }
}

fn scalar_to_string(value: &Value) -> String {
    match value {
        Value::Null => "null".to_string(),
        Value::Bool(boolean) => boolean.to_string(),
        Value::Number(number) => number.to_string(),
        Value::String(string) => string.clone(),
        _ => value.to_string(),
    }
}