|
8 | 8 | "io" |
9 | 9 | "log/slog" |
10 | 10 | "net/http" |
| 11 | + "reflect" |
11 | 12 | "strconv" |
| 13 | + "strings" |
12 | 14 | "sync" |
13 | 15 | "time" |
14 | 16 | ) |
@@ -281,78 +283,140 @@ func (c *Client) parseRateLimitHeaders(resp *http.Response) { |
281 | 283 | } |
282 | 284 | } |
283 | 285 |
|
| 286 | +// getPaginated fetches all pages of results for a given path. |
| 287 | +// It handles GitHub's pagination by requesting 100 items per page until |
| 288 | +// no more results are returned. |
| 289 | +func (c *Client) getPaginated(ctx context.Context, basePath string, result any) error { |
| 290 | + // Use reflection to work with any slice type |
| 291 | + resultVal := reflect.ValueOf(result) |
| 292 | + if resultVal.Kind() != reflect.Ptr || resultVal.Elem().Kind() != reflect.Slice { |
| 293 | + return fmt.Errorf("result must be a pointer to a slice") |
| 294 | + } |
| 295 | + |
| 296 | + sliceVal := resultVal.Elem() |
| 297 | + |
| 298 | + page := 1 |
| 299 | + perPage := 100 // GitHub's maximum per_page value |
| 300 | + |
| 301 | + for { |
| 302 | + // Build path with pagination parameters |
| 303 | + separator := "?" |
| 304 | + if strings.Contains(basePath, "?") { |
| 305 | + separator = "&" |
| 306 | + } |
| 307 | + path := fmt.Sprintf("%s%spage=%d&per_page=%d", basePath, separator, page, perPage) |
| 308 | + |
| 309 | + // Create a new slice to hold this page's results |
| 310 | + pageResult := reflect.New(sliceVal.Type()).Interface() |
| 311 | + |
| 312 | + if err := c.get(ctx, path, pageResult); err != nil { |
| 313 | + return err |
| 314 | + } |
| 315 | + |
| 316 | + // Get the slice value from the pointer |
| 317 | + pageSlice := reflect.ValueOf(pageResult).Elem() |
| 318 | + |
| 319 | + // If we got no results, we're done |
| 320 | + if pageSlice.Len() == 0 { |
| 321 | + break |
| 322 | + } |
| 323 | + |
| 324 | + // Append this page's results to the total |
| 325 | + sliceVal = reflect.AppendSlice(sliceVal, pageSlice) |
| 326 | + |
| 327 | + // If we got fewer results than per_page, this is the last page |
| 328 | + if pageSlice.Len() < perPage { |
| 329 | + break |
| 330 | + } |
| 331 | + |
| 332 | + page++ |
| 333 | + } |
| 334 | + |
| 335 | + // Set the final result |
| 336 | + resultVal.Elem().Set(sliceVal) |
| 337 | + return nil |
| 338 | +} |
| 339 | + |
284 | 340 | // GetFollowedUsers returns the users that the authenticated user follows. |
| 341 | +// This method automatically handles pagination to fetch all followed users. |
285 | 342 | func (c *Client) GetFollowedUsers(ctx context.Context) ([]User, error) { |
286 | 343 | var users []User |
287 | | - if err := c.get(ctx, "/user/following", &users); err != nil { |
| 344 | + if err := c.getPaginated(ctx, "/user/following", &users); err != nil { |
288 | 345 | return nil, fmt.Errorf("fetching followed users: %w", err) |
289 | 346 | } |
290 | 347 | return users, nil |
291 | 348 | } |
292 | 349 |
|
293 | 350 | // GetFollowedUsersByUsername returns the users that a specific user follows. |
| 351 | +// This method automatically handles pagination to fetch all followed users. |
294 | 352 | func (c *Client) GetFollowedUsersByUsername(ctx context.Context, username string) ([]User, error) { |
295 | 353 | var users []User |
296 | 354 | path := fmt.Sprintf("/users/%s/following", username) |
297 | | - if err := c.get(ctx, path, &users); err != nil { |
| 355 | + if err := c.getPaginated(ctx, path, &users); err != nil { |
298 | 356 | return nil, fmt.Errorf("fetching users followed by %s: %w", username, err) |
299 | 357 | } |
300 | 358 | return users, nil |
301 | 359 | } |
302 | 360 |
|
303 | 361 | // GetStarredRepos returns repositories starred by the authenticated user. |
| 362 | +// This method automatically handles pagination to fetch all starred repos. |
304 | 363 | func (c *Client) GetStarredRepos(ctx context.Context) ([]Repository, error) { |
305 | 364 | var repos []Repository |
306 | | - if err := c.get(ctx, "/user/starred", &repos); err != nil { |
| 365 | + if err := c.getPaginated(ctx, "/user/starred", &repos); err != nil { |
307 | 366 | return nil, fmt.Errorf("fetching starred repos: %w", err) |
308 | 367 | } |
309 | 368 | return repos, nil |
310 | 369 | } |
311 | 370 |
|
312 | 371 | // GetStarredReposByUsername returns repositories starred by a specific user. |
| 372 | +// This method automatically handles pagination to fetch all starred repos. |
313 | 373 | func (c *Client) GetStarredReposByUsername(ctx context.Context, username string) ([]Repository, error) { |
314 | 374 | var repos []Repository |
315 | 375 | path := fmt.Sprintf("/users/%s/starred", username) |
316 | | - if err := c.get(ctx, path, &repos); err != nil { |
| 376 | + if err := c.getPaginated(ctx, path, &repos); err != nil { |
317 | 377 | return nil, fmt.Errorf("fetching repos starred by %s: %w", username, err) |
318 | 378 | } |
319 | 379 | return repos, nil |
320 | 380 | } |
321 | 381 |
|
322 | 382 | // GetOwnedRepos returns repositories owned by the authenticated user. |
| 383 | +// This method automatically handles pagination to fetch all owned repos. |
323 | 384 | func (c *Client) GetOwnedRepos(ctx context.Context) ([]Repository, error) { |
324 | 385 | var repos []Repository |
325 | | - if err := c.get(ctx, "/user/repos?type=owner", &repos); err != nil { |
| 386 | + if err := c.getPaginated(ctx, "/user/repos?type=owner", &repos); err != nil { |
326 | 387 | return nil, fmt.Errorf("fetching owned repos: %w", err) |
327 | 388 | } |
328 | 389 | return repos, nil |
329 | 390 | } |
330 | 391 |
|
331 | 392 | // GetOwnedReposByUsername returns repositories owned by a specific user. |
| 393 | +// This method automatically handles pagination to fetch all owned repos. |
332 | 394 | func (c *Client) GetOwnedReposByUsername(ctx context.Context, username string) ([]Repository, error) { |
333 | 395 | var repos []Repository |
334 | 396 | path := fmt.Sprintf("/users/%s/repos?type=owner", username) |
335 | | - if err := c.get(ctx, path, &repos); err != nil { |
| 397 | + if err := c.getPaginated(ctx, path, &repos); err != nil { |
336 | 398 | return nil, fmt.Errorf("fetching repos owned by %s: %w", username, err) |
337 | 399 | } |
338 | 400 | return repos, nil |
339 | 401 | } |
340 | 402 |
|
341 | 403 | // GetRecentEvents returns recent events for the authenticated user. |
| 404 | +// This method automatically handles pagination to fetch all recent events. |
342 | 405 | func (c *Client) GetRecentEvents(ctx context.Context, username string) ([]Event, error) { |
343 | 406 | var events []Event |
344 | 407 | path := fmt.Sprintf("/users/%s/events", username) |
345 | | - if err := c.get(ctx, path, &events); err != nil { |
| 408 | + if err := c.getPaginated(ctx, path, &events); err != nil { |
346 | 409 | return nil, fmt.Errorf("fetching events for %s: %w", username, err) |
347 | 410 | } |
348 | 411 | return events, nil |
349 | 412 | } |
350 | 413 |
|
351 | 414 | // GetReceivedEvents returns events received by a user (their feed). |
| 415 | +// This method automatically handles pagination to fetch all received events. |
352 | 416 | func (c *Client) GetReceivedEvents(ctx context.Context, username string) ([]Event, error) { |
353 | 417 | var events []Event |
354 | 418 | path := fmt.Sprintf("/users/%s/received_events", username) |
355 | | - if err := c.get(ctx, path, &events); err != nil { |
| 419 | + if err := c.getPaginated(ctx, path, &events); err != nil { |
356 | 420 | return nil, fmt.Errorf("fetching received events for %s: %w", username, err) |
357 | 421 | } |
358 | 422 | return events, nil |
|
0 commit comments