Code Monkey home page Code Monkey logo

raisable's Introduction

NAME

Raisable - Turn classes into resolvable conditions

VERSION

version 0.001000

SYNOPSIS

# first we declare a condition
package MyCondition::Requirement::Description;
use Moose;

has file        => (is => 'ro');
has description => (is => 'ro');

with 'Raisable';

1;

...

# later you can resolve the condition to try and find a value
my $descr_foo = MyCondition::Requirement::Description
    ->optional(file => 'foo.html');

# you can also require that a value be returned
my $descr_bar = MyCondition::Requirement::Description
    ->required(file => 'bar.html');

# there's also options for giving a default resolution function
my $descr_baz = MyCondition::Requirement::Description
    ->optional_from(sub {
        my $description = $_{description}
            or return undef;
        render_content($description);
    }, file => 'baz.html', description => $user_supplied_value);

...

# now you can handle conditions on an outer scope
MyCondition::Requirement::Description->handle(
    sub { $_->file eq 'foo.html' },
    sub { $_->retry(content => 'The foo HTML file') \},
)->in(sub {
    # ... code that might need the condition resolved
});

DESCRIPTION

This role is an extension of Throwable, implementing conditions that might be resolved by handlers set up in an outer dynamical scope.

Since every Raisable is also a Throwable and can turn into an exception, you can use every extension you could use for Throwable exceptions like description message generation or payload collection.

Declaring condition classes

The only requirement for a class to be turned into a condition is that it's built on Moose. Since this is a normal Moose::Role, you can apply it to anything you normally can. There's no real black magic here.

The simplest form of a condition would look like this:

package Condition;
use Moose;
with 'Raisable';
1;

Note that every condition is also always a Throwable.

You can consume other roles and declare attributes like with any other Moose class. In fact, you will often want to declare attributes that give a closer specification of the condition:

package Condition::ConfigFile;
use Moose;
has filename => (is => 'ro');
with 'Raisable';
1;

The condition object will be available to all resolution handlers. As such, the attributes and methods of your condition class should encapsulate everything that's required to possibly resolve the condition.

Raising conditions explicitly

The simplest case of raising a condition is to just expect a value. There is no other logic attached to the condition besides the (optional or required) request of a value.

This can be done by calling "optional" or "required" on the condition class:

my $file = Condition::ConfigFile
    ->optional(filename => 'app.conf')
    // 'etc/app.conf';

As you can see, the method is called with the constructor arguments for the condition object.

In the above you can also see the difference between "optional" and "required" conditions. An optional condition will return an undefined value if nothing could be resolved, while a required one will turn into an exception if none of the handlers could do anything about it.

Raising with a resolution function

Sometimes there is more logic involved than just fetching a value. Let's declare a condition we raise when we can't parse a line of content:

package Condition::Unparseable;
use Moose;

has line => (is => 'ro');
has type => (is => 'ro');

with 'Raisable';

1;

The condition contains the line we couldn't parse and the type we expect it to be in. The type is useful so handlers can match specific types of lines that made problems. We can incorporate the condition into a parsing routine like the following:

my @parsed;
while (defined( my $line = shift @lines )) {
    my $data = Condition::Unparseable->required_from(
        sub {
            try { decode_json($_{line}) }
            catch { undef };
        },
        line => $line,
        type => 'json',
    );
    push @parsed, @$data;
}

This will try to resolve the value with the passed subroutine, and only if that returns an unacceptable value raise the condition as an object that can be resolved by handlers. The "required_from" and "optional_from" methods in a sense are guarding the routines and intercept in case they return an undefined value.

The above already allows for many possibilities of handling the condition. A handler might return an empty array reference to skip the unparseable lines. It might parse the lines itself and return the data. Or it can retry the callback with a different line.

To extend on what we have now, you could pass a parser to the condition instead of fully encapsulating it, giving the handlers the option of using it directly when resolving the condition. You can pass along the rest of the @lines, so a handler can resolve by combining multiple lines if the outer scope wants to allow some form of multi-line statements.

The passed routine will receive the arguments passed to required_from inside %_ and as a hash passed directly to the subroutine. You should try to keep these as side-effect free as possible, since the handlers can rerun them and modify their arguments.

Just as with "required" and "optional", "required_from" will turn the condition into an exception if it isn't resolved, while calling "optional_from" will give you an undefined value if nothing could be done about it.

Matching conditions

Now that we have declared some conditions and are raising them at certain points in our code, we need a way to add resolutions from the outer dynamic scope.

The handlers are built up as a chain and then walked upwards looking for possible resolutions. Handlers are added to the chain by calling "handle" on the class or role you want to handle. These calls can be chained together to provide multiple ways of handling a condition. The declared chain is active inside a code reference then passe to "in".

Condition::Unparseable
    ->handle(sub {
        warn "Unable to parse: " . $_->line;
        return [];
    })
    ->handle(sub {
        $_->line eq "FIXTURES\n"
            ? [@fixtures]
            : undef;
    })
    ->in(sub { parse_file('somefile.json') });

You'll see that the condition object is now available in $_. It is also given to the subroutine as argument.

When the parse_file invocation above hits a line that isn't valid JSON, the second handler will be asked to resolve the condition. If the line argument is a FIXTURES\n marker, we insert some predeclared records instead.

If the condition couldn't be resolved that way, the uppermost handler will resolve the condition by returning an empty array reference, which leads to it simply skipping unparseable lines. It will also warn if that is the case. If the condition were to contain the source file and line number that is parsed, you could give a more detailed warning at that point.

You can call "handle" on classes and roles. If it is invoked on a role, the declared handlers will be asked to resolve for all conditions that applied the role. This means that you can handle all conditions by calling "handle" on Raisable itself.

Restricted condition matching

In the above example, we have handlers that try to resolve all conditions of the class they were declared on. The fixture handler simply returned an undefined value if it couldn't handle the line. Instead, it could have supplied a separate match routine:

Condition::Unparseable
    ->handle(
        sub { $_->line eq "FIXTURES\n" },
        sub { [@fixtures] },
    )->in(sub { parse_file('somefile.json') });

The match routine receives the same argument as the handler. By default, only code references can be used to match condition objects. There is also a Raisable::Match::Payload role you can consume to match hash references against payload attribute values. Other matching formats can be implemented by extending L>/MATCH>.

Retrying the calculation

The C>Condition::Unparseable> handlers in the examples earlier only returned full values in case something couldn't be parsed. But handlers also have the ability to retry the routine:

Condition::Unparseable
    ->handle(
        sub { $_->line =~ m{^\s+#} },
        sub {
            (my $corrected = $_->line) = s{^\s+#+}{};
            my $data = $_->retry(line => $corrected)
                or return undef;
            $data->{disabled} = 1;
            return $data;
        },
    )->in(sub { parse_file('somefile.json') });

This example uses "retry" to try and parse the line again after it corrected it. This handler will take commented out records and, if they are parseable, add a disabled value to them.

You might ask yourself what happens when "retry" is invoked on a condition raised via "required" or "optional" that don't carry any resolution logic with them. The easy answer is that nothing happens. It is a null operation that simply returns an undefined value. The condition class can override this by extending the "RESOLVE" method.

Skipping the rest of the handlers

In some cases you might want to immediately stop resolving a condition. If you examined a problem and it is that bad that no resolution should be allowed, you have multiple options:

  • Turning the condition into an immediate exception

    If you want to make sure that the computation doesn't continue as usual, you can install a handler that raises the condition to exception level:

    Condition::Unparseable
        ->handle(
            sub { is_a_really_really_bad_condition($_) },
            sub { $_->throw },
        )->in(sub { parse_file('somefile.json') });

    This will throw the condition as an exception if the object was determined to describe a condition we shouldn't try to handle. This is an extreme measure that locks out any higher resolutions, so be careful when thinking about using it.

  • Returning a finalized value

    Instead of turning the condition into an exception, you can also break out of the chain at any point by returning a finalized value:

    Condition::Unparseable
        ->handle(
            sub { not_that_bad_but_still_not_good($_) },
            sub { return $_->final },
        )->in(sub { parse_file('somefile.json') });

    Here we return the result of calling the "final" method on the condition. This will return a special marker telling the condition system to stop trying to resolve the issue and return an undefined value. If the condition was raised as "required", it will be turned into an exception before returning to the code that originally raised it. But if it was declared optional, the calling code will receive an undefined value at this point and can choose to handle the unresolved condition.

    You can also pass a value to "final". This is not useful in this simple case, since you can always just return a value. The feature only really becomes meaningful in more complicated settings.

Note that both of these options stop condition resolution at the point of use and you're basically isolating the matched conditions from the outside.

Matching against custom values

To determine if a handler matches a condition object, the system will call "MATCH" as an object method on the condition. You can override or extend this method to customize how the condition supports to be matched.

The role Raisable::Match::Payload already exists and implements simple matching against attributes declared to be payload.

Changing the values a condition might accept

By default, any defined value is acceptable for the condition to be resolved. An undefined value indicates the inability to resolve the condition. This functionality can be customized by extending the "ACCEPT" method.

The role Raisable::Accept::Typed already exists and implements value acceptance via Moose type constraints.

Default condition resolution method

While the ability to provide a resolution logic routine when raising a condition is nice, some conditions occur often and are usually resolved in the same way. To make this easier, a "RESOLVE" method is used as a fallback resolution. The method will be asked to resolve the condition before all dynamic handlers in the chain, and will also be called on "retry". Any custom resolution function will override this routine.

package Condition::Storage::Locate;
use Moose;

has storage => (is => 'ro');
has key     => (is => 'ro');

sub RESOLVE {
    my ($self) = @_;
    return $self->storage->get($self->key);
}

with 'Raisable';

1;

With this, a handler can return a custom item, look up a different one in the storage, or put something into the storage before retrying the resolution via "retry".

A custom resolution function passed with "required_from" and "optional_from" overrides the calling of "RESOLVE" either at the raising of the condition or on "retry".

Extensions

IMPLEMENTS

METHODS

handle

my $outer_all = ConditionClass->handle( \&handler );
my $inner_all = $outer_all->handle( \&handler );
my $value_all = $inner_all->in( \&might_raise );

my $outer_spec = ConditionClass->handle( $match, \&handler );
my $inner_spec = $outer_spec->handle( $match, \&handler );
my $value_spec = $inner_spec->in( \&might_raise );

Adds a handler to the dynamic condition resolution chain. It can be called on classes or roles that implemented Raisable. Multiple handle calls can be chained together and will match the same class or role. The returned object also provides the "in" method handling the scope in which the handlers are active.

Can be called with one or two arguments. In the one argument form, only a handler is supplied. In the two argument form, the first value is the match specification, and the second one is the handler code reference.

Both the handler and the matcher will be called with the condition as first argument and as topic in $_. The handler will always be called in scalar context.

in

my $value = ConditionClass->handle( \&handler )->in( \&might_raise );

Callable on the return value of "handle". This will invoke the code reference using the chain pointed to by the "handle" return value for resolution.

The context the code reference is invoked in depends on the context of the call to in.

required

my $value = ConditionClass->required( %arguments );

Creates a new condition object and tries to resolve it. If it can't be resolved, the condition will be turned into an exception. This method always return a scalar.

Accepts a hash reference of key/value argument pairs for the condition.

optional

my $value = ConditionClass->optional( %arguments );

Creates a new condition object and tries to resolve it. If it can't be resolved, the condition will return an undefined value. This method always return a scalar.

Accepts a hash reference of key/value argument pairs for the condition.

required_from

my $value = ConditionClass->required_from( \&resolve, %arguments );

Tries to resolve the condition via the code reference given as first argument. The code reference will receive the %arguments that were passed in. If the code reference can't immediately resolve the condition, a condition instance will be created and the handlers get a chance to resolve it.

If no handler could resolve the condition, it will be thrown as an exception. This method always returns a scalar. The resolution function is always called in scalar context.

optional_from

my $value = ConditionClass->optional_from( \&resolve, %arguments );

Tries to resolve the condition via the code reference given as first argument. The code reference will receive the %arguments that were passed in. If the code reference can't immediately resolve the condition, a condition instance will be created and the handlers get a chance to resolve it.

If no handler could resolve the condition, an undefined value will be returned. This method always returns a scalar. The resolution function is always called in scalar context.

retry

my $value = $condition->retry( %override_arguments );

Retries the resolution function passed in via "required_from" or "optional_from". If "required" or l</optional> are used instead and no function was provided, the "RESOLVE" method will be called as object method instead. By default, this is a null operation and returns an undefined value.

The passed in arguments can override and extend the ones passed in when the condition was raised. This allows for retrying with different values. The function always returns a scalar.

final

my $final_marker = $condition->final;
my $final_marker = $condition->final( $value );

Returns a marker that, if returned from a handler, immediately stops the resolution chain and returns the $value or an undefined value if nothing was provided to the point at which the condition was raised.

ACCEPT

my $is_value_valid = ConditionClass->ACCEPT( $value, $file, $line );

This method determines if the $value satisfies the condition. By default only an undefined value is unacceptable. The method also receives the file and (approximate) line number of where the values originates from for better error handling.

MATCH

my $is_match = $condition->MATCH( $value, $file, $line );

Determines if the condition is matched by the $value. By default only code references are valid match values. All other types will issue an error. See Raisable::Match::Payload for an extension allowing you to match against condition attributes.

The method also receives the file and (approximate) line number the match value is coming from for error handling purposes.

A code reference $value will receive the condition as $_ and as first argument.

RESOLVE

my $value = $condition->RESOLVE;

This is basically a fallback routine. If "require" or "optional" were used and no resolution routine supplied, this condition method will be used to try and resolve the condition before the handlers get invoked. It is also used on "retry".

The method is always called in scalar context and must always return a scalar value.

DEFAULT

my $value = $condition->DEFAULT;

The value of this method is returned when no other acceptable value could be found. By default, it returns an undefined value.

CAVEATS

Context restriction

This module currently assumes and enforces scalar context in most cases. Invalid value detection would get much more complicated if dynamic context were provided. Only having to deal with a single context also makes the code more robust and easier to debug.

SEE ALSO

BUGS

Please report any bugs or feature requests to [email protected] or through the web interface at: http://rt.cpan.org/Public/Dist/Display.html?Name=Raisable

AUTHOR

Robert 'phaylon' Sedlacek <[email protected]>

COPYRIGHT AND LICENSE

This software is copyright (c) 2011 by Robert 'phaylon' Sedlacek.

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

raisable's People

Contributors

phaylon avatar

Stargazers

 avatar

Watchers

 avatar James Cloos avatar

Forkers

sergeyromanov

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.