cuelang.org/go@v0.13.0/encoding/jsonschema/constraints_object.go (about)

     1  // Copyright 2019 CUE Authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package jsonschema
    16  
    17  import (
    18  	"strings"
    19  
    20  	"cuelang.org/go/cue"
    21  	"cuelang.org/go/cue/ast"
    22  	"cuelang.org/go/cue/token"
    23  	"cuelang.org/go/internal"
    24  )
    25  
    26  // Object constraints
    27  
    28  func constraintPreserveUnknownFields(key string, n cue.Value, s *state) {
    29  	// x-kubernetes-preserve-unknown-fields stops the API server decoding
    30  	// step from pruning fields which are not specified in the validation
    31  	// schema. This affects fields recursively, but switches back to normal
    32  	// pruning behaviour if nested properties or additionalProperties are
    33  	// specified in the schema. This can either be true or undefined. False
    34  	// is forbidden.
    35  	// Note: by experimentation, "nested properties" means "within a schema
    36  	// within a nested property" not "within a schema that has the properties keyword".
    37  	if !s.boolValue(n) {
    38  		s.errf(n, "x-kubernetes-preserve-unknown-fields value may not be false")
    39  		return
    40  	}
    41  	// TODO check that it's specified on an object type. This requires
    42  	// either setting a bool (hasPreserveUnknownFields?) and checking
    43  	// later or making a new phase and placing this after "type" but
    44  	// before "allOf", because it's important that this value be
    45  	// passed down recursively to allOf and friends.
    46  	s.preserveUnknownFields = true
    47  }
    48  
    49  func constraintGroupVersionKind(key string, n cue.Value, s *state) {
    50  	// x-kubernetes-group-version-kind is used by Kubernetes schemas
    51  	// to indicate the required values of the apiVersion and kind fields.
    52  	items := s.listItems(key, n, false)
    53  	if len(items) != 1 {
    54  		// When there's more than one item, we _could_ generate
    55  		// a disjunction over apiVersion and kind but for now, we'll
    56  		// just ignore it.
    57  		// TODO implement support for multiple items
    58  		return
    59  	}
    60  	var group, version string
    61  	s.processMap(items[0], func(key string, n cue.Value) {
    62  		if strings.HasPrefix(key, "x-") {
    63  			// TODO are x- extension properties actually allowed in this context?
    64  			return
    65  		}
    66  		switch key {
    67  		case "group":
    68  			group, _ = s.strValue(n)
    69  		case "kind":
    70  			s.k8sResourceKind, _ = s.strValue(n)
    71  		case "version":
    72  			version, _ = s.strValue(n)
    73  		default:
    74  			s.errf(n, "unknown field %q in x-kubernetes-group-version-kind item", key)
    75  		}
    76  	})
    77  	if s.k8sResourceKind == "" || version == "" {
    78  		s.errf(n, "x-kubernetes-group-version-kind needs both kind and version fields")
    79  	}
    80  	if group == "" {
    81  		s.k8sAPIVersion = version
    82  	} else {
    83  		s.k8sAPIVersion = group + "/" + version
    84  	}
    85  }
    86  
    87  func constraintAdditionalProperties(key string, n cue.Value, s *state) {
    88  	switch n.Kind() {
    89  	case cue.BoolKind:
    90  		if s.boolValue(n) {
    91  			s.openness = explicitlyOpen
    92  		} else {
    93  			if s.schemaVersion == VersionKubernetesCRD {
    94  				s.errf(n, "additionalProperties may not be set to false in a CRD schema")
    95  				return
    96  			}
    97  			s.openness = explicitlyClosed
    98  		}
    99  		_ = s.object(n)
   100  
   101  	case cue.StructKind:
   102  		obj := s.object(n)
   103  		if len(obj.Elts) == 0 {
   104  			obj.Elts = append(obj.Elts, &ast.Field{
   105  				Label: ast.NewList(ast.NewIdent("string")),
   106  				Value: s.schema(n),
   107  			})
   108  			s.openness = allFieldsCovered
   109  			return
   110  		}
   111  		// [!~(properties|patternProperties)]: schema
   112  		existing := append(s.patterns, excludeFields(obj.Elts)...)
   113  		expr, _ := s.schemaState(n, allTypes, func(s *state) {
   114  			s.preserveUnknownFields = false
   115  		})
   116  		f := internal.EmbedStruct(ast.NewStruct(&ast.Field{
   117  			Label: ast.NewList(ast.NewBinExpr(token.AND, existing...)),
   118  			Value: expr,
   119  		}))
   120  		obj.Elts = append(obj.Elts, f)
   121  		s.openness = allFieldsCovered
   122  
   123  	default:
   124  		s.errf(n, `value of "additionalProperties" must be an object or boolean`)
   125  		return
   126  	}
   127  	s.hasAdditionalProperties = true
   128  }
   129  
   130  func constraintDependencies(key string, n cue.Value, s *state) {
   131  	// Schema and property dependencies.
   132  	// TODO: the easiest implementation is with comprehensions.
   133  	// The nicer implementation is with disjunctions. This has to be done
   134  	// at the very end, replacing properties.
   135  	/*
   136  		*{ property?: _|_ } | {
   137  			property: _
   138  			schema
   139  		}
   140  	*/
   141  }
   142  
   143  func constraintMaxProperties(key string, n cue.Value, s *state) {
   144  	pkg := s.addImport(n, "struct")
   145  	x := ast.NewCall(ast.NewSel(pkg, "MaxFields"), s.uint(n))
   146  	s.add(n, objectType, x)
   147  }
   148  
   149  func constraintMinProperties(key string, n cue.Value, s *state) {
   150  	pkg := s.addImport(n, "struct")
   151  	x := ast.NewCall(ast.NewSel(pkg, "MinFields"), s.uint(n))
   152  	s.add(n, objectType, x)
   153  }
   154  
   155  func constraintPatternProperties(key string, n cue.Value, s *state) {
   156  	if n.Kind() != cue.StructKind {
   157  		s.errf(n, `value of "patternProperties" must be an object, found %v`, n.Kind())
   158  	}
   159  	obj := s.object(n)
   160  	existing := excludeFields(s.obj.Elts)
   161  	s.processMap(n, func(key string, n cue.Value) {
   162  		if !s.checkRegexp(n, key) {
   163  			return
   164  		}
   165  
   166  		// Record the pattern for potential use by
   167  		// additionalProperties because patternProperties are
   168  		// considered before additionalProperties.
   169  		s.patterns = append(s.patterns,
   170  			&ast.UnaryExpr{Op: token.NMAT, X: ast.NewString(key)})
   171  
   172  		// We'll make a pattern constraint of the form:
   173  		// 	[pattern & !~(properties)]: schema
   174  		f := internal.EmbedStruct(ast.NewStruct(&ast.Field{
   175  			Label: ast.NewList(ast.NewBinExpr(
   176  				token.AND,
   177  				append([]ast.Expr{&ast.UnaryExpr{Op: token.MAT, X: ast.NewString(key)}}, existing...)...,
   178  			)),
   179  			Value: s.schema(n),
   180  		}))
   181  		ast.SetRelPos(f, token.NewSection)
   182  		obj.Elts = append(obj.Elts, f)
   183  	})
   184  }
   185  
   186  func constraintEmbeddedResource(key string, n cue.Value, s *state) {
   187  	// TODO:
   188  	// - should fail if type has not been specified as "object"
   189  	// - should fail if neither x-kubernetes-preserve-unknown-fields or properties have been specified
   190  
   191  	// Note: this runs in a phase before the properties keyword so
   192  	// that the embedded expression always comes first in the struct
   193  	// literal.
   194  	resourceDefinitionPath := cue.MakePath(cue.Hid("_embeddedResource", "_"))
   195  	obj := s.object(n)
   196  
   197  	// Generate a reference to a shared schema that all embedded resources
   198  	// can share. If it already exists, that's fine.
   199  	// TODO add an attribute to make it clear what's going on here
   200  	// when encoding a CRD from CUE?
   201  	s.builder.put(resourceDefinitionPath, ast.NewStruct(
   202  		"apiVersion", token.NOT, ast.NewIdent("string"),
   203  		"kind", token.NOT, ast.NewIdent("string"),
   204  		"metadata", token.OPTION, ast.NewStruct(&ast.Ellipsis{}),
   205  	), nil)
   206  	refExpr, err := s.builder.getRef(resourceDefinitionPath)
   207  	if err != nil {
   208  		s.errf(n, `cannot get reference to embedded resource definition: %v`, err)
   209  	} else {
   210  		obj.Elts = append(obj.Elts, &ast.EmbedDecl{
   211  			Expr: refExpr,
   212  		})
   213  	}
   214  	s.allowedTypes &= cue.StructKind
   215  }
   216  
   217  func constraintProperties(key string, n cue.Value, s *state) {
   218  	obj := s.object(n)
   219  
   220  	if n.Kind() != cue.StructKind {
   221  		s.errf(n, `"properties" expected an object, found %v`, n.Kind())
   222  	}
   223  	hasKind := false
   224  	hasAPIVersion := false
   225  	s.processMap(n, func(key string, n cue.Value) {
   226  		// property?: value
   227  		name := ast.NewString(key)
   228  		expr, state := s.schemaState(n, allTypes, func(s *state) {
   229  			s.preserveUnknownFields = false
   230  		})
   231  		f := &ast.Field{Label: name, Value: expr}
   232  		if doc := state.comment(); doc != nil {
   233  			ast.SetComments(f, []*ast.CommentGroup{doc})
   234  		}
   235  		f.Constraint = token.OPTION
   236  		if s.k8sResourceKind != "" && key == "kind" {
   237  			// Define a regular field with the specified kind value.
   238  			f.Constraint = token.ILLEGAL
   239  			f.Value = ast.NewString(s.k8sResourceKind)
   240  			hasKind = true
   241  		}
   242  		if s.k8sAPIVersion != "" && key == "apiVersion" {
   243  			// Define a regular field with the specified value.
   244  			f.Constraint = token.ILLEGAL
   245  			f.Value = ast.NewString(s.k8sAPIVersion)
   246  			hasAPIVersion = true
   247  		}
   248  		if len(obj.Elts) > 0 && len(f.Comments()) > 0 {
   249  			// TODO: change formatter such that either a NewSection on the
   250  			// field or doc comment will cause a new section.
   251  			ast.SetRelPos(f.Comments()[0], token.NewSection)
   252  		}
   253  		if state.deprecated {
   254  			switch expr.(type) {
   255  			case *ast.StructLit:
   256  				obj.Elts = append(obj.Elts, addTag(name, "deprecated", ""))
   257  			default:
   258  				f.Attrs = append(f.Attrs, internal.NewAttr("deprecated", ""))
   259  			}
   260  		}
   261  		obj.Elts = append(obj.Elts, f)
   262  	})
   263  	// It's not entirely clear whether it's OK to have an x-kubernetes-group-version-kind
   264  	// keyword without the kind and apiVersion properties but be defensive
   265  	// and add them anyway even if they're not there already.
   266  	if s.k8sAPIVersion != "" && !hasAPIVersion {
   267  		obj.Elts = append(obj.Elts, &ast.Field{
   268  			Label: ast.NewString("apiVersion"),
   269  			Value: ast.NewString(s.k8sAPIVersion),
   270  		})
   271  	}
   272  	if s.k8sResourceKind != "" && !hasKind {
   273  		obj.Elts = append(obj.Elts, &ast.Field{
   274  			Label: ast.NewString("kind"),
   275  			Value: ast.NewString(s.k8sResourceKind),
   276  		})
   277  	}
   278  	s.hasProperties = true
   279  }
   280  
   281  func constraintPropertyNames(key string, n cue.Value, s *state) {
   282  	// [=~pattern]: _
   283  	if names, _ := s.schemaState(n, cue.StringKind, nil); !isTop(names) {
   284  		x := ast.NewStruct(ast.NewList(names), top())
   285  		s.add(n, objectType, x)
   286  	}
   287  }
   288  
   289  func constraintRequired(key string, n cue.Value, s *state) {
   290  	if n.Kind() != cue.ListKind {
   291  		s.errf(n, `value of "required" must be list of strings, found %v`, n.Kind())
   292  		return
   293  	}
   294  
   295  	obj := s.object(n)
   296  
   297  	// Create field map
   298  	fields := map[string]*ast.Field{}
   299  	for _, d := range obj.Elts {
   300  		f, ok := d.(*ast.Field)
   301  		if !ok {
   302  			continue // Could be embedding? See cirrus.json
   303  		}
   304  		str, _, err := ast.LabelName(f.Label)
   305  		if err == nil {
   306  			fields[str] = f
   307  		}
   308  	}
   309  
   310  	for _, n := range s.listItems("required", n, true) {
   311  		str, ok := s.strValue(n)
   312  		f := fields[str]
   313  		if f == nil && ok {
   314  			f := &ast.Field{
   315  				Label:      ast.NewString(str),
   316  				Value:      top(),
   317  				Constraint: token.NOT,
   318  			}
   319  			fields[str] = f
   320  			obj.Elts = append(obj.Elts, f)
   321  			continue
   322  		}
   323  		if f.Constraint == token.NOT {
   324  			s.errf(n, "duplicate required field %q", str)
   325  		}
   326  		f.Constraint = token.NOT
   327  	}
   328  }