|
@@ -3,9 +3,7 @@
|
|
|
Trades REST API paging logic and request throttling for a
|
|
|
channel and a `for` loop.
|
|
|
|
|
|
-For the moment, *depager* only supports JSON responses.
|
|
|
-
|
|
|
-*depager* requires structs conforming to the following
|
|
|
+*depager* requires values conforming to the following
|
|
|
interfaces:
|
|
|
|
|
|
```go
|
|
@@ -13,129 +11,150 @@ interfaces:
|
|
|
The `Page` interface must wrap server responses. This
|
|
|
allows pagers to calculate page sizes and iterate over
|
|
|
response aggregates.
|
|
|
-
|
|
|
-The underlying implementation must be a pointer to a
|
|
|
-struct containing the desired response fields, all tagged
|
|
|
-appropriately. Any fields corresponding to
|
|
|
-platform-specific error responses should also be
|
|
|
-included.
|
|
|
*/
|
|
|
type Page[T any] interface {
|
|
|
- // Count must return the total number of items to be paged
|
|
|
- Count() uint64
|
|
|
-
|
|
|
- // Elems must return the items from the current page
|
|
|
+ // Elems must return the items from the current page
|
|
|
Elems() []T
|
|
|
}
|
|
|
|
|
|
-type Client interface {
|
|
|
- Get(uri url.URL) ([]byte, error)
|
|
|
-}
|
|
|
-
|
|
|
-type PagedURI interface {
|
|
|
- PageURI(limit, offset uint64) url.URL
|
|
|
+// 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,
|
|
|
+ )
|
|
|
}
|
|
|
-
|
|
|
```
|
|
|
|
|
|
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
|
|
|
+
|
|
|
+ // LastErr must return the first error encountered, if any
|
|
|
LastErr() error
|
|
|
}
|
|
|
+
|
|
|
```
|
|
|
|
|
|
## Example
|
|
|
|
|
|
```go
|
|
|
+package main
|
|
|
+
|
|
|
import (
|
|
|
- dp "idio.link/go/depager"
|
|
|
+ "context"
|
|
|
+ "fmt"
|
|
|
+ "log"
|
|
|
+ "net/http"
|
|
|
+ "net/url"
|
|
|
+ "os"
|
|
|
+
|
|
|
+ dp "idio.link/go/depager/v2"
|
|
|
)
|
|
|
|
|
|
-type pagedURI struct {
|
|
|
- uri *url.URL
|
|
|
+type MyClient struct {
|
|
|
+ pageSize uint64
|
|
|
+ // more fields
|
|
|
}
|
|
|
|
|
|
-func (u pagedURI) PageURI(limit, offset uint64) url.URL {
|
|
|
- /*
|
|
|
- Different APIs use different conventions. Simply map
|
|
|
- limit and offset to the apposite field names with
|
|
|
- whatever semantics the server expects.
|
|
|
- */
|
|
|
- if limit > MaxPageSize {
|
|
|
- limit = MaxPageSize
|
|
|
- }
|
|
|
- uri := *u.uri
|
|
|
- q := (&uri).Query()
|
|
|
- q.Add("first", strconv.FormatUint(offset, 10))
|
|
|
- q.Add("last", strconv.FormatUint(offset+limit-1, 10))
|
|
|
- (&uri).RawQuery = q.Encode()
|
|
|
- return uri
|
|
|
+func (c *MyClient) get(
|
|
|
+ uri *url.URL,
|
|
|
+) (head http.Header, body io.ReadCloser, err error) {
|
|
|
+ // do things
|
|
|
+ return
|
|
|
}
|
|
|
|
|
|
-func NewMyAggregate[T any]() *MyAggregate[T] {
|
|
|
- return &MyAggregate[T]{Items: make([]T, 0, 64)}
|
|
|
+func (c *MyClient) pagify(
|
|
|
+ pathURI *url.URL,
|
|
|
+ first,
|
|
|
+ last uint64,
|
|
|
+) (uri *url.URL) {
|
|
|
+ // glue path to base URI
|
|
|
+ return
|
|
|
}
|
|
|
|
|
|
-type MyAggregate[T any] struct {
|
|
|
- Total int32 `json:"total"`
|
|
|
- Items []T `json:"items"`
|
|
|
+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 (a *MyAggregate[_]) Count() uint64 {
|
|
|
- return uint64(a.Total)
|
|
|
+type MySubclient[T any] struct {
|
|
|
+ MyClient
|
|
|
+
|
|
|
+ path *url.URL
|
|
|
}
|
|
|
|
|
|
-func (a *MyAggregate[T]) Elems() []T {
|
|
|
- return a.Items
|
|
|
+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
|
|
|
}
|
|
|
|
|
|
-// The client can use this func for all its paged responses.
|
|
|
-func makeMyPager[T any](
|
|
|
- client *MyClient,
|
|
|
- resource *url.URL,
|
|
|
-) dp.Pager[T] {
|
|
|
- return dp.NewPager(
|
|
|
- pagedURI{resource},
|
|
|
- client,
|
|
|
- MaxPageSize, // most days, this should be the maximum page size that the server will provide
|
|
|
- func() dp.Page[T] {
|
|
|
- return NewMyAggregate[T]()
|
|
|
- },
|
|
|
- )
|
|
|
+type MyAggregate[T any] struct {
|
|
|
+ Items []T `json:"items"`
|
|
|
}
|
|
|
|
|
|
-/*
|
|
|
-.
|
|
|
-.
|
|
|
-.
|
|
|
-*/
|
|
|
+func (a *MyAggregate[T]) Elems() []T {
|
|
|
+ return a.Items
|
|
|
+}
|
|
|
|
|
|
type Thing struct {
|
|
|
- Id int32 `json:"id"`
|
|
|
+ Id int32 `json:"id"`
|
|
|
Name string `json:"name"`
|
|
|
}
|
|
|
|
|
|
func main() {
|
|
|
- id := 82348
|
|
|
- var thingPager dp.Pager[*Thing] =
|
|
|
- client.GetThings(id)
|
|
|
-
|
|
|
- for thing := range thingPager.Iter() {
|
|
|
- // stay responsive!
|
|
|
- if ctx.Err() != nil {
|
|
|
- break
|
|
|
- }
|
|
|
- // do stuff with each thing
|
|
|
- }
|
|
|
+ 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 := thingPager.LastErr(); err != nil {
|
|
|
- log.Printf("do stuff with things: %v", err)
|
|
|
- os.Exit(1)
|
|
|
- }
|
|
|
+ // finally, check for errors
|
|
|
+ if err := pager.LastErr(); err != nil {
|
|
|
+ log.Printf("do stuff with things: %v", err)
|
|
|
+ os.Exit(1)
|
|
|
+ }
|
|
|
}
|
|
|
```
|
|
|
|