summaryrefslogtreecommitdiff
path: root/src/textual.cc
diff options
context:
space:
mode:
authorJohn Wiegley <johnw@newartisans.com>2010-06-12 21:43:03 -0400
committerJohn Wiegley <johnw@newartisans.com>2010-06-13 01:03:48 -0400
commit6ef755c1330c6e11476ea2e63b8388ad29efb4ef (patch)
treead006748fff38a2748087514908ef370495ec791 /src/textual.cc
parent6f56a7443fea165c73d00029437530d567556994 (diff)
downloadfork-ledger-6ef755c1330c6e11476ea2e63b8388ad29efb4ef.tar.gz
fork-ledger-6ef755c1330c6e11476ea2e63b8388ad29efb4ef.tar.bz2
fork-ledger-6ef755c1330c6e11476ea2e63b8388ad29efb4ef.zip
Added support for assert, check and expr directives
These can occur in many places: ; Within an automated transaction, the assert is evaluated every time ; a posting is matched, with the expression context set to the ; matching posting. = /Food/ assert account("Expenses:Food").total >= $100 2010-06-12 Sample Expenses:Food $100 Assets:Checking ; At file scope, the expression is evaluated with "global" scope. assert account("Expenses:Food").total == $100 ; At the top of a transction, the assertion's scope is the ; transaction. After a posting, the scope is that posting. Note ; however that account totals are only adjusted after successful ; parsing of a transaction, which means that all the assertions below ; are true, even though it appears as though the middle posting should ; affect the total immediately (which is not the case). 2010-06-12 Sample 2 assert account("Expenses:Food").total == $100 Expenses:Food $50 assert account("Expenses:Food").total == $100 Assets:Checking assert account("Expenses:Food").total == $100
Diffstat (limited to 'src/textual.cc')
-rw-r--r--src/textual.cc152
1 files changed, 134 insertions, 18 deletions
diff --git a/src/textual.cc b/src/textual.cc
index 4d133302..113bafe8 100644
--- a/src/textual.cc
+++ b/src/textual.cc
@@ -146,6 +146,9 @@ namespace {
void account_mapping_directive(char * line);
void tag_directive(char * line);
void define_directive(char * line);
+ void assert_directive(char * line);
+ void check_directive(char * line);
+ void expr_directive(char * line);
bool general_directive(char * line);
post_t * parse_post(char * line,
@@ -523,20 +526,74 @@ void instance_t::automated_xact_directive(char * line)
bool reveal_context = true;
try {
+ std::auto_ptr<auto_xact_t> ae
+ (new auto_xact_t(query_t(string(skip_ws(line + 1)),
+ keep_details_t(true, true, true), false)));
+ ae->pos = position_t();
+ ae->pos->pathname = pathname;
+ ae->pos->beg_pos = line_beg_pos;
+ ae->pos->beg_line = linenum;
+ ae->pos->sequence = context.sequence++;
+
+ post_t * last_post = NULL;
+
+ while (peek_whitespace_line()) {
+ std::streamsize len = read_line(line);
+
+ char * p = skip_ws(line);
+ if (! *p)
+ break;
- std::auto_ptr<auto_xact_t> ae
- (new auto_xact_t(query_t(string(skip_ws(line + 1)),
- keep_details_t(true, true, true), false)));
- ae->pos = position_t();
- ae->pos->pathname = pathname;
- ae->pos->beg_pos = line_beg_pos;
- ae->pos->beg_line = linenum;
- ae->pos->sequence = context.sequence++;
+ const std::size_t remlen = std::strlen(p);
- reveal_context = false;
+ if (*p == ';') {
+ item_t * item;
+ if (last_post)
+ item = last_post;
+ else
+ item = ae.get();
- if (parse_posts(context.top_account(), *ae.get(), true)) {
- reveal_context = true;
+ // This is a trailing note, and possibly a metadata info tag
+ item->append_note(p + 1, context.scope, true, current_year);
+ item->pos->end_pos = curr_pos;
+ item->pos->end_line++;
+
+ // If there was no last_post yet, then deferred notes get applied to
+ // the matched posting. Other notes get applied to the auto-generated
+ // posting.
+ ae->deferred_notes->back().apply_to_post = last_post;
+ }
+ else if ((remlen > 7 && *p == 'a' &&
+ std::strncmp(p, "assert", 6) == 0 && std::isspace(p[6])) ||
+ (remlen > 6 && *p == 'c' &&
+ std::strncmp(p, "check", 5) == 0 && std::isspace(p[5])) ||
+ (remlen > 5 && *p == 'e' &&
+ std::strncmp(p, "expr", 4) == 0 && std::isspace(p[4]))) {
+ const char c = *p;
+ p = skip_ws(&p[*p == 'a' ? 6 : (*p == 'c' ? 5 : 4)]);
+ if (! ae->check_exprs)
+ ae->check_exprs = auto_xact_t::check_expr_list();
+ ae->check_exprs->push_back
+ (auto_xact_t::check_expr_pair(expr_t(p),
+ c == 'a' ?
+ auto_xact_t::EXPR_ASSERTION :
+ (c == 'c' ?
+ auto_xact_t::EXPR_CHECK :
+ auto_xact_t::EXPR_GENERAL)));
+ }
+ else {
+ reveal_context = false;
+
+ if (post_t * post =
+ parse_post(p, len - (p - line), context.top_account(),
+ NULL, true)) {
+ reveal_context = true;
+ ae->add_post(post);
+ last_post = post;
+ }
+ reveal_context = true;
+ }
+ }
context.journal.auto_xacts.push_back(ae.get());
@@ -585,6 +642,7 @@ void instance_t::period_xact_directive(char * line)
pe.release();
} else {
+ reveal_context = true;
pe->journal = NULL;
throw parse_error(_("Period transaction failed to balance"));
}
@@ -823,6 +881,26 @@ void instance_t::define_directive(char * line)
def.compile(context.scope); // causes definitions to be established
}
+void instance_t::assert_directive(char * line)
+{
+ expr_t expr(line);
+ if (! expr.calc(context.scope).to_boolean())
+ throw_(parse_error, _("Assertion failed: %1" << line));
+}
+
+void instance_t::check_directive(char * line)
+{
+ expr_t expr(line);
+ if (! expr.calc(context.scope).to_boolean())
+ warning_(_("Check failed: %1" << line));
+}
+
+void instance_t::expr_directive(char * line)
+{
+ expr_t expr(line);
+ expr.calc(context.scope);
+}
+
bool instance_t::general_directive(char * line)
{
char buf[8192];
@@ -845,6 +923,10 @@ bool instance_t::general_directive(char * line)
alias_directive(arg);
return true;
}
+ else if (std::strcmp(p, "assert") == 0) {
+ assert_directive(arg);
+ return true;
+ }
break;
case 'b':
@@ -859,6 +941,10 @@ bool instance_t::general_directive(char * line)
account_mapping_directive(arg);
return true;
}
+ else if (std::strcmp(p, "check") == 0) {
+ check_directive(arg);
+ return true;
+ }
break;
case 'd':
@@ -873,6 +959,10 @@ bool instance_t::general_directive(char * line)
end_directive(arg);
return true;
}
+ else if (std::strcmp(p, "expr") == 0) {
+ expr_directive(arg);
+ return true;
+ }
break;
case 'f':
@@ -1383,25 +1473,51 @@ xact_t * instance_t::parse_xact(char * line,
if (! *p)
break;
- if (*p == ';') {
- item_t * item;
- if (last_post)
- item = last_post;
- else
- item = xact.get();
+ const std::size_t remlen = std::strlen(p);
+
+ item_t * item;
+ if (last_post)
+ item = last_post;
+ else
+ item = xact.get();
+ if (*p == ';') {
// This is a trailing note, and possibly a metadata info tag
item->append_note(p + 1, context.scope, true, current_year);
item->pos->end_pos = curr_pos;
item->pos->end_line++;
- } else {
+ }
+ else if ((remlen > 7 && *p == 'a' &&
+ std::strncmp(p, "assert", 6) == 0 && std::isspace(p[6])) ||
+ (remlen > 6 && *p == 'c' &&
+ std::strncmp(p, "check", 5) == 0 && std::isspace(p[5])) ||
+ (remlen > 5 && *p == 'e' &&
+ std::strncmp(p, "expr", 4) == 0 && std::isspace(p[4]))) {
+ const char c = *p;
+ p = skip_ws(&p[*p == 'a' ? 6 : (*p == 'c' ? 5 : 4)]);
+ expr_t expr(p);
+ bind_scope_t bound_scope(context.scope, *item);
+ if (c == 'e') {
+ expr.calc(bound_scope);
+ }
+ else if (! expr.calc(bound_scope).to_boolean()) {
+ if (c == 'a') {
+ throw_(parse_error, _("Transaction assertion failed: %1" << p));
+ } else {
+ warning_(_("Transaction check failed: %1" << p));
+ }
+ }
+ }
+ else {
reveal_context = false;
if (post_t * post =
parse_post(p, len - (p - line), account, xact.get())) {
+ reveal_context = true;
xact->add_post(post);
last_post = post;
}
+ reveal_context = true;
}
}