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 }