File Coverage

lib/JIRA/REST/Class.pm
Criterion Covered Total %
statement 109 129 84.5
branch 20 32 62.5
condition 4 13 30.7
subroutine 31 34 91.1
pod 24 24 100.0
total 188 232 81.0


line stmt bran cond sub pod time code
1             package JIRA::REST::Class;
2 4     4   4478 use strict;
  4         12  
  4         109  
3 4     4   21 use warnings;
  4         12  
  4         105  
4 4     4   73 use 5.010;
  4         19  
5              
6             our $VERSION = '0.12';
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   20 use Carp;
  4         8  
  4         282  
14 4     4   20 use Clone::Any qw( clone );
  4         10  
  4         38  
15 4     4   1865 use Readonly 2.04;
  4         8509  
  4         171  
16              
17 4     4   1173 use JIRA::REST;
  4         49077  
  4         138  
18 4     4   823 use JIRA::REST::Class::Factory;
  4         7  
  4         130  
19 4     4   19 use parent qw(JIRA::REST::Class::Mixins);
  4         6  
  4         13  
20              
21             #---------------------------------------------------------------------------
22             # Constants
23              
24             Readonly my $DEFAULT_MAXRESULTS => 50;
25              
26             #---------------------------------------------------------------------------
27              
28             sub new {
29 44     44 1 135060 my ( $class, @arglist ) = @_;
30              
31 44         203 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         680 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 17 my ( $self, $url, @args ) = @_;
239 6         17 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 3 my ( $self, $url, @args ) = @_;
263 1         4 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 4 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         3 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 65 my ( $self, @args ) = @_;
363 1         9 my $args = $self->_get_known_args( \@args, qw/ url name data / );
364 1         13 $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       4 my $data = ref $args->{data} ? ${ $args->{data} } : $args->{data};
  0         0  
373              
374             # code cribbed from JIRA::REST
375             #
376 1         3 my $url = $self->rest_api_url_base . $args->{url};
377 1         7 my $rest = $self->REST_CLIENT;
378             my $response = $rest->getUseragent()->post(
379             $url,
380 1         17 %{ $rest->{_headers} },
  1         11  
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       19225 $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         14 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       8 if ( @_ ) {
456 1         2 $self->{maxResults} = shift;
457             }
458 3 100 66     12 unless ( exists $self->{maxResults} && defined $self->{maxResults} ) {
459 1         6 $self->{maxResults} = $DEFAULT_MAXRESULTS;
460             }
461 3         13 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 1000 my $self = shift;
474              
475 3 100       10 unless ( $self->{issue_types} ) {
476 1         4 my $types = $self->get( '/issuetype' );
477             $self->{issue_types} = [ # stop perltidy from pulling
478             map { # these lines together
479 1         5735 $self->make_object( 'issuetype', { data => $_ } );
  7         31  
480             } @$types
481             ];
482             }
483              
484 3 100       10 return @{ $self->{issue_types} } if wantarray;
  2         9  
485 1         3 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 990 my $self = shift;
537              
538 3 100       11 unless ( $self->{project_list} ) {
539              
540             # get the project list from JIRA
541 1         5 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         6201 my $list = $self->{project_list} = [];
547              
548             my $make_project_hash_entry = sub {
549 5     5   7 my $prj = shift;
550 5         18 my $obj = $self->make_object( 'project', { data => $prj } );
551              
552 5         12 push @$list, $obj;
553              
554 5         12 return $obj->id => $obj, $obj->key => $obj, $obj->name => $obj;
555 1         6 };
556              
557             $self->{project_hash} = { ##
558 1         3 map { $make_project_hash_entry->( $_ ) } @$projects
  5         8  
559             };
560             }
561              
562 3 100       9 return @{ $self->{project_list} } if wantarray;
  1         5  
563 2         5 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 2 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         6 $self->projects; # load the project hash if it hasn't been loaded
601              
602 1 50       6 return unless exists $self->{project_hash}->{$proj};
603 1         4 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 9 my $self = shift;
666 1         4 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 1360 my $self = shift;
680 2 50       9 if ( $self->_JIRA_REST_version_has_separate_path ) {
681 2         6 ( my $host = $self->REST_CLIENT->getHost ) =~ s{/$}{}xms;
682 2   50     27 my $path = $self->JIRA_REST->{path} // q{/rest/api/latest};
683 2         7 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 14 my $self = shift;
700 1         2 my $host = $self->REST_CLIENT->getHost;
701 1         7 ( my $url = shift ) =~ s{^$host}{}xms;
702 1         19 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 14     14 1 1980 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 7630 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 25 my $self = shift;
733              
734 2 50       11 unless ( defined $self->args->{username} ) {
735 0         0 $self->args->{username} = $self->user_object->name;
736             }
737              
738 2         8 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 making the JIRA REST API call L</rest/api/latest/myself|https://docs.atlassian.com/jira/REST/cloud/#api/2/myself>.
747             #pod
748             #pod =cut
749              
750             sub user_object {
751 1     1 1 4 my $self = shift;
752              
753 1 50       5 unless ( defined $self->{userobj} ) {
754 1         6 my $data = $self->get( '/myself' );
755 1         6482 $self->{userobj} = $self->make_object( 'user', { data => $data } );
756             }
757              
758 1         4 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 even if the username was read from a C<.netrc> or
765             #pod L<Config::Identity|Config::Identity> file.
766             #pod
767             #pod =cut
768              
769             sub password {
770 2     2 1 23 my $self = shift;
771              
772 2 50       5 unless ( $self->args->{password} ) {
773              
774             # we don't have the password cached, so get it from
775             # the Authorization header we're sending to JIRA
776              
777 0         0 my $rest = $self->JIRA_REST->{rest};
778 0 0       0 if ( my $auth = $rest->{_headers}->{Authorization} ) {
779 0         0 my ( undef, $encoded ) = split /\s+/, $auth;
780 0         0 ( undef, $self->args->{password} ) =
781             split /:/, decode_base64 $encoded;
782             }
783             }
784              
785 2         4 return $self->args->{password};
786             }
787              
788             #pod =accessor B<rest_client_config>
789             #pod
790             #pod An accessor that returns the C<rest_client_config> parameter passed to this
791             #pod object's constructor.
792             #pod
793             #pod =cut
794              
795 1     1 1 20 sub rest_client_config { return shift->args->{rest_client_config} }
796              
797             #pod =accessor B<anonymous>
798             #pod
799             #pod An accessor that returns the C<anonymous> parameter passed to this object's constructor.
800             #pod
801             #pod =cut
802              
803 1     1 1 21 sub anonymous { return shift->args->{anonymous} }
804              
805             #pod =accessor B<proxy>
806             #pod
807             #pod An accessor that returns the C<proxy> parameter passed to this object's constructor.
808             #pod
809             #pod =cut
810              
811 1     1 1 22 sub proxy { return shift->args->{proxy} }
812              
813             #---------------------------------------------------------------------------
814              
815             #pod =begin testing parameter_accessors 15
816             #pod
817             #pod try {
818             #pod print "#\n# Checking parameter accessors\n#\n";
819             #pod
820             #pod my $test = get_test_client();
821             #pod my $url = TestServer_url();
822             #pod
823             #pod my $args = {
824             #pod url => $url,
825             #pod username => 'username',
826             #pod password => 'password',
827             #pod proxy => undef,
828             #pod anonymous => undef,
829             #pod rest_client_config => undef,
830             #pod ssl_verify_none => undef,
831             #pod };
832             #pod
833             #pod # the args accessor will have keys for ALL the possible arguments,
834             #pod # whether they were passed in or not.
835             #pod
836             #pod cmp_deeply( $test,
837             #pod methods( args => { %$args, },
838             #pod url => $args->{url},
839             #pod username => $args->{username},
840             #pod password => $args->{password},
841             #pod proxy => $args->{proxy},
842             #pod anonymous => $args->{anonymous},
843             #pod rest_client_config => $args->{rest_client_config} ),
844             #pod q{All accessors for parameters passed }.
845             #pod q{into the constructor okay});
846             #pod
847             #pod my $ua = $test->REST_CLIENT->getUseragent();
848             #pod $test->SSL_verify_none;
849             #pod cmp_deeply($ua->{ssl_opts}, { SSL_verify_mode => 0, verify_hostname => 0 },
850             #pod q{SSL_verify_none() does disable SSL verification});
851             #pod
852             #pod is($test->rest_api_url_base($url . "/rest/api/latest/foo"),
853             #pod $url . "/rest/api/latest", q{rest_api_url_base() works as expected});
854             #pod
855             #pod is($test->strip_protocol_and_host($test->REST_CLIENT->getHost . "/foo"),
856             #pod "/foo", q{strip_protocol_and_host() works as expected});
857             #pod
858             #pod is($test->maxResults, 50, q{maxResults() default is correct});
859             #pod
860             #pod is($test->maxResults(10), 10, q{maxResults(N) returns N});
861             #pod
862             #pod is($test->maxResults, 10,
863             #pod q{maxResults() was successfully set by previous call});
864             #pod
865             #pod print "# testing user_object() accessor\n";
866             #pod my $userobj = $test->user_object();
867             #pod
868             #pod validate_expected_fields( $userobj, {
869             #pod key => 'packy',
870             #pod name => 'packy',
871             #pod displayName => "Packy Anderson",
872             #pod emailAddress => 'packy\@cpan.org',
873             #pod });
874             #pod
875             #pod };
876             #pod
877             #pod =end testing
878             #pod
879             #pod =cut
880              
881             #---------------------------------------------------------------------------
882              
883             1;
884              
885             __END__
886              
887             =pod
888              
889             =encoding UTF-8
890              
891             =for :stopwords Packy Anderson Alexandr Alexey Ciornii Heumann Manni Melezhik gnustavo jira
892             JRC Gustavo Leite de Mendonça Chaves Atlassian GreenHopper ScriptRunner
893             TODO aggregateprogress aggregatetimeestimate aggregatetimeoriginalestimate
894             assigneeType avatar avatarUrls completeDate displayName duedate
895             emailAddress endDate fieldtype fixVersions fromString genericized iconUrl
896             isAssigneeTypeValid issueTypes issuekeys issuelinks issuetype jql
897             lastViewed maxResults originalEstimate originalEstimateSeconds parentkey
898             projectId rapidViewId remainingEstimate remainingEstimateSeconds
899             resolutiondate sprintlist startDate subtaskIssueTypes timeSpent
900             timeSpentSeconds timeestimate timeoriginalestimate timespent timetracking
901             toString updateAuthor worklog workratio
902              
903             =head1 NAME
904              
905             JIRA::REST::Class - An OO Class module built atop L<JIRA::REST|JIRA::REST> for dealing with JIRA issues and their data as objects.
906              
907             =head1 VERSION
908              
909             version 0.12
910              
911             =head1 SYNOPSIS
912              
913             use JIRA::REST::Class;
914              
915             my $jira = JIRA::REST::Class->new({
916             url => 'https://jira.example.net',
917             username => 'myuser',
918             password => 'mypass',
919             SSL_verify_none => 1, # if your server uses self-signed SSL certs
920             });
921              
922             # get issue by key
923             my ($issue) = $jira->issues( 'MYPROJ-101' );
924              
925             # get multiple issues by key
926             my @issues = $jira->issues( 'MYPROJ-101', 'MYPROJ-102', 'MYPROJ-103' );
927              
928             # get multiple issues through search
929             my @issues =
930             $jira->issues({ jql => q/project = "MYPROJ" and status = "open" / });
931              
932             # get an iterator for a search
933             my $search =
934             $jira->iterator({ jql => q/project = "MYPROJ" and status = "open" / });
935              
936             if ( $search->issue_count ) {
937             printf "Found %d open issues in MYPROJ:\n", $search->issue_count;
938             while ( my $issue = $search->next ) {
939             printf " Issue %s is open\n", $issue->key;
940             }
941             }
942             else {
943             print "No open issues in MYPROJ.\n";
944             }
945              
946             =head1 DESCRIPTION
947              
948             An OO Class module built atop L<JIRA::REST|JIRA::REST> for dealing with JIRA
949             issues and their data as objects.
950              
951             This code is a work in progress, so it's bound to be incomplete. I add methods
952             to it as I discover I need them. I have also coded for fields that might exist
953             in my JIRA server's configuration but not in yours. It is my I<intent>,
954             however, to make things more generic as I go on so they will "just work" no
955             matter how your server is configured.
956              
957             I'm actively working with the author of L<JIRA::REST|JIRA::REST> (thanks
958             gnustavo!) to keep the arguments for C<< JIRA::REST::Class->new >> exactly
959             the same as C<< JIRA::REST->new >>, so I'm just duplicating the
960             documentation for L<< JIRA::REST->new|JIRA::REST/CONSTRUCTOR >>:
961              
962             =head1 CONSTRUCTOR
963              
964             =head2 B<new> I<HASHREF>
965              
966             =head2 B<new> I<URL>, I<USERNAME>, I<PASSWORD>, I<REST_CLIENT_CONFIG>, I<ANONYMOUS>, I<PROXY>, I<SSL_VERIFY_NONE>
967              
968             The constructor can take its arguments from a single hash reference or from
969             a list of positional parameters. The first form is preferred because it lets
970             you specify only the arguments you need. The second form forces you to pass
971             undefined values if you need to pass a specific value to an argument further
972             to the right.
973              
974             The arguments are described below with the names which must be used as the
975             hash keys:
976              
977             =over 4
978              
979             =item * B<url>
980              
981             A string or a URI object denoting the base URL of the JIRA server. This is a
982             required argument.
983              
984             The REST methods described below all accept as a first argument the
985             endpoint's path of the specific API method to call. In general you can pass
986             the complete path, beginning with the prefix denoting the particular API to
987             use (C</rest/api/VERSION>, C</rest/servicedeskapi>, or
988             C</rest/agile/VERSION>). However, to make it easier to invoke JIRA's Core
989             API if you pass a path not starting with C</rest/> it will be prefixed with
990             C</rest/api/latest> or with this URL's path if it has one. This way you can
991             choose a specific version of the JIRA Core API to use instead of the latest
992             one. For example:
993              
994             my $jira = JIRA::REST::Class->new({
995             url => 'https://jira.example.net/rest/api/1',
996             });
997              
998             =item * B<username>
999              
1000             =item * B<password>
1001              
1002             The username and password of a JIRA user to use for authentication.
1003              
1004             If B<anonymous> is false then, if either B<username> or B<password> isn't
1005             defined the module looks them up in either the C<.netrc> file or via
1006             L<Config::Identity|Config::Identity> (which allows C<gpg> encrypted credentials).
1007              
1008             L<Config::Identity|Config::Identity> will look for F<~/.jira-identity> or
1009             F<~/.jira>. You can change the filename stub from C<jira> to a custom stub
1010             with the C<JIRA_REST_IDENTITY> environment variable.
1011              
1012             =item * B<rest_client_config>
1013              
1014             A JIRA::REST object uses a L<REST::Client|REST::Client> object to make the REST
1015             invocations. This optional argument must be a hash reference that can be fed
1016             to the REST::Client constructor. Note that the C<url> argument
1017             overwrites any value associated with the C<host> key in this hash.
1018              
1019             As an extension, the hash reference also accepts one additional argument
1020             called B<proxy> that is an extension to the REST::Client configuration and
1021             will be removed from the hash before passing it on to the REST::Client
1022             constructor. However, this argument is deprecated since v0.017 and you
1023             should avoid it. Instead, use the following argument instead.
1024              
1025             =item * B<proxy>
1026              
1027             To use a network proxy set this argument to the string or URI object
1028             describing the fully qualified URL (including port) to your network proxy.
1029              
1030             =item * B<ssl_verify_none>
1031              
1032             Sets the C<SSL_verify_mode> and C<verify_hostname ssl> options on the
1033             underlying L<REST::Client|REST::Client>'s user agent to 0, thus disabling
1034             them. This allows access to JIRA servers that have self-signed certificates
1035             that don't pass L<LWP::UserAgent|LWP::UserAgent>'s verification methods.
1036              
1037             =item * B<anonymous>
1038              
1039             Tells the module that you want to connect to the specified JIRA server with
1040             no username or password. This way you can access public JIRA servers
1041             without needing to authenticate.
1042              
1043             =back
1044              
1045             =head1 METHODS
1046              
1047             =head2 B<issues> QUERY
1048              
1049             =head2 B<issues> KEY [, KEY...]
1050              
1051             The C<issues> method can be called two ways: either by providing a list of
1052             issue keys, or by proving a single hash reference which describes a JIRA
1053             query in the same format used by L<JIRA::REST|JIRA::REST> (essentially,
1054             C<< jql => "JQL query string" >>).
1055              
1056             The return value is an array of L<JIRA::REST::Class::Issue|JIRA::REST::Class::Issue> objects.
1057              
1058             =head2 B<query> QUERY
1059              
1060             The C<query> method takes a single parameter: a hash reference which
1061             describes a JIRA query in the same format used by L<JIRA::REST|JIRA::REST>
1062             (essentially, C<< jql => "JQL query string" >>).
1063              
1064             The return value is a single L<JIRA::REST::Class::Query|JIRA::REST::Class::Query> object.
1065              
1066             =head2 B<iterator> QUERY
1067              
1068             The C<query> method takes a single parameter: a hash reference which
1069             describes a JIRA query in the same format used by L<JIRA::REST|JIRA::REST>
1070             (essentially, C<< jql => "JQL query string" >>). It accepts an additional
1071             field, however: C<restart_if_lt_total>. If this field is set to a true value,
1072             the iterator will keep track of the number of results fetched and, if when
1073             the results run out this number doesn't match the number of results
1074             predicted by the query, it will restart the query. This is particularly
1075             useful if you are transforming a number of issues through an iterator, and
1076             the transformation causes the issues to no longer match the query.
1077              
1078             The return value is a single
1079             L<JIRA::REST::Class::Iterator|JIRA::REST::Class::Iterator> object. The
1080             issues returned by the query can be obtained in serial by repeatedly calling
1081             L<next|JIRA::REST::Class::Iterator/next> on this object, which returns a
1082             series of L<JIRA::REST::Class::Issue|JIRA::REST::Class::Issue> objects.
1083              
1084             =head2 B<maxResults>
1085              
1086             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).
1087              
1088             Defaults to 50.
1089              
1090             say $jira->maxResults; # returns 50
1091              
1092             $jira->maxResults(10); # only return 10 results at a time
1093              
1094             =head2 B<issue_types>
1095              
1096             Returns a list of defined issue types (as
1097             L<JIRA::REST::Class::Issue::Type|JIRA::REST::Class::Issue::Type> objects)
1098             for this server.
1099              
1100             =head2 B<projects>
1101              
1102             Returns a list of projects (as
1103             L<JIRA::REST::Class::Project|JIRA::REST::Class::Project> objects) for this
1104             server.
1105              
1106             =head2 B<project> PROJECT_ID || PROJECT_KEY || PROJECT_NAME
1107              
1108             Returns a L<JIRA::REST::Class::Project|JIRA::REST::Class::Project> object
1109             for the project specified. Returns undef if the project doesn't exist.
1110              
1111             =head2 B<SSL_verify_none>
1112              
1113             Sets to false the SSL options C<SSL_verify_mode> and C<verify_hostname> on
1114             the L<LWP::UserAgent|LWP::UserAgent> object that is used by
1115             L<REST::Client|REST::Client> (which, in turn, is used by
1116             L<JIRA::REST|JIRA::REST>, which is used by this module).
1117              
1118             =head2 B<name_for_user>
1119              
1120             When passed a scalar that could be a
1121             L<JIRA::REST::Class::User|JIRA::REST::Class::User> object, returns the name
1122             of the user if it is a C<JIRA::REST::Class::User>
1123             object, or the unmodified scalar if it is not.
1124              
1125             =head2 B<key_for_issue>
1126              
1127             When passed a scalar that could be a
1128             L<JIRA::REST::Class::Issue|JIRA::REST::Class::Issue> object, returns the key
1129             of the issue if it is a C<JIRA::REST::Class::Issue>
1130             object, or the unmodified scalar if it is not.
1131              
1132             =head2 B<find_link_name_and_direction>
1133              
1134             When passed two scalars, one that could be a
1135             L<JIRA::REST::Class::Issue::LinkType|JIRA::REST::Class::Issue::LinkType>
1136             object and another that is a direction (inward/outward), returns the name of
1137             the link type and direction if it is a C<JIRA::REST::Class::Issue::LinkType>
1138             object, or attempts to determine the link type and direction from the
1139             provided scalars.
1140              
1141             =head2 B<dump>
1142              
1143             Returns a stringified representation of the object's data generated somewhat
1144             by L<Data::Dumper::Concise|Data::Dumper::Concise>, but not descending into
1145             any objects that might be part of that data. If it finds objects in the
1146             data, it will attempt to represent them in some abbreviated fashion which
1147             may not display all the data in the object. For instance, if the object has
1148             a C<JIRA::REST::Class::Issue> object in it for an issue with the key
1149             C<'JRC-1'>, the object would be represented as the string C<<
1150             'JIRA::REST::Class::Issue->key(JRC-1)' >>. The goal is to provide a gist of
1151             what the contents of the object are without exhaustively dumping EVERYTHING.
1152             I use it a lot for figuring out what's in the results I'm getting back from
1153             the JIRA API.
1154              
1155             =head1 READ-ONLY ACCESSORS
1156              
1157             =head2 B<args>
1158              
1159             An accessor that returns a copy of the arguments passed to the
1160             constructor. Useful for passing around to utility objects.
1161              
1162             =head2 B<url>
1163              
1164             An accessor that returns the C<url> parameter passed to this object's
1165             constructor.
1166              
1167             =head2 B<username>
1168              
1169             An accessor that returns the username used to connect to the JIRA server,
1170             even if the username was read from a C<.netrc> or
1171             L<Config::Identity|Config::Identity> file.
1172              
1173             =head2 B<user_object>
1174              
1175             An accessor that returns the user used to connect to the JIRA server as a
1176             L<JIRA::REST::Class::User|JIRA::REST::Class::User> object, even if the username
1177             was read from a C<.netrc> or L<Config::Identity|Config::Identity> file. Works
1178             by making the JIRA REST API call L</rest/api/latest/myself|https://docs.atlassian.com/jira/REST/cloud/#api/2/myself>.
1179              
1180             =head2 B<password>
1181              
1182             An accessor that returns the password used to connect to the JIRA server,
1183             even if the username was read from a C<.netrc> or
1184             L<Config::Identity|Config::Identity> file.
1185              
1186             =head2 B<rest_client_config>
1187              
1188             An accessor that returns the C<rest_client_config> parameter passed to this
1189             object's constructor.
1190              
1191             =head2 B<anonymous>
1192              
1193             An accessor that returns the C<anonymous> parameter passed to this object's constructor.
1194              
1195             =head2 B<proxy>
1196              
1197             An accessor that returns the C<proxy> parameter passed to this object's constructor.
1198              
1199             =head1 INTERNAL METHODS
1200              
1201             =head2 B<get>
1202              
1203             A wrapper for C<JIRA::REST>'s L<GET|JIRA::REST/"GET RESOURCE [, QUERY]"> method.
1204              
1205             =head2 B<post>
1206              
1207             Wrapper around C<JIRA::REST>'s L<POST|JIRA::REST/"POST RESOURCE, QUERY, VALUE [, HEADERS]"> method.
1208              
1209             =head2 B<put>
1210              
1211             Wrapper around C<JIRA::REST>'s L<PUT|JIRA::REST/"PUT RESOURCE, QUERY, VALUE [, HEADERS]"> method.
1212              
1213             =head2 B<delete>
1214              
1215             Wrapper around C<JIRA::REST>'s L<DELETE|JIRA::REST/"DELETE RESOURCE [, QUERY]"> method.
1216              
1217             =head2 B<data_upload>
1218              
1219             Similar to
1220             L<< JIRA::REST->attach_files|JIRA::REST/"attach_files ISSUE FILE..." >>,
1221             but entirely from memory and only attaches one file at a time. Returns the
1222             L<HTTP::Response|HTTP::Response> object from the post request. Takes the
1223             following named parameters:
1224              
1225             =over 4
1226              
1227             =item + B<url>
1228              
1229             The relative URL to POST to. This will have the hostname and REST version
1230             information prepended to it, so all you need to provide is something like
1231             C</issue/>I<issueIdOrKey>C</attachments>. I'm allowing the URL to be
1232             specified in case I later discover something this can be used for besides
1233             attaching files to issues.
1234              
1235             =item + B<name>
1236              
1237             The name that is specified for this file attachment.
1238              
1239             =item + B<data>
1240              
1241             The actual data to be uploaded. If a reference is provided, it will be
1242             dereferenced before posting the data.
1243              
1244             =back
1245              
1246             I guess that makes it only a I<little> like
1247             C<< JIRA::REST->attach_files >>...
1248              
1249             =head2 B<rest_api_url_base>
1250              
1251             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>.
1252              
1253             =head2 B<strip_protocol_and_host>
1254              
1255             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.
1256              
1257             =head2 B<jira>
1258              
1259             Returns a L<JIRA::REST::Class|JIRA::REST::Class> object with credentials for the last JIRA user.
1260              
1261             =head2 B<factory>
1262              
1263             An accessor for the L<JIRA::REST::Class::Factory|JIRA::REST::Class::Factory>.
1264              
1265             =head2 B<JIRA_REST>
1266              
1267             An accessor that returns the L<JIRA::REST|JIRA::REST> object being used.
1268              
1269             =head2 B<REST_CLIENT>
1270              
1271             An accessor that returns the L<REST::Client|REST::Client> object inside the L<JIRA::REST|JIRA::REST> object being used.
1272              
1273             =head2 B<JSON>
1274              
1275             An accessor that returns the L<JSON|JSON> object inside the L<JIRA::REST|JIRA::REST> object being used.
1276              
1277             =head2 B<make_object>
1278              
1279             A pass-through method that calls L<JIRA::REST::Class::Factory::make_object()|JIRA::REST::Class::Factory/make_object>.
1280              
1281             =head2 B<make_date>
1282              
1283             A pass-through method that calls L<JIRA::REST::Class::Factory::make_date()|JIRA::REST::Class::Factory/make_date>.
1284              
1285             =head2 B<class_for>
1286              
1287             A pass-through method that calls L<JIRA::REST::Class::Factory::get_factory_class()|JIRA::REST::Class::Factory/get_factory_class>.
1288              
1289             =head2 B<obj_isa>
1290              
1291             When passed a scalar that I<could> be an object and a class string,
1292             returns whether the scalar is, in fact, an object of that class.
1293             Looks up the actual class using C<class_for()>, which calls
1294             L<JIRA::REST::Class::Factory::get_factory_class()|JIRA::REST::Class::Factory/get_factory_class>.
1295              
1296             =head2 B<cosmetic_copy> I<THING>
1297              
1298             A utility function to produce a "cosmetic" copy of a thing: it clones
1299             the data structure, but if anything in the structure (other than the
1300             structure itself) is a blessed object, it replaces it with a
1301             stringification of that object that probably doesn't contain all the
1302             data in the object. For instance, if the object has a
1303             C<JIRA::REST::Class::Issue> object in it for an issue with the key
1304             C<'JRC-1'>, the object would be represented as the string
1305             C<< 'JIRA::REST::Class::Issue->key(JRC-1)' >>. The goal is to provide a
1306             gist of what the contents of the object are without exhaustively dumping
1307             EVERYTHING.
1308              
1309             =head1 RELATED CLASSES
1310              
1311             =over 2
1312              
1313             =item * L<JIRA::REST::Class::Factory|JIRA::REST::Class::Factory>
1314              
1315             =item * L<JIRA::REST::Class::Issue|JIRA::REST::Class::Issue>
1316              
1317             =item * L<JIRA::REST::Class::Issue::Type|JIRA::REST::Class::Issue::Type>
1318              
1319             =item * L<JIRA::REST::Class::Iterator|JIRA::REST::Class::Iterator>
1320              
1321             =item * L<JIRA::REST::Class::Mixins|JIRA::REST::Class::Mixins>
1322              
1323             =item * L<JIRA::REST::Class::Project|JIRA::REST::Class::Project>
1324              
1325             =item * L<JIRA::REST::Class::Query|JIRA::REST::Class::Query>
1326              
1327             =item * L<JIRA::REST::Class::User|JIRA::REST::Class::User>
1328              
1329             =back
1330              
1331             =begin test setup 1
1332              
1333             use File::Basename;
1334             use lib dirname($0).'/..';
1335             use MyTest;
1336             use 5.010;
1337              
1338             TestServer_setup();
1339              
1340             END {
1341             TestServer_stop();
1342             }
1343              
1344             use_ok('JIRA::REST::Class');
1345              
1346             sub get_test_client {
1347             state $test =
1348             JIRA::REST::Class->new(TestServer_url(), 'username', 'password');
1349             $test->REST_CLIENT->setTimeout(5);
1350             return $test;
1351             };
1352              
1353             =end test
1354              
1355             =begin testing new 5
1356              
1357             my $jira;
1358             try {
1359             $jira = JIRA::REST::Class->new({
1360             url => TestServer_url(),
1361             username => 'user',
1362             password => 'pass',
1363             proxy => '',
1364             anonymous => 0,
1365             ssl_verify_none => 1,
1366             rest_client_config => {},
1367             });
1368             }
1369             catch {
1370             $jira = $_; # as good a place as any to stash the error, because
1371             # isa_ok() will complain that it's not an object.
1372             };
1373              
1374             isa_ok($jira, 'JIRA::REST::Class', 'JIRA::REST::Class->new');
1375              
1376             my $needs_url_regexp = qr/'?url'? argument must be defined/i;
1377              
1378             throws_ok(
1379             sub {
1380             JIRA::REST::Class->new();
1381             },
1382             $needs_url_regexp,
1383             'JIRA::REST::Class->new with no parameters throws an exception',
1384             );
1385              
1386             throws_ok(
1387             sub {
1388             JIRA::REST::Class->new({
1389             username => 'user',
1390             password => 'pass',
1391             });
1392             },
1393             $needs_url_regexp,
1394             'JIRA::REST::Class->new with no url throws an exception',
1395             );
1396              
1397             throws_ok(
1398             sub {
1399             JIRA::REST::Class->new('http://not.a.good.server.com');
1400             },
1401             qr/No credentials found/,
1402             q{JIRA::REST::Class->new with just url tries to find credentials},
1403             );
1404              
1405             lives_ok(
1406             sub {
1407             JIRA::REST::Class->new(TestServer_url(), 'user', 'pass');
1408             },
1409             q{JIRA::REST::Class->new with url, username, and password does't croak!},
1410             );
1411              
1412             =end testing
1413              
1414             =begin testing get
1415              
1416             validate_wrapper_method( sub { get_test_client()->get('/test'); },
1417             { GET => 'SUCCESS' }, 'get() method works' );
1418              
1419              
1420             =end testing
1421              
1422             =begin testing post
1423              
1424             validate_wrapper_method( sub { get_test_client()->post('/test', "key=value"); },
1425             { POST => 'SUCCESS' }, 'post() method works' );
1426              
1427              
1428             =end testing
1429              
1430             =begin testing put
1431              
1432             validate_wrapper_method( sub { get_test_client()->put('/test', "key=value"); },
1433             { PUT => 'SUCCESS' }, 'put() method works' );
1434              
1435              
1436             =end testing
1437              
1438             =begin testing delete
1439              
1440             validate_wrapper_method( sub { get_test_client()->delete('/test'); },
1441             { DELETE => 'SUCCESS' }, 'delete() method works' );
1442              
1443              
1444             =end testing
1445              
1446             =begin testing data_upload
1447              
1448             my $expected = {
1449             "Content-Disposition" => "form-data; name=\"file\"; filename=\"file.txt\"",
1450             POST => "SUCCESS",
1451             data => "An OO Class module built atop L<JIRA::REST|JIRA::REST> for dealing with "
1452             . "JIRA issues and their data as objects.",
1453             name => "file.txt"
1454             };
1455              
1456             my $test1_name = 'return value from data_upload()';
1457             my $test2_name = 'data_upload() method succeeded';
1458             my $test3_name = 'data_upload() method returned expected data';
1459              
1460             my $test = get_test_client();
1461             my $results;
1462             my $got;
1463              
1464             try {
1465             $results = $test->data_upload({
1466             url => "/data_upload",
1467             name => $expected->{name},
1468             data => $expected->{data},
1469             });
1470             $got = $test->JSON->decode($results->decoded_content);
1471             }
1472             catch {
1473             $results = $_;
1474             diag($test->REST_CLIENT->getHost);
1475             };
1476              
1477             my $test1_ok = isa_ok( $results, 'HTTP::Response', $test1_name );
1478             my $test2_ok = ok($test1_ok && $results->is_success, $test2_name );
1479             $test2_ok ? is_deeply( $got, $expected, $test3_name ) : fail( $test3_name );
1480              
1481             =end testing
1482              
1483             =begin testing issue_types 16
1484              
1485             try {
1486             my $test = get_test_client();
1487              
1488             validate_contextual_accessor($test, {
1489             method => 'issue_types',
1490             class => 'issuetype',
1491             data => [ sort qw/ Bug Epic Improvement Sub-task Story Task /,
1492             'New Feature' ],
1493             });
1494              
1495             print "#\n# Checking the 'Bug' issue type\n#\n";
1496              
1497             my ($bug) = sort $test->issue_types;
1498              
1499             can_ok_abstract( $bug, qw/ description iconUrl id name self subtask / );
1500              
1501             my $host = TestServer_url();
1502              
1503             validate_expected_fields( $bug, {
1504             description => "jira.translation.issuetype.bug.name.desc",
1505             iconUrl => "$host/secure/viewavatar?size=xsmall&avatarId=10303"
1506             . "&avatarType=issuetype",
1507             id => 10004,
1508             name => "Bug",
1509             self => "$host/rest/api/latest/issuetype/10004",
1510             subtask => JSON::PP::false,
1511             });
1512             };
1513              
1514             =end testing
1515              
1516             =begin testing projects 5
1517              
1518             my $test = get_test_client();
1519              
1520             try {
1521             validate_contextual_accessor($test, {
1522             method => 'projects',
1523             class => 'project',
1524             data => [ qw/ JRC KANBAN PACKAY PM SCRUM / ],
1525             });
1526             };
1527              
1528             =end testing
1529              
1530             =begin testing project 17
1531              
1532             try {
1533             print "#\n# Checking the SCRUM project\n#\n";
1534              
1535             my $test = get_test_client();
1536              
1537             my $proj = $test->project('SCRUM');
1538              
1539             can_ok_abstract( $proj, qw/ avatarUrls expand id key name self
1540             category assigneeType components
1541             description issueTypes lead roles versions
1542             allowed_components allowed_versions
1543             allowed_fix_versions allowed_issue_types
1544             allowed_priorities allowed_field_values
1545             field_metadata_exists field_metadata
1546             field_name
1547             / );
1548              
1549             validate_expected_fields( $proj, {
1550             expand => "description,lead,url,projectKeys",
1551             id => 10002,
1552             key => 'SCRUM',
1553             name => "Scrum Software Development Sample Project",
1554             projectTypeKey => "software",
1555             lead => {
1556             class => 'user',
1557             expected => {
1558             key => 'packy'
1559             },
1560             },
1561             });
1562              
1563             validate_contextual_accessor($proj, {
1564             method => 'versions',
1565             class => 'projectvers',
1566             name => "SCRUM project's",
1567             data => [ "Version 1.0", "Version 2.0", "Version 3.0" ],
1568             });
1569             };
1570              
1571             =end testing
1572              
1573             =begin testing parameter_accessors 15
1574              
1575             try {
1576             print "#\n# Checking parameter accessors\n#\n";
1577              
1578             my $test = get_test_client();
1579             my $url = TestServer_url();
1580              
1581             my $args = {
1582             url => $url,
1583             username => 'username',
1584             password => 'password',
1585             proxy => undef,
1586             anonymous => undef,
1587             rest_client_config => undef,
1588             ssl_verify_none => undef,
1589             };
1590              
1591             # the args accessor will have keys for ALL the possible arguments,
1592             # whether they were passed in or not.
1593              
1594             cmp_deeply( $test,
1595             methods( args => { %$args, },
1596             url => $args->{url},
1597             username => $args->{username},
1598             password => $args->{password},
1599             proxy => $args->{proxy},
1600             anonymous => $args->{anonymous},
1601             rest_client_config => $args->{rest_client_config} ),
1602             q{All accessors for parameters passed }.
1603             q{into the constructor okay});
1604              
1605             my $ua = $test->REST_CLIENT->getUseragent();
1606             $test->SSL_verify_none;
1607             cmp_deeply($ua->{ssl_opts}, { SSL_verify_mode => 0, verify_hostname => 0 },
1608             q{SSL_verify_none() does disable SSL verification});
1609              
1610             is($test->rest_api_url_base($url . "/rest/api/latest/foo"),
1611             $url . "/rest/api/latest", q{rest_api_url_base() works as expected});
1612              
1613             is($test->strip_protocol_and_host($test->REST_CLIENT->getHost . "/foo"),
1614             "/foo", q{strip_protocol_and_host() works as expected});
1615              
1616             is($test->maxResults, 50, q{maxResults() default is correct});
1617              
1618             is($test->maxResults(10), 10, q{maxResults(N) returns N});
1619              
1620             is($test->maxResults, 10,
1621             q{maxResults() was successfully set by previous call});
1622              
1623             print "# testing user_object() accessor\n";
1624             my $userobj = $test->user_object();
1625              
1626             validate_expected_fields( $userobj, {
1627             key => 'packy',
1628             name => 'packy',
1629             displayName => "Packy Anderson",
1630             emailAddress => 'packy\@cpan.org',
1631             });
1632              
1633             };
1634              
1635             =end testing
1636              
1637             =head1 SEE ALSO
1638              
1639             =over
1640              
1641             =item * L<JIRA::REST|JIRA::REST>
1642              
1643             C<JIRA::REST::Class> uses C<JIRA::REST> to perform all its interaction with JIRA.
1644              
1645             =item * L<REST::Client|REST::Client>
1646              
1647             C<JIRA::REST> uses a C<REST::Client> object to perform its low-level interactions.
1648              
1649             =item * L<JIRA REST API Reference|https://docs.atlassian.com/jira/REST/latest/>
1650              
1651             Atlassian's official JIRA REST API Reference.
1652              
1653             =back
1654              
1655             =head1 REPOSITORY
1656              
1657             L<https://github.com/packy/JIRA-REST-Class|https://github.com/packy/JIRA-REST-Class>
1658              
1659             =head1 CREDITS
1660              
1661             =over 4
1662              
1663             =item L<Gustavo Leite de Mendonça Chaves|https://metacpan.org/author/GNUSTAVO> <gnustavo@cpan.org>
1664              
1665             Many thanks to Gustavo for L<JIRA::REST|JIRA::REST>, which is what I started
1666             working with when I first wanted to automate my interactions with JIRA in
1667             the summer of 2016, and without which I would have had a LOT of work to do.
1668              
1669             =back
1670              
1671             =head1 AUTHOR
1672              
1673             Packy Anderson <packy@cpan.org>
1674              
1675             =head1 CONTRIBUTORS
1676              
1677             =for stopwords Alexandr Ciornii Alexey Melezhik Manni Heumann
1678              
1679             =over 4
1680              
1681             =item *
1682              
1683             Alexandr Ciornii <alexchorny@gmail.com>
1684              
1685             =item *
1686              
1687             Alexey Melezhik <melezhik@gmail.com>
1688              
1689             =item *
1690              
1691             Manni Heumann <github@lxxi.org>
1692              
1693             =back
1694              
1695             =head1 COPYRIGHT AND LICENSE
1696              
1697             This software is Copyright (c) 2017 by Packy Anderson.
1698              
1699             This is free software, licensed under:
1700              
1701             The Artistic License 2.0 (GPL Compatible)
1702              
1703             =cut