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

     1  // Copyright 2024 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  	"fmt"
    19  	"slices"
    20  	"strconv"
    21  	"strings"
    22  
    23  	"cuelang.org/go/cue"
    24  	"cuelang.org/go/cue/ast"
    25  	"cuelang.org/go/cue/token"
    26  )
    27  
    28  // TODO a bunch of stuff in this file is potentially suitable
    29  // for more general use. Consider moving some of it
    30  // to the cue package.
    31  
    32  func pathConcat(p1, p2 cue.Path) cue.Path {
    33  	sels1, sels2 := p1.Selectors(), p2.Selectors()
    34  	if len(sels1) == 0 {
    35  		return p2
    36  	}
    37  	if len(sels2) == 0 {
    38  		return p1
    39  	}
    40  	return cue.MakePath(slices.Concat(sels1, sels2)...)
    41  }
    42  
    43  func labelsToCUEPath(labels []ast.Label) (cue.Path, error) {
    44  	sels := make([]cue.Selector, len(labels))
    45  	for i, label := range labels {
    46  		// Note: we can't use cue.Label because that doesn't
    47  		// allow hidden fields.
    48  		sels[i] = selectorForLabel(label)
    49  	}
    50  	path := cue.MakePath(sels...)
    51  	if err := path.Err(); err != nil {
    52  		return cue.Path{}, err
    53  	}
    54  	return path, nil
    55  }
    56  
    57  // selectorForLabel is like [cue.Label] except that it allows
    58  // hidden fields, which aren't allowed there because technically
    59  // we can't work out what package to associate with the resulting
    60  // selector. In our case we always imply the local package so
    61  // we don't mind about that.
    62  func selectorForLabel(label ast.Label) cue.Selector {
    63  	if label, _ := label.(*ast.Ident); label != nil && strings.HasPrefix(label.Name, "_") {
    64  		return cue.Hid(label.Name, "_")
    65  	}
    66  	return cue.Label(label)
    67  }
    68  
    69  // pathRefSyntax returns the syntax for an expression which
    70  // looks up the path inside the given root expression's value.
    71  // It returns an error if the path contains any elements with
    72  // type [cue.OptionalConstraint], [cue.RequiredConstraint], or [cue.PatternConstraint],
    73  // none of which are expressible as a CUE index expression.
    74  //
    75  // TODO implement this properly and move to a method on [cue.Path].
    76  func pathRefSyntax(cuePath cue.Path, root ast.Expr) (ast.Expr, error) {
    77  	expr := root
    78  	for _, sel := range cuePath.Selectors() {
    79  		if sel.LabelType() == cue.IndexLabel {
    80  			expr = &ast.IndexExpr{
    81  				X: expr,
    82  				Index: &ast.BasicLit{
    83  					Kind:  token.INT,
    84  					Value: sel.String(),
    85  				},
    86  			}
    87  		} else {
    88  			lab, err := labelForSelector(sel)
    89  			if err != nil {
    90  				return nil, err
    91  			}
    92  			expr = &ast.SelectorExpr{
    93  				X:   expr,
    94  				Sel: lab,
    95  			}
    96  		}
    97  	}
    98  	return expr, nil
    99  }
   100  
   101  // exprAtPath returns an expression that places the given
   102  // expression at the given path.
   103  // For example:
   104  //
   105  //	declAtPath(cue.ParsePath("a.b.#c"), ast.NewIdent("foo"))
   106  //
   107  // would result in the declaration:
   108  //
   109  //	a: b: #c: foo
   110  //
   111  // TODO this is potentially generally useful. It could
   112  // be exposed as a method on [cue.Path], say
   113  // `SyntaxForDefinition` or something.
   114  func exprAtPath(path cue.Path, expr ast.Expr) (ast.Expr, error) {
   115  	for i, sel := range slices.Backward(path.Selectors()) {
   116  		label, err := labelForSelector(sel)
   117  		if err != nil {
   118  			return nil, err
   119  		}
   120  		// A StructLit is inlined if both:
   121  		// - the Lbrace position is invalid
   122  		// - the Label position is valid.
   123  		rel := token.Blank
   124  		if i == 0 {
   125  			rel = token.Newline
   126  		}
   127  		ast.SetPos(label, token.NoPos.WithRel(rel))
   128  		expr = &ast.StructLit{
   129  			Elts: []ast.Decl{
   130  				&ast.Field{
   131  					Label: label,
   132  					Value: expr,
   133  				},
   134  			},
   135  		}
   136  	}
   137  	return expr, nil
   138  }
   139  
   140  // TODO define this as a Label method on cue.Selector?
   141  func labelForSelector(sel cue.Selector) (ast.Label, error) {
   142  	switch sel.LabelType() {
   143  	case cue.StringLabel, cue.DefinitionLabel, cue.HiddenLabel, cue.HiddenDefinitionLabel:
   144  		str := sel.String()
   145  		switch {
   146  		case strings.HasPrefix(str, `"`):
   147  			// It's quoted for a reason, so maintain the quotes.
   148  			return &ast.BasicLit{
   149  				Kind:  token.STRING,
   150  				Value: str,
   151  			}, nil
   152  		case ast.IsValidIdent(str):
   153  			return ast.NewIdent(str), nil
   154  		}
   155  		// Should never happen.
   156  		return nil, fmt.Errorf("cannot form expression for selector %q", sel)
   157  	default:
   158  		return nil, fmt.Errorf("cannot form label for selector %q with type %v", sel, sel.LabelType())
   159  	}
   160  }
   161  
   162  func cuePathToJSONPointer(p cue.Path) string {
   163  	return jsonPointerFromTokens(func(yield func(s string) bool) {
   164  		for _, sel := range p.Selectors() {
   165  			var token string
   166  			switch sel.Type() {
   167  			case cue.StringLabel:
   168  				token = sel.Unquoted()
   169  			case cue.IndexLabel:
   170  				token = strconv.Itoa(sel.Index())
   171  			default:
   172  				panic(fmt.Errorf("cannot convert selector %v to JSON pointer", sel))
   173  			}
   174  			if !yield(token) {
   175  				return
   176  			}
   177  		}
   178  	})
   179  }
   180  
   181  // relPath returns the path to v relative to root,
   182  // which must be a direct ancestor of v.
   183  func relPath(v, root cue.Value) cue.Path {
   184  	rootPath := root.Path().Selectors()
   185  	vPath := v.Path().Selectors()
   186  	if !sliceHasPrefix(vPath, rootPath) {
   187  		panic("value is not inside root")
   188  	}
   189  	return cue.MakePath(vPath[len(rootPath):]...)
   190  }
   191  
   192  func sliceHasPrefix[E comparable](s1, s2 []E) bool {
   193  	if len(s2) > len(s1) {
   194  		return false
   195  	}
   196  	return slices.Equal(s1[:len(s2)], s2)
   197  }