For my long going tisql project I wrote Parse::Boolean
module a while ago. Recently found a new application
for it and it worked pretty well.
In RT we have scrips - condition, action and a template. When something happens
with a ticket, a change checked against conditions of scrips. Only those actions
are applied for which conditions returned a true value. Pretty simple, but
condition is just a a code and we want code to be re-usable.
Conditions are implemented as a modules and controlled by an argument, usually
a parsable string. Strings work good for people. Lots of RT admins can not
write a code for conditions and it's stupid to ask them to wrap a code they
can not write into a module.
We have 'User Defined' condition module for such cases, where you can write
code right in the UI. It helps, but anyway. Here goes another problem. If you
have code in a module that nobody except you can write then this module
should be help for everybody, but it's not. Often people want to mix complex
things with simple conditions and either you have to extend format of
argument in the condition or invent a new thing.
I decided to "invent".
There was nothing to invent actually, but connect technologies together
and that's what I did. Sometimes I think our work is to connect things
together. Recall I said that User Defined allow you to type a condition
using Perl right in the UI. Ok. What if we replace perl with custom
syntax. Parse::BooleanLogic provides a good parser for pretty random
things inside nested parentheses and joined with boolean operators
'AND' and 'OR'. Almost any condition in RT falls into this category
and looks like the following even in perl:
return 1 if ( this OR that OR (that AND that) ) AND else...
return 0;
In RT we have TicketSQL using which you can search tickets and
use the following simple SQL like conditions:
x = 10 OR y LIKE 'string' OR z IS NULL
I decided that in condition it will work pretty well too. In condition
we have the current ticket we check and the change (transaction).
( Type = 'Create' AND Ticket.Status = 'resolved' )
OR ( Type = 'Set' AND Field = 'Status' AND NewValue = 'resolved' )
Looks good and every user can get the syntax, but it's not there yet.
We have modules already that implement conditions and want to reuse them.
Pretty easy to solve:
ModuleName{'argument'} OR !AnotherModule{'argument'}
I'm really proud that my parser allows me to parse syntax like this
without much work:
sub ParseCode {
my $self = shift;
my $code = $self->ScripObj->CustomIsApplicableCode;
my @errors = ();
my $res = $parser->as_array(
$code,
error_cb => sub { push @errors, $_[0]; },
operand_cb => sub {
my $op = shift;
if ( $op =~ /^(!?)($re_exec_module)(?:{$re_module_argument})?$/o ) {
return {
module => $2,
negative => $1,
argument => $parser->dq($3),
};
}
elsif ( $op =~ /^($re_field)\s+($re_bin_op)\s+($re_value)$/o ) {
return { op => $2, lhs => $1, rhs => $3 };
}
elsif ( $op =~ /^($re_field)\s+($re_un_op)$/o ) {
return { op => $2, lhs => $1 };
}
else {
push @errors, "'$op' is not a check 'Complex' condition knows about";
return undef;
}
},
);
return @errors? (undef, @errors) : ($res);
}
It's not only parser, but solver as well:
my $solver = sub {
my $cond = shift;
my $self = $_[0];
if ( $cond->{'op'} ) {
return $self->OpHandler($cond->{'op'})->(
$self->GetField( $cond->{'lhs'}, @_ ),
$self->GetValue( $cond->{'rhs'}, @_ )
);
}
elsif ( $cond->{'module'} ) {
my $module = 'RT::Condition::'. $cond->{'module'};
eval "require $module;1" || die "Require of $module failed.\n$@\n";
my $obj = $module->new (
TransactionObj => $_[1],
TicketObj => $_[2],
Argument => $cond->{'argument'},
CurrentUser => $RT::SystemUser,
);
return $obj->IsApplicable;
} else {
die "Boo";
}
};
sub Solve {
my $self = shift;
my $tree = shift;
my $txn = $self->TransactionObj;
my $ticket = $self->TicketObj;
return $parser->solve( $tree, $solver, $self, $txn, $ticket );
}
That's it. Eveything else is grammar regexpes, column handlers and
and documentation. Available on the CPAN and on the github.