lazyspec 0.8.0

A little TUI & CLI for project documentation.
Documentation
use crate::cli::json::doc_to_json;
use crate::engine::config::Config;
use crate::engine::document::DocType;
use crate::engine::fs::FileSystem;
use crate::engine::store::{Filter, Store};
use anyhow::Result;

fn find_convention_type(config: &Config) -> Option<&str> {
    config
        .documents
        .types
        .iter()
        .find(|t| t.singleton)
        .map(|t| t.name.as_str())
}

fn find_dictum_type_names(config: &Config, convention_type: &str) -> Vec<String> {
    config
        .documents
        .types
        .iter()
        .filter(|t| t.parent_type.as_deref() == Some(convention_type))
        .map(|t| t.name.clone())
        .collect()
}

pub fn run_human(
    store: &Store,
    config: &Config,
    preamble: bool,
    tags: Option<&str>,
    fs: &dyn FileSystem,
) -> Result<String> {
    let convention_type_name = match find_convention_type(config) {
        Some(name) => name,
        None => return Ok(String::new()),
    };

    let conventions = store.list(&Filter {
        doc_type: Some(DocType::new(convention_type_name)),
        ..Filter::default()
    });
    let convention = match conventions.first() {
        Some(c) => c,
        None => return Ok(String::new()),
    };

    let mut output = store.get_body_raw(&convention.path, fs)?;

    if preamble {
        return Ok(output);
    }

    let dictum_type_names = find_dictum_type_names(config, convention_type_name);
    let requested_tags: Vec<&str> = tags
        .map(|t| t.split(',').map(|s| s.trim()).collect())
        .unwrap_or_default();

    let mut dicta = Vec::new();
    for type_name in &dictum_type_names {
        let docs = store.list(&Filter {
            doc_type: Some(DocType::new(type_name)),
            ..Filter::default()
        });
        dicta.extend(docs);
    }

    if !requested_tags.is_empty() {
        dicta.retain(|doc| {
            doc.tags
                .iter()
                .any(|t| requested_tags.contains(&t.as_str()))
        });
    }

    dicta.sort_by(|a, b| a.path.cmp(&b.path));

    for doc in &dicta {
        output.push_str(&format!("\n## {}\n\n", doc.title));
        let body = store.get_body_raw(&doc.path, fs)?;
        output.push_str(&body);
    }

    Ok(output)
}

pub fn run_json(
    store: &Store,
    config: &Config,
    preamble: bool,
    tags: Option<&str>,
    fs: &dyn FileSystem,
) -> Result<String> {
    let convention_type_name = match find_convention_type(config) {
        Some(name) => name,
        None => {
            let output = serde_json::json!({"convention": null, "dicta": []});
            return Ok(serde_json::to_string_pretty(&output)?);
        }
    };

    let conventions = store.list(&Filter {
        doc_type: Some(DocType::new(convention_type_name)),
        ..Filter::default()
    });
    let convention = match conventions.first() {
        Some(c) => c,
        None => {
            let output = serde_json::json!({"convention": null, "dicta": []});
            return Ok(serde_json::to_string_pretty(&output)?);
        }
    };

    let mut conv_json = doc_to_json(convention);
    let conv_body = store.get_body_raw(&convention.path, fs)?;
    conv_json["body"] = serde_json::Value::String(conv_body);

    let dicta_json = if preamble {
        vec![]
    } else {
        let dictum_type_names = find_dictum_type_names(config, convention_type_name);
        let requested_tags: Vec<&str> = tags
            .map(|t| t.split(',').map(|s| s.trim()).collect())
            .unwrap_or_default();

        let mut dicta = Vec::new();
        for type_name in &dictum_type_names {
            let docs = store.list(&Filter {
                doc_type: Some(DocType::new(type_name)),
                ..Filter::default()
            });
            dicta.extend(docs);
        }

        if !requested_tags.is_empty() {
            dicta.retain(|doc| {
                doc.tags
                    .iter()
                    .any(|t| requested_tags.contains(&t.as_str()))
            });
        }

        dicta.sort_by(|a, b| a.path.cmp(&b.path));

        let mut result = Vec::new();
        for doc in &dicta {
            let mut j = doc_to_json(doc);
            let body = store.get_body_raw(&doc.path, fs)?;
            j["body"] = serde_json::Value::String(body);
            result.push(j);
        }
        result
    };

    let output = serde_json::json!({
        "convention": conv_json,
        "dicta": dicta_json,
    });

    Ok(serde_json::to_string_pretty(&output)?)
}