liber-cli 0.1.0

AI-agent-readable company directory CLI
//! `liber validate` — JSON Schema (draft-07) validation of every entity file.

use std::path::Path;

use jsonschema::{Draft, JSONSchema};
use serde_json::{json, Value};

use crate::data;
use crate::output::{emit_ok, exit, CliError, Ctx};
use crate::schemas;

const ENTITIES: &[&str] = &["people", "products", "customers", "chats", "repos"];

#[derive(serde::Serialize)]
struct EntityReport {
    entity: String,
    ok: bool,
    errors: Vec<SchemaError>,
}

#[derive(serde::Serialize)]
struct SchemaError {
    path: String,
    message: String,
}

pub fn run(dir: &Path, ctx: Ctx) -> Result<(), CliError> {
    let mut reports: Vec<EntityReport> = Vec::new();
    let mut all_ok = true;

    for entity in ENTITIES {
        let raw = schemas::for_entity(entity).expect("embedded schema");
        let schema: Value = serde_json::from_str(raw).expect("schema is valid JSON");
        let compiled = JSONSchema::options()
            .with_draft(Draft::Draft7)
            .compile(&schema)
            .map_err(|e| {
                CliError::data(
                    format!("internal: failed to compile schema for {entity}: {e}"),
                    None,
                )
            })?;

        let instance = data::load(dir, entity)?;
        let mut errors: Vec<SchemaError> = Vec::new();
        if let Err(iter) = compiled.validate(&instance) {
            for err in iter {
                errors.push(SchemaError {
                    path: err.instance_path.to_string(),
                    message: err.to_string(),
                });
            }
        }
        let ok = errors.is_empty();
        if !ok {
            all_ok = false;
        }
        reports.push(EntityReport {
            entity: (*entity).to_string(),
            ok,
            errors,
        });
    }

    let payload = json!({
        "ok": all_ok,
        "data_dir": dir.display().to_string(),
        "reports": reports,
    });

    if !all_ok {
        // Print the report on stdout (data) but exit with validation code.
        emit_ok(ctx, payload);
        return Err(CliError {
            code: exit::VALIDATION,
            message: "schema validation failed".into(),
            hint: Some("see the per-entity report above; fix the listed paths".into()),
            retryable: false,
        });
    }

    if ctx.json {
        emit_ok(ctx, payload);
    } else {
        emit_ok(
            ctx,
            json!({ "message": format!("ok — {} entities valid", ENTITIES.len()) }),
        );
    }
    Ok(())
}