cuelang.org/go@v0.13.0/encoding/jsonschema/constraints_combinator.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  	"strconv"
    19  
    20  	"cuelang.org/go/cue"
    21  	"cuelang.org/go/cue/ast"
    22  	"cuelang.org/go/cue/token"
    23  )
    24  
    25  // Constraint combinators.
    26  
    27  func constraintAllOf(key string, n cue.Value, s *state) {
    28  	var knownTypes cue.Kind
    29  	items := s.listItems("allOf", n, false)
    30  	if len(items) == 0 {
    31  		s.errf(n, "allOf requires at least one subschema")
    32  		return
    33  	}
    34  	a := make([]ast.Expr, 0, len(items))
    35  	for _, v := range items {
    36  		x, sub := s.schemaState(v, s.allowedTypes, nil)
    37  		s.allowedTypes &= sub.allowedTypes
    38  		if sub.hasConstraints {
    39  			// This might seem a little odd, since the actual
    40  			// types are the intersection of the known types
    41  			// of the allOf members. However, knownTypes
    42  			// is really there to avoid adding redundant disjunctions.
    43  			// So if we have (int & string) & (disjunction)
    44  			// we definitely don't have to add int or string to
    45  			// disjunction.
    46  			knownTypes |= sub.knownTypes
    47  			a = append(a, x)
    48  		}
    49  	}
    50  	// TODO maybe give an error/warning if s.allowedTypes == 0
    51  	// as that's a known-impossible assertion?
    52  	if len(a) > 0 {
    53  		s.knownTypes &= knownTypes
    54  		if len(a) == 1 {
    55  			// Only one possibility. Use that.
    56  			s.all.add(n, a[0])
    57  			return
    58  		}
    59  		s.all.add(n, ast.NewCall(
    60  			ast.NewIdent("matchN"),
    61  			// TODO it would be nice to be able to use a special sentinel "all" value
    62  			// here rather than redundantly encoding the length of the list.
    63  			&ast.BasicLit{
    64  				Kind:  token.INT,
    65  				Value: strconv.Itoa(len(items)),
    66  			},
    67  			ast.NewList(a...),
    68  		))
    69  	}
    70  }
    71  
    72  func constraintAnyOf(key string, n cue.Value, s *state) {
    73  	var types cue.Kind
    74  	var knownTypes cue.Kind
    75  	items := s.listItems("anyOf", n, false)
    76  	if len(items) == 0 {
    77  		s.errf(n, "anyOf requires at least one subschema")
    78  		return
    79  	}
    80  	a := make([]ast.Expr, 0, len(items))
    81  	for _, v := range items {
    82  		x, sub := s.schemaState(v, s.allowedTypes, nil)
    83  		if sub.allowedTypes == 0 {
    84  			// Nothing is allowed; omit.
    85  			continue
    86  		}
    87  		types |= sub.allowedTypes
    88  		knownTypes |= sub.knownTypes
    89  		a = append(a, x)
    90  	}
    91  	if len(a) == 0 {
    92  		// Nothing at all is allowed.
    93  		s.allowedTypes = 0
    94  		return
    95  	}
    96  	if len(a) == 1 {
    97  		s.all.add(n, a[0])
    98  		return
    99  	}
   100  	s.allowedTypes &= types
   101  	s.knownTypes &= knownTypes
   102  	s.all.add(n, ast.NewCall(
   103  		ast.NewIdent("matchN"),
   104  		&ast.UnaryExpr{
   105  			Op: token.GEQ,
   106  			X: &ast.BasicLit{
   107  				Kind:  token.INT,
   108  				Value: "1",
   109  			},
   110  		},
   111  		ast.NewList(a...),
   112  	))
   113  }
   114  
   115  func constraintOneOf(key string, n cue.Value, s *state) {
   116  	var types cue.Kind
   117  	var knownTypes cue.Kind
   118  	needsConstraint := false
   119  	items := s.listItems("oneOf", n, false)
   120  	if len(items) == 0 {
   121  		s.errf(n, "oneOf requires at least one subschema")
   122  		return
   123  	}
   124  	a := make([]ast.Expr, 0, len(items))
   125  	for _, v := range items {
   126  		x, sub := s.schemaState(v, s.allowedTypes, nil)
   127  		if sub.allowedTypes == 0 {
   128  			// Nothing is allowed; omit
   129  			continue
   130  		}
   131  
   132  		// TODO: make more finegrained by making it two pass.
   133  		if sub.hasConstraints {
   134  			needsConstraint = true
   135  		} else if (types & sub.allowedTypes) != 0 {
   136  			// If there's overlap between the unconstrained elements,
   137  			// we'll definitely need to add a constraint.
   138  			needsConstraint = true
   139  		}
   140  		types |= sub.allowedTypes
   141  		knownTypes |= sub.knownTypes
   142  		a = append(a, x)
   143  	}
   144  	// TODO if there are no elements in the oneOf, validation
   145  	// should fail.
   146  	s.allowedTypes &= types
   147  	if len(a) > 0 && needsConstraint {
   148  		s.knownTypes &= knownTypes
   149  		if len(a) == 1 {
   150  			// Only one possibility. Use that.
   151  			s.all.add(n, a[0])
   152  			return
   153  		}
   154  		s.all.add(n, ast.NewCall(
   155  			ast.NewIdent("matchN"),
   156  			&ast.BasicLit{
   157  				Kind:  token.INT,
   158  				Value: "1",
   159  			},
   160  			ast.NewList(a...),
   161  		))
   162  	}
   163  
   164  	// TODO: oneOf({a:x}, {b:y}, ..., not(anyOf({a:x}, {b:y}, ...))),
   165  	// can be translated to {} | {a:x}, {b:y}, ...
   166  }
   167  
   168  func constraintNot(key string, n cue.Value, s *state) {
   169  	subSchema := s.schema(n)
   170  	s.all.add(n, ast.NewCall(
   171  		ast.NewIdent("matchN"),
   172  		&ast.BasicLit{
   173  			Kind:  token.INT,
   174  			Value: "0",
   175  		},
   176  		ast.NewList(subSchema),
   177  	))
   178  }
   179  
   180  func constraintIf(key string, n cue.Value, s *state) {
   181  	s.ifConstraint = n
   182  }
   183  
   184  func constraintThen(key string, n cue.Value, s *state) {
   185  	s.thenConstraint = n
   186  }
   187  
   188  func constraintElse(key string, n cue.Value, s *state) {
   189  	s.elseConstraint = n
   190  }
   191  
   192  // constraintIfThenElse is not implemented as a standard constraint
   193  // function because it needs to operate knowing about the presence
   194  // of all of "if", "then" and "else".
   195  func constraintIfThenElse(s *state) {
   196  	hasIf, hasThen, hasElse := s.ifConstraint.Exists(), s.thenConstraint.Exists(), s.elseConstraint.Exists()
   197  	if !hasIf || (!hasThen && !hasElse) {
   198  		return
   199  	}
   200  	var ifExpr, thenExpr, elseExpr ast.Expr
   201  	ifExpr, ifSub := s.schemaState(s.ifConstraint, s.allowedTypes, nil)
   202  	if hasThen {
   203  		// The allowed types of the "then" constraint are constrained both
   204  		// by the current constraints and the "if" constraint.
   205  		thenExpr, _ = s.schemaState(s.thenConstraint, s.allowedTypes&ifSub.allowedTypes, nil)
   206  	}
   207  	if hasElse {
   208  		elseExpr, _ = s.schemaState(s.elseConstraint, s.allowedTypes, nil)
   209  	}
   210  	if thenExpr == nil {
   211  		thenExpr = top()
   212  	}
   213  	if elseExpr == nil {
   214  		elseExpr = top()
   215  	}
   216  	s.all.add(s.pos, ast.NewCall(
   217  		ast.NewIdent("matchIf"),
   218  		ifExpr,
   219  		thenExpr,
   220  		elseExpr,
   221  	))
   222  }