CultLib/packages/cultcache-rs/examples/cultcache_interop.rs
Meta Vens f2b3569822 Merge main at cultlib-unity-v2.0.0 (42dce361) into C2a: one owner per collision
Identity: C2a's StoredSchemaId/StoredVersion owns; main's PersistedSchemaId, PersistedIdentity and both MarkWritten are deleted. Stamp rule: ReadStore judges the stamp before the body. Flush over damage refuses (merge-r1-precedence). Mesh single-file writes route through SingleFileMessagePackBackingStore. TS: store-file.ts commits, C2a's header handling kept. Rust: C2a's readable_store_format in front of the grown decoder. Build not yet verified at this commit.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 20:35:33 +02:00

171 lines
5.4 KiB
Rust

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<String>,
}
// 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<CultCacheInteropMark>,
}
fn read_deck(file: &str) -> Result<()> {
let mut cache = CultCache::new();
cache.register_entry_type::<CultCacheInteropDeck>()?;
cache.add_generic_backing_store(SingleFileMessagePackBackingStore::new(file))?;
cache.pull_all_backing_stores()?;
let decks = cache.get_all::<CultCacheInteropDeck>()?;
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::<Vec<_>>(),
}))?
);
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 (`<file>.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(&note.document_id, &note)?;
print_note(&note)
}
fn read_note(file: &str) -> Result<()> {
let mut cache = build_cache(file)?;
cache.pull_all_backing_stores()?;
let notes = cache.get_all::<CultCacheInteropNote>()?;
let note = notes
.first()
.ok_or_else(|| anyhow!("no {INTEROP_TYPE} records found"))?;
print_note(note)
}
fn build_cache(file: &str) -> Result<CultCache> {
let mut cache = CultCache::new();
cache.register_entry_type::<CultCacheInteropNote>()?;
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<String>) -> BTreeMap<String, String> {
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<String, String>, name: &str) -> Result<&'a str> {
options
.get(name)
.map(String::as_str)
.ok_or_else(|| anyhow!("missing required argument --{name}"))
}