mirror of https://github.com/goharbor/harbor.git
235 lines
5.8 KiB
Go
235 lines
5.8 KiB
Go
// 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 (
|
|
"context"
|
|
"reflect"
|
|
"strings"
|
|
"sync"
|
|
"unicode"
|
|
|
|
"github.com/beego/beego/v2/client/orm"
|
|
|
|
"github.com/goharbor/harbor/src/lib/q"
|
|
)
|
|
|
|
var (
|
|
// cache the parsed models
|
|
cache = sync.Map{}
|
|
)
|
|
|
|
type key struct {
|
|
Name string
|
|
Filterable bool
|
|
FilterFunc func(context.Context, orm.QuerySeter, string, interface{}) orm.QuerySeter
|
|
Sortable bool
|
|
}
|
|
|
|
type metadata struct {
|
|
Keys map[string]*key
|
|
DefaultSorts []*q.Sort
|
|
}
|
|
|
|
func (m *metadata) Filterable(key string) (*key, bool) {
|
|
k, exist := m.Keys[key]
|
|
return k, exist
|
|
}
|
|
|
|
func (m *metadata) Sortable(key string) bool {
|
|
k, exist := m.Keys[key]
|
|
if !exist {
|
|
return false
|
|
}
|
|
return k.Sortable
|
|
}
|
|
|
|
// parse the definition of the provided model(fields/methods/annotations) and return the parsed metadata
|
|
func parseModel(model interface{}) *metadata {
|
|
// pointer type
|
|
ptr := reflect.TypeOf(model)
|
|
// struct type
|
|
t := ptr.Elem()
|
|
|
|
// get the metadata from cache first
|
|
fullName := getFullName(t)
|
|
cacheMetadata, exist := cache.Load(fullName)
|
|
if exist {
|
|
return cacheMetadata.(*metadata)
|
|
}
|
|
|
|
// pointer value
|
|
v := reflect.ValueOf(model)
|
|
metadata := &metadata{
|
|
Keys: map[string]*key{},
|
|
}
|
|
// parse fields of the provided model
|
|
for i := 0; i < t.NumField(); i++ {
|
|
field := t.Field(i)
|
|
orm := field.Tag.Get("orm")
|
|
// isn't the database column, skip
|
|
if orm == "-" {
|
|
continue
|
|
}
|
|
|
|
filterable := parseFilterable(field)
|
|
defaultSort, sortable := parseSortable(field)
|
|
column := parseColumn(field)
|
|
|
|
metadata.Keys[field.Name] = &key{
|
|
Name: field.Name,
|
|
Filterable: filterable,
|
|
Sortable: sortable,
|
|
}
|
|
metadata.Keys[column] = &key{
|
|
Name: column,
|
|
Filterable: filterable,
|
|
Sortable: sortable,
|
|
}
|
|
if defaultSort != nil {
|
|
metadata.DefaultSorts = []*q.Sort{defaultSort}
|
|
}
|
|
}
|
|
|
|
// parse filter methods of the provided model
|
|
for i := 0; i < ptr.NumMethod(); i++ {
|
|
methodName := ptr.Method(i).Name
|
|
if !strings.HasPrefix(methodName, "FilterBy") {
|
|
continue
|
|
}
|
|
methodValue := v.MethodByName(methodName)
|
|
if !methodValue.IsValid() {
|
|
continue
|
|
}
|
|
filterFunc, ok := methodValue.Interface().(func(context.Context, orm.QuerySeter, string, interface{}) orm.QuerySeter)
|
|
if !ok {
|
|
continue
|
|
}
|
|
field := strings.TrimPrefix(methodName, "FilterBy")
|
|
metadata.Keys[field] = &key{
|
|
Name: field,
|
|
Filterable: true,
|
|
FilterFunc: filterFunc,
|
|
}
|
|
snakeCaseField := snakeCase(field)
|
|
metadata.Keys[snakeCaseField] = &key{
|
|
Name: snakeCaseField,
|
|
Filterable: true,
|
|
FilterFunc: filterFunc,
|
|
}
|
|
}
|
|
|
|
// parse default sorts method
|
|
methodValue := v.MethodByName("GetDefaultSorts")
|
|
if methodValue.IsValid() {
|
|
values := methodValue.Call(nil)
|
|
if len(values) == 1 {
|
|
if sorts, ok := values[0].Interface().([]*q.Sort); ok && len(sorts) > 0 {
|
|
metadata.DefaultSorts = sorts
|
|
}
|
|
}
|
|
}
|
|
|
|
cache.Store(fullName, metadata)
|
|
return metadata
|
|
}
|
|
|
|
// parseFilterable parses whether the field is filterable according to the field annotation
|
|
// For the following struct definition, "Field1" isn't filterable and "Field2" is filterable
|
|
//
|
|
// type Model struct {
|
|
// Field1 string `filter:"false"`
|
|
// Field2 string
|
|
// }
|
|
func parseFilterable(field reflect.StructField) bool {
|
|
return field.Tag.Get("filter") != "false"
|
|
}
|
|
|
|
// parseSortable parses whether the field is sortable according to the field annotation
|
|
// If the field is sortable and is also specified as the default sort, return a q.Sort model as well
|
|
// For the following struct definition, "Field1" isn't sortable and "Field2", "Field2", "Field4", "Field5" are all sortable
|
|
//
|
|
// type Model struct {
|
|
// Field1 string `sort:"false"`
|
|
// Field2 string `sort:"true;default"`
|
|
// Field3 string `sort:"true;default:desc"`
|
|
// Field4 string `sort:"default"`
|
|
// Field5 string
|
|
// }
|
|
func parseSortable(field reflect.StructField) (*q.Sort, bool) {
|
|
var defaultSort *q.Sort
|
|
for _, item := range strings.Split(field.Tag.Get("sort"), ";") {
|
|
// isn't sortable, return directly
|
|
if item == "false" {
|
|
return nil, false
|
|
}
|
|
if !strings.HasPrefix(item, "default") {
|
|
continue
|
|
}
|
|
defaultSort = &q.Sort{
|
|
Key: field.Name,
|
|
DESC: false,
|
|
}
|
|
if strings.TrimPrefix(item, "default") == ":desc" {
|
|
defaultSort.DESC = true
|
|
}
|
|
}
|
|
return defaultSort, true
|
|
}
|
|
|
|
// parseColumn parses the column name according to the field annotation
|
|
//
|
|
// type Model struct {
|
|
// Field1 string `orm:"column(customized_field1)"`
|
|
// Field2 string
|
|
// }
|
|
//
|
|
// It returns "customized_field1" for "Field1" and returns "field2" for "Field2"
|
|
func parseColumn(field reflect.StructField) string {
|
|
column := ""
|
|
for _, item := range strings.Split(field.Tag.Get("orm"), ";") {
|
|
if !strings.HasPrefix(item, "column") {
|
|
continue
|
|
}
|
|
item = strings.TrimPrefix(item, "column(")
|
|
item = strings.TrimSuffix(item, ")")
|
|
column = item
|
|
break
|
|
}
|
|
if len(column) == 0 {
|
|
column = snakeCase(field.Name)
|
|
}
|
|
return column
|
|
}
|
|
|
|
// convert string to snake case
|
|
func snakeCase(str string) string {
|
|
delim := '_'
|
|
|
|
runes := []rune(str)
|
|
|
|
var out []rune
|
|
for i := 0; i < len(runes); i++ {
|
|
if i > 0 &&
|
|
(unicode.IsUpper(runes[i])) &&
|
|
((i+1 < len(runes) && unicode.IsLower(runes[i+1])) || unicode.IsLower(runes[i-1])) {
|
|
out = append(out, delim)
|
|
}
|
|
out = append(out, unicode.ToLower(runes[i]))
|
|
}
|
|
|
|
return string(out)
|
|
}
|