Saturday, September 12, 2009

Improving usability for people and code reuse

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.

No comments:

Post a Comment