github.com/PDOK/gokoala@v0.50.6/internal/ogc/features/domain/cursor.go (about) 1 package domain 2 3 import ( 4 "bytes" 5 "encoding/base64" 6 "log" 7 "math/big" 8 neturl "net/url" 9 "strings" 10 ) 11 12 const separator = '|' 13 14 // Cursors holds next and previous cursor. Note that we use 15 // 'cursor-based pagination' as opposed to 'offset-based pagination' 16 type Cursors struct { 17 Prev EncodedCursor 18 Next EncodedCursor 19 20 HasPrev bool 21 HasNext bool 22 } 23 24 // EncodedCursor is a scrambled string representation of the fields defined in DecodedCursor 25 type EncodedCursor string 26 27 // DecodedCursor the cursor values after decoding EncodedCursor 28 type DecodedCursor struct { 29 FiltersChecksum []byte 30 FID int64 31 } 32 33 // PrevNextFID previous and next feature id (fid) to encode in cursor. 34 type PrevNextFID struct { 35 Prev int64 36 Next int64 37 } 38 39 // NewCursors create Cursors based on the prev/next feature ids from the datasource 40 // and the provided filters (captured in a hash). 41 func NewCursors(fid PrevNextFID, filtersChecksum []byte) Cursors { 42 return Cursors{ 43 Prev: encodeCursor(fid.Prev, filtersChecksum), 44 Next: encodeCursor(fid.Next, filtersChecksum), 45 46 HasPrev: fid.Prev > 0, 47 HasNext: fid.Next > 0, 48 } 49 } 50 51 func encodeCursor(fid int64, filtersChecksum []byte) EncodedCursor { 52 fidAsBytes := big.NewInt(fid).Bytes() 53 54 // format of the cursor: <encoded fid><separator><encoded checksum> 55 return EncodedCursor(base64.RawURLEncoding.EncodeToString(fidAsBytes) + string(separator) + base64.RawURLEncoding.EncodeToString(filtersChecksum)) 56 } 57 58 // Decode turns encoded cursor into DecodedCursor and verifies the 59 // that the checksum of query params that act as filters hasn't changed 60 func (c EncodedCursor) Decode(filtersChecksum []byte) DecodedCursor { 61 value, err := neturl.QueryUnescape(string(c)) 62 if err != nil || value == "" { 63 return DecodedCursor{filtersChecksum, 0} 64 } 65 66 // split first, then decode 67 encoded := strings.Split(value, string(separator)) 68 if len(encoded) < 2 { 69 log.Printf("cursor '%s' doesn't contain expected separator %c", value, separator) 70 return DecodedCursor{filtersChecksum, 0} 71 } 72 decodedFid, fidErr := base64.RawURLEncoding.DecodeString(encoded[0]) 73 decodedChecksum, checksumErr := base64.RawURLEncoding.DecodeString(encoded[1]) 74 if fidErr != nil || checksumErr != nil { 75 log.Printf("decoding cursor value '%s' failed, defaulting to first page", value) 76 return DecodedCursor{filtersChecksum, 0} 77 } 78 79 // feature id 80 fid := big.NewInt(0).SetBytes(decodedFid).Int64() 81 if fid < 0 { 82 log.Printf("negative feature ID detected: %d, defaulting to first page", fid) 83 fid = 0 84 } 85 86 // checksum 87 if !bytes.Equal(decodedChecksum, filtersChecksum) { 88 log.Printf("filters in query params have changed during pagination, resetting to first page") 89 return DecodedCursor{filtersChecksum, 0} 90 } 91 92 return DecodedCursor{filtersChecksum, fid} 93 } 94 95 func (c EncodedCursor) String() string { 96 return string(c) 97 }