diff --git a/contrib/msggen/msggen/schema.json b/contrib/msggen/msggen/schema.json index c8323e511f09..fa6af3208ef2 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 ?, including more variable:", - " * {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}: '[]' 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 {{." ] @@ -5226,6 +5227,30 @@ } } }, + "examples": [ + { + "request": { + "id": "example:bkpr-report#1", + "method": "bkpr-report", + "params": { + "headers": [ + "Date,Tag,Account,Description,Credit,Debit,BTC/USD,Credit (USD),Debit(USD)" + ], + "format": "{localtime},{tag},{account},{description?Invoice description {description}:{outpoint?Onchain output {outpoint}:{txid?Onchain transaction {txid}}}},{credit?+{credit}:0},{debit?-{debit}:0},{currencyrate},${currencycredit},${currencydebit}", + "escape": "csv" + } + }, + "response": { + "report": [ + "Date,Tag,Account,Description,Credit,Debit,BTC/USD,Credit (USD),Debit(USD)", + "2026-04-02 12:42:03,deposit,wallet,Onchain output 37562cf0b3a5d6783ae02925bcc486bb89afe156abf15e692736e5da9a060018:1,+0.02000000000,0,67140.89,$1342.81,$0.00", + "2026-04-02 12:42:04,invoice,9b3114c8003dc8dedbcf3b7bc9fb04e1575a1112aea33be8369ae9670aff64f4,Invoice description test_bkpr_report_invoice,0,-0.00000123456,67140.89,$0.00,$0.08", + "2026-04-02 12:42:04,onchain_fee,9b3114c8003dc8dedbcf3b7bc9fb04e1575a1112aea33be8369ae9670aff64f4,Onchain transaction f464ff0a67e99a36e83ba3ae12115a57e104fbc97b3bcfdbdec83d00c814319b,0,-0.00004927000,67140.89,$0.00,$3.30" + ], + "format-hint": "simple" + } + } + ], "author": [ "Rusty Russell [rusty@rustcorp.com.au](mailto:rusty@rustcorp.com.au) is mainly responsible." ], diff --git a/doc/schemas/bkpr-report.json b/doc/schemas/bkpr-report.json index 2eed404841de..430d5019fdf7 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 ?, including more variable:", - " * {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}: '[]' 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 {{." ] @@ -112,6 +113,30 @@ } } }, + "examples": [ + { + "request": { + "id": "example:bkpr-report#1", + "method": "bkpr-report", + "params": { + "headers": [ + "Date,Tag,Account,Description,Credit,Debit,BTC/USD,Credit (USD),Debit(USD)" + ], + "format": "{localtime},{tag},{account},{description?Invoice description {description}:{outpoint?Onchain output {outpoint}:{txid?Onchain transaction {txid}}}},{credit?+{credit}:0},{debit?-{debit}:0},{currencyrate},${currencycredit},${currencydebit}", + "escape": "csv" + } + }, + "response": { + "report": [ + "Date,Tag,Account,Description,Credit,Debit,BTC/USD,Credit (USD),Debit(USD)", + "2026-04-02 12:42:03,deposit,wallet,Onchain output 37562cf0b3a5d6783ae02925bcc486bb89afe156abf15e692736e5da9a060018:1,+0.02000000000,0,67140.89,$1342.81,$0.00", + "2026-04-02 12:42:04,invoice,9b3114c8003dc8dedbcf3b7bc9fb04e1575a1112aea33be8369ae9670aff64f4,Invoice description test_bkpr_report_invoice,0,-0.00000123456,67140.89,$0.00,$0.08", + "2026-04-02 12:42:04,onchain_fee,9b3114c8003dc8dedbcf3b7bc9fb04e1575a1112aea33be8369ae9670aff64f4,Onchain transaction f464ff0a67e99a36e83ba3ae12115a57e104fbc97b3bcfdbdec83d00c814319b,0,-0.00004927000,67140.89,$0.00,$3.30" + ], + "format-hint": "simple" + } + } + ], "author": [ "Rusty Russell [rusty@rustcorp.com.au](mailto:rusty@rustcorp.com.au) is mainly responsible." ], diff --git a/plugins/bkpr/report.c b/plugins/bkpr/report.c index 754f29a81e34..17822ca4ff42 100644 --- a/plugins/bkpr/report.c +++ b/plugins/bkpr/report.c @@ -15,6 +15,9 @@ #include #include +/* This is a zero, but treated specially if tested with ? */ +static const char ZERO_AMOUNT[] = "0"; + static const char *report_fmt_acct_name(const tal_t *ctx UNNEEDED, const struct bkpr *bkpr UNNEEDED, const struct income_event *e) @@ -40,6 +43,8 @@ static const char *report_fmt_credit(const tal_t *ctx, const struct bkpr *bkpr UNNEEDED, const struct income_event *e) { + if (amount_msat_is_zero(e->credit)) + return ZERO_AMOUNT; return fmt_amount_msat_btc(ctx, e->credit, false); } @@ -47,6 +52,8 @@ static const char *report_fmt_debit(const tal_t *ctx, const struct bkpr *bkpr UNNEEDED, const struct income_event *e) { + if (amount_msat_is_zero(e->debit)) + return ZERO_AMOUNT; return fmt_amount_msat_btc(ctx, e->debit, false); } @@ -54,6 +61,8 @@ static const char *report_fmt_fees(const tal_t *ctx, const struct bkpr *bkpr UNNEEDED, const struct income_event *e) { + if (amount_msat_is_zero(e->fees)) + return ZERO_AMOUNT; return fmt_amount_msat_btc(ctx, e->fees, false); } @@ -134,7 +143,7 @@ static const char *report_fmt_credit_debit(const tal_t *ctx, if (!amount_msat_is_zero(e->debit)) return tal_fmt(ctx, "-%s", fmt_amount_msat_btc(tmpctx, e->debit, false)); - return "0"; + return ZERO_AMOUNT; } static const char *report_fmt_currency_credit(const tal_t *ctx, @@ -211,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, @@ -222,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; @@ -239,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') { @@ -264,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; @@ -273,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); @@ -287,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; + ifset = parse_report_format(f, start, '}', ':', err); + if (!ifset) { + tal_steal(ctx, *err); + return tal_free(f); + } + 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; - alt = parse_report_format(f, start, '}', err); - if (!alt) { - /* Steal error upwards! */ + ifnotset = parse_report_format(f, start, '}', '\0', err); + if (!ifnotset) { tal_steal(ctx, *err); return tal_free(f); } - /* Consume final } */ + /* 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; } @@ -327,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); @@ -411,15 +447,24 @@ static char *format_event(const tal_t *ctx, } v = fmt->fmt[i](tmpctx, bkpr, e); + /* 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 362566592d70..2654dae7dbb8 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 "+" */ + 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 eb5e91cd0686..69da2676f0a5 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 c62210e6b780..1883c1251a1a 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,22 +1275,24 @@ 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}") + 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: - assert len(r) == 8 + assert len(r) == 9 # Credit or debit? if float(r[2]) > 0: assert r[4] == '+' + r[2] assert float(r[5]) > 0 assert r[6] == '0.00' assert r[7] == '+' + r[5] + assert r[8] == r[2] else: assert r[4] == '-' + r[3] assert float(r[6]) > 0 assert r[5] == '0.00' assert r[7] == '-' + r[6] + assert r[8] == 'NONE' def test_bkpr_report_invoice(node_factory): @@ -1313,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] != '':