sql-hummus 0.1.5

Log files and key-value stores in SQLite.
Documentation
use std::{io::Read, process::ExitCode};

use anyhow::{Context as _, Result};
use camino::Utf8PathBuf;
use clap::Parser as _;
use serde_json::json;
use sql_hummus::{Kv, Log};

use crate::cli::{Command, KvCommand, LogCommand};

mod cli;

fn truthy_code() -> ExitCode {
    0.into()
}

fn falsy_code() -> ExitCode {
    1.into()
}

fn err_code() -> ExitCode {
    2.into()
}

fn main() -> ExitCode {
    match inner_main() {
        Ok(true) => truthy_code(),
        Ok(false) => falsy_code(),
        Err(e) => {
            eprintln!("{e}");
            err_code()
        }
    }
}

fn run_kv_command(path: Utf8PathBuf, cmd: KvCommand) -> Result<bool> {
    match cmd {
        KvCommand::ContainsKey { key } => match Kv::new(path)?.contains_key(key)? {
            true => {
                println!("true");
                Ok(true)
            }
            false => {
                println!("false");
                Ok(false)
            }
        },
        KvCommand::Get { key } => match Kv::new(path)?.get(key)? {
            Some(value) => {
                println!("{value}");
                Ok(true)
            }
            None => {
                eprintln!("Key not in KV file");
                Ok(false)
            }
        },
        KvCommand::Insert { key, value } => {
            Kv::new(path)?.insert(key, value)?;
            Ok(true)
        }
        KvCommand::WithPrefix { prefix } => {
            let kv = Kv::new(path)?;
            let cursor = kv.with_prefix(&prefix)?;
            for row in cursor {
                let (k, v) = row?;
                println!("{}", serde_json::to_string(&(k, v))?);
            }
            Ok(true)
        }
    }
}

fn run_log_command(path: Utf8PathBuf, cmd: LogCommand) -> Result<bool> {
    match cmd {
        LogCommand::Get { index } => {
            let index = match index {
                cli::LogIndex::DateRange(..) => todo!(),
                cli::LogIndex::DateScalar(..) => todo!(),
                cli::LogIndex::NumberRange(..) => todo!(),
                cli::LogIndex::NumberScalar(index) => index,
            };
            let Some(line) = Log::new(&path)?.get_by_index(index)? else {
                eprintln!("{path:?} has no element at index {index}");
                return Ok(false);
            };
            println!("{}", serde_json::to_string(&line)?);
            Ok(true)
        }
        LogCommand::Iter => {
            let log = Log::new(path)?;
            let cursor = log.iter()?;
            for row in cursor {
                let line = row?;
                println!("{}", serde_json::to_string(&line)?);
            }
            Ok(true)
        }
        LogCommand::Push { content } => {
            let content = content_or_stdin(content.as_deref())?;
            let index = Log::new(path)?.push(content)?;
            println!("{index}");
            Ok(true)
        }
    }
}

fn inner_main() -> Result<bool> {
    let cli = cli::Cli::try_parse()?;
    match cli.cmd {
        Command::Kv { cmd, path } => run_kv_command(path, cmd),
        Command::Log { cmd, path } => run_log_command(path, cmd),
        Command::Paste { annotation } => {
            let clipboard = std::process::Command::new("wl-paste")
                .output()
                .context("wl-paste failed")?;
            let clipboard = str::from_utf8(clipboard.stdout.as_slice())?;
            let content = json!({ "annotation": annotation, "clipboard": clipboard });

            let (log, path) = Log::open_default().context("Log::open_default() failed")?;
            let index = log.push(serde_json::to_string(&content)?)?;
            let output = json!({ "content": content, "index": index, "path": path });
            println!("{}", serde_json::to_string(&output)?);
            Ok(true)
        }
        Command::Push { content } => {
            let content = content_or_stdin(content.as_deref())?;
            let (log, path) = Log::open_default().context("Log::open_default() failed")?;
            let index = log.push(content)?;
            let output = json!({ "index": index, "path": path });
            println!("{}", serde_json::to_string(&output)?);
            Ok(true)
        }
    }
}

fn content_or_stdin(content: Option<&str>) -> Result<std::borrow::Cow<'_, str>> {
    match content {
        Some(x) => Ok(x.into()),
        None => {
            let mut buf = Vec::with_capacity(1_024 * 1_024);
            std::io::stdin().read_to_end(&mut buf)?;
            Ok(String::from_utf8(buf)?.into())
        }
    }
}