Files
sausage/examples/notes/src/main.rs
T

85 lines
2.5 KiB
Rust

//! Persistent notes in `/state/notes/notes.json`.
//! Each call has its own process. The add method locks the file before its read-modify-write operation.
//! Atomic replacement lets readers observe a complete list without a lock.
use sausage_plugin::serde_json;
use sausage_plugin::{Result, lock, rpc, schema, state_dir, write_atomic};
/// The stateless notes plugin reads its file on each call.
struct Notes;
/// Parameters for a new note.
#[schema]
struct AddParams {
/// The complete note text, including any newlines.
text: String,
}
/// The result after a note enters the list.
#[schema]
struct Added {
/// The number of notes, including the new note.
count: usize,
}
#[rpc]
impl Notes {
/// Add a note and return the new count.
async fn add(&self, AddParams { text }: AddParams) -> Result<Added> {
let path = state_dir().join("notes.json");
let _guard = lock(&path).await?;
let mut notes = load()?;
notes.push(text);
std::fs::create_dir_all(state_dir())?;
write_atomic(path, serde_json::to_vec(&notes)?)?;
Ok(Added { count: notes.len() })
}
/// List all notes, oldest first.
fn list(&self) -> Result<Vec<String>> {
load()
}
}
/// Read the list, or return an empty list when the file does not exist.
fn load() -> Result<Vec<String>> {
match std::fs::read(state_dir().join("notes.json")) {
Ok(bytes) => Ok(serde_json::from_slice(&bytes)?),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(vec![]),
Err(e) => Err(e.into()),
}
}
/// Start the SDK transport for notes.
fn main() {
sausage_plugin::run(Notes)
}
/// Tests run in fresh worlds under the WASI test runner.
#[cfg(test)]
mod tests {
use super::*;
/// The core lock protects two sequential additions and their counts.
#[sausage_plugin::test]
async fn add_appends_and_counts() {
assert_eq!(
Notes
.add(AddParams {
text: "milk".into()
})
.await
.unwrap()
.count,
1
);
assert_eq!(
Notes
.add(AddParams {
text: "eggs".into()
})
.await
.unwrap()
.count,
2
);
assert_eq!(Notes.list().unwrap(), ["milk", "eggs"]);
}
/// A fresh test world has no notes file.
#[test]
fn list_is_empty_without_a_file() {
assert!(Notes.list().unwrap().is_empty());
}
}