NAME
Salvation::MacroProcessor - Macros definition and processing engine
DESCRIPTION
What is it?
Salvation::MacroProcessor is another architectural solution.
It is aimed to help to avoid code doubling and increase re-usability of code snippets by providing some kind of macros definition and processing engine.
It provides an architecture and a core functions, but leaves creation of the code that will actually do something up to you.
What should I do to use it?
First of all, you should use Moose.
Next, you should define a Salvation::MacroProcessor::Hook-derived class with specific name and methods.
Next, you should define method descriptions. Consider using functions from Salvation::MacroProcessor or Salvation::MacroProcessor::ForRoles modules for this.
Then you'll be able to use your descriptions via Salvation::MacroProcessor::Spec module.
Can I look at some example besides tests?
Oh, yes, you can.
Say we have a class Class1
:
package Class1;
use Moose;
no Moose;
This is a base class for, say, our little ORM. It has method named get_many
which selects any arbitrary amount of rows from database and returns each row as a Class1
class instance.
sub get_many
{
my ( $self, @query ) = @_;
my @rows = &do_real_db_select_and_make_objects( $self, \@query ); # content of this function does not mean anything for us
return &create_iterator( \@rows ); # this somehow creates an iterator object which does L<Salvation::MacroProcessor::Iterator::Compliance> role
}
Class1
also has some methods matching column names:
sub id; # is primary key
sub column1;
sub column2;
sub column3;
. And sometimes you need to query database with following conditions:
where ( ( column1 = 'some value' and column2 is null and column3 is not null ) or ( column1 is null and column2 = 'some other value' ) )
. You're doing it using your ORM, which results in, say, following call:
my $it = Class1 -> get_many(
_clause => [
cond => [
_clause => [
cond => [
column1 => 'some value',
column2 => { is => undef },
column3 => { 'is not' => undef }
]
],
_clause => [
cond => [
column1 => { is => undef },
column2 => 'some other value'
]
]
],
logic => 'or'
]
)
. And every time you need to make this query - you need to take this call with you. Of course you can put in inside some function, then call that function to retrieve your conditions and be happy. Then you will encounter the need to mix this query with some other parts, or change it slightly, and your function you used to retrieve your conditions could grow if it has been written without future in mind. Alright, let's imagine you are the best and you've done everything right here. Doesn't matter. Moving forward.
You already have a method inside Class1
, which will check if current instance matches criteria above, or not:
sub check_if_this_is_the_object;
This method does return true
or false
, checking your object. So, when you need to select objects, you use your get_many
call with arguments, and when you need to check one object - you call check_if_this_is_the_object
. Kinda not unified.
Well then, let's see what you can do.
Let's define a class named Salvation::MacroProcessor::Hooks::Class1
and define two methods: one to select objects and one to check existing objects.
package Salvation::MacroProcessor::Hooks::Class1;
use Moose;
extends 'Salvation::MacroProcessor::Hooks';
sub select
{
my ( $self, $spec, $additional_query ) = @_;
my $it = $spec -> class() -> get_many( @{ $spec -> query() }, @$additional_query ); # select many objects
return Salvation::MacroProcessor::Iterator -> new(
postfilter => sub{ $spec -> __postfilter_each( shift ) }, # kind of common statement
iterator => $it
);
}
sub check
{
my ( $self, $spec, $object ) = @_;
my $it = $self -> select( $spec, [ _id => $object -> id() ] );
my $cnt = 0;
my $db_object = undef;
while( defined( my $row = $it -> next() ) )
{
die if $cnt ++;
$db_object = $row;
}
return ( defined( $db_object ) and ( $object -> id() == $db_object -> id() ) ); # check if got the same object
}
no Moose;
Then let's add description for method of Class1
and apply a role aimed to simplify our life:
use Salvation::MacroProcessor;
with 'Salvation::MacroProcessor::Role';
smp_add_description check_if_this_is_the_object => (
query => sub
{
my $value = shift;
return ( $value ? \@criteria_from_the_example_above : \@inverted_version_of_the_criteria_above );
}
);
. So now we have hook - implementation of our specific logic of querying database, and also we have one description for a method. So we can do this:
my $it = Class1 -> smp_select(
[ check_if_this_is_the_object => true ] # to select some objects matching criteria
);
my $it = Class1 -> smp_select(
[ check_if_this_is_the_object => false ]
);
my $bool = $Class1_instance -> smp_check(
[ check_if_this_is_the_object => true ] # to check one object to match criteria
);
my $bool = $Class1_instance -> smp_check(
[ check_if_this_is_the_object => false ]
);
. And also we could easily mix this criteria with other ones which will be needed in the future. Needless to say we have an implementation of check_if_this_is_the_object
criteria not only in one separate place, but in the nearest place to the class: inside this class.
Later, if we want to create class Class2
which will be a Class1
-derived class,
package Class2;
use Moose;
extends 'Class1';
no Moose;
, we don't need to do anything else to use Salvation::MacroProcessor then what we have already done. That is, you need to do nothing: don't need to create the same description as for parent class, don't need to create another hook, nothing! You're provided with full Salvation::MacroProcessor functionality inherited from parent class right out-of-the-box.
This is key concept of Salvation::MacroProcessor and its method descriptions which could be defined once and reused endless amount of times, easily mixed with other descriptions, imported from other classes via connectors, and so on.
You can continue reading the docs for appropriate base classes and other modules, read the tests, or just experiment by yourself to learn more about Salvation::MacroProcessor.
REQUIRES
FUNCTIONS
smp_add_description
smp_add_description some_method_name => (
query => $query,
postfilter => $postfilter,
required_shares => $required_shares,
required_filters => $required_filters,
excludes_filters => $excludes_filters
);
Create description for method.
Method descriptions are the thing we are here for.
Each argument besides some_method_name
is optional, though it should have at least query
specified for description to make any sense.
- some_method_name
-
String, is a name of a method that is already present in your class.
As Salvation::MacroProcessor is for describing methods which are already present, for the sake of semantics you should always already have a method being described, though its implementation is not strict to not relay on such method's description.
Is also a name of description.
- query
-
ArrayRef or CodeRef, represents query part which needs to be applied to the query to get an object which satisfies specified criteria.
When ArrayRef, it won't be modified anyhow, but will be applied to the query as-is.
When CodeRef, it should return an ArrayRef which will be then applied to the query. Supplied function should match one of the following signatures:
- ( Any $value )
-
Where
$value
is, well, a value supplied by you, or any other developer, as a condition for the filter.Function should match this signature when no
required_shares
is specified. -
Where
$shares
contains data returned by your shares' code. I.e., if you have defined a share like this:smp_add_share my_share => sub { return MyShareObject -> new(); };
, and
$required_shares
is following ArrayRef:[ 'my_share' ]
, then
$shares
will be somewhat like that:{ my_share => [ $MyShareObject_instance ] }
. You can safely access
$MyShareObject_instance
from your$query
and make any manipulations you want.Meaning of
$value
is unchanged from what have been said previously.Function should match this signature when
required_shares
is specified.
- postfilter
-
A CodeRef matching following signature:
( Any $object, Any $value )
, where
$value
is a value supplied by you, or any other developer, as a condition for the filter, and$object
is an object representing a single row of data returned by the query.This code is executed for each
$object
returned by the query in order to filter object list before returning it to a caller.Boolean value should be returned,
false
means "skip this object" andtrue
means "yes, this object is what we want". -
An ArrayRef of shares' names which are required by this filter and which will be passed to
query
' CodeRef. - required_filters
-
An ArrayRef of other descriptions' names which you are required to use in order to use this one. If at least one is missing from your query - an error will be thrown.
- excludes_filters
-
An ArrayRef of other descriptions' names which should not be used together with this one. If at least one is included in your query - an error will be thrown.
smp_add_alias
smp_add_alias 'synonym' => 'original name';
Create an alias for description.
smp_add_connector
smp_add_connector 'custom connector name' => (
required_shares => $required_shares,
code => $code
);
Create a connector. Connectors are used to import descriptions from other classes.
required_shares
has the same requirements and meaning as an argument of the same name of smp_add_description function.
code
is a CodeRef and has the same requirements as the query
argument of smp_add_description function, though its meaning is different.
$code
is called when we need to include filters from different class in a query. It is used to wrap query parts of such foreing descriptions and then apply them to current query. $value
passed to the $code
call is always of type ArrayRef[Any]
, instead of plain Any
as in smp_add_description' $query
, and this array could contain anything that is specified by you as query parts in descriptions, in almost any combinations.
Let's check out the example. Imagine that you have class Class1
which has something like that:
smp_add_description some_method => (
query => [
column => 'value'
]
);
. Also you have class Class2
which in its case has something like that:
smp_add_description some_other_method => (
query => [
other_column => 'other value'
]
);
smp_add_connector 'Class1 connector' => (
code => sub
{
my $value = shift;
# $value here is always an ArrayRef
return [ class1_descriptions => $value ];
}
);
smp_import_descriptions
class => 'Class1',
prefix => 'c1_',
connector => 'Class1 connector'
;
. Then, you are trying to execute following query using Class2
as base object:
[
[ c1_some_method => 'dummy' ],
[ some_other_method => 'dummy' ]
]
. Such query will be expanded into following object using specified connector:
[
other_column => 'other value',
class1_descriptions => [
column => 'value'
]
]
.
required_shares
is an optional argument, code
is a required one.
smp_add_share
smp_add_share 'some name' => $code;
Create a share.
Shares are implementation of user-defined instance-wide static variables for query object, kind of like Moose' attributes (see Moose::Menual::Attributes), but much lighter. In fact, shares are most like "builder" definitions.
Each share has only two properties: name ('some name'
in the example above) and factory code ($code
in the example above).
$code
is a CodeRef.
$code
is executed no more than one time for one query. $code
function is called when a description requiring this share is encountered during query composition and a value for this share has not been initialized for current query object yet.
$code
is always executed in a list context.
$code
could return
any amount of objects of any type, including an amount equal to zero. These results will be provided to you as-is.
smp_import_descriptions
smp_import_descriptions
class => $class,
prefix => $prefix,
list => $list,
connector => $connector
;
Import descriptions from another class.
Arguments:
- class
-
String, a name of a class from which descriptions will be imported.
This class should be loaded manually in order to import descriptions.
Note that actual import is lazy: each description will be imported at the time it first being required by the query, though it may be necessary to load
$class
class before smp_import_descriptions statement in order to get the list of$class
' methods. Seelist
argument documentation for more details. - prefix
-
String, custom prefix which will be applied to each and every description name.
Optional argument.
Imagine that you have class
Class1
which in its case has description for methodmethod
. You also have classClass2
which has this:smp_import_descriptions class => 'Class1', prefix => 'some_prefix_', connector => $connector ;
. It means that description for method
method
of classClass1
will be exposed to classClass2
as description with the namesome_prefix_method
.It also is used to prefix each element of
required_shares
,required_filters
andexcludes_filters
lists of each imported descriptions, though prefixed shares' and filters' names will be used for internal checks only. - list
-
ArrayRef, a list of descriptions' names to be imported.
Optional argument.
If specified, this list will be trusted and no additional checks for existense of such methods in the
$class
class will be executed. So the class$class
could be loaded anytime later, before actual usage of imported descriptions.If omitted, the system will try to build the list itself, accessing
$class
immediately at smp_import_descriptions call, requiring$class
class to be loaded. - connector
-
String, name of connector which will be used to import descriptions.
smp_import_shares
smp_import_shares
class => $class,
prefix => $prefix,
list => $list
;
Import shares from another class.
Mostly the same as smp_import_descriptions, but works with shares instead of descriptions and has no connector
argument as it is not necessary here.
If you're importing descriptions which require some shares - you should either define such shares in your target class, or import such descriptions from source class.