# $Id: Object.pm 13 2008-01-25 19:17:27Z dkrotkine $ package RT::Client::REST::Object; =head1 NAME RT::Client::REST::Object -- base class for RT objects. =head1 SYNOPSIS # Create a new type package RT::Client::REST::MyType; use base qw(RT::Client::REST::Object); sub _attributes {{ myattribute => { validation => { type => SCALAR, }, }, }} sub rt_type { "mytype" } 1; =head1 DESCRIPTION The RT::Client::REST::Object module is a superclass providing a whole bunch of class and object methods in order to streamline the development of RT's REST client interface. =head1 ATTRIBUTES Attributes are defined by method C<_attributes> that should be defined in your class. This method returns a reference to a hash whose keys are the attributes. The values of the hash are attribute settings, which are as follows: =over 2 =item list If set to true, this is a list attribute. See L below. =item validation A hash reference. This is passed to validation routines when associated mutator is called. See L for reference. =item rest_name This specifies this attribute's REST name. For example, attribute "final_priority" corresponds to RT REST's "FinalPriority". This option may be omitted if the two only differ in first letter capitalization. =item form2value Convert form value (one that comes from the server) into attribute-digestible format. =item value2form Convert value into REST form format. =back Example: sub _attributes {{ id => { validation => { type => SCALAR, regex => qr/^\d+$/, }, form2value => sub { shift =~ m~^ticket/(\d+)$~i; return $1; }, value2form => sub { return 'ticket/' . shift; }, }, admin_cc => { validation => { type => ARRAYREF, }, list => 1, rest_name => 'AdminCc', }, }} =head1 LIST ATTRIBUTE PROPERTIES List attributes have the following properties: =over 2 =item * When called as accessors, return a list of items =item * When called as mutators, only accept an array reference =item * Convenience methods "add_attr" and "delete_attr" are available. For example: # Get the list my @requestors = $ticket->requestors; # Replace with a new list $ticket->requestors( [qw(dude@localhost)] ); # Add some random guys to the current list $ticket->add_requestors('randomguy@localhost', 'evil@local'); =back =head1 SPECIAL ATTRIBUTES B and B are special attributes. They are used by various DB-related methods and are especially relied upon by B, B, and B features. =head1 METHODS =over 2 =cut use strict; use warnings; use vars qw($VERSION); $VERSION = '0.09'; use Error qw(:try); use Params::Validate; use RT::Client::REST::Object::Exception 0.04; use RT::Client::REST::SearchResult 0.02; =item new Constructor =cut sub new { my $class = shift; if (@_ & 1) { RT::Client::REST::Object::OddNumberOfArgumentsException->throw; } my $self = bless {}, ref($class) || $class; my %opts = @_; my $id = delete($opts{id}); if (defined($id)) {{ $self->id($id); if ($self->can('parent_id')) { # If object can parent_id, we assume that it's needed for # retrieval. my $parent_id = delete($opts{parent_id}); if (defined($parent_id)) { $self->parent_id($parent_id); } else { last; } } if ($self->autoget) { $self->retrieve; } }} while (my ($k, $v) = each(%opts)) { $self->$k($v); } return $self; } =item _generate_methods This class method generates accessors and mutators based on B<_attributes> method which your class should provide. For items that are lists, 'add_' and 'delete_' methods are created. For instance, the following two attributes specified in B<_attributes> will generate methods 'creator', 'cc', 'add_cc', and 'delete_cc': creator => { validation => { type => SCALAR }, }, cc => { list => 1, validation => { type => ARRAYREF }, }, =cut sub _generate_methods { my $class = shift; my $attributes = $class->_attributes; while (my ($method, $settings) = each(%$attributes)) { no strict 'refs'; *{$class . '::' . $method} = sub { my $self = shift; if (@_) { my $caller = defined((caller(1))[3]) ? (caller(1))[3] : ''; if ($settings->{validation} && # Don't validate values from the server $caller ne __PACKAGE__ . '::from_form') { my @v = @_; Params::Validate::validation_options( on_fail => sub { no warnings 'uninitialized'; RT::Client::REST::Object::InvalidValueException ->throw( "'@v' is not a valid value for attribute '$method'" ); }, ); validate_pos(@_, $settings->{validation}); } $self->{'_' . $method} = shift; $self->_mark_dirty($method); # Let's try to autosync, shall we? Logic is a bit hairy # in order to make it efficient. if ($self->autosync && $self->can('store') && # OK, so id is special. This is so that 'new' would # work. 'id' ne $method && 'parent_id' ne $method && # Plus we don't want to store right after retrieving # (that's where from_form is called from). $caller ne __PACKAGE__ . '::from_form') { $self->store; } } if ($settings->{list}) { my $retval = $self->{'_' . $method} || []; return @$retval; } else { return $self->{'_' . $method}; } }; if ($settings->{list}) { # Generate convenience methods for list manipulation. my $add_method = $class . '::add_' . $method; my $delete_method = $class . '::delete_' . $method; *$add_method = sub { my $self = shift; unless (@_) { RT::Client::REST::Object::NoValuesProvidedException ->throw; } my @values = $self->$method; my %values = map { $_, 1 } @values; # Now add new values for (@_) { $values{$_} = 1; } $self->$method([keys %values]); }; *$delete_method = sub { my $self = shift; unless (@_) { RT::Client::REST::Object::NoValuesProvidedException ->throw; } my @values = $self->$method; my %values = map { $_, 1 } @values; # Now delete values for (@_) { delete $values{$_}; } $self->$method([keys %values]); }; } } } =item _mark_dirty($attrname) Mark an attribute as dirty. =cut sub _mark_dirty { my ($self, $attr) = @_; $self->{__dirty}{$attr} = 1; } =item _dirty Return the list of dirty attributes. =cut sub _dirty { my $self = shift; if (exists($self->{__dirty})) { return keys %{$self->{__dirty}}; } return; } =item _mark_dirty_cf($attrname) Mark an custom flag as dirty. =cut sub _mark_dirty_cf { my ($self, $cf) = @_; $self->{__dirty_cf}{$cf} = 1; } =item _dirty_cf Return the list of dirty custom flags. =cut sub _dirty_cf { my $self = shift; if (exists($self->{__dirty_cf})) { return keys %{$self->{__dirty_cf}}; } return; } =item to_form($all) Convert the object to 'form' (used by REST protocol). This is done based on B<_attributes> method. If C<$all> is true, create a form from all of the object's attributes and custom flags, otherwise use only dirty (see B<_dirty> method) attributes and custom flags. Defaults to the latter. =cut sub to_form { my ($self, $all) = @_; my $attributes = $self->_attributes; my @attrs = ($all ? keys(%$attributes) : $self->_dirty); my %hash; for my $attr (@attrs) { my $rest_name = (exists($attributes->{$attr}{rest_name}) ? $attributes->{$attr}{rest_name} : ucfirst($attr)); my $value; if (exists($attributes->{$attr}{value2form})) { $value = $attributes->{$attr}{value2form}($self->$attr); } elsif ($attributes->{$attr}{list}) { $value = join(',', $self->$attr); } else { $value = (defined($self->$attr) ? $self->$attr : 'Not set'); } $hash{$rest_name} = $value; } my @cfs = ($all ? $self->cf : $self->_dirty_cf); for my $cf (@cfs) { $hash{'CF-' . $cf} = $self->cf($cf); } return \%hash; } =item from_form Set object's attributes from form received from RT server. =cut sub from_form { my $self = shift; unless (@_) { RT::Client::REST::Object::NoValuesProvidedException->throw; } my $hash = shift; unless ('HASH' eq ref($hash)) { RT::Client::REST::Object::InvalidValueException->throw( "Expecting a hash reference as argument to 'from_form'", ); } # lowercase hash keys my $i = 0; $hash = { map { ($i++ & 1) ? $_ : lc } %$hash }; my $attributes = $self->_attributes; my %rest2attr; # Mapping of REST names to our attributes; while (my ($attr, $value) = each(%$attributes)) { my $rest_name = (exists($attributes->{$attr}{rest_name}) ? lc($attributes->{$attr}{rest_name}) : $attr); $rest2attr{$rest_name} = $attr; } # Now set attbibutes: while (my ($key, $value) = each(%$hash)) { if ($key =~ s/^cf-//) { # Handle custom fields. if ($value =~ /,/) { # OK, this is questionable. $value = [ split(/\s*,\s*/, $value) ]; } $self->cf($key, $value); next; } unless (exists($rest2attr{$key})) { warn "Unknown key: $key\n"; next; } if ($value =~ m/not set/i) { $value = undef; } my $method = $rest2attr{$key}; if (exists($attributes->{$method}{form2value})) { $value = $attributes->{$method}{form2value}($value); } elsif ($attributes->{$method}{list}) { $value = [split(/\s*,\s*/, $value)], } $self->$method($value); } return; } sub retrieve { my $self = shift; $self->_assert_rt_and_id; my $rt = $self->rt; my ($hash) = $rt->show(type => $self->rt_type, id => $self->id); $self->from_form($hash); $self->{__dirty} = {}; $self->{__dirty_cf} = {}; return $self; } sub store { my $self = shift; $self->_assert_rt; my $rt = $self->rt; if (defined($self->id)) { $rt->edit( type => $self->rt_type, id => $self->id, set => $self->to_form, ); } else { my $id = $rt->create( type => $self->rt_type, set => $self->to_form, @_, ); $self->id($id); } $self->{__dirty} = {}; return $self; } sub search { my $self = shift; if (@_ & 1) { RT::Client::REST::Object::OddNumberOfArgumentsException->throw; } $self->_assert_rt; my %opts = @_; my $limits = delete($opts{limits}) || []; my $query = ''; for my $limit (@$limits) { my $kw; try { $kw = $self->_attr2keyword($limit->{attribute}); } catch RT::Clite::REST::Object::InvalidAttributeException with { RT::Client::REST::Object::InvalidSearchParametersException ->throw(shift->message); }; my $op = $limit->{operator}; my $val = $limit->{value}; my $agg = $limit->{aggregator} || 'and'; if (length($query)) { $query = "($query) $agg $kw $op '$val'"; } else { $query = "$kw $op '$val'"; } } my $orderby; try { # Defaults to 'id' at the moment. Do not rely on this -- # implementation may change! $orderby = (delete($opts{reverseorder}) ? '-' : '+') . ($self->_attr2keyword(delete($opts{orderby}) || 'id')); } catch RT::Clite::REST::Object::InvalidAttributeException with { RT::Client::REST::Object::InvalidSearchParametersException->throw( shift->message, ); }; my $rt = $self->rt; my @results; try { @results = $rt->search( type => $self->rt_type, query => $query, orderby => $orderby, ); } catch RT::Client::REST::InvalidQueryException with { RT::Client::REST::Object::InvalidSearchParametersException->throw; }; return RT::Client::REST::SearchResult->new( ids => \@results, object => sub { $self->new(id => shift, rt => $rt) }, ); } sub count { my $self = shift; $self->_assert_rt; return $self->search(@_)->count; } sub _attr2keyword { my ($self, $attr) = @_; my $attributes = $self->_attributes; unless (exists($attributes->{$attr})) { no warnings 'uninitialized'; RT::Clite::REST::Object::InvalidAttributeException->throw( "Attribute '$attr' does not exist in object type '" . ref($self) . "'" ); } return (exists($attributes->{$attr}{rest_name}) ? $attributes->{$attr}{rest_name} : ucfirst($attr)); } sub _assert_rt_and_id { my $self = shift; my $method = shift || (caller(1))[3]; unless (defined($self->rt)) { RT::Client::REST::Object::RequiredAttributeUnsetException ->throw("Cannot '$method': 'rt' attribute of the object ". "is not set"); } unless (defined($self->id)) { RT::Client::REST::Object::RequiredAttributeUnsetException ->throw("Cannot '$method': 'id' attribute of the object ". "is not set"); } } sub _assert_rt { my $self = shift; my $method = shift || (caller(1))[3]; unless (defined($self->rt)) { RT::Client::REST::Object::RequiredAttributeUnsetException ->throw("Cannot '$method': 'rt' attribute of the object ". "is not set"); } } =item param($name, $value) Set an arbitrary parameter. =cut sub param { my $self = shift; unless (@_) { RT::Client::REST::Object::NoValuesProvidedException->throw; } my $name = shift; if (@_) { $self->{__param}{$name} = shift; } return $self->{__param}{$name}; } =item cf([$name, [$value]]) Given no arguments, returns the list of custom field names. With one argument, returns the value of custom field C<$name>. With two arguments, sets custom field C<$name> to C<$value>. Given a reference to a hash, uses it as a list of custom fields and their values, returning the new list of all custom field names. =cut sub cf { my $self = shift; unless (@_) { # Return a list of CFs. return keys %{$self->{__cf}}; } my $name = shift; if ('HASH' eq ref($name)) { while (my ($k, $v) = each(%$name)) { $self->{__cf}{lc($k)} = $v; $self->_mark_dirty_cf($k); } return keys %{$self->{__cf}}; } else { $name = lc $name; if (@_) { $self->{__cf}{$name} = shift; $self->_mark_dirty_cf($name); } return $self->{__cf}{$name}; } } =item rt Get or set the 'rt' object, which should be of type L. =cut sub rt { my $self = shift; if (@_) { my $rt = shift; unless (UNIVERSAL::isa($rt, 'RT::Client::REST')) { RT::Client::REST::Object::InvalidValueException->throw; } $self->{__rt} = $rt; } return $self->{__rt}; } =back =head1 DB METHODS The following are methods that have to do with reading, creating, updating, and searching objects. =over 2 =item count Takes the same arguments as C but returns the actual count of the found items. Throws the same exceptions. =item retrieve Retrieve object's attributes. Note that 'id' attribute must be set for this to work. =item search (%opts) This method is used for searching objects. It returns an object of type L, which can then be used to process results. C<%opts> is a list of key-value pairs, which are as follows: =over 2 =item limits This is a reference to array containing hash references with limits to apply to the search (think SQL limits). =item orderby Specifies attribute to sort the result by (in ascending order). =item reverseorder If set to a true value, sorts by attribute specified by B in descending order. =back If the client cannot construct the query from the specified arguments, or if the server cannot make it out, C is thrown. =item store Store the object. If 'id' is set, this is an update; otherwise, a new object is created and the 'id' attribute is set. Note that only changed (dirty) attributes are sent to the server. =back =head1 CLASS METHODS =over 2 =item use_single_rt This method takes a single argument -- L object and makes this class use it for all instantiations. For example: my $rt = RT::Client::REST->new(%args); # Make all tickets use this RT: RT::Client::REST::Ticket->use_single_rt($rt); # Now make all objects use it: RT::Client::REST::Object->use_single_rt($rt); =cut sub use_single_rt { my ($class, $rt) = @_; unless (UNIVERSAL::isa($rt, 'RT::Client::REST')) { RT::Client::REST::Object::InvalidValueException->throw; } no strict 'refs'; no warnings 'redefine'; *{(ref($class) || $class) . '::rt'} = sub { $rt }; } =item use_autostore Turn autostoring on and off. Autostoring means that you do not have to explicitly call C on an object - it will be called when the object goes out of scope. # Autostore tickets: RT::Client::REST::Ticket->use_autostore(1); my $ticket = RT::Client::REST::Ticket->new(%opts)->retrieve; $ticket->priority(10); # Don't have to call store(). =cut sub autostore {} sub use_autostore { my ($class, $autostore) = @_; no strict 'refs'; no warnings 'redefine'; *{(ref($class) || $class) . '::autostore'} = sub { $autostore }; } sub DESTROY { my $self = shift; $self->autostore && $self->can('store') && $self->store; } =item use_autoget Turn autoget feature on or off (off by default). When set to on, C will be automatically called from the constructor if it is called with that object's special attributes (see L above). RT::Client::Ticket->use_autoget(1); my $ticket = RT::Client::Ticket->new(id => 1); # Now all attributes are available: my $subject = $ticket->subject; =cut sub autoget {} sub use_autoget { my ($class, $autoget) = @_; no strict 'refs'; no warnings 'redefine'; *{(ref($class) || $class) . '::autoget'} = sub { $autoget }; } =item use_autosync Turn autosync feature on or off (off by default). When set, every time an attribute is changed, C method is invoked. This may be pretty expensive. =cut sub autosync {} sub use_autosync { my ($class, $autosync) = @_; no strict 'refs'; no warnings 'redefine'; *{(ref($class) || $class) . '::autosync'} = sub { $autosync }; } =item be_transparent This turns on B and B. Transparency is a neat idea, but it may be expensive and slow. Depending on your circumstances, you may want a finer control of your objects. Transparency makes C and C calls invisible: RT::Client::REST::Ticket->be_transparent($rt); my $ticket = RT::Client::REST::Ticket->new(id => $id); # retrieved $ticket->add_cc('you@localhost.localdomain'); # stored $ticket->status('stalled'); # stored # etc. Do not forget to pass RT::Client::REST object to this method. =cut sub be_transparent { my ($class, $rt) = @_; $class->use_autosync(1); $class->use_autoget(1); $class->use_single_rt($rt); } =back =head1 SEE ALSO L, L. =head1 AUTHOR Dmitri Tikhonov =cut 1;