mirror of
https://github.com/goharbor/harbor.git
synced 2024-11-26 04:05:40 +01:00
Implement query string builder
This commit defines the API query string format and provides the builders to build query string to query model Signed-off-by: Wenkai Yin <yinw@vmware.com>
This commit is contained in:
parent
69119b6410
commit
8abb630b4c
@ -28,6 +28,8 @@ paths:
|
|||||||
- $ref: '#/parameters/projectName'
|
- $ref: '#/parameters/projectName'
|
||||||
- $ref: '#/parameters/page'
|
- $ref: '#/parameters/page'
|
||||||
- $ref: '#/parameters/pageSize'
|
- $ref: '#/parameters/pageSize'
|
||||||
|
- $ref: '#/parameters/query'
|
||||||
|
# TODO remove it
|
||||||
- name: name
|
- name: name
|
||||||
in: query
|
in: query
|
||||||
description: Query the repositories by name
|
description: Query the repositories by name
|
||||||
@ -500,6 +502,12 @@ paths:
|
|||||||
'500':
|
'500':
|
||||||
$ref: '#/responses/500'
|
$ref: '#/responses/500'
|
||||||
parameters:
|
parameters:
|
||||||
|
query:
|
||||||
|
name: q
|
||||||
|
description: Query string to query resources. Supported query patterns are "exact match(k=v)", "fuzzy match(k=~v)", "range(k=[min~max])", "list with union releationship(k={v1 v2 v3})" and "list with intersetion relationship(k=(v1 v2 v3))". All of these query patterns should be put in the query string "q=xxx" and splitted by ",". e.g. q=k1=v1,k2=~v2,k3=[min~max]
|
||||||
|
in: query
|
||||||
|
type: string
|
||||||
|
required: false
|
||||||
requestId:
|
requestId:
|
||||||
name: X-Request-Id
|
name: X-Request-Id
|
||||||
description: An unique ID for the request
|
description: An unique ID for the request
|
||||||
|
@ -16,14 +16,16 @@ package orm
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/astaxie/beego/orm"
|
"github.com/astaxie/beego/orm"
|
||||||
"github.com/goharbor/harbor/src/pkg/q"
|
"github.com/goharbor/harbor/src/pkg/q"
|
||||||
)
|
)
|
||||||
|
|
||||||
// QuerySetter generates the query setter according to the query
|
// QuerySetter generates the query setter according to the query. "ignoredCols" is used to set the
|
||||||
func QuerySetter(ctx context.Context, model interface{}, query *q.Query) (orm.QuerySeter, error) {
|
// columns that will not be queried
|
||||||
|
func QuerySetter(ctx context.Context, model interface{}, query *q.Query, ignoredCols ...string) (orm.QuerySeter, error) {
|
||||||
ormer, err := FromContext(ctx)
|
ormer, err := FromContext(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -32,7 +34,49 @@ func QuerySetter(ctx context.Context, model interface{}, query *q.Query) (orm.Qu
|
|||||||
if query == nil {
|
if query == nil {
|
||||||
return qs, nil
|
return qs, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// the program will panic when querying the columns that doesn't exist
|
||||||
|
// list the supported columns first to avoid the panic
|
||||||
|
cols := listQueriableCols(model, ignoredCols...)
|
||||||
for k, v := range query.Keywords {
|
for k, v := range query.Keywords {
|
||||||
|
if _, exist := cols[k]; !exist {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// fuzzy match
|
||||||
|
f, ok := v.(*q.FuzzyMatchValue)
|
||||||
|
if ok {
|
||||||
|
qs = qs.Filter(k+"__icontains", f.Value)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// range
|
||||||
|
r, ok := v.(*q.Range)
|
||||||
|
if ok {
|
||||||
|
if r.Min != nil {
|
||||||
|
qs = qs.Filter(k+"__gte", r.Min)
|
||||||
|
}
|
||||||
|
if r.Max != nil {
|
||||||
|
qs = qs.Filter(k+"__lte", r.Max)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// or list
|
||||||
|
ol, ok := v.(*q.OrList)
|
||||||
|
if ok {
|
||||||
|
qs = qs.Filter(k+"__in", ol.Values...)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// and list
|
||||||
|
_, ok = v.(*q.OrList)
|
||||||
|
if ok {
|
||||||
|
// do nothing as and list needs to be handled by the logic of DAO
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// exact match
|
||||||
qs = qs.Filter(k, v)
|
qs = qs.Filter(k, v)
|
||||||
}
|
}
|
||||||
if query.PageSize > 0 {
|
if query.PageSize > 0 {
|
||||||
@ -44,6 +88,60 @@ func QuerySetter(ctx context.Context, model interface{}, query *q.Query) (orm.Qu
|
|||||||
return qs, nil
|
return qs, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// list the columns that can be queried
|
||||||
|
// e.g. for the following model the columns that can be queried are:
|
||||||
|
// "Field2", "customized_field2", "Field3" and "field3"
|
||||||
|
// type model struct{
|
||||||
|
// Field1 string `orm:"-"`
|
||||||
|
// Field2 string `orm:"column(customized_field2)"`
|
||||||
|
// Field3 string
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// set "ignoredCols" to ignore the specified columns
|
||||||
|
func listQueriableCols(model interface{}, ignoredCols ...string) map[string]struct{} {
|
||||||
|
if model == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
ignored := map[string]struct{}{}
|
||||||
|
for _, ig := range ignoredCols {
|
||||||
|
ignored[ig] = struct{}{}
|
||||||
|
}
|
||||||
|
cols := map[string]struct{}{}
|
||||||
|
t := reflect.Indirect(reflect.ValueOf(model)).Type()
|
||||||
|
for i := 0; i < t.NumField(); i++ {
|
||||||
|
field := t.Field(i)
|
||||||
|
orm := field.Tag.Get("orm")
|
||||||
|
if orm == "-" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
colName := ""
|
||||||
|
for _, str := range strings.Split(orm, ";") {
|
||||||
|
if strings.HasPrefix(str, "column") {
|
||||||
|
str = strings.TrimPrefix(str, "column(")
|
||||||
|
str = strings.TrimSuffix(str, ")")
|
||||||
|
if len(str) > 0 {
|
||||||
|
colName = str
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(colName) == 0 {
|
||||||
|
// TODO convert the field.Name to snake_case
|
||||||
|
}
|
||||||
|
if _, exist := ignored[colName]; exist {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, exist := ignored[field.Name]; exist {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if len(colName) != 0 {
|
||||||
|
cols[colName] = struct{}{}
|
||||||
|
}
|
||||||
|
cols[field.Name] = struct{}{}
|
||||||
|
}
|
||||||
|
return cols
|
||||||
|
}
|
||||||
|
|
||||||
// ParamPlaceholderForIn returns a string that contains placeholders for sql keyword "in"
|
// ParamPlaceholderForIn returns a string that contains placeholders for sql keyword "in"
|
||||||
// e.g. n=3, returns "?,?,?"
|
// e.g. n=3, returns "?,?,?"
|
||||||
func ParamPlaceholderForIn(n int) string {
|
func ParamPlaceholderForIn(n int) string {
|
||||||
|
61
src/internal/orm/query_test.go
Normal file
61
src/internal/orm/query_test.go
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
// Copyright Project Harbor Authors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package orm
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestListQueriableCols(t *testing.T) {
|
||||||
|
type model struct {
|
||||||
|
Field1 string `orm:"column(field1)" json:"field1"`
|
||||||
|
Field2 string `orm:"column(customized_field2)"`
|
||||||
|
Field3 string
|
||||||
|
Field4 string `orm:"column(field4)"`
|
||||||
|
}
|
||||||
|
// without ignoring columns
|
||||||
|
cols := listQueriableCols(&model{})
|
||||||
|
require.Len(t, cols, 7)
|
||||||
|
_, exist := cols["Field1"]
|
||||||
|
assert.True(t, exist)
|
||||||
|
_, exist = cols["field1"]
|
||||||
|
assert.True(t, exist)
|
||||||
|
_, exist = cols["Field2"]
|
||||||
|
assert.True(t, exist)
|
||||||
|
_, exist = cols["customized_field2"]
|
||||||
|
assert.True(t, exist)
|
||||||
|
_, exist = cols["Field3"]
|
||||||
|
assert.True(t, exist)
|
||||||
|
_, exist = cols["Field4"]
|
||||||
|
assert.True(t, exist)
|
||||||
|
_, exist = cols["field4"]
|
||||||
|
assert.True(t, exist)
|
||||||
|
|
||||||
|
// with ignoring columns
|
||||||
|
cols = listQueriableCols(&model{}, "Field4")
|
||||||
|
require.Len(t, cols, 5)
|
||||||
|
_, exist = cols["Field1"]
|
||||||
|
assert.True(t, exist)
|
||||||
|
_, exist = cols["field1"]
|
||||||
|
assert.True(t, exist)
|
||||||
|
_, exist = cols["Field2"]
|
||||||
|
assert.True(t, exist)
|
||||||
|
_, exist = cols["customized_field2"]
|
||||||
|
assert.True(t, exist)
|
||||||
|
_, exist = cols["Field3"]
|
||||||
|
assert.True(t, exist)
|
||||||
|
}
|
@ -253,8 +253,11 @@ func (d *dao) DeleteProjectBlob(ctx context.Context, projectID int64, blobIDs ..
|
|||||||
if len(blobIDs) == 0 {
|
if len(blobIDs) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
ol := &q.OrList{}
|
||||||
kw := q.KeyWords{"blob_id__in": blobIDs}
|
for _, blobID := range blobIDs {
|
||||||
|
ol.Values = append(ol.Values, blobID)
|
||||||
|
}
|
||||||
|
kw := q.KeyWords{"blob_id": ol}
|
||||||
qs, err := orm.QuerySetter(ctx, &models.ProjectBlob{}, q.New(kw))
|
qs, err := orm.QuerySetter(ctx, &models.ProjectBlob{}, q.New(kw))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -160,7 +160,7 @@ func (suite *DaoTestSuite) TestListBlobs() {
|
|||||||
suite.Len(blobs, 1)
|
suite.Len(blobs, 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
blobs, err = suite.dao.ListBlobs(ctx, q.New(q.KeyWords{"digest__in": []string{digest1, digest2}}))
|
blobs, err = suite.dao.ListBlobs(ctx, q.New(q.KeyWords{"digest": &q.OrList{Values: []interface{}{digest1, digest2}}}))
|
||||||
if suite.Nil(err) {
|
if suite.Nil(err) {
|
||||||
suite.Len(blobs, 2)
|
suite.Len(blobs, 2)
|
||||||
}
|
}
|
||||||
@ -193,7 +193,11 @@ func (suite *DaoTestSuite) TestFindBlobsShouldUnassociatedWithProject() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
blobs, err := suite.dao.ListBlobs(ctx, q.New(q.KeyWords{"digest__in": blobDigests}))
|
ol := &q.OrList{}
|
||||||
|
for _, blobDigest := range blobDigests {
|
||||||
|
ol.Values = append(ol.Values, blobDigest)
|
||||||
|
}
|
||||||
|
blobs, err := suite.dao.ListBlobs(ctx, q.New(q.KeyWords{"digest": ol}))
|
||||||
suite.Nil(err)
|
suite.Nil(err)
|
||||||
suite.Len(blobs, 5)
|
suite.Len(blobs, 5)
|
||||||
|
|
||||||
|
@ -126,7 +126,11 @@ func (m *manager) List(ctx context.Context, params ListParams) ([]*Blob, error)
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(params.BlobDigests) > 0 {
|
if len(params.BlobDigests) > 0 {
|
||||||
kw["digest__in"] = params.BlobDigests
|
ol := &q.OrList{}
|
||||||
|
for _, blobDigest := range params.BlobDigests {
|
||||||
|
ol.Values = append(ol.Values, blobDigest)
|
||||||
|
}
|
||||||
|
kw["digest"] = ol
|
||||||
}
|
}
|
||||||
|
|
||||||
blobs, err := m.dao.ListBlobs(ctx, q.New(kw))
|
blobs, err := m.dao.ListBlobs(ctx, q.New(kw))
|
||||||
|
193
src/pkg/q/builder.go
Normal file
193
src/pkg/q/builder.go
Normal file
@ -0,0 +1,193 @@
|
|||||||
|
// Copyright Project Harbor Authors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package q
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
ierror "github.com/goharbor/harbor/src/internal/error"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Build query sting into the Query model
|
||||||
|
// query string format: q=k=v,k=~v,k=[min~max],k={v1 v2 v3},k=(v1 v2 v3),page=1,page_size=10
|
||||||
|
// exact match: k=v
|
||||||
|
// fuzzy match: k=~v
|
||||||
|
// range: k=[min~max]
|
||||||
|
// or list: k={v1 v2 v3}
|
||||||
|
// and list: k=(v1 v2 v3)
|
||||||
|
func Build(q string) (*Query, error) {
|
||||||
|
if len(q) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
query := &Query{Keywords: map[string]interface{}{}}
|
||||||
|
params := strings.Split(q, ",")
|
||||||
|
for _, param := range params {
|
||||||
|
strs := strings.SplitN(param, "=", 2)
|
||||||
|
if len(strs) != 2 || len(strs[0]) == 0 || len(strs[1]) == 0 {
|
||||||
|
return nil, ierror.New(nil).
|
||||||
|
WithCode(ierror.BadRequestCode).
|
||||||
|
WithMessage(`the query string must contain "=" and the key/value cannot be empty`)
|
||||||
|
}
|
||||||
|
if strs[0] == "page" {
|
||||||
|
i, err := strconv.ParseInt(strs[1], 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return nil, ierror.New(nil).
|
||||||
|
WithCode(ierror.BadRequestCode).
|
||||||
|
WithMessage("page must be integer")
|
||||||
|
}
|
||||||
|
query.PageNumber = i
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if strs[0] == "page_size" {
|
||||||
|
i, err := strconv.ParseInt(strs[1], 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return nil, ierror.New(nil).
|
||||||
|
WithCode(ierror.BadRequestCode).
|
||||||
|
WithMessage("page_size must be integer")
|
||||||
|
}
|
||||||
|
query.PageSize = i
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
value, err := parsePattern(strs[1])
|
||||||
|
if err != nil {
|
||||||
|
return nil, ierror.New(err).
|
||||||
|
WithCode(ierror.BadRequestCode).
|
||||||
|
WithMessage("invalid query string value: %s", strs[1])
|
||||||
|
}
|
||||||
|
query.Keywords[strs[0]] = value
|
||||||
|
}
|
||||||
|
return query, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parsePattern(value string) (interface{}, error) {
|
||||||
|
// empty string
|
||||||
|
if len(value) == 0 {
|
||||||
|
return value, nil
|
||||||
|
}
|
||||||
|
switch value[0] {
|
||||||
|
case '~':
|
||||||
|
return parseFuzzyMatchValue(value)
|
||||||
|
case '[':
|
||||||
|
return parseRange(value)
|
||||||
|
case '{':
|
||||||
|
return parseOrList(value)
|
||||||
|
case '(':
|
||||||
|
return parseAndList(value)
|
||||||
|
default:
|
||||||
|
// others: exact match
|
||||||
|
return escapeValue(value), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseFuzzyMatchValue(value string) (*FuzzyMatchValue, error) {
|
||||||
|
if len(value) < 2 || value[0] != '~' {
|
||||||
|
return nil, fmt.Errorf(`fuzzy match value must start with "~" and contain at least 1 other characters`)
|
||||||
|
}
|
||||||
|
return &FuzzyMatchValue{
|
||||||
|
Value: value[1:],
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseRange(value string) (*Range, error) {
|
||||||
|
length := len(value)
|
||||||
|
if value[length-1] != ']' || strings.Count(value, "~") != 1 {
|
||||||
|
return nil, fmt.Errorf(`range must start with "[", end with "]" and contains only one "~"`)
|
||||||
|
}
|
||||||
|
strs := strings.SplitN(value[1:length-1], "~", 2)
|
||||||
|
min := strings.TrimSpace(strs[0])
|
||||||
|
max := strings.TrimSpace(strs[1])
|
||||||
|
if len(min) == 0 && len(max) == 0 {
|
||||||
|
return nil, fmt.Errorf(`min and max at least one should be set in range'`)
|
||||||
|
}
|
||||||
|
r := &Range{}
|
||||||
|
if len(min) > 0 {
|
||||||
|
r.Min = parseValue(min)
|
||||||
|
}
|
||||||
|
if len(max) > 0 {
|
||||||
|
r.Max = parseValue(max)
|
||||||
|
}
|
||||||
|
return r, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseOrList(value string) (*OrList, error) {
|
||||||
|
values, err := parseList(value, '{')
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
ol := &OrList{}
|
||||||
|
for _, v := range values {
|
||||||
|
ol.Values = append(ol.Values, v)
|
||||||
|
}
|
||||||
|
return ol, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseAndList(value string) (*AndList, error) {
|
||||||
|
values, err := parseList(value, '(')
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
al := &AndList{}
|
||||||
|
for _, v := range values {
|
||||||
|
al.Values = append(al.Values, v)
|
||||||
|
}
|
||||||
|
return al, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseList(value string, c rune) ([]interface{}, error) {
|
||||||
|
length := len(value)
|
||||||
|
if c == '{' && value[length-1] != '}' {
|
||||||
|
return nil, fmt.Errorf(`or list must start with "{" and end with "}"`)
|
||||||
|
}
|
||||||
|
if c == '(' && value[length-1] != ')' {
|
||||||
|
return nil, fmt.Errorf(`and list must start with "(" and end with ")"`)
|
||||||
|
}
|
||||||
|
var vs []interface{}
|
||||||
|
strs := strings.Split(value[1:length-1], " ")
|
||||||
|
for _, str := range strs {
|
||||||
|
v := parseValue(str)
|
||||||
|
if s, ok := v.(string); ok && len(s) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
vs = append(vs, v)
|
||||||
|
}
|
||||||
|
return vs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// try to parse value as time first, then integer, and last string
|
||||||
|
func parseValue(value string) interface{} {
|
||||||
|
value = strings.TrimSpace(value)
|
||||||
|
// try to parse time
|
||||||
|
time, err := time.Parse("2006-01-02T15:04:05", value)
|
||||||
|
if err == nil {
|
||||||
|
return time
|
||||||
|
}
|
||||||
|
// try to parse integer
|
||||||
|
i, err := strconv.ParseInt(value, 10, 64)
|
||||||
|
if err == nil {
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
// if the value isn't time and integer, treat it as string
|
||||||
|
return strings.Trim(value, `"'`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// escape the special character
|
||||||
|
func escapeValue(value string) string {
|
||||||
|
if len(value) > 0 && value[0] == '\\' {
|
||||||
|
return value[1:]
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
273
src/pkg/q/builder_test.go
Normal file
273
src/pkg/q/builder_test.go
Normal file
@ -0,0 +1,273 @@
|
|||||||
|
// Copyright Project Harbor Authors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package q
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestParseFuzzyMatchValue(t *testing.T) {
|
||||||
|
// empty string
|
||||||
|
value := ""
|
||||||
|
v, err := parseFuzzyMatchValue(value)
|
||||||
|
require.NotNil(t, err)
|
||||||
|
|
||||||
|
// contain no other characters except "~"
|
||||||
|
value = "~"
|
||||||
|
v, err = parseFuzzyMatchValue(value)
|
||||||
|
require.NotNil(t, err)
|
||||||
|
|
||||||
|
// valid fuzzy match value
|
||||||
|
value = "~a"
|
||||||
|
v, err = parseFuzzyMatchValue(value)
|
||||||
|
require.Nil(t, err)
|
||||||
|
assert.Equal(t, "a", v.Value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseRange(t *testing.T) {
|
||||||
|
// contains only "["
|
||||||
|
value := "["
|
||||||
|
v, err := parseRange(value)
|
||||||
|
require.NotNil(t, err)
|
||||||
|
|
||||||
|
// contains no ","
|
||||||
|
value = "[]"
|
||||||
|
v, err = parseRange(value)
|
||||||
|
require.NotNil(t, err)
|
||||||
|
|
||||||
|
// contains no other character
|
||||||
|
value = "[~]"
|
||||||
|
v, err = parseRange(value)
|
||||||
|
require.NotNil(t, err)
|
||||||
|
|
||||||
|
// contains multiple "~"
|
||||||
|
value = "[~~]"
|
||||||
|
v, err = parseRange(value)
|
||||||
|
require.NotNil(t, err)
|
||||||
|
|
||||||
|
// contains multiple char
|
||||||
|
value = "[1~2~3]"
|
||||||
|
v, err = parseRange(value)
|
||||||
|
require.NotNil(t, err)
|
||||||
|
|
||||||
|
// valid value
|
||||||
|
value = "[1~]"
|
||||||
|
v, err = parseRange(value)
|
||||||
|
require.Nil(t, err)
|
||||||
|
assert.Equal(t, int64(1), v.Min.(int64))
|
||||||
|
assert.Nil(t, v.Max)
|
||||||
|
|
||||||
|
// valid value
|
||||||
|
value = "[~2]"
|
||||||
|
v, err = parseRange(value)
|
||||||
|
assert.Equal(t, int64(2), v.Max.(int64))
|
||||||
|
assert.Nil(t, v.Min)
|
||||||
|
|
||||||
|
// valid value
|
||||||
|
value = "[1~2]"
|
||||||
|
v, err = parseRange(value)
|
||||||
|
require.Nil(t, err)
|
||||||
|
assert.Equal(t, int64(1), v.Min.(int64))
|
||||||
|
assert.Equal(t, int64(2), v.Max.(int64))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseOrList(t *testing.T) {
|
||||||
|
// invalid
|
||||||
|
value := "}{"
|
||||||
|
v, err := parseOrList(value)
|
||||||
|
require.NotNil(t, err)
|
||||||
|
|
||||||
|
// valid value, contains no element
|
||||||
|
value = "{}"
|
||||||
|
v, err = parseOrList(value)
|
||||||
|
require.Nil(t, err)
|
||||||
|
assert.Len(t, v.Values, 0)
|
||||||
|
|
||||||
|
// valid value, contains only one element
|
||||||
|
value = "{1}"
|
||||||
|
v, err = parseOrList(value)
|
||||||
|
require.Nil(t, err)
|
||||||
|
require.Len(t, v.Values, 1)
|
||||||
|
assert.Equal(t, int64(1), v.Values[0].(int64))
|
||||||
|
|
||||||
|
// valid value, contains multiple elements
|
||||||
|
value = "{1 2 3}"
|
||||||
|
v, err = parseOrList(value)
|
||||||
|
require.Nil(t, err)
|
||||||
|
require.Len(t, v.Values, 3)
|
||||||
|
assert.Equal(t, int64(1), v.Values[0].(int64))
|
||||||
|
assert.Equal(t, int64(2), v.Values[1].(int64))
|
||||||
|
assert.Equal(t, int64(3), v.Values[2].(int64))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseAndList(t *testing.T) {
|
||||||
|
// invalid
|
||||||
|
value := ")("
|
||||||
|
v, err := parseAndList(value)
|
||||||
|
require.NotNil(t, err)
|
||||||
|
|
||||||
|
// valid value, contains no element
|
||||||
|
value = "()"
|
||||||
|
v, err = parseAndList(value)
|
||||||
|
require.Nil(t, err)
|
||||||
|
assert.Len(t, v.Values, 0)
|
||||||
|
|
||||||
|
// valid value, contains only one element
|
||||||
|
value = "(1)"
|
||||||
|
v, err = parseAndList(value)
|
||||||
|
require.Nil(t, err)
|
||||||
|
require.Len(t, v.Values, 1)
|
||||||
|
assert.Equal(t, int64(1), v.Values[0].(int64))
|
||||||
|
|
||||||
|
// valid value, contains multiple elements
|
||||||
|
value = "(1 2 3)"
|
||||||
|
v, err = parseAndList(value)
|
||||||
|
require.Nil(t, err)
|
||||||
|
require.Len(t, v.Values, 3)
|
||||||
|
assert.Equal(t, int64(1), v.Values[0].(int64))
|
||||||
|
assert.Equal(t, int64(2), v.Values[1].(int64))
|
||||||
|
assert.Equal(t, int64(3), v.Values[2].(int64))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseValue(t *testing.T) {
|
||||||
|
// time
|
||||||
|
value := "2020-03-04T17:08:23"
|
||||||
|
v := parseValue(value)
|
||||||
|
_, ok := v.(time.Time)
|
||||||
|
require.True(t, ok)
|
||||||
|
|
||||||
|
// integer
|
||||||
|
value = "1"
|
||||||
|
v = parseValue(value)
|
||||||
|
i, ok := v.(int64)
|
||||||
|
require.True(t, ok)
|
||||||
|
assert.Equal(t, int64(1), i)
|
||||||
|
|
||||||
|
// empty string
|
||||||
|
value = ""
|
||||||
|
v = parseValue(value)
|
||||||
|
str, ok := v.(string)
|
||||||
|
require.True(t, ok)
|
||||||
|
assert.Equal(t, "", str)
|
||||||
|
|
||||||
|
// not empty string
|
||||||
|
value = "abc"
|
||||||
|
v = parseValue(value)
|
||||||
|
str, ok = v.(string)
|
||||||
|
require.True(t, ok)
|
||||||
|
assert.Equal(t, "abc", str)
|
||||||
|
|
||||||
|
// not empty string
|
||||||
|
value = `"abc"`
|
||||||
|
v = parseValue(value)
|
||||||
|
str, ok = v.(string)
|
||||||
|
require.True(t, ok)
|
||||||
|
assert.Equal(t, "abc", str)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEscapeValue(t *testing.T) {
|
||||||
|
// empty string
|
||||||
|
value := ""
|
||||||
|
v := escapeValue(value)
|
||||||
|
assert.Equal(t, "", v)
|
||||||
|
|
||||||
|
// string contains no special character
|
||||||
|
value = "abc"
|
||||||
|
v = escapeValue(value)
|
||||||
|
assert.Equal(t, "abc", v)
|
||||||
|
|
||||||
|
// string starts with special character
|
||||||
|
value = `\~abc`
|
||||||
|
v = escapeValue(value)
|
||||||
|
assert.Equal(t, "~abc", v)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParsePattern(t *testing.T) {
|
||||||
|
// empty string
|
||||||
|
value := ""
|
||||||
|
v, err := parsePattern(value)
|
||||||
|
require.Nil(t, err)
|
||||||
|
_, ok := v.(string)
|
||||||
|
assert.True(t, ok)
|
||||||
|
|
||||||
|
// fuzzy match
|
||||||
|
value = "~a"
|
||||||
|
v, err = parsePattern(value)
|
||||||
|
require.Nil(t, err)
|
||||||
|
_, ok = v.(*FuzzyMatchValue)
|
||||||
|
assert.True(t, ok)
|
||||||
|
|
||||||
|
// range
|
||||||
|
value = "[1~3]"
|
||||||
|
v, err = parsePattern(value)
|
||||||
|
require.Nil(t, err)
|
||||||
|
_, ok = v.(*Range)
|
||||||
|
assert.True(t, ok)
|
||||||
|
|
||||||
|
// or list
|
||||||
|
value = "{1 2}"
|
||||||
|
v, err = parsePattern(value)
|
||||||
|
require.Nil(t, err)
|
||||||
|
_, ok = v.(*OrList)
|
||||||
|
assert.True(t, ok)
|
||||||
|
|
||||||
|
// and list
|
||||||
|
value = "(1 3)"
|
||||||
|
v, err = parsePattern(value)
|
||||||
|
require.Nil(t, err)
|
||||||
|
_, ok = v.(*AndList)
|
||||||
|
assert.True(t, ok)
|
||||||
|
|
||||||
|
// exact match
|
||||||
|
value = "a"
|
||||||
|
v, err = parsePattern(value)
|
||||||
|
require.Nil(t, err)
|
||||||
|
_, ok = v.(string)
|
||||||
|
assert.True(t, ok)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuild(t *testing.T) {
|
||||||
|
// empty string
|
||||||
|
q := ``
|
||||||
|
query, err := Build(q)
|
||||||
|
require.Nil(t, err)
|
||||||
|
assert.Nil(t, query)
|
||||||
|
|
||||||
|
// contains only ";"
|
||||||
|
q = `,`
|
||||||
|
query, err = Build(q)
|
||||||
|
require.NotNil(t, err)
|
||||||
|
|
||||||
|
// invalid page
|
||||||
|
q = `page=a`
|
||||||
|
query, err = Build(q)
|
||||||
|
require.NotNil(t, err)
|
||||||
|
|
||||||
|
// invalid page
|
||||||
|
q = `page_size=a`
|
||||||
|
query, err = Build(q)
|
||||||
|
require.NotNil(t, err)
|
||||||
|
|
||||||
|
// valid query string
|
||||||
|
q = `k=v,page=1,page_size=10`
|
||||||
|
query, err = Build(q)
|
||||||
|
require.Nil(t, err)
|
||||||
|
assert.Equal(t, int64(1), query.PageNumber)
|
||||||
|
assert.Equal(t, int64(10), query.PageSize)
|
||||||
|
assert.Equal(t, "v", query.Keywords["k"].(string))
|
||||||
|
}
|
@ -47,3 +47,24 @@ func Copy(query *Query) *Query {
|
|||||||
}
|
}
|
||||||
return q
|
return q
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Range query
|
||||||
|
type Range struct {
|
||||||
|
Min interface{}
|
||||||
|
Max interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AndList query
|
||||||
|
type AndList struct {
|
||||||
|
Values []interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// OrList query
|
||||||
|
type OrList struct {
|
||||||
|
Values []interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// FuzzyMatchValue query
|
||||||
|
type FuzzyMatchValue struct {
|
||||||
|
Value interface{}
|
||||||
|
}
|
||||||
|
@ -53,22 +53,24 @@ func (r *repositoryAPI) ListRepositories(ctx context.Context, params operation.L
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return r.SendError(ctx, err)
|
return r.SendError(ctx, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// set query
|
// set query
|
||||||
query := &q.Query{
|
var query *q.Query
|
||||||
Keywords: map[string]interface{}{
|
if params.Q != nil {
|
||||||
"ProjectID": project.ProjectID,
|
query, err = q.Build(*params.Q)
|
||||||
},
|
if err != nil {
|
||||||
|
return r.SendError(ctx, err)
|
||||||
}
|
}
|
||||||
// TODO support fuzzy match
|
|
||||||
if params.Name != nil {
|
|
||||||
query.Keywords["Name"] = *(params.Name)
|
|
||||||
}
|
}
|
||||||
if params.Page != nil {
|
|
||||||
query.PageNumber = *(params.Page)
|
if query == nil {
|
||||||
|
query = &q.Query{Keywords: map[string]interface{}{}}
|
||||||
}
|
}
|
||||||
if params.PageSize != nil {
|
if query.Keywords == nil {
|
||||||
query.PageSize = *(params.PageSize)
|
query.Keywords = map[string]interface{}{}
|
||||||
}
|
}
|
||||||
|
query.Keywords["ProjectID"] = project.ProjectID
|
||||||
|
|
||||||
total, err := r.repoCtl.Count(ctx, query)
|
total, err := r.repoCtl.Count(ctx, query)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return r.SendError(ctx, err)
|
return r.SendError(ctx, err)
|
||||||
|
Loading…
Reference in New Issue
Block a user