diff --git a/lib/RedMiner/API.pm b/lib/RedMiner/API.pm index 006716f9007e68eaef3dfb65f9cb245230829781..d7e38a26846e299e1c25d9c3ae6cb71b6af1de22 100644 --- a/lib/RedMiner/API.pm +++ b/lib/RedMiner/API.pm @@ -4,38 +4,233 @@ use 5.014004; use strict; use warnings; -require Exporter; -use AutoLoader qw(AUTOLOAD); +our $VERSION = '0.01'; -our @ISA = qw(Exporter); +# 2DO: fully implement project API +# 2DO: fully implement issues API +# 2DO: fully implement membership API -# Items to export into callers namespace by default. Note: do not export -# names by default without a very good reason. Use EXPORT_OK instead. -# Do not simply export all your public functions/methods/constants. +use LWP::UserAgent; +use JSON::XS qw/encode_json decode_json/; +use Encode qw/decode/; -# This allows declaration use RedMiner::API ':all'; -# If you do not need this, moving things directly into @EXPORT or @EXPORT_OK -# will save memory. -our %EXPORT_TAGS = ( 'all' => [ qw( - -) ] ); +=pod + +=encoding UTF-8 + +=head1 RedMiner::API -our @EXPORT_OK = ( @{ $EXPORT_TAGS{'all'} } ); +Wrapper package for RedMine REST API (http://www.redmine.org/projects/redmine/wiki/Rest_api). -our @EXPORT = qw( +=cut + +sub new +{ + my $class = shift; + my %arg = @_; -); + my $self = { + error => '', + protocol => $arg{protocol} // 'http', + ua => LWP::UserAgent->new, + }; + + foreach my $param (qw/host user pass key/) { + $self->{$param} = $arg{$param} // ''; + } + + if (length $self->{host} && $self->{host} =~ m|^(https?)://|i) { + $self->{protocol} = lc $1; + $self->{host} =~ s/^https?://i; + } else { + $self->{protocol} = 'http' if $self->{protocol} !~ /^https?$/i; + } + + my $auth = ''; + if (!length $self->{key} && length $self->{user}) { + $auth = $self->{user}; + if (length $self->{pass}) { + $auth .= ':' . $self->{pass}; + } + $auth .= '@'; + } + $self->{uri} = "$self->{protocol}://$auth$self->{host}"; + + $self->{ua}->default_header('Content-Type' => 'application/json'); + + if (length $self->{key}) { + $self->{ua}->default_header('X-Redmine-API-Key' => $self->{key}); + } + + bless $self, $class; +} + +sub error { $_[0]->{error} } +sub _set_error { $_[0]->{error} = $_[1] // ''; return; } + +sub _set_arg_error +{ + my $self = shift; + my $error = shift; + + $self->{raw_response} = ''; + $self->{raw_content} = ''; + + return $self->_set_error($error); +} + +sub rawResponse { $_[0]->{raw_response} // '' } +sub rawContent { $_[0]->{raw_content} // '' } + +sub _request +{ + my $self = shift; + my $method = shift // return $self->_set_arg_error('Request method missing'); + my $path = shift // return $self->_set_arg_error('Request path missing'); + my $data = shift; + + if ($method !~ /^(?:GET|POST|PUT|DELETE)$/) { + $method = 'GET'; + } + + $self->_set_error; + + my $request = HTTP::Request->new( + $method, sprintf('%s/%s.json', $self->{uri}, $path) + ); + + if ($method ne 'GET' && defined $data) { + my $json = eval { Encode::decode('UTF-8', JSON::XS::encode_json($data)) }; + if ($@) { + return $self->_set_arg_error('Malformed input data:' . $@); + } + $request->header('Content-Length' => length $json); + $request->content($json); + } + + return $request; +} + +sub _response +{ + my $self = shift; + my $request = shift // return; + my $response = $self->{ua}->request($request); + + $self->{raw_response} = $response->as_string; + $self->{raw_content} = $response->content; + + if (!$response->is_success) { + # FIXME: decode into error object + return $self->_set_error($response->status_line); + } + + return eval { + JSON::XS::decode_json($response->decoded_content) + } // $self->_set_error($@); +} + +sub createProject +{ + my $self = shift; + my $data = shift; + $self->_response( + $self->_request('POST', 'projects', { project => $data }) + ); +} + +# TESTME +sub project +{ + my $self = shift; + my $project_id = shift // return $self->_set_arg_error('Incorrect project ID'); + $self->_response( + $self->_request('GET', 'projects/' . $project_id) + ); +} + +# FIXME: implement handling of limit+offset+total_count parameters +# TESTME +sub projects +{ + my $self = shift; + $self->_response( + $self->_request('GET', 'projects') + ); +} + +# Undocumented: parent_id, inherit_members +sub updateProject +{ + my $self = shift; + my $project_id = shift // return $self->_set_arg_error('Incorrect project ID'); + my $data = shift; + $self->_response( + $self->_request('PUT', 'projects/' . $project_id, { project => $data }) + ); +} + +# TESTME +sub deleteProject +{ + my $self = shift; + my $project_id = shift // return $self->_set_arg_error('Incorrect project ID'); + $self->_response( + $self->_request('DELETE', 'projects/' . $project_id) + ); +} + +# FIXME: implement handling of limit+offset+total_count parameters +sub projectMemberships +{ + my $self = shift; + my $project_id = shift // return $self->_set_arg_error('Incorrect project ID'); + $self->_response( + $self->_request('GET', 'projects/' . $project_id . '/memberships') + ); +} + +# FIXME: set*, not update* +# Setting membership for a group: group_id +sub updateProjectMembership +{ + my $self = shift; + my $project_id = shift // return $self->_set_arg_error('Incorrect project ID'); + my $data = shift; + $self->_response( + $self->_request('POST', 'projects/' . $project_id . '/memberships', { membership => $data }) + ); +} -our $VERSION = '0.01'; +=head1 SEE ALSO +Mention other useful documentation such as the documentation of +related modules or operating system documentation (such as man pages +in UNIX), or any relevant external documentation such as RFCs or +standards. -# Preloaded methods go here. +If you have a mailing list set up for your module, mention it here. -# Autoload methods go after =cut, and are processed by the autosplit program. +If you have a web site set up for your module, mention it here. + +=head1 AUTHOR + +Anton Soldatov, E<lt>anton@localE<gt> + +=head1 COPYRIGHT AND LICENSE + +Copyright (C) 2014 by Anton Soldatov + +This library is free software; you can redistribute it and/or modify +it under the same terms as Perl itself, either Perl version 5.14.4 or, +at your option, any later version of Perl 5 you may have available. + + +=cut 1; + __END__ -# Below is stub documentation for your module. You'd better edit it! =head1 NAME