Files
kubevela/pkg/definition/defkit/validator.go
Ayush Kumar 012a134829 Feat: extend fluent builder API validator patterns (#7092)
* Feat: Add NotEmpty and NegativePattern constraints to StringParam; implement Closed for MapParam

Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com>

* feat: add validation support for array and map parameters

- Introduced validators for ArrayParam and MapParam, allowing for cross-field validation within structured parameters.
- Added NonEmpty validation for ArrayParam to ensure arrays are not empty.
- Implemented ConditionalStructOp for conditional struct generation based on specified conditions.
- Created a new Validator type for defining validation rules with optional guard conditions.
- Added tests for various validation scenarios, including mutual exclusion and conditional parameters.
- Enhanced the CUE generation logic to incorporate new validation features and conditional struct handling.

Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com>

* feat: extend fluent API with new scoped field conditions and improve validation checks

Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com>

* feat: enhance ArrayParam with NotEmpty constraint and update ScopedField documentation

Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com>

* feat: rename ScopedField to LocalField for improved clarity in condition building

Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com>

* feat: refactor local field conditions to use RegexMatch and streamline condition building

Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com>

* feat: simplify condition handling by removing unused comparison types and refactoring NotCondition usage

Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com>

* refactor: remove unused raw CUE block handling from baseDefinition and ComponentDefinition

Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com>

* test: update condition handling in parameter tests to use NotExpr and Cond methods

Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com>

* refactor: remove negative pattern handling from StringParam and related tests

Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com>

* feat: add support for emitting raw header blocks in template generation

Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com>

* refactor: remove non-empty check from ArrayParam and update related tests

Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com>

* refactor: convert parameter constraint tests to use Ginkgo and Gomega for improved readability and maintainability

Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com>

* feat: extend fluent APIs for OAM with new CUE generation tests and condition evaluations

Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com>

* refactor: clean up whitespace in component, cuegen, expr, param, and resource files

Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com>

* feat: enhance CUE generation by adding support for new expression types and iterator references

Signed-off-by: Ayush Kumar <aykumar@guidewire.com>
Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com>

* refactor: remove unnecessary whitespace in cuegen.go

Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com>

* refactor: rename LenOf to LenOfExpr for clarity in comparison methods

Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com>

* feat: enhance CUE generation and validation for string arrays in ArrayParam

Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com>

* ci: retrigger checks

Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com>

---------

Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com>
Signed-off-by: Ayush Kumar <aykumar@guidewire.com>
Co-authored-by: Ayush Kumar <aykumar@guidewire.com>
2026-04-09 14:25:06 +01:00

240 lines
8.6 KiB
Go

/*
Copyright 2025 The KubeVela 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 defkit
// Validator represents a CUE _validate* block pattern used for cross-field validation.
// Validators emit blocks like:
//
// _validateTenantName: {
// "tenantName must not end with a hyphen": true
// if tenantName =~ ".*-$" {
// "tenantName must not end with a hyphen": false
// }
// }
//
// Validators can be guarded (only active when a condition is true) and can be attached
// at different levels: top-level parameter, inside map/struct params, or inside array elements.
type Validator struct {
message string // the validation message (used as CUE field key)
failCond Condition // when this condition is true, validation fails
guardCond Condition // optional: validator only active when guard is true
name string // optional: override the CUE variable name (default: derived from message)
}
// Validate creates a new Validator with the given error message.
// The message is used both as the CUE field key and as the error shown on failure.
func Validate(message string) *Validator {
return &Validator{message: message}
}
// FailWhen sets the condition that causes validation to fail.
// When this condition evaluates to true, the validator emits false for the message key.
func (v *Validator) FailWhen(cond Condition) *Validator {
v.failCond = cond
return v
}
// OnlyWhen sets a guard condition — the validator is only active when this condition is true.
// If the guard condition is false, the entire validator block is skipped.
func (v *Validator) OnlyWhen(guard Condition) *Validator {
v.guardCond = guard
return v
}
// WithName overrides the CUE variable name for the validator block.
// By default, the name is derived from the message (e.g., "_validateTenantName").
func (v *Validator) WithName(name string) *Validator {
v.name = name
return v
}
// Message returns the validation message.
func (v *Validator) Message() string { return v.message }
// FailCondition returns the fail condition.
func (v *Validator) FailCondition() Condition { return v.failCond }
// GuardCondition returns the guard condition, or nil if not set.
func (v *Validator) GuardCondition() Condition { return v.guardCond }
// CUEName returns the CUE variable name for this validator.
func (v *Validator) CUEName() string { return v.name }
// LocalField creates a reference to a field in the current CUE scope (no "parameter." prefix).
// Used inside validators for condition-building on sibling fields.
// The name is emitted verbatim — supports dot-paths ("Principal.AWS")
// and array indexing ("expiration[0].date").
//
// Compare with defkit.String("name") / defkit.Bool("name") which reference
// top-level parameters and emit "parameter.name" in CUE.
//
// Example: LocalField("tenantName").Matches(".*-$")
func LocalField(name string) *LocalFieldRef {
return &LocalFieldRef{fieldName: name}
}
// LocalFieldRef represents a reference to a field within the current scope.
// It implements Value so it can be used with upstream Comparison, LenValueCondition, etc.
// It provides ergonomic condition builder methods: Matches(), Eq(), IsSet(), LenEq(), Gte(), etc.
type LocalFieldRef struct {
fieldName string
}
// Value/Expr interface implementation — allows LocalFieldRef to be used
// with upstream types like Comparison, LenValueCondition, etc.
func (s *LocalFieldRef) expr() {}
func (s *LocalFieldRef) value() {}
// Name returns the field name.
func (s *LocalFieldRef) Name() string { return s.fieldName }
// Matches creates a condition that checks if this field matches a regex pattern.
// Example: LocalField("tenantName").Matches(".*-$") generates: tenantName =~ ".*-$"
// Reuses upstream RegexMatchCondition.
func (s *LocalFieldRef) Matches(pattern string) Condition {
return RegexMatch(s, pattern)
}
// Eq creates a condition comparing this field to a value.
// Example: LocalField("type").Eq("aws") generates: type == "aws"
// Reuses upstream Comparison type.
func (s *LocalFieldRef) Eq(val any) Condition {
return Eq(s, Lit(val))
}
// Ne creates a condition checking this field is not equal to a value.
// Reuses upstream Comparison type.
func (s *LocalFieldRef) Ne(val any) Condition {
return Ne(s, Lit(val))
}
// IsSet creates a condition that checks if this field has a value (not bottom).
// Example: LocalField("role").IsSet() generates: role != _|_
// Reuses upstream PathExistsCondition.
func (s *LocalFieldRef) IsSet() Condition {
return PathExists(s.fieldName)
}
// NotSet creates a condition that checks if this field is not set (is bottom).
// Example: LocalField("role").NotSet() generates: role == _|_
func (s *LocalFieldRef) NotSet() Condition {
return Not(PathExists(s.fieldName))
}
// LenEq creates a condition that checks if this field's length equals n.
// Example: LocalField("Principal.AWS").LenEq(0) generates: len(Principal.AWS) == 0
// Reuses upstream LenValueCondition via LenEq().
func (s *LocalFieldRef) LenEq(n int) Condition {
return LenEq(s, n)
}
// LenGt creates a condition that checks if this field's length is greater than n.
// Reuses upstream LenValueCondition via LenGt().
func (s *LocalFieldRef) LenGt(n int) Condition {
return LenGt(s, n)
}
// IsEmpty creates a condition that checks if this field has length 0.
func (s *LocalFieldRef) IsEmpty() Condition {
return s.LenEq(0)
}
// Gte creates a condition comparing this field >= another local field.
// Example: LocalField("days").Gte(LocalField("expiration[0].days"))
// generates: days >= expiration[0].days
// Reuses upstream Comparison type via Ge().
func (s *LocalFieldRef) Gte(other *LocalFieldRef) Condition {
return Ge(s, other)
}
// LenOfExpr wraps a Value expression and provides comparison methods that produce Conditions.
// It emits len(<value>) in CUE. Reuses upstream LenValueCondition.
//
// Example: LenOf(Plus(Lit("tenant-"), Reference("parameter.governance.tenantName"), Lit("-"), name)).Gt(63)
// generates: len("tenant-" + parameter.governance.tenantName + "-" + parameter.name) > 63
type LenOfExpr struct {
inner Value
}
// LenOf creates a length expression wrapper around any Value.
func LenOf(v Value) *LenOfExpr {
return &LenOfExpr{inner: v}
}
// Gt creates a condition: len(inner) > n. Returns upstream *LenValueCondition.
func (l *LenOfExpr) Gt(n int) Condition {
return LenGt(l.inner, n)
}
// Gte creates a condition: len(inner) >= n. Returns upstream *LenValueCondition.
func (l *LenOfExpr) Gte(n int) Condition {
return LenGe(l.inner, n)
}
// Eq creates a condition: len(inner) == n. Returns upstream *LenValueCondition.
func (l *LenOfExpr) Eq(n int) Condition {
return LenEq(l.inner, n)
}
// TimeParse creates a Value representing time.Parse(layout, expr) in CUE.
// Used for date comparison validators.
//
// Example: TimeParse("2006-01-02T15:04:05Z", LocalField("date"))
// generates: time.Parse("2006-01-02T15:04:05Z", date)
func TimeParse(layout string, field *LocalFieldRef) *TimeParseExpr {
return &TimeParseExpr{layout: layout, fieldName: field.fieldName}
}
// TimeParseExpr represents a time.Parse() call in CUE.
type TimeParseExpr struct {
layout string
fieldName string
}
func (t *TimeParseExpr) expr() {}
func (t *TimeParseExpr) value() {}
// Layout returns the time layout string.
func (t *TimeParseExpr) Layout() string { return t.layout }
// FieldName returns the field name passed to time.Parse.
func (t *TimeParseExpr) FieldName() string { return t.fieldName }
// Gte creates a condition: time.Parse(layout, fieldA) >= time.Parse(layout, fieldB)
// Reuses upstream Comparison type via Ge().
func (t *TimeParseExpr) Gte(other *TimeParseExpr) Condition {
return Ge(t, other)
}
// RawCUECondition wraps a raw CUE expression string as a Condition.
// Use this for expressions too complex to model with the fluent API.
//
// Example: CUEExpr(`len("tenant-"+parameter.governance.tenantName+"-"+name) > 63`)
type RawCUECondition struct {
baseCondition
rawExpr string
}
// CUEExpr creates a condition from a raw CUE expression string.
// The expression is emitted verbatim in the generated CUE.
func CUEExpr(rawExpr string) *RawCUECondition {
return &RawCUECondition{rawExpr: rawExpr}
}
// Expr returns the raw CUE expression.
func (c *RawCUECondition) Expr() string { return c.rawExpr }