diff --git a/docs/start/README.md b/docs/start/README.md index 91447789..c178c7f0 100644 --- a/docs/start/README.md +++ b/docs/start/README.md @@ -18,10 +18,10 @@ this page. | --- | --- | --- | --- | --- | --- | | **C#** | [first call, tested](#c-first-call) | [package](../../packages/cultmath/README.md); snippet not yet | [package](../../src/GameCult.Geometry/README.md); snippet not yet | [package](../../src/GameCult.Networking); snippet not yet | [quickstart](../../src/GameCult.Mesh/docs/durable-node-quickstart.md); run by `scripts/verify-eve-getting-started.ps1` | | **TypeScript** | [first call, tested](#typescript-first-call) | not yet | not yet | [package](../../packages/cultnet-ts/README.md); snippet not yet | [package](../../packages/cultmesh-ts/README.md); snippet not yet | -| **Rust** | [package](../../packages/cultcache-rs/README.md); snippet not yet | [native core](../../packages/cultmath/native/cultmath-core); snippet not yet | not yet | [package](../../packages/cultnet-rs/README.md), [examples](../../packages/cultnet-rs/examples); snippet not yet | [package](../../packages/cultmesh-rs); snippet not yet | -| **Python** | [package](../../packages/cultcache-py/README.md); snippet not yet | not yet | not yet | [package](../../packages/cultnet-py/README.md); snippet not yet | [package](../../packages/cultmesh-py/README.md); snippet not yet | +| **Rust** | [first call, tested](#rust-first-call) | [native core](../../packages/cultmath/native/cultmath-core); snippet not yet | not yet | [package](../../packages/cultnet-rs/README.md), [examples](../../packages/cultnet-rs/examples); snippet not yet | [package](../../packages/cultmesh-rs); snippet not yet | +| **Python** | [first call, tested](#python-first-call) | not yet | not yet | [package](../../packages/cultnet-py/README.md); snippet not yet | [package](../../packages/cultmesh-py/README.md); snippet not yet | -The Rust and Python CultCache first calls are the next cut for this page. For the whole +For the whole picture of what each piece is for, see the [package chooser in the top-level README](../../README.md#which-package-do-i-want). @@ -109,6 +109,108 @@ const byName = reopened.getByName(itemDocument, "Potion"); The test imports from `../src/index`; an application imports `@gamecult/cultcache-ts`. More: `packages/cultcache-ts/README.md`. +## Rust first call + +Same shape with `derive(DatabaseEntry)`: slots are `#[cultcache(key = N)]`, so the `.cc` payload is the +slot-indexed array the C# runtime writes. The source is +[`packages/cultcache-rs/tests/first_call.rs`](../../packages/cultcache-rs/tests/first_call.rs), run by +`cargo test --test first_call` in `packages/cultcache-rs`; a second test in that file fails if the block +below stops matching the file. The Rust runtime has no name lookup, so the read-back is by key. More: +`packages/cultcache-rs/README.md`. + +```rust +#[derive(Clone, Debug, PartialEq, Eq, DatabaseEntry)] +#[cultcache(type = "item")] +struct Item { + #[cultcache(key = 0)] + name: String, + #[cultcache(key = 1)] + category: String, + #[cultcache(key = 2, default)] + value: u32, +} + +fn first_call() -> anyhow::Result<()> { + let dir = tempfile::tempdir()?; + let path = dir.path().join("first-call.cc"); + + let mut cache = CultCache::new(); + cache.register_entry_type::()?; + cache.add_generic_backing_store(SingleFileMessagePackBackingStore::new(&path)); + cache.pull_all_backing_stores()?; + cache.put("item:potion", &Item { + name: "Potion".to_string(), + category: "Consumable".to_string(), + value: 50, + })?; // written to the .cc file before this returns + + let mut reopened = CultCache::new(); + reopened.register_entry_type::()?; + reopened.add_generic_backing_store(SingleFileMessagePackBackingStore::new(&path)); + reopened.pull_all_backing_stores()?; // read it back + let potion = reopened.get_required::("item:potion")?; + assert_eq!(potion.name, "Potion"); + assert_eq!(potion.value, 50); + Ok(()) +} +``` + +## Python first call + +A document is a definition with an encoder and decoder; `name=` and `indexes=` register the name lookup +and indexes. The source is +[`packages/cultcache-py/tests/test_first_call.py`](../../packages/cultcache-py/tests/test_first_call.py), +run by `python -m unittest tests.test_first_call` in `packages/cultcache-py` with `src` on `PYTHONPATH` +and `msgpack` installed (the store needs it). A second test fails if the block below stops matching the +file. More: `packages/cultcache-py/README.md`. + +```python +import tempfile +from dataclasses import asdict, dataclass +from pathlib import Path + +from cultcache_py import CultCache, SingleFileMessagePackBackingStore, define_document_type + + +@dataclass +class Item: + name: str + category: str + value: int + + +item_doc = define_document_type( + "item", + encode=lambda item: asdict(item), + decode=lambda raw: Item(**raw), + name="name", + indexes={"category": "category"}, +) + + +def first_call() -> Item: + path = Path(tempfile.mkdtemp()) / "first-call.cc" + + cache = ( + CultCache.builder() + .register_document_type(item_doc) + .add_generic_store(SingleFileMessagePackBackingStore(path)) + .build() + ) + cache.pull_all_backing_stores() + cache.put(item_doc, "item:potion", Item("Potion", "Consumable", 50)) # written to the .cc file + + reopened = ( + CultCache.builder() + .register_document_type(item_doc) + .add_generic_store(SingleFileMessagePackBackingStore(path)) + .build() + ) + reopened.pull_all_backing_stores() # read it back + assert reopened.get_by_name(item_doc, "Potion") == reopened.get_required(item_doc, "item:potion") + return reopened.get_required(item_doc, "item:potion") +``` + ## Where next - Smallest durable mesh path: `src/GameCult.Mesh/docs/durable-node-quickstart.md`. diff --git a/packages/cultcache-py/tests/test_first_call.py b/packages/cultcache-py/tests/test_first_call.py new file mode 100644 index 00000000..f8d4b52b --- /dev/null +++ b/packages/cultcache-py/tests/test_first_call.py @@ -0,0 +1,67 @@ +"""The Python CultCache first call from docs/start/README.md. The docs show the region between the +`snippet` markers verbatim, and test_docs_show_the_tested_snippet fails when they differ.""" + +import unittest +from pathlib import Path + +# snippet:begin +import tempfile +from dataclasses import asdict, dataclass +from pathlib import Path + +from cultcache_py import CultCache, SingleFileMessagePackBackingStore, define_document_type + + +@dataclass +class Item: + name: str + category: str + value: int + + +item_doc = define_document_type( + "item", + encode=lambda item: asdict(item), + decode=lambda raw: Item(**raw), + name="name", + indexes={"category": "category"}, +) + + +def first_call() -> Item: + path = Path(tempfile.mkdtemp()) / "first-call.cc" + + cache = ( + CultCache.builder() + .register_document_type(item_doc) + .add_generic_store(SingleFileMessagePackBackingStore(path)) + .build() + ) + cache.pull_all_backing_stores() + cache.put(item_doc, "item:potion", Item("Potion", "Consumable", 50)) # written to the .cc file + + reopened = ( + CultCache.builder() + .register_document_type(item_doc) + .add_generic_store(SingleFileMessagePackBackingStore(path)) + .build() + ) + reopened.pull_all_backing_stores() # read it back + assert reopened.get_by_name(item_doc, "Potion") == reopened.get_required(item_doc, "item:potion") + return reopened.get_required(item_doc, "item:potion") +# snippet:end + + +class FirstCallTests(unittest.TestCase): + def test_first_call_round_trips_through_a_second_cache(self) -> None: + self.assertEqual(first_call(), Item("Potion", "Consumable", 50)) + + def test_docs_show_the_tested_snippet(self) -> None: + source = Path(__file__).read_text(encoding="utf-8") + snippet = source.split("# snippet:begin\n", 1)[1].split("# snippet:end\n", 1)[0] + docs = (Path(__file__).parents[3] / "docs" / "start" / "README.md").read_text(encoding="utf-8") + self.assertIn("```python\n" + snippet + "```", docs) + + +if __name__ == "__main__": + unittest.main() diff --git a/packages/cultcache-rs/tests/first_call.rs b/packages/cultcache-rs/tests/first_call.rs new file mode 100644 index 00000000..6859ae07 --- /dev/null +++ b/packages/cultcache-rs/tests/first_call.rs @@ -0,0 +1,56 @@ +//! The Rust CultCache first call from docs/start/README.md. The docs show the region between the +//! `snippet` markers verbatim, and `docs_show_the_tested_snippet` fails when they differ. + +use cultcache_rs::{CultCache, DatabaseEntry, SingleFileMessagePackBackingStore}; + +// snippet:begin +#[derive(Clone, Debug, PartialEq, Eq, DatabaseEntry)] +#[cultcache(type = "item")] +struct Item { + #[cultcache(key = 0)] + name: String, + #[cultcache(key = 1)] + category: String, + #[cultcache(key = 2, default)] + value: u32, +} + +fn first_call() -> anyhow::Result<()> { + let dir = tempfile::tempdir()?; + let path = dir.path().join("first-call.cc"); + + let mut cache = CultCache::new(); + cache.register_entry_type::()?; + cache.add_generic_backing_store(SingleFileMessagePackBackingStore::new(&path)); + cache.pull_all_backing_stores()?; + cache.put("item:potion", &Item { + name: "Potion".to_string(), + category: "Consumable".to_string(), + value: 50, + })?; // written to the .cc file before this returns + + let mut reopened = CultCache::new(); + reopened.register_entry_type::()?; + reopened.add_generic_backing_store(SingleFileMessagePackBackingStore::new(&path)); + reopened.pull_all_backing_stores()?; // read it back + let potion = reopened.get_required::("item:potion")?; + assert_eq!(potion.name, "Potion"); + assert_eq!(potion.value, 50); + Ok(()) +} +// snippet:end + +#[test] +fn first_call_round_trips_through_a_second_cache() { + first_call().unwrap(); +} + +#[test] +fn docs_show_the_tested_snippet() { + let source = std::fs::read_to_string(concat!(env!("CARGO_MANIFEST_DIR"), "/tests/first_call.rs")).unwrap(); + let begin = source.find("// snippet:begin\n").unwrap() + "// snippet:begin\n".len(); + let end = source.find("// snippet:end\n").unwrap(); + let docs = std::fs::read_to_string(concat!(env!("CARGO_MANIFEST_DIR"), "/../../docs/start/README.md")).unwrap(); + let block = format!("```rust\n{}```", &source[begin..end]); + assert!(docs.contains(&block), "docs/start/README.md no longer shows tests/first_call.rs verbatim"); +}