Interop: an element id crosses C#, TypeScript, Python and Rust
The C# cultcache interop peer gains an object-list document (a deck of marks). C# writes it with cache-minted ids; TypeScript, Python and Rust read the ids as ordinary members, and TypeScript-written ids survive a C# read. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
09dabd38c5
commit
640ad9a437
3 changed files with 116 additions and 0 deletions
|
|
@ -26,6 +26,38 @@ struct CultCacheInteropNote {
|
|||
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
|
||||
|
|
@ -37,6 +69,7 @@ fn main() -> Result<()> {
|
|||
match mode.as_str() {
|
||||
"write" => write_note(file, require_arg(&options, "runtime-id")?)?,
|
||||
"read" => read_note(file)?,
|
||||
"read-deck" => read_deck(file)?,
|
||||
_ => return Err(anyhow!("unknown mode {mode}")),
|
||||
}
|
||||
Ok(())
|
||||
|
|
|
|||
|
|
@ -1173,6 +1173,43 @@ test("CultCache v1 MessagePack stores are readable across TS, Rust, C#, and Pyth
|
|||
}
|
||||
});
|
||||
|
||||
test("CultCache element ids cross C#, TypeScript, Python and Rust as ordinary members", async () => {
|
||||
await buildInteropPeers();
|
||||
const tempDir = await mkdtemp(join(tmpdir(), "cultcache-deck-"));
|
||||
const file = join(tempDir, "deck.cc");
|
||||
const written = await runJsonCommand("csharp-write-deck", dotnetCommand, [
|
||||
csharpInteropDll, "write-deck", "--file", file,
|
||||
], cultLibRoot);
|
||||
assert.equal(written.ids.length, 3);
|
||||
assert.equal(new Set(written.ids).size, 3, "two look-alike marks got distinct ids");
|
||||
for (const id of written.ids) assert.match(id, /^[0-9a-f]{12}$/u);
|
||||
|
||||
// TypeScript: the id is slot 1 of each mark, read from the raw payload.
|
||||
const store = decode(await readFile(file)) as any[];
|
||||
const payload = decode(store[2][0][3]) as any[];
|
||||
assert.deepEqual(payload[1].map((mark: any[]) => mark[1]), written.ids);
|
||||
|
||||
// Python and Rust read the same store.
|
||||
const python = await runJsonCommand("python-deck", pythonCommand, [
|
||||
"-c",
|
||||
"import msgpack,json,sys;s=msgpack.unpackb(open(sys.argv[1],'rb').read(),raw=False);p=msgpack.unpackb(s[2][0][3],raw=False);print(json.dumps({'ids':[m[1] for m in p[1]]}))",
|
||||
file,
|
||||
], cultcachePyRoot);
|
||||
assert.deepEqual(python.ids, written.ids);
|
||||
const rust = await runJsonCommand("rust-read-deck", rustInteropBinary, ["read-deck", "--file", file], cultcacheRsRoot);
|
||||
assert.deepEqual(rust.ids, written.ids);
|
||||
|
||||
// TypeScript writes the ids; C# keeps them.
|
||||
const tsIds = ["t00000000001", "t00000000002", "t00000000003"];
|
||||
store[2][0][3] = encode(["deck:csharp", [["twin", tsIds[0]], ["twin", tsIds[1]], ["other", tsIds[2]]]]);
|
||||
const tsFile = join(tempDir, "ts-deck.cc");
|
||||
await writeFile(tsFile, encode(store));
|
||||
const back = await runJsonCommand("csharp-read-deck", dotnetCommand, [
|
||||
csharpInteropDll, "read-deck", "--file", tsFile,
|
||||
], cultLibRoot);
|
||||
assert.deepEqual(back.ids, tsIds);
|
||||
});
|
||||
|
||||
test("CultCache interop reader accepts missing compatible trailing slots and rejects mismatched slots", async () => {
|
||||
const tempDir = await mkdtemp(join(tmpdir(), "cultcache-interop-"));
|
||||
const compatible = join(tempDir, "compatible.msgpack");
|
||||
|
|
|
|||
|
|
@ -38,6 +38,12 @@ static async Task<int> ProgramMainAsync(string[] args)
|
|||
case "read":
|
||||
await ReadAsync(file);
|
||||
return 0;
|
||||
case "write-deck":
|
||||
await WriteDeckAsync(file);
|
||||
return 0;
|
||||
case "read-deck":
|
||||
await ReadDeckAsync(file);
|
||||
return 0;
|
||||
default:
|
||||
throw new InvalidOperationException($"Unknown mode {mode}.");
|
||||
}
|
||||
|
|
@ -77,6 +83,31 @@ static async Task ReadAsync(string file)
|
|||
WriteJsonLine(note);
|
||||
}
|
||||
|
||||
// A record with an object list: the cache mints each mark's element id, and every runtime reads it as an ordinary member.
|
||||
static async Task WriteDeckAsync(string file)
|
||||
{
|
||||
var cache = BuildCache(file);
|
||||
await cache.PullAllBackingStoresAsync();
|
||||
var deck = new CultCacheInteropDeck
|
||||
{
|
||||
DocumentId = "deck:csharp",
|
||||
Marks = [new CultCacheInteropMark { Label = "twin" }, new CultCacheInteropMark { Label = "twin" }, new CultCacheInteropMark { Label = "other" }]
|
||||
};
|
||||
await cache.AddAsync(deck, new CultRecordHandle<CultCacheInteropDeck>(new CultRecordKey(deck.DocumentId)));
|
||||
cache.FlushAllBackingStores();
|
||||
WriteDeckJsonLine(deck);
|
||||
}
|
||||
|
||||
static async Task ReadDeckAsync(string file)
|
||||
{
|
||||
var cache = BuildCache(file);
|
||||
await cache.PullAllBackingStoresAsync();
|
||||
WriteDeckJsonLine(cache.AllEntries.OfType<CultCacheInteropDeck>().Single());
|
||||
}
|
||||
|
||||
static void WriteDeckJsonLine(CultCacheInteropDeck deck) =>
|
||||
Console.Out.WriteLine(JsonSerializer.Serialize(new { documentId = deck.DocumentId, ids = deck.Marks.Select(mark => mark.Id).ToArray() }));
|
||||
|
||||
// Two routed single-file stores: each file is a complete single-store snapshot holding only its own type.
|
||||
static void WriteRouted(string catalogFile, string runFile)
|
||||
{
|
||||
|
|
@ -176,3 +207,18 @@ public sealed class CultCacheInteropRunNote
|
|||
[Key(2)] public string AuthorRuntimeId { get; set; } = string.Empty;
|
||||
[Key(3)] public string Body { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
[CultDocument("cultcache.interop-deck", "cultcache.interop_deck.v1")]
|
||||
[MessagePackObject]
|
||||
public sealed class CultCacheInteropDeck
|
||||
{
|
||||
[Key(0)] [CultName] public string DocumentId { get; set; } = string.Empty;
|
||||
[Key(1)] public List<CultCacheInteropMark> Marks { get; set; } = new();
|
||||
}
|
||||
|
||||
[MessagePackObject]
|
||||
public sealed class CultCacheInteropMark
|
||||
{
|
||||
[Key(0)] public string Label { get; set; } = string.Empty;
|
||||
[Key(1)] [CultElementId] public string Id { get; set; } = string.Empty;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue