use anyhow::Result; use anyhow::anyhow; use cultcache_rs::CultCache; use cultcache_rs::DatabaseEntry; use cultcache_rs::SingleFileMessagePackBackingStore; use serde_json::json; use std::collections::BTreeMap; const INTEROP_TYPE: &str = "cultcache.interop-note"; const INTEROP_SCHEMA_VERSION: &str = "cultcache.interop_note.v1"; #[derive(Clone, Debug, PartialEq, Eq, DatabaseEntry)] #[cultcache(type = "cultcache.interop-note", schema = "CultCacheInteropNote")] struct CultCacheInteropNote { #[cultcache(key = 0)] schema_version: String, #[cultcache(key = 1)] document_id: String, #[cultcache(key = 2)] author_runtime_id: String, #[cultcache(key = 3)] title: String, #[cultcache(key = 4)] body: String, #[cultcache(key = 5, default)] tags: Vec, } // An object list: each mark is a positional array [label, id], the id an ordinary member at slot 1. #[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)] struct CultCacheInteropMark(String, String); #[derive(Clone, Debug, PartialEq, Eq, DatabaseEntry)] #[cultcache(type = "cultcache.interop-deck", schema = "CultCacheInteropDeck")] struct CultCacheInteropDeck { #[cultcache(key = 0)] document_id: String, #[cultcache(key = 1, default)] marks: Vec, } fn read_deck(file: &str) -> Result<()> { let mut cache = CultCache::new(); cache.register_entry_type::()?; cache.add_generic_backing_store(SingleFileMessagePackBackingStore::new(file))?; cache.pull_all_backing_stores()?; let decks = cache.get_all::()?; let deck = decks .first() .ok_or_else(|| anyhow!("no cultcache.interop-deck records found"))?; println!( "{}", serde_json::to_string(&json!({ "documentId": deck.document_id, "ids": deck.marks.iter().map(|mark| mark.1.clone()).collect::>(), }))? ); Ok(()) } fn main() -> Result<()> { let mut args = std::env::args().skip(1); let mode = args .next() .ok_or_else(|| anyhow!("expected mode: write | read | hold-lock"))?; let options = parse_args(args.collect()); let file = require_arg(&options, "file")?; match mode.as_str() { "write" => write_note(file, require_arg(&options, "runtime-id")?)?, "read" => read_note(file)?, "read-deck" => read_deck(file)?, "hold-lock" => hold_lock(file, require_arg(&options, "ms")?.parse()?)?, _ => return Err(anyhow!("unknown mode {mode}")), } Ok(()) } /// Holds the store lock as the library opens it (`.lock`, fs2 exclusive), for `ms`. fn hold_lock(file: &str, ms: u64) -> Result<()> { let lock = std::fs::OpenOptions::new() .create(true) .read(true) .truncate(false) .write(true) .open(format!("{file}.lock"))?; fs2::FileExt::lock_exclusive(&lock)?; println!("{}", json!({ "held": true })); std::io::Write::flush(&mut std::io::stdout())?; std::thread::sleep(std::time::Duration::from_millis(ms)); fs2::FileExt::unlock(&lock)?; Ok(()) } fn write_note(file: &str, runtime_id: &str) -> Result<()> { let mut cache = build_cache(file)?; cache.pull_all_backing_stores()?; let note = CultCacheInteropNote { schema_version: INTEROP_SCHEMA_VERSION.to_string(), document_id: format!("note:{runtime_id}"), author_runtime_id: runtime_id.to_string(), title: format!("{runtime_id} wrote a CultCache note"), body: "The v1 store format is the contract.".to_string(), tags: vec![ runtime_id.to_string(), "rust".to_string(), "interop".to_string(), ], }; cache.put(¬e.document_id, ¬e)?; print_note(¬e) } fn read_note(file: &str) -> Result<()> { let mut cache = build_cache(file)?; cache.pull_all_backing_stores()?; let notes = cache.get_all::()?; let note = notes .first() .ok_or_else(|| anyhow!("no {INTEROP_TYPE} records found"))?; print_note(note) } fn build_cache(file: &str) -> Result { let mut cache = CultCache::new(); cache.register_entry_type::()?; cache.add_generic_backing_store(SingleFileMessagePackBackingStore::new(file))?; Ok(cache) } fn print_note(note: &CultCacheInteropNote) -> Result<()> { println!( "{}", serde_json::to_string(&json!({ "schemaVersion": note.schema_version, "documentId": note.document_id, "authorRuntimeId": note.author_runtime_id, "title": note.title, "body": note.body, "tags": note.tags, }))? ); Ok(()) } fn parse_args(args: Vec) -> BTreeMap { let mut parsed = BTreeMap::new(); let mut index = 0; while index < args.len() { let token = &args[index]; if !token.starts_with("--") { index += 1; continue; } let name = token.trim_start_matches("--").to_string(); let value = args .get(index + 1) .cloned() .unwrap_or_else(|| panic!("missing value for --{name}")); parsed.insert(name, value); index += 2; } parsed } fn require_arg<'a>(options: &'a BTreeMap, name: &str) -> Result<&'a str> { options .get(name) .map(String::as_str) .ok_or_else(|| anyhow!("missing required argument --{name}")) }