diff --git a/staging/src/k8s.io/apimachinery/pkg/api/validate/constraints/constraints.go b/staging/src/k8s.io/apimachinery/pkg/api/validate/constraints/constraints.go new file mode 100644 index 00000000000..1689d3c0796 --- /dev/null +++ b/staging/src/k8s.io/apimachinery/pkg/api/validate/constraints/constraints.go @@ -0,0 +1,32 @@ +/* +Copyright 2025 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package constraints + +// Signed is a constraint that permits any signed integer type. +type Signed interface { + ~int | ~int8 | ~int16 | ~int32 | ~int64 +} + +// Unsigned is a constraint that permits any unsigned integer type. +type Unsigned interface { + ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr +} + +// Integer is a constraint that permits any integer type. +type Integer interface { + Signed | Unsigned +} diff --git a/staging/src/k8s.io/apimachinery/pkg/api/validate/content/errors.go b/staging/src/k8s.io/apimachinery/pkg/api/validate/content/errors.go new file mode 100644 index 00000000000..2cd871d2ca3 --- /dev/null +++ b/staging/src/k8s.io/apimachinery/pkg/api/validate/content/errors.go @@ -0,0 +1,29 @@ +/* +Copyright 2014 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package content + +import ( + "fmt" + + "k8s.io/apimachinery/pkg/api/validate/constraints" +) + +// MinError returns a string explanation of a "must be greater than or equal" +// validation failure. +func MinError[T constraints.Integer](min T) string { + return fmt.Sprintf("must be greater than or equal to %d", min) +} diff --git a/staging/src/k8s.io/apimachinery/pkg/api/validate/limits.go b/staging/src/k8s.io/apimachinery/pkg/api/validate/limits.go new file mode 100644 index 00000000000..5f5fe83a4df --- /dev/null +++ b/staging/src/k8s.io/apimachinery/pkg/api/validate/limits.go @@ -0,0 +1,37 @@ +/* +Copyright 2024 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package validate + +import ( + "context" + + "k8s.io/apimachinery/pkg/api/operation" + "k8s.io/apimachinery/pkg/api/validate/constraints" + "k8s.io/apimachinery/pkg/api/validate/content" + "k8s.io/apimachinery/pkg/util/validation/field" +) + +// Minimum verifies that the specified value is greater than or equal to min. +func Minimum[T constraints.Integer](_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ *T, min T) field.ErrorList { + if value == nil { + return nil + } + if *value < min { + return field.ErrorList{field.Invalid(fldPath, *value, content.MinError(min)).WithOrigin("minimum")} + } + return nil +} diff --git a/staging/src/k8s.io/apimachinery/pkg/api/validate/limits_test.go b/staging/src/k8s.io/apimachinery/pkg/api/validate/limits_test.go new file mode 100644 index 00000000000..60b785b758d --- /dev/null +++ b/staging/src/k8s.io/apimachinery/pkg/api/validate/limits_test.go @@ -0,0 +1,111 @@ +/* +Copyright 2024 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package validate + +import ( + "context" + "regexp" + "testing" + + "k8s.io/apimachinery/pkg/api/operation" + "k8s.io/apimachinery/pkg/api/validate/constraints" + "k8s.io/apimachinery/pkg/util/validation/field" +) + +func TestMinimum(t *testing.T) { + testMinimumPositive[int](t) + testMinimumNegative[int](t) + testMinimumPositive[int8](t) + testMinimumNegative[int8](t) + testMinimumPositive[int16](t) + testMinimumNegative[int16](t) + testMinimumPositive[int32](t) + testMinimumNegative[int32](t) + testMinimumPositive[int64](t) + testMinimumNegative[int64](t) + + testMinimumPositive[uint](t) + testMinimumPositive[uint8](t) + testMinimumPositive[uint16](t) + testMinimumPositive[uint32](t) + testMinimumPositive[uint64](t) +} + +type minimumTestCase[T constraints.Integer] struct { + value T + min T + err string // regex +} + +func testMinimumPositive[T constraints.Integer](t *testing.T) { + t.Helper() + cases := []minimumTestCase[T]{{ + value: 0, + min: 0, + }, { + value: 0, + min: 1, + err: "fldpath: Invalid value.*must be greater than or equal to", + }, { + value: 1, + min: 1, + }, { + value: 1, + min: 2, + err: "fldpath: Invalid value.*must be greater than or equal to", + }} + doTestMinimum[T](t, cases) +} + +func testMinimumNegative[T constraints.Signed](t *testing.T) { + t.Helper() + cases := []minimumTestCase[T]{{ + value: -1, + min: -1, + }, { + value: -2, + min: -1, + err: "fldpath: Invalid value.*must be greater than or equal to", + }} + + doTestMinimum[T](t, cases) +} + +func doTestMinimum[T constraints.Integer](t *testing.T, cases []minimumTestCase[T]) { + t.Helper() + for i, tc := range cases { + v := tc.value + result := Minimum(context.Background(), operation.Operation{}, field.NewPath("fldpath"), &v, nil, tc.min) + if len(result) > 0 && tc.err == "" { + t.Errorf("case %d: unexpected failure: %v", i, fmtErrs(result)) + continue + } + if len(result) == 0 && tc.err != "" { + t.Errorf("case %d: unexpected success: expected %q", i, tc.err) + continue + } + if len(result) > 0 { + if len(result) > 1 { + t.Errorf("case %d: unexepected multi-error: %v", i, fmtErrs(result)) + continue + } + if re := regexp.MustCompile(tc.err); !re.MatchString(result[0].Error()) { + t.Errorf("case %d: wrong error\nexpected: %q\n got: %v", i, tc.err, fmtErrs(result)) + } + } + } +} diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/minimum/doc.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/minimum/doc.go new file mode 100644 index 00000000000..b9bd2b9276d --- /dev/null +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/minimum/doc.go @@ -0,0 +1,63 @@ +/* +Copyright 2024 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// +k8s:validation-gen=TypeMeta +// +k8s:validation-gen-scheme-registry=k8s.io/code-generator/cmd/validation-gen/testscheme.Scheme + +// This is a test package. +package minimum + +import "k8s.io/code-generator/cmd/validation-gen/testscheme" + +var localSchemeBuilder = testscheme.New() + +type Struct struct { + TypeMeta int + + // +k8s:minimum=1 + IntField int `json:"intField"` + // +k8s:minimum=1 + IntPtrField *int `json:"intPtrField"` + + // "int8" becomes "byte" somewhere in gengo. We don't need it so just skip it. + + // +k8s:minimum=1 + Int16Field int16 `json:"int16Field"` + // +k8s:minimum=1 + Int32Field int32 `json:"int32Field"` + // +k8s:minimum=1 + Int64Field int64 `json:"int64Field"` + + // +k8s:minimum=1 + UintField uint `json:"uintField"` + // +k8s:minimum=1 + UintPtrField *uint `json:"uintPtrField"` + + // +k8s:minimum=1 + Uint16Field uint16 `json:"uint16Field"` + // +k8s:minimum=1 + Uint32Field uint32 `json:"uint32Field"` + // +k8s:minimum=1 + Uint64Field uint64 `json:"uint64Field"` + + // +k8s:minimum=1 + TypedefField IntType `json:"typedefField"` + // +k8s:minimum=1 + TypedefPtrField *IntType `json:"typedefPtrField"` +} + +// +k8s:minimum=1 +type IntType int diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/minimum/doc_test.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/minimum/doc_test.go new file mode 100644 index 00000000000..d084889683e --- /dev/null +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/minimum/doc_test.go @@ -0,0 +1,64 @@ +/* +Copyright 2024 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package minimum + +import ( + "testing" + + "k8s.io/apimachinery/pkg/api/validate/content" + "k8s.io/apimachinery/pkg/util/validation/field" + "k8s.io/utils/ptr" +) + +func Test(t *testing.T) { + st := localSchemeBuilder.Test(t) + + st.Value(&Struct{ + // all zero values + IntPtrField: ptr.To(0), + UintPtrField: ptr.To(uint(0)), + TypedefPtrField: ptr.To(IntType(0)), + }).ExpectInvalid( + field.Invalid(field.NewPath("intField"), 0, content.MinError(1)), + field.Invalid(field.NewPath("intPtrField"), 0, content.MinError(1)), + field.Invalid(field.NewPath("int16Field"), 0, content.MinError(1)), + field.Invalid(field.NewPath("int32Field"), 0, content.MinError(1)), + field.Invalid(field.NewPath("int64Field"), 0, content.MinError(1)), + field.Invalid(field.NewPath("uintField"), uint(0), content.MinError(1)), + field.Invalid(field.NewPath("uintPtrField"), uint(0), content.MinError(1)), + field.Invalid(field.NewPath("uint16Field"), uint(0), content.MinError(1)), + field.Invalid(field.NewPath("uint32Field"), uint(0), content.MinError(1)), + field.Invalid(field.NewPath("uint64Field"), uint(0), content.MinError(1)), + field.Invalid(field.NewPath("typedefField"), 0, content.MinError(1)), + field.Invalid(field.NewPath("typedefPtrField"), 0, content.MinError(1)), + ) + + st.Value(&Struct{ + IntField: 1, + IntPtrField: ptr.To(1), + Int16Field: 1, + Int32Field: 1, + Int64Field: 1, + UintField: 1, + Uint16Field: 1, + Uint32Field: 1, + Uint64Field: 1, + UintPtrField: ptr.To(uint(1)), + TypedefField: IntType(1), + TypedefPtrField: ptr.To(IntType(1)), + }).ExpectValid() +} diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/minimum/zz_generated.validations.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/minimum/zz_generated.validations.go new file mode 100644 index 00000000000..ba995970a06 --- /dev/null +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/minimum/zz_generated.validations.go @@ -0,0 +1,146 @@ +//go:build !ignore_autogenerated +// +build !ignore_autogenerated + +/* +Copyright The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by validation-gen. DO NOT EDIT. + +package minimum + +import ( + context "context" + fmt "fmt" + + operation "k8s.io/apimachinery/pkg/api/operation" + safe "k8s.io/apimachinery/pkg/api/safe" + validate "k8s.io/apimachinery/pkg/api/validate" + field "k8s.io/apimachinery/pkg/util/validation/field" + testscheme "k8s.io/code-generator/cmd/validation-gen/testscheme" +) + +func init() { localSchemeBuilder.Register(RegisterValidations) } + +// RegisterValidations adds validation functions to the given scheme. +// Public to allow building arbitrary schemes. +func RegisterValidations(scheme *testscheme.Scheme) error { + scheme.AddValidationFunc((*Struct)(nil), func(ctx context.Context, op operation.Operation, obj, oldObj interface{}, subresources ...string) field.ErrorList { + if len(subresources) == 0 { + return Validate_Struct(ctx, op, nil /* fldPath */, obj.(*Struct), safe.Cast[*Struct](oldObj)) + } + return field.ErrorList{field.InternalError(nil, fmt.Errorf("no validation found for %T, subresources: %v", obj, subresources))} + }) + return nil +} + +func Validate_IntType(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *IntType) (errs field.ErrorList) { + // type IntType + errs = append(errs, validate.Minimum(ctx, op, fldPath, obj, oldObj, 1)...) + + return errs +} + +func Validate_Struct(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *Struct) (errs field.ErrorList) { + // field Struct.TypeMeta has no validation + + // field Struct.IntField + errs = append(errs, + func(fldPath *field.Path, obj, oldObj *int) (errs field.ErrorList) { + errs = append(errs, validate.Minimum(ctx, op, fldPath, obj, oldObj, 1)...) + return + }(fldPath.Child("intField"), &obj.IntField, safe.Field(oldObj, func(oldObj *Struct) *int { return &oldObj.IntField }))...) + + // field Struct.IntPtrField + errs = append(errs, + func(fldPath *field.Path, obj, oldObj *int) (errs field.ErrorList) { + errs = append(errs, validate.Minimum(ctx, op, fldPath, obj, oldObj, 1)...) + return + }(fldPath.Child("intPtrField"), obj.IntPtrField, safe.Field(oldObj, func(oldObj *Struct) *int { return oldObj.IntPtrField }))...) + + // field Struct.Int16Field + errs = append(errs, + func(fldPath *field.Path, obj, oldObj *int16) (errs field.ErrorList) { + errs = append(errs, validate.Minimum(ctx, op, fldPath, obj, oldObj, 1)...) + return + }(fldPath.Child("int16Field"), &obj.Int16Field, safe.Field(oldObj, func(oldObj *Struct) *int16 { return &oldObj.Int16Field }))...) + + // field Struct.Int32Field + errs = append(errs, + func(fldPath *field.Path, obj, oldObj *int32) (errs field.ErrorList) { + errs = append(errs, validate.Minimum(ctx, op, fldPath, obj, oldObj, 1)...) + return + }(fldPath.Child("int32Field"), &obj.Int32Field, safe.Field(oldObj, func(oldObj *Struct) *int32 { return &oldObj.Int32Field }))...) + + // field Struct.Int64Field + errs = append(errs, + func(fldPath *field.Path, obj, oldObj *int64) (errs field.ErrorList) { + errs = append(errs, validate.Minimum(ctx, op, fldPath, obj, oldObj, 1)...) + return + }(fldPath.Child("int64Field"), &obj.Int64Field, safe.Field(oldObj, func(oldObj *Struct) *int64 { return &oldObj.Int64Field }))...) + + // field Struct.UintField + errs = append(errs, + func(fldPath *field.Path, obj, oldObj *uint) (errs field.ErrorList) { + errs = append(errs, validate.Minimum(ctx, op, fldPath, obj, oldObj, 1)...) + return + }(fldPath.Child("uintField"), &obj.UintField, safe.Field(oldObj, func(oldObj *Struct) *uint { return &oldObj.UintField }))...) + + // field Struct.UintPtrField + errs = append(errs, + func(fldPath *field.Path, obj, oldObj *uint) (errs field.ErrorList) { + errs = append(errs, validate.Minimum(ctx, op, fldPath, obj, oldObj, 1)...) + return + }(fldPath.Child("uintPtrField"), obj.UintPtrField, safe.Field(oldObj, func(oldObj *Struct) *uint { return oldObj.UintPtrField }))...) + + // field Struct.Uint16Field + errs = append(errs, + func(fldPath *field.Path, obj, oldObj *uint16) (errs field.ErrorList) { + errs = append(errs, validate.Minimum(ctx, op, fldPath, obj, oldObj, 1)...) + return + }(fldPath.Child("uint16Field"), &obj.Uint16Field, safe.Field(oldObj, func(oldObj *Struct) *uint16 { return &oldObj.Uint16Field }))...) + + // field Struct.Uint32Field + errs = append(errs, + func(fldPath *field.Path, obj, oldObj *uint32) (errs field.ErrorList) { + errs = append(errs, validate.Minimum(ctx, op, fldPath, obj, oldObj, 1)...) + return + }(fldPath.Child("uint32Field"), &obj.Uint32Field, safe.Field(oldObj, func(oldObj *Struct) *uint32 { return &oldObj.Uint32Field }))...) + + // field Struct.Uint64Field + errs = append(errs, + func(fldPath *field.Path, obj, oldObj *uint64) (errs field.ErrorList) { + errs = append(errs, validate.Minimum(ctx, op, fldPath, obj, oldObj, 1)...) + return + }(fldPath.Child("uint64Field"), &obj.Uint64Field, safe.Field(oldObj, func(oldObj *Struct) *uint64 { return &oldObj.Uint64Field }))...) + + // field Struct.TypedefField + errs = append(errs, + func(fldPath *field.Path, obj, oldObj *IntType) (errs field.ErrorList) { + errs = append(errs, validate.Minimum(ctx, op, fldPath, obj, oldObj, 1)...) + errs = append(errs, Validate_IntType(ctx, op, fldPath, obj, oldObj)...) + return + }(fldPath.Child("typedefField"), &obj.TypedefField, safe.Field(oldObj, func(oldObj *Struct) *IntType { return &oldObj.TypedefField }))...) + + // field Struct.TypedefPtrField + errs = append(errs, + func(fldPath *field.Path, obj, oldObj *IntType) (errs field.ErrorList) { + errs = append(errs, validate.Minimum(ctx, op, fldPath, obj, oldObj, 1)...) + errs = append(errs, Validate_IntType(ctx, op, fldPath, obj, oldObj)...) + return + }(fldPath.Child("typedefPtrField"), obj.TypedefPtrField, safe.Field(oldObj, func(oldObj *Struct) *IntType { return oldObj.TypedefPtrField }))...) + + return errs +} diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/validators/common.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/validators/common.go index d72950a7846..0f1dcd5c8b5 100644 --- a/staging/src/k8s.io/code-generator/cmd/validation-gen/validators/common.go +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/validators/common.go @@ -49,3 +49,23 @@ func isNilableType(t *types.Type) bool { } return false } + +func realType(t *types.Type) *types.Type { + for { + if t.Kind == types.Alias { + t = t.Underlying + } else if t.Kind == types.Pointer { + t = t.Elem + } else { + break + } + } + return t +} + +func rootTypeString(src, dst *types.Type) string { + if src == dst { + return src.String() + } + return src.String() + " -> " + dst.String() +} diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/validators/each.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/validators/each.go index 50a8ae47d9a..35e6a2eccc0 100644 --- a/staging/src/k8s.io/code-generator/cmd/validation-gen/validators/each.go +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/validators/each.go @@ -107,19 +107,6 @@ func (lttv listTypeTagValidator) GetValidations(context Context, _ []string, pay return Validations{}, nil } -func realType(t *types.Type) *types.Type { - for { - if t.Kind == types.Alias { - t = t.Underlying - } else if t.Kind == types.Pointer { - t = t.Elem - } else { - break - } - } - return t -} - func (lttv listTypeTagValidator) Docs() TagDoc { doc := TagDoc{ Tag: lttv.TagName(), diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/validators/limits.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/validators/limits.go new file mode 100644 index 00000000000..499d78838cb --- /dev/null +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/validators/limits.go @@ -0,0 +1,80 @@ +/* +Copyright 2024 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package validators + +import ( + "fmt" + "strconv" + + "k8s.io/apimachinery/pkg/util/sets" + "k8s.io/gengo/v2/types" +) + +const ( + minimumTagName = "k8s:minimum" +) + +func init() { + RegisterTagValidator(minimumTagValidator{}) +} + +type minimumTagValidator struct{} + +func (minimumTagValidator) Init(_ Config) {} + +func (minimumTagValidator) TagName() string { + return minimumTagName +} + +var minimumTagValidScopes = sets.New( + ScopeAny, +) + +func (minimumTagValidator) ValidScopes() sets.Set[Scope] { + return minimumTagValidScopes +} + +var ( + minimumValidator = types.Name{Package: libValidationPkg, Name: "Minimum"} +) + +func (minimumTagValidator) GetValidations(context Context, _ []string, payload string) (Validations, error) { + var result Validations + + if t := realType(context.Type); !types.IsInteger(t) { + return result, fmt.Errorf("can only be used on integer types (%s)", rootTypeString(context.Type, t)) + } + + intVal, err := strconv.Atoi(payload) + if err != nil { + return result, fmt.Errorf("failed to parse tag payload as int: %w", err) + } + result.AddFunction(Function(minimumTagName, DefaultFlags, minimumValidator, intVal)) + return result, nil +} + +func (mtv minimumTagValidator) Docs() TagDoc { + return TagDoc{ + Tag: mtv.TagName(), + Scopes: mtv.ValidScopes().UnsortedList(), + Description: "Indicates that a numeric field has a minimum value.", + Payloads: []TagPayloadDoc{{ + Description: "", + Docs: "This field must be greater than or equal to x.", + }}, + } +}