cuelang.org/go@v0.13.0/encoding/jsonschema/constraints_generic.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  	"errors"
    19  	"fmt"
    20  	"net/url"
    21  	"strings"
    22  
    23  	"cuelang.org/go/cue"
    24  	"cuelang.org/go/cue/ast"
    25  	"cuelang.org/go/cue/token"
    26  )
    27  
    28  // Generic constraints
    29  
    30  func constraintAddDefinitions(key string, n cue.Value, s *state) {
    31  	if n.Kind() != cue.StructKind {
    32  		s.errf(n, `%q expected an object, found %s`, key, n.Kind())
    33  	}
    34  
    35  	s.processMap(n, func(key string, n cue.Value) {
    36  		// Ensure that we are going to make a definition
    37  		// for this node.
    38  		s.ensureDefinition(n)
    39  		s.schema(n)
    40  	})
    41  }
    42  
    43  func constraintComment(key string, n cue.Value, s *state) {
    44  }
    45  
    46  func constraintConst(key string, n cue.Value, s *state) {
    47  	s.all.add(n, s.constValue(n))
    48  	s.allowedTypes &= n.Kind()
    49  	s.knownTypes &= n.Kind()
    50  }
    51  
    52  func constraintDefault(key string, n cue.Value, s *state) {
    53  	// TODO make the default value available in a separate
    54  	// template-like CUE value outside of the usual schema output.
    55  }
    56  
    57  func constraintDeprecated(key string, n cue.Value, s *state) {
    58  	if s.boolValue(n) {
    59  		s.deprecated = true
    60  	}
    61  }
    62  
    63  func constraintDescription(key string, n cue.Value, s *state) {
    64  	s.description, _ = s.strValue(n)
    65  }
    66  
    67  func constraintEnum(key string, n cue.Value, s *state) {
    68  	var a []ast.Expr
    69  	var types cue.Kind
    70  	for _, x := range s.listItems("enum", n, true) {
    71  		if (s.allowedTypes & x.Kind()) == 0 {
    72  			// Enum value is redundant because it's
    73  			// not in the allowed type set.
    74  			continue
    75  		}
    76  		a = append(a, s.constValue(x))
    77  		types |= x.Kind()
    78  	}
    79  	s.knownTypes &= types
    80  	s.allowedTypes &= types
    81  	if len(a) > 0 {
    82  		s.all.add(n, ast.NewBinExpr(token.OR, a...))
    83  	}
    84  }
    85  
    86  func constraintExamples(key string, n cue.Value, s *state) {
    87  	if n.Kind() != cue.ListKind {
    88  		s.errf(n, `value of "examples" must be an array, found %v`, n.Kind())
    89  	}
    90  }
    91  
    92  func constraintNullable(key string, n cue.Value, s *state) {
    93  	null := ast.NewNull()
    94  	setPos(null, n)
    95  	s.nullable = null
    96  }
    97  
    98  func constraintRef(key string, n cue.Value, s *state) {
    99  	u := s.resolveURI(n)
   100  	if u == nil {
   101  		return
   102  	}
   103  	schemaRoot := s.schemaRoot()
   104  	if u.Fragment == "" && schemaRoot.isRoot && sameSchemaRoot(u, schemaRoot.id) {
   105  		// It's a reference to the root of the schema being
   106  		// generated. This never maps to something different.
   107  		s.all.add(n, s.refExpr(n, "", cue.Path{}))
   108  		return
   109  	}
   110  	importPath, path, err := cueLocationForRef(s, n, u, schemaRoot)
   111  	if err != nil {
   112  		s.errf(n, "%v", err)
   113  		return
   114  	}
   115  	if e := s.refExpr(n, importPath, path); e != nil {
   116  		s.all.add(n, e)
   117  	}
   118  }
   119  
   120  func cueLocationForRef(s *state, n cue.Value, u *url.URL, schemaRoot *state) (importPath string, path cue.Path, err error) {
   121  	if ds, ok := s.defs[u.String()]; ok {
   122  		// We already know about the schema, so use the information that's stored for it.
   123  		return ds.importPath, ds.path, nil
   124  	}
   125  	loc := SchemaLoc{
   126  		ID: u,
   127  	}
   128  	var base cue.Value
   129  	isAnchor := u.Fragment != "" && !strings.HasPrefix(u.Fragment, "/")
   130  	if !isAnchor {
   131  		// It's a JSON pointer reference.
   132  		if sameSchemaRoot(u, s.rootID) {
   133  			base = s.root
   134  		} else if sameSchemaRoot(u, schemaRoot.id) {
   135  			// it's within the current schema.
   136  			base = schemaRoot.pos
   137  		}
   138  		if base.Exists() {
   139  			target, err := lookupJSONPointer(schemaRoot.pos, u.Fragment)
   140  			if err != nil {
   141  				if errors.Is(err, errRefNotFound) {
   142  					return "", cue.Path{}, fmt.Errorf("reference to non-existent schema")
   143  				}
   144  				return "", cue.Path{}, fmt.Errorf("invalid JSON Pointer: %v", err)
   145  			}
   146  			if ds := s.defForValue.get(target); ds != nil {
   147  				// There's a definition in place for the value, which gives
   148  				// us our answer.
   149  				return ds.importPath, ds.path, nil
   150  			}
   151  			s.ensureDefinition(target)
   152  			loc.IsLocal = true
   153  			loc.Path = relPath(target, s.root)
   154  		}
   155  	}
   156  	importPath, path, err = s.cfg.MapRef(loc)
   157  	if err != nil {
   158  		return "", cue.Path{}, fmt.Errorf("cannot determine CUE location for JSON Schema location %v: %v", loc, err)
   159  	}
   160  	// TODO we'd quite like to avoid invoking MapRef many times
   161  	// for the same reference, but in general we don't necessily know
   162  	// the canonical URI of the schema until we've done at least one pass.
   163  	// There are potentially ways to do it, but leave it for now in favor
   164  	// of simplicity.
   165  	return importPath, path, nil
   166  }
   167  
   168  func constraintTitle(key string, n cue.Value, s *state) {
   169  	s.title, _ = s.strValue(n)
   170  }
   171  
   172  func constraintIntOrString(key string, n cue.Value, s *state) {
   173  	// See x-kubernetes-int-or-string in
   174  	// https://kubernetes.io/docs/reference/kubernetes-api/extend-resources/custom-resource-definition-v1/#JSONSchemaProps.
   175  	s.setTypeUsed(n, stringType)
   176  	s.setTypeUsed(n, numType)
   177  	s.add(n, numType, ast.NewIdent("int"))
   178  	s.allowedTypes &= cue.StringKind | cue.IntKind
   179  }
   180  
   181  func constraintType(key string, n cue.Value, s *state) {
   182  	var types cue.Kind
   183  	set := func(n cue.Value) {
   184  		str, ok := s.strValue(n)
   185  		if !ok {
   186  			s.errf(n, "type value should be a string")
   187  		}
   188  		switch str {
   189  		case "null":
   190  			types |= cue.NullKind
   191  			s.setTypeUsed(n, nullType)
   192  			// TODO: handle OpenAPI restrictions.
   193  		case "boolean":
   194  			types |= cue.BoolKind
   195  			s.setTypeUsed(n, boolType)
   196  		case "string":
   197  			types |= cue.StringKind
   198  			s.setTypeUsed(n, stringType)
   199  		case "number":
   200  			types |= cue.NumberKind
   201  			s.setTypeUsed(n, numType)
   202  		case "integer":
   203  			types |= cue.IntKind
   204  			s.setTypeUsed(n, numType)
   205  			s.add(n, numType, ast.NewIdent("int"))
   206  		case "array":
   207  			types |= cue.ListKind
   208  			s.setTypeUsed(n, arrayType)
   209  			// For OpenAPI, specifically keep track of whether type is array
   210  			// so we can mandate the "items" keyword.
   211  			s.isArray = true
   212  		case "object":
   213  			types |= cue.StructKind
   214  			s.setTypeUsed(n, objectType)
   215  
   216  		default:
   217  			s.errf(n, "unknown type %q", n)
   218  		}
   219  	}
   220  
   221  	switch n.Kind() {
   222  	case cue.StringKind:
   223  		set(n)
   224  	case cue.ListKind:
   225  		if openAPILike.contains(s.schemaVersion) {
   226  			// From https://spec.openapis.org/oas/v3.0.3.html#properties:
   227  			// "Value MUST be a string. Multiple types via an array are not supported."
   228  			s.errf(n, `value of "type" must be a string in %v`, s.schemaVersion)
   229  			return
   230  		}
   231  		for i, _ := n.List(); i.Next(); {
   232  			set(i.Value())
   233  		}
   234  	default:
   235  		s.errf(n, `value of "type" must be a string or list of strings`)
   236  	}
   237  
   238  	s.allowedTypes &= types
   239  }