diff --git a/config-samples/redminer.conf b/config-samples/redminer.conf index 08e5625abe206ba441e47766de842a64a44f4197..8b908b80bc11d680be14565c7d2fbe2cb5a8e499 100644 --- a/config-samples/redminer.conf +++ b/config-samples/redminer.conf @@ -18,6 +18,9 @@ key=xxx # http://www.redmine.org/projects/redmine/wiki/Rest_api#Authentication # You can find your API key on your account page ( /my/account ) when logged in, on the right-hand pane of the default layout. +# Impersonation settings: +work_as=real_user + # User+Password auth is also possible: # [redmine] # host=redmine.example.com diff --git a/lib/RedMiner/API.pm b/lib/RedMiner/API.pm index 9b7ba93c6dd44f21b4e45321ddf91ecc81080e09..8f68dffb4aae8a4410abd245149321c6411c1c4d 100644 --- a/lib/RedMiner/API.pm +++ b/lib/RedMiner/API.pm @@ -4,9 +4,7 @@ use 5.010; use strict; use warnings; -our $VERSION = '0.03'; - -# 2DO: implement (un)?wrapping +our $VERSION = '0.04'; use URI; use URI::QueryParam; @@ -169,8 +167,42 @@ B<user>, B<pass>: User name and password for password-based authentication B<work_as>: User login for impersonation. For details, please refer to http://www.redmine.org/projects/redmine/wiki/Rest_api#User-Impersonation. +=item * + +B<no_wrapper_object>: Automatically add/remove wrapper object for data. See below. + =back +=head3 no_wrapper_object + +By default RedMine API requires you to wrap you object data like this: + + my $project = $redminer->createProject({ + project => { + identifier => 'some-id', + name => 'Some Name', + } + }); + # $project contains something like + # { project => { id => 42, identifier => 'some-id', name => 'Some Name' ... } } + +By default this module follows this convention. However, if you specify something like + + my $redminer = RedMiner::API->new( + host => 'example.com/redmine', + key => 'xxx', + no_wrapper_object => 1, + ); + +you can skip "wrapping" object data like this: + + my $project = $redminer->createProject({ + identifier => 'some-id', + name => 'Some Name', + }); + # $project contains something like + # { id => 42, identifier => 'some-id', name => 'Some Name' ... } + =cut sub new @@ -184,7 +216,7 @@ sub new ua => LWP::UserAgent->new, }; - foreach my $param (qw/host user pass key work_as/) { + foreach my $param (qw/host user pass key work_as no_wrapper_object/) { $self->{$param} = $arg{$param} // ''; } @@ -321,6 +353,10 @@ sub _response return $self->_set_error($@); } + if ($self->{expect_single_object} && $self->{no_wrapper_object}) { + $content = delete $content->{$self->{expect_single_object}}; + } + return $content; } @@ -369,6 +405,7 @@ sub _dispatch_name } $objects = $self->_normalize_objects($objects); + delete $self->{expect_single_object}; my $i = 0; my @objects; @@ -393,9 +430,12 @@ sub _dispatch_name push @objects, $object_id; } - # Add wrapping object, if necessary: - if (defined $data->{content} && pos($objects) == length($objects)) { - if (!exists $data->{content}{$object}) { + if (pos($objects) == length($objects)) { # Last object in the chain: + if ($action eq 'get' || $action eq 'create') { + $self->{expect_single_object} = $object; + } + if (defined $data->{content} && $self->{no_wrapper_object}) { + # Automatically wrap object data, otherwise we pass everything as is: $data->{content} = { $object => $data->{content} }; diff --git a/redminer.pl b/redminer.pl index 22a769c630715f8a564ee62509bbac62a7edb326..d3b81c5c080a7802ae4f6865a1e05b89c554de83 100644 --- a/redminer.pl +++ b/redminer.pl @@ -45,10 +45,12 @@ if (!$layout) { } my $redminer = RedMiner::API->new( - host => $conf->val('redmine', 'host') // '', - user => $conf->val('redmine', 'user') // '', - pass => $conf->val('redmine', 'pass') // '', - key => $conf->val('redmine', 'key') // '', + host => $conf->val('redmine', 'host') // '', + user => $conf->val('redmine', 'user') // '', + pass => $conf->val('redmine', 'pass') // '', + key => $conf->val('redmine', 'key') // '', + work_as => $conf->val('redmine', 'work_as') // '', + no_wrapper_object => 1, ); my $description = $layout? $layout->val('project', 'description') // '' : ''; @@ -67,10 +69,10 @@ if (!$project) { exit 255; } -my $pid = $project->{project}{id}; +my $pid = $project->{id}; say 'Project created with ID ' . $pid; -$redminer->updateProject($project->{project}{id}, { +$redminer->updateProject($pid, { inherit_members => 1, }); @@ -93,8 +95,8 @@ if ($layout) { next; } - say 'Subproject created with ID ' . $subproject->{project}{id}; - $redminer->updateProject($subproject->{project}{id}, { + say 'Subproject created with ID ' . $subproject->{id}; + $redminer->updateProject($subproject->{id}, { parent_id => $pid, inherit_members => 1, }); diff --git a/t/01-dispatching.t b/t/01-dispatching.t index dd7fe6e52526547dfc7dc792dc03c830d5ef0d98..f12bbf9169be53c3a58c8b35526604b377eed591 100644 --- a/t/01-dispatching.t +++ b/t/01-dispatching.t @@ -109,7 +109,7 @@ is_deeply($r, { query => undef, }, 'project'); -$r = $redminer->_dispatch_name('createProject', { name => 'My Project' }); +$r = $redminer->_dispatch_name('createProject', { project => { name => 'My Project' } }); is_deeply($r, { method => 'POST', path => 'projects', @@ -117,7 +117,7 @@ is_deeply($r, { query => undef, }, 'createProject'); -$r = $redminer->_dispatch_name('updateProject', 1, { name => 'My Project' }); +$r = $redminer->_dispatch_name('updateProject', 1, { project => { name => 'My Project' } }); is_deeply($r, { method => 'PUT', path => 'projects/1', @@ -145,7 +145,7 @@ is_deeply($r, { query => { limit => 10, offset => 9 }, }, 'projectMemberships'); -$r = $redminer->_dispatch_name('createProjectMembership', 1, { user_id => 1, role_ids => [ 1 ] }); +$r = $redminer->_dispatch_name('createProjectMembership', 1, { membership => { user_id => 1, role_ids => [ 1 ] } }); is_deeply($r, { method => 'POST', path => 'projects/1/memberships', @@ -153,7 +153,7 @@ is_deeply($r, { query => undef, }, 'createProjectMembership'); -$r = $redminer->_dispatch_name('createIssueWatcher', 1, { user_id => 1 }); +$r = $redminer->_dispatch_name('createIssueWatcher', 1, { watcher => { user_id => 1 } }); is_deeply($r, { method => 'POST', path => 'issues/1/watchers', @@ -189,7 +189,7 @@ is_deeply($r, { query => undef, }, 'timeEntry'); -$r = $redminer->_dispatch_name('createTimeEntry', { issue_id => 42, hours => 1 }); +$r = $redminer->_dispatch_name('createTimeEntry', { time_entry => { issue_id => 42, hours => 1 } }); is_deeply($r, { method => 'POST', path => 'time_entries', @@ -197,7 +197,7 @@ is_deeply($r, { query => undef, }, 'createTimeEntry'); -$r = $redminer->_dispatch_name('updateTimeEntry', 1, { issue_id => 42, hours => 1 }); +$r = $redminer->_dispatch_name('updateTimeEntry', 1, { time_entry => { issue_id => 42, hours => 1 } }); is_deeply($r, { method => 'PUT', path => 'time_entries/1', @@ -225,7 +225,7 @@ is_deeply($r, { query => { limit => 10, offset => 9 }, }, 'projectIssueCategories'); -$r = $redminer->_dispatch_name('createProjectIssueCategory', 1, { name => 'My Category', assign_to_id => 1 }); +$r = $redminer->_dispatch_name('createProjectIssueCategory', 1, { issue_category => { name => 'My Category', assign_to_id => 1 } }); is_deeply($r, { method => 'POST', path => 'projects/1/issue_categories', diff --git a/t/02-realworld.t b/t/02-realworld.t index 998a05704f0be9ef62a8f2441b3df3e11754e2b8..0c827ba1168c5ef0d50cab4297f98524e6566303 100644 --- a/t/02-realworld.t +++ b/t/02-realworld.t @@ -7,7 +7,7 @@ use JSON::XS qw/encode_json/; if ($ENV{REDMINER_API_DEVEL}) { plan tests => 5; } else{ - plan skip_all => 'Tests require RedMine installation'; + plan skip_all => 'Development tests (REDMINER_API_DEVEL not set)'; } eval 'use RedMiner::API'; @@ -34,25 +34,25 @@ my $redminer = RedMiner::API->new( key => $key, ); -my $project = $redminer->createProject({ +my $project = $redminer->createProject({ project => { identifier => 'redminer-api-test', name => 'RedMiner API test', -}); +}}); my $project_id = $project->{project}{id}; ok(defined $project_id, 'New project created with internal ID ' . $project_id); -ok(!defined $redminer->createProject({ +ok(!defined $redminer->createProject({ project => { identifier => 'redminer-api-test', name => 'RedMiner API test', -}), 'Project already exists, error object is ' . JSON::XS::encode_json($redminer->errorDetails)); +}}), 'Project already exists, error object is ' . JSON::XS::encode_json($redminer->errorDetails)); -ok($redminer->updateProject($project_id, { inherit_members => 1 }), 'Project updated'); +ok($redminer->updateProject($project_id, { project => { inherit_members => 1 } }), 'Project updated'); -my $issue = $redminer->createIssue({ +my $issue = $redminer->createIssue({ issue => { project_id => $project_id, subject => 'Test issue for RedMiner::API', description => 'Test description', -}); +}}); ok(defined $issue->{issue}{id}, 'Issue created with ID #' . $issue->{issue}{id}); ok($redminer->deleteProject($project_id), 'Project deleted'); diff --git a/t/03-realworld-no-wrapping.t b/t/03-realworld-no-wrapping.t new file mode 100644 index 0000000000000000000000000000000000000000..6650f30a1344bce29c02c72a8dd7e2d84af87cc3 --- /dev/null +++ b/t/03-realworld-no-wrapping.t @@ -0,0 +1,61 @@ +use strict; +use warnings; + +use Test::More; +use JSON::XS qw/encode_json/; + +if ($ENV{REDMINER_API_DEVEL}) { + plan tests => 5; +} else{ + plan skip_all => 'Development tests (REDMINER_API_DEVEL not set)'; +} + +eval 'use RedMiner::API'; + +# +# Read API key from a simple config file in the format 'host;key' +# +my $host = ''; +my $key = ''; +my $key_fname = $ENV{HOME} . '/.redminer/key'; + +if (!-e $key_fname) { + BAIL_OUT('REDMINER_API_DEVEL set, but key file is not accessible'); +} + +open my $FH_key, '<', $key_fname; +my $key_data = <$FH_key>; +($host, $key) = split /\s*;\s*/, $key_data; +chomp $key_data; +close $FH_key; + +my $redminer = RedMiner::API->new( + host => $host, + key => $key, + no_wrapper_object => 1, +); + +my $project = $redminer->createProject({ + identifier => 'redminer-api-test', + name => 'RedMiner API test', +}); +my $project_id = $project->{id}; +ok(defined $project_id, 'New project created with internal ID ' . $project_id); + +ok(!defined $redminer->createProject({ + identifier => 'redminer-api-test', + name => 'RedMiner API test', +}), 'Project already exists, error object is ' . JSON::XS::encode_json($redminer->errorDetails)); + +ok($redminer->updateProject($project_id, { inherit_members => 1 }), 'Project updated'); + +my $issue = $redminer->createIssue({ + project_id => $project_id, + subject => 'Test issue for RedMiner::API', + description => 'Test description', +}); +ok(defined $issue->{id}, 'Issue created with ID #' . $issue->{id}); + +ok($redminer->deleteProject($project_id), 'Project deleted'); + +exit;