File Coverage

blib/lib/Net/Gitlab.pm
Criterion Covered Total %
statement 33 106 31.1
branch 0 36 0.0
condition 0 6 0.0
subroutine 11 20 55.0
pod 1 1 100.0
total 45 169 26.6


line stmt bran cond sub pod time code
1             package Net::Gitlab;
2              
3             ## no critic( ValuesAndExpressions::ProhibitAccessOfPrivateData )
4              
5              
6 1     1   66610 use utf8;
  1         16  
  1         6  
7 1     1   33 use strict;
  1         2  
  1         19  
8 1     1   4 use warnings;
  1         2  
  1         27  
9 1     1   434 use namespace::autoclean;
  1         17267  
  1         4  
10              
11             our $VERSION = '0.09';
12              
13 1     1   87 use Carp;
  1         3  
  1         53  
14 1     1   439 use HTTP::Request ();
  1         22475  
  1         27  
15 1     1   718 use JSON;
  1         9801  
  1         6  
16 1     1   739 use LWP::UserAgent ();
  1         22725  
  1         32  
17 1     1   504 use Params::Validate::Checks ':all';
  1         4111  
  1         159  
18 1     1   509 use Regexp::Common 'Email::Address';
  1         2688  
  1         4  
19              
20             my $PASSWD_LENGTH = 6; ## no critic ( ValuesAndExpressions::ProhibitMagicNumbers )
21              
22             { # Hide
23              
24             Params::Validate::Checks::register
25             email => qr/$RE{Email}{Address}/,
26             uri => qr/$RE{URI}{HTTP}{-scheme => 'https?'}/,
27             short_password => sub { length $_[0] > $PASSWD_LENGTH };
28              
29             my %validate = (
30              
31             assignee_id => { as 'pos_int' },
32             hook_id => { as 'pos_int' },
33             issue_id => { as 'pos_int' },
34             key_id => { as 'pos_int' },
35             milestone_id => { as 'pos_int' },
36             project_id => { as 'pos_int' },
37             snippet_id => { as 'pos_int' },
38             user_id => { as 'pos_int' },
39              
40             issues_enabled => { type => BOOLEAN },
41             merge_requests_enabled => { type => BOOLEAN },
42             wall_enabled => { type => BOOLEAN },
43             wiki_enabled => { type => BOOLEAN },
44              
45             # Are these hard coded into gitlab? if so, we can further restrict this
46             access_level => { as 'string' },
47             branch => { as 'string' },
48             closed => { as 'string' },
49             code => { as 'string' },
50             default_branch => { as 'string' },
51             description => { as 'string' },
52             due_date => { as 'string' },
53             email => { as 'email' },
54             file_name => { as 'string' },
55             key => { as 'string' },
56             labels => { as 'string' },
57             lifetime => { as 'string' },
58             linkedin => { as 'string' },
59             name => { as 'string' },
60             password => { as 'string', as 'short_password' },
61             path => { as 'string' },
62             private_token => { as 'string' },
63             projects_limit => { as 'pos_int' },
64             sha => { as 'string' },
65             skype => { as 'string' },
66             title => { as 'string' },
67             twitter => { as 'string' },
68             url => { as 'uri' },
69             username => { as 'string' },
70              
71             base_url => { as 'uri' },
72             error => { as 'string' },
73             status_code => { as 'pos_int' },
74              
75             ); ## end %validate
76              
77             my %method = (
78              
79             ## no critic qw( Tics::ProhibitLongLines )
80              
81             login => {
82              
83             action => 'POST',
84             path => 'session',
85             required => [qw( email password )],
86              
87             },
88              
89             users => {
90              
91             action => 'GET',
92             path => 'users',
93              
94             },
95              
96             user => {
97              
98             action => 'GET',
99             path => 'users/',
100             required => [qw( user_id )],
101              
102             },
103              
104             add_user => {
105              
106             action => 'POST',
107             path => 'users',
108             required => [qw( email password username name )],
109             optional => [qw( skype linkedin twitter projects_limit extern_uid provider bio )],
110              
111             },
112              
113             self => {
114              
115             action => 'GET',
116             path => 'user',
117              
118             },
119              
120             self_issues => {
121              
122             action => 'GET',
123             path => 'issues',
124              
125             },
126              
127             self_keys => {
128              
129             action => 'GET',
130             path => 'user/keys',
131              
132             },
133              
134             self_key => {
135              
136             action => 'GET',
137             path => 'user/keys/',
138             required => [qw( user_id )],
139              
140             },
141              
142             add_key => {
143              
144             action => 'POST',
145             path => 'user/keys',
146             required => [qw( title key )],
147              
148             },
149              
150             remove_key => {
151              
152             action => 'DELETE',
153             path => 'user/keys/',
154             required => [qw( key_id )],
155              
156             },
157              
158             projects => {
159              
160             action => 'GET',
161             path => 'projects',
162              
163             },
164              
165             add_project => {
166              
167             action => 'POST',
168             path => 'projects',
169             required => [qw( name )],
170             optional => [
171             qw( code path description default_branch issues_enabled wall_enabled merge_requests_enabled wiki_enabled namespace_id visibility_level ),
172             ],
173              
174             },
175              
176             project => {
177              
178             action => 'GET',
179             path => 'projects/',
180             required => [qw( project_id )],
181              
182             },
183              
184             branches => {
185              
186             action => 'GET',
187             path => 'projects//repository/branches',
188             required => [qw( project_id )],
189              
190             },
191              
192             branch => {
193              
194             action => 'GET',
195             path => 'projects//repository/branches/',
196             required => [qw( project_id branch )],
197              
198             },
199              
200             commits => {
201              
202             action => 'GET',
203             path => 'projects//repository/commits',
204             required => [qw( project_id )],
205              
206             },
207              
208             commit => {
209              
210             action => 'GET',
211             path => 'projects//repository/commits//blob',
212             required => [qw( project_id sha )],
213              
214             },
215              
216             tags => {
217              
218             action => 'GET',
219             path => 'projects//repository/tags',
220             required => [qw( project_id )],
221              
222             },
223              
224             hooks => {
225              
226             action => 'GET',
227             path => 'projects//hooks',
228             required => [qw( project_id )],
229              
230             },
231              
232             hook => {
233              
234             action => 'GET',
235             path => 'projects//hooks/',
236             required => [qw( project_id hook_id )],
237              
238             },
239              
240             issues => {
241              
242             action => 'GET',
243             path => 'projects//issues',
244             required => [qw( project_id )],
245              
246             },
247              
248             issue => {
249              
250             action => 'GET',
251             path => 'projects//issues/',
252             required => [qw( project_id issue_id )],
253              
254             },
255              
256             members => {
257              
258             action => 'GET',
259             path => 'projects//members',
260             required => [qw( project_id )],
261              
262             },
263              
264             member => {
265              
266             action => 'GET',
267             path => 'projects//members/',
268             required => [qw( project_id user_id )],
269              
270             },
271              
272             milestones => {
273              
274             action => 'GET',
275             path => 'projects//milestones',
276             required => [qw( project_id )],
277              
278             },
279              
280             milestone => {
281              
282             action => 'GET',
283             path => 'projects//milestones/',
284             required => [qw( project_id milestone_id )],
285              
286             },
287              
288             snippets => {
289              
290             action => 'GET',
291             path => 'projects//snippets',
292             required => [qw( project_id )],
293              
294             },
295              
296             snippet => {
297              
298             action => 'GET',
299             path => 'projects//snippets/',
300             required => [qw( project_id snippet_id )],
301              
302             },
303              
304             raw_snippet => {
305              
306             action => 'GET',
307             path => 'projects//snippets//raw',
308             required => [qw( project_id snippet_id )],
309              
310             },
311              
312             add_hook => {
313              
314             action => 'POST',
315             path => 'projects//hooks',
316             required => [qw( project_id url )],
317              
318             },
319              
320             add_issue => {
321              
322             action => 'POST',
323             path => 'projects//issues',
324             required => [qw( project_id title )],
325             optional => [qw( description assignee_id milestone_id labels )],
326              
327             },
328              
329             add_member => {
330              
331             action => 'POST',
332             path => 'projects//members',
333             required => [qw( id user_id )],
334             optional => [qw( access_level )],
335              
336             },
337              
338             add_milestone => {
339              
340             action => 'POST',
341             path => 'projects//milestones',
342             required => [qw( project_id title )],
343             optional => [qw( description due_date )],
344              
345             },
346              
347             add_snippet => {
348              
349             action => 'POST',
350             path => 'projects//snippets',
351             required => [qw( project_id title file_name code )],
352             optional => [qw( lifetime )],
353              
354             },
355              
356             modify_hook => {
357              
358             action => 'POST',
359             path => 'projects//hooks/',
360             required => [qw( project_id hook_id url )],
361              
362             },
363              
364             modify_issue => {
365              
366             action => 'PUT',
367             path => 'projects//issues/',
368             required => [qw( project_id issue_id )],
369             optional => [qw( title description assignee_id milestone_id labels )],
370              
371             },
372              
373             modify_member => {
374              
375             action => 'PUT',
376             path => 'projects//members/',
377             required => [qw( project_id user_id access_level )],
378              
379             },
380              
381             modify_milestone => {
382              
383             action => 'PUT',
384             path => 'projects//milestones/',
385             required => [qw( project_id milestone_id )],
386             optional => [qw( title description due_date closed )],
387              
388             },
389              
390             modify_snippet => {
391              
392             action => 'PUT',
393             path => 'projects//snippets/',
394             required => [qw( project_id snippet_id )],
395             optional => [qw( title file_name lifetime code )],
396              
397             },
398              
399             remove_hook => {
400              
401             action => 'DELETE',
402             path => 'projects//hooks',
403             required => [qw( project_id )],
404              
405             },
406              
407             remove_member => {
408              
409             action => 'DELETE',
410             path => 'projects//members/',
411             required => [qw( project_id user_id )],
412              
413             },
414              
415             remove_snippet => {
416              
417             action => 'DELETE',
418             path => 'projects//snippets/',
419             required => [qw( project_id snippet_id )],
420              
421             },
422              
423             groups => {
424              
425             action => 'GET',
426             path => 'groups',
427              
428             },
429              
430             add_group => {
431              
432             action => 'POST',
433             path => 'groups',
434             required => [qw( name path )],
435              
436             },
437              
438             ); ## end %method
439              
440             my $valid_methods = join '|', sort keys %method;
441              
442             ###############################################################################
443              
444             sub _set_get {
445              
446 0     0     my ( $self, $key ) = @_;
447              
448             croak "unknown attribute ($key)"
449 0 0         unless exists $validate{ $key };
450              
451 0           my $validate = $validate{ $key };
452 0           $validate->{ optional } = 1;
453              
454 0           my ( $value ) = validate_pos( @_, $validate );
455              
456 0 0         if ( defined $value ) {
457              
458 0           $self->{ $key } = $value;
459 0           return 1;
460              
461             } else {
462              
463             croak "$key has not been set"
464 0 0         unless exists $self->{ $key };
465              
466 0           return $self->{ $key };
467              
468             }
469             } ## end sub _set_get
470              
471             sub _method {
472              
473 0     0     my $self = shift;
474 0           my $m = shift;
475              
476             croak "unkown method ($m)"
477 0 0         unless exists $method{ $m };
478              
479 0           my $method = $method{ $m };
480              
481 0           my $spec;
482              
483 0 0         if ( exists $method->{ required } ) {
484              
485             croak 'required needs to be an arrayref'
486 0 0         unless ref $method->{ required } eq 'ARRAY';
487              
488 0           $spec->{ $_ } = $validate{ $_ } for @{ $method->{ required } };
  0            
489              
490             }
491              
492 0 0         if ( exists $method->{ optional } ) {
493              
494             croak 'optional needs to be an arrayref'
495 0 0         unless ref $method->{ optional } eq 'ARRAY';
496              
497 0           for my $parm ( @{ $method->{ optional } } ) {
  0            
498              
499             croak "oops ... duplicate key ($parm) in optional and required arrays for method $m"
500 0 0         if exists $spec->{ $parm };
501              
502 0           $spec->{ $parm } = $validate{ $parm };
503 0           $spec->{ $parm }{ optional } = 1;
504              
505             }
506             }
507              
508 0           my %data;
509 0 0         %data = validate_with( params => \@_, spec => $spec )
510             if keys %$spec; ## no critic qw( References::ProhibitDoubleSigils )
511              
512 0 0         if ( keys %data ) {
513              
514 0           return $self->_call_api( $m, \%data );
515              
516             } else {
517              
518 0           return $self->_call_api( $m );
519              
520             }
521             } ## end sub _method
522              
523             our $AUTOLOAD;
524              
525             sub AUTOLOAD { ## no critic qw( ClassHierarchies::ProhibitAutoloading )
526              
527 0     0     my $self = shift;
528              
529 0           ( my $call = $AUTOLOAD ) =~ s/^.*:://;
530              
531 0           my $sub;
532              
533 0 0         if ( exists $validate{ $call } ) {
    0          
534              
535 0     0     $sub = sub { shift->_set_get( $call, @_ ) };
  0            
536              
537             } elsif ( exists $method{ $call } ) {
538              
539 0     0     $sub = sub { shift->_method( $call, @_ ) };
  0            
540              
541             } else {
542              
543 0           croak "Don't know how to handle $call";
544              
545             }
546              
547             ## no critic qw( References::ProhibitDoubleSigils )
548 1     1   167408 no strict 'refs'; ## no critic( TestingAndDebugging::ProhibitNoStrict )
  1         2  
  1         611  
549 0           *$AUTOLOAD = $sub;
550              
551 0           unshift @_, $self;
552              
553 0           goto &$AUTOLOAD;
554              
555             } ## end sub AUTOLOAD
556              
557       0     DESTROY { }
558              
559             sub new {
560              
561 0     0 1   my $class = shift;
562 0   0       my $self = bless {}, ref $class || $class;
563              
564 0           my $validate;
565              
566 0           for my $k ( keys %validate ) {
567              
568 0           $validate->{ $k } = $validate{ $k };
569 0           $validate->{ $k }{ optional } = 1;
570              
571             }
572              
573 0           my %arg = validate_with( params => \@_, spec => $validate );
574              
575 0           $self->$_( $arg{ $_ } ) for keys %arg;
576              
577 0           return $self;
578              
579             } ## end sub new
580              
581 0   0 0     sub _ua { shift->{ ua } ||= LWP::UserAgent->new() }
582              
583             sub _call_api {
584              
585 0     0     my $self = shift;
586              
587 0           my @specs = { type => SCALAR, regex => qr/^($valid_methods)$/ };
588              
589 0 0         push @specs, { type => HASHREF }
590             if scalar @_ > 1;
591              
592 0           my ( $m, $data ) = validate_pos( @_, @specs );
593              
594             croak "no action specified for $m"
595 0 0         unless exists $method{ $m }->{ action };
596              
597 0           my $method = $method{ $m };
598              
599 0           my $action = $method->{ action };
600 0           my $url = sprintf '%s/%s', $self->base_url(), $method->{ path };
601              
602 0           $url =~ s/<$_>/delete $data->{ $_ }/ge for $url =~ /<([^>]*)>/g;
  0            
603              
604 0           my $req = HTTP::Request->new( $action => $url );
605              
606 0           $req->content_type( 'application/json' );
607              
608             $req->header( 'private_token' => $self->private_token() )
609 0 0         unless $method->{ path } eq '/session';
610              
611 0 0         $req->content( encode_json $data )
612             if keys %$data; ## no critic ( References::ProhibitDoubleSigils )
613              
614 0           my $res = $self->_ua->request( $req );
615 0           $self->status_code( $res->code() );
616              
617 0 0         if ( $res->is_success ) {
618              
619 0           return decode_json $res->content;
620              
621             } else {
622              
623 0           $self->error( $res->status_line );
624 0           return;
625              
626             }
627             }; ## end sub _call_api
628             } # No more hiding
629              
630             1;
631              
632             __END__