diff --git a/src/common/metadata/attribute_array.go b/src/common/metadata/attribute_array.go new file mode 100644 index 0000000000..21980a382c --- /dev/null +++ b/src/common/metadata/attribute_array.go @@ -0,0 +1,103 @@ +/* + * Tencent is pleased to support the open source community by making 蓝鲸 available. + * Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved. + * Licensed under the MIT License (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * http://opensource.org/licenses/MIT + * 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 metadata + +import ( + "configcenter/src/common/util" + "encoding/json" + "fmt" + "math" + + "github.com/tidwall/gjson" + "go.mongodb.org/mongo-driver/bson" +) + +// ArrayOption len cap ,option is basic type 's option +type ArrayOption[T any] struct { + Len int `bson:"len" json:"len"` + Cap int `bson:"cap" json:"cap"` + Option T `bson:"option" json:"option"` +} + +// Valid ArrayOption +func (a *ArrayOption[T]) Valid() error { + if a.Len < 0 || a.Len > a.Cap { + return fmt.Errorf("invalid array option,len:%d cap:%d", a.Len, a.Cap) + } + return nil +} + +// ParseArrayOption len cap ,option is basic type 's option +func ParseArrayOption[T any](option any, handle func(v any) (T, error)) (ArrayOption[T], error) { + if option == nil { + return ArrayOption[T]{Len: math.MaxInt, Cap: math.MaxInt}, nil + } + + var result ArrayOption[T] + + optMap := map[string]interface{}{ + "len": math.MaxInt, + "cap": math.MaxInt, + } + switch value := option.(type) { + case ArrayOption[T]: + return value, nil + case bson.M: + optMap = value + case map[string]interface{}: + optMap = value + default: + marshal, err := json.Marshal(option) + if err != nil { + return result, fmt.Errorf("invalid array option,type:%v,value:%v,err:%w", + option, option, err) + } + + lenItem := gjson.GetBytes(marshal, "len") + capItem := gjson.GetBytes(marshal, "cap") + if !lenItem.Exists() || !capItem.Exists() { + return result, fmt.Errorf("invalid array option,type:%v,value:%v,err: not exist len or cap", option, option) + } + optMap["len"] = lenItem.Int() + optMap["cap"] = capItem.Int() + optMap["option"] = gjson.GetBytes(marshal, "option").Value() + } + + lenn, lenOk := optMap["len"] + capp, capOk := optMap["cap"] + if !lenOk || !capOk { + return result, fmt.Errorf("invalid array option,type:%v,value:%v,err: not exist len or cap", option, option) + } + + lenOpt, err := util.GetIntByInterface(lenn) + if err != nil { + return result, err + } + result.Len = lenOpt + capOpt, err := util.GetIntByInterface(capp) + if err != nil { + return result, err + } + result.Cap = capOpt + + var defaultOption T + result.Option = defaultOption + if handle != nil { + t, err := handle(optMap["option"]) + if err != nil { + return ArrayOption[T]{}, err + } + result.Option = t + } + return result, result.Valid() +} diff --git a/src/common/metadata/attribute_array_bool.go b/src/common/metadata/attribute_array_bool.go new file mode 100644 index 0000000000..6ceaea3f39 --- /dev/null +++ b/src/common/metadata/attribute_array_bool.go @@ -0,0 +1,161 @@ +/* + * Tencent is pleased to support the open source community by making 蓝鲸 available. + * Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved. + * Licensed under the MIT License (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * http://opensource.org/licenses/MIT + * 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 metadata + +import ( + "context" + "fmt" + + "configcenter/src/common" + "configcenter/src/common/blog" + "configcenter/src/common/mapstr" + "configcenter/src/common/util" + "configcenter/src/common/valid/attribute/manager/register" +) + +func init() { + // Register the arrayBool attribute type + register.Register(arrayBool{}) +} + +type arrayBool struct { +} + +// Name returns the name of the arrayBool attribute. +func (a arrayBool) Name() string { + return "array_bool" +} + +// DisplayName returns the display name for user. +func (a arrayBool) DisplayName() string { + return "布尔数组" +} + +// RealType returns the db type of the arrayBool attribute. +// Flattened array uses LongChar as storage type +func (a arrayBool) RealType() string { + return common.FieldTypeLongChar +} + +// Info returns the tips for user. +func (a arrayBool) Info() string { + return "布尔值的扁平化数组字段,存储多个true/false值" +} + +// Validate validates the arrayBool attribute value +func (a arrayBool) Validate(ctx context.Context, objID string, propertyType string, required bool, + option, value interface{}) error { + + rid := util.ExtractRequestIDFromContext(ctx) + + if value == nil { + if required { + blog.Errorf("array_bool attribute %s.%s value is required but got nil, rid: %s", + objID, propertyType, rid) + return fmt.Errorf("array_bool attribute %s.%s value is required but got nil", + objID, propertyType) + } + return nil + } + opts, err := ParseArrayOption[any](option, nil) + if err != nil { + blog.Errorf("array_bool parse option failed: %v, rid: %s", err, rid) + return fmt.Errorf("array_bool invalid option: %v", err) + } + + // Validate that value is a slice of any + boolArray, ok := util.ConvertAnyToSlice(value) + if !ok { + blog.Errorf("array_bool attribute %s.%s value must be []interface{}, got %T, rid: %s", + objID, propertyType, value, rid) + return fmt.Errorf("array_bool attribute %s.%s value must be []interface{}, got %T", + objID, propertyType, value) + } + if opts.Cap < len(boolArray) { + return fmt.Errorf("array_bool invalid cap %d, rid: %s", opts.Cap, rid) + } + // Validate each item in the array is a boolean + for i, item := range boolArray { + if _, ok := item.(bool); !ok { + blog.Errorf("array_bool attribute %s.%s array item [%d] type %T is not bool, rid: %s", + objID, propertyType, i, item, rid) + return fmt.Errorf("array_bool attribute %s.%s array item [%v] type %T is not bool", + objID, propertyType, item, item) + } + } + + return nil +} + +// FillLostValue fills the lost value with default value +func (a arrayBool) FillLostValue(ctx context.Context, valData mapstr.MapStr, propertyId string, + defaultValue, option interface{}) error { + + rid := util.ExtractRequestIDFromContext(ctx) + + valData[propertyId] = nil + if defaultValue == nil { + return nil + } + + // Validate default value + defaultArray, ok := util.ConvertAnyToSlice(defaultValue) + if !ok { + blog.Errorf("array_bool default value must be []interface{}, got %T, rid: %s", defaultValue, rid) + return fmt.Errorf("array_bool default value must be []interface{}, got %T", defaultValue) + } + + // Validate each item in default array + for i, item := range defaultArray { + if _, ok := item.(bool); !ok { + blog.Errorf("array_bool default value array item [%d] type %T is not bool, rid: %s", i, item, rid) + return fmt.Errorf("array_bool default value array item [%d] type %T is not bool", i, item) + } + } + + valData[propertyId] = defaultArray + return nil +} + +// ValidateOption validates the option field +func (a arrayBool) ValidateOption(ctx context.Context, option interface{}, defaultVal interface{}) error { + + rid := util.ExtractRequestIDFromContext(ctx) + + _, err := ParseArrayOption[any](option, nil) + if err != nil { + return err + } + if defaultVal == nil { + return nil + } + + // Validate default value + defaultArray, ok := util.ConvertAnyToSlice(defaultVal) + if !ok { + blog.Errorf("array_bool default value must be []interface{}, got %T, rid: %s", defaultVal, rid) + return fmt.Errorf("array_bool default value must be []interface{}, got %T", defaultVal) + } + + // Validate each item in default array + for i, item := range defaultArray { + if _, ok := item.(bool); !ok { + blog.Errorf("array_bool default value array item [%d] type %T is not bool, rid: %s", i, item, rid) + return fmt.Errorf("array_bool default value array item [%d] type %T is not bool", i, item) + } + } + + return nil +} + +var _ register.AttributeTypeI = &arrayBool{} diff --git a/src/common/metadata/attribute_array_date.go b/src/common/metadata/attribute_array_date.go new file mode 100644 index 0000000000..e8924c0b84 --- /dev/null +++ b/src/common/metadata/attribute_array_date.go @@ -0,0 +1,161 @@ +/* + * Tencent is pleased to support the open source community by making 蓝鲸 available. + * Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved. + * Licensed under the MIT License (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * http://opensource.org/licenses/MIT + * 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 metadata + +import ( + "configcenter/src/common" + "configcenter/src/common/blog" + "configcenter/src/common/mapstr" + "configcenter/src/common/util" + "configcenter/src/common/valid/attribute/manager/register" + "context" + "fmt" +) + +func init() { + // Register the arrayDate attribute type + register.Register(arrayDate{}) +} + +type arrayDate struct { +} + +// Name returns the name of the arrayDate attribute. +func (a arrayDate) Name() string { + return "array_date" +} + +// DisplayName returns the display name for user. +func (a arrayDate) DisplayName() string { + return "日期数组" +} + +// RealType returns the db type of the arrayDate attribute. +// Flattened array uses LongChar as storage type +func (a arrayDate) RealType() string { + return common.FieldTypeLongChar +} + +// Info returns the tips for user. +func (a arrayDate) Info() string { + return "日期数组,格式为YYYY-MM-DD" +} + +// Validate validates the arrayDate attribute value +func (a arrayDate) Validate(ctx context.Context, objID string, propertyType string, required bool, + option interface{}, value interface{}) error { + + rid := util.ExtractRequestIDFromContext(ctx) + + if value == nil { + if required { + blog.Errorf("array_date attribute %s.%s value is required but got nil, rid: %s", + objID, propertyType, rid) + return fmt.Errorf("array_date attribute %s.%s value is required but got nil", + objID, propertyType) + } + return nil + } + + // Validate that value is a slice of any + dateArray, ok := util.ConvertAnyToSlice(value) + if !ok { + blog.Errorf("array_date attribute %s.%s value must be []interface{}, got %T, rid: %s", + objID, propertyType, value, rid) + return fmt.Errorf("array_date attribute %s.%s value must be []interface{}, got %T", + objID, propertyType, value) + } + + opts, err := ParseArrayOption[string](option, nil) + if err != nil { + blog.Errorf("array_date parse option failed: %v, rid: %s", err, rid) + return fmt.Errorf("array_date invalid option: %v", err) + } + if opts.Cap < len(dateArray) { + return fmt.Errorf("array_date invalid cap %d, rid: %s", opts.Cap, rid) + } + // Validate each item in the array + for i, item := range dateArray { + // Validate date format + if !util.IsDate(item) { + blog.Errorf("array_date attribute %s.%s array item [%d] type %T is not a valid date, rid: %s", + objID, propertyType, i, item, rid) + return fmt.Errorf("array_date attribute %s.%s array item [%d] is not a valid date", + objID, propertyType, i) + } + } + + return nil +} + +// FillLostValue fills the lost value with default value +func (a arrayDate) FillLostValue(ctx context.Context, valData mapstr.MapStr, propertyId string, + defaultValue, option interface{}) error { + + rid := util.ExtractRequestIDFromContext(ctx) + + valData[propertyId] = nil + if defaultValue == nil { + return nil + } + + // Validate default value + defaultArray, ok := util.ConvertAnyToSlice(defaultValue) + if !ok { + blog.Errorf("array_date default value must be []interface{}, got %T, rid: %s", defaultValue, rid) + return fmt.Errorf("array_date default value must be []interface{}, got %T", defaultValue) + } + + // Validate each item in default array + for i, item := range defaultArray { + if !util.IsDate(item) { + blog.Errorf("array_date default value array item [%d] type %T is not a valid date, rid: %s", i, item, rid) + return fmt.Errorf("array_date default value array item [%d] is not a valid date", i) + } + } + + valData[propertyId] = defaultArray + return nil +} + +// ValidateOption validates the option field +func (a arrayDate) ValidateOption(ctx context.Context, option interface{}, defaultVal interface{}) error { + rid := util.ExtractRequestIDFromContext(ctx) + + _, err := ParseArrayOption[string](option, nil) + if err != nil { + return err + } + if defaultVal == nil { + return nil + } + + // Validate default value + defaultArray, ok := util.ConvertAnyToSlice(defaultVal) + if !ok { + blog.Errorf("array_date default value must be []interface{}, got %T, rid: %s", defaultVal, rid) + return fmt.Errorf("array_date default value must be []interface{}, got %T", defaultVal) + } + + // Validate each item in default array + for i, item := range defaultArray { + if !util.IsDate(item) { + blog.Errorf("array_date default value array item [%d] type %T is not a valid date, rid: %s", i, item, rid) + return fmt.Errorf("array_date default value array item [%v] is not a valid date", item) + } + } + + return nil +} + +var _ register.AttributeTypeI = &arrayDate{} diff --git a/src/common/metadata/attribute_array_document.go b/src/common/metadata/attribute_array_document.go new file mode 100644 index 0000000000..db87670e0e --- /dev/null +++ b/src/common/metadata/attribute_array_document.go @@ -0,0 +1,166 @@ +/* + * Tencent is pleased to support the open source community by making 蓝鲸 available. + * Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved. + * Licensed under the MIT License (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * http://opensource.org/licenses/MIT + * 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 metadata + +import ( + "context" + "fmt" + + "configcenter/src/common" + "configcenter/src/common/blog" + "configcenter/src/common/mapstr" + "configcenter/src/common/util" + "configcenter/src/common/valid/attribute/manager/register" +) + +func init() { + register.Register(&arrayDocument{}) +} + +// arrayDocument represents a Document array attribute type. +type arrayDocument struct{} + +// Name returns the name of the arrayDocument attribute. +func (a *arrayDocument) Name() string { + return "array_document" +} + +// DisplayName returns the display name for user. +func (a *arrayDocument) DisplayName() string { + return "附件数组" +} + +// RealType returns the db type of the document attribute. +func (a *arrayDocument) RealType() string { + return common.FieldTypeLongChar +} + +// Info returns the tips for user. +func (a *arrayDocument) Info() string { + return "附件数组" +} + +// Validate validates the arrayDocument attribute value. +func (a *arrayDocument) Validate(ctx context.Context, objID, propertyType string, required bool, + option, value interface{}) error { + + rid := util.ExtractRequestIDFromContext(ctx) + + if value == nil { + if required { + blog.Errorf("array_document %s.%s required, rid: %s", objID, propertyType, rid) + return fmt.Errorf("array_document %s.%s required", objID, propertyType) + } + return nil + } + + arr, ok := util.ConvertAnyToSlice(value) + if !ok { + blog.Errorf("array_document %s.%s not []interface{}, got %T, rid: %s", + objID, propertyType, value, rid) + return fmt.Errorf("array_document %s.%s must be array", objID, propertyType) + } + + opts, err := a.parseArrayDocumentOption(option) + if err != nil { + blog.Errorf("array_document parse option failed: %v, rid: %s", err, rid) + return fmt.Errorf("array_document invalid option: %v", err) + } + + return a.validateDocumentArray(rid, objID, propertyType, arr, opts) +} + +// FillLostValue fills missing values with default value. +func (a *arrayDocument) FillLostValue(ctx context.Context, valData mapstr.MapStr, + propertyID string, defaultValue, option interface{}) error { + + rid := util.ExtractRequestIDFromContext(ctx) + + valData[propertyID] = nil + if defaultValue == nil { + return nil + } + + arr, ok := util.ConvertAnyToSlice(defaultValue) + if !ok { + blog.Errorf("array_document default not []interface{}, rid: %s", rid) + return fmt.Errorf("array_document default must be array") + } + + opts, err := a.parseArrayDocumentOption(option) + if err != nil { + blog.Errorf("array_document parse option failed: %v, rid: %s", err, rid) + return fmt.Errorf("array_document invalid option: %v", err) + } + + if err := a.validateDocumentArray(rid, "", "", arr, opts); err != nil { + return err + } + + valData[propertyID] = arr + return nil +} + +// ValidateOption validates the option field. +func (a *arrayDocument) ValidateOption(ctx context.Context, option, defaultVal interface{}) error { + + rid := util.ExtractRequestIDFromContext(ctx) + + opts, err := a.parseArrayDocumentOption(option) + if err != nil { + blog.Errorf("array_document parse option failed: %v, rid: %s", err, rid) + return fmt.Errorf("array_document invalid option: %v", err) + } + err = document{}.ValidateOption(ctx, opts.Option, nil) + if err != nil { + return err + } + if defaultVal == nil { + return nil + } + + arr, ok := util.ConvertAnyToSlice(defaultVal) + if !ok { + blog.Errorf("array_document default not []interface{}, rid: %s", rid) + return fmt.Errorf("array_document default must be array") + } + + return a.validateDocumentArray(rid, "", "", arr, opts) +} + +// validateDocumentArray validates all Documents in array are within range. +func (a *arrayDocument) validateDocumentArray(rid, objID, prop string, + arr []interface{}, opts ArrayOption[DocumentOption]) error { + + if opts.Cap < len(arr) { + return fmt.Errorf("array_document invalid cap %d, rid: %s", opts.Cap, rid) + } + for _, v := range arr { + _, err := document{}.ParseValue(v) + if err != nil { + return err + } + } + return nil +} + +// parseArrayDocumentOption parses the option into DocumentOption. +func (a *arrayDocument) parseArrayDocumentOption(option interface{}) (ArrayOption[DocumentOption], error) { + arrayOption, err := ParseArrayOption[DocumentOption](option, document{}.ParseOption) + if err != nil { + return ArrayOption[DocumentOption]{}, err + } + return arrayOption, nil +} + +var _ register.AttributeTypeI = (*arrayDocument)(nil) diff --git a/src/common/metadata/attribute_array_float.go b/src/common/metadata/attribute_array_float.go new file mode 100644 index 0000000000..af557663a8 --- /dev/null +++ b/src/common/metadata/attribute_array_float.go @@ -0,0 +1,187 @@ +/* + * Tencent is pleased to support the open source community by making 蓝鲸 available. + * Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved. + * Licensed under the MIT License (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * http://opensource.org/licenses/MIT + * 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 metadata + +import ( + "context" + "fmt" + + "configcenter/src/common" + "configcenter/src/common/blog" + "configcenter/src/common/mapstr" + "configcenter/src/common/util" + "configcenter/src/common/valid/attribute/manager/register" +) + +func init() { + register.Register(&arrayFloat{}) +} + +// arrayFloat represents a float array attribute type. +type arrayFloat struct{} + +// Name returns the name of the arrayDocument attribute. +func (a *arrayFloat) Name() string { + return "array_float" +} + +// DisplayName returns the display name for user. +func (a *arrayFloat) DisplayName() string { + return "浮点数数组" +} + +// RealType returns the db type of the document attribute. +func (a *arrayFloat) RealType() string { + return common.FieldTypeLongChar +} + +// Info returns the tips for user. +func (a *arrayFloat) Info() string { + return "浮点数数组" +} + +// Validate validates the arrayFloat attribute value. +func (a *arrayFloat) Validate(ctx context.Context, objID, propertyType string, required bool, + option, value interface{}) error { + + rid := util.ExtractRequestIDFromContext(ctx) + + if value == nil { + if required { + blog.Errorf("array_float %s.%s required, rid: %s", objID, propertyType, rid) + return fmt.Errorf("array_float %s.%s required", objID, propertyType) + } + return nil + } + + arr, ok := util.ConvertAnyToSlice(value) + if !ok { + blog.Errorf("array_float %s.%s not []interface{}, got %T, rid: %s", + objID, propertyType, value, rid) + return fmt.Errorf("array_float %s.%s must be array", objID, propertyType) + } + + opts, err := a.parseArrayFloatOption(option) + if err != nil { + blog.Errorf("array_float parse option failed: %v, rid: %s", err, rid) + return fmt.Errorf("array_float invalid option: %v", err) + } + + return a.validateFloatArray(rid, objID, propertyType, arr, opts) +} + +// FillLostValue fills missing values with default value. +func (a *arrayFloat) FillLostValue(ctx context.Context, valData mapstr.MapStr, + propertyID string, defaultValue, option interface{}) error { + + rid := util.ExtractRequestIDFromContext(ctx) + + valData[propertyID] = nil + if defaultValue == nil { + return nil + } + + arr, ok := util.ConvertAnyToSlice(defaultValue) + if !ok { + blog.Errorf("array_float default not []interface{}, rid: %s", rid) + return fmt.Errorf("array_float default must be array") + } + + opts, err := a.parseArrayFloatOption(option) + if err != nil { + blog.Errorf("array_float parse option failed: %v, rid: %s", err, rid) + return fmt.Errorf("array_float invalid option: %v", err) + } + + if err := a.validateFloatArray(rid, "", "", arr, opts); err != nil { + return err + } + + valData[propertyID] = arr + return nil +} + +// ValidateOption validates the option field. +func (a *arrayFloat) ValidateOption(ctx context.Context, option, defaultVal interface{}) error { + + rid := util.ExtractRequestIDFromContext(ctx) + + opts, err := a.parseArrayFloatOption(option) + if err != nil { + blog.Errorf("array_float parse option failed: %v, rid: %s", err, rid) + return fmt.Errorf("array_float invalid option: %v", err) + } + + if opts.Option.Min > opts.Option.Max { + blog.Errorf("array_float min %f > max %f, rid: %s", + opts.Option.Min, opts.Option.Max, rid) + return fmt.Errorf("array_float min must not exceed max") + } + + if defaultVal == nil { + return nil + } + + arr, ok := util.ConvertAnyToSlice(defaultVal) + if !ok { + blog.Errorf("array_float default not []interface{}, rid: %s", rid) + return fmt.Errorf("array_float default must be array") + } + + return a.validateFloatArray(rid, "", "", arr, opts) +} + +// validateFloatArray validates all floats in array are within range. +func (a *arrayFloat) validateFloatArray(rid, objID, prop string, + arr []interface{}, opts ArrayOption[FloatOption]) error { + + if opts.Cap < len(arr) { + return fmt.Errorf("array_float invalid cap %d, rid: %s", opts.Cap, rid) + } + for i, v := range arr { + floatVal, err := util.GetFloat64ByInterface(v) + if err != nil { + if objID != "" { + blog.Errorf("array_float %s.%s item [%d] not float64, rid: %s", + objID, prop, i, rid) + return fmt.Errorf("array_float %s.%s item [%d] not float64", objID, prop, i) + } + blog.Errorf("array_float item [%d] not float64, rid: %s", i, rid) + return fmt.Errorf("array_float item [%d] not float64", i) + } + if floatVal < opts.Option.Min || floatVal > opts.Option.Max { + if objID != "" { + blog.Errorf("array_float %s.%s item [%d] %f not in [%f,%f], rid: %s", + objID, prop, i, floatVal, opts.Option.Min, opts.Option.Max, rid) + return fmt.Errorf("array_float %s.%s item [%d] not in [%f,%f]", + objID, prop, i, opts.Option.Min, opts.Option.Max) + } + blog.Errorf("array_float item [%d] %f not in [%f,%f], rid: %s", + i, floatVal, opts.Option.Min, opts.Option.Max, rid) + return fmt.Errorf("array_float item [%d] not in [%f,%f]", + i, opts.Option.Min, opts.Option.Max) + } + } + return nil +} + +// parseArrayFloatOption parses the option into FloatOption. +func (a *arrayFloat) parseArrayFloatOption(option interface{}) (ArrayOption[FloatOption], error) { + arrayOption, err := ParseArrayOption[FloatOption](option, ParseFloatOption) + if err != nil { + return ArrayOption[FloatOption]{}, err + } + return arrayOption, nil +} + +var _ register.AttributeTypeI = (*arrayFloat)(nil) diff --git a/src/common/metadata/attribute_array_int.go b/src/common/metadata/attribute_array_int.go new file mode 100644 index 0000000000..26021a1eba --- /dev/null +++ b/src/common/metadata/attribute_array_int.go @@ -0,0 +1,189 @@ +/* + * Tencent is pleased to support the open source community by making 蓝鲸 available. + * Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved. + * Licensed under the MIT License (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * http://opensource.org/licenses/MIT + * 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 metadata + +import ( + "context" + "fmt" + + "configcenter/src/common" + "configcenter/src/common/blog" + "configcenter/src/common/mapstr" + "configcenter/src/common/util" + "configcenter/src/common/valid/attribute/manager/register" +) + +func init() { + register.Register(&arrayInt{}) +} + +// arrayInt represents an integer array attribute type. +type arrayInt struct{} + +// Name returns the name of the arrayDocument attribute. +func (a *arrayInt) Name() string { + return "array_int" +} + +// DisplayName returns the display name for user. +func (a *arrayInt) DisplayName() string { + return "整数数组" +} + +// RealType returns the db type of the document attribute. +func (a *arrayInt) RealType() string { + return common.FieldTypeLongChar +} + +// Info returns the tips for user. +func (a *arrayInt) Info() string { + return "整数数组" +} + +// Validate validates the arrayInt attribute value. +func (a *arrayInt) Validate(ctx context.Context, objID, propertyType string, required bool, + option, value interface{}) error { + + rid := util.ExtractRequestIDFromContext(ctx) + + if value == nil { + if required { + blog.Errorf("array_int %s.%s required, rid: %s", objID, propertyType, rid) + return fmt.Errorf("array_int %s.%s required", objID, propertyType) + } + return nil + } + + arr, ok := util.ConvertAnyToSlice(value) + if !ok { + blog.Errorf("array_int %s.%s not []interface{}, got %T, rid: %s", + objID, propertyType, value, rid) + return fmt.Errorf("array_int %s.%s must be array", objID, propertyType) + } + + opts, err := a.parseArrayIntOption(option) + if err != nil { + blog.Errorf("array_int parse option failed: %v, rid: %s", err, rid) + return fmt.Errorf("array_int invalid option: %v", err) + } + + return a.validateIntArray(rid, objID, propertyType, arr, opts) +} + +// FillLostValue fills missing values with default value. +func (a *arrayInt) FillLostValue(ctx context.Context, valData mapstr.MapStr, + propertyID string, defaultValue, option interface{}) error { + + rid := util.ExtractRequestIDFromContext(ctx) + + valData[propertyID] = nil + if defaultValue == nil { + return nil + } + + arr, ok := util.ConvertAnyToSlice(defaultValue) + if !ok { + blog.Errorf("array_int default not []interface{}, rid: %s", rid) + return fmt.Errorf("array_int default must be array") + } + + opts, err := a.parseArrayIntOption(option) + if err != nil { + blog.Errorf("array_int parse option failed: %v, rid: %s", err, rid) + return fmt.Errorf("array_int invalid option: %v", err) + } + + if err := a.validateIntArray(rid, "", "", arr, opts); err != nil { + return err + } + + valData[propertyID] = arr + return nil +} + +// ValidateOption validates the option field. +func (a *arrayInt) ValidateOption(ctx context.Context, option, defaultVal interface{}) error { + + rid := util.ExtractRequestIDFromContext(ctx) + + opts, err := a.parseArrayIntOption(option) + if err != nil { + blog.Errorf("array_int parse option failed: %v, rid: %s", err, rid) + return fmt.Errorf("array_int invalid option: %v", err) + } + + if opts.Option.Min > opts.Option.Max { + blog.Errorf("array_int min %d > max %d, rid: %s", + opts.Option.Min, opts.Option.Max, rid) + return fmt.Errorf("array_int min must not exceed max") + } + + if defaultVal == nil { + return nil + } + + arr, ok := util.ConvertAnyToSlice(defaultVal) + if !ok { + blog.Errorf("array_int default not []interface{}, rid: %s", rid) + return fmt.Errorf("array_int default must be array") + } + + return a.validateIntArray(rid, "", "", arr, opts) +} + +// validateIntArray validates all integers in array are within range. +func (a *arrayInt) validateIntArray(rid, objID, prop string, + arr []interface{}, opts ArrayOption[IntOption]) error { + + if opts.Cap < len(arr) { + return fmt.Errorf("array_int invalid cap %d, rid: %s", opts.Cap, rid) + } + for i, v := range arr { + intVal, err := util.GetInt64ByInterface(v) + if err != nil { + if objID != "" { + blog.Errorf("array_int %s.%s item [%d] not int64, rid: %s", + objID, prop, i, rid) + return fmt.Errorf("array_int %s.%s item [%d] not int64", objID, prop, i) + } + blog.Errorf("array_int item [%d] not int64, rid: %s", i, rid) + return fmt.Errorf("array_int item [%d] not int64", i) + } + + if intVal < opts.Option.Min || intVal > opts.Option.Max { + if objID != "" { + blog.Errorf("array_int %s.%s item [%d] %d not in [%d,%d], rid: %s", + objID, prop, i, intVal, opts.Option.Min, opts.Option.Max, rid) + return fmt.Errorf("array_int %s.%s item [%d] not in [%d,%d]", + objID, prop, i, opts.Option.Min, opts.Option.Max) + } + blog.Errorf("array_int item [%d] %d not in [%d,%d], rid: %s", + i, intVal, opts.Option.Min, opts.Option.Max, rid) + return fmt.Errorf("array_int item [%d] not in [%d,%d]", + i, opts.Option.Min, opts.Option.Max) + } + } + return nil +} + +// parseArrayIntOption parses the option into IntOption. +func (a *arrayInt) parseArrayIntOption(option interface{}) (ArrayOption[IntOption], error) { + arrayOption, err := ParseArrayOption[IntOption](option, ParseIntOption) + if err != nil { + return ArrayOption[IntOption]{}, err + } + fmt.Println(arrayOption) + return arrayOption, nil +} + +var _ register.AttributeTypeI = (*arrayInt)(nil) diff --git a/src/common/metadata/attribute_array_longchar.go b/src/common/metadata/attribute_array_longchar.go new file mode 100644 index 0000000000..67934b6adc --- /dev/null +++ b/src/common/metadata/attribute_array_longchar.go @@ -0,0 +1,269 @@ +/* + * Tencent is pleased to support the open source community by making 蓝鲸 available. + * Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved. + * Licensed under the MIT License (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * http://opensource.org/licenses/MIT + * 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 metadata + +import ( + "context" + "fmt" + "regexp" + + "configcenter/src/common" + "configcenter/src/common/blog" + "configcenter/src/common/mapstr" + "configcenter/src/common/util" + "configcenter/src/common/valid/attribute/manager/register" +) + +func init() { + // Register the arrayLongchar attribute type + register.Register(arrayLongchar{}) +} + +type arrayLongchar struct { +} + +// Name returns the name of the arrayLongchar attribute. +func (a arrayLongchar) Name() string { + return "array_longchar" +} + +// DisplayName returns the display name for user. +func (a arrayLongchar) DisplayName() string { + return "长字符数组" +} + +// RealType returns the db type of the arrayLongchar attribute. +// Flattened array uses LongChar as storage type +func (a arrayLongchar) RealType() string { + return common.FieldTypeLongChar +} + +// Info returns the tips for user. +func (a arrayLongchar) Info() string { + return "长字符数组" +} + +// Validate validates the arrayLongchar attribute value +func (a arrayLongchar) Validate(ctx context.Context, objID string, propertyType string, required bool, + option, value interface{}) error { + + rid := util.ExtractRequestIDFromContext(ctx) + + if value == nil { + if required { + blog.Errorf("array_longchar attribute %s.%s value is required but got nil, rid: %s", + objID, propertyType, rid) + return fmt.Errorf("array_longchar attribute %s.%s value is required but got nil", + objID, propertyType) + } + return nil + } + + // Validate that value is a slice of any + strArray, ok := util.ConvertAnyToSlice(value) + if !ok { + blog.Errorf("array_longchar attribute %s.%s value must be []interface{}, got %T, rid: %s", + objID, propertyType, value, rid) + return fmt.Errorf("array_longchar attribute %s.%s value must be []interface{}, got %T", + objID, propertyType, value) + } + + arrayOpt, err := ParseArrayOption[string](option, func(v any) (string, error) { + if v == nil { + return "", nil + } + s, ok := v.(string) + if !ok { + return s, fmt.Errorf("invalid type %T for option %s, rid: %s", v, option, rid) + } + return s, nil + }) + if err != nil { + return err + } + if arrayOpt.Cap < len(strArray) { + return fmt.Errorf("array_longchar invalid cap %d, rid: %s", arrayOpt.Cap, rid) + } + regex := arrayOpt.Option + // Compile regex pattern + pattern, err := regexp.Compile(regex) + if err != nil { + blog.Errorf("array_longchar invalid regex pattern %s, err: %v, rid: %s", regex, err, rid) + return fmt.Errorf("array_longchar invalid regex pattern: %v", err) + } + + // Validate each item in the array + for i, item := range strArray { + strVal, ok := item.(string) + if !ok { + blog.Errorf("array_longchar attribute %s.%s array item [%d] type %T is not string, rid: %s", objID, + propertyType, i, item, rid) + return fmt.Errorf("array_longchar attribute %s.%s array item [%d] type %T is not string", objID, + propertyType, i, item) + } + + // Validate length + if len(strVal) > common.FieldTypeLongLenChar { + blog.Errorf("array_longchar attribute %s.%s array item [%d] length %d exceeds max %d, rid: %s", + objID, propertyType, i, len(strVal), common.FieldTypeLongLenChar, rid) + return fmt.Errorf("array_longchar attribute %s.%s array item [%d] length exceeds max %d", + objID, propertyType, i, common.FieldTypeLongLenChar) + } + + // Validate regex + if !pattern.MatchString(strVal) { + blog.Errorf("array_longchar attribute %s.%s array item [%d] value '%s' does not match regex, rid: %s", + objID, propertyType, i, strVal, rid) + return fmt.Errorf("array_longchar attribute %s.%s array item [%d] does not match regex pattern", + objID, propertyType, i) + } + } + + return nil +} + +// FillLostValue fills the lost value with default value +func (a arrayLongchar) FillLostValue(ctx context.Context, valData mapstr.MapStr, propertyId string, + defaultValue, option interface{}) error { + + rid := util.ExtractRequestIDFromContext(ctx) + + valData[propertyId] = nil + if defaultValue == nil { + return nil + } + + // Validate default value + strArray, ok := util.ConvertAnyToSlice(defaultValue) + if !ok { + blog.Errorf("array_longchar default value must be []interface{}, got %T, rid: %s", defaultValue, rid) + return fmt.Errorf("array_longchar default value must be []interface{}, got %T", defaultValue) + } + + // Parse option for regex pattern + regex := common.FieldTypeLongCharRegexp + + arrayOpt, err := ParseArrayOption[string](option, func(v any) (string, error) { + if v == nil { + return "", nil + } + s, ok := v.(string) + if !ok { + return s, fmt.Errorf("invalid type %T for option %s, rid: %s", v, option, rid) + } + return s, nil + }) + if err != nil { + return err + } + if arrayOpt.Cap < len(strArray) { + return fmt.Errorf("array_longchar invalid cap %d, rid: %s", arrayOpt.Cap, rid) + } + + // Compile regex pattern + pattern, err := regexp.Compile(regex) + if err != nil { + blog.Errorf("array_longchar invalid regex pattern %s, err: %v, rid: %s", regex, err, rid) + return fmt.Errorf("array_longchar invalid regex pattern: %v", err) + } + + // Validate each item in default array + for i, item := range strArray { + strVal, ok := item.(string) + if !ok { + blog.Errorf("array_longchar default value array item [%d] type %T is not string, rid: %s", i, item, rid) + return fmt.Errorf("array_longchar default value array item [%d] type %T is not string", i, item) + } + + if len(strVal) > common.FieldTypeLongLenChar { + blog.Errorf("array_longchar default value array item [%d] length %d exceeds max %d, rid: %s", i, len(strVal), + common.FieldTypeLongLenChar, rid) + return fmt.Errorf("array_longchar default value array item [%d] length exceeds max %d", i, + common.FieldTypeLongLenChar) + } + + if !pattern.MatchString(strVal) { + blog.Errorf("array_longchar default value array item [%d] value '%s' does not match regex, rid: %s", + i, strVal, rid) + return fmt.Errorf("array_longchar default value array item [%d] does not match regex pattern", i) + } + } + + valData[propertyId] = strArray + return nil +} + +// ValidateOption validates the option field +func (a arrayLongchar) ValidateOption(ctx context.Context, option interface{}, defaultVal interface{}) error { + + rid := util.ExtractRequestIDFromContext(ctx) + + arrayOpt, err := ParseArrayOption[string](option, func(v any) (string, error) { + if v == nil { + return "", nil + } + s, ok := v.(string) + if !ok { + return s, fmt.Errorf("invalid type %T for option %s, rid: %s", v, option, rid) + } + return s, nil + }) + if err != nil { + return err + } + + if defaultVal == nil { + return nil + } + + // Validate default value + strArray, ok := util.ConvertAnyToSlice(defaultVal) + if !ok { + blog.Errorf("array_longchar default value must be []interface{}, got %T, rid: %s", defaultVal, rid) + return fmt.Errorf("array_longchar default value must be []interface{}, got %T", defaultVal) + } + + // Get regex pattern + regex := arrayOpt.Option + + pattern, err := regexp.Compile(regex) + if err != nil { + blog.Errorf("array_longchar invalid regex pattern %s, err: %v, rid: %s", regex, err, rid) + return fmt.Errorf("array_longchar invalid regex pattern: %v", err) + } + + // Validate each item in default array + for i, item := range strArray { + strVal, ok := item.(string) + if !ok { + blog.Errorf("array_longchar default value array item [%d] type %T is not string, rid: %s", i, item, rid) + return fmt.Errorf("array_longchar default value array item [%d] type %T is not string", i, item) + } + + if len(strVal) > common.FieldTypeLongLenChar { + blog.Errorf("array_longchar default value array item [%d] length exceeds max %d, rid: %s", + i, common.FieldTypeLongLenChar, rid) + return fmt.Errorf("array_longchar default value array item [%d] length exceeds max %d", + i, common.FieldTypeLongLenChar) + } + + if !pattern.MatchString(strVal) { + blog.Errorf("array_longchar default value array item [%d] does not match regex, rid: %s", i, rid) + return fmt.Errorf("array_longchar default value array item [%d] does not match regex pattern", i) + } + } + + return nil +} + +var _ register.AttributeTypeI = &arrayLongchar{} diff --git a/src/common/metadata/attribute_array_singlechar.go b/src/common/metadata/attribute_array_singlechar.go new file mode 100644 index 0000000000..6cf4f748a2 --- /dev/null +++ b/src/common/metadata/attribute_array_singlechar.go @@ -0,0 +1,273 @@ +/* + * Tencent is pleased to support the open source community by making 蓝鲸 available. + * Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved. + * Licensed under the MIT License (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * http://opensource.org/licenses/MIT + * 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 metadata + +import ( + "context" + "fmt" + "regexp" + + "configcenter/src/common" + "configcenter/src/common/blog" + "configcenter/src/common/mapstr" + "configcenter/src/common/util" + "configcenter/src/common/valid/attribute/manager/register" +) + +func init() { + // Register the arraySinglechar attribute type + register.Register(arraySinglechar{}) +} + +type arraySinglechar struct { +} + +// Name returns the name of the arraySinglechar attribute. +func (a arraySinglechar) Name() string { + return "array_singlechar" +} + +// DisplayName returns the display name for user. +func (a arraySinglechar) DisplayName() string { + return "短字符数组" +} + +// RealType returns the db type of the arraySinglechar attribute. +// Flattened array uses LongChar as storage type +func (a arraySinglechar) RealType() string { + return common.FieldTypeLongChar +} + +// Info returns the tips for user. +func (a arraySinglechar) Info() string { + return "短字符数组" +} + +// Validate validates the arraySinglechar attribute value +func (a arraySinglechar) Validate(ctx context.Context, objID string, propertyType string, required bool, + option, value interface{}) error { + + rid := util.ExtractRequestIDFromContext(ctx) + + if value == nil { + if required { + blog.Errorf("array_singlechar attribute %s.%s value is required but got nil, rid: %s", + objID, propertyType, rid) + return fmt.Errorf("array_singlechar attribute %s.%s value is required but got nil", + objID, propertyType) + } + return nil + } + + // Validate that value is a slice of any + strArray, ok := util.ConvertAnyToSlice(value) + if !ok { + blog.Errorf("array_singlechar attribute %s.%s value must be []interface{}, got %T, rid: %s", + objID, propertyType, value, rid) + return fmt.Errorf("array_singlechar attribute %s.%s value must be []interface{}, got %T", + objID, propertyType, value) + } + + // Parse option for regex pattern + regex := common.FieldTypeSingleCharRegexp + arrayOpt, err := ParseArrayOption[string](option, func(v any) (string, error) { + if v == nil { + return "", nil + } + s, ok := v.(string) + if !ok { + return s, fmt.Errorf("invalid type %T for option %s, rid: %s", v, option, rid) + } + return s, nil + }) + if err != nil { + return err + } + if arrayOpt.Cap < len(strArray) { + return fmt.Errorf("array_singlechar invalid cap %d, rid: %s", + arrayOpt.Cap, rid) + } + regex = arrayOpt.Option + // Compile regex pattern + pattern, err := regexp.Compile(regex) + if err != nil { + blog.Errorf("array_singlechar invalid regex pattern %s, err: %v, rid: %s", regex, err, rid) + return fmt.Errorf("array_singlechar invalid regex pattern: %v", err) + } + + // Validate each item in the array + for i, item := range strArray { + strVal, ok := item.(string) + if !ok { + blog.Errorf("array_singlechar attribute %s.%s array item [%d] type %T is not string, rid: %s", + objID, propertyType, i, item, rid) + return fmt.Errorf("array_singlechar attribute %s.%s array item [%d] type %T is not string", + objID, propertyType, i, item) + } + + // Validate length + if len(strVal) > common.FieldTypeSingleLenChar { + blog.Errorf("array_singlechar attribute %s.%s array item [%d] length %d exceeds max %d, rid: %s", + objID, propertyType, i, len(strVal), common.FieldTypeSingleLenChar, rid) + return fmt.Errorf("array_singlechar attribute %s.%s array item [%d] length exceeds max %d", + objID, propertyType, i, common.FieldTypeSingleLenChar) + } + + // Validate regex + if !pattern.MatchString(strVal) { + blog.Errorf("array_singlechar attribute %s.%s array item [%d] value '%s' does not match regex, rid: %s", + objID, propertyType, i, strVal, rid) + return fmt.Errorf("array_singlechar attribute %s.%s array item [%d] does not match regex pattern", + objID, propertyType, i) + } + } + + return nil +} + +// FillLostValue fills the lost value with default value +func (a arraySinglechar) FillLostValue(ctx context.Context, valData mapstr.MapStr, propertyId string, + defaultValue, option interface{}) error { + + rid := util.ExtractRequestIDFromContext(ctx) + + valData[propertyId] = nil + if defaultValue == nil { + return nil + } + + // Validate default value + strArray, ok := util.ConvertAnyToSlice(defaultValue) + if !ok { + blog.Errorf("array_singlechar default value must be []interface{}, got %T, rid: %s", defaultValue, rid) + return fmt.Errorf("array_singlechar default value must be []interface{}, got %T", defaultValue) + } + + // Parse option for regex pattern + regex := common.FieldTypeSingleCharRegexp + + arrayOpt, err := ParseArrayOption[string](option, func(v any) (string, error) { + if v == nil { + return "", nil + } + s, ok := v.(string) + if !ok { + return s, fmt.Errorf("invalid type %T for option %s, rid: %s", v, option, rid) + } + return s, nil + }) + if err != nil { + return err + } + if arrayOpt.Cap < len(strArray) { + return fmt.Errorf("array_singlechar invalid cap %d, rid: %s", + arrayOpt.Cap, rid) + } + + // Compile regex pattern + pattern, err := regexp.Compile(regex) + if err != nil { + blog.Errorf("array_singlechar invalid regex pattern %s, err: %v, rid: %s", regex, err, rid) + return fmt.Errorf("array_singlechar invalid regex pattern: %v", err) + } + + // Validate each item in default array + for i, item := range strArray { + strVal, ok := item.(string) + if !ok { + blog.Errorf("array_singlechar default value array item [%d] type %T is not string, rid: %s", i, item, rid) + return fmt.Errorf("array_singlechar default value array item [%d] type %T is not string", i, item) + } + + if len(strVal) > common.FieldTypeSingleLenChar { + blog.Errorf("array_singlechar default value array item [%d] length %d exceeds max %d, rid: %s", + i, len(strVal), common.FieldTypeSingleLenChar, rid) + return fmt.Errorf("array_singlechar default value array item [%d] length exceeds max %d", + i, common.FieldTypeSingleLenChar) + } + + if !pattern.MatchString(strVal) { + blog.Errorf("array_singlechar default value array item [%d] value '%s' does not match regex, rid: %s", + i, strVal, rid) + return fmt.Errorf("array_singlechar default value array item [%d] does not match regex pattern", i) + } + } + + valData[propertyId] = strArray + return nil +} + +// ValidateOption validates the option field +func (a arraySinglechar) ValidateOption(ctx context.Context, option, defaultVal interface{}) error { + + rid := util.ExtractRequestIDFromContext(ctx) + + arrayOpt, err := ParseArrayOption[string](option, func(v any) (string, error) { + if v == nil { + return "", nil + } + s, ok := v.(string) + if !ok { + return s, fmt.Errorf("invalid type %T for option %s, rid: %s", v, option, rid) + } + return s, nil + }) + if err != nil { + return err + } + + if defaultVal == nil { + return nil + } + + // Validate default value + strArray, ok := util.ConvertAnyToSlice(defaultVal) + if !ok { + blog.Errorf("array_singlechar default value must be []interface{}, got %T, rid: %s", defaultVal, rid) + return fmt.Errorf("array_singlechar default value must be []interface{}, got %T", defaultVal) + } + + // Get regex pattern + regex := arrayOpt.Option + + pattern, err := regexp.Compile(regex) + if err != nil { + blog.Errorf("array_singlechar invalid regex pattern %s, err: %v, rid: %s", regex, err, rid) + return fmt.Errorf("array_singlechar invalid regex pattern: %v", err) + } + + // Validate each item in default array + for i, item := range strArray { + strVal, ok := item.(string) + if !ok { + blog.Errorf("array_singlechar default value array item [%d] type %T is not string, rid: %s", i, item, rid) + return fmt.Errorf("array_singlechar default value array item [%d] type %T is not string", i, item) + } + + if len(strVal) > common.FieldTypeSingleLenChar { + blog.Errorf("array_singlechar default value array item [%d] length exceeds max %d, rid: %s", + i, common.FieldTypeSingleLenChar, rid) + return fmt.Errorf("array_singlechar default value array item [%d] length exceeds max %d", + i, common.FieldTypeSingleLenChar) + } + + if !pattern.MatchString(strVal) { + blog.Errorf("array_singlechar default value array item [%d] does not match regex, rid: %s", i, rid) + return fmt.Errorf("array_singlechar default value array item [%d] does not match regex pattern", i) + } + } + + return nil +} + +var _ register.AttributeTypeI = &arraySinglechar{} diff --git a/src/common/metadata/attribute_array_time.go b/src/common/metadata/attribute_array_time.go new file mode 100644 index 0000000000..05c91d4a41 --- /dev/null +++ b/src/common/metadata/attribute_array_time.go @@ -0,0 +1,159 @@ +/* + * Tencent is pleased to support the open source community by making 蓝鲸 available. + * Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved. + * Licensed under the MIT License (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * http://opensource.org/licenses/MIT + * 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 metadata + +import ( + "configcenter/src/common" + "configcenter/src/common/blog" + "configcenter/src/common/mapstr" + "configcenter/src/common/util" + "configcenter/src/common/valid/attribute/manager/register" + "context" + "fmt" +) + +func init() { + // Register the arrayTime attribute type + register.Register(arrayTime{}) +} + +type arrayTime struct { +} + +// Name returns the name of the arrayTime attribute. +func (a arrayTime) Name() string { + return "array_time" +} + +// DisplayName returns the display name for user. +func (a arrayTime) DisplayName() string { + return "时间数组" +} + +// RealType returns the db type of the arrayTime attribute. +// Flattened array uses LongChar as storage type +func (a arrayTime) RealType() string { + return common.FieldTypeLongChar +} + +// Info returns the tips for user. +func (a arrayTime) Info() string { + return "时间数组" +} + +// Validate validates the arrayTime attribute value +func (a arrayTime) Validate(ctx context.Context, objID string, propertyType string, required bool, + option interface{}, value interface{}) error { + + rid := util.ExtractRequestIDFromContext(ctx) + + if value == nil { + if required { + blog.Errorf("array_time attribute %s.%s value is required but got nil, rid: %s", + objID, propertyType, rid) + return fmt.Errorf("array_time attribute %s.%s value is required but got nil", + objID, propertyType) + } + return nil + } + + // Validate that value is a slice of any + timeArray, ok := util.ConvertAnyToSlice(value) + if !ok { + blog.Errorf("array_time attribute %s.%s value must be []interface{}, got %T, rid: %s", + objID, propertyType, value, rid) + return fmt.Errorf("array_time attribute %s.%s value must be []interface{}, got %T", + objID, propertyType, value) + } + + opts, err := ParseArrayOption[string](option, nil) + if err != nil { + blog.Errorf("array_time parse option failed: %v, rid: %s", err, rid) + return fmt.Errorf("array_time invalid option: %v", err) + } + if opts.Cap < len(timeArray) { + return fmt.Errorf("array_time invalid cap %d, rid: %s", opts.Cap, rid) + } + // Validate each item in the array + for i, item := range timeArray { + // Validate time format + if _, ok := util.IsTime(item); !ok { + blog.Errorf("array_time default value array item [%d] type %T is not a valid time, rid: %s", i, item, rid) + return fmt.Errorf("array_time default value array item [%v] is not a valid time", item) + } + } + + return nil +} + +// FillLostValue fills the lost value with default value +func (a arrayTime) FillLostValue(ctx context.Context, valData mapstr.MapStr, propertyId string, + defaultValue, option interface{}) error { + + rid := util.ExtractRequestIDFromContext(ctx) + + valData[propertyId] = nil + if defaultValue == nil { + return nil + } + + // Validate default value + defaultArray, ok := util.ConvertAnyToSlice(defaultValue) + if !ok { + blog.Errorf("array_time default value must be []interface{}, got %T, rid: %s", defaultValue, rid) + return fmt.Errorf("array_time default value must be []interface{}, got %T", defaultValue) + } + + // Validate each item in default array + for i, item := range defaultArray { + if _, ok := util.IsTime(item); !ok { + blog.Errorf("array_time default value array item [%d] type %T is not a valid time, rid: %s", i, item, rid) + return fmt.Errorf("array_time default value array item [%v] is not a valid time", item) + } + } + + valData[propertyId] = defaultArray + return nil +} + +// ValidateOption validates the option field +func (a arrayTime) ValidateOption(ctx context.Context, option interface{}, defaultVal interface{}) error { + rid := util.ExtractRequestIDFromContext(ctx) + + _, err := ParseArrayOption[string](option, nil) + if err != nil { + return err + } + if defaultVal == nil { + return nil + } + + // Validate default value + defaultArray, ok := util.ConvertAnyToSlice(defaultVal) + if !ok { + blog.Errorf("array_time default value must be []interface{}, got %T, rid: %s", defaultVal, rid) + return fmt.Errorf("array_time default value must be []interface{}, got %T", defaultVal) + } + + // Validate each item in default array + for i, item := range defaultArray { + if _, ok := util.IsTime(item); !ok { + blog.Errorf("array_time default value array item [%d] type %T is not a valid time, rid: %s", i, item, rid) + return fmt.Errorf("array_time default value array item [%v] is not a valid time", item) + } + } + + return nil +} + +var _ register.AttributeTypeI = &arrayTime{} diff --git a/src/common/metadata/attribute_document.go b/src/common/metadata/attribute_document.go new file mode 100644 index 0000000000..d9de0bb927 --- /dev/null +++ b/src/common/metadata/attribute_document.go @@ -0,0 +1,271 @@ +/* + * Tencent is pleased to support the open source community by making 蓝鲸 available. + * Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved. + * Licensed under the MIT License (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * http://opensource.org/licenses/MIT + * 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 metadata + +import ( + "context" + "encoding/json" + "fmt" + "regexp" + + "configcenter/src/common" + "configcenter/src/common/blog" + "configcenter/src/common/mapstr" + "configcenter/src/common/util" + "configcenter/src/common/valid/attribute/manager/register" +) + +func init() { + // Register the document attribute type + register.Register(document{}) +} + +// document represents a document attribute type. +type document struct { +} + +// Name returns the name of the document attribute. +func (d document) Name() string { + return "document" +} + +// DisplayName returns the display name for user. +func (d document) DisplayName() string { + return "文件" +} + +// RealType returns the db type of the document attribute. +func (d document) RealType() string { + return common.FieldTypeLongChar +} + +// Info returns the tips for user. +func (d document) Info() string { + return "文件,图片类型字段" +} + +// Validate validate option and value +func (d document) Validate(ctx context.Context, objID string, propertyType string, required bool, option interface{}, value interface{}) error { + + val, err := d.ParseValue(value) + if err != nil { + return fmt.Errorf("document attribute %s.%s value must be a string, got %T", objID, propertyType, value) + } + if len(val.Value) == 0 { + if required { + blog.Errorf("document attribute %s.%s value is required, but got empty, rid: %s", objID, propertyType, + util.ExtractRequestIDFromContext(ctx)) + return fmt.Errorf("document attribute %s.%s value is required, but got empty", objID, propertyType) + } + return nil + } + + if len(val.Name) > common.FieldTypeLongLenChar || len(val.Value) > common.FieldTypeLongLenChar { + return fmt.Errorf("document attribute %s.%s value length exceeds the maximum limit of %d characters", objID, + propertyType, common.FieldTypeLongLenChar) + } + + rid := util.ExtractRequestIDFromContext(ctx) + // option compatible with the scene where the option is not set in the model attribute. + dOption, err := d.ParseOption(option) + if err != nil { + blog.Errorf("parse document option failed, option: %v, error: %v, rid: %s", option, err, rid) + return fmt.Errorf("document option is not a valid DocumentOption type: %v, error: %v", option, err) + } + match, err := regexp.MatchString(dOption.Regex, val.Value) + if err != nil || !match { + blog.Errorf("default value %s not matches string option %s, err: %v, rid: %s", val, dOption.Regex, err, rid) + return fmt.Errorf("string default value not match regex") + } + + return nil +} + +// DocumentOption defines validation rules for a document or file. +// It specifies constraints such as allowed file suffixes, maximum file size, +// filename matching rules, and the logical file type. +type DocumentOption struct { + // AllowSuffixes specifies the allowed file suffixes (without dot), e.g. ["jpg", "png", "pdf"]. + // If empty, no suffix restriction is applied. + AllowSuffixes []string `json:"allow_suffixes,omitempty"` + + // AllowSize specifies the maximum allowed file size in bytes. + // If zero, no size limit is enforced. + AllowSize int64 `json:"allow_size,omitempty"` + + // Regex defines an optional regular expression used to validate + // the file name or other string fields depending on the context. + Regex string `json:"regex,omitempty"` + + // Type indicates the logical category of the file, such as image, + // document, video, or audio. The value should exist in DocumentOptionTypeRela. + Type string `json:"type,omitempty"` +} + +// DocumentOptionTypeRela defines the supported file type categories. +// It is implemented as a set for efficient membership checking. +var DocumentOptionTypeRela = map[string]struct{}{ + "image": {}, + "document": {}, + "video": {}, + "audio": {}, +} + +// DocumentValueDocument defines the document value format +type DocumentValueDocument struct { + Value string `json:"value"` + Name string `json:"name"` +} + +// ParseOption Parse document Option +func (d document) ParseOption(option interface{}) (DocumentOption, error) { + if option == nil { + return DocumentOption{}, nil + } + + switch val := option.(type) { + case *DocumentOption: + return *val, nil + case DocumentOption: + return val, nil + + } + optBytes, err := json.Marshal(option) + if err != nil { + return DocumentOption{}, fmt.Errorf("document option is not a valid DocumentOption type: %v", option) + } + res := DocumentOption{} + if err := json.Unmarshal(optBytes, &res); err != nil { + return DocumentOption{}, fmt.Errorf("document option is not a valid DocumentOption type: %v, error: %v", + option, err) + } + + return res, nil +} + +// ParseValue Parse document Value +func (d document) ParseValue(value interface{}) (DocumentValueDocument, error) { + if value == nil { + return DocumentValueDocument{}, nil + } + + switch val := value.(type) { + case *DocumentValueDocument: + return *val, nil + case DocumentValueDocument: + return val, nil + } + + valBytes, err := json.Marshal(value) + if err != nil { + return DocumentValueDocument{}, fmt.Errorf("document value is not a valid value type: %v", value) + } + res := DocumentValueDocument{} + if err := json.Unmarshal(valBytes, &res); err != nil { + return DocumentValueDocument{}, fmt.Errorf("document value is not a valid value type: %v, error: %v", value, + err) + } + + return res, nil +} + +// FillLostValue Fill document LostValue +func (d document) FillLostValue(ctx context.Context, valData mapstr.MapStr, name string, + defaultValue, option interface{}) error { + + rid := util.ExtractRequestIDFromContext(ctx) + + valData[name] = nil + if defaultValue == nil { + return nil + } + + defaultVal, err := d.ParseValue(defaultValue) + if err != nil { + return fmt.Errorf("single char default value not string, value: %v, rid: %s", defaultValue, rid) + } + + if len(defaultVal.Value) == 0 && len(defaultVal.Name) == 0 { + return nil + } + + // option compatible with the scene where the option is not set in the model attribute. + dOption, err := d.ParseOption(option) + if err != nil { + return err + } + + match, err := regexp.MatchString(dOption.Regex, defaultVal.Value) + if err != nil || !match { + return fmt.Errorf("the current string does not conform to regular verification rules") + } + valData[name] = defaultVal + return nil +} + +// ValidateOption Validate document Option +func (d document) ValidateOption(ctx context.Context, option interface{}, defaultVal interface{}) error { + + rid := util.ExtractRequestIDFromContext(ctx) + // option compatible with the scene where the option is not set in the model attribute. + dOption, err := d.ParseOption(option) + if err != nil { + return err + } + + // allow suffixes is required, if not set, return error + if len(dOption.AllowSuffixes) == 0 { + blog.Errorf("document option allow_suffixes is required, but not set, rid: %s", rid) + return fmt.Errorf("document option allow_suffixes is required, but not set") + } + if len(dOption.Type) == 0 { + blog.Errorf("document option type is required, but not set, rid: %s", rid) + return fmt.Errorf("document option type is required, but not set") + } + if _, ok := DocumentOptionTypeRela[dOption.Type]; !ok { + blog.Errorf("document option type %s is not supported, rid: %s", dOption.Type, rid) + return fmt.Errorf("document option type %s is not supported", dOption.Type) + } + + if len(dOption.Regex) == 0 { + return nil + } + + value := DocumentValueDocument{} + if defaultVal != nil { + value, err = d.ParseValue(defaultVal) + if err != nil { + blog.Errorf("string type default value %+v type %T is invalid, err: %s, rid: %s", defaultVal, defaultVal, err, rid) + return fmt.Errorf("field default value, not string type") + } + } + + if _, err := regexp.Compile(dOption.Regex); err != nil { + blog.Errorf("regular expression %s is invalid, err: %, rid: %s", dOption.Regex, err, rid) + return fmt.Errorf("regular is wrong") + } + + if defaultVal == nil { + return nil + } + + match, err := regexp.MatchString(dOption.Regex, value.Value) + if err != nil || !match { + blog.Errorf("default value %s not matches string option %s, err: %v, rid: %s", value.Value, dOption.Regex, err, rid) + return fmt.Errorf("string default value not match regex") + } + + return nil +} + +var _ register.AttributeTypeI = &document{} diff --git a/src/common/metadata/attribute_test/attribute_array_option_test.go b/src/common/metadata/attribute_test/attribute_array_option_test.go new file mode 100644 index 0000000000..7ee4d80d80 --- /dev/null +++ b/src/common/metadata/attribute_test/attribute_array_option_test.go @@ -0,0 +1,1106 @@ +/* + * Tencent is pleased to support the open source community by making 蓝鲸 available. + * Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved. + * Licensed under the MIT License (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * http://opensource.org/licenses/MIT + * 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 metadata + +import ( + "configcenter/src/common/metadata" + "context" + "encoding/json" + "strings" + "testing" + "time" +) + +func TestArrayIntOptionParse(t *testing.T) { + + tests := []struct { + name string + option map[string]interface{} + value any + expectErr bool + }{ + + { + name: "normal-int-array", + value: []any{1, 2, 3}, + option: map[string]interface{}{ + "len": 1, + "cap": 3, + "option": map[string]interface{}{ + "min": 1, + "max": 10, + }, + }, + }, + { + name: "empty-array-valid", + value: []any{}, + option: map[string]interface{}{ + "len": 0, + "cap": 3, + }, + }, + { + name: "len-not-enough", + value: []any{1}, + option: map[string]interface{}{ + "len": 2, + "cap": 3, + }, + expectErr: false, + }, + { + name: "exceed-cap", + value: []any{1, 2, 3, 4, 5, 6}, + option: map[string]interface{}{ + "len": 1, + "cap": 5, + }, + expectErr: true, + }, + { + name: "element-min-violation", + value: []any{0, 2, 3}, + option: map[string]interface{}{ + "len": 1, + "cap": 3, + "option": map[string]interface{}{ + "min": 1, + "max": 10, + }, + }, + expectErr: true, + }, + { + name: "element-max-violation", + value: []any{1, 2, 20}, + option: map[string]interface{}{ + "len": 1, + "cap": 3, + "option": map[string]interface{}{ + "min": 1, + "max": 10, + }, + }, + expectErr: true, + }, + { + name: "element-type-error", + value: []any{1, json.Number("2"), 3}, + option: map[string]interface{}{ + "len": 1, + "cap": 3, + }, + expectErr: false, // util.GetInt64ByInterface + }, + { + name: "not-array", + value: 123, + option: map[string]interface{}{ + "len": 1, + "cap": 3, + }, + expectErr: true, + }, + { + name: "option-missing", + value: []any{1, 2, 3}, + expectErr: true, + }, { + name: "int-option-missing", + value: []any{1, 2, 3}, + option: map[string]interface{}{ + "len": 1, + "cap": 3, + }, + }, + { + name: "len-equal-boundary", + value: []any{1}, + option: map[string]interface{}{ + "len": 1, + "cap": 3, + }, + }, + { + name: "cap-equal-boundary", + value: []any{1, 2, 3}, + option: map[string]interface{}{ + "len": 1, + "cap": 3, + }, + }, + } + + for _, tt := range tests { + + t.Run(tt.name, func(t *testing.T) { + + attr := metadata.Attribute{ + PropertyType: "array_int", + Option: tt.option, + } + err := attr.Validate(context.Background(), tt.value, "test") + + if tt.expectErr { + if err.ErrCode == 0 { + t.Fatalf("expect error:%v", err) + } + return + } + + if err.ErrCode != 0 { + t.Fatalf("unexpected error: %v", err) + } + }) + } +} + +func TestArrayFloatOptionParse(t *testing.T) { + + tests := []struct { + name string + option map[string]interface{} + value any + expectErr bool + }{ + + { + name: "normal-int-array", + value: []any{1.3, 2.1, 3.2}, + option: map[string]interface{}{ + "len": 1, + "cap": 3, + "option": map[string]interface{}{ + "min": 1.1, + "max": 10, + }, + }, + }, + { + name: "empty-array-valid", + value: []any{}, + option: map[string]interface{}{ + "len": 0, + "cap": 3, + }, + }, + { + name: "len-not-enough", + value: []any{1.3}, + option: map[string]interface{}{ + "len": 2, + "cap": 3, + }, + expectErr: false, + }, + { + name: "exceed-cap", + value: []any{1.3, 2, 3, 4, 5, 6}, + option: map[string]interface{}{ + "len": 1, + "cap": 5, + }, + expectErr: true, + }, + { + name: "element-min-violation", + value: []any{0, 2, 3}, + option: map[string]interface{}{ + "len": 1, + "cap": 3, + "option": map[string]interface{}{ + "min": 1.5, + "max": 10, + }, + }, + expectErr: true, + }, + { + name: "element-max-violation", + value: []any{1.3, 2, 20}, + option: map[string]interface{}{ + "len": 1, + "cap": 3, + "option": map[string]interface{}{ + "min": 1, + "max": 10.5, + }, + }, + expectErr: true, + }, + { + name: "element-type-error", + value: []any{1.3, "2", 3}, + option: map[string]interface{}{ + "len": 1, + "cap": 3, + }, + expectErr: false, // util.GetFloat64ByInterface + }, + { + name: "not-array", + value: 123, + option: map[string]interface{}{ + "len": 1, + "cap": 3, + }, + expectErr: true, + }, + { + name: "option-missing", + value: []any{1.3, 2, 3}, + expectErr: true, + }, { + name: "float-option-missing", + value: []any{1.3, 2, 3}, + option: map[string]interface{}{ + "len": 1, + "cap": 3, + }, + }, + { + name: "len-equal-boundary", + value: []any{1.3}, + option: map[string]interface{}{ + "len": 1, + "cap": 3, + }, + }, + { + name: "cap-equal-boundary", + value: []any{1.3, 2, 3}, + option: map[string]interface{}{ + "len": 1, + "cap": 3, + }, + }, + } + + for _, tt := range tests { + + t.Run(tt.name, func(t *testing.T) { + + attr := metadata.Attribute{ + PropertyType: "array_float", + Option: tt.option, + } + err := attr.Validate(context.Background(), tt.value, "test") + + if tt.expectErr { + if err.ErrCode == 0 { + t.Fatalf("expect error:%v", err) + } + return + } + + if err.ErrCode != 0 { + t.Fatalf("unexpected error: %v", err) + } + }) + } +} + +func TestArrayBoolOptionParse(t *testing.T) { + + tests := []struct { + name string + option map[string]interface{} + value any + expectErr bool + }{ + + { + name: "normal-bool-array", + value: []any{true, false, true}, + option: map[string]interface{}{ + "len": 2, + "cap": 3, + }, + }, + { + name: "empty-array-valid", + value: []any{}, + option: map[string]interface{}{ + "len": 2, + "cap": 3, + }, + }, + { + name: "len-not-enough", + value: []any{true}, + option: map[string]interface{}{ + "len": 2, + "cap": 3, + }, + expectErr: false, + }, + { + name: "exceed-cap", + value: []any{true, false, true}, + option: map[string]interface{}{ + "len": 1, + "cap": 2, + }, + expectErr: true, + }, + { + name: "element-type-error", + value: []any{1, 2, true}, + expectErr: true, + }, + { + name: "not-array", + value: true, + expectErr: true, + }, + { + name: "option-missing", + value: []any{true, false, true}, + expectErr: true, + }, { + name: "bool-option-missing", + value: []any{true, false, true}, + option: map[string]interface{}{ + "len": 1, + "cap": 3, + }, + }, + { + name: "len-equal-boundary", + value: []any{true}, + option: map[string]interface{}{ + "len": 1, + "cap": 3, + }, + }, + { + name: "cap-equal-boundary", + value: []any{true, false, true}, + option: map[string]interface{}{ + "len": 1, + "cap": 3, + }, + }, + } + + for _, tt := range tests { + + t.Run(tt.name, func(t *testing.T) { + + attr := metadata.Attribute{ + PropertyType: "array_bool", + Option: tt.option, + } + err := attr.Validate(context.Background(), tt.value, "test") + + if tt.expectErr { + if err.ErrCode == 0 { + t.Fatalf("expect error:%v", err) + } + return + } + + if err.ErrCode != 0 { + t.Fatalf("unexpected error: %v", err) + } + }) + } +} + +func TestArraySingleCharOptionParse(t *testing.T) { + + tests := []struct { + name string + option map[string]interface{} + value any + expectErr bool + }{ + + { + name: "normal-singlechar-array", + value: []any{"abc", "中文"}, + option: map[string]interface{}{ + "len": 2, + "cap": 10, + "option": ".+", + }, + }, { + name: "charLength-singlechar-array", + value: []any{strings.Repeat("中文", (256/len("中文"))+1)}, + option: map[string]interface{}{ + "len": 1, + "cap": 10, + "option": ".+", + }, + expectErr: true, + }, + { + name: "empty-array-valid", + value: []any{}, + option: map[string]interface{}{ + "len": 2, + "cap": 3, + }, + }, + { + name: "len-not-enough", + value: []any{"中文"}, + option: map[string]interface{}{ + "len": 2, + "cap": 3, + }, + expectErr: false, + }, + { + name: "exceed-cap", + value: []any{"abc", "中文", "!@#$%^&*()_+"}, + option: map[string]interface{}{ + "len": 1, + "cap": 2, + }, + expectErr: true, + }, + { + name: "element-type-error", + value: []any{1, 2, true}, + option: map[string]interface{}{ + "len": 1, + "cap": 2, + }, + expectErr: true, + }, + { + name: "not-array", + value: true, + option: map[string]interface{}{ + "len": 1, + "cap": 2, + }, + expectErr: true, + }, + { + name: "option-missing", + value: []any{"abc", "中文"}, + expectErr: true, + }, { + name: "singlechar-option-missing", + value: []any{"abc", "中文"}, + option: map[string]interface{}{ + "len": 1, + "cap": 3, + }, + }, + { + name: "len-equal-boundary", + value: []any{"abc", "中文"}, + option: map[string]interface{}{ + "len": 2, + "cap": 3, + }, + }, + { + name: "cap-equal-boundary", + value: []any{"abc", "中文", "!@#$%^&*()_+"}, + option: map[string]interface{}{ + "len": 1, + "cap": 3, + }, + }, + } + + for _, tt := range tests { + + t.Run(tt.name, func(t *testing.T) { + + attr := metadata.Attribute{ + PropertyType: "array_singlechar", + Option: tt.option, + } + err := attr.Validate(context.Background(), tt.value, "test") + + if tt.expectErr { + if err.ErrCode == 0 { + t.Fatalf("expect error:%v", err) + } + return + } + + if err.ErrCode != 0 { + t.Fatalf("unexpected error: %v", err) + } + }) + } +} + +func TestArrayLongCharOptionParse(t *testing.T) { + + tests := []struct { + name string + option map[string]interface{} + value any + expectErr bool + }{ + + { + name: "normal-longchar-array", + value: []any{"abc", "中文"}, + option: map[string]interface{}{ + "len": 2, + "cap": 10, + "option": ".+", + }, + }, + { + name: "charLength-longchar-array", + value: []any{strings.Repeat("中文", (2000/len("中文"))+1)}, + option: map[string]interface{}{ + "len": 1, + "cap": 10, + "option": ".+", + }, + expectErr: true, + }, + { + name: "empty-array-valid", + value: []any{}, + option: map[string]interface{}{ + "len": 2, + "cap": 3, + }, + }, + { + name: "len-not-enough", + value: []any{"中文"}, + option: map[string]interface{}{ + "len": 2, + "cap": 3, + }, + expectErr: false, + }, + { + name: "exceed-cap", + value: []any{"abc", "中文", "!@#$%^&*()_+"}, + option: map[string]interface{}{ + "len": 1, + "cap": 2, + }, + expectErr: true, + }, + { + name: "element-type-error", + value: []any{1, 2, true}, + option: map[string]interface{}{ + "len": 1, + "cap": 2, + }, + expectErr: true, + }, + { + name: "not-array", + value: true, + option: map[string]interface{}{ + "len": 1, + "cap": 2, + }, + expectErr: true, + }, + { + name: "option-missing", + value: []any{"abc", "中文"}, + expectErr: true, + }, { + name: "longchar-option-missing", + value: []any{"abc", "中文"}, + option: map[string]interface{}{ + "len": 1, + "cap": 3, + }, + }, + { + name: "len-equal-boundary", + value: []any{"abc", "中文"}, + option: map[string]interface{}{ + "len": 2, + "cap": 3, + }, + }, + { + name: "cap-equal-boundary", + value: []any{"abc", "中文", "!@#$%^&*()_+"}, + option: map[string]interface{}{ + "len": 1, + "cap": 3, + }, + }, + } + + for _, tt := range tests { + + t.Run(tt.name, func(t *testing.T) { + + attr := metadata.Attribute{ + PropertyType: "array_longchar", + Option: tt.option, + } + err := attr.Validate(context.Background(), tt.value, "test") + + if tt.expectErr { + if err.ErrCode == 0 { + t.Fatalf("expect error:%v", err) + } + return + } + + if err.ErrCode != 0 { + t.Fatalf("unexpected error: %v", err) + } + }) + } +} + +func TestArrayDateOptionParse(t *testing.T) { + now := time.Now() + tests := []struct { + name string + option map[string]interface{} + value any + expectErr bool + }{ + + { + name: "normal-date-array", + value: []any{now.Format(time.DateOnly), now.Add(20 * time.Hour).Format(time.DateOnly)}, + option: map[string]interface{}{ + "len": 2, + "cap": 10, + "option": ".+", + }, + }, + { + name: "empty-array-valid", + value: []any{}, + option: map[string]interface{}{ + "len": 2, + "cap": 3, + }, + }, + { + name: "len-not-enough", + value: []any{now.Format(time.DateOnly), now.Add(20 * time.Hour).Format(time.DateOnly)}, + option: map[string]interface{}{ + "len": 3, + "cap": 3, + }, + expectErr: false, + }, + { + name: "exceed-cap", + value: []any{now.Format(time.DateOnly), now.Add(20 * time.Hour).Format(time.DateOnly)}, + option: map[string]interface{}{ + "len": 1, + "cap": 1, + }, + expectErr: true, + }, + { + name: "element-format-error", + value: []any{now.Format(time.DateOnly), now.Format(time.RFC3339)}, + option: map[string]interface{}{ + "len": 1, + "cap": 2, + }, + expectErr: true, + }, + { + name: "element-type-error", + value: []any{now.Format(time.DateOnly), now.Unix()}, + option: map[string]interface{}{ + "len": 1, + "cap": 2, + }, + expectErr: true, + }, + { + name: "not-array", + value: now.Format(time.DateOnly), + option: map[string]interface{}{ + "len": 1, + "cap": 2, + }, + expectErr: true, + }, + { + name: "option-missing", + value: []any{now.Format(time.DateOnly), now.Add(20 * time.Hour).Format(time.DateOnly)}, + expectErr: true, + }, + { + name: "date-option-missing", + value: []any{now.Format(time.DateOnly), now.Add(20 * time.Hour).Format(time.DateOnly)}, + option: map[string]interface{}{ + "len": 1, + "cap": 3, + }, + expectErr: false, + }, + { + name: "len-equal-boundary", + value: []any{now.Format(time.DateOnly), now.Add(20 * time.Hour).Format(time.DateOnly)}, + option: map[string]interface{}{ + "len": 2, + "cap": 3, + }, + }, + { + name: "cap-equal-boundary", + value: []any{now.Format(time.DateOnly), now.Add(20 * time.Hour).Format(time.DateOnly)}, + option: map[string]interface{}{ + "len": 1, + "cap": 3, + }, + expectErr: false, + }, + } + + for _, tt := range tests { + + t.Run(tt.name, func(t *testing.T) { + + attr := metadata.Attribute{ + PropertyType: "array_date", + Option: tt.option, + } + err := attr.Validate(context.Background(), tt.value, "test") + + if tt.expectErr { + if err.ErrCode == 0 { + t.Fatalf("expect error:%v", err) + } + return + } + + if err.ErrCode != 0 { + t.Fatalf("unexpected error: %v", err) + } + }) + } +} + +func TestArrayTimeOptionParse(t *testing.T) { + now := time.Now() + format := "2006-01-02T15:04:05+08:00" + tests := []struct { + name string + option map[string]interface{} + value any + expectErr bool + }{ + + { + name: "normal-time-array", + value: []any{now.Format(time.DateTime), + now.Format(format), + now.Format("2006-01-02T15:04:05.1+08:00"), + now.Format("2006-01-02T15:04:05.12+08:00"), + now.Format("2006-01-02T15:04:05.123+08:00"), + //now.Format("2006-01-02T15:04:05.123+08:00:00"),//err + //now.Format("2006-01-02T15:04:05.123-07:00"), //err + //now.Format("2006-01-02T15:04:05Z"), //err + }, + option: map[string]interface{}{ + "len": 2, + "cap": 10, + "option": ".+", + }, + }, + { + name: "empty-array-valid", + value: []any{}, + option: map[string]interface{}{ + "len": 2, + "cap": 3, + }, + }, + { + name: "len-not-enough", + value: []any{now.Format(time.DateTime), now.Format(format)}, + option: map[string]interface{}{ + "len": 3, + "cap": 3, + }, + expectErr: false, + }, + { + name: "exceed-cap", + value: []any{now.Format(time.DateTime), now.Format(format)}, + option: map[string]interface{}{ + "len": 1, + "cap": 1, + }, + expectErr: true, + }, + { + name: "element-format-error", + value: []any{now.Format(time.DateTime), now.Format(time.RFC3339)}, + option: map[string]interface{}{ + "len": 1, + "cap": 2, + }, + expectErr: true, + }, + { + name: "element-type-error", + value: []any{now.Format(time.DateTime), now.Unix()}, + option: map[string]interface{}{ + "len": 1, + "cap": 2, + }, + expectErr: true, + }, + { + name: "not-array", + value: now.Format(time.DateTime), + option: map[string]interface{}{ + "len": 1, + "cap": 2, + }, + expectErr: true, + }, + { + name: "option-missing", + value: []any{now.Format(time.DateTime), now.Format(format)}, + expectErr: true, + }, + { + name: "time-option-missing", + value: []any{now.Format(time.DateTime), now.Format(format)}, + option: map[string]interface{}{ + "len": 1, + "cap": 3, + }, + expectErr: false, + }, + { + name: "len-equal-boundary", + value: []any{now.Format(time.DateTime), now.Format(format)}, + option: map[string]interface{}{ + "len": 2, + "cap": 3, + }, + }, + { + name: "cap-equal-boundary", + value: []any{now.Format(time.DateTime), now.Format(format)}, + option: map[string]interface{}{ + "len": 1, + "cap": 3, + }, + expectErr: false, + }, + } + + for _, tt := range tests { + + t.Run(tt.name, func(t *testing.T) { + + attr := metadata.Attribute{ + PropertyType: "array_time", + Option: tt.option, + } + err := attr.Validate(context.Background(), tt.value, "test") + + if tt.expectErr { + if err.ErrCode == 0 { + t.Fatalf("expect error:%v", err) + } + return + } + + if err.ErrCode != 0 { + t.Fatalf("unexpected error: %v", err) + } + }) + } +} + +func TestArrayDocumentOptionParse(t *testing.T) { + tests := []struct { + name string + option map[string]interface{} + value any + expectErr bool + }{ + { + name: "normal-document-array", + value: []any{metadata.DocumentValueDocument{}}, + option: map[string]interface{}{ + "len": 2, + "cap": 3, + "option": metadata.DocumentOption{ + AllowSuffixes: nil, + AllowSize: 1024, + Regex: "", + Type: "image", + }, + }, + }, + { + name: "empty-array-valid", + value: []any{}, + + option: map[string]interface{}{ + "len": 2, + "cap": 3, + "option": metadata.DocumentOption{ + AllowSuffixes: nil, + AllowSize: 1024, + Regex: "", + Type: "image", + }, + }, + }, + { + name: "len-not-enough", + value: []any{metadata.DocumentValueDocument{}}, + + option: map[string]interface{}{ + "len": 2, + "cap": 3, + "option": metadata.DocumentOption{ + AllowSuffixes: nil, + AllowSize: 1024, + Regex: "", + Type: "image", + }, + }, + expectErr: false, + }, + { + name: "exceed-cap", + value: []any{metadata.DocumentValueDocument{}, + metadata.DocumentValueDocument{}, + metadata.DocumentValueDocument{}, + metadata.DocumentValueDocument{}}, + option: map[string]interface{}{ + "len": 2, + "cap": 3, + "option": metadata.DocumentOption{ + AllowSuffixes: nil, + AllowSize: 1024, + Regex: "", + Type: "image", + }, + }, + expectErr: true, + }, + { + name: "element-type-error", + value: []any{metadata.DocumentValueDocument{}, 1}, + option: map[string]interface{}{ + "len": 2, + "cap": 3, + "option": metadata.DocumentOption{ + AllowSuffixes: nil, + AllowSize: 1024, + Regex: "", + Type: "image", + }, + }, + expectErr: true, + }, + { + name: "not-array", + value: metadata.DocumentValueDocument{}, + option: map[string]interface{}{ + "len": 2, + "cap": 3, + "option": metadata.DocumentOption{ + AllowSuffixes: nil, + AllowSize: 1024, + Regex: "", + Type: "image", + }, + }, + expectErr: true, + }, + { + name: "option-missing", + value: []any{metadata.DocumentValueDocument{}}, + expectErr: true, + }, + { + name: "document-option-missing", + value: []any{metadata.DocumentValueDocument{}}, + + option: map[string]interface{}{ + "len": 2, + "cap": 3, + }, + expectErr: false, + }, + { + name: "len-equal-boundary", + value: []any{metadata.DocumentValueDocument{}}, + + option: map[string]interface{}{ + "len": 2, + "cap": 3, + "option": metadata.DocumentOption{ + AllowSuffixes: nil, + AllowSize: 1024, + Regex: "", + Type: "image", + }, + }, + }, + { + name: "cap-equal-boundary", + value: []any{metadata.DocumentValueDocument{}}, + + option: map[string]interface{}{ + "len": 2, + "cap": 3, + "option": metadata.DocumentOption{ + AllowSuffixes: nil, + AllowSize: 1024, + Regex: "", + Type: "image", + }, + }, + expectErr: false, + }, + } + + for _, tt := range tests { + + t.Run(tt.name, func(t *testing.T) { + + attr := metadata.Attribute{ + PropertyType: "array_document", + Option: tt.option, + } + err := attr.Validate(context.Background(), tt.value, "test") + + if tt.expectErr { + if err.ErrCode == 0 { + t.Fatalf("expect error:%v", err) + } + return + } + + if err.ErrCode != 0 { + t.Fatalf("unexpected error: %v", err) + } + }) + } +} diff --git a/src/common/metadata/attribute_test/attribute_option_test.go b/src/common/metadata/attribute_test/attribute_option_test.go new file mode 100644 index 0000000000..e530443a15 --- /dev/null +++ b/src/common/metadata/attribute_test/attribute_option_test.go @@ -0,0 +1,167 @@ +/* + * Tencent is pleased to support the open source community by making 蓝鲸 available. + * Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved. + * Licensed under the MIT License (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * http://opensource.org/licenses/MIT + * 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 metadata + +import ( + "configcenter/src/common" + "configcenter/src/common/metadata" + "context" + "testing" +) + +func TestIntOptionParse(t *testing.T) { + + tests := []struct { + name string + option map[string]interface{} + expectErr bool + }{ + { + name: "normal", + option: map[string]interface{}{ + "min": 1, + "max": 10, + }, + }, + { + name: "min only", + option: map[string]interface{}{ + "min": 1, + }, expectErr: true, + }, + { + name: "max only", + option: map[string]interface{}{ + "max": 10, + }, expectErr: true, + }, + { + name: "type error", + option: map[string]interface{}{ + "min": "1", + }, + expectErr: true, + }, + } + + for _, tt := range tests { + + t.Run(tt.name, func(t *testing.T) { + + attr := metadata.Attribute{ + PropertyType: common.FieldTypeInt, + Option: tt.option, + } + + err := attr.Validate(context.Background(), 5, "test") + + if (err.ErrCode != 0) != tt.expectErr { + t.Fatalf("expect err=%v got=%v", tt.expectErr, err) + } + + }) + } +} + +func TestFloatOption(t *testing.T) { + + tests := []struct { + name string + option map[string]interface{} + expectErr bool + }{ + { + name: "normal", + option: map[string]interface{}{ + "min": 1.4, + "max": 10.4, + }, + }, + { + name: "min only", + option: map[string]interface{}{ + "min": 1.3, + }, expectErr: true, + }, + { + name: "max only", + option: map[string]interface{}{ + "max": 10.5, + }, expectErr: true, + }, + { + name: "type error", + option: map[string]interface{}{ + "min": "1.4", + }, + expectErr: true, + }, + } + + for _, tt := range tests { + + t.Run(tt.name, func(t *testing.T) { + + attr := metadata.Attribute{ + PropertyType: common.FieldTypeFloat, + Option: tt.option, + } + + err := attr.Validate(context.Background(), 5, "test") + + if (err.ErrCode != 0) != tt.expectErr { + t.Fatalf("expect err=%v got=%v", tt.expectErr, err) + } + + }) + } +} + +func TestDocumentOption(t *testing.T) { + + tests := []struct { + name string + option map[string]interface{} + expectErr bool + }{ + { + name: "normal", + option: map[string]interface{}{ + "allow_suffixes": nil, + "allow_size": 1024, + "regex": ".+", + "type": "image", + }, + }, + } + + for _, tt := range tests { + + t.Run(tt.name, func(t *testing.T) { + + attr := metadata.Attribute{ + PropertyType: "document", + Option: tt.option, + } + + err := attr.Validate(context.Background(), metadata.DocumentValueDocument{ + Value: "ok", + Name: "ok", + }, "test") + + if (err.ErrCode != 0) != tt.expectErr { + t.Fatalf("expect err=%v got=%v", tt.expectErr, err) + } + + }) + } +} diff --git a/src/common/metadata/attribute_test/attribute_test.go b/src/common/metadata/attribute_test/attribute_test.go new file mode 100644 index 0000000000..15f064db2f --- /dev/null +++ b/src/common/metadata/attribute_test/attribute_test.go @@ -0,0 +1,202 @@ +/* + * Tencent is pleased to support the open source community by making 蓝鲸 available. + * Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved. + * Licensed under the MIT License (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * http://opensource.org/licenses/MIT + * 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 metadata + +import ( + "configcenter/src/common/metadata" + "configcenter/src/common/util" + "context" + "fmt" + "testing" + + "configcenter/src/common" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +func TestAttributeValidate_Int(t *testing.T) { + + tests := []struct { + name string + attr metadata.Attribute + value interface{} + wantErr bool + }{ + { + name: "int success", + attr: metadata.Attribute{ + PropertyType: common.FieldTypeInt, + Option: map[string]interface{}{ + "min": 1, + "max": 10, + }, + IsRequired: true, + }, + value: 5, + wantErr: false, + }, + { + name: "int out of range", + attr: metadata.Attribute{ + PropertyType: common.FieldTypeInt, + Option: map[string]interface{}{ + "min": 1, + "max": 10, + }, + }, + value: 20, + wantErr: true, + }, + { + name: "int type error", + attr: metadata.Attribute{ + PropertyType: common.FieldTypeInt, + }, + value: "abc", + wantErr: true, + }, + } + + for _, tt := range tests { + + t.Run(tt.name, func(t *testing.T) { + + err := tt.attr.Validate(context.Background(), tt.value, "test") + + if (err.ErrCode != 0) != tt.wantErr { + t.Fatalf("expect err %v got %v", tt.wantErr, err) + } + + }) + } +} + +func TestAttributeValidate_Float(t *testing.T) { + + attr := metadata.Attribute{ + PropertyType: common.FieldTypeFloat, + Option: map[string]interface{}{ + "min": 1.0, + "max": 10.0, + }, + } + + if err := attr.Validate(context.Background(), 5.5, "test"); err.ErrCode != 0 { + t.Fatalf("unexpected error: %v", err) + } + + if err := attr.Validate(context.Background(), 20.1, "test"); err.ErrCode == 0 { + t.Fatalf("expect error") + } +} + +func TestAttributeValidate_Bool(t *testing.T) { + + attr := metadata.Attribute{ + PropertyType: common.FieldTypeBool, + } + + if err := attr.Validate(context.Background(), true, "test"); err.ErrCode != 0 { + t.Fatalf("unexpected error") + } + + if err := attr.Validate(context.Background(), "true", "test"); err.ErrCode == 0 { + t.Fatalf("expect error") + } +} + +func TestAttributeValidate_SingleChar(t *testing.T) { + + attr := metadata.Attribute{ + PropertyType: common.FieldTypeSingleChar, + IsRequired: true, + } + + if err := attr.Validate(context.Background(), "hello", "test"); err.ErrCode != 0 { + t.Fatalf("unexpected error") + } + + if err := attr.Validate(context.Background(), 123, "test"); err.ErrCode == 0 { + t.Fatalf("expect error") + } +} + +func TestAttributeValidate_LongChar(t *testing.T) { + + attr := metadata.Attribute{ + PropertyType: common.FieldTypeLongChar, + } + + if err := attr.Validate(context.Background(), "long text", "test"); err.ErrCode != 0 { + t.Fatalf("unexpected error") + } +} + +func TestAttributeValidate_Date(t *testing.T) { + + attr := metadata.Attribute{ + PropertyType: common.FieldTypeDate, + } + + if err := attr.Validate(context.Background(), "2024-01-01", "test"); err.ErrCode != 0 { + t.Fatalf("unexpected error") + } + + if err := attr.Validate(context.Background(), "invalid-date", "test"); err.ErrCode == 0 { + t.Fatalf("expect error") + } +} + +func TestAttributeValidate_Time(t *testing.T) { + + attr := metadata.Attribute{ + PropertyType: common.FieldTypeTime, + } + + if err := attr.Validate(context.Background(), "2024-01-01 10:00:00", "test"); err.ErrCode != 0 { + t.Fatalf("unexpected error") + } +} + +func TestAttributeValidate_Enum(t *testing.T) { + + attr := metadata.Attribute{ + PropertyType: common.FieldTypeEnum, + Option: []interface{}{ + map[string]interface{}{ + "id": "1", + "name": "test1", + "type": "text", + }, + map[string]interface{}{ + "id": "2", + "name": "test2", + "type": "text", + }, + }, + } + + if err := attr.Validate(context.Background(), "1", "test"); err.ErrCode != 0 { + t.Fatalf("unexpected error") + } + + if err := attr.Validate(context.Background(), "3", "test"); err.ErrCode == 0 { + t.Fatalf("expect error") + } +} + +func TestName(t *testing.T) { + var a any = primitive.A{1} + fmt.Println(util.ConvertAnyToSlice(a)) + fmt.Println(util.ConvertToInterfaceSlice(a)) + fmt.Println(util.ConvertToInterfaceSlice(1)) //conv to single item +} diff --git a/src/common/util/conv.go b/src/common/util/conv.go index 7c6e48ca01..c8d753fb21 100644 --- a/src/common/util/conv.go +++ b/src/common/util/conv.go @@ -252,6 +252,28 @@ func GetStrValsFromArrMapInterfaceByKey(arrI []interface{}, key string) []string return ret } +// ConvertAnyToSlice convert value to interface slice +func ConvertAnyToSlice(v any) ([]any, bool) { + if v == nil { + return nil, true + } + if s, ok := v.([]any); ok { + return s, true + } + val := reflect.ValueOf(v) + kind := val.Kind() + if kind != reflect.Slice && kind != reflect.Array { + return nil, false + } + n := val.Len() + out := make([]any, n) + for i := 0; i < n; i++ { + out[i] = val.Index(i).Interface() + } + return out, true + +} + // ConvertToInterfaceSlice convert value to interface slice func ConvertToInterfaceSlice(value interface{}) []interface{} { rflVal := reflect.ValueOf(value) diff --git a/src/source_controller/coreservice/core/model/attribute_curd.go b/src/source_controller/coreservice/core/model/attribute_curd.go index bf257304b9..c19d6e2c32 100644 --- a/src/source_controller/coreservice/core/model/attribute_curd.go +++ b/src/source_controller/coreservice/core/model/attribute_curd.go @@ -563,6 +563,8 @@ func (m *modelAttribute) validPropertyType(kit *rest.Kit, attribute metadata.Att if attribute.PropertyType != "" { if _, exists := validAttrPropertyTypes[attribute.PropertyType]; !exists { if _, ok := manager.Get(attribute.PropertyType); !ok { + blog.Errorf("cannot find attribute,propertyName:%v,propertyType:%v", attribute.PropertyName, + attribute.PropertyType) return kit.CCError.Errorf(common.CCErrCommParamsIsInvalid, metadata.AttributeFieldPropertyType) } } @@ -678,6 +680,9 @@ func (m *modelAttribute) checkAttributeDefaultValue(kit *rest.Kit, attribute met propertyType == common.FieldTypeEnumQuote { return fmt.Errorf("enum, enummulti, enumquote type default field is nil") } + if _, ok := manager.Get(propertyType); ok { + return nil + } return kit.CCError.Errorf(common.CCErrCommParamsIsInvalid, metadata.AttributeFieldPropertyType) }