File Coverage

lib/JIRA/REST/Class/Mixins.pm
Criterion Covered Total %
statement 139 182 76.3
branch 47 94 50.0
condition 11 27 40.7
subroutine 31 37 83.7
pod 14 14 100.0
total 242 354 68.3


line stmt bran cond sub pod time code
1             package JIRA::REST::Class::Mixins;
2 4     4   97841 use strict;
  4         10  
  4         105  
3 4     4   21 use warnings;
  4         7  
  4         100  
4 4     4   59 use 5.010;
  4         14  
5              
6             our $VERSION = '0.11';
7             our $SOURCE = 'CPAN';
8             ## $SOURCE = 'GitHub'; # COMMENT
9             # the line above will be commented out by Dist::Zilla
10              
11             # ABSTRACT: An mixin class for L<JIRA::REST::Class|JIRA::REST::Class> that other objects can inherit methods from.
12              
13 4     4   20 use Carp;
  4         8  
  4         208  
14 4     4   23 use Clone::Any qw( clone );
  4         6  
  4         28  
15 4     4   1971 use Data::Dumper::Concise;
  4         5177  
  4         234  
16 4     4   408 use MIME::Base64;
  4         462  
  4         181  
17 4     4   399 use Readonly 2.04;
  4         2819  
  4         159  
18 4     4   22 use Scalar::Util qw( blessed reftype );
  4         8  
  4         174  
19 4     4   405 use Try::Tiny;
  4         1524  
  4         6409  
20              
21             sub jira {
22 78     78 1 2115 my $self = shift;
23 78         125 my $args = shift;
24 78 100       186 my $class = ref $self ? ref( $self ) : $self;
25              
26 78 100       252 if ( blessed $self ) {
27              
28             # if we have an object, return it!
29 41 100       120 return $self->{jira} if $self->{jira};
30              
31 36 0 33     107 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       91 if ( $args ) {
38 36         109 $self->{jira} = $class->jira( $args );
39 36         100 $self->{jira_rest} = $self->{jira}->{jira_rest};
40 36         120 return $self->{jira};
41             }
42             }
43              
44             # called with just the class name
45 37         144 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 64187 my $self = shift;
82 150         256 my $args = shift;
83 150 100       387 my $class = ref $self ? ref( $self ) : $self;
84              
85 150 100       537 if ( blessed $self ) {
86              
87             # if we have a factory, return it!
88 70 100       449 if ( $self->{factory} ) {
89 34         157 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       92 if ( $args ) {
95 36         97 $self->{factory} = $class->factory( $args );
96 36         151 return $self->{factory};
97             }
98             }
99              
100             # called with just the class name
101 80         401 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 3416 my $self = shift;
129 80         141 my $args = shift;
130 80 100       224 my $class = ref $self ? ref( $self ) : $self;
131              
132 80 100       310 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       602 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       119 if ( _JIRA_REST_version_has_named_parameters() ) {
148 44         195 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 0     0 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 0         0 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   11 my $version = shift;
177 4         10 my $has_version;
178             try {
179             # we don't want SIGDIE taking us someplace
180             # if VERSION throws an exception
181 4     4   135 local $SIG{__DIE__} = undef;
182              
183 4   33     144 $has_version = JIRA::REST->VERSION && JIRA::REST->VERSION( $version );
184 4         48 };
185 4         66 return $has_version;
186             }
187              
188             sub _JIRA_REST_version_has_named_parameters { ## no critic (Capitalization)
189             ## no critic (ProhibitMagicNumbers)
190 44     44   130 state $retval = _JIRA_REST_version( 0.016 );
191             ## use critic
192 44         147 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         9 return $retval;
200             }
201             ## use critic
202              
203             ## no critic (Capitalization)
204 20     20 1 101364 sub REST_CLIENT { return shift->JIRA_REST->{rest} }
205 1     1 1 23 sub JSON { return shift->JIRA_REST->{json} }
206             ## use critic
207 31     31 1 105 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 3 sub class_for { return shift->factory->get_factory_class( @_ ) }
210              
211             sub obj_isa {
212 2     2 1 403 my ( $self, $obj, $type ) = @_;
213 2 100       14 return unless blessed $obj;
214 1         6 my $class = $self->class_for( $type );
215 1         23 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 1089 shift; # we don't need $self
277 2         9 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   120 my $thing = shift;
314 92         128 my $top = pop;
315              
316 92 100       173 if ( not ref $thing ) {
317 70         186 return $thing;
318             }
319              
320 22     0   52 my $hash_copy = sub { };
321              
322 22 100       63 if ( my $class = blessed $thing ) {
323 5 50       13 if ( $class eq 'JSON::PP::Boolean' ) {
324 0 0       0 return $thing ? 'JSON::PP::true' : 'JSON::PP::false';
325             }
326 5 50       13 if ( $class eq 'JSON' ) {
327 0         0 return "$thing";
328             }
329 5 50       12 if ( $class eq 'REST::Client' ) {
330 0         0 return '%s->host(%s)', $class, $thing->getHost;
331             }
332 5 50       12 if ( $class eq 'DateTime' ) {
333 0         0 return "DateTime( $thing )";
334             }
335 5 50       10 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         7 my $fallback;
348              
349             # see if the object has any of these methods
350 5         11 foreach my $method ( qw/ name key id / ) {
351 5 50       21 if ( $thing->can( $method ) ) {
352 5         12 my $value = $thing->$method;
353              
354             # if the method returned a value, great!
355 5 50       35 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       55 if ( ref $thing eq 'SCALAR' ) {
    100          
    50          
370 0         0 return $$thing;
371             }
372             elsif ( ref $thing eq 'ARRAY' ) {
373 2         8 return __array_copy( $thing );
374             }
375             elsif ( ref $thing eq 'HASH' ) {
376 15         29 return __hash_copy( $thing );
377             }
378 0         0 return $thing;
379             }
380              
381             sub __array_copy {
382 2     2   6 my $thing = shift;
383 2         4 return [ map { __cosmetic_copy( $_ ) } @$thing ];
  10         27  
384             }
385              
386             sub __hash_copy {
387 15     15   22 my $thing = shift;
388 15         36 return +{ map { $_ => __cosmetic_copy( $thing->{$_} ) } keys %$thing };
  80         154  
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 50     50   6201 my ( $self, $in, @args ) = @_;
412 50         114 my $out = {};
413              
414             # get the package->name of the sub that called US
415 50         237 my $sub = $self->__subname( caller 1 );
416              
417             # if we croak, croak from the perspective of our CALLER's caller
418 50         147 local $Carp::CarpLevel = $Carp::CarpLevel + 2;
419              
420             # $in is an arrayref with a single hashref in it
421 50 100 100     413 if ( @$in == 1 && ref $in->[0] && ref $in->[0] eq 'HASH' ) {
      66        
422              
423             # copy that hashref into $in
424 42         985 $in = clone( $in->[0] );
425              
426             # moving arguments using the semi-magical hash reference slice
427 42         128 @{$out}{@args} = delete @{$in}{@args};
  42         168  
  42         163  
428              
429             # if there are leftover keys
430 42 100       165 if ( keys %$in ) {
431 1 50       7 my $arguments = 'argument' . ( keys %$in == 1 ? q{} : q{s} );
432              
433 1         8 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         18 @{$out}{@args} = @$in;
  6         44  
443             }
444             else {
445 2         5 my $got = scalar @$in;
446 2         5 my $max = scalar @args;
447 2         7 my $list = $self->_quoted_list( @args );
448              
449 2         29 croak "$sub: too many arguments - got $got, max $max ($list)";
450             }
451             }
452              
453 47         197 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   1390 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     34 && 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         4 local $Carp::CarpLevel = $Carp::CarpLevel + 2;
555              
556 1         11 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   2146 my ( $self, $msg, @args ) = @_;
593 2 100       10 my $args = @args ? q{(} . join( q{, }, @args ) . q{)} : q{};
594              
595             # get the package->name of the sub that called US
596 2         13 my $sub = $self->__subname( caller 1 );
597              
598 2         16 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   7 my $self = shift;
637 3         19 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 53     53   1070 my ( $self, @caller_n ) = @_;
647 53         1278 Readonly my $OUR_CALLERS_CALLER => 3;
648 53         5668 ( my $sub = $caller_n[$OUR_CALLERS_CALLER] ) =~ s/(.*)::([^:]+)$/$1->$2/xs;
649 53         795 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 Alexandr Alexey Ciornii Melezhik jira JRC Atlassian
663             GreenHopper ScriptRunner TODO aggregateprogress aggregatetimeestimate
664             aggregatetimeoriginalestimate assigneeType avatar avatarUrls completeDate
665             displayName duedate emailAddress endDate fieldtype fixVersions fromString
666             genericized iconUrl isAssigneeTypeValid issueTypes issuekeys issuelinks
667             issuetype jql lastViewed maxResults originalEstimate
668             originalEstimateSeconds parentkey projectId rapidViewId remainingEstimate
669             remainingEstimateSeconds resolutiondate sprintlist startDate
670             subtaskIssueTypes timeSpent timeSpentSeconds timeestimate
671             timeoriginalestimate timespent timetracking toString updateAuthor worklog
672             workratio
673              
674             =head1 NAME
675              
676             JIRA::REST::Class::Mixins - An mixin class for L<JIRA::REST::Class|JIRA::REST::Class> that other objects can inherit methods from.
677              
678             =head1 VERSION
679              
680             version 0.11
681              
682             =head1 METHODS
683              
684             =head2 B<name_for_user>
685              
686             When passed a scalar that could be a
687             L<JIRA::REST::Class::User|JIRA::REST::Class::User> object, returns the name
688             of the user if it is a C<JIRA::REST::Class::User>
689             object, or the unmodified scalar if it is not.
690              
691             =head2 B<key_for_issue>
692              
693             When passed a scalar that could be a
694             L<JIRA::REST::Class::Issue|JIRA::REST::Class::Issue> object, returns the key
695             of the issue if it is a C<JIRA::REST::Class::Issue>
696             object, or the unmodified scalar if it is not.
697              
698             =head2 B<find_link_name_and_direction>
699              
700             When passed two scalars, one that could be a
701             L<JIRA::REST::Class::Issue::LinkType|JIRA::REST::Class::Issue::LinkType>
702             object and another that is a direction (inward/outward), returns the name of
703             the link type and direction if it is a C<JIRA::REST::Class::Issue::LinkType>
704             object, or attempts to determine the link type and direction from the
705             provided scalars.
706              
707             =head2 B<dump>
708              
709             Returns a stringified representation of the object's data generated somewhat
710             by L<Data::Dumper::Concise|Data::Dumper::Concise>, but not descending into
711             any objects that might be part of that data. If it finds objects in the
712             data, it will attempt to represent them in some abbreviated fashion which
713             may not display all the data in the object. For instance, if the object has
714             a C<JIRA::REST::Class::Issue> object in it for an issue with the key
715             C<'JRC-1'>, the object would be represented as the string C<<
716             'JIRA::REST::Class::Issue->key(JRC-1)' >>. The goal is to provide a gist of
717             what the contents of the object are without exhaustively dumping EVERYTHING.
718             I use it a lot for figuring out what's in the results I'm getting back from
719             the JIRA API.
720              
721             =head1 INTERNAL METHODS
722              
723             =head2 B<jira>
724              
725             Returns a L<JIRA::REST::Class|JIRA::REST::Class> object with credentials for the last JIRA user.
726              
727             =head2 B<factory>
728              
729             An accessor for the L<JIRA::REST::Class::Factory|JIRA::REST::Class::Factory>.
730              
731             =head2 B<JIRA_REST>
732              
733             An accessor that returns the L<JIRA::REST|JIRA::REST> object being used.
734              
735             =head2 B<REST_CLIENT>
736              
737             An accessor that returns the L<REST::Client|REST::Client> object inside the L<JIRA::REST|JIRA::REST> object being used.
738              
739             =head2 B<JSON>
740              
741             An accessor that returns the L<JSON|JSON> object inside the L<JIRA::REST|JIRA::REST> object being used.
742              
743             =head2 B<make_object>
744              
745             A pass-through method that calls L<JIRA::REST::Class::Factory::make_object()|JIRA::REST::Class::Factory/make_object>.
746              
747             =head2 B<make_date>
748              
749             A pass-through method that calls L<JIRA::REST::Class::Factory::make_date()|JIRA::REST::Class::Factory/make_date>.
750              
751             =head2 B<class_for>
752              
753             A pass-through method that calls L<JIRA::REST::Class::Factory::get_factory_class()|JIRA::REST::Class::Factory/get_factory_class>.
754              
755             =head2 B<obj_isa>
756              
757             When passed a scalar that I<could> be an object and a class string,
758             returns whether the scalar is, in fact, an object of that class.
759             Looks up the actual class using C<class_for()>, which calls
760             L<JIRA::REST::Class::Factory::get_factory_class()|JIRA::REST::Class::Factory/get_factory_class>.
761              
762             =head2 B<cosmetic_copy> I<THING>
763              
764             A utility function to produce a "cosmetic" copy of a thing: it clones
765             the data structure, but if anything in the structure (other than the
766             structure itself) is a blessed object, it replaces it with a
767             stringification of that object that probably doesn't contain all the
768             data in the object. For instance, if the object has a
769             C<JIRA::REST::Class::Issue> object in it for an issue with the key
770             C<'JRC-1'>, the object would be represented as the string
771             C<< 'JIRA::REST::Class::Issue->key(JRC-1)' >>. The goal is to provide a
772             gist of what the contents of the object are without exhaustively dumping
773             EVERYTHING.
774              
775             =head1 RELATED CLASSES
776              
777             =over 2
778              
779             =item * L<JIRA::REST::Class|JIRA::REST::Class>
780              
781             =item * L<JIRA::REST::Class::Abstract|JIRA::REST::Class::Abstract>
782              
783             =item * L<JIRA::REST::Class::Factory|JIRA::REST::Class::Factory>
784              
785             =item * L<JIRA::REST::Class::FactoryTypes|JIRA::REST::Class::FactoryTypes>
786              
787             =item * L<JIRA::REST::Class::Project|JIRA::REST::Class::Project>
788              
789             =back
790              
791             =begin test setup
792              
793             BEGIN {
794             use File::Basename;
795             use lib dirname($0).'/../lib';
796              
797             use InlineTest;
798             use Clone::Any qw( clone );
799             use Scalar::Util qw(refaddr);
800              
801             use_ok('JIRA::REST::Class::Mixins');
802             use_ok('JIRA::REST::Class::Factory');
803             use_ok('JIRA::REST::Class::FactoryTypes', qw( %TYPES ));
804             }
805              
806             =end test
807              
808             =begin testing constructor 3
809              
810             my $jira = JIRA::REST::Class::Mixins->jira(InlineTest->constructor_args);
811             isa_ok($jira, $TYPES{class}, 'Mixins->jira');
812             isa_ok($jira->JIRA_REST, 'JIRA::REST', 'JIRA::REST::Class->JIRA_REST');
813             isa_ok($jira->REST_CLIENT, 'REST::Client', 'JIRA::REST::Class->REST_CLIENT');
814              
815              
816             =end testing
817              
818             =begin test setup
819              
820             sub get_factory {
821             JIRA::REST::Class::Mixins->factory(InlineTest->constructor_args);
822             }
823              
824              
825             =end test
826              
827             =begin testing factory 2
828              
829             my $factory = get_factory();
830             isa_ok($factory, $TYPES{factory}, 'Mixins->factory');
831             ok(JIRA::REST::Class::Mixins->obj_isa($factory, 'factory'),
832             'Mixins->obj_isa works');
833              
834              
835             =end testing
836              
837             =begin testing cosmetic_copy 3
838              
839             my @PROJ = InlineTest->project_data;
840             my $orig = [ @PROJ ];
841             my $copy = JIRA::REST::Class::Mixins->cosmetic_copy($orig);
842              
843             is_deeply( $orig, $copy, "simple cosmetic copy has same content as original" );
844              
845             cmp_ok( refaddr($orig), 'ne', refaddr($copy),
846             "simple cosmetic copy has different address as original" );
847              
848             # make a complex reference to copy
849             my $factory = get_factory();
850             $orig = [ map { $factory->make_object('project', { data => $_ }) } @PROJ ];
851             $copy = JIRA::REST::Class::Mixins->cosmetic_copy($orig);
852              
853             is_deeply( $copy, [
854             "JIRA::REST::Class::Project->name(JIRA::REST::Class)",
855             "JIRA::REST::Class::Project->name(Kanban software development sample project)",
856             "JIRA::REST::Class::Project->name(PacKay Productions)",
857             "JIRA::REST::Class::Project->name(Project Management Sample Project)",
858             "JIRA::REST::Class::Project->name(Scrum Software Development Sample Project)"
859             ], "complex cosmetic copy is properly serialized");
860              
861             =end testing
862              
863             =begin testing _get_known_args 5
864              
865             package InlineTestMixins;
866             use Test::Exception;
867             use Test::More;
868              
869             sub test_too_many_args {
870             JIRA::REST::Class::Mixins->_get_known_args(
871             [ qw/ url username password rest_client_config proxy
872             ssl_verify_none anonymous unknown1 unknown2 / ],
873             qw/ url username password rest_client_config proxy
874             ssl_verify_none anonymous/
875             );
876             }
877              
878             # also excercizes __subname()
879              
880             throws_ok( sub { test_too_many_args() },
881             qr/^InlineTestMixins->test_too_many_args:/,
882             '_get_known_args constructs caller string okay' );
883              
884             throws_ok( sub { test_too_many_args() },
885             qr/too many arguments/,
886             '_get_known_args catches too many args okay' );
887              
888             sub test_unknown_args {
889             JIRA::REST::Class::Mixins->_get_known_args(
890             [ { map { $_ => $_ } qw/ url username password
891             rest_client_config proxy
892             ssl_verify_none anonymous
893             unknown1 unknown2 / } ],
894             qw/ url username password rest_client_config proxy
895             ssl_verify_none anonymous /
896             );
897             }
898              
899             # also excercizes _quoted_list()
900              
901             throws_ok( sub { test_unknown_args() },
902             qr/unknown arguments - 'unknown1', 'unknown2'/,
903             '_get_known_args catches unknown args okay' );
904              
905             my %expected = (
906             map { $_ => $_ } qw/ url username password
907             rest_client_config proxy
908             ssl_verify_none anonymous /
909             );
910              
911             sub test_positional_args {
912             JIRA::REST::Class::Mixins->_get_known_args(
913             [ qw/ url username password rest_client_config proxy
914             ssl_verify_none anonymous / ],
915             qw/ url username password rest_client_config proxy
916             ssl_verify_none anonymous /
917             );
918             }
919              
920             is_deeply( test_positional_args(), \%expected,
921             '_get_known_args processes positional args okay' );
922              
923             sub test_named_args {
924             JIRA::REST::Class::Mixins->_get_known_args(
925             [ { map { $_ => $_ } qw/ url username password
926             rest_client_config proxy
927             ssl_verify_none anonymous / } ],
928             qw/ url username password rest_client_config proxy
929             ssl_verify_none anonymous /
930             );
931             }
932              
933             is_deeply( test_named_args(), \%expected,
934             '_get_known_args processes named args okay' );
935              
936             =end testing
937              
938             =begin testing _check_required_args 1
939              
940             use Test::Exception;
941             use Test::More;
942              
943             sub test_missing_req_args {
944             my %args = map { $_ => $_ } qw/ username password /;
945             JIRA::REST::Class::Mixins->_check_required_args(
946             \%args,
947             url => "you must specify a URL to connect to",
948             );
949             }
950              
951             throws_ok( sub { test_missing_req_args() },
952             qr/you must specify a URL to connect to/,
953             '_check_required_args identifies missing args okay' );
954              
955             =end testing
956              
957             =begin testing _croakmsg 2
958              
959             package InlineTestMixins;
960             use Test::More;
961              
962             sub test_croakmsg_noargs {
963             JIRA::REST::Class::Mixins->_croakmsg("I died");
964             }
965              
966             # also excercizes __subname()
967              
968             is( test_croakmsg_noargs(),
969             'InlineTestMixins->test_croakmsg_noargs: I died',
970             '_croakmsg constructs no argument string okay' );
971              
972             sub test_croakmsg_args {
973             JIRA::REST::Class::Mixins->_croakmsg("I died", qw/ arg1 arg2 /);
974             }
975              
976             is( test_croakmsg_args(),
977             'InlineTestMixins->test_croakmsg_args(arg1, arg2): I died',
978             '_croakmsg constructs argument string okay' );
979              
980             =end testing
981              
982             =head1 AUTHOR
983              
984             Packy Anderson <packy@cpan.org>
985              
986             =head1 COPYRIGHT AND LICENSE
987              
988             This software is Copyright (c) 2017 by Packy Anderson.
989              
990             This is free software, licensed under:
991              
992             The Artistic License 2.0 (GPL Compatible)
993              
994             =cut