File Coverage

lib/JIRA/REST/Class/Mixins.pm
Criterion Covered Total %
statement 139 178 78.0
branch 47 90 52.2
condition 11 24 45.8
subroutine 31 37 83.7
pod 14 14 100.0
total 242 343 70.5


line stmt bran cond sub pod time code
1             package JIRA::REST::Class::Mixins;
2 4     4   87114 use strict;
  4         11  
  4         99  
3 4     4   18 use warnings;
  4         6  
  4         76  
4 4     4   55 use 5.010;
  4         10  
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 mixin class for L<JIRA::REST::Class|JIRA::REST::Class> that other objects can inherit methods from.
12              
13 4     4   19 use Carp;
  4         4  
  4         192  
14 4     4   23 use Clone::Any qw( clone );
  4         6  
  4         24  
15 4     4   1021 use Data::Dumper::Concise;
  4         4838  
  4         209  
16 4     4   250 use MIME::Base64;
  4         399  
  4         202  
17 4     4   324 use Readonly 2.04;
  4         2787  
  4         157  
18 4     4   20 use Scalar::Util qw( blessed reftype );
  4         5  
  4         153  
19 4     4   292 use Try::Tiny;
  4         1429  
  4         6045  
20              
21             sub jira {
22 78     78 1 1664 my $self = shift;
23 78         94 my $args = shift;
24 78 100       139 my $class = ref $self ? ref( $self ) : $self;
25              
26 78 100       175 if ( blessed $self ) {
27              
28             # if we have an object, return it!
29 41 100       104 return $self->{jira} if $self->{jira};
30              
31 36 0 33     71 if ( !$args && $self->{args} ) {
32 0         0 $args = $self->{args};
33             }
34              
35             # if we have arguments, call ourself using
36             # the class name with those args, and cache the result
37 36 50       68 if ( $args ) {
38 36         59 $self->{jira} = $class->jira( $args );
39 36         91 $self->{jira_rest} = $self->{jira}->{jira_rest};
40 36         99 return $self->{jira};
41             }
42             }
43              
44             # called with just the class name
45 37         127 return JIRA::REST::Class->new( $args );
46             }
47              
48             #---------------------------------------------------------------------------
49              
50             #pod =begin test setup
51             #pod
52             #pod BEGIN {
53             #pod use File::Basename;
54             #pod use lib dirname($0).'/../lib';
55             #pod
56             #pod use InlineTest;
57             #pod use Clone::Any qw( clone );
58             #pod use Scalar::Util qw(refaddr);
59             #pod
60             #pod use_ok('JIRA::REST::Class::Mixins');
61             #pod use_ok('JIRA::REST::Class::Factory');
62             #pod use_ok('JIRA::REST::Class::FactoryTypes', qw( %TYPES ));
63             #pod }
64             #pod
65             #pod =end test
66             #pod
67             #pod =begin testing constructor 3
68             #pod
69             #pod my $jira = JIRA::REST::Class::Mixins->jira(InlineTest->constructor_args);
70             #pod isa_ok($jira, $TYPES{class}, 'Mixins->jira');
71             #pod isa_ok($jira->JIRA_REST, 'JIRA::REST', 'JIRA::REST::Class->JIRA_REST');
72             #pod isa_ok($jira->REST_CLIENT, 'REST::Client', 'JIRA::REST::Class->REST_CLIENT');
73             #pod
74             #pod =end testing
75             #pod
76             #pod =cut
77              
78             #---------------------------------------------------------------------------
79              
80             sub factory {
81 150     150 1 55180 my $self = shift;
82 150         205 my $args = shift;
83 150 100       326 my $class = ref $self ? ref( $self ) : $self;
84              
85 150 100       375 if ( blessed $self ) {
86              
87             # if we have a factory, return it!
88 70 100       346 if ( $self->{factory} ) {
89 34         118 return $self->{factory};
90             }
91              
92             # if we have arguments, call ourself using
93             # the class name with those args, and cache the result
94 36 50       69 if ( $args ) {
95 36         62 $self->{factory} = $class->factory( $args );
96 36         95 return $self->{factory};
97             }
98             }
99              
100             # called with just the class name
101 80         302 return JIRA::REST::Class::Factory->new( 'factory', { args => $args } );
102             }
103              
104             #---------------------------------------------------------------------------
105              
106             #pod =begin test setup
107             #pod
108             #pod sub get_factory {
109             #pod JIRA::REST::Class::Mixins->factory(InlineTest->constructor_args);
110             #pod }
111             #pod
112             #pod =end test
113             #pod
114             #pod =begin testing factory 2
115             #pod
116             #pod my $factory = get_factory();
117             #pod isa_ok($factory, $TYPES{factory}, 'Mixins->factory');
118             #pod ok(JIRA::REST::Class::Mixins->obj_isa($factory, 'factory'),
119             #pod 'Mixins->obj_isa works');
120             #pod
121             #pod =end testing
122             #pod
123             #pod =cut
124              
125             #---------------------------------------------------------------------------
126              
127             sub JIRA_REST { ## no critic (Capitalization)
128 80     80 1 2403 my $self = shift;
129 80         112 my $args = shift;
130 80 100       165 my $class = ref $self ? ref( $self ) : $self;
131              
132 80 100       253 if ( blessed $self ) {
133              
134             # method called on a class object
135              
136             # if we have a copy of the JIRA::REST object, return it!
137 36 50       497 return $self->{jira_rest} if $self->{jira_rest};
138              
139             # if we have arguments, call ourself using
140             # the class name with those args, and cache the result
141 0 0       0 return $self->{jira_rest} = $class->JIRA_REST( $args )
142             if $args;
143             }
144              
145             # called with just the class name
146              
147 44 50       92 if ( _JIRA_REST_version_has_named_parameters() ) {
148 44         169 return JIRA::REST->new( $args );
149             }
150              
151             # still support the old style arguments for JIRA::REST
152             my $jira_rest = JIRA::REST->new(
153             $args->{url}, $args->{username},
154             $args->{password}, $args->{rest_client_config}
155 0         0 );
156              
157 0         0 my $rest = $jira_rest->{rest};
158 0         0 my $ua = $rest->getUseragent;
159             $ua->ssl_opts( SSL_verify_mode => 0, verify_hostname => 0 )
160 0 0       0 if $args->{ssl_verify_none};
161              
162 0         0 return $jira_rest;
163             }
164              
165             ## no critic (Subroutines::ProhibitUnusedPrivateSubroutines)
166             ## these are private to the whole module, not just this package
167             sub _JIRA_REST_version { ## no critic (Capitalization)
168 4     4   11 my $version = shift;
169 4         10 my $has_version;
170             try {
171             # we don't want SIGDIE taking us someplace
172             # if VERSION throws an exception
173 4     4   122 local $SIG{__DIE__} = undef;
174              
175 4   33     110 $has_version = JIRA::REST->VERSION && JIRA::REST->VERSION( $version );
176 4         44 };
177 4         60 return $has_version;
178             }
179              
180             sub _JIRA_REST_version_has_named_parameters { ## no critic (Capitalization)
181             ## no critic (ProhibitMagicNumbers)
182 44     44   70 state $retval = _JIRA_REST_version( 0.016 );
183             ## use critic
184 44         122 return $retval;
185             }
186              
187             sub _JIRA_REST_version_has_separate_path { ## no critic (Capitalization)
188             ## no critic (ProhibitMagicNumbers)
189 2     2   6 state $retval = _JIRA_REST_version( 0.015 );
190             ## use critic
191 2         6 return $retval;
192             }
193             ## use critic
194              
195             ## no critic (Capitalization)
196 20     20 1 96125 sub REST_CLIENT { return shift->JIRA_REST->{rest} }
197 1     1 1 15 sub JSON { return shift->JIRA_REST->{json} }
198             ## use critic
199 31     31 1 83 sub make_object { return shift->factory->make_object( @_ ) }
200 0     0 1 0 sub make_date { return shift->factory->make_date( @_ ) }
201 1     1 1 3 sub class_for { return shift->factory->get_factory_class( @_ ) }
202              
203             sub obj_isa {
204 2     2 1 278 my ( $self, $obj, $type ) = @_;
205 2 100       15 return unless blessed $obj;
206 1         3 my $class = $self->class_for( $type );
207 1         11 return $obj->isa( $class );
208             }
209              
210             sub name_for_user {
211 0     0 1 0 my ( $self, $user ) = @_;
212 0 0       0 return $self->obj_isa( $user, 'user' ) ? $user->name : $user;
213             }
214              
215             sub key_for_issue {
216 0     0 1 0 my ( $self, $issue ) = @_;
217 0 0       0 return $self->obj_isa( $issue, 'issue' ) ? $issue->key : $issue;
218             }
219              
220             sub find_link_name_and_direction {
221 0     0 1 0 my ( $self, $link, $dir ) = @_;
222              
223 0 0       0 return unless defined $link;
224              
225             # determine the link directon, if provided. defaults to inward.
226 0 0 0     0 $dir = ( $dir && $dir =~ /out(?:ward)?/x ) ? 'outward' : 'inward';
227              
228             # if we were passed a link type object, return
229             # the name and the direction we were given
230 0 0       0 if ( $self->obj_isa( $link, 'linktype' ) ) {
231 0         0 return $link->name, $dir;
232             }
233              
234             # search through the link types
235             # work in progress
236             # my @types = $self->jira->link_types;
237             # foreach my $type ( @types ) {
238             # if (lc $link eq lc $type->inward) {
239             # return $type->name, 'inward';
240             # }
241             # if (lc $link eq lc $type->outward) {
242             # return $type->name, 'outward';
243             # }
244             # if (lc $link eq lc $type->name) {
245             # return $type->name, $dir;
246             # }
247             # }
248              
249             # we didn't find anything, so just return what we were passed
250 0         0 return $link, $dir;
251             }
252              
253             ###########################################################################
254              
255             sub dump { ## no critic (ProhibitBuiltinHomonyms)
256 0     0 1 0 my ( $self, @args ) = @_;
257 0         0 my $result;
258 0 0       0 if ( @args ) {
259 0         0 $result = $self->cosmetic_copy( @args );
260             }
261             else {
262 0         0 $result = $self->cosmetic_copy( $self );
263             }
264 0 0       0 return ref( $result ) ? Dumper( $result ) : $result;
265             }
266              
267             sub cosmetic_copy {
268 2     2 1 888 shift; # we don't need $self
269 2         5 return __cosmetic_copy( @_, 'top' );
270             }
271              
272             #---------------------------------------------------------------------------
273              
274             #pod =begin testing cosmetic_copy 3
275             #pod
276             #pod my @PROJ = InlineTest->project_data;
277             #pod my $orig = [ @PROJ ];
278             #pod my $copy = JIRA::REST::Class::Mixins->cosmetic_copy($orig);
279             #pod
280             #pod is_deeply( $orig, $copy, "simple cosmetic copy has same content as original" );
281             #pod
282             #pod cmp_ok( refaddr($orig), 'ne', refaddr($copy),
283             #pod "simple cosmetic copy has different address as original" );
284             #pod
285             #pod # make a complex reference to copy
286             #pod my $factory = get_factory();
287             #pod $orig = [ map { $factory->make_object('project', { data => $_ }) } @PROJ ];
288             #pod $copy = JIRA::REST::Class::Mixins->cosmetic_copy($orig);
289             #pod
290             #pod is_deeply( $copy, [
291             #pod "JIRA::REST::Class::Project->name(JIRA::REST::Class)",
292             #pod "JIRA::REST::Class::Project->name(Kanban software development sample project)",
293             #pod "JIRA::REST::Class::Project->name(PacKay Productions)",
294             #pod "JIRA::REST::Class::Project->name(Project Management Sample Project)",
295             #pod "JIRA::REST::Class::Project->name(Scrum Software Development Sample Project)"
296             #pod ], "complex cosmetic copy is properly serialized");
297             #pod
298             #pod =end testing
299             #pod
300             #pod =cut
301              
302             #---------------------------------------------------------------------------
303              
304             sub __cosmetic_copy {
305 92     92   99 my $thing = shift;
306 92         96 my $top = pop;
307              
308 92 100       133 if ( not ref $thing ) {
309 70         152 return $thing;
310             }
311              
312 22     0   44 my $hash_copy = sub { };
313              
314 22 100       48 if ( my $class = blessed $thing ) {
315 5 50       9 if ( $class eq 'JSON::PP::Boolean' ) {
316 0 0       0 return $thing ? 'JSON::PP::true' : 'JSON::PP::false';
317             }
318 5 50       32 if ( $class eq 'JSON' ) {
319 0         0 return "$thing";
320             }
321 5 50       7 if ( $class eq 'REST::Client' ) {
322 0         0 return '%s->host(%s)', $class, $thing->getHost;
323             }
324 5 50       8 if ( $class eq 'DateTime' ) {
325 0         0 return "DateTime( $thing )";
326             }
327 5 50       9 if ( $top ) {
328 0 0       0 if ( reftype $thing eq 'ARRAY' ) {
329 0         0 chomp( my $data = Dumper( __array_copy( $thing ) ) );
330 0         0 return "bless( $data => $class )";
331             }
332 0 0       0 if ( reftype $thing eq 'HASH' ) {
333 0         0 chomp( my $data = Dumper( __hash_copy( $thing ) ) );
334 0         0 return "bless( $data => $class )";
335             }
336 0         0 return Dumper( $thing );
337             }
338             else {
339 5         6 my $fallback;
340              
341             # see if the object has any of these methods
342 5         7 foreach my $method ( qw/ name key id / ) {
343 5 50       17 if ( $thing->can( $method ) ) {
344 5         12 my $value = $thing->$method;
345              
346             # if the method returned a value, great!
347 5 50       37 return sprintf '%s->%s(%s)', $class, $method, $value
348             if defined $value;
349              
350             # we can use it as a stringification if we have to
351 0   0     0 $fallback //= sprintf '%s->%s(undef)', $class, $method;
352             }
353             }
354              
355             # fall back to either a $class->$method(undef)
356             # or the default stringification
357 0 0       0 return $fallback ? $fallback : "$thing";
358             }
359             }
360              
361 17 50       40 if ( ref $thing eq 'SCALAR' ) {
    100          
    50          
362 0         0 return $$thing;
363             }
364             elsif ( ref $thing eq 'ARRAY' ) {
365 2         4 return __array_copy( $thing );
366             }
367             elsif ( ref $thing eq 'HASH' ) {
368 15         21 return __hash_copy( $thing );
369             }
370 0         0 return $thing;
371             }
372              
373             sub __array_copy {
374 2     2   3 my $thing = shift;
375 2         4 return [ map { __cosmetic_copy( $_ ) } @$thing ];
  10         18  
376             }
377              
378             sub __hash_copy {
379 15     15   17 my $thing = shift;
380 15         33 return +{ map { $_ => __cosmetic_copy( $thing->{$_} ) } keys %$thing };
  80         104  
381             }
382              
383             ###########################################################################
384             #
385             # internal helper functions
386              
387             # accepts a reference to an array and a list of known arguments.
388             #
389             # + if the array has a single element and it's a hashref, it moves
390             # elements based on the argument list from that hashref into a
391             # result hashref and then complains if there are elements in the
392             # first hashref left over.
393             #
394             # + if the array has multiple elements, it assigns the elements to
395             # the result hashref in the order of the argument list, and
396             # complains if the array has more elements than there are arguments.
397             #
398             # In either case, the result hashref is returned.
399              
400             ## no critic (Subroutines::ProhibitUnusedPrivateSubroutines)
401             sub _get_known_args {
402             ## use critic
403 50     50   4934 my ( $self, $in, @args ) = @_;
404 50         91 my $out = {};
405              
406             # get the package->name of the sub that called US
407 50         193 my $sub = $self->__subname( caller 1 );
408              
409             # if we croak, croak from the perspective of our CALLER's caller
410 50         136 local $Carp::CarpLevel = $Carp::CarpLevel + 2;
411              
412             # $in is an arrayref with a single hashref in it
413 50 100 100     351 if ( @$in == 1 && ref $in->[0] && ref $in->[0] eq 'HASH' ) {
      66        
414              
415             # copy that hashref into $in
416 42         905 $in = clone( $in->[0] );
417              
418             # moving arguments using the semi-magical hash reference slice
419 42         95 @{$out}{@args} = delete @{$in}{@args};
  42         157  
  42         141  
420              
421             # if there are leftover keys
422 42 100       132 if ( keys %$in ) {
423 1 50       5 my $arguments = 'argument' . ( keys %$in == 1 ? q{} : q{s} );
424              
425 1         21 croak "$sub: unknown $arguments - "
426             . $self->_quoted_list( sort keys %$in );
427             }
428             }
429             else {
430             # if there aren't more arguments than we have names for
431 8 100       31 if ( @$in <= @args ) {
432              
433             # copy arguments positionally
434 6         16 @{$out}{@args} = @$in;
  6         46  
435             }
436             else {
437 2         3 my $got = scalar @$in;
438 2         3 my $max = scalar @args;
439 2         5 my $list = $self->_quoted_list( @args );
440              
441 2         24 croak "$sub: too many arguments - got $got, max $max ($list)";
442             }
443             }
444              
445 47         176 return $out;
446             }
447              
448             #---------------------------------------------------------------------------
449              
450             #pod =begin testing _get_known_args 5
451             #pod
452             #pod package InlineTestMixins;
453             #pod use Test::Exception;
454             #pod use Test::More;
455             #pod
456             #pod sub test_too_many_args {
457             #pod JIRA::REST::Class::Mixins->_get_known_args(
458             #pod [ qw/ url username password rest_client_config proxy
459             #pod ssl_verify_none anonymous unknown1 unknown2 / ],
460             #pod qw/ url username password rest_client_config proxy
461             #pod ssl_verify_none anonymous/
462             #pod );
463             #pod }
464             #pod
465             #pod # also excercizes __subname()
466             #pod
467             #pod throws_ok( sub { test_too_many_args() },
468             #pod qr/^InlineTestMixins->test_too_many_args:/,
469             #pod '_get_known_args constructs caller string okay' );
470             #pod
471             #pod throws_ok( sub { test_too_many_args() },
472             #pod qr/too many arguments/,
473             #pod '_get_known_args catches too many args okay' );
474             #pod
475             #pod sub test_unknown_args {
476             #pod JIRA::REST::Class::Mixins->_get_known_args(
477             #pod [ { map { $_ => $_ } qw/ url username password
478             #pod rest_client_config proxy
479             #pod ssl_verify_none anonymous
480             #pod unknown1 unknown2 / } ],
481             #pod qw/ url username password rest_client_config proxy
482             #pod ssl_verify_none anonymous /
483             #pod );
484             #pod }
485             #pod
486             #pod # also excercizes _quoted_list()
487             #pod
488             #pod throws_ok( sub { test_unknown_args() },
489             #pod qr/unknown arguments - 'unknown1', 'unknown2'/,
490             #pod '_get_known_args catches unknown args okay' );
491             #pod
492             #pod my %expected = (
493             #pod map { $_ => $_ } qw/ url username password
494             #pod rest_client_config proxy
495             #pod ssl_verify_none anonymous /
496             #pod );
497             #pod
498             #pod sub test_positional_args {
499             #pod JIRA::REST::Class::Mixins->_get_known_args(
500             #pod [ qw/ url username password rest_client_config proxy
501             #pod ssl_verify_none anonymous / ],
502             #pod qw/ url username password rest_client_config proxy
503             #pod ssl_verify_none anonymous /
504             #pod );
505             #pod }
506             #pod
507             #pod is_deeply( test_positional_args(), \%expected,
508             #pod '_get_known_args processes positional args okay' );
509             #pod
510             #pod sub test_named_args {
511             #pod JIRA::REST::Class::Mixins->_get_known_args(
512             #pod [ { map { $_ => $_ } qw/ url username password
513             #pod rest_client_config proxy
514             #pod ssl_verify_none anonymous / } ],
515             #pod qw/ url username password rest_client_config proxy
516             #pod ssl_verify_none anonymous /
517             #pod );
518             #pod }
519             #pod
520             #pod is_deeply( test_named_args(), \%expected,
521             #pod '_get_known_args processes named args okay' );
522             #pod
523             #pod =end testing
524             #pod
525             #pod =cut
526              
527             #---------------------------------------------------------------------------
528              
529             # accepts a hashref and a list of required arguments
530              
531             ## no critic (Subroutines::ProhibitUnusedPrivateSubroutines)
532             sub _check_required_args {
533             ## use critic
534 2     2   1134 my ( $self, $args, @args ) = @_;
535              
536 2         11 while ( my ( $arg, $err ) = splice @args, 0, 2 ) {
537             next
538             if exists $args->{$arg}
539             && defined $args->{$arg}
540 4 50 66     24 && length $args->{$arg};
      66        
541              
542             # get the package->name of the sub that called US
543 1         6 my $sub = $self->__subname( caller 1 );
544              
545             # croak from the perspective of our CALLER's caller
546 1         3 local $Carp::CarpLevel = $Carp::CarpLevel + 2;
547              
548 1         11 croak "$sub: " . $err;
549             }
550 1         3 return;
551             }
552              
553             #---------------------------------------------------------------------------
554              
555             #pod =begin testing _check_required_args 1
556             #pod
557             #pod use Test::Exception;
558             #pod use Test::More;
559             #pod
560             #pod sub test_missing_req_args {
561             #pod my %args = map { $_ => $_ } qw/ username password /;
562             #pod JIRA::REST::Class::Mixins->_check_required_args(
563             #pod \%args,
564             #pod url => "you must specify a URL to connect to",
565             #pod );
566             #pod }
567             #pod
568             #pod throws_ok( sub { test_missing_req_args() },
569             #pod qr/you must specify a URL to connect to/,
570             #pod '_check_required_args identifies missing args okay' );
571             #pod
572             #pod =end testing
573             #pod
574             #pod =cut
575              
576             #---------------------------------------------------------------------------
577              
578             # internal function so I don't have to build a "Package->subroutine:" prefix
579             # whenever I want to croak
580              
581             ## no critic (Subroutines::ProhibitUnusedPrivateSubroutines)
582             sub _croakmsg {
583             ## use critic
584 2     2   1798 my ( $self, $msg, @args ) = @_;
585 2 100       9 my $args = @args ? q{(} . join( q{, }, @args ) . q{)} : q{};
586              
587             # get the package->name of the sub that called US
588 2         11 my $sub = $self->__subname( caller 1 );
589              
590 2         12 return join q{ }, "$sub$args:", $msg;
591             }
592              
593             #---------------------------------------------------------------------------
594              
595             #pod =begin testing _croakmsg 2
596             #pod
597             #pod package InlineTestMixins;
598             #pod use Test::More;
599             #pod
600             #pod sub test_croakmsg_noargs {
601             #pod JIRA::REST::Class::Mixins->_croakmsg("I died");
602             #pod }
603             #pod
604             #pod # also excercizes __subname()
605             #pod
606             #pod is( test_croakmsg_noargs(),
607             #pod 'InlineTestMixins->test_croakmsg_noargs: I died',
608             #pod '_croakmsg constructs no argument string okay' );
609             #pod
610             #pod sub test_croakmsg_args {
611             #pod JIRA::REST::Class::Mixins->_croakmsg("I died", qw/ arg1 arg2 /);
612             #pod }
613             #pod
614             #pod is( test_croakmsg_args(),
615             #pod 'InlineTestMixins->test_croakmsg_args(arg1, arg2): I died',
616             #pod '_croakmsg constructs argument string okay' );
617             #pod
618             #pod =end testing
619             #pod
620             #pod =cut
621              
622             #---------------------------------------------------------------------------
623              
624             #
625             # __PACKAGE__->_quoted_list(qw/ a b c /) returns q/'a', 'b', 'c'/
626             #
627             sub _quoted_list {
628 3     3   6 my $self = shift;
629 3         19 return q{'} . join( q{', '}, @_ ) . q{'};
630             }
631              
632             #
633             # arguments provided by caller(n)
634             # __PACKAGE__->__subname('Some::Pkg', 'filename', lineno, 'Some::Pkg::subname')
635             # returns 'Some::Pkg->subname'
636             #
637             sub __subname {
638 53     53   985 my ( $self, @caller_n ) = @_;
639 53         1211 Readonly my $OUR_CALLERS_CALLER => 3;
640 53         5374 ( my $sub = $caller_n[$OUR_CALLERS_CALLER] ) =~ s/(.*)::([^:]+)$/$1->$2/xs;
641 53         749 return $sub;
642             }
643              
644             # put a reference to JIRA::REST::Class::Abstract here for related classes
645              
646             1;
647              
648             __END__
649              
650             =pod
651              
652             =encoding UTF-8
653              
654             =for :stopwords Packy Anderson Alexandr Alexey Ciornii Heumann Manni Melezhik jira JRC
655             Atlassian GreenHopper ScriptRunner TODO aggregateprogress
656             aggregatetimeestimate aggregatetimeoriginalestimate assigneeType avatar
657             avatarUrls completeDate displayName duedate emailAddress endDate fieldtype
658             fixVersions fromString genericized iconUrl isAssigneeTypeValid issueTypes
659             issuekeys issuelinks issuetype jql lastViewed maxResults originalEstimate
660             originalEstimateSeconds parentkey projectId rapidViewId remainingEstimate
661             remainingEstimateSeconds resolutiondate sprintlist startDate
662             subtaskIssueTypes timeSpent timeSpentSeconds timeestimate
663             timeoriginalestimate timespent timetracking toString updateAuthor worklog
664             workratio
665              
666             =head1 NAME
667              
668             JIRA::REST::Class::Mixins - An mixin class for L<JIRA::REST::Class|JIRA::REST::Class> that other objects can inherit methods from.
669              
670             =head1 VERSION
671              
672             version 0.12
673              
674             =head1 METHODS
675              
676             =head2 B<name_for_user>
677              
678             When passed a scalar that could be a
679             L<JIRA::REST::Class::User|JIRA::REST::Class::User> object, returns the name
680             of the user if it is a C<JIRA::REST::Class::User>
681             object, or the unmodified scalar if it is not.
682              
683             =head2 B<key_for_issue>
684              
685             When passed a scalar that could be a
686             L<JIRA::REST::Class::Issue|JIRA::REST::Class::Issue> object, returns the key
687             of the issue if it is a C<JIRA::REST::Class::Issue>
688             object, or the unmodified scalar if it is not.
689              
690             =head2 B<find_link_name_and_direction>
691              
692             When passed two scalars, one that could be a
693             L<JIRA::REST::Class::Issue::LinkType|JIRA::REST::Class::Issue::LinkType>
694             object and another that is a direction (inward/outward), returns the name of
695             the link type and direction if it is a C<JIRA::REST::Class::Issue::LinkType>
696             object, or attempts to determine the link type and direction from the
697             provided scalars.
698              
699             =head2 B<dump>
700              
701             Returns a stringified representation of the object's data generated somewhat
702             by L<Data::Dumper::Concise|Data::Dumper::Concise>, but not descending into
703             any objects that might be part of that data. If it finds objects in the
704             data, it will attempt to represent them in some abbreviated fashion which
705             may not display all the data in the object. For instance, if the object has
706             a C<JIRA::REST::Class::Issue> object in it for an issue with the key
707             C<'JRC-1'>, the object would be represented as the string C<<
708             'JIRA::REST::Class::Issue->key(JRC-1)' >>. The goal is to provide a gist of
709             what the contents of the object are without exhaustively dumping EVERYTHING.
710             I use it a lot for figuring out what's in the results I'm getting back from
711             the JIRA API.
712              
713             =head1 INTERNAL METHODS
714              
715             =head2 B<jira>
716              
717             Returns a L<JIRA::REST::Class|JIRA::REST::Class> object with credentials for the last JIRA user.
718              
719             =head2 B<factory>
720              
721             An accessor for the L<JIRA::REST::Class::Factory|JIRA::REST::Class::Factory>.
722              
723             =head2 B<JIRA_REST>
724              
725             An accessor that returns the L<JIRA::REST|JIRA::REST> object being used.
726              
727             =head2 B<REST_CLIENT>
728              
729             An accessor that returns the L<REST::Client|REST::Client> object inside the L<JIRA::REST|JIRA::REST> object being used.
730              
731             =head2 B<JSON>
732              
733             An accessor that returns the L<JSON|JSON> object inside the L<JIRA::REST|JIRA::REST> object being used.
734              
735             =head2 B<make_object>
736              
737             A pass-through method that calls L<JIRA::REST::Class::Factory::make_object()|JIRA::REST::Class::Factory/make_object>.
738              
739             =head2 B<make_date>
740              
741             A pass-through method that calls L<JIRA::REST::Class::Factory::make_date()|JIRA::REST::Class::Factory/make_date>.
742              
743             =head2 B<class_for>
744              
745             A pass-through method that calls L<JIRA::REST::Class::Factory::get_factory_class()|JIRA::REST::Class::Factory/get_factory_class>.
746              
747             =head2 B<obj_isa>
748              
749             When passed a scalar that I<could> be an object and a class string,
750             returns whether the scalar is, in fact, an object of that class.
751             Looks up the actual class using C<class_for()>, which calls
752             L<JIRA::REST::Class::Factory::get_factory_class()|JIRA::REST::Class::Factory/get_factory_class>.
753              
754             =head2 B<cosmetic_copy> I<THING>
755              
756             A utility function to produce a "cosmetic" copy of a thing: it clones
757             the data structure, but if anything in the structure (other than the
758             structure itself) is a blessed object, it replaces it with a
759             stringification of that object that probably doesn't contain all the
760             data in the object. For instance, if the object has a
761             C<JIRA::REST::Class::Issue> object in it for an issue with the key
762             C<'JRC-1'>, the object would be represented as the string
763             C<< 'JIRA::REST::Class::Issue->key(JRC-1)' >>. The goal is to provide a
764             gist of what the contents of the object are without exhaustively dumping
765             EVERYTHING.
766              
767             =head1 RELATED CLASSES
768              
769             =over 2
770              
771             =item * L<JIRA::REST::Class|JIRA::REST::Class>
772              
773             =item * L<JIRA::REST::Class::Abstract|JIRA::REST::Class::Abstract>
774              
775             =item * L<JIRA::REST::Class::Factory|JIRA::REST::Class::Factory>
776              
777             =item * L<JIRA::REST::Class::FactoryTypes|JIRA::REST::Class::FactoryTypes>
778              
779             =item * L<JIRA::REST::Class::Project|JIRA::REST::Class::Project>
780              
781             =back
782              
783             =begin test setup
784              
785             BEGIN {
786             use File::Basename;
787             use lib dirname($0).'/../lib';
788              
789             use InlineTest;
790             use Clone::Any qw( clone );
791             use Scalar::Util qw(refaddr);
792              
793             use_ok('JIRA::REST::Class::Mixins');
794             use_ok('JIRA::REST::Class::Factory');
795             use_ok('JIRA::REST::Class::FactoryTypes', qw( %TYPES ));
796             }
797              
798             =end test
799              
800             =begin testing constructor 3
801              
802             my $jira = JIRA::REST::Class::Mixins->jira(InlineTest->constructor_args);
803             isa_ok($jira, $TYPES{class}, 'Mixins->jira');
804             isa_ok($jira->JIRA_REST, 'JIRA::REST', 'JIRA::REST::Class->JIRA_REST');
805             isa_ok($jira->REST_CLIENT, 'REST::Client', 'JIRA::REST::Class->REST_CLIENT');
806              
807              
808             =end testing
809              
810             =begin test setup
811              
812             sub get_factory {
813             JIRA::REST::Class::Mixins->factory(InlineTest->constructor_args);
814             }
815              
816              
817             =end test
818              
819             =begin testing factory 2
820              
821             my $factory = get_factory();
822             isa_ok($factory, $TYPES{factory}, 'Mixins->factory');
823             ok(JIRA::REST::Class::Mixins->obj_isa($factory, 'factory'),
824             'Mixins->obj_isa works');
825              
826              
827             =end testing
828              
829             =begin testing cosmetic_copy 3
830              
831             my @PROJ = InlineTest->project_data;
832             my $orig = [ @PROJ ];
833             my $copy = JIRA::REST::Class::Mixins->cosmetic_copy($orig);
834              
835             is_deeply( $orig, $copy, "simple cosmetic copy has same content as original" );
836              
837             cmp_ok( refaddr($orig), 'ne', refaddr($copy),
838             "simple cosmetic copy has different address as original" );
839              
840             # make a complex reference to copy
841             my $factory = get_factory();
842             $orig = [ map { $factory->make_object('project', { data => $_ }) } @PROJ ];
843             $copy = JIRA::REST::Class::Mixins->cosmetic_copy($orig);
844              
845             is_deeply( $copy, [
846             "JIRA::REST::Class::Project->name(JIRA::REST::Class)",
847             "JIRA::REST::Class::Project->name(Kanban software development sample project)",
848             "JIRA::REST::Class::Project->name(PacKay Productions)",
849             "JIRA::REST::Class::Project->name(Project Management Sample Project)",
850             "JIRA::REST::Class::Project->name(Scrum Software Development Sample Project)"
851             ], "complex cosmetic copy is properly serialized");
852              
853             =end testing
854              
855             =begin testing _get_known_args 5
856              
857             package InlineTestMixins;
858             use Test::Exception;
859             use Test::More;
860              
861             sub test_too_many_args {
862             JIRA::REST::Class::Mixins->_get_known_args(
863             [ qw/ url username password rest_client_config proxy
864             ssl_verify_none anonymous unknown1 unknown2 / ],
865             qw/ url username password rest_client_config proxy
866             ssl_verify_none anonymous/
867             );
868             }
869              
870             # also excercizes __subname()
871              
872             throws_ok( sub { test_too_many_args() },
873             qr/^InlineTestMixins->test_too_many_args:/,
874             '_get_known_args constructs caller string okay' );
875              
876             throws_ok( sub { test_too_many_args() },
877             qr/too many arguments/,
878             '_get_known_args catches too many args okay' );
879              
880             sub test_unknown_args {
881             JIRA::REST::Class::Mixins->_get_known_args(
882             [ { map { $_ => $_ } qw/ url username password
883             rest_client_config proxy
884             ssl_verify_none anonymous
885             unknown1 unknown2 / } ],
886             qw/ url username password rest_client_config proxy
887             ssl_verify_none anonymous /
888             );
889             }
890              
891             # also excercizes _quoted_list()
892              
893             throws_ok( sub { test_unknown_args() },
894             qr/unknown arguments - 'unknown1', 'unknown2'/,
895             '_get_known_args catches unknown args okay' );
896              
897             my %expected = (
898             map { $_ => $_ } qw/ url username password
899             rest_client_config proxy
900             ssl_verify_none anonymous /
901             );
902              
903             sub test_positional_args {
904             JIRA::REST::Class::Mixins->_get_known_args(
905             [ qw/ url username password rest_client_config proxy
906             ssl_verify_none anonymous / ],
907             qw/ url username password rest_client_config proxy
908             ssl_verify_none anonymous /
909             );
910             }
911              
912             is_deeply( test_positional_args(), \%expected,
913             '_get_known_args processes positional args okay' );
914              
915             sub test_named_args {
916             JIRA::REST::Class::Mixins->_get_known_args(
917             [ { map { $_ => $_ } qw/ url username password
918             rest_client_config proxy
919             ssl_verify_none anonymous / } ],
920             qw/ url username password rest_client_config proxy
921             ssl_verify_none anonymous /
922             );
923             }
924              
925             is_deeply( test_named_args(), \%expected,
926             '_get_known_args processes named args okay' );
927              
928             =end testing
929              
930             =begin testing _check_required_args 1
931              
932             use Test::Exception;
933             use Test::More;
934              
935             sub test_missing_req_args {
936             my %args = map { $_ => $_ } qw/ username password /;
937             JIRA::REST::Class::Mixins->_check_required_args(
938             \%args,
939             url => "you must specify a URL to connect to",
940             );
941             }
942              
943             throws_ok( sub { test_missing_req_args() },
944             qr/you must specify a URL to connect to/,
945             '_check_required_args identifies missing args okay' );
946              
947             =end testing
948              
949             =begin testing _croakmsg 2
950              
951             package InlineTestMixins;
952             use Test::More;
953              
954             sub test_croakmsg_noargs {
955             JIRA::REST::Class::Mixins->_croakmsg("I died");
956             }
957              
958             # also excercizes __subname()
959              
960             is( test_croakmsg_noargs(),
961             'InlineTestMixins->test_croakmsg_noargs: I died',
962             '_croakmsg constructs no argument string okay' );
963              
964             sub test_croakmsg_args {
965             JIRA::REST::Class::Mixins->_croakmsg("I died", qw/ arg1 arg2 /);
966             }
967              
968             is( test_croakmsg_args(),
969             'InlineTestMixins->test_croakmsg_args(arg1, arg2): I died',
970             '_croakmsg constructs argument string okay' );
971              
972             =end testing
973              
974             =head1 AUTHOR
975              
976             Packy Anderson <packy@cpan.org>
977              
978             =head1 COPYRIGHT AND LICENSE
979              
980             This software is Copyright (c) 2017 by Packy Anderson.
981              
982             This is free software, licensed under:
983              
984             The Artistic License 2.0 (GPL Compatible)
985              
986             =cut