Cut 5 (Rust). The four mirror loops in put, put_envelope, put_raw_envelope and delete are deleted, and so is put_prepared_batch's route.len() != 1 branch. resolve_route_index returns the single home store (the store that claims the type, else the generic store) and replaces resolve_route_indices. add_backing_store and add_generic_backing_store return Result and refuse a second generic store or a type already claimed by another store, attaching nothing when they refuse. Callers take the Result: the crate's tests, the cultcache_interop example, cultnet-rs's tests and interop peer example, and CultMeshNode::create_node in cultmesh-rs. Tests: second_generic_store_is_rejected, type_claimed_twice_is_rejected; type_specific_store_routes_before_generic_store stays, now attaching the routed store before the generic one. No bytes change. The store-composition contract's runtime table now records Rust, TypeScript and Python as conforming to one home store per type, and the README routing section drops its non-conformance note. Version 0.1.0 -> 0.2.0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
121 lines
3.6 KiB
Rust
121 lines
3.6 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>,
|
|
}
|
|
|
|
fn main() -> Result<()> {
|
|
let mut args = std::env::args().skip(1);
|
|
let mode = args
|
|
.next()
|
|
.ok_or_else(|| anyhow!("expected mode: write | read"))?;
|
|
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)?,
|
|
_ => return Err(anyhow!("unknown mode {mode}")),
|
|
}
|
|
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::<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}"))
|
|
}
|