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  }