Merge pull request #123572 from alexzielenski/cel/name-formats
feature: add name formats library to CEL
This commit is contained in:
		@@ -130,6 +130,13 @@ var baseOpts = []VersionedOptions{
 | 
			
		||||
			library.CIDR(),
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
	// Format Library
 | 
			
		||||
	{
 | 
			
		||||
		IntroducedVersion: version.MajorMinor(1, 31),
 | 
			
		||||
		EnvOptions: []cel.EnvOption{
 | 
			
		||||
			library.Format(),
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// MustBaseEnvSet returns the common CEL base environments for Kubernetes for Version, or panics
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										73
									
								
								staging/src/k8s.io/apiserver/pkg/cel/format.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										73
									
								
								staging/src/k8s.io/apiserver/pkg/cel/format.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,73 @@
 | 
			
		||||
/*
 | 
			
		||||
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 cel
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"reflect"
 | 
			
		||||
 | 
			
		||||
	"github.com/google/cel-go/cel"
 | 
			
		||||
	"github.com/google/cel-go/checker/decls"
 | 
			
		||||
	"github.com/google/cel-go/common/types"
 | 
			
		||||
	"github.com/google/cel-go/common/types/ref"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	FormatObject = decls.NewObjectType("kubernetes.NamedFormat")
 | 
			
		||||
	FormatType   = cel.ObjectType("kubernetes.NamedFormat")
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// Format provdes a CEL representation of kubernetes format
 | 
			
		||||
type Format struct {
 | 
			
		||||
	Name         string
 | 
			
		||||
	ValidateFunc func(string) []string
 | 
			
		||||
 | 
			
		||||
	// Size of the regex string or estimated equivalent regex string used
 | 
			
		||||
	// for cost estimation
 | 
			
		||||
	MaxRegexSize int
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (d *Format) ConvertToNative(typeDesc reflect.Type) (interface{}, error) {
 | 
			
		||||
	return nil, fmt.Errorf("type conversion error from 'Format' to '%v'", typeDesc)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (d *Format) ConvertToType(typeVal ref.Type) ref.Val {
 | 
			
		||||
	switch typeVal {
 | 
			
		||||
	case FormatType:
 | 
			
		||||
		return d
 | 
			
		||||
	case types.TypeType:
 | 
			
		||||
		return FormatType
 | 
			
		||||
	default:
 | 
			
		||||
		return types.NewErr("type conversion error from '%s' to '%s'", FormatType, typeVal)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (d *Format) Equal(other ref.Val) ref.Val {
 | 
			
		||||
	otherDur, ok := other.(*Format)
 | 
			
		||||
	if !ok {
 | 
			
		||||
		return types.MaybeNoSuchOverloadErr(other)
 | 
			
		||||
	}
 | 
			
		||||
	return types.Bool(d.Name == otherDur.Name)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (d *Format) Type() ref.Type {
 | 
			
		||||
	return FormatType
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (d *Format) Value() interface{} {
 | 
			
		||||
	return d
 | 
			
		||||
}
 | 
			
		||||
@@ -25,6 +25,7 @@ import (
 | 
			
		||||
	"github.com/google/cel-go/common/types"
 | 
			
		||||
	"github.com/google/cel-go/common/types/ref"
 | 
			
		||||
	"github.com/google/cel-go/common/types/traits"
 | 
			
		||||
	"k8s.io/apiserver/pkg/cel"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// CostEstimator implements CEL's interpretable.ActualCostEstimator and checker.CostEstimator.
 | 
			
		||||
@@ -152,6 +153,35 @@ func (l *CostEstimator) CallCost(function, overloadId string, args []ref.Val, re
 | 
			
		||||
			cost := uint64(math.Ceil(float64(actualSize(args[0])) * common.StringTraversalCostFactor))
 | 
			
		||||
			return &cost
 | 
			
		||||
		}
 | 
			
		||||
	case "validate":
 | 
			
		||||
		if len(args) >= 2 {
 | 
			
		||||
			format, isFormat := args[0].Value().(*cel.Format)
 | 
			
		||||
			if isFormat {
 | 
			
		||||
				strSize := actualSize(args[1])
 | 
			
		||||
 | 
			
		||||
				// Dont have access to underlying regex, estimate a long regexp
 | 
			
		||||
				regexSize := format.MaxRegexSize
 | 
			
		||||
 | 
			
		||||
				// Copied from CEL implementation for regex cost
 | 
			
		||||
				//
 | 
			
		||||
				// https://swtch.com/~rsc/regexp/regexp1.html applies to RE2 implementation supported by CEL
 | 
			
		||||
				// Add one to string length for purposes of cost calculation to prevent product of string and regex to be 0
 | 
			
		||||
				// in case where string is empty but regex is still expensive.
 | 
			
		||||
				strCost := uint64(math.Ceil((1.0 + float64(strSize)) * common.StringTraversalCostFactor))
 | 
			
		||||
				// We don't know how many expressions are in the regex, just the string length (a huge
 | 
			
		||||
				// improvement here would be to somehow get a count the number of expressions in the regex or
 | 
			
		||||
				// how many states are in the regex state machine and use that to measure regex cost).
 | 
			
		||||
				// For now, we're making a guess that each expression in a regex is typically at least 4 chars
 | 
			
		||||
				// in length.
 | 
			
		||||
				regexCost := uint64(math.Ceil(float64(regexSize) * common.RegexStringLengthCostFactor))
 | 
			
		||||
				cost := strCost * regexCost
 | 
			
		||||
				return &cost
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	case "format.named":
 | 
			
		||||
		// Simply dictionary lookup
 | 
			
		||||
		cost := uint64(1)
 | 
			
		||||
		return &cost
 | 
			
		||||
	case "sign", "asInteger", "isInteger", "asApproximateFloat", "isGreaterThan", "isLessThan", "compareTo", "add", "sub":
 | 
			
		||||
		cost := uint64(1)
 | 
			
		||||
		return &cost
 | 
			
		||||
@@ -375,6 +405,13 @@ func (l *CostEstimator) EstimateCallCost(function, overloadId string, target *ch
 | 
			
		||||
			sz := l.sizeEstimate(args[0])
 | 
			
		||||
			return &checker.CallEstimate{CostEstimate: sz.MultiplyByCostFactor(common.StringTraversalCostFactor)}
 | 
			
		||||
		}
 | 
			
		||||
	case "validate":
 | 
			
		||||
		if target != nil {
 | 
			
		||||
			sz := l.sizeEstimate(args[0])
 | 
			
		||||
			return &checker.CallEstimate{CostEstimate: sz.MultiplyByCostFactor(common.StringTraversalCostFactor).MultiplyByCostFactor(cel.MaxNameFormatRegexSize * common.RegexStringLengthCostFactor)}
 | 
			
		||||
		}
 | 
			
		||||
	case "format.named":
 | 
			
		||||
		return &checker.CallEstimate{CostEstimate: checker.CostEstimate{Min: 1, Max: 1}}
 | 
			
		||||
	case "sign", "asInteger", "isInteger", "asApproximateFloat", "isGreaterThan", "isLessThan", "compareTo", "add", "sub":
 | 
			
		||||
		return &checker.CallEstimate{CostEstimate: checker.CostEstimate{Min: 1, Max: 1}}
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
@@ -23,6 +23,7 @@ import (
 | 
			
		||||
 | 
			
		||||
	"github.com/google/cel-go/cel"
 | 
			
		||||
	"github.com/google/cel-go/checker"
 | 
			
		||||
	"github.com/google/cel-go/common"
 | 
			
		||||
	"github.com/google/cel-go/common/ast"
 | 
			
		||||
	"github.com/google/cel-go/common/types"
 | 
			
		||||
	"github.com/google/cel-go/ext"
 | 
			
		||||
@@ -785,6 +786,42 @@ func TestQuantityCost(t *testing.T) {
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestNameFormatCost(t *testing.T) {
 | 
			
		||||
	cases := []struct {
 | 
			
		||||
		name                string
 | 
			
		||||
		expr                string
 | 
			
		||||
		expectEstimatedCost checker.CostEstimate
 | 
			
		||||
		expectRuntimeCost   uint64
 | 
			
		||||
	}{
 | 
			
		||||
		{
 | 
			
		||||
			name:                "format.named",
 | 
			
		||||
			expr:                `format.named("dns1123subdomain")`,
 | 
			
		||||
			expectEstimatedCost: checker.CostEstimate{Min: 1, Max: 1},
 | 
			
		||||
			expectRuntimeCost:   1,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "format.dns1123Subdomain.validate",
 | 
			
		||||
			expr: `format.named("dns1123Subdomain").value().validate("my-name")`,
 | 
			
		||||
			// Estimated cost doesnt know value at runtime so it is
 | 
			
		||||
			// using an estimated maximum regex length
 | 
			
		||||
			expectEstimatedCost: checker.CostEstimate{Min: 34, Max: 34},
 | 
			
		||||
			expectRuntimeCost:   17,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:                "format.dns1123label.validate",
 | 
			
		||||
			expr:                `format.named("dns1123Label").value().validate("my-name")`,
 | 
			
		||||
			expectEstimatedCost: checker.CostEstimate{Min: 34, Max: 34},
 | 
			
		||||
			expectRuntimeCost:   10,
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, tc := range cases {
 | 
			
		||||
		t.Run(tc.name, func(t *testing.T) {
 | 
			
		||||
			testCost(t, tc.expr, tc.expectEstimatedCost, tc.expectRuntimeCost)
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestSetsCost(t *testing.T) {
 | 
			
		||||
	cases := []struct {
 | 
			
		||||
		name                string
 | 
			
		||||
@@ -1027,6 +1064,8 @@ func testCost(t *testing.T, expr string, expectEsimatedCost checker.CostEstimate
 | 
			
		||||
		ext.Sets(),
 | 
			
		||||
		IP(),
 | 
			
		||||
		CIDR(),
 | 
			
		||||
		Format(),
 | 
			
		||||
		cel.OptionalTypes(),
 | 
			
		||||
		// cel-go v0.17.7 introduced CostEstimatorOptions.
 | 
			
		||||
		// Previous the presence has a cost of 0 but cel fixed it to 1. We still set to 0 here to avoid breaking changes.
 | 
			
		||||
		cel.CostEstimatorOptions(checker.PresenceTestHasCost(false)),
 | 
			
		||||
@@ -1040,7 +1079,11 @@ func testCost(t *testing.T, expr string, expectEsimatedCost checker.CostEstimate
 | 
			
		||||
	}
 | 
			
		||||
	compiled, issues := env.Compile(expr)
 | 
			
		||||
	if len(issues.Errors()) > 0 {
 | 
			
		||||
		t.Fatalf("%v", issues.Errors())
 | 
			
		||||
		var errList []string
 | 
			
		||||
		for _, issue := range issues.Errors() {
 | 
			
		||||
			errList = append(errList, issue.ToDisplayString(common.NewTextSource(expr)))
 | 
			
		||||
		}
 | 
			
		||||
		t.Fatalf("%v", errList)
 | 
			
		||||
	}
 | 
			
		||||
	estCost, err := env.EstimateCost(compiled, est)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										270
									
								
								staging/src/k8s.io/apiserver/pkg/cel/library/format.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										270
									
								
								staging/src/k8s.io/apiserver/pkg/cel/library/format.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,270 @@
 | 
			
		||||
/*
 | 
			
		||||
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 library
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"net/url"
 | 
			
		||||
 | 
			
		||||
	"github.com/asaskevich/govalidator"
 | 
			
		||||
	"github.com/google/cel-go/cel"
 | 
			
		||||
	"github.com/google/cel-go/common/decls"
 | 
			
		||||
	"github.com/google/cel-go/common/types"
 | 
			
		||||
	"github.com/google/cel-go/common/types/ref"
 | 
			
		||||
	apimachineryvalidation "k8s.io/apimachinery/pkg/api/validation"
 | 
			
		||||
	"k8s.io/apimachinery/pkg/util/validation"
 | 
			
		||||
	apiservercel "k8s.io/apiserver/pkg/cel"
 | 
			
		||||
	"k8s.io/kube-openapi/pkg/validation/strfmt"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// Format provides a CEL library exposing common named Kubernetes string
 | 
			
		||||
// validations. Can be used in CRD ValidationRules messageExpression.
 | 
			
		||||
//
 | 
			
		||||
//  Example:
 | 
			
		||||
//
 | 
			
		||||
//    rule:              format.dns1123label.validate(object.metadata.name).hasValue()
 | 
			
		||||
//    messageExpression: format.dns1123label.validate(object.metadata.name).value().join("\n")
 | 
			
		||||
//
 | 
			
		||||
// format.named(name: string) -> ?Format
 | 
			
		||||
//
 | 
			
		||||
//  Returns the Format with the given name, if it exists. Otherwise, optional.none
 | 
			
		||||
//  Allowed names are:
 | 
			
		||||
// 	 - `dns1123Label`
 | 
			
		||||
// 	 - `dns1123Subdomain`
 | 
			
		||||
// 	 - `dns1035Label`
 | 
			
		||||
// 	 - `qualifiedName`
 | 
			
		||||
// 	 - `dns1123LabelPrefix`
 | 
			
		||||
// 	 - `dns1123SubdomainPrefix`
 | 
			
		||||
// 	 - `dns1035LabelPrefix`
 | 
			
		||||
// 	 - `labelValue`
 | 
			
		||||
// 	 - `uri`
 | 
			
		||||
// 	 - `uuid`
 | 
			
		||||
// 	 - `byte`
 | 
			
		||||
// 	 - `date`
 | 
			
		||||
// 	 - `datetime`
 | 
			
		||||
//
 | 
			
		||||
// format.<formatName>() -> Format
 | 
			
		||||
//
 | 
			
		||||
//  Convenience functions for all the named formats are also available
 | 
			
		||||
//
 | 
			
		||||
//  Examples:
 | 
			
		||||
//      format.dns1123Label().validate("my-label-name")
 | 
			
		||||
//      format.dns1123Subdomain().validate("apiextensions.k8s.io")
 | 
			
		||||
//      format.dns1035Label().validate("my-label-name")
 | 
			
		||||
//      format.qualifiedName().validate("apiextensions.k8s.io/v1beta1")
 | 
			
		||||
//      format.dns1123LabelPrefix().validate("my-label-prefix-")
 | 
			
		||||
//      format.dns1123SubdomainPrefix().validate("mysubdomain.prefix.-")
 | 
			
		||||
//      format.dns1035LabelPrefix().validate("my-label-prefix-")
 | 
			
		||||
//      format.uri().validate("http://example.com")
 | 
			
		||||
//          Uses same pattern as isURL, but returns an error
 | 
			
		||||
//      format.uuid().validate("123e4567-e89b-12d3-a456-426614174000")
 | 
			
		||||
//      format.byte().validate("aGVsbG8=")
 | 
			
		||||
//      format.date().validate("2021-01-01")
 | 
			
		||||
//      format.datetime().validate("2021-01-01T00:00:00Z")
 | 
			
		||||
//
 | 
			
		||||
 | 
			
		||||
// <Format>.validate(str: string) -> ?list<string>
 | 
			
		||||
//
 | 
			
		||||
//	Validates the given string against the given format. Returns optional.none
 | 
			
		||||
//	if the string is valid, otherwise a list of validation error strings.
 | 
			
		||||
func Format() cel.EnvOption {
 | 
			
		||||
	return cel.Lib(formatLib)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var formatLib = &format{}
 | 
			
		||||
 | 
			
		||||
type format struct{}
 | 
			
		||||
 | 
			
		||||
func (*format) LibraryName() string {
 | 
			
		||||
	return "format"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func ZeroArgumentFunctionBinding(binding func() ref.Val) decls.OverloadOpt {
 | 
			
		||||
	return func(o *decls.OverloadDecl) (*decls.OverloadDecl, error) {
 | 
			
		||||
		wrapped, err := decls.FunctionBinding(func(values ...ref.Val) ref.Val { return binding() })(o)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
		if len(wrapped.ArgTypes()) != 0 {
 | 
			
		||||
			return nil, fmt.Errorf("function binding must have 0 arguments")
 | 
			
		||||
		}
 | 
			
		||||
		return o, nil
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (*format) CompileOptions() []cel.EnvOption {
 | 
			
		||||
	options := make([]cel.EnvOption, 0, len(formatLibraryDecls))
 | 
			
		||||
	for name, overloads := range formatLibraryDecls {
 | 
			
		||||
		options = append(options, cel.Function(name, overloads...))
 | 
			
		||||
	}
 | 
			
		||||
	for name, constantValue := range ConstantFormats {
 | 
			
		||||
		prefixedName := "format." + name
 | 
			
		||||
		options = append(options, cel.Function(prefixedName, cel.Overload(prefixedName, []*cel.Type{}, apiservercel.FormatType, ZeroArgumentFunctionBinding(func() ref.Val {
 | 
			
		||||
			return constantValue
 | 
			
		||||
		}))))
 | 
			
		||||
	}
 | 
			
		||||
	return options
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (*format) ProgramOptions() []cel.ProgramOption {
 | 
			
		||||
	return []cel.ProgramOption{}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var ConstantFormats map[string]*apiservercel.Format = map[string]*apiservercel.Format{
 | 
			
		||||
	"dns1123Label": {
 | 
			
		||||
		Name:         "DNS1123Label",
 | 
			
		||||
		ValidateFunc: func(s string) []string { return apimachineryvalidation.NameIsDNSLabel(s, false) },
 | 
			
		||||
		MaxRegexSize: 30,
 | 
			
		||||
	},
 | 
			
		||||
	"dns1123Subdomain": {
 | 
			
		||||
		Name:         "DNS1123Subdomain",
 | 
			
		||||
		ValidateFunc: func(s string) []string { return apimachineryvalidation.NameIsDNSSubdomain(s, false) },
 | 
			
		||||
		MaxRegexSize: 60,
 | 
			
		||||
	},
 | 
			
		||||
	"dns1035Label": {
 | 
			
		||||
		Name:         "DNS1035Label",
 | 
			
		||||
		ValidateFunc: func(s string) []string { return apimachineryvalidation.NameIsDNS1035Label(s, false) },
 | 
			
		||||
		MaxRegexSize: 30,
 | 
			
		||||
	},
 | 
			
		||||
	"qualifiedName": {
 | 
			
		||||
		Name:         "QualifiedName",
 | 
			
		||||
		ValidateFunc: validation.IsQualifiedName,
 | 
			
		||||
		MaxRegexSize: 60, // uses subdomain regex
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	"dns1123LabelPrefix": {
 | 
			
		||||
		Name:         "DNS1123LabelPrefix",
 | 
			
		||||
		ValidateFunc: func(s string) []string { return apimachineryvalidation.NameIsDNSLabel(s, true) },
 | 
			
		||||
		MaxRegexSize: 30,
 | 
			
		||||
	},
 | 
			
		||||
	"dns1123SubdomainPrefix": {
 | 
			
		||||
		Name:         "DNS1123SubdomainPrefix",
 | 
			
		||||
		ValidateFunc: func(s string) []string { return apimachineryvalidation.NameIsDNSSubdomain(s, true) },
 | 
			
		||||
		MaxRegexSize: 60,
 | 
			
		||||
	},
 | 
			
		||||
	"dns1035LabelPrefix": {
 | 
			
		||||
		Name:         "DNS1035LabelPrefix",
 | 
			
		||||
		ValidateFunc: func(s string) []string { return apimachineryvalidation.NameIsDNS1035Label(s, true) },
 | 
			
		||||
		MaxRegexSize: 30,
 | 
			
		||||
	},
 | 
			
		||||
	"labelValue": {
 | 
			
		||||
		Name:         "LabelValue",
 | 
			
		||||
		ValidateFunc: validation.IsValidLabelValue,
 | 
			
		||||
		MaxRegexSize: 40,
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	// CRD formats
 | 
			
		||||
	// Implementations sourced from strfmt, which kube-openapi uses as its
 | 
			
		||||
	// format library. There are other CRD formats supported, but they are
 | 
			
		||||
	// covered by other portions of the CEL library (like IP/CIDR), or their
 | 
			
		||||
	// use is discouraged (like bsonobjectid, email, etc)
 | 
			
		||||
	"uri": {
 | 
			
		||||
		Name: "URI",
 | 
			
		||||
		ValidateFunc: func(s string) []string {
 | 
			
		||||
			// Directly call ParseRequestURI since we can get a better error message
 | 
			
		||||
			_, err := url.ParseRequestURI(s)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return []string{err.Error()}
 | 
			
		||||
			}
 | 
			
		||||
			return nil
 | 
			
		||||
		},
 | 
			
		||||
		// Use govalidator url regex to estimate, since ParseRequestURI
 | 
			
		||||
		// doesnt use regex
 | 
			
		||||
		MaxRegexSize: len(govalidator.URL),
 | 
			
		||||
	},
 | 
			
		||||
	"uuid": {
 | 
			
		||||
		Name: "uuid",
 | 
			
		||||
		ValidateFunc: func(s string) []string {
 | 
			
		||||
			if !strfmt.Default.Validates("uuid", s) {
 | 
			
		||||
				return []string{"does not match the UUID format"}
 | 
			
		||||
			}
 | 
			
		||||
			return nil
 | 
			
		||||
		},
 | 
			
		||||
		MaxRegexSize: len(strfmt.UUIDPattern),
 | 
			
		||||
	},
 | 
			
		||||
	"byte": {
 | 
			
		||||
		Name: "byte",
 | 
			
		||||
		ValidateFunc: func(s string) []string {
 | 
			
		||||
			if !strfmt.Default.Validates("byte", s) {
 | 
			
		||||
				return []string{"invalid base64"}
 | 
			
		||||
			}
 | 
			
		||||
			return nil
 | 
			
		||||
		},
 | 
			
		||||
		MaxRegexSize: len(govalidator.Base64),
 | 
			
		||||
	},
 | 
			
		||||
	"date": {
 | 
			
		||||
		Name: "date",
 | 
			
		||||
		ValidateFunc: func(s string) []string {
 | 
			
		||||
			if !strfmt.Default.Validates("date", s) {
 | 
			
		||||
				return []string{"invalid date"}
 | 
			
		||||
			}
 | 
			
		||||
			return nil
 | 
			
		||||
		},
 | 
			
		||||
		// Estimated regex size for RFC3339FullDate which is
 | 
			
		||||
		// a date format. Assume a date-time pattern is longer
 | 
			
		||||
		// so use that to conservatively estimate this
 | 
			
		||||
		MaxRegexSize: len(strfmt.DateTimePattern),
 | 
			
		||||
	},
 | 
			
		||||
	"datetime": {
 | 
			
		||||
		Name: "datetime",
 | 
			
		||||
		ValidateFunc: func(s string) []string {
 | 
			
		||||
			if !strfmt.Default.Validates("datetime", s) {
 | 
			
		||||
				return []string{"invalid datetime"}
 | 
			
		||||
			}
 | 
			
		||||
			return nil
 | 
			
		||||
		},
 | 
			
		||||
		MaxRegexSize: len(strfmt.DateTimePattern),
 | 
			
		||||
	},
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var formatLibraryDecls = map[string][]cel.FunctionOpt{
 | 
			
		||||
	"validate": {
 | 
			
		||||
		cel.MemberOverload("format-validate", []*cel.Type{apiservercel.FormatType, cel.StringType}, cel.OptionalType(cel.ListType(cel.StringType)), cel.BinaryBinding(formatValidate)),
 | 
			
		||||
	},
 | 
			
		||||
	"format.named": {
 | 
			
		||||
		cel.Overload("format-named", []*cel.Type{cel.StringType}, cel.OptionalType(apiservercel.FormatType), cel.UnaryBinding(func(name ref.Val) ref.Val {
 | 
			
		||||
			nameString, ok := name.Value().(string)
 | 
			
		||||
			if !ok {
 | 
			
		||||
				return types.MaybeNoSuchOverloadErr(name)
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			f, ok := ConstantFormats[nameString]
 | 
			
		||||
			if !ok {
 | 
			
		||||
				return types.OptionalNone
 | 
			
		||||
			}
 | 
			
		||||
			return types.OptionalOf(f)
 | 
			
		||||
		})),
 | 
			
		||||
	},
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func formatValidate(arg1, arg2 ref.Val) ref.Val {
 | 
			
		||||
	f, ok := arg1.Value().(*apiservercel.Format)
 | 
			
		||||
	if !ok {
 | 
			
		||||
		return types.MaybeNoSuchOverloadErr(arg1)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	str, ok := arg2.Value().(string)
 | 
			
		||||
	if !ok {
 | 
			
		||||
		return types.MaybeNoSuchOverloadErr(arg2)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	res := f.ValidateFunc(str)
 | 
			
		||||
	if len(res) == 0 {
 | 
			
		||||
		return types.OptionalNone
 | 
			
		||||
	}
 | 
			
		||||
	return types.OptionalOf(types.NewStringList(types.DefaultTypeAdapter, res))
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										230
									
								
								staging/src/k8s.io/apiserver/pkg/cel/library/format_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										230
									
								
								staging/src/k8s.io/apiserver/pkg/cel/library/format_test.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,230 @@
 | 
			
		||||
/*
 | 
			
		||||
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 library_test
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"testing"
 | 
			
		||||
 | 
			
		||||
	"github.com/google/cel-go/common/types"
 | 
			
		||||
	"github.com/google/cel-go/common/types/ref"
 | 
			
		||||
	"k8s.io/apiserver/pkg/cel/library"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func TestFormat(t *testing.T) {
 | 
			
		||||
	type testcase struct {
 | 
			
		||||
		name               string
 | 
			
		||||
		expr               string
 | 
			
		||||
		expectValue        ref.Val
 | 
			
		||||
		expectedCompileErr []string
 | 
			
		||||
		expectedRuntimeErr string
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	cases := []testcase{
 | 
			
		||||
		{
 | 
			
		||||
			name:        "example_usage_dns1123Label",
 | 
			
		||||
			expr:        `format.dns1123Label().validate("my-label-name")`,
 | 
			
		||||
			expectValue: types.OptionalNone,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:        "example_usage_dns1123Subdomain",
 | 
			
		||||
			expr:        `format.dns1123Subdomain().validate("apiextensions.k8s.io")`,
 | 
			
		||||
			expectValue: types.OptionalNone,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:        "example_usage_qualifiedName",
 | 
			
		||||
			expr:        `format.qualifiedName().validate("apiextensions.k8s.io/v1beta1")`,
 | 
			
		||||
			expectValue: types.OptionalNone,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:        "example_usage_dns1123LabelPrefix",
 | 
			
		||||
			expr:        `format.dns1123LabelPrefix().validate("my-label-prefix-")`,
 | 
			
		||||
			expectValue: types.OptionalNone,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:        "example_usage_dns1123SubdomainPrefix",
 | 
			
		||||
			expr:        `format.dns1123SubdomainPrefix().validate("mysubdomain.prefix.-")`,
 | 
			
		||||
			expectValue: types.OptionalNone,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:        "example_usage_dns1035LabelPrefix",
 | 
			
		||||
			expr:        `format.dns1035LabelPrefix().validate("my-label-prefix-")`,
 | 
			
		||||
			expectValue: types.OptionalNone,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:        "example_usage_uri",
 | 
			
		||||
			expr:        `format.uri().validate("http://example.com")`,
 | 
			
		||||
			expectValue: types.OptionalNone,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:        "example_usage_uuid",
 | 
			
		||||
			expr:        `format.uuid().validate("123e4567-e89b-12d3-a456-426614174000")`,
 | 
			
		||||
			expectValue: types.OptionalNone,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:        "example_usage_byte",
 | 
			
		||||
			expr:        `format.byte().validate("aGVsbG8=")`,
 | 
			
		||||
			expectValue: types.OptionalNone,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:        "example_usage_date",
 | 
			
		||||
			expr:        `format.date().validate("2021-01-01")`,
 | 
			
		||||
			expectValue: types.OptionalNone,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:        "example_usage_datetime",
 | 
			
		||||
			expr:        `format.datetime().validate("2021-01-01T00:00:00Z")`,
 | 
			
		||||
			expectValue: types.OptionalNone,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:        "dns1123Label",
 | 
			
		||||
			expr:        `format.dns1123Label().validate("contains a space")`,
 | 
			
		||||
			expectValue: types.OptionalOf(types.NewStringList(types.DefaultTypeAdapter, []string{"a lowercase RFC 1123 label must consist of lower case alphanumeric characters or '-', and must start and end with an alphanumeric character (e.g. 'my-name',  or '123-abc', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?')"})),
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:        "dns1123Subdomain",
 | 
			
		||||
			expr:        `format.dns1123Subdomain().validate("contains a space")`,
 | 
			
		||||
			expectValue: types.OptionalOf(types.NewStringList(types.DefaultTypeAdapter, []string{`a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')`})),
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:        "dns1035Label",
 | 
			
		||||
			expr:        `format.dns1035Label().validate("contains a space")`,
 | 
			
		||||
			expectValue: types.OptionalOf(types.NewStringList(types.DefaultTypeAdapter, []string{`a DNS-1035 label must consist of lower case alphanumeric characters or '-', start with an alphabetic character, and end with an alphanumeric character (e.g. 'my-name',  or 'abc-123', regex used for validation is '[a-z]([-a-z0-9]*[a-z0-9])?')`})),
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:        "qualifiedName",
 | 
			
		||||
			expr:        `format.qualifiedName().validate("contains a space")`,
 | 
			
		||||
			expectValue: types.OptionalOf(types.NewStringList(types.DefaultTypeAdapter, []string{`name part must consist of alphanumeric characters, '-', '_' or '.', and must start and end with an alphanumeric character (e.g. 'MyName',  or 'my.name',  or '123-abc', regex used for validation is '([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]')`})),
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:        "dns1123LabelPrefix",
 | 
			
		||||
			expr:        `format.dns1123LabelPrefix().validate("contains a space-")`,
 | 
			
		||||
			expectValue: types.OptionalOf(types.NewStringList(types.DefaultTypeAdapter, []string{"a lowercase RFC 1123 label must consist of lower case alphanumeric characters or '-', and must start and end with an alphanumeric character (e.g. 'my-name',  or '123-abc', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?')"})),
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:        "dns1123SubdomainPrefix",
 | 
			
		||||
			expr:        `format.dns1123SubdomainPrefix().validate("contains a space-")`,
 | 
			
		||||
			expectValue: types.OptionalOf(types.NewStringList(types.DefaultTypeAdapter, []string{`a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')`})),
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:        "dns1035LabelPrefix",
 | 
			
		||||
			expr:        `format.dns1035LabelPrefix().validate("contains a space-")`,
 | 
			
		||||
			expectValue: types.OptionalOf(types.NewStringList(types.DefaultTypeAdapter, []string{`a DNS-1035 label must consist of lower case alphanumeric characters or '-', start with an alphabetic character, and end with an alphanumeric character (e.g. 'my-name',  or 'abc-123', regex used for validation is '[a-z]([-a-z0-9]*[a-z0-9])?')`})),
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:        "dns1123Label_Success",
 | 
			
		||||
			expr:        `format.dns1123Label().validate("my-label-name")`,
 | 
			
		||||
			expectValue: types.OptionalNone,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:        "dns1123Subdomain_Success",
 | 
			
		||||
			expr:        `format.dns1123Subdomain().validate("example.com")`,
 | 
			
		||||
			expectValue: types.OptionalNone,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:        "dns1035Label_Success",
 | 
			
		||||
			expr:        `format.dns1035Label().validate("my-label-name")`,
 | 
			
		||||
			expectValue: types.OptionalNone,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:        "qualifiedName_Success",
 | 
			
		||||
			expr:        `format.qualifiedName().validate("my.name")`,
 | 
			
		||||
			expectValue: types.OptionalNone,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			// byte is base64
 | 
			
		||||
			name:        "byte_success",
 | 
			
		||||
			expr:        `format.byte().validate("aGVsbG8=")`,
 | 
			
		||||
			expectValue: types.OptionalNone,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			// byte is base64
 | 
			
		||||
			name:        "byte_failure",
 | 
			
		||||
			expr:        `format.byte().validate("aGVsbG8")`,
 | 
			
		||||
			expectValue: types.OptionalOf(types.NewStringList(types.DefaultTypeAdapter, []string{"invalid base64"})),
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "date_success",
 | 
			
		||||
			expr: `format.date().validate("2020-01-01")`,
 | 
			
		||||
			// date is a valid date
 | 
			
		||||
			expectValue: types.OptionalNone,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:        "date_failure",
 | 
			
		||||
			expr:        `format.date().validate("2020-01-32")`,
 | 
			
		||||
			expectValue: types.OptionalOf(types.NewStringList(types.DefaultTypeAdapter, []string{"invalid date"})),
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "datetime_success",
 | 
			
		||||
			expr: `format.datetime().validate("2020-01-01T00:00:00Z")`,
 | 
			
		||||
			// datetime is a valid date
 | 
			
		||||
			expectValue: types.OptionalNone,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:        "datetime_failure",
 | 
			
		||||
			expr:        `format.datetime().validate("2020-01-32T00:00:00Z")`,
 | 
			
		||||
			expectValue: types.OptionalOf(types.NewStringList(types.DefaultTypeAdapter, []string{"invalid datetime"})),
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:        "unknown_format",
 | 
			
		||||
			expr:        `format.named("unknown").hasValue()`,
 | 
			
		||||
			expectValue: types.False,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:        "labelValue_success",
 | 
			
		||||
			expr:        `format.labelValue().validate("my-cool-label-Value")`,
 | 
			
		||||
			expectValue: types.OptionalNone,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "labelValue_failure",
 | 
			
		||||
			expr: `format.labelValue().validate("my-cool-label-Value!!\n\n!!!")`,
 | 
			
		||||
			expectValue: types.OptionalOf(types.NewStringList(types.DefaultTypeAdapter, []string{
 | 
			
		||||
				"a valid label must be an empty string or consist of alphanumeric characters, '-', '_' or '.', and must start and end with an alphanumeric character (e.g. 'MyValue',  or 'my_value',  or '12345', regex used for validation is '(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])?')",
 | 
			
		||||
			})),
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Also test format names and comparisons of all constants
 | 
			
		||||
	for keyLHS := range library.ConstantFormats {
 | 
			
		||||
		cases = append(cases, testcase{
 | 
			
		||||
			name:        "lookup and comparison",
 | 
			
		||||
			expr:        fmt.Sprintf(`format.named("%s").hasValue()`, keyLHS),
 | 
			
		||||
			expectValue: types.True,
 | 
			
		||||
		}, testcase{
 | 
			
		||||
			name:        "comparison with lookup succeeds",
 | 
			
		||||
			expr:        fmt.Sprintf(`format.named("%s").value() == format.%s()`, keyLHS, keyLHS),
 | 
			
		||||
			expectValue: types.True,
 | 
			
		||||
		})
 | 
			
		||||
 | 
			
		||||
		for keyRHS := range library.ConstantFormats {
 | 
			
		||||
			if keyLHS == keyRHS {
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
			cases = append(cases, testcase{
 | 
			
		||||
				name:        fmt.Sprintf("compare_%s_%s", keyLHS, keyRHS),
 | 
			
		||||
				expr:        fmt.Sprintf(`format.%s() == format.%s()`, keyLHS, keyRHS),
 | 
			
		||||
				expectValue: types.False,
 | 
			
		||||
			})
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, tc := range cases {
 | 
			
		||||
		t.Run(tc.name, func(t *testing.T) {
 | 
			
		||||
			testQuantity(t, tc.expr, tc.expectValue, tc.expectedRuntimeErr, tc.expectedCompileErr)
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@@ -21,9 +21,11 @@ import (
 | 
			
		||||
	"testing"
 | 
			
		||||
 | 
			
		||||
	"github.com/google/cel-go/cel"
 | 
			
		||||
	"github.com/google/cel-go/common"
 | 
			
		||||
	"github.com/google/cel-go/common/types"
 | 
			
		||||
	"github.com/google/cel-go/common/types/ref"
 | 
			
		||||
	"github.com/google/cel-go/ext"
 | 
			
		||||
	"github.com/google/go-cmp/cmp"
 | 
			
		||||
	"github.com/stretchr/testify/require"
 | 
			
		||||
 | 
			
		||||
	"k8s.io/apimachinery/pkg/api/resource"
 | 
			
		||||
@@ -34,11 +36,13 @@ import (
 | 
			
		||||
 | 
			
		||||
func testQuantity(t *testing.T, expr string, expectResult ref.Val, expectRuntimeErrPattern string, expectCompileErrs []string) {
 | 
			
		||||
	env, err := cel.NewEnv(
 | 
			
		||||
		cel.OptionalTypes(),
 | 
			
		||||
		ext.Strings(),
 | 
			
		||||
		library.URLs(),
 | 
			
		||||
		library.Regex(),
 | 
			
		||||
		library.Lists(),
 | 
			
		||||
		library.Quantity(),
 | 
			
		||||
		library.Format(),
 | 
			
		||||
	)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatalf("%v", err)
 | 
			
		||||
@@ -79,7 +83,18 @@ func testQuantity(t *testing.T, expr string, expectResult ref.Val, expectRuntime
 | 
			
		||||
		require.Empty(t, missingCompileErrs, "expected compilation errors")
 | 
			
		||||
		return
 | 
			
		||||
	} else if len(issues.Errors()) > 0 {
 | 
			
		||||
		t.Fatalf("%v", issues.Errors())
 | 
			
		||||
		errorStrings := []string{}
 | 
			
		||||
		source := common.NewTextSource(expr)
 | 
			
		||||
		for _, issue := range issues.Errors() {
 | 
			
		||||
			errorStrings = append(errorStrings, issue.ToDisplayString(source))
 | 
			
		||||
		}
 | 
			
		||||
		t.Fatalf("%v", errorStrings)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Typecheck expression
 | 
			
		||||
	_, err = cel.AstToCheckedExpr(compiled)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatalf("%v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	prog, err := env.Program(compiled)
 | 
			
		||||
@@ -99,7 +114,7 @@ func testQuantity(t *testing.T, expr string, expectResult ref.Val, expectRuntime
 | 
			
		||||
		t.Fatalf("%v", err)
 | 
			
		||||
	} else if expectResult != nil {
 | 
			
		||||
		converted := res.Equal(expectResult).Value().(bool)
 | 
			
		||||
		require.True(t, converted, "expectation not equal to output")
 | 
			
		||||
		require.True(t, converted, "expectation not equal to output: %v", cmp.Diff(expectResult.Value(), res.Value()))
 | 
			
		||||
	} else {
 | 
			
		||||
		t.Fatal("expected result must not be nil")
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
@@ -47,4 +47,6 @@ const (
 | 
			
		||||
	MinBoolSize = 4
 | 
			
		||||
	// MinNumberSize is the length of literal 0
 | 
			
		||||
	MinNumberSize = 1
 | 
			
		||||
 | 
			
		||||
	MaxNameFormatRegexSize = 128
 | 
			
		||||
)
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user