Lines
100 %
Functions
60 %
Branches
//! Transaction-draft accumulator for render-only template execution.
//!
//! A template is per-user nomiscript *source* that, when rendered, calls the
//! draft natives (`set-draft-note`, `set-draft-date`, `draft-split`,
//! `draft-tag`) to describe a not-yet-persisted transaction. Those natives
//! mutate a [`TransactionDraft`] held in the session's Store data; the render
//! entry point reads it back via `store.into_data()` after the program runs.
//! Nothing here touches the database — the draft is pure intent the web/CLI
//! layer turns into a pre-filled new-transaction form.
/// One split line a template requested: an account + commodity (both by uuid
/// string, resolved by the template via `get-account`/`get-commodity`), a
/// rational amount, and any per-split tags the template attached via
/// `draft-split-tag`.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DraftSplit {
pub account_id: String,
pub commodity_id: String,
pub value_num: i64,
pub value_denom: i64,
pub tags: Vec<DraftTag>,
}
/// A `(name, value)` tag a template asked to attach to the drafted transaction.
pub struct DraftTag {
pub name: String,
pub value: String,
/// The accumulated result of rendering a template: everything needed to
/// pre-fill a new-transaction form. All fields are optional/append-only; a
/// template sets what it knows and leaves the rest for the user.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct TransactionDraft {
pub note: Option<String>,
pub date: Option<String>,
pub splits: Vec<DraftSplit>,
impl TransactionDraft {
#[must_use]
pub fn new() -> Self {
Self::default()
pub fn set_note(&mut self, note: String) {
self.note = Some(note);
pub fn set_date(&mut self, date: String) {
self.date = Some(date);
/// The number of splits drafted so far — the index the next
/// [`add_split`](Self::add_split) will return.
pub fn split_count(&self) -> usize {
self.splits.len()
/// Appends a split and returns its index — the handle a template passes to
/// [`add_split_tag`](Self::add_split_tag) to tag that specific split.
pub fn add_split(&mut self, split: DraftSplit) -> usize {
self.splits.push(split);
self.splits.len() - 1
pub fn add_tag(&mut self, tag: DraftTag) {
self.tags.push(tag);
/// Attaches a tag to the split at `index`. Returns `false` if the index is
/// out of range (a template handed a stale/invalid handle).
pub fn add_split_tag(&mut self, index: usize, tag: DraftTag) -> bool {
match self.splits.get_mut(index) {
Some(split) => {
split.tags.push(tag);
true
None => false,
#[cfg(test)]
mod tests {
use super::*;
fn split() -> DraftSplit {
DraftSplit {
account_id: "a".to_string(),
commodity_id: "c".to_string(),
value_num: 1,
value_denom: 1,
tags: Vec::new(),
#[test]
fn split_count_predicts_next_add_split_index() {
let mut draft = TransactionDraft::new();
assert_eq!(draft.split_count(), 0);
assert_eq!(draft.add_split(split()), 0);
assert_eq!(draft.split_count(), 1);
assert_eq!(draft.add_split(split()), 1);
assert_eq!(draft.split_count(), 2);
fn add_split_tag_targets_only_the_indexed_split() {
let first = draft.add_split(split());
let second = draft.add_split(split());
assert!(draft.add_split_tag(
first,
DraftTag {
name: "memo".to_string(),
value: "lunch".to_string(),
));
assert_eq!(draft.splits[first].tags.len(), 1);
assert_eq!(draft.splits[first].tags[0].name, "memo");
assert!(draft.splits[second].tags.is_empty());
fn add_split_tag_rejects_out_of_range_handle() {
draft.add_split(split());
assert!(!draft.add_split_tag(
7,
name: "k".to_string(),
value: "v".to_string(),