Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
1b154c2458
|
|||
|
70df2b61b1
|
|||
|
f4b4978e62
|
|||
|
145b1138d7
|
|||
|
fad2e4ff6d
|
@@ -4,6 +4,7 @@ package {{.PkgName}}
|
||||
|
||||
import "go.mongodb.org/mongo-driver/v2/bson"
|
||||
import "git.blackforestbytes.com/BlackForestBytes/goext/exerr"
|
||||
import "git.blackforestbytes.com/BlackForestBytes/goext/wmo"
|
||||
|
||||
const ChecksumIDGenerator = "{{.Checksum}}" // GoExtVersion: {{.GoextVersion}}
|
||||
|
||||
@@ -52,4 +53,6 @@ func New{{.Name}}() {{.Name}} {
|
||||
return {{.Name}}(bson.NewObjectID().Hex())
|
||||
}
|
||||
|
||||
var _ wmo.MongoEntityID = (*{{.Name}})(nil)
|
||||
|
||||
{{end}}
|
||||
@@ -3,10 +3,11 @@ package cursortoken
|
||||
import (
|
||||
"encoding/base32"
|
||||
"encoding/json"
|
||||
"git.blackforestbytes.com/BlackForestBytes/goext/exerr"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.blackforestbytes.com/BlackForestBytes/goext/exerr"
|
||||
)
|
||||
|
||||
type CursorToken interface {
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
package dataext
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"iter"
|
||||
)
|
||||
|
||||
// OrderedMap is like a normal map[TKey, TVal] - but its elements stay in order
|
||||
// NOT THREADSAFE !!!
|
||||
type OrderedMap[TKey comparable, TVal any] struct {
|
||||
m map[TKey]*TVal
|
||||
a []TKey
|
||||
}
|
||||
|
||||
func NewOrderedMap[TKey comparable, TVal any](caps ...int) *OrderedMap[TKey, TVal] {
|
||||
if len(caps) == 0 {
|
||||
return &OrderedMap[TKey, TVal]{
|
||||
m: make(map[TKey]*TVal),
|
||||
a: make([]TKey, 0),
|
||||
}
|
||||
}
|
||||
|
||||
omcap := 0
|
||||
for _, v := range caps {
|
||||
omcap += v
|
||||
}
|
||||
|
||||
return &OrderedMap[TKey, TVal]{
|
||||
m: make(map[TKey]*TVal, omcap),
|
||||
a: make([]TKey, 0, omcap),
|
||||
}
|
||||
}
|
||||
|
||||
func (o *OrderedMap[TKey, TVal]) Get(key TKey) (TVal, bool) {
|
||||
v, ok := o.m[key]
|
||||
if ok {
|
||||
return *v, ok
|
||||
}
|
||||
|
||||
return *new(TVal), false
|
||||
}
|
||||
|
||||
func (o *OrderedMap[TKey, TVal]) GetOrNil(key TKey) *TVal {
|
||||
v, ok := o.m[key]
|
||||
if ok {
|
||||
return v
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *OrderedMap[TKey, TVal]) GetOrDefault(key TKey, defaultVal TVal) TVal {
|
||||
v, ok := o.m[key]
|
||||
if ok {
|
||||
return *v
|
||||
}
|
||||
|
||||
return defaultVal
|
||||
}
|
||||
|
||||
// Add adds the new value to the map
|
||||
// At the end of the map-ordering (even if key already exists, its then "moved" to the end)
|
||||
// returns true if key already existed
|
||||
func (o *OrderedMap[TKey, TVal]) Add(key TKey, val TVal) bool {
|
||||
if _, ok := o.m[key]; ok {
|
||||
o.remFromArray(key)
|
||||
o.m[key] = &val
|
||||
o.a = append(o.a, key)
|
||||
return true
|
||||
}
|
||||
|
||||
o.m[key] = &val
|
||||
o.a = append(o.a, key)
|
||||
return false
|
||||
}
|
||||
|
||||
// AddOrReplace adds the new value to the map
|
||||
// Normally at the end of the map, but if teh key already exists, its only replaced
|
||||
// returns true if key already existed
|
||||
func (o *OrderedMap[TKey, TVal]) AddOrReplace(key TKey, val TVal) bool {
|
||||
if _, ok := o.m[key]; ok {
|
||||
o.m[key] = &val
|
||||
return true
|
||||
}
|
||||
|
||||
o.m[key] = &val
|
||||
o.a = append(o.a, key)
|
||||
return false
|
||||
}
|
||||
|
||||
func (o *OrderedMap[TKey, TVal]) Remove(key TKey) bool {
|
||||
if _, ok := o.m[key]; ok {
|
||||
o.remFromArray(key)
|
||||
delete(o.m, key)
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (o *OrderedMap[TKey, TVal]) Iterate() iter.Seq2[TKey, TVal] {
|
||||
return func(yield func(TKey, TVal) bool) {
|
||||
for _, v := range o.a {
|
||||
if !yield(v, *o.m[v]) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (o *OrderedMap[TKey, TVal]) IterateValues() iter.Seq[TVal] {
|
||||
return func(yield func(TVal) bool) {
|
||||
for _, v := range o.a {
|
||||
if !yield(*o.m[v]) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (o *OrderedMap[TKey, TVal]) IterateKeys() iter.Seq[TKey] {
|
||||
return func(yield func(TKey) bool) {
|
||||
for _, v := range o.a {
|
||||
if !yield(v) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (o *OrderedMap[TKey, TVal]) Array() []TVal {
|
||||
res := make([]TVal, len(o.a))
|
||||
for i, v := range o.a {
|
||||
res[i] = *o.m[v]
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
func (o *OrderedMap[TKey, TVal]) Keys() []TKey {
|
||||
res := make([]TKey, len(o.a))
|
||||
for i, v := range o.a {
|
||||
res[i] = v
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
func (o *OrderedMap[TKey, TVal]) Clear() {
|
||||
mapCap := max(len(o.m), cap(o.a))
|
||||
o.m = make(map[TKey]*TVal, mapCap)
|
||||
o.a = make([]TKey, 0, mapCap)
|
||||
}
|
||||
|
||||
func (o *OrderedMap[TKey, TVal]) Size() int {
|
||||
return len(o.a)
|
||||
}
|
||||
|
||||
func (o *OrderedMap[TKey, TVal]) Capacity() int {
|
||||
return min(cap(o.a), len(o.m))
|
||||
}
|
||||
|
||||
func (o *OrderedMap[TKey, TVal]) Contains(key TKey) bool {
|
||||
_, ok := o.m[key]
|
||||
return ok
|
||||
}
|
||||
|
||||
func (o *OrderedMap[TKey, TVal]) IndexOf(key TKey) int {
|
||||
for i, v := range o.a {
|
||||
if v == key {
|
||||
return i
|
||||
}
|
||||
}
|
||||
|
||||
return -1
|
||||
}
|
||||
|
||||
func (o *OrderedMap[TKey, TVal]) remFromArray(key TKey) {
|
||||
for i, v := range o.a {
|
||||
if v == key {
|
||||
o.a = append(o.a[:i], o.a[i+1:]...)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
panic(fmt.Sprintf("Failed to remove key from OrderedMap -- key '%v' not found", key))
|
||||
}
|
||||
@@ -0,0 +1,429 @@
|
||||
package dataext
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"testing"
|
||||
|
||||
"git.blackforestbytes.com/BlackForestBytes/goext/tst"
|
||||
)
|
||||
|
||||
func TestOrderedMapNew(t *testing.T) {
|
||||
m := NewOrderedMap[string, int](4)
|
||||
|
||||
tst.AssertEqual(t, m.Size(), 0)
|
||||
tst.AssertArrayEqual(t, m.Keys(), []string{})
|
||||
tst.AssertArrayEqual(t, m.Array(), []int{})
|
||||
}
|
||||
|
||||
func TestOrderedMapNewNoCap(t *testing.T) {
|
||||
m := NewOrderedMap[string, int]()
|
||||
|
||||
tst.AssertEqual(t, m.Size(), 0)
|
||||
tst.AssertArrayEqual(t, m.Keys(), []string{})
|
||||
tst.AssertArrayEqual(t, m.Array(), []int{})
|
||||
}
|
||||
|
||||
func TestOrderedMapNewWIthCap(t *testing.T) {
|
||||
m := NewOrderedMap[string, int](4)
|
||||
|
||||
tst.AssertEqual(t, m.Size(), 0)
|
||||
tst.AssertArrayEqual(t, m.Keys(), []string{})
|
||||
tst.AssertArrayEqual(t, m.Array(), []int{})
|
||||
}
|
||||
|
||||
func TestOrderedMapAddAndGet(t *testing.T) {
|
||||
m := NewOrderedMap[string, int](0)
|
||||
|
||||
tst.AssertFalse(t, m.Add("a", 1))
|
||||
tst.AssertFalse(t, m.Add("b", 2))
|
||||
tst.AssertFalse(t, m.Add("c", 3))
|
||||
|
||||
tst.AssertEqual(t, m.Size(), 3)
|
||||
|
||||
v, ok := m.Get("a")
|
||||
tst.AssertTrue(t, ok)
|
||||
tst.AssertEqual(t, v, 1)
|
||||
|
||||
v, ok = m.Get("b")
|
||||
tst.AssertTrue(t, ok)
|
||||
tst.AssertEqual(t, v, 2)
|
||||
|
||||
v, ok = m.Get("c")
|
||||
tst.AssertTrue(t, ok)
|
||||
tst.AssertEqual(t, v, 3)
|
||||
|
||||
v, ok = m.Get("missing")
|
||||
tst.AssertFalse(t, ok)
|
||||
tst.AssertEqual(t, v, 0)
|
||||
}
|
||||
|
||||
func TestOrderedMapOrderPreserved(t *testing.T) {
|
||||
m := NewOrderedMap[string, int](0)
|
||||
|
||||
m.Add("first", 1)
|
||||
m.Add("second", 2)
|
||||
m.Add("third", 3)
|
||||
m.Add("fourth", 4)
|
||||
|
||||
tst.AssertArrayEqual(t, m.Keys(), []string{"first", "second", "third", "fourth"})
|
||||
tst.AssertArrayEqual(t, m.Array(), []int{1, 2, 3, 4})
|
||||
}
|
||||
|
||||
func TestOrderedMapAddMovesExistingToEnd(t *testing.T) {
|
||||
m := NewOrderedMap[string, int](0)
|
||||
|
||||
m.Add("a", 1)
|
||||
m.Add("b", 2)
|
||||
m.Add("c", 3)
|
||||
|
||||
tst.AssertTrue(t, m.Add("a", 10))
|
||||
|
||||
tst.AssertArrayEqual(t, m.Keys(), []string{"b", "c", "a"})
|
||||
tst.AssertArrayEqual(t, m.Array(), []int{2, 3, 10})
|
||||
|
||||
v, ok := m.Get("a")
|
||||
tst.AssertTrue(t, ok)
|
||||
tst.AssertEqual(t, v, 10)
|
||||
}
|
||||
|
||||
func TestOrderedMapAddOrReplaceKeepsOrder(t *testing.T) {
|
||||
m := NewOrderedMap[string, int](0)
|
||||
|
||||
m.Add("a", 1)
|
||||
m.Add("b", 2)
|
||||
m.Add("c", 3)
|
||||
|
||||
tst.AssertTrue(t, m.AddOrReplace("b", 99))
|
||||
|
||||
tst.AssertArrayEqual(t, m.Keys(), []string{"a", "b", "c"})
|
||||
tst.AssertArrayEqual(t, m.Array(), []int{1, 99, 3})
|
||||
|
||||
v, ok := m.Get("b")
|
||||
tst.AssertTrue(t, ok)
|
||||
tst.AssertEqual(t, v, 99)
|
||||
}
|
||||
|
||||
func TestOrderedMapAddOrReplaceNew(t *testing.T) {
|
||||
m := NewOrderedMap[string, int](0)
|
||||
|
||||
tst.AssertFalse(t, m.AddOrReplace("a", 1))
|
||||
tst.AssertFalse(t, m.AddOrReplace("b", 2))
|
||||
|
||||
tst.AssertArrayEqual(t, m.Keys(), []string{"a", "b"})
|
||||
tst.AssertArrayEqual(t, m.Array(), []int{1, 2})
|
||||
}
|
||||
|
||||
func TestOrderedMapGetOrNil(t *testing.T) {
|
||||
m := NewOrderedMap[string, int](0)
|
||||
m.Add("a", 42)
|
||||
|
||||
v := m.GetOrNil("a")
|
||||
if v == nil {
|
||||
t.Errorf("expected non-nil pointer")
|
||||
return
|
||||
}
|
||||
tst.AssertEqual(t, *v, 42)
|
||||
|
||||
v = m.GetOrNil("missing")
|
||||
if v != nil {
|
||||
t.Errorf("expected nil pointer")
|
||||
}
|
||||
}
|
||||
|
||||
func TestOrderedMapGetOrDefault(t *testing.T) {
|
||||
m := NewOrderedMap[string, int](0)
|
||||
m.Add("a", 42)
|
||||
|
||||
tst.AssertEqual(t, m.GetOrDefault("a", -1), 42)
|
||||
tst.AssertEqual(t, m.GetOrDefault("missing", -1), -1)
|
||||
}
|
||||
|
||||
func TestOrderedMapRemove(t *testing.T) {
|
||||
m := NewOrderedMap[string, int](0)
|
||||
|
||||
m.Add("a", 1)
|
||||
m.Add("b", 2)
|
||||
m.Add("c", 3)
|
||||
|
||||
tst.AssertTrue(t, m.Remove("b"))
|
||||
|
||||
tst.AssertEqual(t, m.Size(), 2)
|
||||
tst.AssertArrayEqual(t, m.Keys(), []string{"a", "c"})
|
||||
tst.AssertArrayEqual(t, m.Array(), []int{1, 3})
|
||||
|
||||
_, ok := m.Get("b")
|
||||
tst.AssertFalse(t, ok)
|
||||
tst.AssertFalse(t, m.Contains("b"))
|
||||
}
|
||||
|
||||
func TestOrderedMapRemoveMissing(t *testing.T) {
|
||||
m := NewOrderedMap[string, int](0)
|
||||
|
||||
m.Add("a", 1)
|
||||
tst.AssertFalse(t, m.Remove("missing"))
|
||||
|
||||
tst.AssertEqual(t, m.Size(), 1)
|
||||
tst.AssertArrayEqual(t, m.Keys(), []string{"a"})
|
||||
}
|
||||
|
||||
func TestOrderedMapRemoveFirstAndLast(t *testing.T) {
|
||||
m := NewOrderedMap[string, int](0)
|
||||
|
||||
m.Add("a", 1)
|
||||
m.Add("b", 2)
|
||||
m.Add("c", 3)
|
||||
m.Add("d", 4)
|
||||
|
||||
tst.AssertTrue(t, m.Remove("a"))
|
||||
tst.AssertTrue(t, m.Remove("d"))
|
||||
|
||||
tst.AssertArrayEqual(t, m.Keys(), []string{"b", "c"})
|
||||
tst.AssertArrayEqual(t, m.Array(), []int{2, 3})
|
||||
}
|
||||
|
||||
func TestOrderedMapContains(t *testing.T) {
|
||||
m := NewOrderedMap[string, int](0)
|
||||
m.Add("a", 1)
|
||||
|
||||
tst.AssertTrue(t, m.Contains("a"))
|
||||
tst.AssertFalse(t, m.Contains("b"))
|
||||
|
||||
m.Remove("a")
|
||||
tst.AssertFalse(t, m.Contains("a"))
|
||||
}
|
||||
|
||||
func TestOrderedMapIndexOf(t *testing.T) {
|
||||
m := NewOrderedMap[string, int](0)
|
||||
m.Add("a", 1)
|
||||
m.Add("b", 2)
|
||||
m.Add("c", 3)
|
||||
|
||||
tst.AssertEqual(t, m.IndexOf("a"), 0)
|
||||
tst.AssertEqual(t, m.IndexOf("b"), 1)
|
||||
tst.AssertEqual(t, m.IndexOf("c"), 2)
|
||||
tst.AssertEqual(t, m.IndexOf("missing"), -1)
|
||||
|
||||
m.Add("a", 10) // moves to end
|
||||
tst.AssertEqual(t, m.IndexOf("a"), 2)
|
||||
tst.AssertEqual(t, m.IndexOf("b"), 0)
|
||||
tst.AssertEqual(t, m.IndexOf("c"), 1)
|
||||
}
|
||||
|
||||
func TestOrderedMapClear(t *testing.T) {
|
||||
m := NewOrderedMap[string, int](0)
|
||||
m.Add("a", 1)
|
||||
m.Add("b", 2)
|
||||
m.Add("c", 3)
|
||||
|
||||
m.Clear()
|
||||
|
||||
tst.AssertEqual(t, m.Size(), 0)
|
||||
tst.AssertArrayEqual(t, m.Keys(), []string{})
|
||||
tst.AssertArrayEqual(t, m.Array(), []int{})
|
||||
tst.AssertFalse(t, m.Contains("a"))
|
||||
|
||||
m.Add("x", 99)
|
||||
tst.AssertEqual(t, m.Size(), 1)
|
||||
tst.AssertArrayEqual(t, m.Keys(), []string{"x"})
|
||||
}
|
||||
|
||||
func TestOrderedMapSize(t *testing.T) {
|
||||
m := NewOrderedMap[string, int](0)
|
||||
tst.AssertEqual(t, m.Size(), 0)
|
||||
|
||||
m.Add("a", 1)
|
||||
tst.AssertEqual(t, m.Size(), 1)
|
||||
|
||||
m.Add("b", 2)
|
||||
tst.AssertEqual(t, m.Size(), 2)
|
||||
|
||||
m.Add("a", 10) // replaces, size stays
|
||||
tst.AssertEqual(t, m.Size(), 2)
|
||||
|
||||
m.AddOrReplace("b", 20) // replaces, size stays
|
||||
tst.AssertEqual(t, m.Size(), 2)
|
||||
|
||||
m.Remove("a")
|
||||
tst.AssertEqual(t, m.Size(), 1)
|
||||
}
|
||||
|
||||
func TestOrderedMapIterate(t *testing.T) {
|
||||
m := NewOrderedMap[string, int](0)
|
||||
m.Add("a", 1)
|
||||
m.Add("b", 2)
|
||||
m.Add("c", 3)
|
||||
|
||||
got := make([]int, 0)
|
||||
for v := range m.IterateValues() {
|
||||
got = append(got, v)
|
||||
}
|
||||
|
||||
tst.AssertArrayEqual(t, got, []int{1, 2, 3})
|
||||
}
|
||||
|
||||
func TestOrderedMapIterateBreak(t *testing.T) {
|
||||
m := NewOrderedMap[string, int](0)
|
||||
m.Add("a", 1)
|
||||
m.Add("b", 2)
|
||||
m.Add("c", 3)
|
||||
|
||||
got := make([]int, 0)
|
||||
for v := range m.IterateValues() {
|
||||
got = append(got, v)
|
||||
if v == 2 {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
tst.AssertArrayEqual(t, got, []int{1, 2})
|
||||
}
|
||||
|
||||
func TestOrderedMapIterateSeq2(t *testing.T) {
|
||||
m := NewOrderedMap[string, int](0)
|
||||
m.Add("a", 1)
|
||||
m.Add("b", 2)
|
||||
m.Add("c", 3)
|
||||
|
||||
gotKeys := make([]string, 0)
|
||||
gotVals := make([]int, 0)
|
||||
for k, v := range m.Iterate() {
|
||||
gotKeys = append(gotKeys, k)
|
||||
gotVals = append(gotVals, v)
|
||||
}
|
||||
|
||||
tst.AssertArrayEqual(t, gotKeys, []string{"a", "b", "c"})
|
||||
tst.AssertArrayEqual(t, gotVals, []int{1, 2, 3})
|
||||
}
|
||||
|
||||
func TestOrderedMapIterateSeq2Break(t *testing.T) {
|
||||
m := NewOrderedMap[string, int](0)
|
||||
m.Add("a", 1)
|
||||
m.Add("b", 2)
|
||||
m.Add("c", 3)
|
||||
|
||||
gotKeys := make([]string, 0)
|
||||
gotVals := make([]int, 0)
|
||||
for k, v := range m.Iterate() {
|
||||
gotKeys = append(gotKeys, k)
|
||||
gotVals = append(gotVals, v)
|
||||
if k == "b" {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
tst.AssertArrayEqual(t, gotKeys, []string{"a", "b"})
|
||||
tst.AssertArrayEqual(t, gotVals, []int{1, 2})
|
||||
}
|
||||
|
||||
func TestOrderedMapIterateSeq2AfterReorder(t *testing.T) {
|
||||
m := NewOrderedMap[string, int](0)
|
||||
m.Add("a", 1)
|
||||
m.Add("b", 2)
|
||||
m.Add("c", 3)
|
||||
m.Add("a", 10) // moves "a" to end with new value
|
||||
|
||||
gotKeys := make([]string, 0)
|
||||
gotVals := make([]int, 0)
|
||||
for k, v := range m.Iterate() {
|
||||
gotKeys = append(gotKeys, k)
|
||||
gotVals = append(gotVals, v)
|
||||
}
|
||||
|
||||
tst.AssertArrayEqual(t, gotKeys, []string{"b", "c", "a"})
|
||||
tst.AssertArrayEqual(t, gotVals, []int{2, 3, 10})
|
||||
}
|
||||
|
||||
func TestOrderedMapIterateKeys(t *testing.T) {
|
||||
m := NewOrderedMap[string, int](0)
|
||||
m.Add("a", 1)
|
||||
m.Add("b", 2)
|
||||
m.Add("c", 3)
|
||||
|
||||
got := make([]string, 0)
|
||||
for k := range m.IterateKeys() {
|
||||
got = append(got, k)
|
||||
}
|
||||
|
||||
tst.AssertArrayEqual(t, got, []string{"a", "b", "c"})
|
||||
}
|
||||
|
||||
func TestOrderedMapIterateKeysBreak(t *testing.T) {
|
||||
m := NewOrderedMap[string, int](0)
|
||||
m.Add("a", 1)
|
||||
m.Add("b", 2)
|
||||
m.Add("c", 3)
|
||||
|
||||
got := make([]string, 0)
|
||||
for k := range m.IterateKeys() {
|
||||
got = append(got, k)
|
||||
if k == "b" {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
tst.AssertArrayEqual(t, got, []string{"a", "b"})
|
||||
}
|
||||
|
||||
func TestOrderedMapArrayIsCopy(t *testing.T) {
|
||||
m := NewOrderedMap[string, int](0)
|
||||
m.Add("a", 1)
|
||||
m.Add("b", 2)
|
||||
|
||||
arr := m.Array()
|
||||
arr[0] = 999
|
||||
|
||||
tst.AssertArrayEqual(t, m.Array(), []int{1, 2})
|
||||
}
|
||||
|
||||
func TestOrderedMapKeysIsCopy(t *testing.T) {
|
||||
m := NewOrderedMap[string, int](0)
|
||||
m.Add("a", 1)
|
||||
m.Add("b", 2)
|
||||
|
||||
keys := m.Keys()
|
||||
keys[0] = "zzz"
|
||||
|
||||
tst.AssertArrayEqual(t, m.Keys(), []string{"a", "b"})
|
||||
}
|
||||
|
||||
func TestOrderedMapIntKey(t *testing.T) {
|
||||
m := NewOrderedMap[int, string](0)
|
||||
m.Add(3, "three")
|
||||
m.Add(1, "one")
|
||||
m.Add(2, "two")
|
||||
|
||||
tst.AssertArrayEqual(t, m.Keys(), []int{3, 1, 2})
|
||||
tst.AssertArrayEqual(t, m.Array(), []string{"three", "one", "two"})
|
||||
|
||||
v, ok := m.Get(1)
|
||||
tst.AssertTrue(t, ok)
|
||||
tst.AssertEqual(t, v, "one")
|
||||
}
|
||||
|
||||
func TestOrderedMapStress(t *testing.T) {
|
||||
m := NewOrderedMap[int, int](0)
|
||||
|
||||
for i := 0; i < 100; i++ {
|
||||
m.Add(i, i*10)
|
||||
}
|
||||
tst.AssertEqual(t, m.Size(), 100)
|
||||
|
||||
for i := 0; i < 100; i++ {
|
||||
v, ok := m.Get(i)
|
||||
tst.AssertTrue(t, ok)
|
||||
tst.AssertEqual(t, v, i*10)
|
||||
}
|
||||
|
||||
for i := 0; i < 50; i++ {
|
||||
m.Remove(i * 2)
|
||||
}
|
||||
tst.AssertEqual(t, m.Size(), 50)
|
||||
|
||||
expected := make([]int, 0, 50)
|
||||
for i := 0; i < 50; i++ {
|
||||
expected = append(expected, i*2+1)
|
||||
}
|
||||
keys := m.Keys()
|
||||
slices.Sort(keys)
|
||||
tst.AssertArrayEqual(t, keys, expected)
|
||||
}
|
||||
+21
-4
@@ -2,8 +2,15 @@ package exerr
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"git.blackforestbytes.com/BlackForestBytes/goext/dataext"
|
||||
"git.blackforestbytes.com/BlackForestBytes/goext/langext"
|
||||
)
|
||||
|
||||
type ListenerKey struct {
|
||||
_k string
|
||||
}
|
||||
|
||||
type ListenerOpt struct {
|
||||
NoLog bool
|
||||
}
|
||||
@@ -11,20 +18,30 @@ type ListenerOpt struct {
|
||||
type Listener = func(method Method, v *ExErr, opt ListenerOpt)
|
||||
|
||||
var listenerLock = sync.Mutex{}
|
||||
var listener = make([]Listener, 0)
|
||||
var listener = dataext.NewOrderedMap[ListenerKey, Listener]()
|
||||
|
||||
func RegisterListener(l Listener) {
|
||||
func RegisterListener(l Listener) ListenerKey {
|
||||
listenerLock.Lock()
|
||||
defer listenerLock.Unlock()
|
||||
|
||||
listener = append(listener, l)
|
||||
k := ListenerKey{_k: langext.MustHexUUID()}
|
||||
|
||||
listener.Add(k, l)
|
||||
return k
|
||||
}
|
||||
|
||||
func DeregisterListener(key ListenerKey) {
|
||||
listenerLock.Lock()
|
||||
defer listenerLock.Unlock()
|
||||
|
||||
listener.Remove(key)
|
||||
}
|
||||
|
||||
func (ee *ExErr) CallListener(m Method, opt ListenerOpt) {
|
||||
listenerLock.Lock()
|
||||
defer listenerLock.Unlock()
|
||||
|
||||
for _, v := range listener {
|
||||
for _, v := range listener.Iterate() {
|
||||
v(m, ee, opt)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,8 +9,8 @@ require (
|
||||
github.com/rs/xid v1.6.0
|
||||
github.com/rs/zerolog v1.35.1
|
||||
go.mongodb.org/mongo-driver/v2 v2.6.0
|
||||
golang.org/x/crypto v0.51.0
|
||||
golang.org/x/sys v0.44.0
|
||||
golang.org/x/crypto v0.52.0
|
||||
golang.org/x/sys v0.45.0
|
||||
golang.org/x/term v0.43.0
|
||||
)
|
||||
|
||||
@@ -41,7 +41,7 @@ require (
|
||||
github.com/klauspost/compress v1.18.6 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-colorable v0.1.15 // indirect
|
||||
github.com/mattn/go-isatty v0.0.22 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
@@ -63,8 +63,8 @@ require (
|
||||
github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9 // indirect
|
||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
|
||||
golang.org/x/arch v0.27.0 // indirect
|
||||
golang.org/x/image v0.40.0 // indirect
|
||||
golang.org/x/net v0.54.0 // indirect
|
||||
golang.org/x/image v0.41.0 // indirect
|
||||
golang.org/x/net v0.55.0 // indirect
|
||||
golang.org/x/text v0.37.0 // indirect
|
||||
google.golang.org/protobuf v1.36.11 // indirect
|
||||
modernc.org/libc v1.72.0 // indirect
|
||||
|
||||
@@ -74,6 +74,8 @@ github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
github.com/mattn/go-colorable v0.1.15 h1:+u9SLTRGnXv73cEsnsmoZBom+dMU88B2M0aDcWy0/jY=
|
||||
github.com/mattn/go-colorable v0.1.15/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
github.com/mattn/go-isatty v0.0.21 h1:xYae+lCNBP7QuW4PUnNG61ffM4hVIfm+zUzDuSzYLGs=
|
||||
github.com/mattn/go-isatty v0.0.21/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
|
||||
github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4=
|
||||
@@ -164,12 +166,16 @@ golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
|
||||
golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
|
||||
golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI=
|
||||
golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8=
|
||||
golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988=
|
||||
golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc=
|
||||
golang.org/x/image v0.0.0-20190910094157-69e4b8554b2a/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/image v0.39.0 h1:skVYidAEVKgn8lZ602XO75asgXBgLj9G/FE3RbuPFww=
|
||||
golang.org/x/image v0.39.0/go.mod h1:sIbmppfU+xFLPIG0FoVUTvyBMmgng1/XAMhQ2ft0hpA=
|
||||
golang.org/x/image v0.40.0 h1:Tw4GyDXMo+daZN1znreBRC3VayR1aLFUyUEOLUdW1a8=
|
||||
golang.org/x/image v0.40.0/go.mod h1:uIc348UZMSvS5Z65CVZ7iDPaNobNFEPeJ4kbqTOszmA=
|
||||
golang.org/x/image v0.41.0 h1:8wS72eGJMJaBxK6okTzd4WaXumUlTVlb753MlsSvTCo=
|
||||
golang.org/x/image v0.41.0/go.mod h1:uIc348UZMSvS5Z65CVZ7iDPaNobNFEPeJ4kbqTOszmA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=
|
||||
golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=
|
||||
@@ -181,6 +187,8 @@ golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
|
||||
golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
|
||||
golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w=
|
||||
golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ=
|
||||
golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
|
||||
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||
@@ -194,6 +202,8 @@ golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
|
||||
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ=
|
||||
golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
|
||||
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY=
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
package goext
|
||||
|
||||
const GoextVersion = "0.0.640"
|
||||
const GoextVersion = "0.0.644"
|
||||
|
||||
const GoextVersionTimestamp = "2026-05-12T10:54:15+0200"
|
||||
const GoextVersionTimestamp = "2026-05-30T00:13:03+0200"
|
||||
|
||||
@@ -7,14 +7,23 @@ import (
|
||||
ct "git.blackforestbytes.com/BlackForestBytes/goext/cursortoken"
|
||||
"git.blackforestbytes.com/BlackForestBytes/goext/exerr"
|
||||
"git.blackforestbytes.com/BlackForestBytes/goext/langext"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
"go.mongodb.org/mongo-driver/v2/mongo"
|
||||
)
|
||||
|
||||
type EntityID interface {
|
||||
MarshalBSONValue() (byte, []byte, error)
|
||||
IsZero() bool
|
||||
String() string
|
||||
}
|
||||
|
||||
type MongoEntityID interface {
|
||||
EntityID
|
||||
|
||||
ObjID() (bson.ObjectID, error)
|
||||
Valid() bool
|
||||
}
|
||||
|
||||
type Decodable interface {
|
||||
Decode(v any) error
|
||||
}
|
||||
|
||||
+51
-25
@@ -401,14 +401,18 @@ func createPaginationPipeline[TData any](coll *Coll[TData], token ct.CTKeySort,
|
||||
return nil, nil, exerr.Wrap(err, "failed to get (primary) token-value as mongo-type").Build()
|
||||
}
|
||||
|
||||
if isValidTokenValue(valuePrimary) {
|
||||
if sortPrimary == ct.SortASC {
|
||||
cond = append(cond, bson.M{fieldPrimary: bson.M{"$gt": valuePrimary}}) // We sort ASC on <field> - so we want all entries newer ($gt) than the $primary
|
||||
} else if sortPrimary == ct.SortDESC {
|
||||
cond = append(cond, bson.M{fieldPrimary: bson.M{"$lt": valuePrimary}}) // We sort DESC on <field> - so we want all entries older ($lt) than the $primary
|
||||
}
|
||||
}
|
||||
|
||||
if sortPrimary == ct.SortASC {
|
||||
// We sort ASC on <field> - so we want all entries newer ($gt) than the $primary
|
||||
cond = append(cond, bson.M{fieldPrimary: bson.M{"$gt": valuePrimary}})
|
||||
sort = append(sort, bson.E{Key: fieldPrimary, Value: +1})
|
||||
sort = append(sort, bson.E{Key: fieldPrimary, Value: +1}) // We sort ASC on <field> - so we want all entries newer ($gt) than the $primary
|
||||
} else if sortPrimary == ct.SortDESC {
|
||||
// We sort DESC on <field> - so we want all entries older ($lt) than the $primary
|
||||
cond = append(cond, bson.M{fieldPrimary: bson.M{"$lt": valuePrimary}})
|
||||
sort = append(sort, bson.E{Key: fieldPrimary, Value: -1})
|
||||
sort = append(sort, bson.E{Key: fieldPrimary, Value: -1}) // We sort DESC on <field> - so we want all entries older ($lt) than the $primary
|
||||
}
|
||||
|
||||
if fieldSecondary != nil && sortSecondary != nil && *fieldSecondary != fieldPrimary {
|
||||
@@ -418,26 +422,26 @@ func createPaginationPipeline[TData any](coll *Coll[TData], token ct.CTKeySort,
|
||||
return nil, nil, exerr.Wrap(err, "failed to get (secondary) token-value as mongo-type").Build()
|
||||
}
|
||||
|
||||
if isValidTokenValue(valueSecondary) {
|
||||
if *sortSecondary == ct.SortASC {
|
||||
// the conflict-resolution condition, for entries with the _same_ <field> as the $primary we take the ones with a greater $secondary (= newer)
|
||||
cond = append(cond, bson.M{"$and": bson.A{
|
||||
bson.M{"$or": bson.A{bson.M{fieldPrimary: valuePrimary}, bson.M{fieldPrimary: nil}, bson.M{fieldPrimary: bson.M{"$exists": false}}}},
|
||||
bson.M{*fieldSecondary: bson.M{"$gt": valueSecondary}},
|
||||
}})
|
||||
} else if *sortSecondary == ct.SortDESC {
|
||||
// the conflict-resolution condition, for entries with the _same_ <field> as the $primary we take the ones with a smaller $secondary (= older)
|
||||
cond = append(cond, bson.M{"$and": bson.A{
|
||||
bson.M{"$or": bson.A{bson.M{fieldPrimary: valuePrimary}, bson.M{fieldPrimary: nil}, bson.M{fieldPrimary: bson.M{"$exists": false}}}},
|
||||
bson.M{*fieldSecondary: bson.M{"$lt": valueSecondary}},
|
||||
}})
|
||||
}
|
||||
}
|
||||
|
||||
if *sortSecondary == ct.SortASC {
|
||||
|
||||
// the conflict-resolution condition, for entries with the _same_ <field> as the $primary we take the ones with a greater $secondary (= newer)
|
||||
cond = append(cond, bson.M{"$and": bson.A{
|
||||
bson.M{"$or": bson.A{bson.M{fieldPrimary: valuePrimary}, bson.M{fieldPrimary: nil}, bson.M{fieldPrimary: bson.M{"$exists": false}}}},
|
||||
bson.M{*fieldSecondary: bson.M{"$gt": valueSecondary}},
|
||||
}})
|
||||
|
||||
sort = append(sort, bson.E{Key: *fieldSecondary, Value: +1})
|
||||
|
||||
} else if *sortSecondary == ct.SortDESC {
|
||||
|
||||
// the conflict-resolution condition, for entries with the _same_ <field> as the $primary we take the ones with a smaller $secondary (= older)
|
||||
cond = append(cond, bson.M{"$and": bson.A{
|
||||
bson.M{"$or": bson.A{bson.M{fieldPrimary: valuePrimary}, bson.M{fieldPrimary: nil}, bson.M{fieldPrimary: bson.M{"$exists": false}}}},
|
||||
bson.M{*fieldSecondary: bson.M{"$lt": valueSecondary}},
|
||||
}})
|
||||
|
||||
sort = append(sort, bson.E{Key: *fieldSecondary, Value: -1})
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -449,7 +453,9 @@ func createPaginationPipeline[TData any](coll *Coll[TData], token ct.CTKeySort,
|
||||
|
||||
} else if token.Mode == ct.CTMNormal {
|
||||
|
||||
pipeline = append(pipeline, bson.D{{Key: "$match", Value: bson.M{"$or": cond}}})
|
||||
if len(cond) > 0 {
|
||||
pipeline = append(pipeline, bson.D{{Key: "$match", Value: bson.M{"$or": cond}}})
|
||||
}
|
||||
|
||||
} else if token.Mode == ct.CTMEnd {
|
||||
|
||||
@@ -462,9 +468,15 @@ func createPaginationPipeline[TData any](coll *Coll[TData], token ct.CTKeySort,
|
||||
|
||||
}
|
||||
|
||||
pipeline = append(pipeline, bson.D{{Key: "$sort", Value: sort}})
|
||||
if len(sort) > 0 {
|
||||
pipeline = append(pipeline, bson.D{{Key: "$sort", Value: sort}})
|
||||
}
|
||||
|
||||
pipelineSort := mongo.Pipeline{bson.D{{Key: "$sort", Value: sort}}}
|
||||
pipelineSort := mongo.Pipeline{}
|
||||
|
||||
if len(sort) > 0 {
|
||||
pipelineSort = append(pipelineSort, bson.D{{Key: "$sort", Value: sort}})
|
||||
}
|
||||
|
||||
if pageSize != nil {
|
||||
pipeline = append(pipeline, bson.D{{Key: "$limit", Value: int64(*pageSize + 1)}})
|
||||
@@ -473,6 +485,20 @@ func createPaginationPipeline[TData any](coll *Coll[TData], token ct.CTKeySort,
|
||||
return pipeline, pipelineSort, nil
|
||||
}
|
||||
|
||||
func isValidTokenValue(val any) bool {
|
||||
|
||||
// special handling for Mongo EntityIDs
|
||||
// We want to prevent failing late (in mongoDB layer) when client sends invalid IDs as token values
|
||||
|
||||
if mongoID, ok := val.(MongoEntityID); ok {
|
||||
if !mongoID.Valid() {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func createSortOnlyPipeline(fieldPrimary string, sortPrimary ct.SortDirection, fieldSecondary *string, sortSecondary *ct.SortDirection) ([]bson.D, error) {
|
||||
|
||||
sort := bson.D{}
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
package wmo
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
ct "git.blackforestbytes.com/BlackForestBytes/goext/cursortoken"
|
||||
"git.blackforestbytes.com/BlackForestBytes/goext/exerr"
|
||||
"git.blackforestbytes.com/BlackForestBytes/goext/langext"
|
||||
"git.blackforestbytes.com/BlackForestBytes/goext/rfctime"
|
||||
"git.blackforestbytes.com/BlackForestBytes/goext/tst"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
)
|
||||
|
||||
type TestExampleID string //@id:type
|
||||
|
||||
func (i TestExampleID) MarshalBSONValue() (byte, []byte, error) {
|
||||
if objId, err := bson.ObjectIDFromHex(string(i)); err == nil {
|
||||
tp, data, err := bson.MarshalValue(objId)
|
||||
return byte(tp), data, err
|
||||
} else {
|
||||
return 0, nil, exerr.New(exerr.TypeMarshalEntityID, "Failed to marshal UserID("+i.String()+") to ObjectId").Str("value", string(i)).Type("type", i).Build()
|
||||
}
|
||||
}
|
||||
|
||||
func (i TestExampleID) String() string {
|
||||
return string(i)
|
||||
}
|
||||
|
||||
func (i TestExampleID) ObjID() (bson.ObjectID, error) {
|
||||
return bson.ObjectIDFromHex(string(i))
|
||||
}
|
||||
|
||||
func (i TestExampleID) Valid() bool {
|
||||
_, err := bson.ObjectIDFromHex(string(i))
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func (i TestExampleID) IsZero() bool {
|
||||
return i == ""
|
||||
}
|
||||
|
||||
var _ MongoEntityID = (*TestExampleID)(nil)
|
||||
|
||||
func TestCreatePaginationPipelineWithInvalidObjectIDValue(t *testing.T) {
|
||||
//
|
||||
// {"v1":"2026-05-16T21:44:13.16Z","v2":"6a08e52d144a332c2e2d067b","dir":"DESC","dir2":"DESC","size":50}
|
||||
strtok := "tok_PMRHMMJCHIRDEMBSGYWTANJNGE3FIMRRHI2DIORRGMXDCNS2EIWCE5RSEI5CENTBGA4GKNJSMQYTINDBGMZTEYZSMUZGIMBWG5RCELBCMRUXEIR2EJCEKU2DEIWCEZDJOIZCEORCIRCVGQZCFQRHG2L2MURDUNJQPU======"
|
||||
|
||||
tok, err := ct.Decode(strtok)
|
||||
if err != nil {
|
||||
t.Error("Failed to decode token:", err)
|
||||
return
|
||||
}
|
||||
|
||||
type obj struct {
|
||||
ID TestExampleID `bson:"_id"`
|
||||
Created rfctime.RFC3339NanoTime `bson:"created"`
|
||||
}
|
||||
|
||||
coll := Coll[obj]{coll: nil}
|
||||
coll.init()
|
||||
|
||||
paginationPipeline, doubleSortPipeline, err := createPaginationPipeline[obj](&coll, tok.(ct.CTKeySort), "_id", ct.SortDESC, new("_id"), new(ct.SortDESC), new(15))
|
||||
if err != nil {
|
||||
t.Error("Failed to create pagination Pipeline:", err)
|
||||
return
|
||||
}
|
||||
|
||||
//fmt.Printf("# paginationPipeline:\n%+v\n\n", string(langext.Must(json.Marshal(paginationPipeline))))
|
||||
//fmt.Printf("# doubleSortPipeline:\n%+v\n\n", string(langext.Must(json.Marshal(doubleSortPipeline))))
|
||||
|
||||
tst.AssertEqual(t, string(langext.Must(json.Marshal(paginationPipeline))), `[{"$sort":{"_id":-1}},{"$limit":16}]`) // wrong (_id with wrong date type) fields are not used
|
||||
tst.AssertEqual(t, string(langext.Must(json.Marshal(doubleSortPipeline))), `[{"$sort":{"_id":-1}}]`) // wrong (_id with wrong date type) fields are not used
|
||||
}
|
||||
|
||||
func TestCreatePaginationPipelineWithValidObjectIDValue(t *testing.T) {
|
||||
//
|
||||
// {"v1":"6a08e52d144a332c2e2d067b","v2":"6a08e52d144a332c2e2d067b","dir":"DESC","dir2":"DESC","size":50}
|
||||
strtok := "tok_PMFCAIBAEARHMMJCHIQCENTBGA4GKNJSMQYTINDBGMZTEYZSMUZGIMBWG5RCELAKEAQCAIBCOYZCEORAEI3GCMBYMU2TEZBRGQ2GCMZTGJRTEZJSMQYDMN3CEIWAUIBAEAQCEZDJOIRDUIBCIRCVGQZCFQFCAIBAEARGI2LSGIRDUIBCIRCVGQZCFQFCAIBAEARHG2L2MURDUIBVGAFH2==="
|
||||
|
||||
tok, err := ct.Decode(strtok)
|
||||
if err != nil {
|
||||
t.Error("Failed to decode token:", err)
|
||||
return
|
||||
}
|
||||
|
||||
type obj struct {
|
||||
ID TestExampleID `bson:"_id"`
|
||||
Created rfctime.RFC3339NanoTime `bson:"created"`
|
||||
}
|
||||
|
||||
coll := Coll[obj]{coll: nil}
|
||||
coll.init()
|
||||
|
||||
paginationPipeline, doubleSortPipeline, err := createPaginationPipeline[obj](&coll, tok.(ct.CTKeySort), "_id", ct.SortDESC, new("_id"), new(ct.SortDESC), new(15))
|
||||
if err != nil {
|
||||
t.Error("Failed to create pagination Pipeline:", err)
|
||||
return
|
||||
}
|
||||
|
||||
//fmt.Printf("# paginationPipeline:\n%+v\n\n", string(langext.Must(json.Marshal(paginationPipeline))))
|
||||
//fmt.Printf("# doubleSortPipeline:\n%+v\n\n", string(langext.Must(json.Marshal(doubleSortPipeline))))
|
||||
|
||||
tst.AssertEqual(t, string(langext.Must(json.Marshal(paginationPipeline))), `[{"$match":{"$or":[{"_id":{"$lt":"6a08e52d144a332c2e2d067b"}}]}},{"$sort":{"_id":-1}},{"$limit":16}]`)
|
||||
tst.AssertEqual(t, string(langext.Must(json.Marshal(doubleSortPipeline))), `[{"$sort":{"_id":-1}}]`)
|
||||
}
|
||||
Reference in New Issue
Block a user