Skip to content

Instantly share code, notes, and snippets.

@mjdominus
Created May 17, 2010 17:42
Show Gist options
  • Save mjdominus/404002 to your computer and use it in GitHub Desktop.
Save mjdominus/404002 to your computer and use it in GitHub Desktop.
  • Overview

I have a bunch of classes that represent email filters, and are backed by a legacy database. The interface to the legacy database is through DBIx::Class. A filter is essentially two things: a condition and an action. An email message is passed to the condition object, which reports whether the condition is satisfied; if so, the message is passed to the action object, which performs some action, possibly modifying the message. The legacy database principally involves two tables: mh_conds, representing conditions, and mh_actions, representing actions.

I have code that examines the tables using DBIx::Class and constructs Filter objects, and also code for the reverse direction: you can manufacture a Filter object, including its subsidiary Condition and Action objects, and then store it into the database.

The storage code is peculiar. The way it works is that Condition and Action objects each have an as_rows method, which returns a list of DBIx::Class::Row objects. To install a filter into the database, the filter manager first calls as_rows to get the rows, and then calls insert_or_update on each row.

I know this is weird, and not the expected way to do it. What's the expected way to do it?

Some code samples are attached below. I tried to include only the relevant portions of this medium-sized code base; if something crucial seems missing, please don't hesitate to ask to see it.

  • Technical summary

Relevant portions of all the classes discussed in this section are attached here.

Filters are in a Pobox::Filter hierarchy. A Pobox::Filter::MHLegacy object conforms to the Filter interface but represents a filter object that was fetched from the legacy database or that will be stored into the legacy database. An MHLegacy filter contains a condition, of type Pobox::Filter::Condition::MHCondLegacy, and an action, of type Pobox::Filter::Condition::MHActionLegacy.

The action is much simpler because actions are represented in the database by single records. The MHActionLegacy object is essentially just a wrapper around a single database row, represented by a Pobox::Schema::Result::Action object. This object might previously have been fetched from the database, and possibly modified, or it might have been constructed via Pobox::Schema::Result::Action→new.

Conditions are more complex, and may contain subconditions, joined by boolean operators, so it is best to think as a condition structure as a tree whose leaves are simple conditions such as "Subject line contains 'Make Money Fast!'". The MHCondLegacy object represents a complete condition from the legacy database. It contains one or more Pobox::Filter::Condition::MHBlockLegacy objects, each of which contains one or more Pobox::Filter::Condition::MHRowLegacy object. Each MHRowLegacy object is a wrapper around a single database row, represented by a Pobox::Schema::Result::Condition object. Again, these single-row objects might have been fetched from the database, fetched and then modified, or constructed de novo.

Filters are installed, deleted, and modified by icg2::Account::FilterManager. When asked to install a filter into the database (say in →save_filter), the FilterManager asks the filter for its condition rows and its action rows and then inserts them into the database (in →store_rows). It does this by looping over the rows and calling →insert_or_update on each.

When a filter is asked for its action rows (→action_rows) it delegates the request to its action component via →action→get_rows. MHActionLegacy::get_rows simply returns the single Pobox::Schema::Result::Action object that it encapsulates. Similarly, when a filter is asked for its condition rows it delegates the request to its condition component via →condition→get_rows. For an MHLegacy filter, this calls MHCondLegacy::get_rows, which passes the request to the MHBlockLegacy subobjects and thence to the MHRowLegacy leaf objects. The MHRowLegacy::get_rows method just returns the single Pobox::Schema::Result::Condition objects that its target encapsulates. The MHCondLegacy::get_rows method assembles several rows into a block, adjusting some of the fields along the way, such as the ends_block field, and the top-level MHCondLegacy::get_rows method assembles the groups of block rows into a complete set of database rows representing an entire condition.

package icg2::Account::FilterManager;
sub new {
my ($class, $schema) = @_;
$schema ||= Pobox::Schema->schema;
bless { DB => $schema } => $class;
}
sub schema {
return $_[0]{DB};
}
sub txn_do {
my $self = shift;
$self->schema->txn_do(@_);
}
sub save_filter {
my ($self, $filter) = @_;
return $self->txn_do(
sub {
$self->delete_filter($filter);
$self->store_rows([$filter->action_rows()], "Action");
$self->store_rows([$filter->condition_rows()], "Condition");
return 1;
});
}
sub store_rows {
my ($self, $rows, $table) = @_;
return 0 unless @$rows;
my $result_source = $self->schema->source($table)
or croak "Unknown table '$table'";
for my $row (@$rows) {
$row->result_source($result_source);
}
$self->txn_do(
sub {
for my $row (@$rows) {
$row->update_or_insert;
}
return scalar(@$rows);
});
}
package Pobox::Filter::Action::MHActionLegacy;
use base 'Pobox::Filter::Action';
sub new {
my ($class, $row) = @_;
$row ||= $class->rowFactory->new();
bless { ROW => $row } => $class;
}
sub get_rows {
my $self = shift;
return $self->row();
}
sub row {
$_[0]{ROW};
}
package Pobox::Filter::Condition::MHBlockLegacy;
use base 'Pobox::Filter::Condition::Compound';
sub get_rows {
my $self = shift;
my $op = $self->op;
my @rows = map $_->get_rows, $self->branches;
for my $row (@rows) {
$row->bool_join($op);
$row->ends_block(0);
}
$rows[-1]->ends_block(1);
return @rows;
}
package Pobox::Filter::Condition::MHCondLegacy;
use base 'Pobox::Filter::Condition::Compound';
sub get_rows {
my $self = shift;
my @rows = map $_->get_rows(), $self->branches();
# generate the condid values
for my $i (0 .. $#rows) { $rows[$i]->condid($i) }
return @rows;
}
package Pobox::Filter::Condition::MHRowLegacy;
use base 'Pobox::Filter::Condition';
sub get_rows {
my $self = shift;
return $self->row();
}
sub row { return $_[0]{R} }
package Pobox::Filter::MHLegacy;
sub condition_rows {
my $self = shift;
my %additional_setting = @_;
my @rows = $self->condition->get_rows;
if (! exists $additional_setting{actionid}) {
my $actionid = eval { $self->action->row->actionid() };
$additional_setting{actionid} = $actionid
if defined $actionid;
}
$self->_fixup_rows(\@rows, { actid => $self->actid,
provisionid => $self->provid,
accountid => $self->accountid,
%additional_setting,
});
return wantarray ? @rows : \@rows;
}
sub action_rows {
my $self = shift;
my %additional_setting = @_;
my @rows = $self->action->get_rows;
$self->_fixup_rows(\@rows, { actid => $self->actid,
provisionid => $self->provid,
accountid => $self->accountid,
switch => "on",
basic => "no",
%additional_setting,
});
return wantarray ? @rows : \@rows;
}
# set defaults in an array of rows.
# $rows is a reference to the array of rows
# $vals is a hash mapping column names to desired values
# the $vals are NOT defaults; they replace existing values
sub _fixup_rows {
my ($self, $rows, $val) = @_;
return unless %$val;
for my $row (@$rows) {
for my $k (keys %$val) {
$row->set_column($k, $val->{$k});
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment