bkpr-report: enhance tag syntax from {tag:replacement} to {tag?if-set:if-not-set}
What changed, and why it matters
This commit is a routine feature enhancement for the bookkeeper plugin's report formatting syntax. It expands template tag syntax so users can specify both 'if present' and 'if absent' text around a tag. There is no indication of a security bug being fixed; it is a pre-release user-facing improvement with added tests and documentation updates.
No security action required. Review as normal feature code; ensure parser recursion depth and error paths are adequately tested.
Security signals we found
No security-relevant keywords in commit title or message
No mention of vulnerability, CVE, bug, crash, leak, or exploit
Change is a syntax enhancement with expanded test coverage
Parsing logic includes error handling for unterminated/unknown tags
Evidence from the diff
The change refactors plugins/bkpr/report.c to support a C-style ternary-like tag syntax {tag?if-set:if-not-set} in addition to the previous {tag?fallback} form. It splits the old alt pointer into ifset and ifnotset sub-format trees, updates parsing to handle ‘:’ and ‘?’ delimiters, and adjusts formatting so zero amount fields are treated as absent only when conditionals are present. Tests and JSON schemas are updated accordingly.
Changed components
plugins/bkpr/report.cplugins/bkpr/test/run-format_event.cplugins/bkpr/test/run-parse_report.cdoc/schemas/bkpr-report.jsoncontrib/msggen/msggen/schema.jsontests/test_bookkeeper.pyInspect captured patch +286 / −123
diff --git a/contrib/msggen/msggen/schema.json b/contrib/msggen/msggen/schema.json
index 0dbe3289..3728d08d 100644
--- a/contrib/msggen/msggen/schema.json
+++ b/contrib/msggen/msggen/schema.json
@@ -5160,15 +5160,16 @@
"{currencydebit}: debit amount converted into bkpr-currency",
"{currencycreditdebit}: +credit or -debit (or 0) in bkpr-currency",
"",
- "If a field is unavailable, it expands to an empty string.",
+ "If a field is unavailable, it expands to an empty string (or 0 for credit, debit, fees and creditdebit).",
"",
- "You can provide fallback with ?, which is used when the tag is not present (or zero, for credit, debit, fees and creditdebit). This fallback can include more tags:",
- " * {outpoint?NONE}",
- " * {payment_id?txid: {txid?UNKNOWN}}",
- "The first one the outpoint, or NONE if that is not available. ",
- "The second prints the payment_id, or if that is not available, the string 'txid: ' followed by the txid, or if that is not available, 'txid: UNKNOWN'.",
- "",
- "The text after ? is used only if that tag would otherwise be empty.",
+ "Tags support C-style conditional syntax: {tag[?if-set][:if-not-set]}",
+ " * if-set: text to use when the tag is present (and non-zero for credit, debit, fees and creditdebit). Default is the tag value itself.",
+ " * if-not-set: text to use when the tag is absent (or zero for amount fields). Default is empty string (or 0 for amount fields).",
+ "Either or both parts may be omitted, and each part can itself contain tags. For example:",
+ " * {outpoint:NONE}: the outpoint value, or 'NONE' if not available",
+ " * {credit:0.00}: the credit value, or '0.00' if zero",
+ " * {outpoint?[{outpoint}]:NONE}: '[<value>]' if outpoint is available, or 'NONE' if not",
+ " * {payment_id:{txid:UNKNOWN}}: the payment_id, or the txid if no payment_id, or 'UNKNOWN' if neither",
"",
"To include a literal {, write {{."
]
diff --git a/doc/schemas/bkpr-report.json b/doc/schemas/bkpr-report.json
index 27d69c1a..1ae87b4f 100644
--- a/doc/schemas/bkpr-report.json
+++ b/doc/schemas/bkpr-report.json
@@ -46,15 +46,16 @@
"{currencydebit}: debit amount converted into bkpr-currency",
"{currencycreditdebit}: +credit or -debit (or 0) in bkpr-currency",
"",
- "If a field is unavailable, it expands to an empty string.",
+ "If a field is unavailable, it expands to an empty string (or 0 for credit, debit, fees and creditdebit).",
"",
- "You can provide fallback with ?, which is used when the tag is not present (or zero, for credit, debit, fees and creditdebit). This fallback can include more tags:",
- " * {outpoint?NONE}",
- " * {payment_id?txid: {txid?UNKNOWN}}",
- "The first one the outpoint, or NONE if that is not available. ",
- "The second prints the payment_id, or if that is not available, the string 'txid: ' followed by the txid, or if that is not available, 'txid: UNKNOWN'.",
- "",
- "The text after ? is used only if that tag would otherwise be empty.",
+ "Tags support C-style conditional syntax: {tag[?if-set][:if-not-set]}",
+ " * if-set: text to use when the tag is present (and non-zero for credit, debit, fees and creditdebit). Default is the tag value itself.",
+ " * if-not-set: text to use when the tag is absent (or zero for amount fields). Default is empty string (or 0 for amount fields).",
+ "Either or both parts may be omitted, and each part can itself contain tags. For example:",
+ " * {outpoint:NONE}: the outpoint value, or 'NONE' if not available",
+ " * {credit:0.00}: the credit value, or '0.00' if zero",
+ " * {outpoint?[{outpoint}]:NONE}: '[<value>]' if outpoint is available, or 'NONE' if not",
+ " * {payment_id:{txid:UNKNOWN}}: the payment_id, or the txid if no payment_id, or 'UNKNOWN' if neither",
"",
"To include a literal {, write {{."
]
diff --git a/plugins/bkpr/report.c b/plugins/bkpr/report.c
index fa4d4fa6..17822ca4 100644
--- a/plugins/bkpr/report.c
+++ b/plugins/bkpr/report.c
@@ -220,8 +220,10 @@ struct report_format {
const struct bkpr *bkpr,
const struct income_event *e);
const char **str;
- /* If fmt returns NULL, evaluate these instead. */
- struct report_format **alt;
+ /* If fmt returns non-NULL (and non-ZERO_AMOUNT), evaluate these instead of the value. */
+ struct report_format **ifset;
+ /* If fmt returns NULL (or ZERO_AMOUNT when either ifset/ifnotset is non-NULL), evaluate these. */
+ struct report_format **ifnotset;
};
static void add_literal(struct report_format *f,
@@ -231,15 +233,18 @@ static void add_literal(struct report_format *f,
tal_arr_expand(&f->fmt, NULL);
tal_arr_expand(&f->str,
tal_strndup(f->str, *start, end - *start));
- tal_arr_expand(&f->alt, NULL);
+ tal_arr_expand(&f->ifset, NULL);
+ tal_arr_expand(&f->ifnotset, NULL);
*start = end;
}
}
+/* alt_term is a secondary loop terminator (in addition to term); '\0' means none. */
static struct report_format *
parse_report_format(const tal_t *ctx,
const char **start,
char term,
+ char alt_term,
const char **err)
{
const char *p;
@@ -248,11 +253,12 @@ parse_report_format(const tal_t *ctx,
f = tal(ctx, struct report_format);
f->fmt = tal_arr(f, typeof(*f->fmt), 0);
f->str = tal_arr(f, const char *, 0);
- f->alt = tal_arr(f, struct report_format *, 0);
+ f->ifset = tal_arr(f, struct report_format *, 0);
+ f->ifnotset = tal_arr(f, struct report_format *, 0);
p = *start;
- while (*p != term) {
- struct report_format *alt;
+ while (*p != term && !(alt_term && *p == alt_term)) {
+ struct report_format *ifset, *ifnotset;
const struct report_tag *rt;
if (*p == '\0') {
@@ -273,7 +279,8 @@ parse_report_format(const tal_t *ctx,
lit = tal_strcat(tmpctx, take(lit), "{");
tal_arr_expand(&f->fmt, NULL);
tal_arr_expand(&f->str, lit);
- tal_arr_expand(&f->alt, NULL);
+ tal_arr_expand(&f->ifset, NULL);
+ tal_arr_expand(&f->ifnotset, NULL);
p += 2;
*start = p;
continue;
@@ -282,7 +289,7 @@ parse_report_format(const tal_t *ctx,
/* Emit preceding literal, if any. */
add_literal(f, start, p);
- const char *endtag = p + 1 + strcspn(p+1, "?}");
+ const char *endtag = p + 1 + strcspn(p+1, "?:}");
if (*endtag == '\0') {
*err = tal_fmt(ctx, "Unterminated tag %s", p + 1);
return tal_free(f);
@@ -296,25 +303,45 @@ parse_report_format(const tal_t *ctx,
return tal_free(f);
}
+ ifset = ifnotset = NULL;
if (*endtag == '?') {
+ /* Parse if-set, which ends at ':' or '}' */
*start = endtag + 1;
- alt = parse_report_format(f, start, '}', err);
- if (!alt) {
- /* Steal error upwards! */
+ ifset = parse_report_format(f, start, '}', ':', err);
+ if (!ifset) {
tal_steal(ctx, *err);
return tal_free(f);
}
- /* Consume final } */
+ if (**start == ':') {
+ /* Parse if-not-set */
+ (*start)++;
+ ifnotset = parse_report_format(f, start, '}', '\0', err);
+ if (!ifnotset) {
+ tal_steal(ctx, *err);
+ return tal_free(f);
+ }
+ }
+ /* Consume final '}' */
+ (*start)++;
+ } else if (*endtag == ':') {
+ /* Only if-not-set */
+ *start = endtag + 1;
+ ifnotset = parse_report_format(f, start, '}', '\0', err);
+ if (!ifnotset) {
+ tal_steal(ctx, *err);
+ return tal_free(f);
+ }
+ /* Consume final '}' */
(*start)++;
} else {
assert(*endtag == '}');
- alt = NULL;
*start = endtag + 1;
}
tal_arr_expand(&f->fmt, rt->fmt);
tal_arr_expand(&f->str, NULL);
- tal_arr_expand(&f->alt, alt);
+ tal_arr_expand(&f->ifset, ifset);
+ tal_arr_expand(&f->ifnotset, ifnotset);
p = *start;
}
@@ -336,7 +363,7 @@ struct command_result *param_report_format(struct command *cmd,
if (ret)
return ret;
- *format = parse_report_format(cmd, &start, '\0', &err);
+ *format = parse_report_format(cmd, &start, '\0', '\0', &err);
if (!*format)
return command_fail_badparam(cmd, name, buffer, tok, err);
@@ -420,19 +447,24 @@ static char *format_event(const tal_t *ctx,
}
v = fmt->fmt[i](tmpctx, bkpr, e);
- /* If there's an alternative, we treat ZERO_AMOUNT as missing. */
- if (v == ZERO_AMOUNT && fmt->alt[i])
+ /* Treat ZERO_AMOUNT as absent when there are conditionals. */
+ if (v == ZERO_AMOUNT && (fmt->ifset[i] || fmt->ifnotset[i]))
v = NULL;
if (v) {
- v = escape_value(tmpctx, v, esc);
- out = tal_strcat(ctx, take(out), v);
+ if (fmt->ifset[i]) {
+ out = tal_strcat(ctx, take(out),
+ format_event(tmpctx, fmt->ifset[i], esc, bkpr, e));
+ } else {
+ out = tal_strcat(ctx, take(out),
+ escape_value(tmpctx, v, esc));
+ }
continue;
}
- if (fmt->alt[i]) {
- char *alt = format_event(tmpctx, fmt->alt[i], esc, bkpr, e);
- out = tal_strcat(ctx, take(out), alt);
+ if (fmt->ifnotset[i]) {
+ out = tal_strcat(ctx, take(out),
+ format_event(tmpctx, fmt->ifnotset[i], esc, bkpr, e));
}
}
diff --git a/plugins/bkpr/test/run-format_event.c b/plugins/bkpr/test/run-format_event.c
index 36256659..2654dae7 100644
--- a/plugins/bkpr/test/run-format_event.c
+++ b/plugins/bkpr/test/run-format_event.c
@@ -73,6 +73,21 @@ struct income_event **list_income_events(const tal_t *ctx UNNEEDED,
{ fprintf(stderr, "list_income_events called!\n"); abort(); }
/* AUTOGENERATED MOCKS END */
+/* Helper: make a literal-only format with one entry */
+static struct report_format *make_literal(const tal_t *ctx, const char *s)
+{
+ struct report_format *f = tal(ctx, struct report_format);
+ f->fmt = tal_arr(f, typeof(*f->fmt), 1);
+ f->str = tal_arr(f, const char *, 1);
+ f->ifset = tal_arr(f, struct report_format *, 1);
+ f->ifnotset = tal_arr(f, struct report_format *, 1);
+ f->fmt[0] = NULL;
+ f->str[0] = tal_strdup(f, s);
+ f->ifset[0] = NULL;
+ f->ifnotset[0] = NULL;
+ return f;
+}
+
static void test_format_event_literal_only(void)
{
struct report_format *f = tal(tmpctx, struct report_format);
@@ -82,11 +97,13 @@ static void test_format_event_literal_only(void)
f->fmt = tal_arr(f, typeof(*f->fmt), 1);
f->str = tal_arr(f, const char *, 1);
- f->alt = tal_arr(f, struct report_format *, 1);
+ f->ifset = tal_arr(f, struct report_format *, 1);
+ f->ifnotset = tal_arr(f, struct report_format *, 1);
f->fmt[0] = NULL;
f->str[0] = tal_strdup(f->str, "hello");
- f->alt[0] = NULL;
+ f->ifset[0] = NULL;
+ f->ifnotset[0] = NULL;
s = format_event(tmpctx, f, REPORT_FMT_NONE, bkpr, e);
assert(streq(s, "hello"));
@@ -103,57 +120,56 @@ static void test_format_event_simple_tag(void)
f->fmt = tal_arr(f, typeof(*f->fmt), 3);
f->str = tal_arr(f, const char *, 3);
- f->alt = tal_arr(f, struct report_format *, 3);
+ f->ifset = tal_arr(f, struct report_format *, 3);
+ f->ifnotset = tal_arr(f, struct report_format *, 3);
f->fmt[0] = NULL;
f->str[0] = tal_strdup(f->str, "tag=");
- f->alt[0] = NULL;
+ f->ifset[0] = NULL;
+ f->ifnotset[0] = NULL;
f->fmt[1] = report_fmt_tag;
f->str[1] = NULL;
- f->alt[1] = NULL;
+ f->ifset[1] = NULL;
+ f->ifnotset[1] = NULL;
f->fmt[2] = NULL;
f->str[2] = tal_strdup(f->str, "!");
- f->alt[2] = NULL;
+ f->ifset[2] = NULL;
+ f->ifnotset[2] = NULL;
s = format_event(tmpctx, f, REPORT_FMT_NONE, bkpr, e);
assert(streq(s, "tag=invoice!"));
}
+/* {payment_id:NONE} - if-not-set fallback */
static void test_format_event_fallback(void)
{
struct report_format *f = tal(tmpctx, struct report_format);
- struct report_format *alt = tal(f, struct report_format);
struct income_event *e = tal(tmpctx, struct income_event);
struct bkpr *bkpr = tal(tmpctx, struct bkpr);
char *s;
- alt->fmt = tal_arr(alt, typeof(*alt->fmt), 1);
- alt->str = tal_arr(alt, const char *, 1);
- alt->alt = tal_arr(alt, struct report_format *, 1);
- alt->fmt[0] = NULL;
- alt->str[0] = tal_strdup(alt->str, "NONE");
- alt->alt[0] = NULL;
-
f->fmt = tal_arr(f, typeof(*f->fmt), 1);
f->str = tal_arr(f, const char *, 1);
- f->alt = tal_arr(f, struct report_format *, 1);
+ f->ifset = tal_arr(f, struct report_format *, 1);
+ f->ifnotset = tal_arr(f, struct report_format *, 1);
f->fmt[0] = report_fmt_payment_id;
f->str[0] = NULL;
- f->alt[0] = alt;
+ f->ifset[0] = NULL;
+ f->ifnotset[0] = make_literal(f, "NONE");
e->payment_id = NULL;
s = format_event(tmpctx, f, REPORT_FMT_NONE, bkpr, e);
assert(streq(s, "NONE"));
}
+/* {description:{txid:LAST}} - nested if-not-set */
static void test_format_event_nested_fallback(void)
{
struct report_format *f = tal(tmpctx, struct report_format);
- struct report_format *alt1 = tal(f, struct report_format);
- struct report_format *alt2 = tal(f, struct report_format);
+ struct report_format *ifnotset1 = tal(f, struct report_format);
struct income_event *e = tal(tmpctx, struct income_event);
struct bkpr *bkpr = tal(tmpctx, struct bkpr);
char *s;
@@ -161,26 +177,23 @@ static void test_format_event_nested_fallback(void)
e->txid = NULL;
e->desc = NULL;
- alt2->fmt = tal_arr(alt2, typeof(*alt2->fmt), 1);
- alt2->str = tal_arr(alt2, const char *, 1);
- alt2->alt = tal_arr(alt2, struct report_format *, 1);
- alt2->fmt[0] = NULL;
- alt2->str[0] = tal_strdup(alt2->str, "LAST");
- alt2->alt[0] = NULL;
-
- alt1->fmt = tal_arr(alt1, typeof(*alt1->fmt), 1);
- alt1->str = tal_arr(alt1, const char *, 1);
- alt1->alt = tal_arr(alt1, struct report_format *, 1);
- alt1->fmt[0] = report_fmt_txid;
- alt1->str[0] = NULL;
- alt1->alt[0] = alt2;
+ ifnotset1->fmt = tal_arr(ifnotset1, typeof(*ifnotset1->fmt), 1);
+ ifnotset1->str = tal_arr(ifnotset1, const char *, 1);
+ ifnotset1->ifset = tal_arr(ifnotset1, struct report_format *, 1);
+ ifnotset1->ifnotset = tal_arr(ifnotset1, struct report_format *, 1);
+ ifnotset1->fmt[0] = report_fmt_txid;
+ ifnotset1->str[0] = NULL;
+ ifnotset1->ifset[0] = NULL;
+ ifnotset1->ifnotset[0] = make_literal(ifnotset1, "LAST");
f->fmt = tal_arr(f, typeof(*f->fmt), 1);
f->str = tal_arr(f, const char *, 1);
- f->alt = tal_arr(f, struct report_format *, 1);
+ f->ifset = tal_arr(f, struct report_format *, 1);
+ f->ifnotset = tal_arr(f, struct report_format *, 1);
f->fmt[0] = report_fmt_desc;
f->str[0] = NULL;
- f->alt[0] = alt1;
+ f->ifset[0] = NULL;
+ f->ifnotset[0] = ifnotset1;
s = format_event(tmpctx, f, REPORT_FMT_NONE, bkpr, e);
assert(streq(s, "LAST"));
@@ -197,11 +210,13 @@ static void test_format_event_csv_escape_value(void)
f->fmt = tal_arr(f, typeof(*f->fmt), 1);
f->str = tal_arr(f, const char *, 1);
- f->alt = tal_arr(f, struct report_format *, 1);
+ f->ifset = tal_arr(f, struct report_format *, 1);
+ f->ifnotset = tal_arr(f, struct report_format *, 1);
f->fmt[0] = report_fmt_desc;
f->str[0] = NULL;
- f->alt[0] = NULL;
+ f->ifset[0] = NULL;
+ f->ifnotset[0] = NULL;
s = format_event(tmpctx, f, REPORT_FMT_CSV, bkpr, e);
assert(streq(s, "\"hello, \"\"world\"\"\""));
@@ -218,24 +233,93 @@ static void test_format_event_missing_no_fallback(void)
f->fmt = tal_arr(f, typeof(*f->fmt), 3);
f->str = tal_arr(f, const char *, 3);
- f->alt = tal_arr(f, struct report_format *, 3);
+ f->ifset = tal_arr(f, struct report_format *, 3);
+ f->ifnotset = tal_arr(f, struct report_format *, 3);
f->fmt[0] = NULL;
f->str[0] = tal_strdup(f->str, "A");
- f->alt[0] = NULL;
+ f->ifset[0] = NULL;
+ f->ifnotset[0] = NULL;
f->fmt[1] = report_fmt_payment_id;
f->str[1] = NULL;
- f->alt[1] = NULL;
+ f->ifset[1] = NULL;
+ f->ifnotset[1] = NULL;
f->fmt[2] = NULL;
f->str[2] = tal_strdup(f->str, "B");
- f->alt[2] = NULL;
+ f->ifset[2] = NULL;
+ f->ifnotset[2] = NULL;
s = format_event(tmpctx, f, REPORT_FMT_NONE, bkpr, e);
assert(streq(s, "AB"));
}
+/* {credit?+{credit}:0} - ifset wraps value, ifnotset is "0" */
+static void test_format_event_ifset(void)
+{
+ struct report_format *f = tal(tmpctx, struct report_format);
+ struct report_format *ifset = tal(f, struct report_format);
+ struct income_event *e = tal(tmpctx, struct income_event);
+ struct bkpr *bkpr = tal(tmpctx, struct bkpr);
+ char *s;
+
+ /* ifset format: "+{credit}" */
+ ifset->fmt = tal_arr(ifset, typeof(*ifset->fmt), 2);
+ ifset->str = tal_arr(ifset, const char *, 2);
+ ifset->ifset = tal_arr(ifset, struct report_format *, 2);
+ ifset->ifnotset = tal_arr(ifset, struct report_format *, 2);
+ ifset->fmt[0] = NULL;
+ ifset->str[0] = tal_strdup(ifset, "+");
+ ifset->ifset[0] = NULL;
+ ifset->ifnotset[0] = NULL;
+ ifset->fmt[1] = report_fmt_credit;
+ ifset->str[1] = NULL;
+ ifset->ifset[1] = NULL;
+ ifset->ifnotset[1] = NULL;
+
+ f->fmt = tal_arr(f, typeof(*f->fmt), 1);
+ f->str = tal_arr(f, const char *, 1);
+ f->ifset = tal_arr(f, struct report_format *, 1);
+ f->ifnotset = tal_arr(f, struct report_format *, 1);
+ f->fmt[0] = report_fmt_credit;
+ f->str[0] = NULL;
+ f->ifset[0] = ifset;
+ f->ifnotset[0] = make_literal(f, "0");
+
+ /* Non-zero credit: ifset fires, output "+<amount>" */
+ e->credit = AMOUNT_MSAT(100000000);
+ s = format_event(tmpctx, f, REPORT_FMT_NONE, bkpr, e);
+ assert(strstr(s, "+"));
+
+ /* Zero credit: ifnotset fires, output "0" */
+ e->credit = AMOUNT_MSAT(0);
+ s = format_event(tmpctx, f, REPORT_FMT_NONE, bkpr, e);
+ assert(streq(s, "0"));
+}
+
+/* {credit} with no conditionals: zero outputs "0" (ZERO_AMOUNT passthrough) */
+static void test_format_event_zero_amount_no_conditionals(void)
+{
+ struct report_format *f = tal(tmpctx, struct report_format);
+ struct income_event *e = tal(tmpctx, struct income_event);
+ struct bkpr *bkpr = tal(tmpctx, struct bkpr);
+ char *s;
+
+ f->fmt = tal_arr(f, typeof(*f->fmt), 1);
+ f->str = tal_arr(f, const char *, 1);
+ f->ifset = tal_arr(f, struct report_format *, 1);
+ f->ifnotset = tal_arr(f, struct report_format *, 1);
+ f->fmt[0] = report_fmt_credit;
+ f->str[0] = NULL;
+ f->ifset[0] = NULL;
+ f->ifnotset[0] = NULL;
+
+ e->credit = AMOUNT_MSAT(0);
+ s = format_event(tmpctx, f, REPORT_FMT_NONE, bkpr, e);
+ assert(streq(s, "0"));
+}
+
int main(int argc, char *argv[])
{
common_setup(argv[0]);
@@ -245,6 +329,8 @@ int main(int argc, char *argv[])
test_format_event_nested_fallback();
test_format_event_csv_escape_value();
test_format_event_missing_no_fallback();
+ test_format_event_ifset();
+ test_format_event_zero_amount_no_conditionals();
common_shutdown();
}
diff --git a/plugins/bkpr/test/run-parse_report.c b/plugins/bkpr/test/run-parse_report.c
index eb5e91cd..69da2676 100644
--- a/plugins/bkpr/test/run-parse_report.c
+++ b/plugins/bkpr/test/run-parse_report.c
@@ -72,22 +72,25 @@ static void test_parse_report_format_simple(void)
struct report_format *f;
const char *err, *start = "hello {tag} world";
- f = parse_report_format(tmpctx, &start, '\0', &err);
+ f = parse_report_format(tmpctx, &start, '\0', '\0', &err);
assert(f);
assert(streq(start, ""));
assert(tal_count(f->fmt) == 3);
assert(f->fmt[0] == NULL);
assert(streq(f->str[0], "hello "));
- assert(f->alt[0] == NULL);
+ assert(f->ifset[0] == NULL);
+ assert(f->ifnotset[0] == NULL);
assert(f->fmt[1] == report_fmt_tag);
assert(f->str[1] == NULL);
- assert(f->alt[1] == NULL);
+ assert(f->ifset[1] == NULL);
+ assert(f->ifnotset[1] == NULL);
assert(f->fmt[2] == NULL);
assert(streq(f->str[2], " world"));
- assert(f->alt[2] == NULL);
+ assert(f->ifset[2] == NULL);
+ assert(f->ifnotset[2] == NULL);
}
static void test_parse_report_format_escaped_open_brace(void)
@@ -95,7 +98,7 @@ static void test_parse_report_format_escaped_open_brace(void)
struct report_format *f;
const char *err, *start = "a{{b";
- f = parse_report_format(tmpctx, &start, '\0', &err);
+ f = parse_report_format(tmpctx, &start, '\0', '\0', &err);
assert(f);
assert(streq(start, ""));
@@ -105,92 +108,131 @@ static void test_parse_report_format_escaped_open_brace(void)
assert(tal_count(f->str) == 2);
assert(streq(f->str[0], "a{"));
assert(streq(f->str[1], "b"));
- assert(tal_count(f->alt) == 2);
- assert(f->alt[0] == NULL);
- assert(f->alt[1] == NULL);
+ assert(tal_count(f->ifnotset) == 2);
+ assert(f->ifnotset[0] == NULL);
+ assert(f->ifnotset[1] == NULL);
}
-static void test_parse_report_format_alt_simple(void)
+/* {description:NONE} => description, or NONE if absent */
+static void test_parse_report_format_ifnotset_simple(void)
{
struct report_format *f;
- const char *err, *start = "{description?NONE}";
+ const char *err, *start = "{description:NONE}";
- f = parse_report_format(tmpctx, &start, '\0', &err);
+ f = parse_report_format(tmpctx, &start, '\0', '\0', &err);
assert(f);
assert(streq(start, ""));
assert(tal_count(f->fmt) == 1);
assert(f->fmt[0] == report_fmt_desc);
assert(f->str[0] == NULL);
- assert(f->alt[0] != NULL);
+ assert(f->ifset[0] == NULL);
+ assert(f->ifnotset[0] != NULL);
- assert(tal_count(f->alt[0]->fmt) == 1);
- assert(f->alt[0]->fmt[0] == NULL);
- assert(streq(f->alt[0]->str[0], "NONE"));
- assert(f->alt[0]->alt[0] == NULL);
+ assert(tal_count(f->ifnotset[0]->fmt) == 1);
+ assert(f->ifnotset[0]->fmt[0] == NULL);
+ assert(streq(f->ifnotset[0]->str[0], "NONE"));
+ assert(f->ifnotset[0]->ifnotset[0] == NULL);
}
-static void test_parse_report_format_alt_nested(void)
+/* {description:{txid:{outpoint:NONE}}} => nested if-not-set */
+static void test_parse_report_format_ifnotset_nested(void)
{
struct report_format *f, *a1, *a2;
- const char *err, *start = "{description?{txid?{outpoint?NONE}}}";
+ const char *err, *start = "{description:{txid:{outpoint:NONE}}}";
- f = parse_report_format(tmpctx, &start, '\0', &err);
+ f = parse_report_format(tmpctx, &start, '\0', '\0', &err);
assert(f);
assert(streq(start, ""));
assert(tal_count(f->fmt) == 1);
assert(f->fmt[0] == report_fmt_desc);
assert(f->str[0] == NULL);
- assert(f->alt[0] != NULL);
+ assert(f->ifset[0] == NULL);
+ assert(f->ifnotset[0] != NULL);
- a1 = f->alt[0];
+ a1 = f->ifnotset[0];
assert(tal_count(a1->fmt) == 1);
assert(a1->fmt[0] == report_fmt_txid);
assert(a1->str[0] == NULL);
- assert(a1->alt[0] != NULL);
+ assert(a1->ifset[0] == NULL);
+ assert(a1->ifnotset[0] != NULL);
- a2 = a1->alt[0];
+ a2 = a1->ifnotset[0];
assert(tal_count(a2->fmt) == 1);
assert(a2->fmt[0] == report_fmt_outpoint);
assert(a2->str[0] == NULL);
- assert(a2->alt[0] != NULL);
+ assert(a2->ifset[0] == NULL);
+ assert(a2->ifnotset[0] != NULL);
- assert(tal_count(a2->alt[0]->fmt) == 1);
- assert(a2->alt[0]->fmt[0] == NULL);
- assert(streq(a2->alt[0]->str[0], "NONE"));
- assert(a2->alt[0]->alt[0] == NULL);
+ assert(tal_count(a2->ifnotset[0]->fmt) == 1);
+ assert(a2->ifnotset[0]->fmt[0] == NULL);
+ assert(streq(a2->ifnotset[0]->str[0], "NONE"));
+ assert(a2->ifnotset[0]->ifnotset[0] == NULL);
}
-static void test_parse_report_format_alt_with_suffix(void)
+/* {description:{txid}X}Y => if-not-set with suffix */
+static void test_parse_report_format_ifnotset_with_suffix(void)
{
struct report_format *f, *a1;
- const char *err, *start = "{description?{txid}X}Y";
+ const char *err, *start = "{description:{txid}X}Y";
- f = parse_report_format(tmpctx, &start, '\0', &err);
+ f = parse_report_format(tmpctx, &start, '\0', '\0', &err);
assert(f);
assert(streq(start, ""));
assert(tal_count(f->fmt) == 2);
assert(f->fmt[0] == report_fmt_desc);
- assert(f->alt[0] != NULL);
+ assert(f->ifset[0] == NULL);
+ assert(f->ifnotset[0] != NULL);
assert(f->fmt[1] == NULL);
assert(streq(f->str[1], "Y"));
- a1 = f->alt[0];
+ a1 = f->ifnotset[0];
assert(tal_count(a1->fmt) == 2);
assert(a1->fmt[0] == report_fmt_txid);
- assert(a1->alt[0] == NULL);
+ assert(a1->ifnotset[0] == NULL);
assert(a1->fmt[1] == NULL);
assert(streq(a1->str[1], "X"));
}
+/* {credit?+{credit}:0} => if-set and if-not-set */
+static void test_parse_report_format_ifset_and_ifnotset(void)
+{
+ struct report_format *f, *s1;
+ const char *err, *start = "{credit?+{credit}:0}";
+
+ f = parse_report_format(tmpctx, &start, '\0', '\0', &err);
+ assert(f);
+ assert(streq(start, ""));
+
+ assert(tal_count(f->fmt) == 1);
+ assert(f->fmt[0] == report_fmt_credit);
+ assert(f->str[0] == NULL);
+ assert(f->ifset[0] != NULL);
+ assert(f->ifnotset[0] != NULL);
+
+ /* if-set: "+{credit}" */
+ s1 = f->ifset[0];
+ assert(tal_count(s1->fmt) == 2);
+ assert(s1->fmt[0] == NULL);
+ assert(streq(s1->str[0], "+"));
+ assert(s1->fmt[1] == report_fmt_credit);
+ assert(s1->ifset[1] == NULL);
+ assert(s1->ifnotset[1] == NULL);
+
+ /* if-not-set: "0" */
+ assert(tal_count(f->ifnotset[0]->fmt) == 1);
+ assert(f->ifnotset[0]->fmt[0] == NULL);
+ assert(streq(f->ifnotset[0]->str[0], "0"));
+}
+
static void test_parse_report_format_unknown_tag(void)
{
struct report_format *f;
const char *err, *start = "{nope}";
- f = parse_report_format(tmpctx, &start, '\0', &err);
+ f = parse_report_format(tmpctx, &start, '\0', '\0', &err);
assert(!f);
assert(err);
assert(strstr(err, "Unknown tag nope"));
@@ -201,7 +243,7 @@ static void test_parse_report_format_unterminated_tag(void)
struct report_format *f;
const char *err, *start = "{description";
- f = parse_report_format(tmpctx, &start, '\0', &err);
+ f = parse_report_format(tmpctx, &start, '\0', '\0', &err);
assert(!f);
assert(err);
assert(strstr(err, "Unterminated tag"));
@@ -210,9 +252,9 @@ static void test_parse_report_format_unterminated_tag(void)
static void test_parse_report_format_unterminated_nested_alt(void)
{
struct report_format *f;
- const char *err, *start = "{description?{txid}";
+ const char *err, *start = "{description:{txid}";
- f = parse_report_format(tmpctx, &start, '\0', &err);
+ f = parse_report_format(tmpctx, &start, '\0', '\0', &err);
assert(!f);
assert(err);
assert(strstr(err, "Unterminated tag"));
@@ -221,9 +263,9 @@ static void test_parse_report_format_unterminated_nested_alt(void)
static void test_parse_report_format_unknown_nested_tag(void)
{
struct report_format *f;
- const char *err, *start = "{description?{nope}}";
+ const char *err, *start = "{description:{nope}}";
- f = parse_report_format(tmpctx, &start, '\0', &err);
+ f = parse_report_format(tmpctx, &start, '\0', '\0', &err);
assert(!f);
assert(err);
assert(strstr(err, "Unknown tag nope"));
@@ -234,9 +276,10 @@ int main(int argc, char *argv[])
common_setup(argv[0]);
test_parse_report_format_simple();
test_parse_report_format_escaped_open_brace();
- test_parse_report_format_alt_simple();
- test_parse_report_format_alt_nested();
- test_parse_report_format_alt_with_suffix();
+ test_parse_report_format_ifnotset_simple();
+ test_parse_report_format_ifnotset_nested();
+ test_parse_report_format_ifnotset_with_suffix();
+ test_parse_report_format_ifset_and_ifnotset();
test_parse_report_format_unknown_tag();
test_parse_report_format_unterminated_tag();
test_parse_report_format_unterminated_nested_alt();
diff --git a/tests/test_bookkeeper.py b/tests/test_bookkeeper.py
index fb6b23b3..1883c125 100644
--- a/tests/test_bookkeeper.py
+++ b/tests/test_bookkeeper.py
@@ -1251,7 +1251,7 @@ def test_bkpr_report_tags_and_fallback(node_factory):
res = l1.rpc.call(
"bkpr-report",
{
- "format": "{tag}|{account}|{outpoint?NONE}|{txid?NONE}|{payment_id?NONE}|{bkpr-currency?NONE}|{currencyrate?NONE}",
+ "format": "{tag}|{account}|{outpoint:NONE}|{txid:NONE}|{payment_id:NONE}|{bkpr-currency:NONE}|{currencyrate:NONE}",
"headers": ['tag",account,outpoint,txid,payment_id,bkpr-currency,currencyrate', ",,,,,,"],
},
)
@@ -1275,7 +1275,7 @@ def test_bkpr_report_tags_and_fallback(node_factory):
assert any(r[2] == "NONE" or r[3] == "NONE" or r[4] == "NONE" for r in rows)
# Fancier fields should work, too
- res = l1.rpc.bkpr_report(format="{tag}|{account}|{credit}|{debit}|{creditdebit}|{currencycredit}|{currencydebit}|{currencycreditdebit}|{credit?NONE}")
+ res = l1.rpc.bkpr_report(format="{tag}|{account}|{credit}|{debit}|{creditdebit}|{currencycredit}|{currencydebit}|{currencycreditdebit}|{credit:NONE}")
rows = [line.split("|") for line in res["report"]]
for r in rows:
@@ -1315,7 +1315,7 @@ def test_bkpr_report_invoice(node_factory):
assert invline == f"invoice,{cid},test_bkpr_report_invoice,-0.00000123456,"
# Test nested tags while we're here!
- lines = l1.rpc.bkpr_report(format="{tag},{account},{description},{outpoint},{txid},{description?{outpoint?txid: {txid?UNKNOWN}}},{creditdebit}", escape='csv')['report']
+ lines = l1.rpc.bkpr_report(format="{tag},{account},{description},{outpoint},{txid},{description:{outpoint:txid: {txid:UNKNOWN}}},{creditdebit}", escape='csv')['report']
for l in lines[1:]:
parts = l.split(',')
if parts[2] != '':
Why this scored 21/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.