mirror of
https://github.com/rancher/steve.git
synced 2025-04-27 11:00:48 +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
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
@ -16,6 +17,7 @@ const (
|
||||
continueParam = "continue"
|
||||
limitParam = "limit"
|
||||
filterParam = "filter"
|
||||
sortParam = "sort"
|
||||
)
|
||||
|
||||
// ListOptions represents the query parameters that may be included in a list request.
|
||||
@ -23,6 +25,7 @@ type ListOptions struct {
|
||||
ChunkSize int
|
||||
Resume string
|
||||
Filters []Filter
|
||||
Sort Sort
|
||||
}
|
||||
|
||||
// Filter represents a field to filter by.
|
||||
@ -33,6 +36,25 @@ type Filter struct {
|
||||
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.
|
||||
func ParseQuery(apiOp *types.APIRequest) *ListOptions {
|
||||
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]})
|
||||
}
|
||||
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{
|
||||
ChunkSize: chunkSize,
|
||||
Resume: cont,
|
||||
Filters: filterOpts,
|
||||
Sort: sort,
|
||||
}
|
||||
}
|
||||
|
||||
@ -148,3 +180,19 @@ func matchesAll(obj map[string]interface{}, filters []Filter) bool {
|
||||
}
|
||||
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.SortList(list, opts.Sort)
|
||||
|
||||
for _, item := range list {
|
||||
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 {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
|
Loading…
Reference in New Issue
Block a user