Skip to content

Commit

Permalink
Encode cursor
Browse files Browse the repository at this point in the history
  • Loading branch information
rkettelerij committed Sep 29, 2023
1 parent 7e7a66e commit bcaadea
Show file tree
Hide file tree
Showing 9 changed files with 65 additions and 43 deletions.
2 changes: 1 addition & 1 deletion ogc/features/datasources/datasource.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (
type Datasource interface {

// GetFeatures returns a FeatureCollection from the underlying datasource and a Cursor for pagination
GetFeatures(collection string, cursor int, limit int) (*domain.FeatureCollection, domain.Cursor)
GetFeatures(collection string, cursor int64, limit int) (*domain.FeatureCollection, domain.Cursor)

// GetFeature returns a specific Feature from the FeatureCollection of the underlying datasource
GetFeature(collection string, featureID string) *domain.Feature
Expand Down
12 changes: 6 additions & 6 deletions ogc/features/datasources/fakedb.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,13 @@ func (FakeDB) Close() {
// noop
}

func (fdb FakeDB) GetFeatures(_ string, cursor int, limit int) (*domain.FeatureCollection, domain.Cursor) {
func (fdb FakeDB) GetFeatures(_ string, cursor int64, limit int) (*domain.FeatureCollection, domain.Cursor) {
low := cursor
high := low + limit
high := low + int64(limit)

last := high > len(fdb.featureCollection.Features)
last := high > int64(len(fdb.featureCollection.Features))
if last {
high = len(fdb.featureCollection.Features)
high = int64(len(fdb.featureCollection.Features))
}
if high < 0 {
high = 0
Expand Down Expand Up @@ -68,7 +68,7 @@ func generateFakeFeatureCollection() *domain.FeatureCollection {
"purpose": gofakeit.Blurb(),

// we use an explicit cursor column in our fake data to keep things simple
cursorColumnName: i,
cursorColumnName: int64(i),
}

feature := domain.Feature{}
Expand All @@ -81,7 +81,7 @@ func generateFakeFeatureCollection() *domain.FeatureCollection {

// the collection must be ordered by the cursor column
sort.Slice(feats, func(i, j int) bool {
return feats[i].Properties[cursorColumnName].(int) < feats[j].Properties[cursorColumnName].(int)
return feats[i].Properties[cursorColumnName].(int64) < feats[j].Properties[cursorColumnName].(int64)
})

fc := domain.FeatureCollection{}
Expand Down
2 changes: 1 addition & 1 deletion ogc/features/datasources/geopackage.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ func (GeoPackage) Close() {
// TODO: clean up DB connection to gpkg
}

func (GeoPackage) GetFeatures(collection string, cursor int, limit int) (*domain.FeatureCollection, domain.Cursor) {
func (GeoPackage) GetFeatures(collection string, cursor int64, limit int) (*domain.FeatureCollection, domain.Cursor) {
// TODO: not implemented yet
log.Printf("TODO: return data from gpkg for collection %s using cursor %d with limt %d",
collection, cursor, limit)
Expand Down
78 changes: 50 additions & 28 deletions ogc/features/domain/domain.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,19 @@ import (
"strconv"

"github.com/go-spatial/geom/encoding/geojson"
"github.com/sqids/sqids-go"
)

const (
cursorAlphabet = "1Vti5BYcjOdTXunDozKPm4syvG6galxLM8eIrUS2bWqZCNkwpR309JFAHfh7EQ" // generated on https://sqids.org/playground
)

var (
cursorCodec, _ = sqids.New(sqids.Options{
Alphabet: cursorAlphabet,
Blocklist: nil, // disable blocklist
MinLength: 8,
})
)

// featureCollectionType allows the GeoJSON type to be automatically set during json marshalling
Expand Down Expand Up @@ -47,38 +60,18 @@ type Link struct {

// Cursor since we use cursor-based pagination as opposed to offset-based pagination
type Cursor struct {
Prev EncodedCursorValue
Next EncodedCursorValue
Prev EncodedCursor
Next EncodedCursor

IsFirst bool
IsLast bool
}

type EncodedCursorValue string

func (c EncodedCursorValue) Decode() int {
decoded := string(c)
var result int
if decoded == "" {
result = 0
} else {
result, _ = strconv.Atoi(decoded)
if result < 0 {
result = 0
}
}
return result
}

func encodeCursorValue(value int) EncodedCursorValue {
return EncodedCursorValue(strconv.Itoa(value))
}

func NewCursor(features []*Feature, column string, limit int, last bool) Cursor {
if len(features) == 0 {
return Cursor{}
}
max := len(features) - 1
max := int64(len(features) - 1)

start := features[0].Properties[column]
end := features[max].Properties[column]
Expand All @@ -92,20 +85,49 @@ func NewCursor(features []*Feature, column string, limit int, last bool) Cursor
end = 0
}

prev := start.(int)
prev := start.(int64)
if prev != 0 {
prev -= max
if prev < 0 {
prev = 0
}
}
next := end.(int)
next := end.(int64)

return Cursor{
Prev: encodeCursorValue(prev),
Next: encodeCursorValue(next),
Prev: encodeCursor(prev),
Next: encodeCursor(next),

IsFirst: next < limit,
IsFirst: next < int64(limit),
IsLast: last,
}
}

// EncodedCursor is a scrambled string representation of a consecutive ordered integer cursor
type EncodedCursor string

func encodeCursor(value int64) EncodedCursor {
encodedValue, err := cursorCodec.Encode([]uint64{uint64(value)})
if err != nil {
log.Printf("failed to encode cursor value %d, defaulting to unencoded value.", value)
return EncodedCursor(strconv.FormatInt(value, 10))
}
return EncodedCursor(encodedValue)
}

func (c EncodedCursor) Decode() int64 {
value := string(c)
if value == "" {
return 0
}
decodedValue := cursorCodec.Decode(value)
if len(decodedValue) > 1 {
log.Printf("encountered more than one cursor value after decoding: '%v', "+
"this is not allowed! Defaulting to first value.", decodedValue)
}
if len(decodedValue) == 0 {
log.Printf("decoding cursor value '%v' failed, defaulting to first page", decodedValue)
return 0
}
return int64(decodedValue[0])
}
4 changes: 2 additions & 2 deletions ogc/features/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ func NewFeatures(e *engine.Engine, router *chi.Mux) *Features {
func (f *Features) CollectionContent() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
collectionID := chi.URLParam(r, "collectionId")
encodedCursorValue := domain.EncodedCursorValue(r.URL.Query().Get("cursor"))
encodedCursor := domain.EncodedCursor(r.URL.Query().Get("cursor"))
limit, err := getLimit(r)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
Expand All @@ -72,7 +72,7 @@ func (f *Features) CollectionContent() http.HandlerFunc {
return
}

fc, cursor := f.datasource.GetFeatures(collectionID, encodedCursorValue.Decode(), limit)
fc, cursor := f.datasource.GetFeatures(collectionID, encodedCursor.Decode(), limit)
if fc == nil {
http.NotFound(w, r)
return
Expand Down
2 changes: 1 addition & 1 deletion ogc/features/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ func TestFeatures_CollectionContent(t *testing.T) {
name: "Request GeoJSON for 'foo' collection using limit of 2 and cursor to next page",
fields: fields{
configFile: "ogc/features/testdata/config_features.yaml",
url: "http://localhost:8080/collections/:collectionId/items?cursor=9&limit=2",
url: "http://localhost:8080/collections/tunneldelen/items?f=json&cursor=iUMnUmcz&limit=2",
collectionID: "foo",
format: "json",
},
Expand Down
2 changes: 1 addition & 1 deletion ogc/features/testdata/expected_foo_collection.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"rel": "next",
"title": "Next page",
"type": "application/geo+json",
"href": "http://localhost:8080/collections/foo/items?f=json\u0026cursor=9\u0026limit=10"
"href": "http://localhost:8080/collections/foo/items?f=json&cursor=iUMnUmcz&limit=10"
}
],
"numberReturned": 10,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,13 @@
"rel": "next",
"title": "Next page",
"type": "application/geo+json",
"href": "http://localhost:8080/collections/foo/items?f=json\u0026cursor=10\u0026limit=2"
"href": "http://localhost:8080/collections/foo/items?f=json&cursor=9GxZGfBr&limit=2"
},
{
"rel": "prev",
"title": "Previous page",
"type": "application/geo+json",
"href": "http://localhost:8080/collections/foo/items?f=json\u0026cursor=8\u0026limit=2"
"href": "http://localhost:8080/collections/foo/items?f=json&cursor=VCHYvtZJ&limit=2"
}
],
"numberReturned": 2,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"rel": "next",
"title": "Next page",
"type": "application/geo+json",
"href": "http://localhost:8080/collections/foo/items?f=json\u0026cursor=1\u0026limit=2"
"href": "http://localhost:8080/collections/foo/items?f=json&cursor=KXzJBocg&limit=2"
}
],
"numberReturned": 2,
Expand Down

0 comments on commit bcaadea

Please sign in to comment.