File Coverage

blib/lib/RT/Client/REST/Object.pm
Criterion Covered Total %
statement 212 322 65.8
branch 50 116 43.1
condition 23 59 38.9
subroutine 52 62 83.8
pod 18 18 100.0
total 355 577 61.5


line stmt bran cond sub pod time code
1             #!perl
2             # vim: softtabstop=4 tabstop=4 shiftwidth=4 ft=perl expandtab smarttab
3             # PODNAME: RT::Client::REST::Object
4             # ABSTRACT: base class for RT objects
5              
6 9     9   112887 use strict;
  9         39  
  9         256  
7 9     9   43 use warnings;
  9         19  
  9         415  
8              
9             package RT::Client::REST::Object;
10             $RT::Client::REST::Object::VERSION = '0.71';
11              
12 9     9   4097 use Try::Tiny;
  9         17181  
  9         533  
13 9     9   4323 use Params::Validate;
  9         53427  
  9         560  
14 9     9   3925 use RT::Client::REST::Object::Exception;
  9         35  
  9         76  
15 9     9   4233 use RT::Client::REST::SearchResult;
  9         26  
  9         237  
16 9     9   8564 use DateTime;
  9         4841566  
  9         425  
17 9     9   4832 use DateTime::Format::DateParse;
  9         73813  
  9         2136  
18              
19              
20             sub new {
21 20     20 1 27007 my $class = shift;
22              
23 20 50       93 if (@_ & 1) {
24 0         0 RT::Client::REST::Object::OddNumberOfArgumentsException->throw;
25             }
26              
27 20   33     119 my $self = bless {}, ref($class) || $class;
28 20         53 my %opts = @_;
29              
30 20         48 my $id = delete($opts{id});
31 20 100       59 if (defined($id)) {{
32 2         3 $self->id($id);
  2         7  
33 2 50       11 if ($self->can('parent_id')) {
34             # If object can parent_id, we assume that it's needed for
35             # retrieval.
36 0         0 my $parent_id = delete($opts{parent_id});
37 0 0       0 if (defined($parent_id)) {
38 0         0 $self->parent_id($parent_id);
39             } else {
40 0         0 last;
41             }
42             }
43 2 100       8 if ($self->autoget) {
44 1         4 $self->retrieve;
45             }
46             }}
47              
48 20         104 while (my ($k, $v) = each(%opts)) {
49 0         0 $self->$k($v);
50             }
51              
52 20         85 return $self;
53             }
54              
55              
56             sub _generate_methods {
57 12     12   187 my $class = shift;
58 12         42 my $attributes = $class->_attributes;
59              
60 12         100 while (my ($method, $settings) = each(%$attributes)) {
61 9     9   75 no strict 'refs'; ## no critic (ProhibitNoStrict)
  9         26  
  9         1299  
62              
63 169         835 *{$class . '::' . $method} = sub {
64 64     64   18318 my $self = shift;
65              
66 64 100       151 if (@_) {
67 22 100       83 my $caller = defined((caller(1))[3]) ? (caller(1))[3] : '';
68              
69 22 100 66     1301 if ($settings->{validation} &&
70             # Don't validate values from the server
71             $caller ne __PACKAGE__ . '::from_form')
72             {
73 19         49 my @v = @_;
74             Params::Validate::validation_options(
75             on_fail => sub {
76 9     9   70 no warnings 'uninitialized';
  9         34  
  9         22684  
77 1     1   8 RT::Client::REST::Object::InvalidValueException
78             ->throw(
79             "'@v' is not a valid value for attribute '$method'"
80             );
81             },
82 19         122 );
83 19         1147 validate_pos(@_, $settings->{validation});
84             }
85              
86 21         183 $self->{'_' . $method} = shift;
87 21         73 $self->_mark_dirty($method);
88              
89             # Let's try to autosync, shall we? Logic is a bit hairy
90             # in order to make it efficient.
91 21 50 66     55 if ($self->autosync && $self->can('store') &&
      100        
      66        
      66        
92             # OK, so id is special. This is so that 'new' would
93             # work.
94             'id' ne $method &&
95             'parent_id' ne $method &&
96              
97             # Plus we don't want to store right after retrieving
98             # (that's where from_form is called from).
99             $caller ne __PACKAGE__ . '::from_form')
100             {
101 2         20 $self->store;
102             }
103             }
104              
105 63 100       152 if ($settings->{list}) {
106 8   50     26 my $retval = $self->{'_' . $method} || [];
107 8         41 return @$retval;
108             } else {
109 55         288 return $self->{'_' . $method};
110             }
111 169         837 };
112              
113 169 100       414 if ($settings->{is_datetime}) {
114 20         110 *{$class. '::' . $method . '_datetime'} = sub {
115             # All dates are in UTC
116             # http://requesttracker.wikia.com/wiki/REST#Data_format
117              
118 3     3   20730 my ($self) = shift;
119 3 100       11 if (@_) {
120 2 100       18 unless ($_[0]->isa('DateTime')) {
121 1         18 RT::Client::REST::Object::InvalidValueException
122             ->throw(
123             "'$_[0]' is not a valid value for attribute '${method}_datetime'"
124             );
125              
126             }
127 1         5 my $z = $_[0]->clone;
128 1         14 $z->set_time_zone('UTC');
129 1         272 $self->$method($_[0]->strftime('%a %b %d %T %Y'));
130 1         6 return $z;
131             }
132              
133 1         5 return DateTime::Format::DateParse->parse_datetime($self->$method, 'UTC');
134              
135 20         82 };
136             }
137              
138 169 100       609 if ($settings->{list}) {
139             # Generate convenience methods for list manipulation.
140 9         61 my $add_method = $class . '::add_' . $method;
141 9         20 my $delete_method = $class . '::delete_' . $method;
142              
143             *$add_method = sub {
144 1     1   33 my $self = shift;
145              
146 1 50       4 unless (@_) {
147 0         0 RT::Client::REST::Object::NoValuesProvidedException
148             ->throw;
149             }
150              
151 1         5 my @values = $self->$method;
152 1         2 my %values = map { $_, 1 } @values;
  2         7  
153              
154             # Now add new values
155 1         4 for (@_) {
156 3         7 $values{$_} = 1;
157             }
158              
159 1         5 $self->$method([keys %values]);
160 9         70 };
161              
162             *$delete_method = sub {
163 1     1   34 my $self = shift;
164              
165 1 50       4 unless (@_) {
166 0         0 RT::Client::REST::Object::NoValuesProvidedException
167             ->throw;
168             }
169              
170 1         4 my @values = $self->$method;
171 1         3 my %values = map { $_, 1 } @values;
  5         11  
172              
173             # Now delete values
174 1         4 for (@_) {
175 1         3 delete $values{$_};
176             }
177              
178 1         7 $self->$method([keys %values]);
179 9         89 };
180             }
181             }
182             }
183              
184              
185             sub _mark_dirty {
186 21     21   45 my ($self, $attr) = @_;
187 21         56 $self->{__dirty}{$attr} = 1;
188             }
189              
190              
191             sub _dirty {
192 3     3   8 my $self = shift;
193              
194 3 100       12 if (exists($self->{__dirty})) {
195 2         5 return keys %{$self->{__dirty}};
  2         12  
196             }
197              
198 1         2 return;
199             }
200              
201              
202             sub _mark_dirty_cf {
203 0     0   0 my ($self, $cf) = @_;
204 0         0 $self->{__dirty_cf}{$cf} = 1;
205             }
206              
207              
208             sub _dirty_cf {
209 1     1   4 my $self = shift;
210              
211 1 50       3 if (exists($self->{__dirty_cf})) {
212 0         0 return keys %{$self->{__dirty_cf}};
  0         0  
213             }
214              
215 1         3 return;
216             }
217              
218              
219             sub to_form {
220 1     1 1 13 my ($self, $all) = @_;
221 1         5 my $attributes = $self->_attributes;
222              
223 1 50       11 my @attrs = ($all ? keys(%$attributes) : $self->_dirty);
224              
225 1         3 my %hash;
226              
227 1         3 for my $attr (@attrs) {
228             my $rest_name = (exists($attributes->{$attr}{rest_name}) ?
229 0 0       0 $attributes->{$attr}{rest_name} : ucfirst($attr));
230              
231 0         0 my $value;
232 0 0       0 if (exists($attributes->{$attr}{value2form})) {
    0          
233 0         0 $value = $attributes->{$attr}{value2form}($self->$attr)
234             } elsif ($attributes->{$attr}{list}) {
235 0         0 $value = join(',', $self->$attr)
236             } else {
237 0 0       0 $value = (defined($self->$attr) ? $self->$attr : '');
238             }
239              
240 0         0 $hash{$rest_name} = $value;
241             }
242 1 50       8 my @cfs = ($all ? $self->cf : $self->_dirty_cf);
243 1         3 for my $cf (@cfs) {
244 0         0 $hash{'CF-' . $cf} = $self->cf($cf);
245             }
246              
247 1         6 return \%hash;
248             }
249              
250              
251             sub from_form {
252 0     0 1 0 my $self = shift;
253              
254 0 0       0 unless (@_) {
255 0         0 RT::Client::REST::Object::NoValuesProvidedException->throw;
256             }
257              
258 0         0 my $hash = shift;
259              
260 0 0       0 unless ('HASH' eq ref($hash)) {
261 0         0 RT::Client::REST::Object::InvalidValueException->throw(
262             q|Expecting a hash reference as argument to 'from_form'|,
263             );
264             }
265              
266             # lowercase hash keys
267 0         0 my $i = 0;
268 0 0       0 $hash = { map { ($i++ & 1) ? $_ : lc } %$hash };
  0         0  
269              
270 0         0 my $attributes = $self->_attributes;
271 0         0 my %rest2attr; # Mapping of REST names to our attributes;
272 0         0 while (my ($attr, $settings) = each(%$attributes)) {
273             my $rest_name = (exists($attributes->{$attr}{rest_name}) ?
274 0 0       0 lc($attributes->{$attr}{rest_name}) : $attr);
275 0         0 $rest2attr{$rest_name} = [ $attr, $settings ];
276             }
277              
278             # Now set attributes:
279 0         0 while (my ($key, $value) = each(%$hash)) {
280             # Handle custom fields, ideally /(?(1)})/ would be appened to RE
281 0 0       0 if ( $key =~ m%^(?:cf|customfield)(?:-|\.\{)([#\s\w_:()?/-]+)% ){
282 0         0 $key = $1;
283              
284             # XXX very sketchy. Will fail on long form data e.g; wiki CF
285 0 0       0 if ($value =~ /,/) {
286 0         0 $value = [ split(/\s*,\s*/, $value) ];
287             }
288              
289 0         0 $self->cf($key, $value);
290 0         0 next;
291             }
292              
293 0 0       0 unless (exists($rest2attr{$key})) {
294 0         0 warn "Unknown key: $key\n";
295 0         0 next;
296             }
297              
298 0         0 my ( $method, $settings) = @{$rest2attr{$key}};
  0         0  
299              
300 0 0 0     0 if ($settings->{is_datetime} and $value eq 'Not set') {
301 0         0 $value = undef;
302             }
303              
304 0 0       0 if (exists($attributes->{$method}{form2value})) {
    0          
305 0         0 $value = $attributes->{$method}{form2value}($value);
306             } elsif ($attributes->{$method}{list}) {
307 0         0 $value = [split(/\s*,\s*/, $value)],
308             }
309 0         0 $self->$method($value);
310             }
311              
312 0         0 return;
313             }
314              
315             sub retrieve {
316 3     3 1 1823 my $self = shift;
317              
318 3         11 $self->_assert_rt_and_id;
319              
320 1         9 my $rt = $self->rt;
321              
322 1         6 my ($hash) = $rt->show(type => $self->rt_type, id => $self->id);
323 0         0 $self->from_form($hash);
324              
325 0         0 $self->{__dirty} = {};
326 0         0 $self->{__dirty_cf} = {};
327              
328 0         0 return $self;
329             }
330              
331             sub store {
332 2     2 1 740 my $self = shift;
333              
334 2         9 $self->_assert_rt;
335              
336 1         3 my $rt = $self->rt;
337              
338 1 50       4 if (defined($self->id)) {
339 1         10 $rt->edit(
340             type => $self->rt_type,
341             id => $self->id,
342             set => $self->to_form,
343             );
344             }
345             else {
346 0         0 my $id = $rt->create(
347             type => $self->rt_type,
348             set => $self->to_form,
349             @_,
350             );
351 0         0 $self->id($id);
352             }
353              
354 0         0 $self->{__dirty} = {};
355              
356 0         0 return $self;
357             }
358              
359             sub search {
360 3     3 1 730 my $self = shift;
361              
362 3 50       11 if (@_ & 1) {
363 0         0 RT::Client::REST::Object::OddNumberOfArgumentsException->throw;
364             }
365              
366 3         9 $self->_assert_rt;
367              
368 2         5 my %opts = @_;
369              
370 2   50     10 my $limits = delete($opts{limits}) || [];
371 2         3 my $query = '';
372              
373 2         7 for my $limit (@$limits) {
374 0         0 my $kw;
375             try {
376 0     0   0 $kw = $self->_attr2keyword($limit->{attribute});
377             }
378             catch {
379 0 0 0 0   0 die $_ unless blessed $_ && $_->can('rethrow');
380              
381 0 0       0 if ($_->isa('RT::Clite::REST::Object::InvalidAttributeException')) {
382 0         0 RT::Client::REST::Object::InvalidSearchParametersException
383             ->throw(shift->message);
384             }
385             else {
386 0         0 $_->rethrow
387             }
388 0         0 };
389 0         0 my $op = $limit->{operator};
390 0         0 my $val = $limit->{value};
391 0   0     0 my $agg = $limit->{aggregator} || 'and';
392              
393 0 0       0 if (length($query)) {
394 0         0 $query = "($query) $agg $kw $op '$val'";
395             } else {
396 0         0 $query = "$kw $op '$val'";
397             }
398             }
399              
400 2         5 my $orderby;
401             try {
402             # Defaults to 'id' at the moment. Do not rely on this --
403             # implementation may change!
404             $orderby = (delete($opts{reverseorder}) ? '-' : '+') .
405 2 50 50 2   231 ($self->_attr2keyword(delete($opts{orderby}) || 'id'));
406             }
407             catch {
408 0 0 0 0   0 die $_ unless blessed $_ && $_->can('rethrow');
409              
410 0 0       0 if ($_->isa('RT::Client::REST::Object::InvalidAttributeException')) {
411 0         0 RT::Client::REST::Object::InvalidSearchParametersException->throw(
412             shift->message,
413             )
414             }
415             else {
416 0         0 $_->rethrow;
417             }
418 2         15 };
419              
420 2         39 my $rt = $self->rt;
421 2         3 my @results;
422             try {
423 2     2   184 @results = $rt->search(
424             type => $self->rt_type,
425             query => $query,
426             orderby => $orderby,
427             );
428             }
429             catch {
430 2 50 33 2   2261 die $_ unless blessed $_ && $_->can('rethrow');
431              
432 2 50       17 if ($_->isa('RT::Client::REST::InvalidQueryException')) {
433 0         0 RT::Client::REST::Object::InvalidSearchParametersException->throw;
434             }
435             else {
436 2         8 $_->rethrow;
437             }
438 2         12 };
439              
440             return RT::Client::REST::SearchResult->new(
441             ids => \@results,
442 0     0   0 object => sub { $self->new(id => shift, rt => $rt) },
443 0         0 );
444             }
445              
446             sub count {
447 2     2 1 723 my $self = shift;
448 2         7 $self->_assert_rt;
449 1         10 return $self->search(@_)->count;
450             }
451              
452             sub _attr2keyword {
453 2     2   6 my ($self, $attr) = @_;
454 2         7 my $attributes = $self->_attributes;
455              
456 2 50       12 unless (exists($attributes->{$attr})) {
457 9     9   98 no warnings 'uninitialized';
  9         33  
  9         6040  
458 0         0 RT::Clite::REST::Object::InvalidAttributeException->throw(
459             "Attribute '$attr' does not exist in object type '" .
460             ref($self) . "'"
461             );
462             }
463              
464             return (exists($attributes->{$attr}{rest_name}) ?
465             $attributes->{$attr}{rest_name} :
466 2 50       14 ucfirst($attr));
467             }
468              
469             sub _assert_rt_and_id {
470 30     30   45 my $self = shift;
471 30   66     93 my $method = shift || (caller(1))[3];
472              
473 30 100       427 unless (defined($self->rt)) {
474 8         67 RT::Client::REST::Object::RequiredAttributeUnsetException
475             ->throw("Cannot '$method': 'rt' attribute of the object ".
476             "is not set");
477             }
478              
479 22 100       66 unless (defined($self->id)) {
480 8         49 RT::Client::REST::Object::RequiredAttributeUnsetException
481             ->throw("Cannot '$method': 'id' attribute of the object ".
482             "is not set");
483             }
484             }
485              
486             sub _assert_rt {
487 7     7   11 my $self = shift;
488 7   33     26 my $method = shift || (caller(1))[3];
489              
490 7 100       304 unless (defined($self->rt)) {
491 3         17 RT::Client::REST::Object::RequiredAttributeUnsetException
492             ->throw("Cannot '$method': 'rt' attribute of the object ".
493             "is not set");
494             }
495             }
496              
497              
498             sub param {
499 0     0 1 0 my $self = shift;
500              
501 0 0       0 unless (@_) {
502 0         0 RT::Client::REST::Object::NoValuesProvidedException->throw;
503             }
504              
505 0         0 my $name = shift;
506              
507 0 0       0 if (@_) {
508 0         0 $self->{__param}{$name} = shift;
509             }
510              
511 0         0 return $self->{__param}{$name};
512             }
513              
514              
515             sub cf {
516 0     0 1 0 my $self = shift;
517              
518 0 0       0 unless (@_) {
519             # Return a list of CFs.
520 0         0 return keys %{$self->{__cf}};
  0         0  
521             }
522              
523 0         0 my $name = shift;
524 0 0       0 if ('HASH' eq ref($name)) {
525 0         0 while (my ($k, $v) = each(%$name)) {
526 0         0 $self->{__cf}{lc($k)} = $v;
527 0         0 $self->_mark_dirty_cf($k);
528             }
529 0         0 return keys %{$self->{__cf}};
  0         0  
530             } else {
531 0         0 $name = lc $name;
532 0 0       0 if (@_) {
533 0         0 $self->{__cf}{$name} = shift;
534 0         0 $self->_mark_dirty_cf($name);
535             }
536 0         0 return $self->{__cf}{$name};
537             }
538             }
539              
540              
541             sub rt {
542 68     68 1 14433 my $self = shift;
543              
544 68 100       147 if (@_) {
545 18         27 my $rt = shift;
546 18 100       85 unless (UNIVERSAL::isa($rt, 'RT::Client::REST')) {
547 7         28 RT::Client::REST::Object::InvalidValueException->throw;
548             }
549 11         25 $self->{__rt} = $rt;
550             }
551              
552 61         196 return $self->{__rt};
553             }
554              
555              
556             sub use_single_rt {
557 2     2 1 6 my ($class, $rt) = @_;
558              
559 2 100       8 unless (UNIVERSAL::isa($rt, 'RT::Client::REST')) {
560 1         21 RT::Client::REST::Object::InvalidValueException->throw;
561             }
562              
563 9     9   81 no strict 'refs'; ## no critic (ProhibitNoStrict)
  9         34  
  9         323  
564 9     9   67 no warnings 'redefine';
  9         21  
  9         1038  
565 1   33 1   4 *{(ref($class) || $class) . '::rt'} = sub { $rt };
  1         12  
  1         28  
566             }
567              
568              
569       41 1   sub autostore {}
570              
571             sub use_autostore {
572 0     0 1 0 my ($class, $autostore) = @_;
573              
574 9     9   92 no strict 'refs'; ## no critic (ProhibitNoStrict)
  9         23  
  9         333  
575 9     9   66 no warnings 'redefine';
  9         24  
  9         1362  
576 0   0 0   0 *{(ref($class) || $class) . '::autostore'} = sub { $autostore };
  0         0  
  0         0  
577             }
578              
579             sub DESTROY {
580 38     38   17004 my $self = shift;
581              
582 38 50 33     141 $self->autostore && $self->can('store') && $self->store;
583             }
584              
585              
586       21 1   sub autoget {}
587              
588             sub use_autoget {
589 2     2 1 5 my ($class, $autoget) = @_;
590              
591 9     9   76 no strict 'refs'; ## no critic (ProhibitNoStrict)
  9         24  
  9         303  
592 9     9   56 no warnings 'redefine';
  9         19  
  9         998  
593 2   33 2   7 *{(ref($class) || $class) . '::autoget'} = sub { $autoget };
  2         14  
  2         7  
594             }
595              
596              
597       19 1   sub autosync {}
598              
599             sub use_autosync {
600 2     2 1 6 my ($class, $autosync) = @_;
601              
602 9     9   100 no strict 'refs'; ## no critic (ProhibitNoStrict)
  9         36  
  9         353  
603 9     9   62 no warnings 'redefine';
  9         27  
  9         1275  
604 2   33 5   7 *{(ref($class) || $class) . '::autosync'} = sub { $autosync };
  2         18  
  5         46  
605             }
606              
607              
608             sub be_transparent {
609 2     2 1 159 my ($class, $rt) = @_;
610 2         10 $class->use_autosync(1);
611 2         9 $class->use_autoget(1);
612 2         24 $class->use_single_rt($rt);
613             }
614              
615              
616             1;
617              
618             __END__
619              
620             =pod
621              
622             =encoding UTF-8
623              
624             =head1 NAME
625              
626             RT::Client::REST::Object - base class for RT objects
627              
628             =head1 VERSION
629              
630             version 0.71
631              
632             =head1 SYNOPSIS
633              
634             # Create a new type
635             package RT::Client::REST::MyType;
636              
637             use parent qw(RT::Client::REST::Object);
638              
639             sub _attributes {{
640             myattribute => {
641             validation => {
642             type => SCALAR,
643             },
644             },
645             }}
646              
647             sub rt_type { "mytype" }
648              
649             1;
650              
651             =head1 DESCRIPTION
652              
653             The RT::Client::REST::Object module is a superclass providing a whole
654             bunch of class and object methods in order to streamline the development
655             of RT's REST client interface.
656              
657             =head1 ATTRIBUTES
658              
659             Attributes are defined by method C<_attributes> that should be defined
660             in your class. This method returns a reference to a hash whose keys are
661             the attributes. The values of the hash are attribute settings, which are
662             as follows:
663              
664             =over 2
665              
666             =item list
667              
668             If set to true, this is a list attribute. See
669             L</LIST ATTRIBUTE PROPERTIES> below.
670              
671             =item validation
672              
673             A hash reference. This is passed to validation routines when associated
674             mutator is called. See L<Params::Validate> for reference.
675              
676             =item rest_name
677              
678             =for stopwords FinalPriority
679              
680             This specifies this attribute's REST name. For example, attribute
681             "final_priority" corresponds to RT REST's "FinalPriority". This option
682             may be omitted if the two only differ in first letter capitalization.
683              
684             =item form2value
685              
686             Convert form value (one that comes from the server) into attribute-digestible
687             format.
688              
689             =item value2form
690              
691             Convert value into REST form format.
692              
693             =back
694              
695             Example:
696              
697             sub _attributes {{
698             id => {
699             validation => {
700             type => SCALAR,
701             regex => qr/^\d+$/,
702             },
703             form2value => sub {
704             shift =~ m~^ticket/(\d+)$~i;
705             return $1;
706             },
707             value2form => sub {
708             return 'ticket/' . shift;
709             },
710             },
711             admin_cc => {
712             validation => {
713             type => ARRAYREF,
714             },
715             list => 1,
716             rest_name => 'AdminCc',
717             },
718             }}
719              
720             =head1 LIST ATTRIBUTE PROPERTIES
721              
722             List attributes have the following properties:
723              
724             =over 2
725              
726             =item *
727              
728             When called as accessors, return a list of items
729              
730             =item *
731              
732             When called as mutators, only accept an array reference
733              
734             =item *
735              
736             Convenience methods "add_attr" and "delete_attr" are available. For
737             example:
738              
739             # Get the list
740             my @requestors = $ticket->requestors;
741              
742             # Replace with a new list
743             $ticket->requestors( [qw(dude@localhost)] );
744              
745             # Add some random guys to the current list
746             $ticket->add_requestors('randomguy@localhost', 'evil@local');
747              
748             =back
749              
750             =for stopwords autoget autostore autosync
751              
752             =head1 SPECIAL ATTRIBUTES
753              
754             B<id> and B<parent_id> are special attributes. They are used by
755             various DB-related methods and are especially relied upon by:
756              
757             =over 2
758              
759             =item autostore
760              
761             =item autosync
762              
763             =item autoget
764              
765             =back
766              
767             =head1 METHODS
768              
769             =over 2
770              
771             =item new
772              
773             Constructor
774              
775             =item _generate_methods
776              
777             This class method generates accessors and mutators based on
778             B<_attributes> method which your class should provide. For items
779             that are lists, 'add_' and 'delete_' methods are created. For instance,
780             the following two attributes specified in B<_attributes> will generate
781             methods 'creator', 'cc', 'add_cc', and 'delete_cc':
782              
783             creator => {
784             validation => { type => SCALAR },
785             },
786             cc => {
787             list => 1,
788             validation => { type => ARRAYREF },
789             },
790              
791             =item _mark_dirty($attrname)
792              
793             Mark an attribute as dirty.
794              
795             =item _dirty
796              
797             Return the list of dirty attributes.
798              
799             =item _mark_dirty_cf($attrname)
800              
801             Mark an custom flag as dirty.
802              
803             =item _dirty_cf
804              
805             Return the list of dirty custom flags.
806              
807             =item to_form($all)
808              
809             Convert the object to 'form' (used by REST protocol). This is done based on
810             B<_attributes> method. If C<$all> is true, create a form from all of the
811             object's attributes and custom flags, otherwise use only dirty (see B<_dirty>
812             method) attributes and custom flags. Defaults to the latter.
813              
814             =item from_form
815              
816             Set object's attributes from form received from RT server.
817              
818             =item param($name, $value)
819              
820             Set an arbitrary parameter.
821              
822             =item cf([$name, [$value]])
823              
824             Given no arguments, returns the list of custom field names. With
825             one argument, returns the value of custom field C<$name>. With two
826             arguments, sets custom field C<$name> to C<$value>. Given a reference
827             to a hash, uses it as a list of custom fields and their values, returning
828             the new list of all custom field names.
829              
830             =item rt
831              
832             Get or set the 'rt' object, which should be of type L<RT::Client::REST>.
833              
834             =back
835              
836             =head1 DB METHODS
837              
838             The following are methods that have to do with reading, creating, updating,
839             and searching objects.
840              
841             =over 2
842              
843             =item count
844              
845             Takes the same arguments as C<search()> but returns the actual count of
846             the found items. Throws the same exceptions.
847              
848             =item retrieve
849              
850             Retrieve object's attributes. Note that 'id' attribute must be set for this
851             to work.
852              
853             =item search (%opts)
854              
855             This method is used for searching objects. It returns an object of type
856             L<RT::Client::REST::SearchResult>, which can then be used to process
857             results. C<%opts> is a list of key-value pairs, which are as follows:
858              
859             =over 2
860              
861             =item limits
862              
863             This is a reference to array containing hash references with limits to
864             apply to the search (think SQL limits).
865              
866             =for stopwords orderby reverseorder
867              
868             =item orderby
869              
870             Specifies attribute to sort the result by (in ascending order).
871              
872             =item reverseorder
873              
874             If set to a true value, sorts by attribute specified by B<orderby> in
875             descending order.
876              
877             =back
878              
879             If the client cannot construct the query from the specified arguments,
880             or if the server cannot make it out,
881             C<RT::Client::REST::Object::InvalidSearchParametersException> is thrown.
882              
883             =item store
884              
885             Store the object. If 'id' is set, this is an update; otherwise, a new
886             object is created and the 'id' attribute is set. Note that only changed
887             (dirty) attributes are sent to the server.
888              
889             =back
890              
891             =head1 CLASS METHODS
892              
893             =over 2
894              
895             =item use_single_rt
896              
897             =for stopwords instantiations
898              
899             This method takes a single argument -- L<RT::Client::REST> object
900             and makes this class use it for all instantiations. For example:
901              
902             my $rt = RT::Client::REST->new(%args);
903              
904             # Make all tickets use this RT:
905             RT::Client::REST::Ticket->use_single_rt($rt);
906              
907             # Now make all objects use it:
908             RT::Client::REST::Object->use_single_rt($rt);
909              
910             =for stopwords autostoring autostore
911              
912             =item use_autostore
913              
914             Turn I<autostoring> on and off. I<Autostoring> means that you do not have
915             to explicitly call C<store()> on an object - it will be called when
916             the object goes out of scope.
917              
918             # Autostore tickets:
919             RT::Client::REST::Ticket->use_autostore(1);
920             my $ticket = RT::Client::REST::Ticket->new(%opts)->retrieve;
921             $ticket->priority(10);
922             # Don't have to call store().
923              
924             =item use_autoget
925              
926             Turn I<autoget> feature on or off (off by default). When set to on,
927             C<retrieve()> will be automatically called from the constructor if
928             it is called with that object's special attributes (see
929             L</SPECIAL ATTRIBUTES> above).
930              
931             RT::Client::Ticket->use_autoget(1);
932             my $ticket = RT::Client::Ticket->new(id => 1);
933             # Now all attributes are available:
934             my $subject = $ticket->subject;
935              
936             =for stopwords autosync
937              
938             =item use_autosync
939              
940             Turn I<autosync> feature on or off (off by default). When set, every time
941             an attribute is changed, C<store()> method is invoked. This may be pretty
942             expensive.
943              
944             =item be_transparent
945              
946             This turns on B<autosync> and B<autoget>. Transparency is a neat idea,
947             but it may be expensive and slow. Depending on your circumstances, you
948             may want a finer control of your objects. Transparency makes
949             C<retrieve()> and C<store()> calls invisible:
950              
951             RT::Client::REST::Ticket->be_transparent($rt);
952              
953             my $ticket = RT::Client::REST::Ticket->new(id => $id); # retrieved
954             $ticket->add_cc('you@localhost.localdomain'); # stored
955             $ticket->status('stalled'); # stored
956              
957             # etc.
958              
959             Do not forget to pass RT::Client::REST object to this method.
960              
961             =back
962              
963             =head1 SEE ALSO
964              
965             L<RT::Client::REST::Ticket>,
966             L<RT::Client::REST::SearchResult>.
967              
968             =head1 AUTHOR
969              
970             Dean Hamstead <dean@fragfest.com.au>
971              
972             =head1 COPYRIGHT AND LICENSE
973              
974             This software is copyright (c) 2022, 2020 by Dmitri Tikhonov.
975              
976             This is free software; you can redistribute it and/or modify it under
977             the same terms as the Perl 5 programming language system itself.
978              
979             =cut