mirror of
https://github.com/niusmallnan/steve.git
synced 2025-06-22 12:47:04 +00:00
Add sorting to partition store
Extend the partition store to parse the "sort" query parameter as a sorting condition. Dot notation is used to denote the object field. Preceding the key with "-" denotes descending (reverse) order. Example sorting by name: GET /v1/secrets?sort=metadata.name Reverse sorting by name: GET /v1/secrets?sort=-metadata.name All values are converted to strings and sorted lexicographically.
This commit is contained in:
parent
f8eaa11d83
commit
adecbd9122
@ -2,6 +2,7 @@
|
|||||||
package listprocessor
|
package listprocessor
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@ -16,6 +17,7 @@ const (
|
|||||||
continueParam = "continue"
|
continueParam = "continue"
|
||||||
limitParam = "limit"
|
limitParam = "limit"
|
||||||
filterParam = "filter"
|
filterParam = "filter"
|
||||||
|
sortParam = "sort"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ListOptions represents the query parameters that may be included in a list request.
|
// ListOptions represents the query parameters that may be included in a list request.
|
||||||
@ -23,6 +25,7 @@ type ListOptions struct {
|
|||||||
ChunkSize int
|
ChunkSize int
|
||||||
Resume string
|
Resume string
|
||||||
Filters []Filter
|
Filters []Filter
|
||||||
|
Sort Sort
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter represents a field to filter by.
|
// Filter represents a field to filter by.
|
||||||
@ -33,6 +36,25 @@ type Filter struct {
|
|||||||
match string
|
match string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SortOrder represents whether the list should be ascending or descending.
|
||||||
|
type SortOrder int
|
||||||
|
|
||||||
|
const (
|
||||||
|
// ASC stands for ascending order.
|
||||||
|
ASC SortOrder = iota
|
||||||
|
// DESC stands for descending (reverse) order.
|
||||||
|
DESC
|
||||||
|
)
|
||||||
|
|
||||||
|
// Sort represents the criteria to sort on.
|
||||||
|
// The subfield to sort by is represented in a request query using . notation, e.g. 'metadata.name'.
|
||||||
|
// The subfield is internally represented as a slice, e.g. [metadata, name].
|
||||||
|
// The order is represented by prefixing the sort key by '-', e.g. sort=-metadata.name.
|
||||||
|
type Sort struct {
|
||||||
|
field []string
|
||||||
|
order SortOrder
|
||||||
|
}
|
||||||
|
|
||||||
// ParseQuery parses the query params of a request and returns a ListOptions.
|
// ParseQuery parses the query params of a request and returns a ListOptions.
|
||||||
func ParseQuery(apiOp *types.APIRequest) *ListOptions {
|
func ParseQuery(apiOp *types.APIRequest) *ListOptions {
|
||||||
chunkSize := getLimit(apiOp)
|
chunkSize := getLimit(apiOp)
|
||||||
@ -47,10 +69,20 @@ func ParseQuery(apiOp *types.APIRequest) *ListOptions {
|
|||||||
}
|
}
|
||||||
filterOpts = append(filterOpts, Filter{field: strings.Split(filter[0], "."), match: filter[1]})
|
filterOpts = append(filterOpts, Filter{field: strings.Split(filter[0], "."), match: filter[1]})
|
||||||
}
|
}
|
||||||
|
sort := Sort{}
|
||||||
|
sortKey := q.Get(sortParam)
|
||||||
|
if sortKey != "" && sortKey[0] == '-' {
|
||||||
|
sort.order = DESC
|
||||||
|
sortKey = sortKey[1:]
|
||||||
|
}
|
||||||
|
if sortKey != "" {
|
||||||
|
sort.field = strings.Split(sortKey, ".")
|
||||||
|
}
|
||||||
return &ListOptions{
|
return &ListOptions{
|
||||||
ChunkSize: chunkSize,
|
ChunkSize: chunkSize,
|
||||||
Resume: cont,
|
Resume: cont,
|
||||||
Filters: filterOpts,
|
Filters: filterOpts,
|
||||||
|
Sort: sort,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -148,3 +180,19 @@ func matchesAll(obj map[string]interface{}, filters []Filter) bool {
|
|||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SortList sorts the slice by the provided sort criteria.
|
||||||
|
func SortList(list []unstructured.Unstructured, s Sort) []unstructured.Unstructured {
|
||||||
|
if len(s.field) == 0 {
|
||||||
|
return list
|
||||||
|
}
|
||||||
|
sort.Slice(list, func(i, j int) bool {
|
||||||
|
iField := convert.ToString(data.GetValueN(list[i].Object, s.field...))
|
||||||
|
jField := convert.ToString(data.GetValueN(list[j].Object, s.field...))
|
||||||
|
if s.order == ASC {
|
||||||
|
return iField < jField
|
||||||
|
}
|
||||||
|
return jField < iField
|
||||||
|
})
|
||||||
|
return list
|
||||||
|
}
|
||||||
|
@ -796,3 +796,322 @@ func TestFilterList(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSortList(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
objects []unstructured.Unstructured
|
||||||
|
sort Sort
|
||||||
|
want []unstructured.Unstructured
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "sort metadata.name",
|
||||||
|
objects: []unstructured.Unstructured{
|
||||||
|
{
|
||||||
|
Object: map[string]interface{}{
|
||||||
|
"kind": "apple",
|
||||||
|
"metadata": map[string]interface{}{
|
||||||
|
"name": "fuji",
|
||||||
|
},
|
||||||
|
"data": map[string]interface{}{
|
||||||
|
"color": "pink",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Object: map[string]interface{}{
|
||||||
|
"kind": "apple",
|
||||||
|
"metadata": map[string]interface{}{
|
||||||
|
"name": "honeycrisp",
|
||||||
|
},
|
||||||
|
"data": map[string]interface{}{
|
||||||
|
"color": "pink",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Object: map[string]interface{}{
|
||||||
|
"kind": "apple",
|
||||||
|
"metadata": map[string]interface{}{
|
||||||
|
"name": "granny-smith",
|
||||||
|
},
|
||||||
|
"data": map[string]interface{}{
|
||||||
|
"color": "green",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
sort: Sort{
|
||||||
|
field: []string{"metadata", "name"},
|
||||||
|
},
|
||||||
|
want: []unstructured.Unstructured{
|
||||||
|
{
|
||||||
|
Object: map[string]interface{}{
|
||||||
|
"kind": "apple",
|
||||||
|
"metadata": map[string]interface{}{
|
||||||
|
"name": "fuji",
|
||||||
|
},
|
||||||
|
"data": map[string]interface{}{
|
||||||
|
"color": "pink",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Object: map[string]interface{}{
|
||||||
|
"kind": "apple",
|
||||||
|
"metadata": map[string]interface{}{
|
||||||
|
"name": "granny-smith",
|
||||||
|
},
|
||||||
|
"data": map[string]interface{}{
|
||||||
|
"color": "green",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Object: map[string]interface{}{
|
||||||
|
"kind": "apple",
|
||||||
|
"metadata": map[string]interface{}{
|
||||||
|
"name": "honeycrisp",
|
||||||
|
},
|
||||||
|
"data": map[string]interface{}{
|
||||||
|
"color": "pink",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "reverse sort metadata.name",
|
||||||
|
objects: []unstructured.Unstructured{
|
||||||
|
{
|
||||||
|
Object: map[string]interface{}{
|
||||||
|
"kind": "apple",
|
||||||
|
"metadata": map[string]interface{}{
|
||||||
|
"name": "fuji",
|
||||||
|
},
|
||||||
|
"data": map[string]interface{}{
|
||||||
|
"color": "pink",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Object: map[string]interface{}{
|
||||||
|
"kind": "apple",
|
||||||
|
"metadata": map[string]interface{}{
|
||||||
|
"name": "honeycrisp",
|
||||||
|
},
|
||||||
|
"data": map[string]interface{}{
|
||||||
|
"color": "pink",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Object: map[string]interface{}{
|
||||||
|
"kind": "apple",
|
||||||
|
"metadata": map[string]interface{}{
|
||||||
|
"name": "granny-smith",
|
||||||
|
},
|
||||||
|
"data": map[string]interface{}{
|
||||||
|
"color": "green",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
sort: Sort{
|
||||||
|
field: []string{"metadata", "name"},
|
||||||
|
order: DESC,
|
||||||
|
},
|
||||||
|
want: []unstructured.Unstructured{
|
||||||
|
{
|
||||||
|
Object: map[string]interface{}{
|
||||||
|
"kind": "apple",
|
||||||
|
"metadata": map[string]interface{}{
|
||||||
|
"name": "honeycrisp",
|
||||||
|
},
|
||||||
|
"data": map[string]interface{}{
|
||||||
|
"color": "pink",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Object: map[string]interface{}{
|
||||||
|
"kind": "apple",
|
||||||
|
"metadata": map[string]interface{}{
|
||||||
|
"name": "granny-smith",
|
||||||
|
},
|
||||||
|
"data": map[string]interface{}{
|
||||||
|
"color": "green",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Object: map[string]interface{}{
|
||||||
|
"kind": "apple",
|
||||||
|
"metadata": map[string]interface{}{
|
||||||
|
"name": "fuji",
|
||||||
|
},
|
||||||
|
"data": map[string]interface{}{
|
||||||
|
"color": "pink",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid field",
|
||||||
|
objects: []unstructured.Unstructured{
|
||||||
|
{
|
||||||
|
Object: map[string]interface{}{
|
||||||
|
"kind": "apple",
|
||||||
|
"metadata": map[string]interface{}{
|
||||||
|
"name": "granny-smith",
|
||||||
|
},
|
||||||
|
"data": map[string]interface{}{
|
||||||
|
"color": "green",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Object: map[string]interface{}{
|
||||||
|
"kind": "apple",
|
||||||
|
"metadata": map[string]interface{}{
|
||||||
|
"name": "fuji",
|
||||||
|
},
|
||||||
|
"data": map[string]interface{}{
|
||||||
|
"color": "pink",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Object: map[string]interface{}{
|
||||||
|
"kind": "apple",
|
||||||
|
"metadata": map[string]interface{}{
|
||||||
|
"name": "honeycrisp",
|
||||||
|
},
|
||||||
|
"data": map[string]interface{}{
|
||||||
|
"color": "pink",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
sort: Sort{
|
||||||
|
field: []string{"data", "productType"},
|
||||||
|
},
|
||||||
|
want: []unstructured.Unstructured{
|
||||||
|
{
|
||||||
|
Object: map[string]interface{}{
|
||||||
|
"kind": "apple",
|
||||||
|
"metadata": map[string]interface{}{
|
||||||
|
"name": "granny-smith",
|
||||||
|
},
|
||||||
|
"data": map[string]interface{}{
|
||||||
|
"color": "green",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Object: map[string]interface{}{
|
||||||
|
"kind": "apple",
|
||||||
|
"metadata": map[string]interface{}{
|
||||||
|
"name": "fuji",
|
||||||
|
},
|
||||||
|
"data": map[string]interface{}{
|
||||||
|
"color": "pink",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Object: map[string]interface{}{
|
||||||
|
"kind": "apple",
|
||||||
|
"metadata": map[string]interface{}{
|
||||||
|
"name": "honeycrisp",
|
||||||
|
},
|
||||||
|
"data": map[string]interface{}{
|
||||||
|
"color": "pink",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "unsorted",
|
||||||
|
objects: []unstructured.Unstructured{
|
||||||
|
{
|
||||||
|
Object: map[string]interface{}{
|
||||||
|
"kind": "apple",
|
||||||
|
"metadata": map[string]interface{}{
|
||||||
|
"name": "granny-smith",
|
||||||
|
},
|
||||||
|
"data": map[string]interface{}{
|
||||||
|
"color": "green",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Object: map[string]interface{}{
|
||||||
|
"kind": "apple",
|
||||||
|
"metadata": map[string]interface{}{
|
||||||
|
"name": "fuji",
|
||||||
|
},
|
||||||
|
"data": map[string]interface{}{
|
||||||
|
"color": "pink",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Object: map[string]interface{}{
|
||||||
|
"kind": "apple",
|
||||||
|
"metadata": map[string]interface{}{
|
||||||
|
"name": "honeycrisp",
|
||||||
|
},
|
||||||
|
"data": map[string]interface{}{
|
||||||
|
"color": "pink",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
sort: Sort{},
|
||||||
|
want: []unstructured.Unstructured{
|
||||||
|
{
|
||||||
|
Object: map[string]interface{}{
|
||||||
|
"kind": "apple",
|
||||||
|
"metadata": map[string]interface{}{
|
||||||
|
"name": "granny-smith",
|
||||||
|
},
|
||||||
|
"data": map[string]interface{}{
|
||||||
|
"color": "green",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Object: map[string]interface{}{
|
||||||
|
"kind": "apple",
|
||||||
|
"metadata": map[string]interface{}{
|
||||||
|
"name": "fuji",
|
||||||
|
},
|
||||||
|
"data": map[string]interface{}{
|
||||||
|
"color": "pink",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Object: map[string]interface{}{
|
||||||
|
"kind": "apple",
|
||||||
|
"metadata": map[string]interface{}{
|
||||||
|
"name": "honeycrisp",
|
||||||
|
},
|
||||||
|
"data": map[string]interface{}{
|
||||||
|
"color": "pink",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
got := SortList(test.objects, test.sort)
|
||||||
|
assert.Equal(t, test.want, got)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -128,6 +128,7 @@ func (s *Store) List(apiOp *types.APIRequest, schema *types.APISchema) (types.AP
|
|||||||
}
|
}
|
||||||
|
|
||||||
list := listprocessor.FilterList(stream, opts.Filters)
|
list := listprocessor.FilterList(stream, opts.Filters)
|
||||||
|
list = listprocessor.SortList(list, opts.Sort)
|
||||||
|
|
||||||
for _, item := range list {
|
for _, item := range list {
|
||||||
item := item
|
item := item
|
||||||
|
@ -278,6 +278,85 @@ func TestList(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "with sorting",
|
||||||
|
apiOps: []*types.APIRequest{
|
||||||
|
newRequest("sort=metadata.name"),
|
||||||
|
newRequest("sort=-metadata.name"),
|
||||||
|
},
|
||||||
|
partitions: []Partition{
|
||||||
|
mockPartition{
|
||||||
|
name: "all",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
objects: map[string]*unstructured.UnstructuredList{
|
||||||
|
"all": {
|
||||||
|
Items: []unstructured.Unstructured{
|
||||||
|
newApple("fuji").Unstructured,
|
||||||
|
newApple("granny-smith").Unstructured,
|
||||||
|
newApple("bramley").Unstructured,
|
||||||
|
newApple("crispin").Unstructured,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: []types.APIObjectList{
|
||||||
|
{
|
||||||
|
Objects: []types.APIObject{
|
||||||
|
newApple("bramley").toObj(),
|
||||||
|
newApple("crispin").toObj(),
|
||||||
|
newApple("fuji").toObj(),
|
||||||
|
newApple("granny-smith").toObj(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Objects: []types.APIObject{
|
||||||
|
newApple("granny-smith").toObj(),
|
||||||
|
newApple("fuji").toObj(),
|
||||||
|
newApple("crispin").toObj(),
|
||||||
|
newApple("bramley").toObj(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "multi-partition sort=metadata.name",
|
||||||
|
apiOps: []*types.APIRequest{
|
||||||
|
newRequest("sort=metadata.name"),
|
||||||
|
},
|
||||||
|
partitions: []Partition{
|
||||||
|
mockPartition{
|
||||||
|
name: "green",
|
||||||
|
},
|
||||||
|
mockPartition{
|
||||||
|
name: "yellow",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
objects: map[string]*unstructured.UnstructuredList{
|
||||||
|
"pink": {
|
||||||
|
Items: []unstructured.Unstructured{
|
||||||
|
newApple("fuji").Unstructured,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"green": {
|
||||||
|
Items: []unstructured.Unstructured{
|
||||||
|
newApple("granny-smith").Unstructured,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"yellow": {
|
||||||
|
Items: []unstructured.Unstructured{
|
||||||
|
newApple("crispin").Unstructured,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: []types.APIObjectList{
|
||||||
|
{
|
||||||
|
Objects: []types.APIObject{
|
||||||
|
newApple("crispin").toObj(),
|
||||||
|
newApple("granny-smith").toObj(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
t.Run(test.name, func(t *testing.T) {
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
Loading…
Reference in New Issue
Block a user