1- // Copyright 2025 Google LLC
1+ // Copyright 2025, 2026 Google LLC
22//
33// Licensed under the Apache License, Version 2.0 (the "License");
44// you may not use this file except in compliance with the License.
@@ -13,7 +13,6 @@ package validators
1313
1414import (
1515 "fmt"
16- "math"
1716 "regexp"
1817 "strings"
1918
@@ -55,7 +54,8 @@ func (r *RegexValidator) Validate(
5554 }
5655
5756 // helper: validate flattened cty.Values against regex, returning first error
58- validateValues := func (values []cty.Value , path config.Path ) error {
57+ validateValues := func (value cty.Value , path config.Path ) error {
58+ values := evaluateAndFlatten (value )
5959 for _ , val := range values {
6060 if val .Type () != cty .String {
6161 continue
@@ -73,7 +73,7 @@ func (r *RegexValidator) Validate(
7373
7474 // iterate targets using shared logic
7575 err = IterateRuleTargets (bp , mod , rule , group , modIdx , func (t Target ) error {
76- return validateValues (t .Values , t .Path )
76+ return validateValues (t .Value , t .Path )
7777 })
7878 return err
7979}
@@ -100,7 +100,8 @@ func (v *AllowedEnumValidator) normalizeAllowed(allowedRaw interface{}) ([]strin
100100}
101101
102102// checkValues iterates through cty.Values to ensure they exist within the allowed set, handling nulls and casing.
103- func (v * AllowedEnumValidator ) checkValues (values []cty.Value , path config.Path , allowedSet map [string ]struct {}, allowedList []string , caseSensitive bool , allowNull bool , errMsg string ) error {
103+ func (v * AllowedEnumValidator ) checkValues (value cty.Value , path config.Path , allowedSet map [string ]struct {}, allowedList []string , caseSensitive bool , allowNull bool , errMsg string ) error {
104+ values := evaluateAndFlatten (value )
104105 for _ , val := range values {
105106 if val .IsNull () {
106107 if allowNull {
@@ -190,15 +191,72 @@ func (v *AllowedEnumValidator) Validate(
190191
191192 // 4. Iterate and validate user-provided values
192193 return IterateRuleTargets (bp , mod , rule , group , modIdx , func (t Target ) error {
193- return v .checkValues (t .Values , t .Path , allowedSet , allowedList , caseSensitive , allowNull , rule .ErrorMessage )
194+ return v .checkValues (t .Value , t .Path , allowedSet , allowedList , caseSensitive , allowNull , rule .ErrorMessage )
194195 })
195196}
196197
197198// RangeValidator implements the Validator interface for 'range' type.
198199type RangeValidator struct {}
199200
201+ // checkBounds validates a single integer value against the provided min and max bounds.
202+ func (r * RangeValidator ) checkBounds (value int , min * int , max * int , customErrMsg string , path config.Path ) error {
203+ if min != nil && value < * min {
204+ msg := customErrMsg
205+ if msg == "" {
206+ msg = fmt .Sprintf ("value %d is less than the minimum allowed value of %d" , value , * min )
207+ }
208+ return config.BpError {Err : fmt .Errorf ("%s" , msg ), Path : path }
209+ }
210+ if max != nil && value > * max {
211+ msg := customErrMsg
212+ if msg == "" {
213+ msg = fmt .Sprintf ("value %d is greater than the maximum allowed value of %d" , value , * max )
214+ }
215+ return config.BpError {Err : fmt .Errorf ("%s" , msg ), Path : path }
216+ }
217+ return nil
218+ }
219+
220+ // validateTarget applies range validation to a given cty.Value.
221+ func (r * RangeValidator ) validateTarget (
222+ value cty.Value ,
223+ path config.Path ,
224+ min * int ,
225+ max * int ,
226+ lengthCheck bool ,
227+ customErrMsg string ) error {
228+
229+ if lengthCheck {
230+ if ! value .Type ().IsListType () && ! value .Type ().IsTupleType () {
231+ return config.BpError {
232+ Err : fmt .Errorf ("length_check can only be used with list types, not %s" , value .Type ().FriendlyName ()),
233+ Path : path ,
234+ }
235+ }
236+ return r .checkBounds (value .LengthInt (), min , max , customErrMsg , path )
237+ }
238+
239+ values := evaluateAndFlatten (value )
240+ for _ , val := range values {
241+ if val .IsNull () || ! val .IsKnown () {
242+ continue
243+ }
244+ if val .Type () == cty .Number {
245+ f , _ := val .AsBigFloat ().Float64 ()
246+ if err := r .checkBounds (int (f ), min , max , customErrMsg , path ); err != nil {
247+ return err
248+ }
249+ } else {
250+ return config.BpError {
251+ Err : fmt .Errorf ("range validator only supports numbers, not %s" , val .Type ().FriendlyName ()),
252+ Path : path ,
253+ }
254+ }
255+ }
256+ return nil
257+ }
258+
200259// Validate checks if the variables specified in the rule match the provided range pattern.
201- // This function focuses on the predicate and uses IterateRuleTargets from targets.go to resolve targets.
202260func (r * RangeValidator ) Validate (
203261 bp config.Blueprint ,
204262 mod config.Module ,
@@ -207,65 +265,30 @@ func (r *RangeValidator) Validate(
207265 modIdx int ) error {
208266
209267 modPath := config .Root .Groups .At (bp .GroupIndex (group .Name )).Modules .At (modIdx ).Source
210- // Extract min
211268
212- min , err := parseIntInput (rule .Inputs , "min" , math . MinInt )
269+ min , err := parseIntInput (rule .Inputs , "min" )
213270 if err != nil {
214- return config.BpError {
215- Err : fmt .Errorf (
216- "validation rule for module %q: %v" , mod .ID , err ),
217- Path : modPath ,
218- }
271+ return config.BpError {Err : fmt .Errorf ("validation rule for module %q: %v" , mod .ID , err ), Path : modPath }
219272 }
220- // Extract max
221- max , err := parseIntInput (rule .Inputs , "max" , math . MaxInt )
273+
274+ max , err := parseIntInput (rule .Inputs , "max" )
222275 if err != nil {
276+ return config.BpError {Err : fmt .Errorf ("validation rule for module %q: %v" , mod .ID , err ), Path : modPath }
277+ }
278+
279+ if min == nil && max == nil {
223280 return config.BpError {
224- Err : fmt .Errorf (
225- "validation rule for module %q: %v" , mod .ID , err ),
281+ Err : fmt .Errorf ("range validator for module %q must have at least one of 'min' or 'max' defined" , mod .ID ),
226282 Path : modPath ,
227283 }
228284 }
229285
230- // helper: validate flattened cty.Values against range, returning first error
231- validateValues := func (values []cty.Value , path config.Path ) error {
232- for _ , val := range values {
233- if val .IsNull () || ! val .IsKnown () {
234- continue
235- }
236- var checkValue int
237- var isList bool
238- if val .Type ().IsListType () {
239- checkValue = val .LengthInt ()
240- isList = true
241- } else if val .Type () == cty .Number {
242- f , _ := val .AsBigFloat ().Float64 ()
243- checkValue = int (f )
244- } else {
245- return config.BpError {
246- Err : fmt .Errorf ("range validator only supports numbers or lists, not %s" , val .Type ().FriendlyName ()),
247- Path : path ,
248- }
249- }
250- effectiveMin := min
251- if isList {
252- effectiveMin = 0 // Default min is 0 for lists if not specified
253- }
254-
255- if checkValue < effectiveMin || checkValue > max {
256- msg := rule .ErrorMessage
257- if msg == "" {
258- msg = fmt .Sprintf ("value %d is out of range [%d, %d]" , checkValue , effectiveMin , max )
259- }
260- return config.BpError {Err : fmt .Errorf ("error: %s" , msg ), Path : path }
261- }
262- }
263- return nil
286+ checkListLength , err := parseBoolInput (rule .Inputs , "length_check" , false )
287+ if err != nil {
288+ return config.BpError {Err : fmt .Errorf ("validation rule for module %q: %v" , mod .ID , err ), Path : modPath }
264289 }
265290
266- // iterate targets using shared logic
267- err = IterateRuleTargets (bp , mod , rule , group , modIdx , func (t Target ) error {
268- return validateValues (t .Values , t .Path )
291+ return IterateRuleTargets (bp , mod , rule , group , modIdx , func (t Target ) error {
292+ return r .validateTarget (t .Value , t .Path , min , max , checkListLength , rule .ErrorMessage )
269293 })
270- return err
271- }
294+ }
0 commit comments