File Coverage

lib/JIRA/REST/Class.pm
Criterion Covered Total %
statement 107 123 86.9
branch 19 28 67.8
condition 4 13 30.7
subroutine 31 34 91.1
pod 24 24 100.0
total 185 222 83.3


line stmt bran cond sub pod time code
1             package JIRA::REST::Class;
2 4     4   6677 use strict;
  4         10  
  4         152  
3 4     4   32 use warnings;
  4         13  
  4         142  
4 4     4   100 use 5.010;
  4         34  
5              
6             our $VERSION = '0.11';
7             our $SOURCE = 'CPAN';
8             ## $SOURCE = 'GitHub'; # COMMENT
9             # the line above will be commented out by Dist::Zilla
10              
11             # ABSTRACT: An OO Class module built atop L<JIRA::REST|JIRA::REST> for dealing with JIRA issues and their data as objects.
12              
13 4     4   28 use Carp;
  4         60  
  4         368  
14 4     4   23 use Clone::Any qw( clone );
  4         15  
  4         48  
15 4     4   3735 use Readonly 2.04;
  4         11227  
  4         250  
16              
17 4     4   2145 use JIRA::REST;
  4         58004  
  4         150  
18 4     4   14716 use JIRA::REST::Class::Factory;
  4         15  
  4         223  
19 4     4   28 use parent qw(JIRA::REST::Class::Mixins);
  4         9  
  4         21  
20              
21             #---------------------------------------------------------------------------
22             # Constants
23              
24             Readonly my $DEFAULT_MAXRESULTS => 50;
25              
26             #---------------------------------------------------------------------------
27              
28             sub new {
29 44     44 1 38039 my ( $class, @arglist ) = @_;
30              
31 44         220 my $args = $class->_get_known_args(
32             \@arglist,
33             qw/ url username password rest_client_config
34             proxy ssl_verify_none anonymous /
35             );
36              
37 44         781 return bless {
38             jira_rest => $class->JIRA_REST( clone( $args ) ),
39             factory => $class->factory( clone( $args ) ),
40             args => clone( $args ),
41             userobj => undef,
42             }, $class;
43             }
44              
45             #---------------------------------------------------------------------------
46             #
47             # using Inline::Test to generate testing files from tests
48             # declared next to the code that it's testing
49             #
50              
51             #pod =begin test setup 1
52             #pod
53             #pod use File::Basename;
54             #pod use lib dirname($0).'/..';
55             #pod use MyTest;
56             #pod use 5.010;
57             #pod
58             #pod TestServer_setup();
59             #pod
60             #pod END {
61             #pod TestServer_stop();
62             #pod }
63             #pod
64             #pod use_ok('JIRA::REST::Class');
65             #pod
66             #pod sub get_test_client {
67             #pod state $test =
68             #pod JIRA::REST::Class->new(TestServer_url(), 'username', 'password');
69             #pod $test->REST_CLIENT->setTimeout(5);
70             #pod return $test;
71             #pod };
72             #pod
73             #pod =end test
74             #pod
75             #pod =cut
76              
77             #---------------------------------------------------------------------------
78              
79             #pod =begin testing new 5
80             #pod
81             #pod my $jira;
82             #pod try {
83             #pod $jira = JIRA::REST::Class->new({
84             #pod url => TestServer_url(),
85             #pod username => 'user',
86             #pod password => 'pass',
87             #pod proxy => '',
88             #pod anonymous => 0,
89             #pod ssl_verify_none => 1,
90             #pod rest_client_config => {},
91             #pod });
92             #pod }
93             #pod catch {
94             #pod $jira = $_; # as good a place as any to stash the error, because
95             #pod # isa_ok() will complain that it's not an object.
96             #pod };
97             #pod
98             #pod isa_ok($jira, 'JIRA::REST::Class', 'JIRA::REST::Class->new');
99             #pod
100             #pod my $needs_url_regexp = qr/'?url'? argument must be defined/i;
101             #pod
102             #pod throws_ok(
103             #pod sub {
104             #pod JIRA::REST::Class->new();
105             #pod },
106             #pod $needs_url_regexp,
107             #pod 'JIRA::REST::Class->new with no parameters throws an exception',
108             #pod );
109             #pod
110             #pod throws_ok(
111             #pod sub {
112             #pod JIRA::REST::Class->new({
113             #pod username => 'user',
114             #pod password => 'pass',
115             #pod });
116             #pod },
117             #pod $needs_url_regexp,
118             #pod 'JIRA::REST::Class->new with no url throws an exception',
119             #pod );
120             #pod
121             #pod throws_ok(
122             #pod sub {
123             #pod JIRA::REST::Class->new('http://not.a.good.server.com');
124             #pod },
125             #pod qr/No credentials found/,
126             #pod q{JIRA::REST::Class->new with just url tries to find credentials},
127             #pod );
128             #pod
129             #pod lives_ok(
130             #pod sub {
131             #pod JIRA::REST::Class->new(TestServer_url(), 'user', 'pass');
132             #pod },
133             #pod q{JIRA::REST::Class->new with url, username, and password does't croak!},
134             #pod );
135             #pod
136             #pod =end testing
137             #pod
138             #pod =cut
139              
140             #---------------------------------------------------------------------------
141              
142             #pod =method B<issues> QUERY
143             #pod
144             #pod =method B<issues> KEY [, KEY...]
145             #pod
146             #pod The C<issues> method can be called two ways: either by providing a list of
147             #pod issue keys, or by proving a single hash reference which describes a JIRA
148             #pod query in the same format used by L<JIRA::REST|JIRA::REST> (essentially,
149             #pod C<< jql => "JQL query string" >>).
150             #pod
151             #pod The return value is an array of L<JIRA::REST::Class::Issue|JIRA::REST::Class::Issue> objects.
152             #pod
153             #pod =cut
154              
155             sub issues {
156 0     0 1 0 my ( $self, @args ) = @_;
157 0 0 0     0 if ( @args == 1 && ref $args[0] eq 'HASH' ) {
158 0         0 return $self->query( $args[0] )->issues;
159             }
160             else {
161 0         0 my $jql = sprintf 'key in (%s)', join q{,} => @args;
162 0         0 return $self->query( { jql => $jql } )->issues;
163             }
164             }
165              
166             #---------------------------------------------------------------------------
167             #
168             # =begin testing issues
169             # =end testing
170             #
171             #---------------------------------------------------------------------------
172              
173             #pod =method B<query> QUERY
174             #pod
175             #pod The C<query> method takes a single parameter: a hash reference which
176             #pod describes a JIRA query in the same format used by L<JIRA::REST|JIRA::REST>
177             #pod (essentially, C<< jql => "JQL query string" >>).
178             #pod
179             #pod The return value is a single L<JIRA::REST::Class::Query|JIRA::REST::Class::Query> object.
180             #pod
181             #pod =cut
182              
183             sub query {
184 0     0 1 0 my $self = shift;
185 0         0 my $args = shift;
186              
187 0         0 my $query = $self->post( '/search', $args );
188 0         0 return $self->make_object( 'query', { data => $query } );
189             }
190              
191             #---------------------------------------------------------------------------
192             #
193             # =begin testing query
194             # =end testing
195             #
196             #---------------------------------------------------------------------------
197              
198             #pod =method B<iterator> QUERY
199             #pod
200             #pod The C<query> method takes a single parameter: a hash reference which
201             #pod describes a JIRA query in the same format used by L<JIRA::REST|JIRA::REST>
202             #pod (essentially, C<< jql => "JQL query string" >>). It accepts an additional
203             #pod field, however: C<restart_if_lt_total>. If this field is set to a true value,
204             #pod the iterator will keep track of the number of results fetched and, if when
205             #pod the results run out this number doesn't match the number of results
206             #pod predicted by the query, it will restart the query. This is particularly
207             #pod useful if you are transforming a number of issues through an iterator, and
208             #pod the transformation causes the issues to no longer match the query.
209             #pod
210             #pod The return value is a single
211             #pod L<JIRA::REST::Class::Iterator|JIRA::REST::Class::Iterator> object. The
212             #pod issues returned by the query can be obtained in serial by repeatedly calling
213             #pod L<next|JIRA::REST::Class::Iterator/next> on this object, which returns a
214             #pod series of L<JIRA::REST::Class::Issue|JIRA::REST::Class::Issue> objects.
215             #pod
216             #pod =cut
217              
218             sub iterator {
219 0     0 1 0 my $self = shift;
220 0         0 my $args = shift;
221 0         0 return $self->make_object( 'iterator', { iterator_args => $args } );
222             }
223              
224             #---------------------------------------------------------------------------
225             #
226             # =begin testing iterator
227             # =end testing
228             #
229             #---------------------------------------------------------------------------
230              
231             #pod =internal_method B<get>
232             #pod
233             #pod A wrapper for C<JIRA::REST>'s L<GET|JIRA::REST/"GET RESOURCE [, QUERY]"> method.
234             #pod
235             #pod =cut
236              
237             sub get {
238 6     6 1 22 my ( $self, $url, @args ) = @_;
239 6         27 return $self->JIRA_REST->GET( $url, undef, @args );
240             }
241              
242             #---------------------------------------------------------------------------
243              
244             #pod =begin testing get
245             #pod
246             #pod validate_wrapper_method( sub { get_test_client()->get('/test'); },
247             #pod { GET => 'SUCCESS' }, 'get() method works' );
248             #pod
249             #pod =end testing
250             #pod
251             #pod =cut
252              
253             #---------------------------------------------------------------------------
254              
255             #pod =internal_method B<post>
256             #pod
257             #pod Wrapper around C<JIRA::REST>'s L<POST|JIRA::REST/"POST RESOURCE, QUERY, VALUE [, HEADERS]"> method.
258             #pod
259             #pod =cut
260              
261             sub post {
262 1     1 1 6 my ( $self, $url, @args ) = @_;
263 1         5 return $self->JIRA_REST->POST( $url, undef, @args );
264             }
265              
266             #---------------------------------------------------------------------------
267              
268             #pod =begin testing post
269             #pod
270             #pod validate_wrapper_method( sub { get_test_client()->post('/test', "key=value"); },
271             #pod { POST => 'SUCCESS' }, 'post() method works' );
272             #pod
273             #pod =end testing
274             #pod
275             #pod =cut
276              
277             #---------------------------------------------------------------------------
278              
279             #pod =internal_method B<put>
280             #pod
281             #pod Wrapper around C<JIRA::REST>'s L<PUT|JIRA::REST/"PUT RESOURCE, QUERY, VALUE [, HEADERS]"> method.
282             #pod
283             #pod =cut
284              
285             sub put {
286 1     1 1 5 my ( $self, $url, @args ) = @_;
287 1         4 return $self->JIRA_REST->PUT( $url, undef, @args );
288             }
289              
290             #---------------------------------------------------------------------------
291              
292             #pod =begin testing put
293             #pod
294             #pod validate_wrapper_method( sub { get_test_client()->put('/test', "key=value"); },
295             #pod { PUT => 'SUCCESS' }, 'put() method works' );
296             #pod
297             #pod =end testing
298             #pod
299             #pod =cut
300              
301             #---------------------------------------------------------------------------
302              
303             #pod =internal_method B<delete>
304             #pod
305             #pod Wrapper around C<JIRA::REST>'s L<DELETE|JIRA::REST/"DELETE RESOURCE [, QUERY]"> method.
306             #pod
307             #pod =cut
308              
309             sub delete { ## no critic (ProhibitBuiltinHomonyms)
310 1     1 1 4 my ( $self, $url, @args ) = @_;
311 1         5 return $self->JIRA_REST->DELETE( $url, @args );
312             }
313              
314             #---------------------------------------------------------------------------
315              
316             #pod =begin testing delete
317             #pod
318             #pod validate_wrapper_method( sub { get_test_client()->delete('/test'); },
319             #pod { DELETE => 'SUCCESS' }, 'delete() method works' );
320             #pod
321             #pod =end testing
322             #pod
323             #pod =cut
324              
325             #---------------------------------------------------------------------------
326              
327             #pod =internal_method B<data_upload>
328             #pod
329             #pod Similar to
330             #pod L<< JIRA::REST->attach_files|JIRA::REST/"attach_files ISSUE FILE..." >>,
331             #pod but entirely from memory and only attaches one file at a time. Returns the
332             #pod L<HTTP::Response|HTTP::Response> object from the post request. Takes the
333             #pod following named parameters:
334             #pod
335             #pod =over 4
336             #pod
337             #pod =item + B<url>
338             #pod
339             #pod The relative URL to POST to. This will have the hostname and REST version
340             #pod information prepended to it, so all you need to provide is something like
341             #pod C</issue/>I<issueIdOrKey>C</attachments>. I'm allowing the URL to be
342             #pod specified in case I later discover something this can be used for besides
343             #pod attaching files to issues.
344             #pod
345             #pod =item + B<name>
346             #pod
347             #pod The name that is specified for this file attachment.
348             #pod
349             #pod =item + B<data>
350             #pod
351             #pod The actual data to be uploaded. If a reference is provided, it will be
352             #pod dereferenced before posting the data.
353             #pod
354             #pod =back
355             #pod
356             #pod I guess that makes it only a I<little> like
357             #pod C<< JIRA::REST->attach_files >>...
358             #pod
359             #pod =cut
360              
361             sub data_upload {
362 1     1 1 46 my ( $self, @args ) = @_;
363 1         8 my $args = $self->_get_known_args( \@args, qw/ url name data / );
364 1         14 $self->_check_required_args(
365             $args,
366             url => 'you must specify a URL to upload to',
367             name => 'you must specify a name for the file data',
368             data => 'you must specify the file data',
369             );
370              
371 1         3 my $name = $args->{name};
372 1 50       5 my $data = ref $args->{data} ? ${ $args->{data} } : $args->{data};
  0         0  
373              
374             # code cribbed from JIRA::REST
375             #
376 1         5 my $url = $self->rest_api_url_base . $args->{url};
377 1         16 my $rest = $self->REST_CLIENT;
378             my $response = $rest->getUseragent()->post(
379             $url,
380 1         24 %{ $rest->{_headers} },
  1         21  
381             'X-Atlassian-Token' => 'nocheck',
382             'Content-Type' => 'form-data',
383             'Content' => [
384             file => [ undef, $name, Content => $data ],
385             ],
386             );
387              
388             #<<< perltidy should ignore these lines
389 1 50       23388 $response->is_success
390             or croak $self->JIRA_REST->_error( ## no critic (ProtectPrivateSubs)
391             $self->_croakmsg( $response->status_line, $name )
392             );
393             #>>>
394              
395 1         20 return $response;
396             }
397              
398             #---------------------------------------------------------------------------
399              
400             #pod =begin testing data_upload
401             #pod
402             #pod my $expected = {
403             #pod "Content-Disposition" => "form-data; name=\"file\"; filename=\"file.txt\"",
404             #pod POST => "SUCCESS",
405             #pod data => "An OO Class module built atop L<JIRA::REST|JIRA::REST> for dealing with "
406             #pod . "JIRA issues and their data as objects.",
407             #pod name => "file.txt"
408             #pod };
409             #pod
410             #pod my $test1_name = 'return value from data_upload()';
411             #pod my $test2_name = 'data_upload() method succeeded';
412             #pod my $test3_name = 'data_upload() method returned expected data';
413             #pod
414             #pod my $test = get_test_client();
415             #pod my $results;
416             #pod my $got;
417             #pod
418             #pod try {
419             #pod $results = $test->data_upload({
420             #pod url => "/data_upload",
421             #pod name => $expected->{name},
422             #pod data => $expected->{data},
423             #pod });
424             #pod $got = $test->JSON->decode($results->decoded_content);
425             #pod }
426             #pod catch {
427             #pod $results = $_;
428             #pod diag($test->REST_CLIENT->getHost);
429             #pod };
430             #pod
431             #pod my $test1_ok = isa_ok( $results, 'HTTP::Response', $test1_name );
432             #pod my $test2_ok = ok($test1_ok && $results->is_success, $test2_name );
433             #pod $test2_ok ? is_deeply( $got, $expected, $test3_name ) : fail( $test3_name );
434             #pod
435             #pod =end testing
436             #pod
437             #pod =cut
438              
439             #---------------------------------------------------------------------------
440              
441             #pod =method B<maxResults>
442             #pod
443             #pod A getter/setter method that allows setting a global default for the L<maxResults pagination parameter for JIRA's REST API |https://docs.atlassian.com/jira/REST/latest/#pagination>. This determines the I<maximum> number of results returned by the L<issues|/"issues QUERY"> and L<query|/"query QUERY"> methods; and the initial number of results fetched by the L<iterator|/"iterator QUERY"> (when L<next|JIRA::REST::Class::Iterator/next> exhausts that initial cache of results it will automatically make subsequent calls to the REST API to fetch more results).
444             #pod
445             #pod Defaults to 50.
446             #pod
447             #pod say $jira->maxResults; # returns 50
448             #pod
449             #pod $jira->maxResults(10); # only return 10 results at a time
450             #pod
451             #pod =cut
452              
453             sub maxResults {
454 3     3 1 7 my $self = shift;
455 3 100       11 if ( @_ ) {
456 1         3 $self->{maxResults} = shift;
457             }
458 3 100 66     17 unless ( exists $self->{maxResults} && defined $self->{maxResults} ) {
459 1         9 $self->{maxResults} = $DEFAULT_MAXRESULTS;
460             }
461 3         17 return $self->{maxResults};
462             }
463              
464             #pod =method B<issue_types>
465             #pod
466             #pod Returns a list of defined issue types (as
467             #pod L<JIRA::REST::Class::Issue::Type|JIRA::REST::Class::Issue::Type> objects)
468             #pod for this server.
469             #pod
470             #pod =cut
471              
472             sub issue_types {
473 3     3 1 1973 my $self = shift;
474              
475 3 100       21 unless ( $self->{issue_types} ) {
476 1         5 my $types = $self->get( '/issuetype' );
477             $self->{issue_types} = [ # stop perltidy from pulling
478             map { # these lines together
479 1         9057 $self->make_object( 'issuetype', { data => $_ } );
  7         55  
480             } @$types
481             ];
482             }
483              
484 3 100       18 return @{ $self->{issue_types} } if wantarray;
  2         18  
485 1         9 return $self->{issue_types};
486             }
487              
488             #---------------------------------------------------------------------------
489              
490             #pod =begin testing issue_types 16
491             #pod
492             #pod try {
493             #pod my $test = get_test_client();
494             #pod
495             #pod validate_contextual_accessor($test, {
496             #pod method => 'issue_types',
497             #pod class => 'issuetype',
498             #pod data => [ sort qw/ Bug Epic Improvement Sub-task Story Task /,
499             #pod 'New Feature' ],
500             #pod });
501             #pod
502             #pod print "#\n# Checking the 'Bug' issue type\n#\n";
503             #pod
504             #pod my ($bug) = sort $test->issue_types;
505             #pod
506             #pod can_ok_abstract( $bug, qw/ description iconUrl id name self subtask / );
507             #pod
508             #pod my $host = TestServer_url();
509             #pod
510             #pod validate_expected_fields( $bug, {
511             #pod description => "jira.translation.issuetype.bug.name.desc",
512             #pod iconUrl => "$host/secure/viewavatar?size=xsmall&avatarId=10303"
513             #pod . "&avatarType=issuetype",
514             #pod id => 10004,
515             #pod name => "Bug",
516             #pod self => "$host/rest/api/latest/issuetype/10004",
517             #pod subtask => JSON::PP::false,
518             #pod });
519             #pod };
520             #pod
521             #pod =end testing
522             #pod
523             #pod =cut
524              
525             #---------------------------------------------------------------------------
526              
527             #pod =method B<projects>
528             #pod
529             #pod Returns a list of projects (as
530             #pod L<JIRA::REST::Class::Project|JIRA::REST::Class::Project> objects) for this
531             #pod server.
532             #pod
533             #pod =cut
534              
535             sub projects {
536 3     3 1 1296 my $self = shift;
537              
538 3 100       14 unless ( $self->{project_list} ) {
539              
540             # get the project list from JIRA
541 1         7 my $projects = $self->get( '/project' );
542              
543             # build a list, and make a hash so we can
544             # grab projects later by id, key, or name.
545              
546 1         8264 my $list = $self->{project_list} = [];
547              
548             my $make_project_hash_entry = sub {
549 5     5   8 my $prj = shift;
550 5         22 my $obj = $self->make_object( 'project', { data => $prj } );
551              
552 5         26 push @$list, $obj;
553              
554 5         19 return $obj->id => $obj, $obj->key => $obj, $obj->name => $obj;
555 1         6 };
556              
557             $self->{project_hash} = { ##
558 1         4 map { $make_project_hash_entry->( $_ ) } @$projects
  5         12  
559             };
560             }
561              
562 3 100       10 return @{ $self->{project_list} } if wantarray;
  1         6  
563 2         7 return $self->{project_list};
564             }
565              
566             #---------------------------------------------------------------------------
567              
568             #pod =begin testing projects 5
569             #pod
570             #pod my $test = get_test_client();
571             #pod
572             #pod try {
573             #pod validate_contextual_accessor($test, {
574             #pod method => 'projects',
575             #pod class => 'project',
576             #pod data => [ qw/ JRC KANBAN PACKAY PM SCRUM / ],
577             #pod });
578             #pod };
579             #pod
580             #pod =end testing
581             #pod
582             #pod =cut
583              
584             #---------------------------------------------------------------------------
585              
586             #pod =method B<project> PROJECT_ID || PROJECT_KEY || PROJECT_NAME
587             #pod
588             #pod Returns a L<JIRA::REST::Class::Project|JIRA::REST::Class::Project> object
589             #pod for the project specified. Returns undef if the project doesn't exist.
590             #pod
591             #pod =cut
592              
593             sub project {
594 1     1 1 3 my $self = shift;
595 1   50     4 my $proj = shift || return; # if nothing was passed, we return nothing
596              
597             # if we were passed a project object, just return it
598 1 50       8 return $proj if $self->obj_isa( $proj, 'project' );
599              
600 1         4 $self->projects; # load the project hash if it hasn't been loaded
601              
602 1 50       5 return unless exists $self->{project_hash}->{$proj};
603 1         3 return $self->{project_hash}->{$proj};
604             }
605              
606             #---------------------------------------------------------------------------
607              
608             #pod =begin testing project 17
609             #pod
610             #pod try {
611             #pod print "#\n# Checking the SCRUM project\n#\n";
612             #pod
613             #pod my $test = get_test_client();
614             #pod
615             #pod my $proj = $test->project('SCRUM');
616             #pod
617             #pod can_ok_abstract( $proj, qw/ avatarUrls expand id key name self
618             #pod category assigneeType components
619             #pod description issueTypes lead roles versions
620             #pod allowed_components allowed_versions
621             #pod allowed_fix_versions allowed_issue_types
622             #pod allowed_priorities allowed_field_values
623             #pod field_metadata_exists field_metadata
624             #pod field_name
625             #pod / );
626             #pod
627             #pod validate_expected_fields( $proj, {
628             #pod expand => "description,lead,url,projectKeys",
629             #pod id => 10002,
630             #pod key => 'SCRUM',
631             #pod name => "Scrum Software Development Sample Project",
632             #pod projectTypeKey => "software",
633             #pod lead => {
634             #pod class => 'user',
635             #pod expected => {
636             #pod key => 'packy'
637             #pod },
638             #pod },
639             #pod });
640             #pod
641             #pod validate_contextual_accessor($proj, {
642             #pod method => 'versions',
643             #pod class => 'projectvers',
644             #pod name => "SCRUM project's",
645             #pod data => [ "Version 1.0", "Version 2.0", "Version 3.0" ],
646             #pod });
647             #pod };
648             #pod
649             #pod =end testing
650             #pod
651             #pod =cut
652              
653             #---------------------------------------------------------------------------
654              
655             #pod =method B<SSL_verify_none>
656             #pod
657             #pod Sets to false the SSL options C<SSL_verify_mode> and C<verify_hostname> on
658             #pod the L<LWP::UserAgent|LWP::UserAgent> object that is used by
659             #pod L<REST::Client|REST::Client> (which, in turn, is used by
660             #pod L<JIRA::REST|JIRA::REST>, which is used by this module).
661             #pod
662             #pod =cut
663              
664             sub SSL_verify_none { ## no critic (NamingConventions::Capitalization)
665 1     1 1 11 my $self = shift;
666 1         5 return $self->REST_CLIENT->getUseragent()->ssl_opts(
667             SSL_verify_mode => 0,
668             verify_hostname => 0
669             );
670             }
671              
672             #pod =internal_method B<rest_api_url_base>
673             #pod
674             #pod Returns the base URL for this JIRA server's REST API. For example, if your JIRA server is at C<http://jira.example.com>, this would return C<http://jira.example.com/rest/api/latest>.
675             #pod
676             #pod =cut
677              
678             sub rest_api_url_base {
679 2     2 1 1557 my $self = shift;
680 2 50       12 if ( $self->_JIRA_REST_version_has_separate_path ) {
681 2         8 ( my $host = $self->REST_CLIENT->getHost ) =~ s{/$}{}xms;
682 2   50     33 my $path = $self->JIRA_REST->{path} // q{/rest/api/latest};
683 2         12 return $host . $path;
684             }
685             else {
686 0         0 my ( $base )
687             = $self->REST_CLIENT->getHost =~ m{^(.+?rest/api/[^/]+)/?}xms;
688 0   0     0 return $base // $self->REST_CLIENT->getHost . '/rest/api/latest';
689             }
690             }
691              
692             #pod =internal_method B<strip_protocol_and_host>
693             #pod
694             #pod A method to take the provided URL and strip the protocol and host from it. For example, if the URL C<http://jira.example.com/rest/api/latest> was passed to this method, C</rest/api/latest> would be returned.
695             #pod
696             #pod =cut
697              
698             sub strip_protocol_and_host {
699 1     1 1 15 my $self = shift;
700 1         4 my $host = $self->REST_CLIENT->getHost;
701 1         8 ( my $url = shift ) =~ s{^$host}{}xms;
702 1         22 return $url;
703             }
704              
705             #pod =accessor B<args>
706             #pod
707             #pod An accessor that returns a copy of the arguments passed to the
708             #pod constructor. Useful for passing around to utility objects.
709             #pod
710             #pod =cut
711              
712 12     12 1 2386 sub args { return shift->{args} }
713              
714             #pod =accessor B<url>
715             #pod
716             #pod An accessor that returns the C<url> parameter passed to this object's
717             #pod constructor.
718             #pod
719             #pod =cut
720              
721 2     2 1 9161 sub url { return shift->args->{url} }
722              
723             #pod =accessor B<username>
724             #pod
725             #pod An accessor that returns the username used to connect to the JIRA server,
726             #pod even if the username was read from a C<.netrc> or
727             #pod L<Config::Identity|Config::Identity> file.
728             #pod
729             #pod =cut
730              
731             sub username {
732 2     2 1 34 my $self = shift;
733              
734 2 50       7 unless ( defined $self->args->{username} ) {
735 0         0 $self->args->{username} = $self->user_object->name;
736             }
737              
738 2         6 return $self->args->{username};
739             }
740              
741             #pod =accessor B<user_object>
742             #pod
743             #pod An accessor that returns the user used to connect to the JIRA server as a
744             #pod L<JIRA::REST::Class::User|JIRA::REST::Class::User> object, even if the username
745             #pod was read from a C<.netrc> or L<Config::Identity|Config::Identity> file. Works
746             #pod by calling C</rest/api/latest/myself>.
747             #pod
748             #pod =cut
749              
750             sub user_object {
751 1     1 1 2 my $self = shift;
752              
753 1 50       4 unless ( defined $self->{userobj} ) {
754 1         5 my $data = $self->get( '/myself' );
755 1         6985 $self->{userobj} = $self->make_object( 'user', { data => $data } );
756             }
757              
758 1         5 return $self->{userobj};
759             }
760              
761             #pod =accessor B<password>
762             #pod
763             #pod An accessor that returns the password used to connect to the JIRA server.
764             #pod Currently only works if the password was passed into the class constructor.
765             #pod Work is being done to return the password when the password was read from a
766             #pod C<.netrc> or L<Config::Identity|Config::Identity> file.
767             #pod
768             #pod =cut
769              
770 2     2 1 36 sub password { return shift->args->{password} }
771              
772             #pod =accessor B<rest_client_config>
773             #pod
774             #pod An accessor that returns the C<rest_client_config> parameter passed to this
775             #pod object's constructor.
776             #pod
777             #pod =cut
778              
779 1     1 1 25 sub rest_client_config { return shift->args->{rest_client_config} }
780              
781             #pod =accessor B<anonymous>
782             #pod
783             #pod An accessor that returns the C<anonymous> parameter passed to this object's constructor.
784             #pod
785             #pod =cut
786              
787 1     1 1 25 sub anonymous { return shift->args->{anonymous} }
788              
789             #pod =accessor B<proxy>
790             #pod
791             #pod An accessor that returns the C<proxy> parameter passed to this object's constructor.
792             #pod
793             #pod =cut
794              
795 1     1 1 27 sub proxy { return shift->args->{proxy} }
796              
797             #---------------------------------------------------------------------------
798              
799             #pod =begin testing parameter_accessors 15
800             #pod
801             #pod try {
802             #pod print "#\n# Checking parameter accessors\n#\n";
803             #pod
804             #pod my $test = get_test_client();
805             #pod my $url = TestServer_url();
806             #pod
807             #pod my $args = {
808             #pod url => $url,
809             #pod username => 'username',
810             #pod password => 'password',
811             #pod proxy => undef,
812             #pod anonymous => undef,
813             #pod rest_client_config => undef,
814             #pod ssl_verify_none => undef,
815             #pod };
816             #pod
817             #pod # the args accessor will have keys for ALL the possible arguments,
818             #pod # whether they were passed in or not.
819             #pod
820             #pod cmp_deeply( $test,
821             #pod methods( args => { %$args, },
822             #pod url => $args->{url},
823             #pod username => $args->{username},
824             #pod password => $args->{password},
825             #pod proxy => $args->{proxy},
826             #pod anonymous => $args->{anonymous},
827             #pod rest_client_config => $args->{rest_client_config} ),
828             #pod q{All accessors for parameters passed }.
829             #pod q{into the constructor okay});
830             #pod
831             #pod my $ua = $test->REST_CLIENT->getUseragent();
832             #pod $test->SSL_verify_none;
833             #pod cmp_deeply($ua->{ssl_opts}, { SSL_verify_mode => 0, verify_hostname => 0 },
834             #pod q{SSL_verify_none() does disable SSL verification});
835             #pod
836             #pod is($test->rest_api_url_base($url . "/rest/api/latest/foo"),
837             #pod $url . "/rest/api/latest", q{rest_api_url_base() works as expected});
838             #pod
839             #pod is($test->strip_protocol_and_host($test->REST_CLIENT->getHost . "/foo"),
840             #pod "/foo", q{strip_protocol_and_host() works as expected});
841             #pod
842             #pod is($test->maxResults, 50, q{maxResults() default is correct});
843             #pod
844             #pod is($test->maxResults(10), 10, q{maxResults(N) returns N});
845             #pod
846             #pod is($test->maxResults, 10,
847             #pod q{maxResults() was successfully set by previous call});
848             #pod
849             #pod print "# testing user_object() accessor\n";
850             #pod my $userobj = $test->user_object();
851             #pod
852             #pod validate_expected_fields( $userobj, {
853             #pod key => 'packy',
854             #pod name => 'packy',
855             #pod displayName => "Packy Anderson",
856             #pod emailAddress => 'packy\@cpan.org',
857             #pod });
858             #pod
859             #pod };
860             #pod
861             #pod =end testing
862             #pod
863             #pod =cut
864              
865             #---------------------------------------------------------------------------
866              
867             1;
868              
869             __END__
870              
871             =pod
872              
873             =encoding UTF-8
874              
875             =for :stopwords Packy Anderson Alexandr Alexey Ciornii Melezhik gnustavo jira JRC Gustavo
876             Leite de Mendonça Chaves Atlassian GreenHopper ScriptRunner TODO
877             aggregateprogress aggregatetimeestimate aggregatetimeoriginalestimate
878             assigneeType avatar avatarUrls completeDate displayName duedate
879             emailAddress endDate fieldtype fixVersions fromString genericized iconUrl
880             isAssigneeTypeValid issueTypes issuekeys issuelinks issuetype jql
881             lastViewed maxResults originalEstimate originalEstimateSeconds parentkey
882             projectId rapidViewId remainingEstimate remainingEstimateSeconds
883             resolutiondate sprintlist startDate subtaskIssueTypes timeSpent
884             timeSpentSeconds timeestimate timeoriginalestimate timespent timetracking
885             toString updateAuthor worklog workratio
886              
887             =head1 NAME
888              
889             JIRA::REST::Class - An OO Class module built atop L<JIRA::REST|JIRA::REST> for dealing with JIRA issues and their data as objects.
890              
891             =head1 VERSION
892              
893             version 0.11
894              
895             =head1 SYNOPSIS
896              
897             use JIRA::REST::Class;
898              
899             my $jira = JIRA::REST::Class->new({
900             url => 'https://jira.example.net',
901             username => 'myuser',
902             password => 'mypass',
903             SSL_verify_none => 1, # if your server uses self-signed SSL certs
904             });
905              
906             # get issue by key
907             my ($issue) = $jira->issues( 'MYPROJ-101' );
908              
909             # get multiple issues by key
910             my @issues = $jira->issues( 'MYPROJ-101', 'MYPROJ-102', 'MYPROJ-103' );
911              
912             # get multiple issues through search
913             my @issues =
914             $jira->issues({ jql => q/project = "MYPROJ" and status = "open" / });
915              
916             # get an iterator for a search
917             my $search =
918             $jira->iterator({ jql => q/project = "MYPROJ" and status = "open" / });
919              
920             if ( $search->issue_count ) {
921             printf "Found %d open issues in MYPROJ:\n", $search->issue_count;
922             while ( my $issue = $search->next ) {
923             printf " Issue %s is open\n", $issue->key;
924             }
925             }
926             else {
927             print "No open issues in MYPROJ.\n";
928             }
929              
930             =head1 DESCRIPTION
931              
932             An OO Class module built atop L<JIRA::REST|JIRA::REST> for dealing with JIRA
933             issues and their data as objects.
934              
935             This code is a work in progress, so it's bound to be incomplete. I add methods
936             to it as I discover I need them. I have also coded for fields that might exist
937             in my JIRA server's configuration but not in yours. It is my I<intent>,
938             however, to make things more generic as I go on so they will "just work" no
939             matter how your server is configured.
940              
941             I'm actively working with the author of L<JIRA::REST|JIRA::REST> (thanks
942             gnustavo!) to keep the arguments for C<< JIRA::REST::Class->new >> exactly
943             the same as C<< JIRA::REST->new >>, so I'm just duplicating the
944             documentation for L<< JIRA::REST->new|JIRA::REST/CONSTRUCTOR >>:
945              
946             =head1 CONSTRUCTOR
947              
948             =head2 B<new> I<HASHREF>
949              
950             =head2 B<new> I<URL>, I<USERNAME>, I<PASSWORD>, I<REST_CLIENT_CONFIG>, I<ANONYMOUS>, I<PROXY>, I<SSL_VERIFY_NONE>
951              
952             The constructor can take its arguments from a single hash reference or from
953             a list of positional parameters. The first form is preferred because it lets
954             you specify only the arguments you need. The second form forces you to pass
955             undefined values if you need to pass a specific value to an argument further
956             to the right.
957              
958             The arguments are described below with the names which must be used as the
959             hash keys:
960              
961             =over 4
962              
963             =item * B<url>
964              
965             A string or a URI object denoting the base URL of the JIRA server. This is a
966             required argument.
967              
968             The REST methods described below all accept as a first argument the
969             endpoint's path of the specific API method to call. In general you can pass
970             the complete path, beginning with the prefix denoting the particular API to
971             use (C</rest/api/VERSION>, C</rest/servicedeskapi>, or
972             C</rest/agile/VERSION>). However, to make it easier to invoke JIRA's Core
973             API if you pass a path not starting with C</rest/> it will be prefixed with
974             C</rest/api/latest> or with this URL's path if it has one. This way you can
975             choose a specific version of the JIRA Core API to use instead of the latest
976             one. For example:
977              
978             my $jira = JIRA::REST::Class->new({
979             url => 'https://jira.example.net/rest/api/1',
980             });
981              
982             =item * B<username>
983              
984             =item * B<password>
985              
986             The username and password of a JIRA user to use for authentication.
987              
988             If B<anonymous> is false then, if either B<username> or B<password> isn't
989             defined the module looks them up in either the C<.netrc> file or via
990             L<Config::Identity|Config::Identity> (which allows C<gpg> encrypted credentials).
991              
992             L<Config::Identity|Config::Identity> will look for F<~/.jira-identity> or
993             F<~/.jira>. You can change the filename stub from C<jira> to a custom stub
994             with the C<JIRA_REST_IDENTITY> environment variable.
995              
996             =item * B<rest_client_config>
997              
998             A JIRA::REST object uses a L<REST::Client|REST::Client> object to make the REST
999             invocations. This optional argument must be a hash reference that can be fed
1000             to the REST::Client constructor. Note that the C<url> argument
1001             overwrites any value associated with the C<host> key in this hash.
1002              
1003             As an extension, the hash reference also accepts one additional argument
1004             called B<proxy> that is an extension to the REST::Client configuration and
1005             will be removed from the hash before passing it on to the REST::Client
1006             constructor. However, this argument is deprecated since v0.017 and you
1007             should avoid it. Instead, use the following argument instead.
1008              
1009             =item * B<proxy>
1010              
1011             To use a network proxy set this argument to the string or URI object
1012             describing the fully qualified URL (including port) to your network proxy.
1013              
1014             =item * B<ssl_verify_none>
1015              
1016             Sets the C<SSL_verify_mode> and C<verify_hostname ssl> options on the
1017             underlying L<REST::Client|REST::Client>'s user agent to 0, thus disabling
1018             them. This allows access to JIRA servers that have self-signed certificates
1019             that don't pass L<LWP::UserAgent|LWP::UserAgent>'s verification methods.
1020              
1021             =item * B<anonymous>
1022              
1023             Tells the module that you want to connect to the specified JIRA server with
1024             no username or password. This way you can access public JIRA servers
1025             without needing to authenticate.
1026              
1027             =back
1028              
1029             =head1 METHODS
1030              
1031             =head2 B<issues> QUERY
1032              
1033             =head2 B<issues> KEY [, KEY...]
1034              
1035             The C<issues> method can be called two ways: either by providing a list of
1036             issue keys, or by proving a single hash reference which describes a JIRA
1037             query in the same format used by L<JIRA::REST|JIRA::REST> (essentially,
1038             C<< jql => "JQL query string" >>).
1039              
1040             The return value is an array of L<JIRA::REST::Class::Issue|JIRA::REST::Class::Issue> objects.
1041              
1042             =head2 B<query> QUERY
1043              
1044             The C<query> method takes a single parameter: a hash reference which
1045             describes a JIRA query in the same format used by L<JIRA::REST|JIRA::REST>
1046             (essentially, C<< jql => "JQL query string" >>).
1047              
1048             The return value is a single L<JIRA::REST::Class::Query|JIRA::REST::Class::Query> object.
1049              
1050             =head2 B<iterator> QUERY
1051              
1052             The C<query> method takes a single parameter: a hash reference which
1053             describes a JIRA query in the same format used by L<JIRA::REST|JIRA::REST>
1054             (essentially, C<< jql => "JQL query string" >>). It accepts an additional
1055             field, however: C<restart_if_lt_total>. If this field is set to a true value,
1056             the iterator will keep track of the number of results fetched and, if when
1057             the results run out this number doesn't match the number of results
1058             predicted by the query, it will restart the query. This is particularly
1059             useful if you are transforming a number of issues through an iterator, and
1060             the transformation causes the issues to no longer match the query.
1061              
1062             The return value is a single
1063             L<JIRA::REST::Class::Iterator|JIRA::REST::Class::Iterator> object. The
1064             issues returned by the query can be obtained in serial by repeatedly calling
1065             L<next|JIRA::REST::Class::Iterator/next> on this object, which returns a
1066             series of L<JIRA::REST::Class::Issue|JIRA::REST::Class::Issue> objects.
1067              
1068             =head2 B<maxResults>
1069              
1070             A getter/setter method that allows setting a global default for the L<maxResults pagination parameter for JIRA's REST API |https://docs.atlassian.com/jira/REST/latest/#pagination>. This determines the I<maximum> number of results returned by the L<issues|/"issues QUERY"> and L<query|/"query QUERY"> methods; and the initial number of results fetched by the L<iterator|/"iterator QUERY"> (when L<next|JIRA::REST::Class::Iterator/next> exhausts that initial cache of results it will automatically make subsequent calls to the REST API to fetch more results).
1071              
1072             Defaults to 50.
1073              
1074             say $jira->maxResults; # returns 50
1075              
1076             $jira->maxResults(10); # only return 10 results at a time
1077              
1078             =head2 B<issue_types>
1079              
1080             Returns a list of defined issue types (as
1081             L<JIRA::REST::Class::Issue::Type|JIRA::REST::Class::Issue::Type> objects)
1082             for this server.
1083              
1084             =head2 B<projects>
1085              
1086             Returns a list of projects (as
1087             L<JIRA::REST::Class::Project|JIRA::REST::Class::Project> objects) for this
1088             server.
1089              
1090             =head2 B<project> PROJECT_ID || PROJECT_KEY || PROJECT_NAME
1091              
1092             Returns a L<JIRA::REST::Class::Project|JIRA::REST::Class::Project> object
1093             for the project specified. Returns undef if the project doesn't exist.
1094              
1095             =head2 B<SSL_verify_none>
1096              
1097             Sets to false the SSL options C<SSL_verify_mode> and C<verify_hostname> on
1098             the L<LWP::UserAgent|LWP::UserAgent> object that is used by
1099             L<REST::Client|REST::Client> (which, in turn, is used by
1100             L<JIRA::REST|JIRA::REST>, which is used by this module).
1101              
1102             =head2 B<name_for_user>
1103              
1104             When passed a scalar that could be a
1105             L<JIRA::REST::Class::User|JIRA::REST::Class::User> object, returns the name
1106             of the user if it is a C<JIRA::REST::Class::User>
1107             object, or the unmodified scalar if it is not.
1108              
1109             =head2 B<key_for_issue>
1110              
1111             When passed a scalar that could be a
1112             L<JIRA::REST::Class::Issue|JIRA::REST::Class::Issue> object, returns the key
1113             of the issue if it is a C<JIRA::REST::Class::Issue>
1114             object, or the unmodified scalar if it is not.
1115              
1116             =head2 B<find_link_name_and_direction>
1117              
1118             When passed two scalars, one that could be a
1119             L<JIRA::REST::Class::Issue::LinkType|JIRA::REST::Class::Issue::LinkType>
1120             object and another that is a direction (inward/outward), returns the name of
1121             the link type and direction if it is a C<JIRA::REST::Class::Issue::LinkType>
1122             object, or attempts to determine the link type and direction from the
1123             provided scalars.
1124              
1125             =head2 B<dump>
1126              
1127             Returns a stringified representation of the object's data generated somewhat
1128             by L<Data::Dumper::Concise|Data::Dumper::Concise>, but not descending into
1129             any objects that might be part of that data. If it finds objects in the
1130             data, it will attempt to represent them in some abbreviated fashion which
1131             may not display all the data in the object. For instance, if the object has
1132             a C<JIRA::REST::Class::Issue> object in it for an issue with the key
1133             C<'JRC-1'>, the object would be represented as the string C<<
1134             'JIRA::REST::Class::Issue->key(JRC-1)' >>. The goal is to provide a gist of
1135             what the contents of the object are without exhaustively dumping EVERYTHING.
1136             I use it a lot for figuring out what's in the results I'm getting back from
1137             the JIRA API.
1138              
1139             =head1 READ-ONLY ACCESSORS
1140              
1141             =head2 B<args>
1142              
1143             An accessor that returns a copy of the arguments passed to the
1144             constructor. Useful for passing around to utility objects.
1145              
1146             =head2 B<url>
1147              
1148             An accessor that returns the C<url> parameter passed to this object's
1149             constructor.
1150              
1151             =head2 B<username>
1152              
1153             An accessor that returns the username used to connect to the JIRA server,
1154             even if the username was read from a C<.netrc> or
1155             L<Config::Identity|Config::Identity> file.
1156              
1157             =head2 B<user_object>
1158              
1159             An accessor that returns the user used to connect to the JIRA server as a
1160             L<JIRA::REST::Class::User|JIRA::REST::Class::User> object, even if the username
1161             was read from a C<.netrc> or L<Config::Identity|Config::Identity> file. Works
1162             by calling C</rest/api/latest/myself>.
1163              
1164             =head2 B<password>
1165              
1166             An accessor that returns the password used to connect to the JIRA server.
1167             Currently only works if the password was passed into the class constructor.
1168             Work is being done to return the password when the password was read from a
1169             C<.netrc> or L<Config::Identity|Config::Identity> file.
1170              
1171             =head2 B<rest_client_config>
1172              
1173             An accessor that returns the C<rest_client_config> parameter passed to this
1174             object's constructor.
1175              
1176             =head2 B<anonymous>
1177              
1178             An accessor that returns the C<anonymous> parameter passed to this object's constructor.
1179              
1180             =head2 B<proxy>
1181              
1182             An accessor that returns the C<proxy> parameter passed to this object's constructor.
1183              
1184             =head1 INTERNAL METHODS
1185              
1186             =head2 B<get>
1187              
1188             A wrapper for C<JIRA::REST>'s L<GET|JIRA::REST/"GET RESOURCE [, QUERY]"> method.
1189              
1190             =head2 B<post>
1191              
1192             Wrapper around C<JIRA::REST>'s L<POST|JIRA::REST/"POST RESOURCE, QUERY, VALUE [, HEADERS]"> method.
1193              
1194             =head2 B<put>
1195              
1196             Wrapper around C<JIRA::REST>'s L<PUT|JIRA::REST/"PUT RESOURCE, QUERY, VALUE [, HEADERS]"> method.
1197              
1198             =head2 B<delete>
1199              
1200             Wrapper around C<JIRA::REST>'s L<DELETE|JIRA::REST/"DELETE RESOURCE [, QUERY]"> method.
1201              
1202             =head2 B<data_upload>
1203              
1204             Similar to
1205             L<< JIRA::REST->attach_files|JIRA::REST/"attach_files ISSUE FILE..." >>,
1206             but entirely from memory and only attaches one file at a time. Returns the
1207             L<HTTP::Response|HTTP::Response> object from the post request. Takes the
1208             following named parameters:
1209              
1210             =over 4
1211              
1212             =item + B<url>
1213              
1214             The relative URL to POST to. This will have the hostname and REST version
1215             information prepended to it, so all you need to provide is something like
1216             C</issue/>I<issueIdOrKey>C</attachments>. I'm allowing the URL to be
1217             specified in case I later discover something this can be used for besides
1218             attaching files to issues.
1219              
1220             =item + B<name>
1221              
1222             The name that is specified for this file attachment.
1223              
1224             =item + B<data>
1225              
1226             The actual data to be uploaded. If a reference is provided, it will be
1227             dereferenced before posting the data.
1228              
1229             =back
1230              
1231             I guess that makes it only a I<little> like
1232             C<< JIRA::REST->attach_files >>...
1233              
1234             =head2 B<rest_api_url_base>
1235              
1236             Returns the base URL for this JIRA server's REST API. For example, if your JIRA server is at C<http://jira.example.com>, this would return C<http://jira.example.com/rest/api/latest>.
1237              
1238             =head2 B<strip_protocol_and_host>
1239              
1240             A method to take the provided URL and strip the protocol and host from it. For example, if the URL C<http://jira.example.com/rest/api/latest> was passed to this method, C</rest/api/latest> would be returned.
1241              
1242             =head2 B<jira>
1243              
1244             Returns a L<JIRA::REST::Class|JIRA::REST::Class> object with credentials for the last JIRA user.
1245              
1246             =head2 B<factory>
1247              
1248             An accessor for the L<JIRA::REST::Class::Factory|JIRA::REST::Class::Factory>.
1249              
1250             =head2 B<JIRA_REST>
1251              
1252             An accessor that returns the L<JIRA::REST|JIRA::REST> object being used.
1253              
1254             =head2 B<REST_CLIENT>
1255              
1256             An accessor that returns the L<REST::Client|REST::Client> object inside the L<JIRA::REST|JIRA::REST> object being used.
1257              
1258             =head2 B<JSON>
1259              
1260             An accessor that returns the L<JSON|JSON> object inside the L<JIRA::REST|JIRA::REST> object being used.
1261              
1262             =head2 B<make_object>
1263              
1264             A pass-through method that calls L<JIRA::REST::Class::Factory::make_object()|JIRA::REST::Class::Factory/make_object>.
1265              
1266             =head2 B<make_date>
1267              
1268             A pass-through method that calls L<JIRA::REST::Class::Factory::make_date()|JIRA::REST::Class::Factory/make_date>.
1269              
1270             =head2 B<class_for>
1271              
1272             A pass-through method that calls L<JIRA::REST::Class::Factory::get_factory_class()|JIRA::REST::Class::Factory/get_factory_class>.
1273              
1274             =head2 B<obj_isa>
1275              
1276             When passed a scalar that I<could> be an object and a class string,
1277             returns whether the scalar is, in fact, an object of that class.
1278             Looks up the actual class using C<class_for()>, which calls
1279             L<JIRA::REST::Class::Factory::get_factory_class()|JIRA::REST::Class::Factory/get_factory_class>.
1280              
1281             =head2 B<cosmetic_copy> I<THING>
1282              
1283             A utility function to produce a "cosmetic" copy of a thing: it clones
1284             the data structure, but if anything in the structure (other than the
1285             structure itself) is a blessed object, it replaces it with a
1286             stringification of that object that probably doesn't contain all the
1287             data in the object. For instance, if the object has a
1288             C<JIRA::REST::Class::Issue> object in it for an issue with the key
1289             C<'JRC-1'>, the object would be represented as the string
1290             C<< 'JIRA::REST::Class::Issue->key(JRC-1)' >>. The goal is to provide a
1291             gist of what the contents of the object are without exhaustively dumping
1292             EVERYTHING.
1293              
1294             =head1 RELATED CLASSES
1295              
1296             =over 2
1297              
1298             =item * L<JIRA::REST::Class::Factory|JIRA::REST::Class::Factory>
1299              
1300             =item * L<JIRA::REST::Class::Issue|JIRA::REST::Class::Issue>
1301              
1302             =item * L<JIRA::REST::Class::Issue::Type|JIRA::REST::Class::Issue::Type>
1303              
1304             =item * L<JIRA::REST::Class::Iterator|JIRA::REST::Class::Iterator>
1305              
1306             =item * L<JIRA::REST::Class::Mixins|JIRA::REST::Class::Mixins>
1307              
1308             =item * L<JIRA::REST::Class::Project|JIRA::REST::Class::Project>
1309              
1310             =item * L<JIRA::REST::Class::Query|JIRA::REST::Class::Query>
1311              
1312             =item * L<JIRA::REST::Class::User|JIRA::REST::Class::User>
1313              
1314             =back
1315              
1316             =begin test setup 1
1317              
1318             use File::Basename;
1319             use lib dirname($0).'/..';
1320             use MyTest;
1321             use 5.010;
1322              
1323             TestServer_setup();
1324              
1325             END {
1326             TestServer_stop();
1327             }
1328              
1329             use_ok('JIRA::REST::Class');
1330              
1331             sub get_test_client {
1332             state $test =
1333             JIRA::REST::Class->new(TestServer_url(), 'username', 'password');
1334             $test->REST_CLIENT->setTimeout(5);
1335             return $test;
1336             };
1337              
1338             =end test
1339              
1340             =begin testing new 5
1341              
1342             my $jira;
1343             try {
1344             $jira = JIRA::REST::Class->new({
1345             url => TestServer_url(),
1346             username => 'user',
1347             password => 'pass',
1348             proxy => '',
1349             anonymous => 0,
1350             ssl_verify_none => 1,
1351             rest_client_config => {},
1352             });
1353             }
1354             catch {
1355             $jira = $_; # as good a place as any to stash the error, because
1356             # isa_ok() will complain that it's not an object.
1357             };
1358              
1359             isa_ok($jira, 'JIRA::REST::Class', 'JIRA::REST::Class->new');
1360              
1361             my $needs_url_regexp = qr/'?url'? argument must be defined/i;
1362              
1363             throws_ok(
1364             sub {
1365             JIRA::REST::Class->new();
1366             },
1367             $needs_url_regexp,
1368             'JIRA::REST::Class->new with no parameters throws an exception',
1369             );
1370              
1371             throws_ok(
1372             sub {
1373             JIRA::REST::Class->new({
1374             username => 'user',
1375             password => 'pass',
1376             });
1377             },
1378             $needs_url_regexp,
1379             'JIRA::REST::Class->new with no url throws an exception',
1380             );
1381              
1382             throws_ok(
1383             sub {
1384             JIRA::REST::Class->new('http://not.a.good.server.com');
1385             },
1386             qr/No credentials found/,
1387             q{JIRA::REST::Class->new with just url tries to find credentials},
1388             );
1389              
1390             lives_ok(
1391             sub {
1392             JIRA::REST::Class->new(TestServer_url(), 'user', 'pass');
1393             },
1394             q{JIRA::REST::Class->new with url, username, and password does't croak!},
1395             );
1396              
1397             =end testing
1398              
1399             =begin testing get
1400              
1401             validate_wrapper_method( sub { get_test_client()->get('/test'); },
1402             { GET => 'SUCCESS' }, 'get() method works' );
1403              
1404              
1405             =end testing
1406              
1407             =begin testing post
1408              
1409             validate_wrapper_method( sub { get_test_client()->post('/test', "key=value"); },
1410             { POST => 'SUCCESS' }, 'post() method works' );
1411              
1412              
1413             =end testing
1414              
1415             =begin testing put
1416              
1417             validate_wrapper_method( sub { get_test_client()->put('/test', "key=value"); },
1418             { PUT => 'SUCCESS' }, 'put() method works' );
1419              
1420              
1421             =end testing
1422              
1423             =begin testing delete
1424              
1425             validate_wrapper_method( sub { get_test_client()->delete('/test'); },
1426             { DELETE => 'SUCCESS' }, 'delete() method works' );
1427              
1428              
1429             =end testing
1430              
1431             =begin testing data_upload
1432              
1433             my $expected = {
1434             "Content-Disposition" => "form-data; name=\"file\"; filename=\"file.txt\"",
1435             POST => "SUCCESS",
1436             data => "An OO Class module built atop L<JIRA::REST|JIRA::REST> for dealing with "
1437             . "JIRA issues and their data as objects.",
1438             name => "file.txt"
1439             };
1440              
1441             my $test1_name = 'return value from data_upload()';
1442             my $test2_name = 'data_upload() method succeeded';
1443             my $test3_name = 'data_upload() method returned expected data';
1444              
1445             my $test = get_test_client();
1446             my $results;
1447             my $got;
1448              
1449             try {
1450             $results = $test->data_upload({
1451             url => "/data_upload",
1452             name => $expected->{name},
1453             data => $expected->{data},
1454             });
1455             $got = $test->JSON->decode($results->decoded_content);
1456             }
1457             catch {
1458             $results = $_;
1459             diag($test->REST_CLIENT->getHost);
1460             };
1461              
1462             my $test1_ok = isa_ok( $results, 'HTTP::Response', $test1_name );
1463             my $test2_ok = ok($test1_ok && $results->is_success, $test2_name );
1464             $test2_ok ? is_deeply( $got, $expected, $test3_name ) : fail( $test3_name );
1465              
1466             =end testing
1467              
1468             =begin testing issue_types 16
1469              
1470             try {
1471             my $test = get_test_client();
1472              
1473             validate_contextual_accessor($test, {
1474             method => 'issue_types',
1475             class => 'issuetype',
1476             data => [ sort qw/ Bug Epic Improvement Sub-task Story Task /,
1477             'New Feature' ],
1478             });
1479              
1480             print "#\n# Checking the 'Bug' issue type\n#\n";
1481              
1482             my ($bug) = sort $test->issue_types;
1483              
1484             can_ok_abstract( $bug, qw/ description iconUrl id name self subtask / );
1485              
1486             my $host = TestServer_url();
1487              
1488             validate_expected_fields( $bug, {
1489             description => "jira.translation.issuetype.bug.name.desc",
1490             iconUrl => "$host/secure/viewavatar?size=xsmall&avatarId=10303"
1491             . "&avatarType=issuetype",
1492             id => 10004,
1493             name => "Bug",
1494             self => "$host/rest/api/latest/issuetype/10004",
1495             subtask => JSON::PP::false,
1496             });
1497             };
1498              
1499             =end testing
1500              
1501             =begin testing projects 5
1502              
1503             my $test = get_test_client();
1504              
1505             try {
1506             validate_contextual_accessor($test, {
1507             method => 'projects',
1508             class => 'project',
1509             data => [ qw/ JRC KANBAN PACKAY PM SCRUM / ],
1510             });
1511             };
1512              
1513             =end testing
1514              
1515             =begin testing project 17
1516              
1517             try {
1518             print "#\n# Checking the SCRUM project\n#\n";
1519              
1520             my $test = get_test_client();
1521              
1522             my $proj = $test->project('SCRUM');
1523              
1524             can_ok_abstract( $proj, qw/ avatarUrls expand id key name self
1525             category assigneeType components
1526             description issueTypes lead roles versions
1527             allowed_components allowed_versions
1528             allowed_fix_versions allowed_issue_types
1529             allowed_priorities allowed_field_values
1530             field_metadata_exists field_metadata
1531             field_name
1532             / );
1533              
1534             validate_expected_fields( $proj, {
1535             expand => "description,lead,url,projectKeys",
1536             id => 10002,
1537             key => 'SCRUM',
1538             name => "Scrum Software Development Sample Project",
1539             projectTypeKey => "software",
1540             lead => {
1541             class => 'user',
1542             expected => {
1543             key => 'packy'
1544             },
1545             },
1546             });
1547              
1548             validate_contextual_accessor($proj, {
1549             method => 'versions',
1550             class => 'projectvers',
1551             name => "SCRUM project's",
1552             data => [ "Version 1.0", "Version 2.0", "Version 3.0" ],
1553             });
1554             };
1555              
1556             =end testing
1557              
1558             =begin testing parameter_accessors 15
1559              
1560             try {
1561             print "#\n# Checking parameter accessors\n#\n";
1562              
1563             my $test = get_test_client();
1564             my $url = TestServer_url();
1565              
1566             my $args = {
1567             url => $url,
1568             username => 'username',
1569             password => 'password',
1570             proxy => undef,
1571             anonymous => undef,
1572             rest_client_config => undef,
1573             ssl_verify_none => undef,
1574             };
1575              
1576             # the args accessor will have keys for ALL the possible arguments,
1577             # whether they were passed in or not.
1578              
1579             cmp_deeply( $test,
1580             methods( args => { %$args, },
1581             url => $args->{url},
1582             username => $args->{username},
1583             password => $args->{password},
1584             proxy => $args->{proxy},
1585             anonymous => $args->{anonymous},
1586             rest_client_config => $args->{rest_client_config} ),
1587             q{All accessors for parameters passed }.
1588             q{into the constructor okay});
1589              
1590             my $ua = $test->REST_CLIENT->getUseragent();
1591             $test->SSL_verify_none;
1592             cmp_deeply($ua->{ssl_opts}, { SSL_verify_mode => 0, verify_hostname => 0 },
1593             q{SSL_verify_none() does disable SSL verification});
1594              
1595             is($test->rest_api_url_base($url . "/rest/api/latest/foo"),
1596             $url . "/rest/api/latest", q{rest_api_url_base() works as expected});
1597              
1598             is($test->strip_protocol_and_host($test->REST_CLIENT->getHost . "/foo"),
1599             "/foo", q{strip_protocol_and_host() works as expected});
1600              
1601             is($test->maxResults, 50, q{maxResults() default is correct});
1602              
1603             is($test->maxResults(10), 10, q{maxResults(N) returns N});
1604              
1605             is($test->maxResults, 10,
1606             q{maxResults() was successfully set by previous call});
1607              
1608             print "# testing user_object() accessor\n";
1609             my $userobj = $test->user_object();
1610              
1611             validate_expected_fields( $userobj, {
1612             key => 'packy',
1613             name => 'packy',
1614             displayName => "Packy Anderson",
1615             emailAddress => 'packy\@cpan.org',
1616             });
1617              
1618             };
1619              
1620             =end testing
1621              
1622             =head1 SEE ALSO
1623              
1624             =over
1625              
1626             =item * L<JIRA::REST|JIRA::REST>
1627              
1628             C<JIRA::REST::Class> uses C<JIRA::REST> to perform all its interaction with JIRA.
1629              
1630             =item * L<REST::Client|REST::Client>
1631              
1632             C<JIRA::REST> uses a C<REST::Client> object to perform its low-level interactions.
1633              
1634             =item * L<JIRA REST API Reference|https://docs.atlassian.com/jira/REST/latest/>
1635              
1636             Atlassian's official JIRA REST API Reference.
1637              
1638             =back
1639              
1640             =head1 REPOSITORY
1641              
1642             L<https://github.com/packy/JIRA-REST-Class|https://github.com/packy/JIRA-REST-Class>
1643              
1644             =head1 CREDITS
1645              
1646             =over 4
1647              
1648             =item L<Gustavo Leite de Mendonça Chaves|https://metacpan.org/author/GNUSTAVO> <gnustavo@cpan.org>
1649              
1650             Many thanks to Gustavo for L<JIRA::REST|JIRA::REST>, which is what I started
1651             working with when I first wanted to automate my interactions with JIRA in
1652             the summer of 2016, and without which I would have had a LOT of work to do.
1653              
1654             =back
1655              
1656             =head1 AUTHOR
1657              
1658             Packy Anderson <packy@cpan.org>
1659              
1660             =head1 CONTRIBUTORS
1661              
1662             =for stopwords Alexandr Ciornii Alexey Melezhik
1663              
1664             =over 4
1665              
1666             =item *
1667              
1668             Alexandr Ciornii <alexchorny@gmail.com>
1669              
1670             =item *
1671              
1672             Alexey Melezhik <melezhik@gmail.com>
1673              
1674             =back
1675              
1676             =head1 COPYRIGHT AND LICENSE
1677              
1678             This software is Copyright (c) 2017 by Packy Anderson.
1679              
1680             This is free software, licensed under:
1681              
1682             The Artistic License 2.0 (GPL Compatible)
1683              
1684             =cut