NAME
Rope - Tied objects
VERSION
Version 0.37
SYNOPSIS
package Knot;
use Rope;
prototyped (
bend_update_count => 0,
loops => 1,
hitches => 10,
...
);
properties (
bends => {
type => sub { $_[0] =~ m/^\d+$/ ? $_[0] : die "$_[0] != integer" },
value => 10,
initable => 1,
configurable => 1,
enumerable => 1,
required => 1,
trigger => sub {
my ($self, $value) = @_;
$self->{bend_update_count}++;
return $value;
}
delete_trigger => sub { ... }
},
...
);
function add_loops => sub {
my ($self, $loop) = @_;
$self->{loops} += $loop;
};
1;
...
my $k = Knot->new();
say $k->{loops}; # 1;
$k->{add_loops}(5);
say $k->{loops}; # 6;
$k->{add_loops} = 5; # errors
DESCRIPTION
Rope
is an Object Orientation system that is built on top of perls core hash tying implementation. It extends the functionality which is available with all the modern features you would expect from an modern OO system. This includes clear class and role definitions. With Rope you also get out of the box sorted objects, where the order you define persists.
CONFIGURE PROPERTIES
initable
If set to a true value then this property can be initialised during the object creationg, when calling ->new(%params). If set to false then on initialisation the code will die with a relevant error when you try to initialise it. (Cannot initalise Object ($name) property ($key) as initable is not set to true.)
writeable
If set to a true value then this property value can be updated after initialisation with any other value. If set to false then the code will die with a relevant error. If writeable is true then configurable is not checked and redundent. (Cannot set Object ($name) property ($key) it is only readable)
configurable
If set to a true value then this property value can be updated after initialisation with a value that matches the type of the existing. If you try to set a value which is not of the same type the code will die with a relevant error. (Cannot change Object ($name) property ($key) type). If you set to false and writeable is also false you will get the same error as writeable false.
enumerable
If set to a true value then the property will be enumerable when you itterate the object for example when you call keys %{$self}. If set to false then the property will be hidden from any itteration. Note also that when itterating your object keys are already ordered based on the order they were assigned.
private
If set to a true value then the property will be private, If you try to access the property from outside of the object definition then an error will be thrown. (Cannot access Object (Custom) property ($key) as it is private)
required
If set to a true value then this property is required at initialisation, either by a value key being set or via passing into ->new. I would suggest using this in conjunction with initable when you require a value is passed. If no value is passed and required is set to true then the code will die with a relevant error. (Required property ($key) in object ($object) not set)
type
The type property/key expects a code ref to be passed, all values that are then set either during initialisation or writing will run through this ref. Rope expects that you return the final value from this ref so you can use coercion via a type.
builder
The buidler property/key expects either a code ref or a scalar that represents the name of a sub routine in your class (currently not functions/properties but may extend in the future). It expects the value for that property to be returned from either the code ref or sub routine. Within a builder you can also add new properties to your object by extending the passed defintion, when extending this way I would suggest using ++$_[0]->{keys} to set the index so that sorting is persistent further down the line.
trigger
The trigger property/key expects a code ref that expects the value for that property to be returned so it can also be used for coecion on setting of a property. Two values are passed into a trigger the first being $self which allows you to access and manipulate the existing objects other properties, the second is the value that is being set for the property being called.
delete_trigger
A delete trigger works the same as a trigger but is called on delete instead of set. The second param is the last set value for that property.
predicate
A predicate allows you to define an accessor to check whether a property is set. You have a few options, you can pass a positive integer or code reference and that will default the predicate to "has_$prop". You can also pass a string to name the accessor like "has_my_thing" or finally if you would like more customisation you can pass a hash that defines the object properties. You cannot create triggers or hooks on predicates directly.
clearer
A clearer allows you to define an accessor to clear a property (set it to undef). You have a few options, you can pass a positive integer or code reference and that will default the clearer to "clear_$prop". You can also pass a string to name the accessor like "clear_my_thing" or finally if you would like more customisation you can pass a hash that defines the object properties. You cannot create triggers or hooks on clearers. when using clearers and type checking in conjuction you need to ensure the type supports undef or you need to create your own custom clearer using a code ref or hash ref.
index
The index property/key expects an integer, if you do not set then this integer it's automatically generated and associated to the property. You will only want to set this is you always want to have a property last when itterating
handles_via
See Rope::Handles::Hash.
handles
See Rope::Handles::Array.
KEYWORDS
property
Extends the current object definition with a single new property
property one => (
initable => 1,
writeable => 0,
enumerable => 1,
builder => sub {
return 200;
}
);
property [qw/a b c/] => ( ... );
properties
Extends the current object definition with multiple new properties
properties (
two => {
type => sub { $_[0] =~ m/^\d+$/ ? $_[0] : die "$_[0] != integer" },
value => 10,
initable => 1,
configurable => 1,
enumerable => 1,
required => 1
},
[qw/a b c/] => { ... }
...
);
prototyped
Extends the current object definition with multiple new properties where initable, writable and enumerable are all set to a true value.
prototyped (
three => 10,
[qw/a b c/] => 211
...
);
function
Extends the current object definition with a new property that acts as a function. A function has initable, writeable, enumerable and configurable all set to false so it cannot be changed/set once the object is instantiated.
function four => sub {
my ($self, $param) = @_;
...
};
NOTE: traditional sub routines work and should be inherited also.
sub four {
my ($self, $param) = @_;
...
}
extends
The extends keyword allows you to extend your current definition with another object, your object will inherit all the properties of that extended object.
package Ping;
use Rope;
extends 'Pong';
with
The with keyword allows you to include roles in your current object definition, your object will inherit all the properties of that role.
package Ping;
use Rope;
with 'Pong';
requires
The requires keyword allows you to define properties which are required for either a role or an object, it works in both directions.
package Pong;
use Rope::Role;
requires qw/host/;
function ping => sub { ... };
function pong => sub { ... };
package Ping;
use Rope;
requires qw/ping pong/;
with 'Pong';
prototyped (
host => '...'
);
before
The before keyword allows you to hook in before you call a sub routine or setting a value. If a defined response is returned it will be carried through to any subsequent calls including the setting of a property.
package Locked;
use Rope;
use Rope::Autoload;
property count => (
value => 0,
configurable => 1,
enumerable => 1
);
function two => sub {
my ($self, $count) = @_;
$self->count = $count;
return $self->count;
};
1;
package Load;
use Rope;
extends 'Locked';
before count => sub {
my ($self, $val) = @_;
$val = $val * 2;
return $val;
};
before two => sub {
my ($self, $count) = @_;
... do something that doesn't return a value ...
return;
};
1;
around
The around keyword allows you to hook in around the calling of a sub routine or setting a value. If a defined response is returned it will be carried through to any subsequent calls including the setting of a property. To chain you must use the second param as a code reference.
package Loading;
use Rope;
extends 'Load';
around two => sub {
my ($self, $cb, $val) = @_;
$val = $val * 4;
return $cb->($val);
};
1;
after
The after keyword allows you to hook in after the calling of a sub routine or setting a value. If a defined response is returned it will be carried through to any subsequent calls including the setting of a property.
package Loaded;
use Rope;
extends 'Loading';
after two => sub {
my ($self, $val) = @_;
$val = $val * 4;
return $val;
};
1;
locked
The locked keyword can be used to lock your Object so that it can no longer be extended with new properties/keys.
package Locked;
use Rope;
property one => ( ... );
locked;
1;
Once you have an object initialised you can toggle whether it is locked by calling 'locked' with either a true or false value.
$obj->locked(1);
$obj->locked(0);
METHODS
new
Along with class definitions you can also generate object using Rope itself, the options are the same as described above.
my $knot = Rope->new({
name => 'Knot',
properties => [
loops => 1,
hitches => {
type => Int,
value => 10,
initable => 0,
configurable => 0,
},
add_loops => sub {
my ($self, $loop) = @_;
$self->{loops} += $loop;
}
]
});
my $with = Rope->new({
use => [ 'Rope::Autoload' ],
with => [ 'Knot' ],
requires => [ qw/loops hitches add_loops/ ],
properties => [ bends => { type => Int, initable => 1, configurable => 1 }, ... ]
}, bends => 5);
$knot->{loops};
$with->loops;
INITIALISE
An before hook into new, the Rope META structure is passed as a param and can be validated and extended from within the sub routine.
my $knot = Rope->new({
name => 'Knot',
properties => [
loops => 1,
],
INITIALISE => sub {
$_[1]->{properties}->{hitches} = {
type => Int,
value => 10,
initable => 0,
configurable => 0,
};
$_[1]->{properties}->{add_loops} => sub {
my ($self, $loop) = @_;
$self->{loops} += $loop;
};
}
});
INITIALISED
An after hook into new, the initialised Rope is passed as $self and can be validated and extended from within the sub routine.
my $knot = Rope->new({
name => 'Knot',
properties => [
loops => 1,
],
INITIALISED => sub {
$_[0]->{hitches} = 10;
$_[0]->{add_loops} = sub {
my ($self, $loop) = @_;
$self->{loops} += $loop;
};
}
});
destroy
For objects that are running in a long process, like under a web server, you will want to explicitly call destroy on your objects as to achieve the correct scoping a reference has to be held in memory. If running under a script the problem will not exists as destruction happens when that script ends.
$knot->destroy();
get_initialised
This returns an existing initialised object by index set in order of initialisation, as long as it has not been destroyed by scope or calling the ->destroy explicitly, it will return undef in those cases.
Rope->get_initialised('Knot', 0);
get_meta
This returns the existing META definition for an object.
Rope->get_meta('Knot');
NOTE: this is now read only
clear_meta
Rope->clear_meta('Knot');
set_meta
Extend or redefine an object meta definition.
package Knot;
use Rope;
prototyped (
one => 1,
two => 2,
three =>3
);
1;
package Hitch
extends 'Knot';
1;
my $meta = Ropt->get_meta('Hitch');
Rope->clear_meta('Hitch');
delete $meta->{properties};
Rope->set_meta($meta);
Hitch->new->one;
from_data
Initialise a Rope object from a perl hash struct.
my $obj = Rope::Object->from_data({
one => 1,
two => {
a => 1,
b => 2
},
three => [
{
a => 1,
b => 2
}
]
}, { use => 'Rope::Monkey' });
...
$obj->one;
$obj->two->{a};
$obj->three->[0]->{a};
from_nested_data
Initialise a Rope object from a nested perl hash struct.
my $obj = Rope::Object->from_nested_data({
one => 1,
two => {
a => 1,
b => 2
},
three => [
{
a => 1,
b => 2
}
]
}, { use => 'Rope::Autoload' });
...
$obj->one;
$obj->two->a;
$obj->three->[0]->a;
NOTE: this generates an object definition per array item so in many cases it is better you define your object definitions yourself and then write the relevant logic to initialise them. I will eventually look into perhaps itterating all items finding all unique keys and then initialiasing, that will just take a bit more time though.
from_array
Initialise a Rope object from a ordered perl array struct.
my $obj = Rope::Object->from_array([
one => 1,
two => {
a => 1,
b => 2
},
three => [
{
a => 1,
b => 2
}
]
], { use => 'Rope::Monkey' });
...
$obj->one;
$obj->two->{a};
$obj->three->[0]->{a};
from_nested_array
Initialise a Rope object from a nested perl array struct.
my $obj = Rope::Object->from_nested_array([
one => 1,
two => [
a => 1,
b => 2
],
three => [
[
a => 1,
b => 2
]
],
four => [qw/one two three/, { ROPE_scope => 'ARRAY' }]
}, { use => 'Rope::Autoload' });
...
$obj->one;
$obj->two->a;
$obj->three->[0]->a;
OBJECT CLASS DEFINITION
package Builder;
use Rope;
property one => (
initable => 1,
writeable => 0,
enumerable => 1,
builder => sub {
return 200;
}
);
property two => (
writeable => 0,
enumerable => 0,
builder => sub {
$_[0]->{properties}->{three} = {
value => 'works',
writeable => 0,
index => ++$_[0]->{keys}
};
return $_[0]->{properties}->{one}->{value} + 11;
}
);
1;
OBJECT ROLE DEFINITION
package Builder::Role;
use Rope::Role;
property one => (
initable => 1,
writeable => 0,
enumerable => 1,
builder => sub {
return 200;
}
);
1;
FACTORY AND CHAINS
With Rope you can also create chained and factory properties. A chained property is where you define several code blocks to be called in order that you define, they effectively hook into the Rope cores 'after' callback. A factory sub allows you to define a property which will react based upon the passed in parameters. Factories and Chains can be used in conjunction, the following is and example of a factory into a chain.
package Church;
use Rope;
use Rope::Autoload;
use Rope::Factory qw/Str/;
use Rope::Chain;
prototyped (
been_cannot_find => [],
found => []
);
function reset => sub {
$_[0]->been_cannot_find = [];
$_[0]->found = [];
};
factory add => (
[Str] => sub {
$_[0]->reset() if $_[1] eq 'reset';
},
[Str, Str] => sub {
$_[0]->reset() if $_[1] eq 'reset';
push @{$_[0]->found}, $_[2];
},
sub { return 'fallback' }
);
chain add => 'ephesus' => sub {
push @{ $_[0]->been_cannot_find }, 'Ephesus';
return;
};
chain add => 'smyrna' => sub {
push @{ $_[0]->been_cannot_find }, 'Smyrna';
return;
};
chain add => 'pergamon' => sub {
push @{ $_[0]->been_cannot_find }, 'Pergamon';
return;
};
chain add => 'thyatira' => sub {
push @{ $_[0]->been_cannot_find }, 'Thyatira';
return $_[0]->been_cannot_find;
};
...
my $asia = Church->new();
$asia->add('reset');
$asia->add('reset', 'House of Mary');
$asia->reset();
$asia->thyatira();
Notes: If you chain into a factory, the factory will accept not the passed params but the response from the 'last' chain. Returning undef is a hack that only works when chaining a chain, your first defined chain in this approach should return either the passed request params or some valid response. If you define multiple factories with the same name, the initial will be extended not re-written, so extending a factory or chain should work accross inheritance. If you would like to extend and existing et chain two factories together you can by passing an integer to the factory you would like to create or extend, you must understand the order they are instantiated for this to work as expected.
factory add => 1 => ( # using the index 0 would extend the existing this extends after a new 'factory'
[ArrayRef] => sub {
push @{$_[1]}, 'Sardis';
return $_[1];
},
);
AUTOLOADING
If you do not enjoy accessing properties as hash keys and would instead like to access them as package routines then you can simply include Rope::Autoload
and this will use perls internal AUTOLOAD functionality to expose your properties as routines.
package Builder;
use Rope;
use Rope::Autoload;
...
1;
So you can write
$builder->thing = 10;
Instead of
$builder->{thing} = 10;
MONKEY PATCHING
If for some reason you do not like the Autoload approach then Rope also has support for monkey patching the package routines.
package Builder;
use Rope;
use Rope::Monkey;
...
monkey;
1;
$builder->thing = 10;
TYPES
Rope also includes additional helpers for defining properties with fixed types, see Rope::Type
for more information. ( internally that uses Type::Standard
for the actual type checking. )
package Knot;
use Rope;
use Rope::Type qw/int/;
int loops => 1;
int hitches => 10;
1;
CAVEATS
before, around and after hooks will work on any property regardless of configuration unless the value is a code reference, here the property must be readonly (you get this by using the function keyword). This is to prevent unexpected behaviour when trying if you have code which sets the property with a new code ref.
If you have a long running process I would suggest calling ->destroy() directly, i hook into the actual DESTROY method however this may or may not be called if Rope keeps the variable in scope.
AUTHOR
LNATION, <email at lnation.org>
BUGS
Please report any bugs or feature requests to bug-rope at rt.cpan.org
, or through the web interface at https://rt.cpan.org/NoAuth/ReportBug.html?Queue=Rope. I will be notified, and then you'll automatically be notified of progress on your bug as I make changes.
SUPPORT
You can find documentation for this module with the perldoc command.
perldoc Rope
You can also look for information at:
RT: CPAN's request tracker (report bugs here)
CPAN Ratings
Search CPAN
ACKNOWLEDGEMENTS
LICENSE AND COPYRIGHT
This software is Copyright (c) 2023 by LNATION.
This is free software, licensed under:
The Artistic License 2.0 (GPL Compatible)