File Coverage

blib/lib/RT/Client/REST/Object.pm
Criterion Covered Total %
statement 212 322 65.8
branch 50 118 42.3
condition 23 62 37.1
subroutine 52 62 83.8
pod 18 18 100.0
total 355 582 61.0


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