Skip to content

Commit 9ea816d

Browse files
authored
Cue: List constraints (#1060)
1 parent 2158b2c commit 9ea816d

File tree

21 files changed

+518
-13
lines changed

21 files changed

+518
-13
lines changed

internal/ast/types.go

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,9 @@ const (
6565
GreaterThanEqualOp Op = ">="
6666
RegexMatchOp Op = "=~"
6767
NotRegexMatchOp Op = "!~"
68+
MinItemsOp Op = "minItems"
69+
MaxItemsOp Op = "maxItems"
70+
UniqueItemsOp Op = "uniqueItems"
6871
)
6972

7073
type TypeConstraint struct {
@@ -732,17 +735,22 @@ func (t DisjunctionType) DeepCopy() DisjunctionType {
732735
}
733736

734737
type ArrayType struct {
735-
ValueType Type `yaml:"value_type"`
738+
ValueType Type `yaml:"value_type"`
739+
Constraints []TypeConstraint `json:",omitempty"`
736740
}
737741

738742
func (t *ArrayType) AcceptsValue(value any) bool {
739743
return t.ValueType.AcceptsValue(value)
740744
}
741745

742746
func (t ArrayType) DeepCopy() ArrayType {
743-
return ArrayType{
747+
newT := ArrayType{
744748
ValueType: t.ValueType.DeepCopy(),
745749
}
750+
for _, c := range t.Constraints {
751+
newT.Constraints = append(newT.Constraints, c.DeepCopy())
752+
}
753+
return newT
746754
}
747755

748756
func (t ArrayType) IsArrayOf(acceptedKinds ...Kind) bool {

internal/jennies/golang/templates/types/struct_validation_method.tmpl

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,41 @@ func (resource {{ .def.Name|formatObjectName }}) Validate() error {
2626
{{- if $needsDereference }}
2727
if {{ .SelfName }} != nil {
2828
{{- end }}
29+
{{- range (resolveRefs .Type).Array.Constraints }}
30+
{{- if eq .Op "minItems" }}
31+
{{- $errors := importStdPkg "errors" }}
32+
if len({{ $dereference }}{{ $.SelfName }}) < {{ .Args | first }} {
33+
errs = append(errs, cog.MakeBuildErrors("{{ $.ConstraintPath }}", errors.New("must have at least {{ .Args | first }} items"))...)
34+
}
35+
{{- else if eq .Op "maxItems" }}
36+
{{- $errors := importStdPkg "errors" }}
37+
if len({{ $dereference }}{{ $.SelfName }}) > {{ .Args | first }} {
38+
errs = append(errs, cog.MakeBuildErrors("{{ $.ConstraintPath }}", errors.New("must have at most {{ .Args | first }} items"))...)
39+
}
40+
{{- else if eq .Op "uniqueItems" }}
41+
{{- $errors := importStdPkg "errors" }}
42+
{{- $seen := importStdPkg "fmt" }}
43+
{{- $seenVar := print "seen" ($.ConstraintPath | toGoIdent) }}
44+
{{ $seenVar }} := make(map[string]struct{})
45+
for _, item{{ $depth }} := range {{ $dereference }}{{ $.SelfName }} {
46+
key{{ $depth }} := fmt.Sprintf("%v", item{{ $depth }})
47+
if _, exists{{ $depth }} := {{ $seenVar }}[key{{ $depth }}]; exists{{ $depth }} {
48+
errs = append(errs, cog.MakeBuildErrors("{{ $.ConstraintPath }}", errors.New("must have unique items"))...)
49+
break
50+
}
51+
{{ $seenVar }}[key{{ $depth }}] = struct{}{}
52+
}
53+
{{- end }}
54+
{{- end }}
55+
{{- if resolvesToConstraints (resolveRefs .Type).Array.ValueType }}
2956

3057
for i{{ $depth }} := range {{ $dereference }}{{ .SelfName }} {
3158
{{- $selfKey := print (ternary (print "(*" .SelfName ")") .SelfName $needsDereference) "[i" $depth "]" }}
3259
{{- $constraintPath := print .ConstraintPath "[\"+strconv.Itoa(i" $depth ")+\"]" }}
3360
{{- $strconv := importStdPkg "strconv" -}}
3461
{{- template "type_validate_check" (dict "ConstraintPath" $constraintPath "Type" (resolveRefs .Type).Array.ValueType "Nullable" (resolveRefs .Type).Array.ValueType.Nullable "Dereference" false "SelfName" $selfKey "Depth" (add1 $depth)) }}
3562
}
63+
{{- end }}
3664
{{- if $needsDereference }}}{{ end }}
3765
{{- else if resolvesToMap .Type }}
3866
{{- $needsDereference := and .Type.IsRef .Type.Nullable }}
@@ -88,11 +116,11 @@ func (resource {{ .def.Name|formatObjectName }}) Validate() error {
88116

89117
{{- define "type_constraints" }}
90118
{{- $dereference := ternary "*" "" .Type.Nullable }}
91-
{{- $errors := importStdPkg "errors" -}}
92119
{{- range .Constraints }}
93120
{{- $leftOperand := print $dereference $.SelfName }}
94121
{{- $rightOperand := .Args | first }}
95122
{{- $operator := .Op }}
123+
{{- $errors := importStdPkg "errors" -}}
96124
{{- if eq .Op "minLength" }}
97125
{{- $leftOperand = print "len([]rune(" $leftOperand "))" }}
98126
{{- $operator = ">=" }}

internal/jennies/golang/tmpl.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,9 @@ func initTemplates(config Config, apiRefCollector *common.APIReferenceCollector)
6464
"resolvesToConstraints": func(_ ast.Type) string {
6565
panic("resolvesToConstraints() needs to be overridden by a jenny")
6666
},
67+
"toGoIdent": func(_ string) string {
68+
panic("toGoIdent() needs to be overridden by a jenny")
69+
},
6770
"formatValue": func(destinationType ast.Type, value any) string {
6871
panic("formatValue() needs to be overridden by a jenny")
6972
},

internal/jennies/golang/validation.go

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"fmt"
55
"slices"
66
"strings"
7+
"unicode"
78

89
"github.com/grafana/cog/internal/ast"
910
"github.com/grafana/cog/internal/jennies/common"
@@ -67,7 +68,7 @@ func (jenny validationMethods) generateForObject(buffer *strings.Builder, contex
6768
}
6869

6970
if typeDef.IsArray() {
70-
return resolvesToConstraints(typeDef.AsArray().ValueType)
71+
return len(typeDef.AsArray().Constraints) != 0 || resolvesToConstraints(typeDef.AsArray().ValueType)
7172
}
7273

7374
if typeDef.IsConstantRef() {
@@ -94,6 +95,15 @@ func (jenny validationMethods) generateForObject(buffer *strings.Builder, contex
9495
"importStdPkg": func(pkg string) string {
9596
return imports.Add(pkg, pkg)
9697
},
98+
"toGoIdent": func(s string) string {
99+
var b strings.Builder
100+
for _, r := range s {
101+
if unicode.IsLetter(r) || unicode.IsDigit(r) {
102+
b.WriteRune(r)
103+
}
104+
}
105+
return b.String()
106+
},
97107
})
98108

99109
rendered, err := tmpl.Render("types/struct_validation_method.tmpl", map[string]any{

internal/jennies/jsonschema/schema.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -337,10 +337,24 @@ func (jenny Schema) formatArray(typeDef ast.Type) Definition {
337337

338338
definition.Set("type", "array")
339339
definition.Set("items", jenny.formatType(typeDef.AsArray().ValueType))
340+
jenny.addArrayConstraints(definition, typeDef)
340341

341342
return definition
342343
}
343344

345+
func (jenny Schema) addArrayConstraints(definition *orderedmap.Map[string, any], typeDef ast.Type) {
346+
for _, constraint := range typeDef.AsArray().Constraints {
347+
switch constraint.Op {
348+
case ast.MinItemsOp:
349+
definition.Set("minItems", constraint.Args[0])
350+
case ast.MaxItemsOp:
351+
definition.Set("maxItems", constraint.Args[0])
352+
case ast.UniqueItemsOp:
353+
definition.Set("uniqueItems", true)
354+
}
355+
}
356+
}
357+
344358
func (jenny Schema) formatMap(typeDef ast.Type) Definition {
345359
definition := orderedmap.New[string, any]()
346360

internal/jennies/terraform/types.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,7 @@ func (formatter *typeFormatter) formatArrayAttributes(def ast.Type) string {
230230
formatter.packageMapper("github.com/hashicorp/terraform-plugin-framework/attr")
231231
defVal = fmt.Sprintf("Default: listdefault.StaticValue(%s),\n", formatter.parseArrayOrMapDefaults(def.AsArray().ValueType, def.Default, ListDefault))
232232
}
233+
arrayValidator := formatter.validators.arrayConstraintValidator(def.AsArray().Constraints)
233234
validator := formatter.validators.validateList(def.AsArray().ValueType)
234235

235236
switch def.AsArray().ValueType.Kind {
@@ -264,7 +265,9 @@ func (formatter *typeFormatter) formatArrayAttributes(def ast.Type) string {
264265
buffer.WriteString(defVal)
265266
}
266267

267-
if validator != "" {
268+
if arrayValidator != "" {
269+
buffer.WriteString(fmt.Sprintf("Validators: %s", arrayValidator))
270+
} else if validator != "" {
268271
buffer.WriteString(fmt.Sprintf("Validators: %s", validator))
269272
}
270273

@@ -278,6 +281,9 @@ func (formatter *typeFormatter) formatArrayAttributes(def ast.Type) string {
278281
if defVal != "" {
279282
buffer.WriteString(defVal)
280283
}
284+
if arrayValidator != "" {
285+
buffer.WriteString(fmt.Sprintf("Validators: %s", arrayValidator))
286+
}
281287
}
282288
buffer.WriteString(fmt.Sprintf("},\n"))
283289

internal/jennies/terraform/validators.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,29 @@ func (v *validators) constraints(validator scalarValidator, constraints []ast.Ty
169169
return buffer.String()
170170
}
171171

172+
func (v *validators) arrayConstraintValidator(constraints []ast.TypeConstraint) string {
173+
if len(constraints) == 0 {
174+
return ""
175+
}
176+
177+
var buffer strings.Builder
178+
v.typeFormatter.packageMapper("github.com/hashicorp/terraform-plugin-framework/schema/validator")
179+
v.typeFormatter.packageMapper("github.com/hashicorp/terraform-plugin-framework-validators/listvalidator")
180+
buffer.WriteString("[]validator.List{\n")
181+
for _, c := range constraints {
182+
switch c.Op {
183+
case ast.MinItemsOp:
184+
buffer.WriteString(fmt.Sprintf("listvalidator.SizeAtLeast(%s),\n", formatScalar(c.Args[0])))
185+
case ast.MaxItemsOp:
186+
buffer.WriteString(fmt.Sprintf("listvalidator.SizeAtMost(%s),\n", formatScalar(c.Args[0])))
187+
case ast.UniqueItemsOp:
188+
buffer.WriteString("listvalidator.UniqueValues(),\n")
189+
}
190+
}
191+
buffer.WriteString("},\n")
192+
return buffer.String()
193+
}
194+
172195
func (v *validators) validateList(def ast.Type) string {
173196
var buffer strings.Builder
174197
switch def.Kind {

internal/simplecue/generator.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -961,6 +961,53 @@ func extractNumber(e cueast.Expr) (string, bool) {
961961
return "", false
962962
}
963963

964+
func (g *generator) declareListConstraints(v cue.Value) ([]ast.TypeConstraint, error) {
965+
conjuncts := appendSplit(nil, cue.AndOp, v)
966+
967+
if len(conjuncts) == 1 {
968+
return nil, nil
969+
}
970+
971+
var constraints []ast.TypeConstraint
972+
973+
for _, conjunct := range conjuncts {
974+
op, args := conjunct.Expr()
975+
if op != cue.CallOp {
976+
continue
977+
}
978+
979+
switch fmt.Sprint(args[0]) {
980+
case "list.MinItems":
981+
scalar, err := cueConcreteToScalar(args[1])
982+
if err != nil {
983+
return nil, err
984+
}
985+
constraints = append(constraints, ast.TypeConstraint{
986+
Op: ast.MinItemsOp,
987+
Args: []any{scalar},
988+
})
989+
990+
case "list.MaxItems":
991+
scalar, err := cueConcreteToScalar(args[1])
992+
if err != nil {
993+
return nil, err
994+
}
995+
constraints = append(constraints, ast.TypeConstraint{
996+
Op: ast.MaxItemsOp,
997+
Args: []any{scalar},
998+
})
999+
1000+
case "list.UniqueItems":
1001+
constraints = append(constraints, ast.TypeConstraint{
1002+
Op: ast.UniqueItemsOp,
1003+
Args: nil,
1004+
})
1005+
}
1006+
}
1007+
1008+
return constraints, nil
1009+
}
1010+
9641011
func (g *generator) declareList(v cue.Value, defVal any, hints ast.JenniesHints) (ast.Type, error) {
9651012
typeDef := ast.NewArray(ast.Any(), ast.Hints(hints), ast.Default(defVal))
9661013

@@ -982,6 +1029,12 @@ func (g *generator) declareList(v cue.Value, defVal any, hints ast.JenniesHints)
9821029
v = dvals[0]
9831030
}
9841031

1032+
constraints, err := g.declareListConstraints(v)
1033+
if err != nil {
1034+
return ast.Type{}, err
1035+
}
1036+
typeDef.Array.Constraints = constraints
1037+
9851038
e := v.LookupPath(cue.MakePath(cue.AnyIndex))
9861039
if !e.Exists() {
9871040
// unreachable?

schemas/compiler_passes.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@
77
"properties": {
88
"value_type": {
99
"$ref": "#/$defs/AstType"
10+
},
11+
"constraints": {
12+
"items": {
13+
"$ref": "#/$defs/AstTypeConstraint"
14+
},
15+
"type": "array"
1016
}
1117
},
1218
"additionalProperties": false,

schemas/pipeline.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -528,6 +528,17 @@
528528
"package_root": {
529529
"type": "string",
530530
"description": "Root path for imports.\nEx: github.com/grafana/cog/generated"
531+
},
532+
"overrides_templates": {
533+
"items": {
534+
"type": "string"
535+
},
536+
"type": "array",
537+
"description": "OverridesTemplatesDirectories holds a list of directories containing templates\ndefining blocks used to override parts of builders/types/...."
538+
},
539+
"skip_post_formatting": {
540+
"type": "boolean",
541+
"description": "SkipPostFormatting disables formatting of Go files done with go imports\nafter code generation."
531542
}
532543
},
533544
"additionalProperties": false,

0 commit comments

Comments
 (0)