From 74515134300e3d5fb567768091f2da2c163c794d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Harald=20J=C3=B6rg?= Date: Sat, 10 Feb 2018 20:43:59 +0100 Subject: [PATCH 1/6] Prevent 100-users.t from overwriting my bio --- xt/v3/100-users.t | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/xt/v3/100-users.t b/xt/v3/100-users.t index 082c78b..3c91cd1 100644 --- a/xt/v3/100-users.t +++ b/xt/v3/100-users.t @@ -16,8 +16,12 @@ diag( 'Using user = ' . $ENV{GITHUB_USER} ); ok( $gh ); ok( $user ); +# Remember the original value of bio +my $ou = $user->show(); +my $obio = $ou->{bio}; + diag( 'Updating ..' ); -my $bio = 'another Perl programmer and Father'; +my $bio = 'Testing Net::GitHub - please come back in a minute'; my $uu = $user->update( bio => $bio ); is($uu->{bio}, $bio); @@ -27,6 +31,9 @@ is($u->{bio}, $bio); delete $u->{updated_at}; delete $uu->{updated_at}; is_deeply($u, $uu); +# Restore bio +my $ru = $user->update( bio => $obio ); +is($ru->{bio},$obio,"Value of user's Bio restored"); =pod diag("Testing follow/unfollow"); From 2bf50d78c8914f7bcc22b90859c56d8ec38f550b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Harald=20J=C3=B6rg?= Date: Sat, 10 Feb 2018 20:46:16 +0100 Subject: [PATCH 2/6] Documentation: /issue/ needs number, not id --- lib/Net/GitHub/V3/Issues.pm | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/lib/Net/GitHub/V3/Issues.pm b/lib/Net/GitHub/V3/Issues.pm index 5b7af46..dd6bb47 100644 --- a/lib/Net/GitHub/V3/Issues.pm +++ b/lib/Net/GitHub/V3/Issues.pm @@ -156,7 +156,7 @@ Bissue($issue_id); + my $issue = $issue->issue($issue_number); =item create_issue @@ -173,7 +173,7 @@ Bupdate_issue( $issue_id, { + my $isu = $issue->update_issue( $issue_number, { state => 'closed' } ); @@ -195,9 +195,9 @@ L =item delete_comment - my @comments = $issue->comments($issue_id); + my @comments = $issue->comments($issue_number); my $comment = $issue->comment($comment_id); - my $comment = $issue->create_comment($issue_id, { + my $comment = $issue->create_comment($issue_number, { "body" => "a new comment" }); my $comment = $issue->update_comment($comment_id, { @@ -217,7 +217,7 @@ L =item repos_events - my @events = $issue->events($issue_id); + my @events = $issue->events($issue_number); my @events = $issue->repos_events; my $event = $issue->event($event_id); @@ -263,12 +263,12 @@ L =item milestone_labels - my @labels = $issue->issue_label($issue_id); - my @labels = $issue->create_issue_label($issue_id, ['New Label']); - my $st = $issue->delete_issue_label($issue_id, $label_name); - my @labels = $issue->replace_issue_label($issue_id, ['New Label']); - my $st = $issue->delete_issue_labels($issue_id); - my @lbales = $issue->milestone_labels($milestone_id); + my @labels = $issue->issue_label($issue_number); + my @labels = $issue->create_issue_label($issue_number, ['New Label']); + my $st = $issue->delete_issue_label($issue_number, $label_name); + my @labels = $issue->replace_issue_label($issue_number, ['New Label']); + my $st = $issue->delete_issue_labels($issue_number); + my @lables = $issue->milestone_labels($milestone_id); =back From 6c286b39e85053051ab01de73fe08b2ce4e44b43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Harald=20J=C3=B6rg?= Date: Mon, 12 Feb 2018 15:30:16 +0100 Subject: [PATCH 3/6] Documentation: /issue/ needs number, not id --- lib/Net/GitHub/V3.pm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Net/GitHub/V3.pm b/lib/Net/GitHub/V3.pm index b2a3ec7..2b2f020 100644 --- a/lib/Net/GitHub/V3.pm +++ b/lib/Net/GitHub/V3.pm @@ -304,7 +304,7 @@ L =head3 issue my @issues = $gh->issue->issues(); - my $issue = $gh->issue->issue($issue_id); + my $issue = $gh->issue->issue($issue_number); L From 7bb50b37e22d059b5aef609dfd5ee89c9fb99954 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Harald=20J=C3=B6rg?= Date: Mon, 12 Feb 2018 16:51:08 +0100 Subject: [PATCH 4/6] Initial patch for item-per-item pagination for the Issues submodule --- lib/Net/GitHub/V3.pm | 24 ++++++- lib/Net/GitHub/V3/Issues.pm | 87 +++++++++++++++++++++---- lib/Net/GitHub/V3/Query.pm | 115 ++++++++++++++++++++++++++++++++- lib/Net/GitHub/V3/ResultSet.pm | 76 ++++++++++++++++++++++ xt/v3/400-pagination.t | 104 +++++++++++++++++++++++++++++ 5 files changed, 392 insertions(+), 14 deletions(-) create mode 100644 lib/Net/GitHub/V3/ResultSet.pm create mode 100644 xt/v3/400-pagination.t diff --git a/lib/Net/GitHub/V3.pm b/lib/Net/GitHub/V3.pm index 2b2f020..e8f6476 100644 --- a/lib/Net/GitHub/V3.pm +++ b/lib/Net/GitHub/V3.pm @@ -222,7 +222,7 @@ from the API. By switching B on you can make the be turned into exceptions instead, so that you don't have to check for error response after every call. -=head3 next_url, last_url, prev_url, first_url, per_page +=head3 Iterating over pages: next_url, last_url, prev_url, first_url, per_page Any methods which return multiple results I be paginated. After performing a query you should check to see if there are more results. These attributes will @@ -242,6 +242,28 @@ See Github's documentation: L push @issues, $gh->issue->next_page; } + +=head3 Iterating over items: next_xxx and close_xxx + +The queries which can return paginated results can also be evaluated one by +one, like this: + + while (my $issue = $gh->issue->next_repos_issue( @args )) { + # do something with $issue + } + +The arguments to next_repos_issue are the same as for repos_issues. +In that case, new API calls will be performed only when needed to fetch more +items. An undefined return value means there are no more items. Do not +ignore this return value because the next call to next_repos_issues will, +once again, start from the first issue. + +If you want to start over with the first item without having to fetch all +items, call the corresponding close method: + + $gh->issue->close_repos_issue(@args); + + =head3 ua To set the proxy for ua, you can do something like following diff --git a/lib/Net/GitHub/V3/Issues.pm b/lib/Net/GitHub/V3/Issues.pm index dd6bb47..b41ed3c 100644 --- a/lib/Net/GitHub/V3/Issues.pm +++ b/lib/Net/GitHub/V3/Issues.pm @@ -11,20 +11,54 @@ with 'Net::GitHub::V3::Query'; sub issues { my $self = shift; - my $args = @_ % 2 ? shift : { @_ }; + return $self->query(_issues_arg2url(@_)); +} + +sub next_issue { + my $self = shift; + + return $self->next(_issues_arg2url(@_)); +} + +sub close_issue { + my $self = shift; + + return $self->close(_issues_arg2url(@_)); +} + + +sub _issues_arg2url { + my $args = @_ % 2 ? shift : { @_ }; my @p; foreach my $p (qw/filter state labels sort direction since/) { push @p, "$p=" . $args->{$p} if exists $args->{$p}; } my $u = '/issues'; $u .= '?' . join('&', @p) if @p; - return $self->query($u); + return $u; } sub repos_issues { my $self = shift; + return $self->query($self->_repos_issues_arg2url(@_)); +} + +sub next_repos_issue { + my $self = shift; + + return $self->next($self->_repos_issues_arg2url(@_)); +} + +sub close_repos_issue { + my $self = shift; + + return $self->close($self->_repos_issues_arg2url(@_)); +} + +sub _repos_issues_arg2url { + my $self = shift; if (@_ < 2) { unshift @_, $self->repo; unshift @_, $self->u; @@ -37,7 +71,7 @@ sub repos_issues { } my $u = "/repos/" . uri_escape($user) . "/" . uri_escape($repos) . '/issues'; $u .= '?' . join('&', @p) if @p; - return $self->query($u); + return $u; } ## build methods on fly @@ -48,32 +82,32 @@ my %__methods = ( update_issue => { url => "/repos/%s/%s/issues/%s", method => 'PATCH', args => 1 }, ## http://developer.github.com/v3/issues/comments/ - comments => { url => "/repos/%s/%s/issues/%s/comments" }, + comments => { url => "/repos/%s/%s/issues/%s/comments", paginate => 1 }, comment => { url => "/repos/%s/%s/issues/comments/%s" }, create_comment => { url => "/repos/%s/%s/issues/%s/comments", method => 'POST', args => 1 }, update_comment => { url => "/repos/%s/%s/issues/comments/%s", method => 'PATCH', args => 1 }, delete_comment => { url => "/repos/%s/%s/issues/comments/%s", method => 'DELETE', check_status => 204 }, # http://developer.github.com/v3/issues/events/ - events => { url => "/repos/%s/%s/issues/%s/events" }, - repos_events => { url => "/repos/%s/%s/issues/events" }, + events => { url => "/repos/%s/%s/issues/%s/events", paginate => 1 }, + repos_events => { url => "/repos/%s/%s/issues/events" , paginate => 1 }, event => { url => "/repos/%s/%s/issues/events/%s" }, # http://developer.github.com/v3/issues/labels/ - labels => { url => "/repos/%s/%s/labels" }, + labels => { url => "/repos/%s/%s/labels", paginate => 1 }, label => { url => "/repos/%s/%s/labels/%s" }, create_label => { url => "/repos/%s/%s/labels", method => 'POST', args => 1 }, update_label => { url => "/repos/%s/%s/labels/%s", method => 'PATCH', args => 1 }, delete_label => { url => "/repos/%s/%s/labels/%s", method => 'DELETE', check_status => 204 }, - issue_labels => { url => "/repos/%s/%s/issues/%s/labels" }, + issue_labels => { url => "/repos/%s/%s/issues/%s/labels", paginate => 1 }, create_issue_label => { url => "/repos/%s/%s/issues/%s/labels", method => 'POST', args => 1 }, delete_issue_label => { url => "/repos/%s/%s/issues/%s/labels/%s", method => 'DELETE', check_status => 204 }, replace_issue_label => { url => "/repos/%s/%s/issues/%s/labels", method => 'PUT', args => 1 }, delete_issue_labels => { url => "/repos/%s/%s/issues/%s/labels", method => 'DELETE', check_status => 204 }, - milestone_labels => { url => "/repos/%s/%s/milestones/%s/labels" }, + milestone_labels => { url => "/repos/%s/%s/milestones/%s/labels", paginate => 1 }, # http://developer.github.com/v3/issues/milestones/ - milestone => { url => "/repos/%s/%s/milestones/%s" }, + milestone => { url => "/repos/%s/%s/milestones/%s", paginate => 1 }, create_milestone => { url => "/repos/%s/%s/milestones", method => 'POST', args => 1 }, update_milestone => { url => "/repos/%s/%s/milestones/%s", method => 'PATCH', args => 1 }, delete_milestone => { url => "/repos/%s/%s/milestones/%s", method => 'DELETE', check_status => 204 }, @@ -85,6 +119,24 @@ __build_methods(__PACKAGE__, %__methods); sub milestones { my $self = shift; + return $self->query($self->_milestones_arg2url(@_)); +} + +sub next_milestone { + my $self = shift; + + return $self->next($self->_milestones_arg2url(@_)); +} + +sub close_milestone { + my $self = shift; + + return $self->close($self->_milestones_arg2url(@_)); +} + +sub _milestones_arg2url { + my $self = shift; + if (@_ < 3) { unshift @_, $self->repo; unshift @_, $self->u; @@ -97,7 +149,7 @@ sub milestones { } my $u = "/repos/" . uri_escape($user) . "/" . uri_escape($repos) . '/milestones'; $u .= '?' . join('&', @p) if @p; - return $self->query($u); + return $u; } no Moo; @@ -130,6 +182,10 @@ L my @issues = $issue->issues(); my @issues = $issue->issues(filter => 'assigned', state => 'open'); + while (my $next_issue = $issues->next_issue(...)) { ...; } + +Returns issues assigned to the authenticated user. + =back @@ -153,6 +209,7 @@ Brepos_issues($user, $repos); my @issues = $issue->repos_issues( { state => 'open' } ); my @issues = $issue->repos_issues($user, $repos, { state => 'open' } ); + while (my $r_issue = $issue->next_repos_issue(...)) { ...; } =item issue @@ -196,6 +253,7 @@ L =item delete_comment my @comments = $issue->comments($issue_number); + while (my $comment = $issue->next_comment($issue_number)) { ...; } my $comment = $issue->comment($comment_id); my $comment = $issue->create_comment($issue_number, { "body" => "a new comment" @@ -218,7 +276,9 @@ L =item repos_events my @events = $issue->events($issue_number); + while (my $event = $issue->next_event($issue_number)) { ...; } my @events = $issue->repos_events; + while (my $r_event = $issue->next_repos_event) { ...; } my $event = $issue->event($event_id); =back @@ -240,6 +300,7 @@ L =item delete_label my @labels = $issue->labels; + while (my $label = $issue->next_label) { ...; } my $label = $issue->label($label_name); my $label = $issue->create_label( { "name" => "API", @@ -263,12 +324,13 @@ L =item milestone_labels - my @labels = $issue->issue_label($issue_number); + my @labels = $issue->issue_labels($issue_number); my @labels = $issue->create_issue_label($issue_number, ['New Label']); my $st = $issue->delete_issue_label($issue_number, $label_name); my @labels = $issue->replace_issue_label($issue_number, ['New Label']); my $st = $issue->delete_issue_labels($issue_number); my @lables = $issue->milestone_labels($milestone_id); + while (my $label = $issue->next_milestone_label($milestone_id)) { ...; } =back @@ -290,6 +352,7 @@ L my @milestones = $issue->milestones; my @milestones = $issue->milestones( { state => 'open' } ); + while (my $milestone = $issue->next_milestone( ... )) { ...; } my $milestone = $issue->milestone($milestone_id); my $milestone = $issue->create_milestone( { "title" => "String", diff --git a/lib/Net/GitHub/V3/Query.pm b/lib/Net/GitHub/V3/Query.pm index 6b3c353..fef8d58 100644 --- a/lib/Net/GitHub/V3/Query.pm +++ b/lib/Net/GitHub/V3/Query.pm @@ -10,11 +10,13 @@ use LWP::UserAgent; use HTTP::Request; use Carp qw/croak/; use URI::Escape; -use Types::Standard qw(Int Str Bool InstanceOf Object); +use Types::Standard qw(Int Str Bool InstanceOf Object HashRef); use Cache::LRU; use Scalar::Util qw(looks_like_number); +use Net::GitHub::V3::ResultSet; + use Moo::Role; # configurable args @@ -127,6 +129,54 @@ has 'cache' => ( } ); +# per-page pagination + +has 'result_sets' => ( + isa => HashRef, + is => 'ro', + default => sub { {} }, +); + +sub next { + my $self = shift; + my ($url) = @_; + my $result_set; + $result_set = $self->result_sets->{$url} or do { + $result_set = Net::GitHub::V3::ResultSet->new( url => $url ); + $self->result_sets->{$url} = $result_set; + }; + my $results = $result_set->results; + my $cursor = $result_set->cursor; + if ( $cursor > $#$results ) { + return if $result_set->done; + my $next_url = $result_set->next_url || $result_set->url; + my $new_result = $self->query($next_url); + $result_set->results(ref $new_result eq 'ARRAY' ? + $new_result : + [$new_result] + ); + $result_set->cursor(0); + if ($self->has_next_page) { + $result_set->next_url($self->next_url); + } + else { + $result_set->done(1); + } + } + my $result = $result_set->results->[$result_set->cursor]; + $result_set->cursor($result_set->cursor + 1); + return $result; +} + + +sub close { + my $self = shift; + my ($url) = @_; + delete $self->result_sets->{$url}; + return; +} + + sub query { my $self = shift; @@ -363,6 +413,7 @@ sub __build_methods { my $check_status = $v->{check_status}; my $is_u_repo = $v->{is_u_repo}; # need auto shift u/repo my $preview_version = $v->{preview}; + my $paginate = $v->{paginate}; no strict 'refs'; no warnings 'once'; @@ -396,6 +447,51 @@ sub __build_methods { return $self->query($method, $u, @qargs); } }; + if ($paginate) { + # Add methods next... and close... + # Make method names singular (next_comments to next_comment) + $m =~ s/s$//; + *{"${package}::next_${m}"} = sub { + my $self = shift; + + # count how much %s inside u + my $n = 0; while ($url =~ /\%s/g) { $n++ } + + ## if is_u_repo, both ($user, $repo, @args) or (@args) should be supported + if ( ($is_u_repo or index($url, '/repos/%s/%s') > -1) and @_ < $n + $args) { + unshift @_, ($self->u, $self->repo); + } + + # make url, replace %s with real args + my @uargs = splice(@_, 0, $n); + my $u = sprintf($url, @uargs); + + # if preview API, set preview version + $self->accept_version($preview_version) if $preview_version; + + return $self->next($u); + }; + *{"${package}::close_${m}"} = sub { + my $self = shift; + + # count how much %s inside u + my $n = 0; while ($url =~ /\%s/g) { $n++ } + + ## if is_u_repo, both ($user, $repo, @args) or (@args) should be supported + if ( ($is_u_repo or index($url, '/repos/%s/%s') > -1) and @_ < $n + $args) { + unshift @_, ($self->u, $self->repo); + } + + # make url, replace %s with real args + my @uargs = splice(@_, 0, $n); + my $u = sprintf($url, @uargs); + + # if preview API, set preview version + $self->accept_version($preview_version) if $preview_version; + + $self->close($u); + }; + } } } @@ -491,6 +587,23 @@ Calls C with C. See L Adjusts next_url to be a new url in the pagination space I.E. you are jumping to a new index in the pagination +=item result_sets + +For internal use by the item-per-item pagination: This is a store of +the state(s) for the pagination. Each entry maps the initial URL of a +GitHub query to a L object. + +=item next($url) + +Returns the next item for the query which started at $url, or undef if +there are no more items. + +=item close($url) + +Terminates the item-per-item pagination for the query which started at +$url. + + =back =head3 NG_DEBUG diff --git a/lib/Net/GitHub/V3/ResultSet.pm b/lib/Net/GitHub/V3/ResultSet.pm new file mode 100644 index 0000000..97f04ec --- /dev/null +++ b/lib/Net/GitHub/V3/ResultSet.pm @@ -0,0 +1,76 @@ +package Net::GitHub::V3::ResultSet; + +our $VERSION = '0.01'; +our $AUTHORITY = 'cpan:FAYLAND'; + +use Types::Standard qw(Int Str ArrayRef Bool); +use Moo; + +has 'url' => ( is => 'rw', isa => Str, required => 1); +has 'results' => ( is => 'rw', isa => ArrayRef, default => sub { [] } ); +has 'cursor' => ( is => 'rw', isa => Int, default => 0 ); +has 'done' => ( is => 'rw', isa => Bool, default => 0 ); +has 'next_url' => ( is => 'rw', isa => Str ); + +no Moo; + +1; +__END__ + +=head1 NAME + +Net::GitHub::V3::ResultSet + +=head1 SYNOPSIS + +For use by the role L: + + use Net::GitHub::V3::ResultSet; + + $result_set = Net::GitHub::V3::ResultSet->new( url => $url ); + ... + +=head1 DESCRIPTION + +Objects in this class store the current status of a GitHub query while +the user iterates over individual items. This happens behind the +scenes, users of Net::GitHub::V3 don't need to know about this class. + +Each of the V3 submodules holds one of these objects for every +different pageable query which it handles. + +The attributes have the following function: + +=over 4 + +=item url + +Required for creating the object: This is the URL where a pageable +GitHub query starts, and this URL will be used to identify the +pagination when retrieving the next object, and also for the first +call to the GitHub API. + +=item results + +An array reference holding the current page as retrieved by the most +recent call to the GitHub API. + +=item cursor + +An integer pointing to the "next" position within the current page +from which the next method will fetch an item. + +=item done + +A boolean indicating that there's no more item to be fetched from the +API: The current results are the last. + +=item next_url + +The url from which more results can be fetched. Will be empty if +there are no more pages. + +=back + +=cut + diff --git a/xt/v3/400-pagination.t b/xt/v3/400-pagination.t new file mode 100644 index 0000000..81f2db4 --- /dev/null +++ b/xt/v3/400-pagination.t @@ -0,0 +1,104 @@ +#!/usr/bin/env perl + +use strict; +use warnings; +use Test::More; +use Net::GitHub::V3; +use Net::GitHub::V3::Iterator; + +# For this test we are using the repository of Net::GitHub itself. +# We filter for "all" states to make sure that the test doesn't fail +# if at some time there are not enough open issues! +# This test makes two API calls. + +plan skip_all => 'Please export environment variable GITHUB_USER/GITHUB_PASS' + unless $ENV{GITHUB_USER} and $ENV{GITHUB_PASS}; + +my $gh = Net::GitHub::V3->new( login => $ENV{GITHUB_USER}, + pass => $ENV{GITHUB_PASS}); +diag( 'Using user = ' . $ENV{GITHUB_USER} ); + +$gh->set_default_user_repo('fayland', 'perl-net-github'); +$gh->per_page(2); +my $issue = $gh->issue; + +ok( $gh ); +ok( $issue ); +my $result; + +# Testing the guts, checking result set internals to see whether a +# HTTP request has been performed + +my $url = '/repos/fayland/perl-net-github/issues?state=all'; +$result = $issue->next($url); +ok($result); +is($issue->result_sets->{$url}->cursor,1,"First result of first page"); +diag $result->{title}; + +$result = $issue->next($url); +ok($result); +is($issue->result_sets->{$url}->cursor,2,"Second result of first page"); +diag $result->{title}; + +$result = $issue->next($url); +ok($result); +is($issue->result_sets->{$url}->cursor,1,"First result of second page"); +diag $result->{title}; + +$issue->close($url); + +# Now testing with the "official" pagination interfaces. +# We use the *closed* issues of perl-net-github because they should be +# rather stable, keeping the tests valid. +# +# We iterate through the issues until we have a defined title, then +# through the comments for this issue until we have a defined author. + +$issue->per_page(100); # Keep API usage back to normal + +my $issue_found = ''; +my $search_title = 'rate limit headers'; +my $search_author = 'fayland'; +my $search_body = "ty, new version uploaded. Thanks\n"; + +my $issue_count = 0; +ISSUE: +while ( my $closed_issue = $issue->next_repos_issue({state => 'closed'}) ) { + if ($closed_issue->{title} ne $search_title) { + $issue_count++; + next ISSUE; + } + $issue_found = $issue_count; + pass("Issue '$search_title' found after $issue_found iterations"); + my $issue_number = $closed_issue->{number}; + + my $comment_found = 0; + COMMENT: + while ( my $comment = $issue->next_comment($issue_number) ) { + next COMMENT unless $comment->{user}{login} eq $search_author; + $comment_found = 1; + is($comment->{body},$search_body); + } + $issue->close_comment($issue_number); + ok($comment_found,"Comment by '$search_author' found"); + + my $event_found = 0; + EVENT: + while ( my $event = $issue->next_event($issue_number) ) { + next EVENT unless $event->{event} eq 'closed'; + $event_found = 1; + is($event->{actor}{login},$search_author, + "'$search_author' closed this issue"); + } + ok($event_found); + $issue->close_event($issue_number); + last ISSUE; +} + +if (! $issue_found) { + fail("Issue not found, no tests for comments, events etc."); +} + +$issue->close_repos_issue({state => 'closed'}); + +done_testing; From cc209ccc1c5fee9b26d23fd478ca7558532d5667 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Harald=20J=C3=B6rg?= Date: Wed, 21 Feb 2018 23:43:23 +0100 Subject: [PATCH 5/6] Item-per-item pagination for all submodules with exception of Search --- lib/Net/GitHub/V3/Events.pm | 32 +++-- lib/Net/GitHub/V3/Gists.pm | 24 +++- lib/Net/GitHub/V3/Issues.pm | 2 +- lib/Net/GitHub/V3/Orgs.pm | 44 +++++-- lib/Net/GitHub/V3/PullRequests.pm | 56 +++++--- lib/Net/GitHub/V3/Query.pm | 5 +- lib/Net/GitHub/V3/Repos.pm | 206 ++++++++++++++++++++++++++---- lib/Net/GitHub/V3/Users.pm | 45 ++++++- xt/v3/400-pagination.t | 178 +++++++++++++++++++++++++- 9 files changed, 523 insertions(+), 69 deletions(-) diff --git a/lib/Net/GitHub/V3/Events.pm b/lib/Net/GitHub/V3/Events.pm index b032562..f1d39be 100644 --- a/lib/Net/GitHub/V3/Events.pm +++ b/lib/Net/GitHub/V3/Events.pm @@ -12,19 +12,19 @@ with 'Net::GitHub::V3::Query'; ## build methods on fly my %__methods = ( - events => { url => '/events' }, - repos_events => { url => "/repos/%s/%s/events" }, - issues_events => { url => "/repos/%s/%s/issues/events" }, - networks_events => { url => "/networks/%s/%s/events" }, - orgs_events => { url => "/orgs/%s/events" }, + events => { url => '/events', paginate => 1 }, + repos_events => { url => "/repos/%s/%s/events", paginate => 1 }, + issues_events => { url => "/repos/%s/%s/issues/events", paginate => 1 }, + networks_events => { url => "/networks/%s/%s/events", paginate => 1 }, + orgs_events => { url => "/orgs/%s/events", paginate => 1 }, - user_received_events => { url => "/users/%s/received_events" }, - user_public_received_events => { url => "/users/%s/received_events/public" }, + user_received_events => { url => "/users/%s/received_events", paginate => 1 }, + user_public_received_events => { url => "/users/%s/received_events/public", paginate => 1 }, - user_events => { url => "/users/%s/events" }, - user_public_events => { url => "/users/%s/events/public" }, + user_events => { url => "/users/%s/events", paginate => 1 }, + user_public_events => { url => "/users/%s/events/public", paginate => 1 }, - user_orgs_events => { url => "/users/%s/events/orgs/%s" }, + user_orgs_events => { url => "/users/%s/events/orgs/%s", paginate => 1 }, ); __build_methods(__PACKAGE__, %__methods); @@ -51,13 +51,14 @@ Net::GitHub::V3::Events - GitHub Events API =head3 Events -L +L =over 4 =item events my @events = $event->events(); + while (my $ne = $event->next_event) { ...; } =item repos_events @@ -68,10 +69,14 @@ L my @events = $event->repos_events($user, $repo); my @events = $event->issues_events($user, $repo); my @events = $event->networks_events($user, $repo); + while (my $ur_event = next_repos_event($user,$repo) { ...; } + while (my $ur_event = next_issues_event($user,$repo) { ...; } + while (my $ur_event = next_networks_event($user,$repo) { ...; } =item orgs_events my @events = $event->orgs_events($org); + while (my $org_event = $event->next_orgs_event) { ...; } =item user_received_events @@ -85,10 +90,15 @@ L my @events = $event->user_public_received_events($user); my @events = $event->user_events($user); my @events = $event->user_public_events($user); + while (my $u_event = $event->next_user_received_event) { ...; } + while (my $u_event = $event->next_user_public_received_event) { ...; } + while (my $u_event = $event->next_user_event) { ...; } + while (my $u_event = $event->next_user_public_event) { ...; } =item user_orgs_events my @events = $event->user_orgs_events($user, $org); + while (my $o_event = $event->next_org_event) { ...; } =back diff --git a/lib/Net/GitHub/V3/Gists.pm b/lib/Net/GitHub/V3/Gists.pm index 7e99360..845aa5b 100644 --- a/lib/Net/GitHub/V3/Gists.pm +++ b/lib/Net/GitHub/V3/Gists.pm @@ -16,10 +16,24 @@ sub gists { return $self->query($u); } +sub next_gist { + my ( $self, $user ) = @_; + + my $u = $user ? "/users/" . uri_escape($user) . '/gists' : '/gists'; + return $self->next($u); +} + +sub close_gist { + my ( $self, $user ) = @_; + + my $u = $user ? "/users/" . uri_escape($user) . '/gists' : '/gists'; + return $self->close($u); +} + ## build methods on fly my %__methods = ( - public_gists => { url => "/gists/public" }, - starred_gists => { url => "/gists/starred" }, + public_gists => { url => "/gists/public", paginate => 1 }, + starred_gists => { url => "/gists/starred", paginate => 1 }, gist => { url => "/gists/%s" }, create => { url => "/gists", method => "POST", args => 1 }, update => { url => "/gists/%s", method => "PATCH", args => 1 }, @@ -30,7 +44,7 @@ my %__methods = ( delete => { url => "/gists/%s", method => "DELETE", check_status => 204 }, # http://developer.github.com/v3/gists/comments/ - comments => { url => "/gists/%s/comments" }, + comments => { url => "/gists/%s/comments", paginate => 1 }, comment => { url => "/gists/%s/comments/%s" }, create_comment => { url => "/gists/%s/comments", method => 'POST', args => 1 }, update_comment => { url => "/gists/%s/comments/%s", method => 'PATCH', args => 1 }, @@ -68,6 +82,7 @@ L my @gists = $gist->gists; my @gists = $gist->gists('nothingmuch'); + while (my $g = $gist->next_gist) { ...; } =item public_gists @@ -75,6 +90,8 @@ L my @gists = $gist->public_gists; my @gists = $gist->starred_gists; + while (my $g = $gist->next_public_gist) { ...; } + while (my $g = $gist->next_starred_gist) { ...; } =item gist @@ -134,6 +151,7 @@ L =item delete_comment my @comments = $gist->comments(); + while (my $c = $gist->next_comment) { ...; } my $comment = $gist->comment($comment_id); my $comment = $gist->create_comment($gist_id, { "body" => "a new comment" diff --git a/lib/Net/GitHub/V3/Issues.pm b/lib/Net/GitHub/V3/Issues.pm index b41ed3c..c595814 100644 --- a/lib/Net/GitHub/V3/Issues.pm +++ b/lib/Net/GitHub/V3/Issues.pm @@ -107,7 +107,7 @@ my %__methods = ( milestone_labels => { url => "/repos/%s/%s/milestones/%s/labels", paginate => 1 }, # http://developer.github.com/v3/issues/milestones/ - milestone => { url => "/repos/%s/%s/milestones/%s", paginate => 1 }, + milestone => { url => "/repos/%s/%s/milestones/%s" }, create_milestone => { url => "/repos/%s/%s/milestones", method => 'POST', args => 1 }, update_milestone => { url => "/repos/%s/%s/milestones/%s", method => 'PATCH', args => 1 }, delete_milestone => { url => "/repos/%s/%s/milestones/%s", method => 'DELETE', check_status => 204 }, diff --git a/lib/Net/GitHub/V3/Orgs.pm b/lib/Net/GitHub/V3/Orgs.pm index 5470bba..402ce2b 100644 --- a/lib/Net/GitHub/V3/Orgs.pm +++ b/lib/Net/GitHub/V3/Orgs.pm @@ -16,18 +16,32 @@ sub orgs { return $self->query($u); } +sub next_org { + my ( $self, $user ) = @_; + + my $u = $user ? "/users/" . uri_escape($user) . '/orgs' : '/user/orgs'; + return $self->next($u); +} + +sub close_org { + my ( $self, $user ) = @_; + + my $u = $user ? "/users/" . uri_escape($user) . '/orgs' : '/user/orgs'; + return $self->close($u); +} + ## build methods on fly my %__methods = ( org => { url => "/orgs/%s" }, update_org => { url => "/orgs/%s", method => 'PATCH', args => 1 }, # Members - members => { url => "/orgs/%s/members" }, - owner_members => { url => "/orgs/%s/members?role=admin" }, - no_2fa_members => { url => "/orgs/%s/members?filter=2fa_disabled" }, - outside_collaborators => { url => "/orgs/%s/outside_collaborators" }, + members => { url => "/orgs/%s/members", paginate => 1 }, + owner_members => { url => "/orgs/%s/members?role=admin", paginate => 1 }, + no_2fa_members => { url => "/orgs/%s/members?filter=2fa_disabled", paginate => 1 }, + outside_collaborators => { url => "/orgs/%s/outside_collaborators", paginate => 1 }, is_member => { url => "/orgs/%s/members/%s", check_status => 204 }, delete_member => { url => "/orgs/%s/members/%s", method => 'DELETE', check_status => 204 }, - public_members => { url => "/orgs/%s/public_members" }, + public_members => { url => "/orgs/%s/public_members", paginate => 1 }, is_public_member => { url => "/orgs/%s/public_members/%s", check_status => 204 }, publicize_member => { url => "/orgs/%s/public_members/%s", method => 'PUT', check_status => 204 }, conceal_member => { url => "/orgs/%s/public_members/%s", method => 'DELETE', check_status => 204 }, @@ -35,17 +49,17 @@ my %__methods = ( update_membership => { url => "/orgs/:org/memberships/:username", method => 'PUT', args => 1 }, delete_membership => { url => "/orgs/:org/memberships/:username", method => 'DELETE', check_status => 204 }, # Org Teams API - teams => { url => "/orgs/%s/teams" }, + teams => { url => "/orgs/%s/teams", paginate => 1 }, team => { url => "/teams/%s" }, create_team => { url => "/orgs/%s/teams", method => 'POST', args => 1 }, update_team => { url => "/teams/%s", method => 'PATCH', args => 1 }, delete_team => { url => "/teams/%s", method => 'DELETE', check_status => 204 }, - team_members => { url => "/teams/%s/members" }, + team_members => { url => "/teams/%s/members", paginate => 1 }, is_team_member => { url => "/teams/%s/members/%s", check_status => 204 }, add_team_member => { url => "/teams/%s/members/%s", method => 'PUT', check_status => 204 }, delete_team_member => { url => "/teams/%s/members/%s", method => 'DELETE', check_status => 204 }, - team_maintainers => { url => "/teams/%s/members?role=maintainer" }, - team_repos => { url => "/teams/%s/repos" }, + team_maintainers => { url => "/teams/%s/members?role=maintainer", paginate => 1 }, + team_repos => { url => "/teams/%s/repos", paginate => 1 }, is_team_repos => { url => "/teams/%s/repos/%s", check_status => 204 }, add_team_repos => { url => "/teams/%s/repos/%s", method => 'PUT', args => 1, check_status => 204 }, delete_team_repos => { url => "/teams/%s/repos/%s", method => 'DELETE', check_status => 204 }, @@ -82,6 +96,7 @@ L my @orgs = $org->orgs(); # /user/org my @orgs = $org->orgs( 'nothingmuch' ); # /users/:user/org + while (my $o = $org->next_org) { ...; } =item org @@ -106,6 +121,7 @@ L =item delete_member my @members = $org->members('perlchina'); + while (my $m = $org->next_member) { ...; } my $is_member = $org->is_member('perlchina', 'fayland'); my $st = $org->delete_member('perlchina', 'fayland'); @@ -118,6 +134,7 @@ L =item conceal_member my @members = $org->public_members('perlchina'); + while (my $public_member = $org->next_public_member) { ...; } my $is_public_member = $org->is_public_member('perlchina', 'fayland'); my $st = $org->publicize_member('perlchina', 'fayland'); my $st = $org->conceal_member('perlchina', 'fayland'); @@ -125,14 +142,17 @@ L =item owner_members my @admins = $org->owner_members('perlchina'); + while (my $admin = $org->next_owner_member) { ...; } =item no_2fa_members my @no_2fa_members = $org->no_2fa_members('perlchina'); + while (my $n2a_m = $org->next_no_2fa_member) { ...; } =item outside_collaborators my @collaborators = $org->outside_collaborators('perlchina'); + while (my $helper = $org->next_outside_collaborator) { ...; } =item membership @@ -165,6 +185,8 @@ L =item delete_team my @teams = $org->teams('perlchina'); + while (my $team = $org->next_team('perlchina')) { ...; } + my $team = $org->team($team_id); my $team = $org->create_team('perlchina', { "name" => "new team" @@ -183,6 +205,7 @@ L =item delete_team_member my @members = $org->team_members($team_id); + while (my $member = $org->next_team_member($team_id)) { ...; } my $is_team_member = $org->is_team_member($team_id, 'fayland'); my $st = $org->add_team_member($team_id, 'fayland'); my $st = $org->delete_team_member($team_id, 'fayland'); @@ -190,6 +213,7 @@ L =item team_maintainers my @maintainers = $org->team_maintainers($team_id); + while (my $maintainer = $org->next_team_maintainer($team_id)) { ...; } =item team_repos @@ -200,6 +224,8 @@ L =item delete_item_repos my @repos = $org->team_repos($team_id); + while (my $repo = $org->next_team_repo($team_id)) { ...; } + my $is_team_repos = $org->is_team_repos($team_id, 'Hello-World'); my $st = $org->add_team_repos($team_id, 'Hello-World'); my $st = $org->add_team_repos($team_id, 'YoinkOrg/Hello-World', { permission => 'admin' }); diff --git a/lib/Net/GitHub/V3/PullRequests.pm b/lib/Net/GitHub/V3/PullRequests.pm index fdb0092..4a03855 100644 --- a/lib/Net/GitHub/V3/PullRequests.pm +++ b/lib/Net/GitHub/V3/PullRequests.pm @@ -11,6 +11,24 @@ use URI::Escape; with 'Net::GitHub::V3::Query'; sub pulls { + my $self = shift; + + return $self->query($self->_pulls_arg2url(@_)); +} + +sub next_pull { + my $self = shift; + + return $self->next($self->_pulls_arg2url(@_)); +} + +sub close_pull { + my $self = shift; + + return $self->close($self->_pulls_arg2url(@_)); +} + +sub _pulls_arg2url { my $self = shift @_; my $args = pop @_; @@ -21,7 +39,7 @@ sub pulls { my $uri = URI->new('/repos/' . uri_escape($user) . '/' . uri_escape($repos) . '/pulls'); $uri->query_form($args); - return $self->query($uri->as_string); + return $uri->as_string; } ## build methods on fly @@ -32,20 +50,20 @@ my %__methods = ( create_pull => { url => "/repos/%s/%s/pulls", method => "POST", args => 1 }, update_pull => { url => "/repos/%s/%s/pulls/%s", method => "PATCH", args => 1 }, - commits => { url => "/repos/%s/%s/pulls/%s/commits" }, - files => { url => "/repos/%s/%s/pulls/%s/files" }, + commits => { url => "/repos/%s/%s/pulls/%s/commits", paginate => 1 }, + files => { url => "/repos/%s/%s/pulls/%s/files", paginate => 1 }, is_merged => { url => "/repos/%s/%s/pulls/%s/merge", check_status => 204 }, merge => { url => "/repos/%s/%s/pulls/%s/merge", method => "PUT" }, # http://developer.github.com/v3/pulls/comments/ - comments => { url => "/repos/%s/%s/pulls/%s/comments" }, + comments => { url => "/repos/%s/%s/pulls/%s/comments", paginate => 1 }, comment => { url => "/repos/%s/%s/pulls/comments/%s" }, create_comment => { url => "/repos/%s/%s/pulls/%s/comments", method => 'POST', args => 1 }, update_comment => { url => "/repos/%s/%s/pulls/comments/%s", method => 'PATCH', args => 1 }, delete_comment => { url => "/repos/%s/%s/pulls/comments/%s", method => 'DELETE', check_status => 204 }, # https://developer.github.com/v3/pulls/review_requests/ - reviewers => { url => "/repos/%s/%s/pulls/%s/requested_reviewers", preview => "black-cat-preview" }, + reviewers => { url => "/repos/%s/%s/pulls/%s/requested_reviewers", preview => "black-cat-preview", paginate => 1 }, add_reviewers => { url => "/repos/%s/%s/pulls/%s/requested_reviewers", method => 'POST', args => 1, preview => "black-cat-preview" }, delete_reviewers => { url => "/repos/%s/%s/pulls/%s/requested_reviewers", method => 'DELETE', check_status => 204, args => 1, preview => "black-cat-preview" }, ); @@ -93,10 +111,11 @@ L my @pulls = $pull_request->pulls(); my @pulls = $pull_request->pulls( { state => 'open' } ); + while (my $pr = $pull_request->next_pull( { state => 'open' } )) { ...; } =item pull - my $pull = $pull_request->pull($pull_id); + my $pull = $pull_request->pull($pull_number); =item create_pull @@ -108,21 +127,24 @@ L "head" => "octocat:new-feature", "base" => "master" } ); - my $pull = $pull_request->update_pull( $pull_id, $new_pull_data ); + my $pull = $pull_request->update_pull( $pull_number, $new_pull_data ); =item commits =item files - my @commits = $pull_request->commits($pull_id); - my @files = $pull_request->files($pull_id); + my @commits = $pull_request->commits($pull_number); + my @files = $pull_request->files($pull_number); + while (my $commit = $pull_request->next_commit($pull_number)) { ...; } + while (my $file = $pull_request->next_file($pull_number)) { ...; } + =item is_merged =item merge - my $is_merged = $pull_request->is_merged($pull_id); - my $result = $pull_request->merge($pull_id); + my $is_merged = $pull_request->is_merged($pull_number); + my $result = $pull_request->merge($pull_number); =back @@ -142,9 +164,10 @@ L =item delete_comment - my @comments = $pull_request->comments($pull_id); + my @comments = $pull_request->comments($pull_number); + while (my $comment = $pull_request->next_comment($pull_number)) { ...; } my $comment = $pull_request->comment($comment_id); - my $comment = $pull_request->create_comment($pull_id, { + my $comment = $pull_request->create_comment($pull_number, { "body" => "a new comment", commit_id => '586fe4be94c32248043b344e99fa15c72b40d1c2', path => 'test', @@ -169,11 +192,12 @@ L =item delete_reviewers - my @reviewers = $pull_request->reviewers($pull_id); - my $result = $pull_request->add_reviewers($pull_id, { + my @reviewers = $pull_request->reviewers($pull_number); + while (my $reviewer = $pull_request->next_reviever($pull_number)) { ...; } + my $result = $pull_request->add_reviewers($pull_number, { reviewers => [$user1, $user2], ); - my $result = $pull_request->delete_reviewers($pull_id, { + my $result = $pull_request->delete_reviewers($pull_number, { reviewers => [$user1, $user2], ); diff --git a/lib/Net/GitHub/V3/Query.pm b/lib/Net/GitHub/V3/Query.pm index fef8d58..0781540 100644 --- a/lib/Net/GitHub/V3/Query.pm +++ b/lib/Net/GitHub/V3/Query.pm @@ -451,7 +451,8 @@ sub __build_methods { # Add methods next... and close... # Make method names singular (next_comments to next_comment) $m =~ s/s$//; - *{"${package}::next_${m}"} = sub { + my $m_name = ref $paginate ? $paginate->{name} : $m; + *{"${package}::next_${m_name}"} = sub { my $self = shift; # count how much %s inside u @@ -471,7 +472,7 @@ sub __build_methods { return $self->next($u); }; - *{"${package}::close_${m}"} = sub { + *{"${package}::close_${m_name}"} = sub { my $self = shift; # count how much %s inside u diff --git a/lib/Net/GitHub/V3/Repos.pm b/lib/Net/GitHub/V3/Repos.pm index 550a407..6c1806c 100644 --- a/lib/Net/GitHub/V3/Repos.pm +++ b/lib/Net/GitHub/V3/Repos.pm @@ -15,6 +15,25 @@ with 'Net::GitHub::V3::Query'; sub list { my ( $self, $args ) = @_; + return $self->query(_repos_arg2url($args)); +} + + +sub next_repo { + my ( $self, $args ) = @_; + + return $self->next(_repos_arg2url($args)); +} + +sub close_repo { + my ( $self, $args ) = @_; + + return $self->close(_repos_arg2url($args)); +} + +sub _repos_arg2url { + my ($args) = @_; + # for old unless (ref($args) eq 'HASH') { $args = { type => $args }; @@ -22,18 +41,55 @@ sub list { my $uri = URI->new('/user/repos'); $uri->query_form($args); - return $self->query($uri->as_string); + return $uri->as_string; } + sub list_all { my ( $self, $since ) = @_; + + return $self->query(_all_repos_arg2url($since)); +} + +sub next_all_repo { + my ( $self, $since ) = @_; + + return $self->next(_all_repos_arg2url($since)); +} + +sub close_all_repo { + my ( $self, $since ) = @_; + + return $self->close(_all_repos_arg2url($since)); +} + +sub _all_repos_arg2url { + my ( $since ) = @_; $since ||= 'first'; my $u = '/repositories'; $u .= '?since=' . $since if $since ne 'first'; - return $self->query($u); + return $u; } sub list_user { + my $self = shift; + + return $self->query($self->_user_repos_arg2url(@_)); +} + +sub next_user_repo { + my $self = shift; + + return $self->next($self->_user_repos_arg2url(@_)); +} + +sub close_user_repo { + my $self = shift; + + return $self->close($self->_user_repos_arg2url(@_)); +} + +sub _user_repos_arg2url { my ($self, $user, $args) = @_; $user ||= $self->u; @@ -44,17 +100,36 @@ sub list_user { my $uri = URI->new("/users/" . uri_escape($user) . "/repos"); $uri->query_form($args); - return $self->query($uri->as_string); + return $uri->as_string; } sub list_org { + my $self = shift; + + return $self->query($self->_org_repos_arg2url(@_)); +} + +sub next_org_repo { + my $self = shift; + + return $self->next($self->_org_repos_arg2url(@_)); +} + +sub close_org_repo { + my $self = shift; + + return $self->close($self->_org_repos_arg2url(@_)); +} + +sub _org_repos_arg2url { my ($self, $org, $type) = @_; $type ||= 'all'; my $u = "/orgs/" . uri_escape($org) . "/repos"; $u .= '?type=' . $type if $type ne 'all'; - return $self->query($u); + return $u; } + sub create { my ( $self, $data ) = @_; @@ -100,6 +175,24 @@ sub upload_asset { sub commits { my $self = shift; + + return $self->query($self->_commits_arg2url(@_)); +} + +sub next_commit { + my $self = shift; + + return $self->next($self->_commits_arg2url(@_)); +} + +sub close_commit { + my $self = shift; + + return $self->close($self->_commits_arg2url(@_)); +} + +sub _commits_arg2url { + my $self = shift; if (@_ < 2) { unshift @_, $self->repo; unshift @_, $self->u; @@ -108,11 +201,31 @@ sub commits { my $uri = URI->new("/repos/" . uri_escape($user) . "/" . uri_escape($repos) . '/commits'); $uri->query_form($args); - return $self->query($uri->as_string); + return $uri->as_string; } + + sub list_deployments { my $self = shift; + + return $self->query($self->deployments_arg2url(@_)); +} + +sub next_deployment { + my $self = shift; + + return $self->next($self->deployments_arg2url(@_)); +} + +sub close_deployment { + my $self = shift; + + return $self->close($self->deployments_arg2url(@_)); +} + +sub _deployments_arg2url { + my $self = shift; if (@_ < 2) { unshift @_, $self->repo; unshift @_, $self->u; @@ -121,33 +234,34 @@ sub list_deployments { my $uri = URI->new("/repos/" . uri_escape($user) . "/" . uri_escape($repos) . '/deployments'); $uri->query_form($args); - return $self->query($uri->as_string); + return $uri->as_string; } + ## build methods on fly my %__methods = ( get => { url => "/repos/%s/%s" }, update => { url => "/repos/%s/%s", method => 'PATCH', args => 1 }, - contributors => { url => "/repos/%s/%s/contributors" }, + contributors => { url => "/repos/%s/%s/contributors", paginate => 1 }, languages => { url => "/repos/%s/%s/languages" }, - teams => { url => "/repos/%s/%s/teams" }, - tags => { url => "/repos/%s/%s/tags" }, - branches => { url => "/repos/%s/%s/branches" }, + teams => { url => "/repos/%s/%s/teams", paginate => 1 }, + tags => { url => "/repos/%s/%s/tags", paginate => 1 }, + branches => { url => "/repos/%s/%s/branches", paginate => 1 }, branch => { url => "/repos/%s/%s/branches/%s" }, delete => { url => "/repos/%s/%s", method => 'DELETE', check_status => 204 }, # http://developer.github.com/v3/repos/collaborators/ - collaborators => { url => "/repos/%s/%s/collaborators" }, + collaborators => { url => "/repos/%s/%s/collaborators", paginate => 1 }, is_collaborator => { url => "/repos/%s/%s/collaborators/%s", check_status => 204 }, add_collaborator => { url => "/repos/%s/%s/collaborators/%s", method => 'PUT', check_status => 204 }, delete_collaborator => { url => "/repos/%s/%s/collaborators/%s", method => 'DELETE', check_status => 204 }, # http://developer.github.com/v3/repos/commits/ commit => { url => "/repos/%s/%s/commits/%s" }, - comments => { url => "/repos/%s/%s/comments" }, + comments => { url => "/repos/%s/%s/comments", paginate => 1 }, comment => { url => "/repos/%s/%s/comments/%s" }, - commit_comments => { url => "/repos/%s/%s/commits/%s/comments" }, + commit_comments => { url => "/repos/%s/%s/commits/%s/comments", paginate => 1 }, create_comment => { url => "/repos/%s/%s/commits/%s/comments", method => 'POST', args => 1 }, update_comment => { url => "/repos/%s/%s/comments/%s", method => 'PATCH', args => 1 }, delete_comment => { url => "/repos/%s/%s/comments/%s", method => 'DELETE', check_status => 204 }, @@ -158,38 +272,38 @@ my %__methods = ( get_content => { url => "/repos/%s/%s/contents/%s" }, # http://developer.github.com/v3/repos/downloads/ - downloads => { url => "/repos/%s/%s/downloads" }, + downloads => { url => "/repos/%s/%s/downloads", paginate => 1 }, download => { url => "/repos/%s/%s/downloads/%s" }, delete_download => { url => "/repos/%s/%s/downloads/%s", method => 'DELETE', check_status => 204 }, # http://developer.github.com/v3/repos/releases/ - releases => { url => "/repos/%s/%s/releases" }, + releases => { url => "/repos/%s/%s/releases", paginate => 1 }, release => { url => "/repos/%s/%s/releases/%s" }, create_release => { url => "/repos/%s/%s/releases", method => 'POST', args => 1 }, update_release => { url => "/repos/%s/%s/releases/%s", method => 'PATCH', args => 1 }, delete_release => { url => "/repos/%s/%s/releases/%s", method => 'DELETE', check_status => 204 }, - release_assets => { url => "/repos/%s/%s/releases/%s/assets" }, + release_assets => { url => "/repos/%s/%s/releases/%s/assets", paginate => 1 }, release_asset => { url => "/repos/%s/%s/releases/%s/assets/%s" }, update_release_asset => { url => "/repos/%s/%s/releases/%s/assets/%s", method => 'PATCH', args => 1 }, delete_release_asset => { url => "/repos/%s/%s/releases/%s/assets/%s", method => 'DELETE', check_status => 204 }, - forks => { url => "/repos/%s/%s/forks" }, + forks => { url => "/repos/%s/%s/forks", paginate => 1 }, # http://developer.github.com/v3/repos/keys/ - keys => { url => "/repos/%s/%s/keys" }, + keys => { url => "/repos/%s/%s/keys", paginate => 1 }, key => { url => "/repos/%s/%s/keys/%s" }, create_key => { url => "/repos/%s/%s/keys", method => 'POST', args => 1 }, update_key => { url => "/repos/%s/%s/keys/%s", method => 'PATCH', check_status => 204, args => 1 }, delete_key => { url => "/repos/%s/%s/keys/%s", method => 'DELETE', check_status => 204 }, # http://developer.github.com/v3/repos/watching/ - watchers => { url => "/repos/%s/%s/watchers" }, + watchers => { url => "/repos/%s/%s/watchers", paginate => 1 }, is_watching => { url => "/user/watched/%s/%s", is_u_repo => 1, check_status => 204 }, watch => { url => "/user/watched/%s/%s", is_u_repo => 1, method => 'PUT', check_status => 204 }, unwatch => { url => "/user/watched/%s/%s", is_u_repo => 1, method => 'DELETE', check_status => 204 }, - subscribers => { url => "/repos/%s/%s/subscribers" }, + subscribers => { url => "/repos/%s/%s/subscribers", paginate => 1 }, subscription => { url => "/repos/%s/%s/subscription" }, is_subscribed => { url => "/repos/%s/%s/subscription", check_status => 200 }, subscribe => { url => "/repos/%s/%s/subscription", method => 'PUT', @@ -197,7 +311,7 @@ my %__methods = ( unsubscribe => { url => "/repos/%s/%s/subscription", method => 'DELETE', check_status => 204 }, # http://developer.github.com/v3/repos/hooks/ - hooks => { url => "/repos/%s/%s/hooks" }, + hooks => { url => "/repos/%s/%s/hooks", paginate => 1 }, hook => { url => "/repos/%s/%s/hooks/%s" }, delete_hook => { url => "/repos/%s/%s/hooks/%s", method => 'DELETE', check_status => 204 }, test_hook => { url => "/repos/%s/%s/hooks/%s/test", method => 'POST', check_status => 204 }, @@ -208,13 +322,13 @@ my %__methods = ( merges => { url => "/repos/%s/%s/merges", method => 'POST', args => 1 }, # http://developer.github.com/v3/repos/statuses/ - list_statuses => { url => "/repos/%s/%s/statuses/%s" }, + list_statuses => { url => "/repos/%s/%s/statuses/%s", paginate => { name => 'status' } }, create_status => { url => "/repos/%s/%s/statuses/%s", method => 'POST', args => 1 }, # https://developer.github.com/v3/repos/deployments create_deployment => { url => "/repos/%s/%s/deployments", method => 'POST', args => 1 }, - create_deployment_status => { url => "/repos/%s/%s/deployments/%s/statuses", method => 'POST', args => 1 }, - list_deployment_statuses => { url => "/repos/%s/%s/deployments/%s/statuses", method => 'GET'}, + create_deployment_status => { url => "/repos/%s/%s/deployments/%s/statuses", method => 'POST', args => 1}, + list_deployment_statuses => { url => "/repos/%s/%s/deployments/%s/statuses", method => 'GET', paginate => { name => 'deployment_status' } }, contributor_stats => { url => "/repos/%s/%s/stats/contributors", method => 'GET'}, commit_activity => { url => "/repos/%s/%s/stats/commit_activity", method => 'GET'}, @@ -356,6 +470,17 @@ L my @rp = $repos->list_org('perlchina'); my @rp = $repos->list_org('perlchina', 'public'); +=item next_repo, next_all_repo, next_user_repo, next_org_repo + + # Iterate over your repositories + while (my $repo = $repos->next_repo) { ...; } + # Iterate over all public repositories + while (my $repo = $repos->next_all_repo(500)) { ...; } + # Iterate over repositories of another user + while (my $repo = $repos->next_user_repo('c9s')) { ...; } + # Iterate over repositories of an organisation + while (my $repo = $repos->next_org_repo('perlchina','public')) { ...; } + =item create # create for yourself @@ -416,6 +541,9 @@ Btags; my @branches = $repos->branches; my $branch = $repos->branch('master'); + while (my $contributor = $repos->next_contributor) { ...; } + while (my $team = $repos->next_team) { ... ; } + while (my $tags = $repos->next_tag) { ... ; } =back @@ -434,6 +562,7 @@ L =item delete_collaborator my @collaborators = $repos->collaborators; + while (my $collaborator = $repos->next_collaborator) { ...; } my $is = $repos->is_collaborator('fayland'); $repos->add_collaborator('fayland'); $repos->delete_collaborator('fayland'); @@ -455,6 +584,7 @@ L author => 'fayland' }); my $commit = $repos->commit($sha); + while (my $commit = $repos->next_commit({...})) { ...; } =item comments @@ -469,7 +599,9 @@ L =item delete_comment my @comments = $repos->comments; + while (my $comment = $repos->next_comment) { ...; } my @comments = $repos->commit_comments($sha); + while (my $comment = $repos->next_commit_comment($sha)) { ...; } my $comment = $repos->create_comment($sha, { "body" => "Nice change", "commit_id" => "6dcb09b5b57875f334f61aebed695e2e4193db5e", @@ -500,6 +632,7 @@ L =item create_fork my @forks = $repos->forks; + while (my $fork = $repos->next_fork) { ...; } my $fork = $repos->create_fork; my $fork = $repos->create_fork($org); @@ -522,6 +655,7 @@ L =item delete_key my @keys = $repos->keys; + while (my $key = $repos->next_key) { ...; } my $key = $repos->key($key_id); # get key $repos->create_key( { title => 'title', @@ -544,6 +678,7 @@ L =item watchers my @watchers = $repos->watchers; + while (my $watcher = $repos->next_watcher) { ...; } =item watched @@ -581,6 +716,10 @@ Watcher methods use the GitHub 'subscription' terminology. Returns a list of subscriber data hashes. +=item next_subscriber + +Returns the next subscriber in the list, or undef if there are no more subscribers. + =item is_subscribed Returns true or false if you are subscribed @@ -617,6 +756,8 @@ L =item hooks +=item next_hook + =item hook =item create_hook @@ -628,6 +769,7 @@ L =item delete_hook my @hooks = $repos->hooks; + while (my $hook = $repos->next_hook) { ...; } my $hook = $repos->hook($hook_id); my $hook = $repos->create_hook($hook_hash); my $hook = $repos->update_hook($hook_id, $new_hook_hash); @@ -667,6 +809,10 @@ Or: my @statuses = $repos->list_statuses('fayland', 'perl-net-github', $sha); +=item next_status + + while (my $status = $repos->next_status($sha)) { ...; } + =item create_status $gh->set_default_user_repo('fayland', 'perl-net-github'); @@ -701,6 +847,7 @@ L =item releases my @releases = $repos->releases(); + while (my $release = $repos->next_release) { ...; } =item release @@ -732,6 +879,7 @@ L =item release_assets my @release_assets = $repos->release_assets($release_id); + while (my $asset = $repos->next_release_asset($release_id)) { ...; } =item upload_asset @@ -768,6 +916,12 @@ L 'ref' => 'feature-branch', }); +=item next_deployment + + while (my $deployment = $repos->next_deployment( $owner, $repo, { + 'ref' => 'feature-branch', + }) { ...; } + =item create_deployment my $response = $repos->create_deployment( $owner, $repo, { @@ -779,6 +933,10 @@ L my $response = $repos->list_deployment_statuses( $owner, $repo, $deployment_id ); +=item next_deployment_status + + while (my $status = next_deployment_status($o,$r,$id)) { ...; } + =item create_deployment_status my $response = $repos->create_deployment_status( $owner, $repo, $deployment_id, { diff --git a/lib/Net/GitHub/V3/Users.pm b/lib/Net/GitHub/V3/Users.pm index d77d2f5..49ec549 100644 --- a/lib/Net/GitHub/V3/Users.pm +++ b/lib/Net/GitHub/V3/Users.pm @@ -29,12 +29,29 @@ sub add_email { sub remove_email { (shift)->query( 'DELETE', '/user/emails', [ @_ ] ); } + sub followers { my ($self, $user) = @_; my $u = $user ? "/users/" . uri_escape($user) . '/followers' : '/user/followers'; return $self->query($u); } + +sub next_follower { + my ($self, $user) = @_; + + my $u = $user ? "/users/" . uri_escape($user) . '/followers' : '/user/followers'; + return $self->next($u); +} + +sub close_follower { + my ($self, $user) = @_; + + my $u = $user ? "/users/" . uri_escape($user) . '/followers' : '/user/followers'; + return $self->close($u); +} + + sub following { my ($self, $user) = @_; @@ -42,16 +59,30 @@ sub following { return $self->query($u); } +sub next_following { + my ($self, $user) = @_; + + my $u = $user ? "/users/" . uri_escape($user) . '/following' : '/user/following'; + return $self->next($u); +} + +sub close_following { + my ($self, $user) = @_; + + my $u = $user ? "/users/" . uri_escape($user) . '/following' : '/user/following'; + return $self->close($u); +} + ## build methods on fly my %__methods = ( - emails => { url => "/user/emails" }, + emails => { url => "/user/emails", paginate => 1 }, is_following => { url => "/user/following/%s", check_status => 204 }, follow => { url => "/user/following/%s", method => 'PUT', check_status => 204 }, unfollow => { url => "/user/following/%s", method => 'DELETE', check_status => 204 }, - keys => { url => "/user/keys" }, + keys => { url => "/user/keys", paginate => 1 }, key => { url => "/user/keys/%s" }, create_key => { url => "/user/keys", method => 'POST', args => 1 }, update_key => { url => "/user/keys/%s", method => 'PATCH', args => 1 }, @@ -118,6 +149,7 @@ L $user->add_email( 'another@email.com' ); $user->add_email( 'batch1@email.com', 'batch2@email.com' ); my $emails = $user->emails; + while ($email = $user->next_email) { ...; } $user->remove_email( 'another@email.com' ); $user->remove_email( 'batch1@email.com', 'batch2@email.com' ); @@ -133,10 +165,18 @@ L =item following +=item next_follower + +=item next_following + my $followers = $user->followers; my $followers = $user->followers($user); my $following = $user->following; my $following = $user->following($user); + my $next_follower = $user->next_follower + my $next_follower = $user->next_follower($user) + my $next_following = $user->next_following + my $next_following = $user->next_following($user) =item is_following @@ -168,6 +208,7 @@ L =item delete_key my $keys = $user->keys; + while (my $key = $user->next_key) { ...; } my $key = $user->key($key_id); # get key $user->create_key({ title => 'title', diff --git a/xt/v3/400-pagination.t b/xt/v3/400-pagination.t index 81f2db4..a579caa 100644 --- a/xt/v3/400-pagination.t +++ b/xt/v3/400-pagination.t @@ -4,7 +4,6 @@ use strict; use warnings; use Test::More; use Net::GitHub::V3; -use Net::GitHub::V3::Iterator; # For this test we are using the repository of Net::GitHub itself. # We filter for "all" states to make sure that the test doesn't fail @@ -101,4 +100,181 @@ if (! $issue_found) { $issue->close_repos_issue({state => 'closed'}); + +# More pagination... +# -- Submodule Net::GitHub::V3::Events +my $event = $gh->event; + +# ---- Public events +my $next_event = $event->next_event; +is(ref $next_event,'HASH'); +$event->close_event; + +# ---- Events for a repository +my $next_repos_event = $event->next_repos_event; +is(ref $next_repos_event,'HASH'); +is($next_repos_event->{repo}{name},'fayland/perl-net-github'); +$event->close_repos_event; + +# ---- Just checking whether the functions are correctly defined +foreach my $function (qw(repos_event issues_event networks_event + orgs_event + user_received_event user_public_received_event + user_event user_public_event + user_orgs_event + )) { + foreach my $action (qw(next close)) { + my $method = "${action}_${function}"; + ok($event->can($method),"Events::$method is defined"); + } +} + + +# -- Submodule Net::GitHub::V3::Gists +my $gist = $gh->gist; +foreach my $function (qw(gist + public_gist starred_gist + comment + )) { + foreach my $action (qw(next close)) { + my $method = "${action}_${function}"; + ok($gist->can($method),"Gists::$method is defined"); + } +} + +is(scalar keys %{$gist->result_sets}, 0, 'All result sets are closed'); + + +# -- Submodule Net::GitHub::V3::Orgs +my $org = $gh->org; +foreach my $function (qw(org + member owner_member no_2fa_member + public_member + outside_collaborator + team team_member team_maintainer team_repo + )) { + foreach my $action (qw(next close)) { + my $method = "${action}_${function}"; + ok($org->can($method),"Orgs::$method is defined"); + } +} +is(scalar keys %{$org->result_sets}, 0, 'All result sets are closed'); + + +# -- Submodule Net::GitHub::V3::PullRequests +my $pull_request = $gh->pull_request; + +# Find the PR which caused all this +my $first_pr = $pull_request->next_pull( + { head => 'HaraldJoerg:auto-pagination'} +); +is($first_pr->{number},86,'PR identified'); + +# Find a particular commit message +my $message_found = 0; +while (my $commit = $pull_request->next_commit($first_pr->{number})) { + next unless $commit->{commit}{message} =~ /^Initial patch/; + $message_found = 1; +} +ok($message_found,'Iterating through commit messages'); +$pull_request->close_commit($first_pr->{number}); + +my $second_pr = $pull_request->next_pull( + { head => 'HaraldJoerg:auto-pagination'} +); +ok(! $second_pr,'Only one PR in this selection'); +$pull_request->close_pull( + { head => 'HaraldJoerg:auto-pagination'} +); + +foreach my $function (qw(file + comment + reviewer + )) { + foreach my $action (qw(next close)) { + my $method = "${action}_${function}"; + ok($pull_request->can($method),"PullRequests::$method is defined"); + } +} +is(scalar keys %{$pull_request->result_sets}, 0, 'All result sets are closed'); + + +# -- Submodule Net::GitHub::V3::Repos +my $repos = $gh->repos; + +my $repo_found = 0; +# -- this has been disabled: It works, but takes many API requests. +# while (my $r = $repos->next_repo()) { +# if ($r->{name} eq 'perl-net-github') { +# $repo_found = 1; +# last; +# } +# } +# ok($repo_found,"'perl-net-github' is listed under repos"); +# $repos->close_repo; + +$repo_found = 0; +while (my $r = $repos->next_user_repo('fayland')) { + if ($r->{name} eq 'perl-net-github') { + $repo_found = 1; + last; + } +} +ok($repo_found,"'perl-net-github' is listed under fayland's repos"); +$repos->close_user_repo('fayland'); + +# -- this has been disabled: I don't know a stable repository +# associated with an organisation +# $repo_found = 0; +# while (my $r = $repos->next_org_repo('perlchina','public')) { +# if ($r->{name} eq 'perl-net-gitgub') { +# $repo_found = 1; +# last; +# } +# } +# ok($repo_found,"'perl-net-github' is listed under perlchina's public repos"); +# $repos->close_org_repo('perlchina','public'); + +# This should grab three fairly recent commits +my $selection = { since => '2018-01-01T00:00:00', + until => '2018-01-07T00:00:00', + }; +my @commits = (); +while (my $commit = $repos->next_commit($selection)) { + push @commits,$commit; +} +is(scalar @commits,3,"Three commits on 05-06 Jan 2018"); +$repos->close_commit($selection); + +foreach my $function (qw(comment commit_comment + download + release release_asset + fork + deployment key + subscriber watcher + hook + status deployment_status + )) { + foreach my $action (qw(next close)) { + my $method = "${action}_${function}"; + ok($repos->can($method),"Repos::$method is defined"); + } +} +is(scalar keys %{$repos->result_sets}, 0, 'All result sets are closed'); + + +# -- Submodule Net::GitHub::V3::Users +my $user = $gh->user; + +foreach my $function (qw(follower following + email + key + )) { + foreach my $action (qw(next close)) { + my $method = "${action}_${function}"; + ok($user->can($method),"Users::$method is defined"); + } +} +is(scalar keys %{$user->result_sets}, 0, 'All result sets are closed'); + done_testing; From 3208d6487e14b09320f48aea7dad34b513b1d440 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Harald=20J=C3=B6rg?= Date: Thu, 22 Feb 2018 19:25:50 +0100 Subject: [PATCH 6/6] Add documentation when close_xxx is needed --- lib/Net/GitHub/V3.pm | 35 +++++++++++++++++++++++++++-------- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/lib/Net/GitHub/V3.pm b/lib/Net/GitHub/V3.pm index e8f6476..2bcb25d 100644 --- a/lib/Net/GitHub/V3.pm +++ b/lib/Net/GitHub/V3.pm @@ -243,7 +243,7 @@ See Github's documentation: L } -=head3 Iterating over items: next_xxx and close_xxx +=head3 Iterating over individual items: next_xxx and close_xxx The queries which can return paginated results can also be evaluated one by one, like this: @@ -252,17 +252,36 @@ one, like this: # do something with $issue } -The arguments to next_repos_issue are the same as for repos_issues. -In that case, new API calls will be performed only when needed to fetch more -items. An undefined return value means there are no more items. Do not -ignore this return value because the next call to next_repos_issues will, -once again, start from the first issue. +The arguments to next_repos_issue are the same as for repos_issues, +and is also applicable to all other interfaces which offer a next_xxx +method. All available next_xxx methods are listed in the +documentation of the corresponding modules, see the list below. -If you want to start over with the first item without having to fetch all -items, call the corresponding close method: +If you loop over the next_xxx interfaces, new API calls will be +performed automatically, but only when needed to fetch more items. An +undefined return value means there are no more items. + +To start over with the first item, you need to close the iteration. +Every next_xxx method has a corresponding close_xxx method which must +be called with exactly the same parameters as the next_xxx method to +take effect: $gh->issue->close_repos_issue(@args); +If you use Net::GitHub::V3 in a command line program, there is no need +to call the close_xxx methods at all. As soon as the Net::GitHub::V3 +object $gh goes out of scope, everything is neatly cleaned up. + +However, if you have a long-lived Net::GitHub::V3 object, e.g. in a +persistent service process which provides an own interface to its +users and talks to GitHub under the hood, then it is advisable to +close the iterations when you're done with them. + +For brevity and because they usually are not needed, the close_xxx +methods are not listed with their modules. It is guaranteed that +I next_xxx method has a corresponding close_xxx method. + + =head3 ua