File Coverage

lib/JIRA/REST/Class.pm
Criterion Covered Total %
statement 100 115 86.9
branch 17 24 70.8
condition 4 13 30.7
subroutine 30 33 90.9
pod 23 23 100.0
total 174 208 83.6


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