File Coverage

lib/JIRA/REST/Class/Mixins.pm
Criterion Covered Total %
statement 144 182 79.1
branch 50 94 53.1
condition 13 27 48.1
subroutine 31 37 83.7
pod 14 14 100.0
total 252 354 71.1


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