From c53d59c41d0849266c1e6139dd69754cf176e194 Mon Sep 17 00:00:00 2001 From: Tim Hockin Date: Mon, 9 Mar 2026 17:38:22 -0700 Subject: [PATCH] Add DV tag '+k8s:maximum' --- .../pkg/api/validate/content/errors.go | 6 + .../apimachinery/pkg/api/validate/limits.go | 11 + .../pkg/api/validate/limits_test.go | 83 ++++++- .../output_tests/tags/maximum/doc.go | 62 ++++++ .../output_tests/tags/maximum/doc_test.go | 100 +++++++++ .../tags/maximum/zz_generated.validations.go | 209 ++++++++++++++++++ .../cmd/validation-gen/validators/limits.go | 52 ++++- 7 files changed, 520 insertions(+), 3 deletions(-) create mode 100644 staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/maximum/doc.go create mode 100644 staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/maximum/doc_test.go create mode 100644 staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/maximum/zz_generated.validations.go 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 index a4a1b5574c6..13eeced1a06 100644 --- a/staging/src/k8s.io/apimachinery/pkg/api/validate/content/errors.go +++ b/staging/src/k8s.io/apimachinery/pkg/api/validate/content/errors.go @@ -29,6 +29,12 @@ func MinError[T constraints.Integer](min T) string { return fmt.Sprintf("must be greater than or equal to %d", min) } +// MaxError returns a string explanation of a "must be less than or equal" +// validation failure. +func MaxError[T constraints.Integer](max T) string { + return fmt.Sprintf("must be less than or equal to %d", max) +} + // MaxLenError returns a string explanation of a "string too long" validation // failure. func MaxLenError(length int) string { diff --git a/staging/src/k8s.io/apimachinery/pkg/api/validate/limits.go b/staging/src/k8s.io/apimachinery/pkg/api/validate/limits.go index 8f3a001650e..15c8728e957 100644 --- a/staging/src/k8s.io/apimachinery/pkg/api/validate/limits.go +++ b/staging/src/k8s.io/apimachinery/pkg/api/validate/limits.go @@ -89,6 +89,17 @@ func Minimum[T constraints.Integer](_ context.Context, _ operation.Operation, fl return nil } +// Maximum verifies that the specified value is less than or equal to max. +func Maximum[T constraints.Integer](_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ *T, max T) field.ErrorList { + if value == nil { + return nil + } + if *value > max { + return field.ErrorList{field.Invalid(fldPath, *value, content.MaxError(max)).WithOrigin("maximum")} + } + return nil +} + // MinLength verifies that the specified value is at least min characters, if non-nil. func MinLength[T ~string](_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ *T, min int) field.ErrorList { if value == 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 index 82af4cbbdbc..c1de1198ab0 100644 --- a/staging/src/k8s.io/apimachinery/pkg/api/validate/limits_test.go +++ b/staging/src/k8s.io/apimachinery/pkg/api/validate/limits_test.go @@ -18,6 +18,7 @@ package validate import ( "context" + "fmt" "regexp" "testing" @@ -31,7 +32,7 @@ func TestMaxLength(t *testing.T) { name string value string max int - wantErrs field.ErrorList // regex + wantErrs field.ErrorList }{{ name: "empty string", value: "", @@ -255,12 +256,90 @@ func doTestMinimum[T constraints.Integer](t *testing.T, cases []minimumTestCase[ } } +func TestMaximum(t *testing.T) { + testMaximumPositive[int](t) + testMaximumNegative[int](t) + testMaximumPositive[int8](t) + testMaximumNegative[int8](t) + testMaximumPositive[int16](t) + testMaximumNegative[int16](t) + testMaximumPositive[int32](t) + testMaximumNegative[int32](t) + testMaximumPositive[int64](t) + testMaximumNegative[int64](t) + + testMaximumPositive[uint](t) + testMaximumPositive[uint8](t) + testMaximumPositive[uint16](t) + testMaximumPositive[uint32](t) + testMaximumPositive[uint64](t) +} + +type maximumTestCase[T constraints.Integer] struct { + max T + value T + wantErrs field.ErrorList +} + +func testMaximumPositive[T constraints.Integer](t *testing.T) { + t.Helper() + cases := []maximumTestCase[T]{{ + max: 0, + value: 0, + }, { + max: 0, + value: 1, + wantErrs: field.ErrorList{ + field.Invalid(field.NewPath("fldpath"), nil, "must be less than or equal to").WithOrigin("maximum"), + }, + }, { + max: 1, + value: 1, + }, { + max: 1, + value: 2, + wantErrs: field.ErrorList{ + field.Invalid(field.NewPath("fldpath"), nil, "must be less than or equal to").WithOrigin("maximum"), + }, + }} + doTestMaximum[T](t, cases) +} + +func testMaximumNegative[T constraints.Signed](t *testing.T) { + t.Helper() + cases := []maximumTestCase[T]{{ + max: -1, + value: -1, + }, { + max: -2, + value: -1, + wantErrs: field.ErrorList{ + field.Invalid(field.NewPath("fldpath"), nil, "must be less than or equal to").WithOrigin("maximum"), + }, + }} + + doTestMaximum[T](t, cases) +} + +func doTestMaximum[T constraints.Integer](t *testing.T, cases []maximumTestCase[T]) { + t.Helper() + matcher := field.ErrorMatcher{}.ByOrigin().ByDetailSubstring().ByField().ByType() + for _, tc := range cases { + name := fmt.Sprintf("%T (%v <= %v)", tc.value, tc.value, tc.max) + t.Run(name, func(t *testing.T) { + v := tc.value + gotErrs := Maximum(context.Background(), operation.Operation{}, field.NewPath("fldpath"), &v, nil, tc.max) + matcher.Test(t, tc.wantErrs, gotErrs) + }) + } +} + func TestMaxBytes(t *testing.T) { cases := []struct { name string value string max int - wantErrs field.ErrorList // regex + wantErrs field.ErrorList }{{ name: "empty string", value: "", diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/maximum/doc.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/maximum/doc.go new file mode 100644 index 00000000000..461f077225c --- /dev/null +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/maximum/doc.go @@ -0,0 +1,62 @@ +/* +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. +// +k8s:validation-gen-nolint +package maximum + +import "k8s.io/code-generator/cmd/validation-gen/testscheme" + +var localSchemeBuilder = testscheme.New() + +type Struct struct { + TypeMeta int + + // +k8s:maximum=1 + IntField int `json:"intField"` + // +k8s:maximum=1 + IntPtrField *int `json:"intPtrField"` + + // "int8" becomes "byte" somewhere in gengo. We don't need it so just skip it. + + // +k8s:maximum=1 + Int16Field int16 `json:"int16Field"` + // +k8s:maximum=1 + Int32Field int32 `json:"int32Field"` + // +k8s:maximum=1 + Int64Field int64 `json:"int64Field"` + + // +k8s:maximum=1 + UintField uint `json:"uintField"` + // +k8s:maximum=1 + UintPtrField *uint `json:"uintPtrField"` + + // +k8s:maximum=1 + Uint16Field uint16 `json:"uint16Field"` + // +k8s:maximum=1 + Uint32Field uint32 `json:"uint32Field"` + // +k8s:maximum=1 + Uint64Field uint64 `json:"uint64Field"` + + TypedefField IntType `json:"typedefField"` + TypedefPtrField *IntType `json:"typedefPtrField"` +} + +// +k8s:maximum=1 +type IntType int diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/maximum/doc_test.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/maximum/doc_test.go new file mode 100644 index 00000000000..fcc154d0a47 --- /dev/null +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/maximum/doc_test.go @@ -0,0 +1,100 @@ +/* +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 maximum + +import ( + "testing" + + "k8s.io/apimachinery/pkg/util/validation/field" + "k8s.io/utils/ptr" +) + +func Test(t *testing.T) { + st := localSchemeBuilder.Test(t) + + st.Value(&Struct{ + // invalid values (greater than maximum=1) + IntField: 2, + IntPtrField: ptr.To(2), + Int16Field: 2, + Int32Field: 2, + Int64Field: 2, + UintField: 2, + Uint16Field: 2, + Uint32Field: 2, + Uint64Field: 2, + UintPtrField: ptr.To(uint(2)), + TypedefField: IntType(2), + TypedefPtrField: ptr.To(IntType(2)), + }).ExpectMatches(field.ErrorMatcher{}.ByType().ByField().ByDetailSubstring(), field.ErrorList{ + field.Invalid(field.NewPath("intField"), nil, ""), + field.Invalid(field.NewPath("intPtrField"), nil, ""), + field.Invalid(field.NewPath("int16Field"), nil, ""), + field.Invalid(field.NewPath("int32Field"), nil, ""), + field.Invalid(field.NewPath("int64Field"), nil, ""), + field.Invalid(field.NewPath("uintField"), nil, ""), + field.Invalid(field.NewPath("uintPtrField"), nil, ""), + field.Invalid(field.NewPath("uint16Field"), nil, ""), + field.Invalid(field.NewPath("uint32Field"), nil, ""), + field.Invalid(field.NewPath("uint64Field"), nil, ""), + field.Invalid(field.NewPath("typedefField"), nil, ""), + field.Invalid(field.NewPath("typedefPtrField"), nil, ""), + }) + // Test validation ratcheting + st.Value(&Struct{ + IntField: 2, + IntPtrField: ptr.To(2), + Int16Field: 2, + Int32Field: 2, + Int64Field: 2, + UintField: 2, + Uint16Field: 2, + Uint32Field: 2, + Uint64Field: 2, + UintPtrField: ptr.To(uint(2)), + TypedefField: IntType(2), + TypedefPtrField: ptr.To(IntType(2)), + }).OldValue(&Struct{ + IntField: 2, + IntPtrField: ptr.To(2), + Int16Field: 2, + Int32Field: 2, + Int64Field: 2, + UintField: 2, + Uint16Field: 2, + Uint32Field: 2, + Uint64Field: 2, + UintPtrField: ptr.To(uint(2)), + TypedefField: IntType(2), + TypedefPtrField: ptr.To(IntType(2)), + }).ExpectValid() + + 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/maximum/zz_generated.validations.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/maximum/zz_generated.validations.go new file mode 100644 index 00000000000..000d3f02b0f --- /dev/null +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/maximum/zz_generated.validations.go @@ -0,0 +1,209 @@ +//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 maximum + +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 { + // type Struct + scheme.AddValidationFunc((*Struct)(nil), func(ctx context.Context, op operation.Operation, obj, oldObj interface{}) field.ErrorList { + switch op.Request.SubresourcePath() { + case "/": + 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, subresource: %v", obj, op.Request.SubresourcePath()))} + }) + return nil +} + +// Validate_IntType validates an instance of IntType according +// to declarative validation rules in the API schema. +func Validate_IntType(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *IntType) (errs field.ErrorList) { + errs = append(errs, validate.Maximum(ctx, op, fldPath, obj, oldObj, 1)...) + + return errs +} + +// Validate_Struct validates an instance of Struct according +// to declarative validation rules in the API schema. +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, oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) { + return nil + } + // call field-attached validations + errs = append(errs, validate.Maximum(ctx, op, fldPath, obj, oldObj, 1)...) + return + }(fldPath.Child("intField"), &obj.IntField, safe.Field(oldObj, func(oldObj *Struct) *int { return &oldObj.IntField }), oldObj != nil)...) + + // field Struct.IntPtrField + errs = append(errs, + func(fldPath *field.Path, obj, oldObj *int, oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) { + return nil + } + // call field-attached validations + errs = append(errs, validate.Maximum(ctx, op, fldPath, obj, oldObj, 1)...) + return + }(fldPath.Child("intPtrField"), obj.IntPtrField, safe.Field(oldObj, func(oldObj *Struct) *int { return oldObj.IntPtrField }), oldObj != nil)...) + + // field Struct.Int16Field + errs = append(errs, + func(fldPath *field.Path, obj, oldObj *int16, oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) { + return nil + } + // call field-attached validations + errs = append(errs, validate.Maximum(ctx, op, fldPath, obj, oldObj, 1)...) + return + }(fldPath.Child("int16Field"), &obj.Int16Field, safe.Field(oldObj, func(oldObj *Struct) *int16 { return &oldObj.Int16Field }), oldObj != nil)...) + + // field Struct.Int32Field + errs = append(errs, + func(fldPath *field.Path, obj, oldObj *int32, oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) { + return nil + } + // call field-attached validations + errs = append(errs, validate.Maximum(ctx, op, fldPath, obj, oldObj, 1)...) + return + }(fldPath.Child("int32Field"), &obj.Int32Field, safe.Field(oldObj, func(oldObj *Struct) *int32 { return &oldObj.Int32Field }), oldObj != nil)...) + + // field Struct.Int64Field + errs = append(errs, + func(fldPath *field.Path, obj, oldObj *int64, oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) { + return nil + } + // call field-attached validations + errs = append(errs, validate.Maximum(ctx, op, fldPath, obj, oldObj, 1)...) + return + }(fldPath.Child("int64Field"), &obj.Int64Field, safe.Field(oldObj, func(oldObj *Struct) *int64 { return &oldObj.Int64Field }), oldObj != nil)...) + + // field Struct.UintField + errs = append(errs, + func(fldPath *field.Path, obj, oldObj *uint, oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) { + return nil + } + // call field-attached validations + errs = append(errs, validate.Maximum(ctx, op, fldPath, obj, oldObj, 1)...) + return + }(fldPath.Child("uintField"), &obj.UintField, safe.Field(oldObj, func(oldObj *Struct) *uint { return &oldObj.UintField }), oldObj != nil)...) + + // field Struct.UintPtrField + errs = append(errs, + func(fldPath *field.Path, obj, oldObj *uint, oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) { + return nil + } + // call field-attached validations + errs = append(errs, validate.Maximum(ctx, op, fldPath, obj, oldObj, 1)...) + return + }(fldPath.Child("uintPtrField"), obj.UintPtrField, safe.Field(oldObj, func(oldObj *Struct) *uint { return oldObj.UintPtrField }), oldObj != nil)...) + + // field Struct.Uint16Field + errs = append(errs, + func(fldPath *field.Path, obj, oldObj *uint16, oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) { + return nil + } + // call field-attached validations + errs = append(errs, validate.Maximum(ctx, op, fldPath, obj, oldObj, 1)...) + return + }(fldPath.Child("uint16Field"), &obj.Uint16Field, safe.Field(oldObj, func(oldObj *Struct) *uint16 { return &oldObj.Uint16Field }), oldObj != nil)...) + + // field Struct.Uint32Field + errs = append(errs, + func(fldPath *field.Path, obj, oldObj *uint32, oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) { + return nil + } + // call field-attached validations + errs = append(errs, validate.Maximum(ctx, op, fldPath, obj, oldObj, 1)...) + return + }(fldPath.Child("uint32Field"), &obj.Uint32Field, safe.Field(oldObj, func(oldObj *Struct) *uint32 { return &oldObj.Uint32Field }), oldObj != nil)...) + + // field Struct.Uint64Field + errs = append(errs, + func(fldPath *field.Path, obj, oldObj *uint64, oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) { + return nil + } + // call field-attached validations + errs = append(errs, validate.Maximum(ctx, op, fldPath, obj, oldObj, 1)...) + return + }(fldPath.Child("uint64Field"), &obj.Uint64Field, safe.Field(oldObj, func(oldObj *Struct) *uint64 { return &oldObj.Uint64Field }), oldObj != nil)...) + + // field Struct.TypedefField + errs = append(errs, + func(fldPath *field.Path, obj, oldObj *IntType, oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) { + return nil + } + // call the type's validation function + 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 }), oldObj != nil)...) + + // field Struct.TypedefPtrField + errs = append(errs, + func(fldPath *field.Path, obj, oldObj *IntType, oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) { + return nil + } + // call the type's validation function + 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 }), oldObj != nil)...) + + return errs +} 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 index 8141e8f8393..31c39d4b4e0 100644 --- 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 @@ -28,6 +28,7 @@ import ( const ( maxItemsTagName = "k8s:maxItems" minimumTagName = "k8s:minimum" + maximumTagName = "k8s:maximum" minLengthTagName = "k8s:minLength" maxLengthTagName = "k8s:maxLength" maxBytesTagName = "k8s:maxBytes" @@ -36,6 +37,7 @@ const ( func init() { RegisterTagValidator(maxItemsTagValidator{}) RegisterTagValidator(minimumTagValidator{}) + RegisterTagValidator(maximumTagValidator{}) RegisterTagValidator(minLengthTagValidator{}) RegisterTagValidator(maxLengthTagValidator{}) RegisterTagValidator(maxBytesTagValidator{}) @@ -244,7 +246,55 @@ func (mtv minimumTagValidator) Docs() TagDoc { Description: "Indicates that a numeric field has a minimum value.", Payloads: []TagPayloadDoc{{ Description: "", - Docs: "This field must be greater than or equal to x.", + Docs: "This field must be greater than or equal to X.", + }}, + PayloadsType: codetags.ValueTypeInt, + PayloadsRequired: true, + } +} + +type maximumTagValidator struct{} + +func (maximumTagValidator) Init(_ Config) {} + +func (maximumTagValidator) TagName() string { + return maximumTagName +} + +var maximumTagValidScopes = sets.New(ScopeType, ScopeField, ScopeListVal, ScopeMapKey, ScopeMapVal) + +func (maximumTagValidator) ValidScopes() sets.Set[Scope] { + return maximumTagValidScopes +} + +var maximumValidator = types.Name{Package: libValidationPkg, Name: "Maximum"} + +func (maximumTagValidator) GetValidations(context Context, tag codetags.Tag) (Validations, error) { + var result Validations + + // This tag can apply to value and pointer fields, as well as typedefs + // (which should never be pointers). We need to check the concrete type. + if t := util.NonPointer(util.NativeType(context.Type)); !types.IsInteger(t) { + return result, fmt.Errorf("can only be used on integer types (%s)", rootTypeString(context.Type, t)) + } + + intVal, err := util.ParseInt(tag.Value) + if err != nil { + return result, fmt.Errorf("failed to parse tag payload as int: %w", err) + } + result.AddFunction(Function(maximumTagName, DefaultFlags, maximumValidator, intVal)) + return result, nil +} + +func (mtv maximumTagValidator) Docs() TagDoc { + return TagDoc{ + Tag: mtv.TagName(), + StabilityLevel: TagStabilityLevelBeta, + Scopes: mtv.ValidScopes().UnsortedList(), + Description: "Indicates that a numeric field has a maximum value.", + Payloads: []TagPayloadDoc{{ + Description: "", + Docs: "This field must be less than or equal to X.", }}, PayloadsType: codetags.ValueTypeInt, PayloadsRequired: true,