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