NAME

Sub::Spec::Clause::features - Specify subroutine features

VERSION

version 0.12

SYNOPSIS

In your spec:

features => {
    FEATURE => VALUE,
    ...
}

DESCRIPTION

The 'features' clause allows subroutines to express their features. It is extensible so you can specify more features.

Below is the list of defined features. New feature clause may be defined by providing Sub::Spec::Clause::features::check_<CLAUSE>().

reverse => BOOL|HASHREF

Default is false. If set to true, specifies that subroutine supports reverse operation. To reverse, caller can add special argument '-reverse'. For example:

$SPEC{triple} = {args=>{num=>'num*'}, features=>{reverse=>1}};
sub triple {
    my %args = @_;
    my $num  = $args{num};
    [200, "OK", $args{-reverse} ? $num/3 : $num*3];
}

triple(num=>12);              # => 36
triple(num=>12, -reverse=>1); # =>  4

DRAFT: Conditional reversibility: if set to hashref, sub can declare partial reversibility. Caller can call with special argument '-reverse_action' set to 'test' and sub must return a bool result indicating if the particular action is reversible. Example:

$SPEC{multiply_by} = {args     => {num=>'num*', multiplier=>'num*'},
                      features => {reverse=>{...}}};
sub multiply_by {
    my %args = @_;
    my $n    = $args{num};
    my $m    = $args{multiplier};
    if ($args{-reverse_action} && $args{-reverse_action} eq 'test') {
        # we can only reverse if multiplier is not zero
        return [200, "OK", $m == 0 ? 1:0];
    }
    [200, "OK", $args{-reverse} ? $n/$m : $n*$m];
}

undo => BOOL|HASHREF

Default is false. If set to true, specifies that subroutine supports undo operation. Undo is similar to 'reverse' but needs some state to be saved and restored for do/undo operation, while reverse can work solely from the arguments.

Caller must provide one or more special arguments: -undo_action, -undo_hint, -undo_data, -redo_data when dealing with do/undo stuffs.

do

To perform normal operation, caller must set -undo_action to 'do' and optionally pass -undo_hint for hints on how to save undo data (e.g. if undo_data is to be saved on a file, -undo_hint can contain filename or base directory). Sub must save undo data, perform action, and return result along with saved undo data in the response metadata (4th argument of response), example:

return [200, "OK", $result, {undo_data=>$undo_data}];

Undo data should contain information (or reference to information) to restore to previous state later. This information should be persistent (e.g. in a file/database) when necessary. For example, if undo data is saved in a file, undo_data can contain the filename. If undo data is saved in a memory structure, undo_data can refer to this memory structure, and so on. Undo data should be serializable. Caller should store this undo data in the undo stack (note: undo stack management is the caller's task).

If -undo_action is false/undef, sub must assume caller want to perform action but without saving undo data.

undo

To perform an undo, caller must set -undo_action to 'undo' and pass back the undo data in -undo_data. Sub must restore previous state using undo data (or return 412 if undo data is invalid/unusable). After a successful undo, sub must return 200. A redo data can be provided in the response metadata by the sub if necessary:

return [200, "OK", undef, {redo_data=>...}];

Caller should then mark that the undo data has already been used (action has been undone).

redo

To redo a previously undone action, caller must set -undo_action to 'redo' and -redo_data to redo data given by the sub when doing undo previously (if any), and also optionally -undo_hint. Sub should return 412 if redo data is invalid/unusable. Sub must redo the action and can optionally return a new undo_data.

return [200, "OK", $result, {undo_data=>...}];

After a successful redo, action can be undone again using the previous (or new) undo_data.

Example (in this example, undo data is only stored in memory):

use Cwd qw(abs_path);
use File::Slurp;

$SPEC{lc_file} = {
    summary  => 'Convert the *content* of file into all-lowercase',
    args     => {path=>'str*'},
    features => {undo=>1},
};
sub lc_file {
    my %args        = @_;
    my $path        = $args{path};
    my $undo_action = $args{-undo_action} // '';
    my $undo_data   = $args{-undo_data};

    $path = abs_path($path)
        or return [500, "Can't get file absolute path"];

    if ($undo_action eq 'undo') {
        write_file $path, $undo_data->{content}; # restore original content
        utime undef, $undo_data->{mtime}, $path; # as well as original mtime
        return [200, "OK"];
    } else {
        my @st = stat($path)
            or return [500, "Can't stat file"];
        my $content = read_file($path);
        my $undo_data = {mtime=>$st[9], content=>$content};
        write_file $path, lc($content);
        return [200, "OK", undef, {undo_data=>$undo_data}];
    }
}

To perform action, caller calls lc_file() and store the undo data:

my $res = lc_file(path=>"/foo/bar", -undo_action=>"do");
die "Failed: $res->[0] - $res->[1]" unless $res->[0] == 200;
my $undo_data = $res->[3]{undo_data};

To perform undo:

$res = lc_file(path=>"/foo/bar", -undo_action=>"undo", -undo_data=>$undo_data);
die "Can't undo: $res->[0] - $res->[1]" unless $res->[0] == 200;
my $redo_data = $res->[3]{redo_data};

To perform redo:

lc_file(path=>"/foo/bar", -undo_action=>"redo", -redo_data=>$redo_data);
die "Can't redo: $res->[0] - $res->[1]" unless $res->[0] == 200;

dry_run => 1

Specifies that subroutine supports dry-run (simulation) mode. Example:

use Log::Any '$log';

$SPEC{rmre} = {
    summary  => 'Delete files in curdir matching a regex',
    args     => {re=>'str*'},
    features => {dry_run=>1}
};
sub rmre {
    my %args    = @_;
    my $re      = qr/$args{re}/;
    my $dry_run = $args{-dry_run};

    opendir my($dir), ".";
    while (my $f = readdir($dir)) {
        next unless $f =~ $re;
        $log->info("Deleting $f ...");
        next if $dry_run;
        unlink $f;
    }
    [200, "OK"];
}

pure => 1

Specifies that subroutine is "pure" and has no "side effects" (these are terms from functional programming / computer science). Having a side effect means changing something, somewhere (e.g. setting the value of a global variable, modifies its arguments, writing some data to disk, changing system date/time, etc.) Specifying a function as pure means, among others:

  • the function needs not be involved in undo operation;

  • you can safely include it during dry run;

TODO

transaction/atomicity

i18n (is translatable, supported languages, locales)

progress bar/estimated duration (we can query each sub how long would an action takes beforehand, or caller pas a callback that gets called multiple times to update progress bar)

SEE ALSO

Sub::Spec

AUTHOR

Steven Haryanto <stevenharyanto@gmail.com>

COPYRIGHT AND LICENSE

This software is copyright (c) 2011 by Steven Haryanto.

This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself.