NAME
Punk::Model::JSONFile - a Punk::Model backend over a JSON file
SYNOPSIS
# the app
database data => { backend => 'Punk::Model::JSONFile', file => 'share/data.json' };
model;
# share/data.json
{ "modules": [ { "slug": "punk", "name": "Punk", "position": 1 }, ... ] }
# the model
package MyApp::Model::Module;
use Punk::Model;
database 'data';
table 'modules'; # the "modules" array in the file
field slug => { type => 'string', primary => 1 };
field name => { type => 'string' };
field position => { type => 'integer' };
# anywhere
my $page = $c->model('Module')->search({ name => { starts_with => 'P' } },
{ order_by => 'position' });
$c->model('Module')->create({ slug => 'hamt', name => 'HAMT', position => 2 });
DESCRIPTION
The whole Punk::Model contract over one JSON file: the file is an object, and each model's table names an array of rows in it, so one file can hold several tables, and values beside them that "document" hands back. It is for data that is small, edited by hand or written rarely, and wants to live in the repository rather than a database: a list of featured items, a site's settings, a snapshot of an upstream API.
Reading
The file is read through File::Raw::JSON and kept, per process, until it changes. "Changes" means its inode, its size or its mtime: a write here, or any tool that writes atomically, renames a new file into place, which can land within the second of the last read, so the mtime alone would miss it. A missing file has no rows. A file that is not a JSON object has no rows and warns once for each version of it, rather than on every read.
Filters, order_by, limit and the after token are those of Punk::Model, checked by the same code as Punk::Model::DBI, with the same croaks and the same token, evaluated with SQL's three-valued logic: undef (JSON null) never equals, compares or is in anything; != leaves nulls out; not_in over a list holding a null matches nothing. Values compare as numbers when both look like numbers, otherwise as strings; null sorts first. like takes % and _ and is case-sensitive.
Every row handed out is a deep copy.
Writing
create, update and delete each rewrite the whole file. They hold an exclusive flock on <file>.lock while they do (where the platform has flock), so two workers cannot lose each other's change; they read the file afresh rather than from the cache; and they write a temporary file and rename it over the old one, so a reader never sees half of it. The file is written pretty-printed with sorted keys, so it diffs well under version control.
create keeps the declared columns with defined values (validation, when the model has any, has already run in Punk::Model). Without a primary key value it takes the next integer after the largest one, and croaks when the existing keys are not all integers; an existing key croaks. update needs the primary key, sets the declared columns given, and returns the row, or undef when there is none. delete(%key) returns how many rows it removed.
A broken file is never written over: a write croaks instead.
THE CONTRACT
get(%key)
The first row whose key columns equal the values, copied, or undef.
search(\%filter, \%opts)
A page, { rows, has_more_data, next }, as on Punk::Model::DBI; see "Reading".
count(\%filter)
How many rows match.
all()
search({}, {}).
create(\%data)
Appends the row and returns it; see "Writing".
update(\%key_and_changes)
Changes the row the primary key names and returns it, or undef.
delete(%key)
Removes the matching rows and returns how many.
METHODS
file
The path, as the database gave it.
document
The whole file, decoded and copied, or undef: for values kept beside the tables, such as a count or a timestamp.
new
Called by Punk::Model with the database options (which must include a file), the table, the primary key and the columns.