Lines
92.38 %
Functions
21.43 %
Branches
100 %
//! Render-only draft natives.
//!
//! These host fns take no database action; each mutates the
//! [`TransactionDraft`](crate::draft::TransactionDraft) held in the session's
//! Store data, which the render entry point reads back via `store.into_data()`.
//! They are *functionally* render-only: the linker binds them on BOTH the
//! normal eval channel ([`super::link`]) and the render surface
//! ([`super::link_render`]) — the import must resolve wherever a compiled module
//! references it — but the draft accumulator is armed only under render
//! ([`SessionData::for_render`]). On the normal channel `with_draft` finds no
//! draft and traps, so a non-render program can compile/link a draft native yet
//! never accumulate anything. That trap is a safety net, not the boundary; the
//! real gate is the render compiler-spec allowlist (a normal template/eval that
//! shouldn't see these simply doesn't have them in scope). They are harmless on
//! the normal channel either way: no DB, no secret, no mutation.
//! `draft-split` takes the entity refs returned by `get-account` /
//! `get-commodity` plus a rational amount; it reads each entity's `id` field
//! (slot 0, per `ENTITY_LAYOUTS`) host-side so the draft records stable uuids.
use nomiscript::EntityKind;
use scripting::runtime::{read_entity_string_field, read_ratio_arg, read_string_arg};
use wasmtime::{ArrayRef, Caller, Linker, Rooted, StructRef};
use crate::draft::{DraftSplit, DraftTag};
use crate::session::SessionData;
pub const REGISTERED_COMMANDS: &[&str] = &[
"set-draft-note",
"set-draft-date",
"draft-split",
"draft-tag",
"draft-split-tag",
];
type StringArg = Option<Rooted<ArrayRef>>;
type StructArg = Option<Rooted<StructRef>>;
type Fut<'a, O> = Box<dyn std::future::Future<Output = wasmtime::Result<O>> + Send + 'a>;
pub fn register(linker: &mut Linker<SessionData>) -> wasmtime::Result<()> {
linker.func_wrap_async(
"nomi",
"template_set_draft_note",
|mut caller: Caller<'_, SessionData>, (note_arg,): (StringArg,)| -> Fut<'_, i32> {
Box::new(async move {
let note = read_string_arg(&mut caller, note_arg)?
.ok_or_else(|| wasmtime::Error::msg("set-draft-note: missing note"))?;
caller.data().with_draft(|d| d.set_note(note))?;
Ok(1)
})
},
)?;
"template_set_draft_date",
|mut caller: Caller<'_, SessionData>, (date_arg,): (StringArg,)| -> Fut<'_, i32> {
let date = read_string_arg(&mut caller, date_arg)?
.ok_or_else(|| wasmtime::Error::msg("set-draft-date: missing date"))?;
caller.data().with_draft(|d| d.set_date(date))?;
"template_draft_split",
|mut caller: Caller<'_, SessionData>,
(account_arg, commodity_arg, amount_arg): (StructArg, StructArg, StructArg)|
-> Fut<'_, i32> {
let account_id =
read_entity_string_field(&mut caller, account_arg, EntityKind::Account, "id")?
.ok_or_else(|| wasmtime::Error::msg("draft-split: missing account"))?;
let commodity_id = read_entity_string_field(
&mut caller,
commodity_arg,
EntityKind::Commodity,
"id",
)?
.ok_or_else(|| wasmtime::Error::msg("draft-split: missing commodity"))?;
let (value_num, value_denom) = read_ratio_arg(&mut caller, amount_arg)?
.ok_or_else(|| wasmtime::Error::msg("draft-split: missing amount"))?;
// Return the split's index as the handle a template passes to
// `draft-split-tag` to tag this specific split. Reject the absurd
// >i32::MAX case BEFORE inserting so a failed call never leaves a
// partially-mutated draft (and a stale handle can never silently
// address the wrong split).
let mut index: Option<usize> = None;
caller.data().with_draft(|d| {
if i32::try_from(d.split_count()).is_ok() {
index = Some(d.add_split(DraftSplit {
account_id,
commodity_id,
value_num,
value_denom,
tags: Vec::new(),
}));
}
})?;
let index =
index.ok_or_else(|| wasmtime::Error::msg("draft-split: too many splits"))?;
i32::try_from(index)
.map_err(|_| wasmtime::Error::msg("draft-split: too many splits"))
"template_draft_tag",
(name_arg, value_arg): (StringArg, StringArg)|
let name = read_string_arg(&mut caller, name_arg)?
.ok_or_else(|| wasmtime::Error::msg("draft-tag: missing name"))?;
let value = read_string_arg(&mut caller, value_arg)?
.ok_or_else(|| wasmtime::Error::msg("draft-tag: missing value"))?;
caller
.data()
.with_draft(|d| d.add_tag(DraftTag { name, value }))?;
"template_draft_split_tag",
(index_arg, name_arg, value_arg): (i32, StringArg, StringArg)|
let index = usize::try_from(index_arg).map_err(|_| {
wasmtime::Error::msg("draft-split-tag: split index must be non-negative")
.ok_or_else(|| wasmtime::Error::msg("draft-split-tag: missing name"))?;
.ok_or_else(|| wasmtime::Error::msg("draft-split-tag: missing value"))?;
let mut ok = false;
.with_draft(|d| ok = d.add_split_tag(index, DraftTag { name, value }))?;
if !ok {
return Err(wasmtime::Error::msg(format!(
"draft-split-tag: no draft split at index {index} \
(pass the value returned by draft-split)"
)));
Ok(())