|
@@ -13,20 +13,20 @@ allows pagers to calculate page sizes and iterate over
|
|
|
response aggregates.
|
|
|
*/
|
|
|
type Page[T any] interface {
|
|
|
- // Elems must return the items from the current page
|
|
|
- Elems() []T
|
|
|
+ // Elems must return the items from the current page
|
|
|
+ Elems() []T
|
|
|
}
|
|
|
|
|
|
// Exposes the part of the client that depager understands.
|
|
|
type Client[T any] interface {
|
|
|
- // NextPage returns the next page or it returns an error
|
|
|
- NextPage(
|
|
|
- offset uint64, // item offset at which to start page
|
|
|
- ) (
|
|
|
- page Page[T],
|
|
|
- count uint64, // total count of all items being paged
|
|
|
- err error,
|
|
|
- )
|
|
|
+ // NextPage returns the next page or it returns an error
|
|
|
+ NextPage(
|
|
|
+ page Page[T],
|
|
|
+ offset uint64, // item offset at which to start page
|
|
|
+ ) (
|
|
|
+ count uint64, // total count of all items being paged
|
|
|
+ err error,
|
|
|
+ )
|
|
|
}
|
|
|
```
|
|
|
|
|
@@ -34,11 +34,11 @@ And in return, *depager* provides the following:
|
|
|
|
|
|
```go
|
|
|
type Pager[T any] interface {
|
|
|
- // Iter is intended to be used in a for-range loop
|
|
|
- Iter() <-chan T
|
|
|
+ // Iter is intended to be used in a for-range loop
|
|
|
+ Iter() <-chan T
|
|
|
|
|
|
- // LastErr must return the first error encountered, if any
|
|
|
- LastErr() error
|
|
|
+ // LastErr must return the first error encountered, if any
|
|
|
+ LastErr() error
|
|
|
}
|
|
|
|
|
|
```
|
|
@@ -49,112 +49,124 @@ type Pager[T any] interface {
|
|
|
package main
|
|
|
|
|
|
import (
|
|
|
- "context"
|
|
|
- "fmt"
|
|
|
- "log"
|
|
|
- "net/http"
|
|
|
- "net/url"
|
|
|
- "os"
|
|
|
-
|
|
|
- dp "idio.link/go/depager/v2"
|
|
|
+ "context"
|
|
|
+ "fmt"
|
|
|
+ "log"
|
|
|
+ "net/http"
|
|
|
+ "net/url"
|
|
|
+ "os"
|
|
|
+
|
|
|
+ dp "idio.link/go/depager/v3"
|
|
|
)
|
|
|
|
|
|
type MyClient struct {
|
|
|
- pageSize uint64
|
|
|
- // more fields
|
|
|
+ pageSize uint64
|
|
|
+ // more fields
|
|
|
}
|
|
|
|
|
|
func (c *MyClient) get(
|
|
|
- uri *url.URL,
|
|
|
+ uri *url.URL,
|
|
|
) (head http.Header, body io.ReadCloser, err error) {
|
|
|
- // do things
|
|
|
- return
|
|
|
+ // do things
|
|
|
+ return
|
|
|
}
|
|
|
|
|
|
func (c *MyClient) pagify(
|
|
|
- pathURI *url.URL,
|
|
|
- first,
|
|
|
- last uint64,
|
|
|
+ pathURI *url.URL,
|
|
|
+ first,
|
|
|
+ last uint64,
|
|
|
) (uri *url.URL) {
|
|
|
- // glue path to base URI
|
|
|
- return
|
|
|
+ // glue path to base URI
|
|
|
+ return
|
|
|
}
|
|
|
|
|
|
-func (c *MyClient) Things(id int) dp.Pager[*Thing] {
|
|
|
- // TODO validate; if used elsewhere, take boxed id instead
|
|
|
- path := "/pile/%d/things"
|
|
|
- subClient := &MySubclient[*Thing]{
|
|
|
- MyClient: c,
|
|
|
- path: url.Parse(fmt.Sprintf(path, id)),
|
|
|
- }
|
|
|
-
|
|
|
- return dp.NewPager(subClient, c.pageSize)
|
|
|
+func (c *MyClient) Things(id int) dp.Pager[Thing] {
|
|
|
+ // TODO validate; if used elsewhere, take boxed id instead
|
|
|
+ path := "/pile/%d/things"
|
|
|
+ subClient := &MySubclient[Thing]{
|
|
|
+ MyClient: c,
|
|
|
+ path: url.Parse(fmt.Sprintf(path, id)),
|
|
|
+ }
|
|
|
+
|
|
|
+ // The page pool length determines the number of pages
|
|
|
+ // that depager will buffer. All pages must have equal,
|
|
|
+ // nonzero capacity. The capacity of the first page is
|
|
|
+ // taken to be the page size for all requests.
|
|
|
+ pagePool := make(chan Page[Thing], 4)
|
|
|
+ for i := 0; i < cap(pagePool); i++ {
|
|
|
+ tmp := MyAggregate[Thing](make([]Thing, 0, c.pageSize))
|
|
|
+ pagePool <- &tmp
|
|
|
+ }
|
|
|
+ return dp.NewPager(subClient, pagePool)
|
|
|
}
|
|
|
|
|
|
type MySubclient[T any] struct {
|
|
|
- MyClient
|
|
|
+ MyClient
|
|
|
|
|
|
- path *url.URL
|
|
|
+ path *url.URL
|
|
|
}
|
|
|
|
|
|
func (c *MySubclient[T]) NextPage(
|
|
|
- offset uint64,
|
|
|
-) (page dp.Page[T], totalItems uint64, err error) {
|
|
|
- /*
|
|
|
- Different APIs use different conventions. Simply map
|
|
|
- `offset` to the apposite fields or headers with whatever
|
|
|
- semantics the server expects.
|
|
|
-
|
|
|
- Most days, the page size should be the largest that the
|
|
|
- server is willing to accommodate.
|
|
|
- */
|
|
|
- first := offset
|
|
|
- last := first + c.pageSize - 1
|
|
|
-
|
|
|
- uri := c.pagify(c.path, first, last)
|
|
|
- header, body, err := c.get(uri)
|
|
|
- page := &MyAggregate[T]{}
|
|
|
- // parsing, etc.
|
|
|
-
|
|
|
- /*
|
|
|
- When returning the total count of all items to be paged,
|
|
|
- if the server API only provides you the total number of
|
|
|
- pages, simply calculate total*pageSize and return that.
|
|
|
- */
|
|
|
- return page, totalItems, nil
|
|
|
+ page dp.Page[T],
|
|
|
+ offset uint64,
|
|
|
+) (totalItems uint64, err error) {
|
|
|
+ /*
|
|
|
+ Different APIs use different conventions. Simply map
|
|
|
+ `offset` to the apposite fields or headers with whatever
|
|
|
+ semantics the server expects.
|
|
|
+
|
|
|
+ Most days, the page size should be the largest that the
|
|
|
+ server is willing to accommodate.
|
|
|
+ */
|
|
|
+ first := offset
|
|
|
+ last := first + c.pageSize - 1
|
|
|
+
|
|
|
+ uri := c.pagify(c.path, first, last)
|
|
|
+ header, body, err := c.get(uri)
|
|
|
+ // parsing, etc.
|
|
|
+
|
|
|
+ dst := *page.(*MyAggregate[T])
|
|
|
+ dst = dst[:min(cap(dst), len(src))]
|
|
|
+ copy(dst, src) // update values
|
|
|
+ *page.(*MyAggregate[T]) = dst // update slice
|
|
|
+
|
|
|
+ /*
|
|
|
+ When returning the total count of all items to be paged,
|
|
|
+ if the server API only provides you the total number of
|
|
|
+ pages, simply calculate total*pageSize and return that.
|
|
|
+ */
|
|
|
+ return totalItems, nil
|
|
|
}
|
|
|
|
|
|
-type MyAggregate[T any] struct {
|
|
|
- Items []T `json:"items"`
|
|
|
-}
|
|
|
+type MyAggregate[T any] []T
|
|
|
|
|
|
func (a *MyAggregate[T]) Elems() []T {
|
|
|
- return a.Items
|
|
|
+ return []T(*a)
|
|
|
}
|
|
|
|
|
|
type Thing struct {
|
|
|
- Id int32 `json:"id"`
|
|
|
- Name string `json:"name"`
|
|
|
+ Id int32 `json:"id"`
|
|
|
+ Name string `json:"name"`
|
|
|
}
|
|
|
|
|
|
func main() {
|
|
|
- ctx, _ := context.WithCancel(context.Background())
|
|
|
- client := NewMyClient(...)
|
|
|
- id := 82348
|
|
|
- pager := client.Things(id)
|
|
|
- for e := range pager.Iter() {
|
|
|
- // there could be many pages; stay responsive!
|
|
|
- if ctx.Err() != nil {
|
|
|
- break
|
|
|
- }
|
|
|
- // do stuff with each thing
|
|
|
- }
|
|
|
-
|
|
|
- // finally, check for errors
|
|
|
- if err := pager.LastErr(); err != nil {
|
|
|
- log.Printf("do stuff with things: %v", err)
|
|
|
- os.Exit(1)
|
|
|
- }
|
|
|
+ ctx, _ := context.WithCancel(context.Background())
|
|
|
+ client := NewMyClient(...)
|
|
|
+ id := 82348
|
|
|
+ pager := client.Things(id)
|
|
|
+ for e := range pager.Iter() {
|
|
|
+ // there could be many pages; stay responsive!
|
|
|
+ if ctx.Err() != nil {
|
|
|
+ break
|
|
|
+ }
|
|
|
+ // do stuff with each thing
|
|
|
+ }
|
|
|
+
|
|
|
+ // finally, check for errors
|
|
|
+ if err := pager.LastErr(); err != nil {
|
|
|
+ log.Printf("do stuff with things: %v", err)
|
|
|
+ os.Exit(1)
|
|
|
+ }
|
|
|
}
|
|
|
```
|
|
|
|