NAME

Punk::Model::Computed - a base for read-only model backends whose rows are worked out, not stored

SYNOPSIS

# the backend
package MyApp::Backend::Clock;
use parent 'Punk::Model::Computed';

__PACKAGE__->answers(
    zones => { ask => [qw(zone at)], ops => { zone => ['in'] } },
    runs  => { ask => [qw(rule at)], ops => { at => [qw(> >= < <=)] } },
);

sub _get_zones {
    my ($self, $f) = @_;
    my $zone = $f->{zone}{'='} // $self->error(zone => 'zone is required');
    return $self->wrap(zone => sub { My::Zones->row($zone) });
}

sub _search_runs {
    my ($self, $f, $o) = @_;
    my $limit = $self->limit($o, 10, 50, 'runs');
    my $key   = $self->cache_key(search => $f, $o);
    return $self->cached($key, sub { ... { rows => \@rows, has_more_data => 0, next => undef } });
}

# the application
database clock => { backend => 'MyApp::Backend::Clock', cache => 512 };

# a model
package MyApp::Model::Zone;
use Punk::Model;
database 'clock';
table    'zones';
field zone   => { type => 'string', primary => 1 };
field at     => { type => 'integer' };
field offset => { type => 'integer' };

# a controller
my $row = eval { $c->model('Zone')->get(zone => $c->param('zone')) };
return $c->json({ error => $@ }, 400)
    if ref $@ && $@->isa('Punk::Model::Computed::Error');

DESCRIPTION

A backend for Punk::Model whose rows are not kept anywhere but worked out from the filter: a calculator, a parser, a call into a module. The filter is the question, and the rows are its answers. A subclass says which tables it answers and how each may be asked, and writes one method a verb. The base does the rest of the contract: it checks every filter and option the way Punk's own backends check theirs, hands the subclass a question in one shape, and croaks on create, update and delete.

Two kinds of mistake are told apart. A program's (a field the model does not declare, a field the table is not asked by, an operator or option it does not take, an order_by) croaks, as on Punk::Model::DBI. A visitor's (a zone that does not exist, input over a cap) dies with a Punk::Model::Computed::Error naming the field, which a controller can catch and show.

DECLARING THE TABLES

answers

__PACKAGE__->answers($table => \%spec, ...);
my @tables = __PACKAGE__->answers;

Declares the tables the class answers, each with:

ask

The fields the table may be asked by. A filter on any other declared field of the model croaks, as an answer and not a question.

ops

{ field => [operators] }: the operators each field takes beyond =, from Punk's set (!=, <, <=, >, >=, in, not_in, like, starts_with). A field not named here takes = only.

as_is

Fields whose value is taken whole, a hash or a list included, rather than read as operators: an options hash given to get.

A subclass of a subclass inherits its parent's tables and may add or redeclare some. With no arguments, answers lists them all.

WRITING ONE

A model's table names the table it asks, and new croaks for a table the class does not answer. Each verb calls a method of the subclass named for it and the table:

_get_<table>($self, $filter)
_search_<table>($self, $filter, $options)
_count_<table>($self, $filter)

$filter is { field => { op => value } }, a plain value being { '=' => value }. $options holds only limit and after. _get_ returns a row or undef, _search_ the page { rows, has_more_data, next } (next a token the same method reads back from after), and _count_ a number. A verb a table has no method for croaks, naming the ones it has.

all is search({}, {}).

METHODS

new

Built by Punk::Model, once a worker a model, with the database options, the model's table, primary and columns, and the application. The database may give cache, how many answers "cached" keeps (256 when not given, 0 for none).

table

The table this backend answers for.

app

The application, or undef when built without one. It is held weakly.

get

count

all

The contract, as "WRITING ONE" describes.

create

update

delete

Croak: nothing is stored. A subclass may define its own.

filter

my $f = $self->filter(\%filter);

A filter checked and normalised as the verbs do it, for a method of the subclass's own that takes the same fields.

options

my $o = $self->options(\%options);

The search options checked: limit and after.

limit

my $n = $self->limit($options, $default, $max, $what);

The page size: limit from the options, else $default. Not a whole number from 1 is an error on limit ("limit must be a whole number from 1 to 50"), and over $max is one too ("at most 50 runs at a time"). $what names the rows, rows when not given.

error

$self->error($field, $message);

Dies with a Punk::Model::Computed::Error.

wrap

my @r = $self->wrap($field => sub { Some::Module->work($input) });

Runs the code in the caller's context and returns what it returns. A die inside becomes an error on $field in the module's own words (see "said" in Punk::Model::Computed::Error). One that is already a Punk::Model::Computed::Error passes through unchanged.

cached

my $answer = $self->cached($key, sub { ... });

What the code returns, kept under $key among the backend's last cache answers and given back from there while it stays, the least recently used going first. An undef key runs the code every time, for an answer that depends on when it is asked. The cache is the worker's own, and a kept answer is the same reference each time, so a caller must not change it.

cache_key

my $key = $self->cache_key(search => $filter, $options);

A key for "cached": the table and the parts, written so that two different values never give the same key. Hashes are read in sorted order, so two equal filters give one key. An object is its string.

cache_size

How many answers are kept.

cache_clear

Forgets them.

SEE ALSO

Punk::Model, Punk::Model::Computed::Error, Punk::Model::JSONFile, Punk::Model::Markdown.