Tag Sync Script
Introduction
This script synchronizes tags between a transaction and its single split. When a transaction is created with exactly one split (two entries: positive and negative), it copies tags bidirectionally:
- If the transaction has tags but the split does not, copy them to the split
- If the split has tags but the transaction does not, copy them to the transaction
Cargo configuration
[package]
name = "tag-sync"
version = "0.1.0"
edition = "2021"
[lib]
name = "tag_sync"
crate-type = ["cdylib"]
[dependencies]
scripting-sdk = { path = "../../scripting/sdk", default-features = false }
scripting-sdk-macro = { path = "../../scripting/sdk-macro" }
[profile.release]
opt-level = "s"
lto = true
panic = "abort"
[profile.dev]
panic = "abort"
[workspace]
Header
#![no_std] use scripting_sdk::prelude::*; use scripting_sdk_macro::script;
Checking the applicability
We only apply when the primary entity is a transaction with exactly two splits (one logical split entry = two accounting entries).
fn is_single_split_transaction(ctx: &Context) -> bool {
if ctx.primary_entity_type().ok() != Some(EntityType::Transaction) {
return false;
}
let primary_idx = ctx.primary_entity_idx();
ctx.splits_for(primary_idx).flatten().count() == 2
}
Synchronizing the tags
#[script(trigger_fn = is_single_split_transaction)]
fn sync_tags(ctx: &mut Context) -> Result<()> {
let primary_idx = ctx.primary_entity_idx();
Find the split entity indices by scanning all entities:
let mut split_indices = [0u32; 2];
let mut split_count = 0usize;
for idx in 0..ctx.entity_count() {
let Ok(entity) = ctx.entity(idx) else {
continue;
};
if entity.entity_type().ok() == Some(EntityType::Split)
&& entity.parent_idx() == primary_idx as i32
&& split_count < 2
{
split_indices[split_count] = idx;
split_count += 1;
}
}
if split_count != 2 {
return Ok(());
}
Collect tag counts for the transaction and each split. The "note" tag is a system tag set by the transaction form and should not count as a user tag for synchronization purposes:
fn is_user_tag(ctx: &Context, idx: u32) -> bool {
ctx.tag(idx)
.ok()
.and_then(|t| t.name().ok())
.is_some_and(|n| n != "note")
}
let tx_tag_count = (0..ctx.entity_count())
.filter(|&idx| {
ctx.entity(idx).ok().is_some_and(|e| {
e.entity_type().ok() == Some(EntityType::Tag)
&& e.parent_idx() == primary_idx as i32
&& is_user_tag(ctx, idx)
})
})
.count();
let split0_tag_count = ctx.tags_for(split_indices[0]).flatten().count();
let split1_tag_count = ctx.tags_for(split_indices[1]).flatten().count();
let splits_have_tags = split0_tag_count > 0 || split1_tag_count > 0;
Collect tag entity indices into a fixed buffer. We collect indices first to
avoid holding an immutable borrow on the context while calling create_tag:
fn collect_tag_indices(ctx: &Context, parent_idx: u32, buf: &mut [u32; 16]) -> usize {
let mut count = 0usize;
for idx in 0..ctx.entity_count() {
let Ok(entity) = ctx.entity(idx) else { continue };
if entity.entity_type().ok() == Some(EntityType::Tag)
&& entity.parent_idx() == parent_idx as i32
&& is_user_tag(ctx, idx)
&& count < 16
{
buf[count] = idx;
count += 1;
}
}
count
}
fn copy_tags(ctx: &mut Context, source_parent: u32, target: i32) -> Result<()> {
let mut tag_indices = [0u32; 16];
let count = collect_tag_indices(ctx, source_parent, &mut tag_indices);
for i in 0..count {
let mut name_buf = [0u8; 128];
let mut value_buf = [0u8; 128];
let (nlen, vlen) = {
let tag = ctx.tag(tag_indices[i])?;
let name = tag.name()?;
let value = tag.value()?;
let nl = name.len().min(128);
let vl = value.len().min(128);
name_buf[..nl].copy_from_slice(&name.as_bytes()[..nl]);
value_buf[..vl].copy_from_slice(&value.as_bytes()[..vl]);
(nl, vl)
};
let name = core::str::from_utf8(&name_buf[..nlen]).unwrap_or("");
let value = core::str::from_utf8(&value_buf[..vlen]).unwrap_or("");
ctx.create_tag(target, name, value)?;
}
Ok(())
}
Copy transaction tags to splits when splits have no tags:
if tx_tag_count > 0 && !splits_have_tags {
copy_tags(ctx, primary_idx, split_indices[0] as i32)?;
copy_tags(ctx, primary_idx, split_indices[1] as i32)?;
Copy split tags to transaction when transaction has no tags. We take tags from the first split that has any:
} else if tx_tag_count == 0 && splits_have_tags {
let source_idx = if split0_tag_count > 0 {
split_indices[0]
} else {
split_indices[1]
};
copy_tags(ctx, source_idx, primary_idx as i32)?;
}
Ok(())
}
Building
Use org-babel-tangle on this file and then:
cargo build --release --target=wasm32-unknown-unknown
to build target/wasm32-unknown-unknown/release/tag_sync.wasm