ghee 0.6.1

That thin layer of data change management over the filesystem
Documentation
use std::{
    fs::{create_dir_all, hard_link, File},
    io::{stdin, BufRead, BufReader, Read},
    path::PathBuf,
};

use anyhow::Result;
use ghee_lang::{Key, Value, Xattr};
use thiserror::Error;

use crate::{paths::PathBufExt, table_info, Record};

#[derive(Error, Debug)]
pub enum InsErr {
    #[error("Table info not found at {0}")]
    TableInfoNotFound(PathBuf),

    #[error("Path {0} not found")]
    PathNotFound(PathBuf),

    #[error("An IO error occurred at {path}: {err}")]
    IoError { err: std::io::Error, path: PathBuf },

    #[error("A JSON error occurred: {0}")]
    JsonError(serde_json::Error),

    #[error("Raw (unnamed) json value not supported: {0}")]
    RawJsonValue(serde_json::Value),

    #[error("Record wouldn't be included in any index")]
    IncludedInNoIndex,
}

/// Interpret a JSON object as parsed xattr values
fn json_to_record(json: &serde_json::Value) -> Result<Record> {
    if let serde_json::Value::Object(o) = json {
        let mut record = Record::new();
        for (k, v) in o {
            let xattr = Xattr::from(k.as_str());
            let value = Value::try_from(v.clone())?;
            record.insert(xattr, value);
        }

        Ok(record)
    } else {
        Err(InsErr::RawJsonValue(json.clone()).into())
    }
}

fn write_record_as_xattrs(path: &PathBuf, record: &Record) -> Result<()> {
    for (k, v) in record {
        let bytes = v.as_bytes();

        xattr::set(path, k.to_osstring(), bytes.as_slice()).map_err(|err| InsErr::IoError {
            err,
            path: path.clone(),
        })?;
    }

    Ok(())
}

/**
 * Insert records (from a file or stdin) into the table at `table_path`, updating all related indices.
 *
 * If `records_path` is `None` then records will be taken from stdin. Records are JSON objects,
 * one per line.
 */
pub fn ins(table_path: &PathBuf, records_path: &Option<PathBuf>, verbose: bool) -> Result<()> {
    // Read records from the path if provided, else stdin
    let r: Box<dyn Read> = if let Some(path) = records_path {
        Box::new(File::open(path).map_err(|_| InsErr::PathNotFound(path.clone()))?)
    } else {
        Box::new(stdin())
    };

    let r = BufReader::new(r);

    let records_iter = r.lines().map(|line| {
        let line = line.unwrap_or_else(|e| panic!("Error reading records line: {}", e));

        let json_record: serde_json::Value = serde_json::from_str(&line)
            .map_err(InsErr::JsonError)
            .unwrap_or_else(|e| panic!("Error parsing line as JSON: {}", e));
        let record = json_to_record(&json_record)
            .unwrap_or_else(|e| panic!("Error interpreting JSON as xattrs: {}", e));
        record
    });

    ins_records(table_path, records_iter, verbose)
}

/**
 * Insert records from an iterator into the table at `table_path`, updating all related indices.
 *
 * To be inserted, a record must belong to at least one index, i.e. have all of that index's key
 * xattrs.
 */
pub fn ins_records(
    table_path: &PathBuf,
    records: impl Iterator<Item = Record>,
    verbose: bool,
) -> Result<()> {
    let info =
        table_info(table_path)?.ok_or_else(|| InsErr::TableInfoNotFound(table_path.clone()))?;

    for record in records {
        // The paths within indices where this record would be located, if any
        //
        // Tuple is (table_path, record_path)
        let paths: Vec<(&PathBuf, PathBuf)> = {
            // Put shorter-pathed indices first
            let ordered = {
                let mut ordered: Vec<(&Key, &PathBuf)> = info.indices_abs().iter().collect();
                ordered.sort_by_key(|(_k, p)| p.to_string_lossy().len());
                ordered
            };

            ordered
                .into_iter()
                .map(|(key, table_path)| {
                    let record_path = key.path_for_record(table_path.clone(), &record);
                    (table_path, record_path)
                })
                .filter(|(_table_path, record_path)| record_path.is_ok())
                .map(|(table_path, record_path)| (table_path, record_path.unwrap()))
                .collect()
        };

        if paths.is_empty() {
            return Err(InsErr::IncludedInNoIndex.into());
        }

        let (_index_path, record_path) = &paths[0];

        create_dir_all(record_path.parent().unwrap()).map_err(|err| InsErr::IoError {
            err,
            path: record_path.parent().unwrap().into(),
        })?;

        File::create(&record_path).map_err(|err| InsErr::IoError {
            err,
            path: record_path.clone(),
        })?;

        write_record_as_xattrs(&record_path, &record)?;

        let record_path_display = record_path.relative_to_curdir_if_possible();

        if verbose {
            eprintln!("Initialized {}", record_path_display.display());
        }

        // Hardlink to all other indices
        for (_dest_index_path, dest_record_path) in paths.iter().skip(1) {
            create_dir_all(dest_record_path.parent().unwrap()).map_err(|err| InsErr::IoError {
                err,
                path: dest_record_path.parent().unwrap().into(),
            })?;

            hard_link(&record_path, &dest_record_path).map_err(|err| InsErr::IoError {
                err,
                path: record_path.clone(),
            })?;

            if verbose {
                let dest_record_path_display = dest_record_path.relative_to_curdir_if_possible();
                eprintln!(
                    "Linked {} -> {}",
                    dest_record_path_display.display(),
                    record_path_display.display()
                );
            }
        }
    }

    Ok(())
}

#[cfg(test)]
mod test {
    use ghee_lang::Value;

    use crate::{test_support::Scenario, xattr_values};

    #[test]
    fn test_ins() {
        let s = Scenario::new("ins");

        let mut record_path = s.dir1.clone();
        record_path.push("0");

        let mut indexed_record_path = s.dir2.clone();
        indexed_record_path.push("1");
        indexed_record_path.push("2");

        assert!(record_path.exists());
        assert!(indexed_record_path.exists());

        let xattrs = xattr_values(&record_path).unwrap();
        assert_eq!(xattrs.len(), 4);

        assert_eq!(xattrs[&s.xattr1].clone(), Value::Number(0f64));
        assert_eq!(xattrs[&s.xattr2].clone(), Value::Number(1f64));
        assert_eq!(xattrs[&s.xattr3].clone(), Value::Number(2f64));
        assert_eq!(xattrs[&s.xattr4].clone(), Value::Number(3f64));

        let indexed_xattrs = xattr_values(&indexed_record_path).unwrap();
        assert_eq!(xattrs.len(), 4);

        assert_eq!(indexed_xattrs[&s.xattr1].clone(), Value::Number(0f64));
        assert_eq!(indexed_xattrs[&s.xattr2].clone(), Value::Number(1f64));
        assert_eq!(indexed_xattrs[&s.xattr3].clone(), Value::Number(2f64));
        assert_eq!(indexed_xattrs[&s.xattr4].clone(), Value::Number(3f64));
    }
}