this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

encoding/jsonschema: add structBuilder type

This type plays a key role in the upcoming jsonschema
refactoring: it moves the generation of the final syntax
from an ad-hoc approach to a more general approach
that allows placing a given piece of syntax anywhere
in the final result.

Even though it's not exported, the functionality
stands alone and could potentially be moved into
another package in time.

Signed-off-by: Roger Peppe <rogpeppe@gmail.com>
Change-Id: I68054a40d420ed1b27a887ee16395d15e21c97ee
Reviewed-on: https://review.gerrithub.io/c/cue-lang/cue/+/1205772
Reviewed-by: Daniel Martí <mvdan@mvdan.cc>
Unity-Result: CUE porcuepine <cue.porcuepine@gmail.com>
TryBot-Result: CUEcueckoo <cueckoo@cuelang.org>

+562 -27
-27
encoding/jsonschema/ref.go
··· 428 428 } 429 429 return u.Host + p, cue.Path{}, nil 430 430 } 431 - 432 - // pathRefSyntax returns the syntax for an expression which 433 - // looks up the path inside the given root expression's value. 434 - // It returns an error if the path contains any elements with 435 - // type [cue.OptionalConstraint], [cue.RequiredConstraint], or [cue.PatternConstraint], 436 - // none of which are expressible as a CUE index expression. 437 - // 438 - // TODO implement this properly and move to a method on [cue.Path]. 439 - func pathRefSyntax(cuePath cue.Path, root ast.Expr) (ast.Expr, error) { 440 - expr := root 441 - for _, sel := range cuePath.Selectors() { 442 - switch sel.LabelType() { 443 - case cue.StringLabel, cue.DefinitionLabel: 444 - ident := sel.String() 445 - if !ast.IsValidIdent(ident) { 446 - return nil, fmt.Errorf("cannot form expression for path %q", cuePath) 447 - } 448 - expr = &ast.SelectorExpr{ 449 - X: expr, 450 - Sel: ast.NewIdent(sel.String()), 451 - } 452 - default: 453 - return nil, fmt.Errorf("cannot form expression for path %q", cuePath) 454 - } 455 - } 456 - return expr, nil 457 - }
+281
encoding/jsonschema/structbuilder.go
··· 1 + package jsonschema 2 + 3 + import ( 4 + "cmp" 5 + "fmt" 6 + 7 + "cuelang.org/go/cue" 8 + "cuelang.org/go/cue/ast" 9 + "cuelang.org/go/cue/token" 10 + ) 11 + 12 + // structBuilder builds a struct value incrementally by 13 + // putting values for its component paths. 14 + // The [structBuilder.getRef] method can be used 15 + // to obtain reliable references into the resulting struct. 16 + type structBuilder struct { 17 + root structBuilderNode 18 + 19 + // refIdents records all the identifiers that refer to entries 20 + // at the top level of the struct, keyed by the selector 21 + // they're referring to. 22 + // 23 + // The [Ident.Node] field needs to refer to the field value rather 24 + // than the field label, and we don't know that until the syntax 25 + // method has been invoked, so we fix up the [Ident.Node] fields when 26 + // that happens. 27 + refIdents map[cue.Selector][]*ast.Ident 28 + 29 + // rootRefIdents is like refIdents but for references to the 30 + // struct root itself. 31 + rootRefIdents []*ast.Ident 32 + } 33 + 34 + // structBuilderNode represents one node in the tree of values 35 + // being built. 36 + type structBuilderNode struct { 37 + // value holds the value associated with the node, if any. 38 + // This does not include entries added underneath it by 39 + // [structBuilder.put]. 40 + value ast.Expr 41 + 42 + // comment holds any doc comment associated with the value. 43 + comment *ast.CommentGroup 44 + 45 + // entries holds the children of this node, keyed by the 46 + // name of each child's struct field selector. 47 + entries map[cue.Selector]*structBuilderNode 48 + } 49 + 50 + // put associates value with the given path. It reports whether 51 + // the value was successfully put, returning false if a value 52 + // already exists for the path. 53 + func (b *structBuilder) put(p cue.Path, value ast.Expr, comment *ast.CommentGroup) bool { 54 + e := b.entryForPath(p) 55 + if e.value != nil { 56 + // redefinition 57 + return false 58 + } 59 + e.value = value 60 + e.comment = comment 61 + return true 62 + } 63 + 64 + const rootIdentName = "_schema" 65 + 66 + // getRef returns CUE syntax for a reference to the path p within b. 67 + // It ensures that, if possible, the identifier at the start of the 68 + // reference expression has the correct target node. 69 + func (b *structBuilder) getRef(p cue.Path) (ast.Expr, error) { 70 + if err := p.Err(); err != nil { 71 + return nil, fmt.Errorf("invalid path %v", p) 72 + } 73 + sels := p.Selectors() 74 + if len(sels) == 0 { 75 + // There's no natural name for the root element, 76 + // so use an arbitrary one. 77 + ref := ast.NewIdent(rootIdentName) 78 + 79 + b.rootRefIdents = append(b.rootRefIdents, ref) 80 + return ref, nil 81 + } 82 + base, err := labelForSelector(sels[0]) 83 + if err != nil { 84 + return nil, err 85 + } 86 + baseExpr, ok := base.(*ast.Ident) 87 + if !ok { 88 + return nil, fmt.Errorf("initial element of path %q must be expressed as an identifier", p) 89 + } 90 + // The base identifier needs to refer to the 91 + // first element of the path; the rest doesn't matter. 92 + if b.refIdents == nil { 93 + b.refIdents = make(map[cue.Selector][]*ast.Ident) 94 + } 95 + b.refIdents[sels[0]] = append(b.refIdents[sels[0]], baseExpr) 96 + return pathRefSyntax(cue.MakePath(sels[1:]...), baseExpr) 97 + } 98 + 99 + func (b *structBuilder) entryForPath(p cue.Path) *structBuilderNode { 100 + if err := p.Err(); err != nil { 101 + panic(fmt.Errorf("invalid path %v", p)) 102 + } 103 + sels := p.Selectors() 104 + 105 + n := &b.root 106 + for _, sel := range sels { 107 + if n.entries == nil { 108 + n.entries = make(map[cue.Selector]*structBuilderNode) 109 + } 110 + n1, ok := n.entries[sel] 111 + if !ok { 112 + n1 = &structBuilderNode{} 113 + n.entries[sel] = n1 114 + } 115 + n = n1 116 + } 117 + return n 118 + } 119 + 120 + // syntax returns an expression for the whole struct. 121 + func (b *structBuilder) syntax() (*ast.File, error) { 122 + var db declBuilder 123 + if err := b.appendDecls(&b.root, &db); err != nil { 124 + return nil, err 125 + } 126 + // Fix up references (we don't need to do this if the root is a single 127 + // expression, because that only happens when there's nothing 128 + // to refer to). 129 + for _, decl := range db.decls { 130 + if f, ok := decl.(*ast.Field); ok { 131 + for _, ident := range b.refIdents[selectorForLabel(f.Label)] { 132 + ident.Node = f.Value 133 + } 134 + } 135 + } 136 + 137 + var f *ast.File 138 + if len(b.rootRefIdents) == 0 { 139 + // No reference to root, so can use declarations as they are. 140 + f = &ast.File{ 141 + Decls: db.decls, 142 + } 143 + } else { 144 + rootExpr := exprFromDecls(db.decls) 145 + // Fix up references to the root node. 146 + for _, ident := range b.rootRefIdents { 147 + ident.Node = rootExpr 148 + } 149 + rootRef, err := b.getRef(cue.Path{}) 150 + if err != nil { 151 + return nil, err 152 + } 153 + f = &ast.File{ 154 + Decls: []ast.Decl{ 155 + &ast.EmbedDecl{Expr: rootRef}, 156 + &ast.Field{ 157 + Label: ast.NewIdent(rootIdentName), 158 + Value: rootExpr, 159 + }, 160 + }, 161 + } 162 + } 163 + if b.root.comment != nil { 164 + // If Doc is true, as it is for comments on fields, 165 + // then the CUE formatting will join it to any import 166 + // directives, which is not what we want, as then 167 + // it will no longer appear as a comment on the file. 168 + // So set Doc to false to prevent that happening. 169 + b.root.comment.Doc = false 170 + ast.SetComments(f, []*ast.CommentGroup{b.root.comment}) 171 + } 172 + 173 + return f, nil 174 + } 175 + 176 + func (b *structBuilder) appendDecls(n *structBuilderNode, db *declBuilder) (_err error) { 177 + if n.value != nil { 178 + if len(n.entries) > 0 { 179 + // We've got a value associated with this node and also some entries inside it. 180 + // We need to make a struct literal to hold the value and those entries 181 + // because the value might be scalar and 182 + // #x: string 183 + // #x: #y: bool 184 + // is not allowed. 185 + // 186 + // So make a new declBuilder instance with a fresh empty path 187 + // to build the declarations to put inside a struct literal. 188 + db0 := db 189 + db = &declBuilder{} 190 + defer func() { 191 + if _err != nil { 192 + return 193 + } 194 + db0.decls, _err = appendField(db0.decls, cue.MakePath(db0.path...), exprFromDecls(db.decls), n.comment) 195 + }() 196 + } 197 + // Note: when the path is empty, we rely on the outer level 198 + // to add any doc comment required. 199 + db.decls, _err = appendField(db.decls, cue.MakePath(db.path...), n.value, n.comment) 200 + if _err != nil { 201 + return _err 202 + } 203 + } 204 + // TODO slices.SortedFunc(maps.Keys(n.entries), cmpSelector) 205 + for _, sel := range sortedKeys(n.entries, cmpSelector) { 206 + entry := n.entries[sel] 207 + db.pushPath(sel) 208 + err := b.appendDecls(entry, db) 209 + db.popPath() 210 + if err != nil { 211 + return err 212 + } 213 + } 214 + return nil 215 + } 216 + 217 + type declBuilder struct { 218 + decls []ast.Decl 219 + path []cue.Selector 220 + } 221 + 222 + func (b *declBuilder) pushPath(sel cue.Selector) { 223 + b.path = append(b.path, sel) 224 + } 225 + 226 + func (b *declBuilder) popPath() { 227 + b.path = b.path[:len(b.path)-1] 228 + } 229 + 230 + func exprFromDecls(decls []ast.Decl) ast.Expr { 231 + if len(decls) == 1 { 232 + if decl, ok := decls[0].(*ast.EmbedDecl); ok { 233 + // It's a single embedded expression which we can use directly. 234 + return decl.Expr 235 + } 236 + } 237 + return &ast.StructLit{ 238 + Elts: decls, 239 + } 240 + } 241 + 242 + func appendDeclsExpr(decls []ast.Decl, expr ast.Expr) []ast.Decl { 243 + switch expr := expr.(type) { 244 + case *ast.StructLit: 245 + decls = append(decls, expr.Elts...) 246 + default: 247 + elt := &ast.EmbedDecl{Expr: expr} 248 + ast.SetRelPos(elt, token.NewSection) 249 + decls = append(decls, elt) 250 + } 251 + return decls 252 + } 253 + func appendField(decls []ast.Decl, path cue.Path, v ast.Expr, comment *ast.CommentGroup) ([]ast.Decl, error) { 254 + if len(path.Selectors()) == 0 { 255 + return appendDeclsExpr(decls, v), nil 256 + } 257 + expr, err := exprAtPath(path, v) 258 + if err != nil { 259 + return nil, err 260 + } 261 + // exprAtPath will always return a struct literal with exactly 262 + // one element when the path is non-empty. 263 + structLit := expr.(*ast.StructLit) 264 + elt := structLit.Elts[0] 265 + if comment != nil { 266 + ast.SetComments(elt, []*ast.CommentGroup{comment}) 267 + } 268 + ast.SetRelPos(elt, token.NewSection) 269 + return append(decls, elt), nil 270 + } 271 + 272 + func cmpSelector(s1, s2 cue.Selector) int { 273 + if s1 == s2 { 274 + // Avoid String allocation when we can. 275 + return 0 276 + } 277 + if c := cmp.Compare(s1.Type(), s2.Type()); c != 0 { 278 + return c 279 + } 280 + return cmp.Compare(s1.String(), s2.String()) 281 + }
+110
encoding/jsonschema/structbuilder_test.go
··· 1 + package jsonschema 2 + 3 + import ( 4 + "strings" 5 + "testing" 6 + 7 + "github.com/go-quicktest/qt" 8 + 9 + "cuelang.org/go/cue" 10 + "cuelang.org/go/cue/ast" 11 + "cuelang.org/go/cue/ast/astutil" 12 + "cuelang.org/go/cue/format" 13 + "cuelang.org/go/cue/token" 14 + "cuelang.org/go/internal" 15 + ) 16 + 17 + func TestStructBuilderShadowedRef(t *testing.T) { 18 + var b structBuilder 19 + ref, err := b.getRef(cue.ParsePath("#foo.bar.baz")) 20 + qt.Assert(t, qt.IsNil(err)) 21 + ok := b.put(cue.ParsePath("#foo.bar.baz"), ast.NewString("hello"), nil) 22 + qt.Assert(t, qt.IsTrue(ok)) 23 + ok = b.put(cue.ParsePath("#bar.#foo.xxx"), ref, nil) 24 + qt.Assert(t, qt.IsTrue(ok)) 25 + assertStructBuilderSyntax(t, &b, `#bar: #foo: xxx: #foo_1.bar.baz 26 + 27 + #foo_1=#foo: bar: baz: "hello" 28 + `) 29 + } 30 + 31 + func TestStructBuilderSelfRef(t *testing.T) { 32 + var b structBuilder 33 + ref, err := b.getRef(cue.Path{}) 34 + qt.Assert(t, qt.IsNil(err)) 35 + ok := b.put(cue.Path{}, ast.NewStruct(ast.NewIdent("next"), token.OPTION, ref), nil) 36 + qt.Assert(t, qt.IsTrue(ok)) 37 + assertStructBuilderSyntax(t, &b, ` 38 + _schema 39 + _schema: { 40 + next?: _schema 41 + } 42 + `) 43 + } 44 + 45 + func TestStructBuilderEntryInsideValue(t *testing.T) { 46 + var b structBuilder 47 + ok := b.put(cue.ParsePath("#foo"), ast.NewString("hello"), internal.NewComment(true, "foo comment")) 48 + qt.Assert(t, qt.IsTrue(ok)) 49 + ok = b.put(cue.ParsePath("#foo.#bar.#baz"), ast.NewString("goodbye"), internal.NewComment(true, "baz comment")) 50 + qt.Assert(t, qt.IsTrue(ok)) 51 + assertStructBuilderSyntax(t, &b, ` 52 + // foo comment 53 + #foo: { 54 + "hello" 55 + 56 + // baz comment 57 + #bar: #baz: "goodbye" 58 + } 59 + `) 60 + } 61 + 62 + func TestStructBuilderNonIdentifierStringNode(t *testing.T) { 63 + var b structBuilder 64 + ref, err := b.getRef(cue.ParsePath(`#foo."a b".baz`)) 65 + qt.Assert(t, qt.IsNil(err)) 66 + ok := b.put(cue.ParsePath(`#foo."a b".baz`), ast.NewString("hello"), nil) 67 + qt.Assert(t, qt.IsTrue(ok)) 68 + ok = b.put(cue.ParsePath("#bar.#foo.xxx"), ref, nil) 69 + qt.Assert(t, qt.IsTrue(ok)) 70 + assertStructBuilderSyntax(t, &b, ` 71 + #bar: #foo: xxx: #foo_1."a b".baz 72 + 73 + #foo_1=#foo: "a b": baz: "hello" 74 + `) 75 + } 76 + 77 + func TestStructBuilderNonIdentifierStringNodeAtRoot(t *testing.T) { 78 + var b structBuilder 79 + _, err := b.getRef(cue.ParsePath(`"a b".baz`)) 80 + qt.Assert(t, qt.ErrorMatches(err, `initial element of path "\\"a b\\"\.baz" must be expressed as an identifier`)) 81 + } 82 + 83 + func TestStructBuilderRedefinition(t *testing.T) { 84 + var b structBuilder 85 + ok := b.put(cue.ParsePath(`a.b.c`), ast.NewString("hello"), nil) 86 + qt.Assert(t, qt.IsTrue(ok)) 87 + ok = b.put(cue.ParsePath(`a.b.c`), ast.NewString("hello"), nil) 88 + qt.Assert(t, qt.IsFalse(ok)) 89 + } 90 + 91 + func TestStructBuilderNonPresentNodeOmittedFromSyntax(t *testing.T) { 92 + var b structBuilder 93 + _, err := b.getRef(cue.ParsePath(`b.c`)) 94 + qt.Assert(t, qt.IsNil(err)) 95 + _, err = b.getRef(cue.ParsePath(`a.c.d`)) 96 + qt.Assert(t, qt.IsNil(err)) 97 + ok := b.put(cue.ParsePath(`a.b`), ast.NewString("hello"), nil) 98 + qt.Assert(t, qt.IsTrue(ok)) 99 + assertStructBuilderSyntax(t, &b, `a: b: "hello"`) 100 + } 101 + 102 + func assertStructBuilderSyntax(t *testing.T, b *structBuilder, want string) { 103 + f, err := b.syntax() 104 + qt.Assert(t, qt.IsNil(err)) 105 + err = astutil.Sanitize(f) 106 + qt.Assert(t, qt.IsNil(err)) 107 + data, err := format.Node(f) 108 + qt.Assert(t, qt.IsNil(err)) 109 + qt.Assert(t, qt.Equals(strings.TrimSpace(string(data)), strings.TrimSpace(want))) 110 + }
+171
encoding/jsonschema/util.go
··· 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 + "strings" 21 + 22 + "cuelang.org/go/cue" 23 + "cuelang.org/go/cue/ast" 24 + "cuelang.org/go/cue/token" 25 + ) 26 + 27 + // TODO a bunch of stuff in this file is potentially suitable 28 + // for more general use. Consider moving some of it 29 + // to the cue package. 30 + 31 + func pathConcat(p1, p2 cue.Path) cue.Path { 32 + sels1, sels2 := p1.Selectors(), p2.Selectors() 33 + if len(sels1) == 0 { 34 + return p2 35 + } 36 + if len(sels2) == 0 { 37 + return p1 38 + } 39 + return cue.MakePath(append(slices.Clip(sels1), sels2...)...) 40 + } 41 + 42 + func labelsToCUEPath(labels []ast.Label) (cue.Path, error) { 43 + sels := make([]cue.Selector, len(labels)) 44 + for i, label := range labels { 45 + // Note: we can't use cue.Label because that doesn't 46 + // allow hidden fields. 47 + sels[i] = selectorForLabel(label) 48 + } 49 + path := cue.MakePath(sels...) 50 + if err := path.Err(); err != nil { 51 + return cue.Path{}, err 52 + } 53 + return path, nil 54 + } 55 + 56 + // selectorForLabel is like [cue.Label] except that it allows 57 + // hidden fields, which aren't allowed there because technically 58 + // we can't work out what package to associate with the resulting 59 + // selector. In our case we always imply the local package so 60 + // we don't mind about that. 61 + func selectorForLabel(label ast.Label) cue.Selector { 62 + if label, _ := label.(*ast.Ident); label != nil && strings.HasPrefix(label.Name, "_") { 63 + return cue.Hid(label.Name, "_") 64 + } 65 + return cue.Label(label) 66 + } 67 + 68 + // pathRefSyntax returns the syntax for an expression which 69 + // looks up the path inside the given root expression's value. 70 + // It returns an error if the path contains any elements with 71 + // type [cue.OptionalConstraint], [cue.RequiredConstraint], or [cue.PatternConstraint], 72 + // none of which are expressible as a CUE index expression. 73 + // 74 + // TODO implement this properly and move to a method on [cue.Path]. 75 + func pathRefSyntax(cuePath cue.Path, root ast.Expr) (ast.Expr, error) { 76 + expr := root 77 + for _, sel := range cuePath.Selectors() { 78 + if sel.LabelType() == cue.IndexLabel { 79 + expr = &ast.IndexExpr{ 80 + X: expr, 81 + Index: &ast.BasicLit{ 82 + Kind: token.INT, 83 + Value: sel.String(), 84 + }, 85 + } 86 + } else { 87 + lab, err := labelForSelector(sel) 88 + if err != nil { 89 + return nil, err 90 + } 91 + expr = &ast.SelectorExpr{ 92 + X: expr, 93 + Sel: lab, 94 + } 95 + } 96 + } 97 + return expr, nil 98 + } 99 + 100 + // exprAtPath returns an expression that places the given 101 + // expression at the given path. 102 + // For example: 103 + // 104 + // declAtPath(cue.ParsePath("a.b.#c"), ast.NewIdent("foo")) 105 + // 106 + // would result in the declaration: 107 + // 108 + // a: b: #c: foo 109 + // 110 + // TODO this is potentially generally useful. It could 111 + // be exposed as a method on [cue.Path], say 112 + // `SyntaxForDefinition` or something. 113 + func exprAtPath(path cue.Path, expr ast.Expr) (ast.Expr, error) { 114 + sels := path.Selectors() 115 + for i := len(sels) - 1; i >= 0; i-- { 116 + sel := sels[i] 117 + label, err := labelForSelector(sel) 118 + if err != nil { 119 + return nil, err 120 + } 121 + // A StructLit is inlined if both: 122 + // - the Lbrace position is invalid 123 + // - the Label position is valid. 124 + rel := token.Blank 125 + if i == 0 { 126 + rel = token.Newline 127 + } 128 + ast.SetPos(label, token.NoPos.WithRel(rel)) 129 + expr = &ast.StructLit{ 130 + Elts: []ast.Decl{ 131 + &ast.Field{ 132 + Label: label, 133 + Value: expr, 134 + }, 135 + }, 136 + } 137 + } 138 + return expr, nil 139 + } 140 + 141 + // TODO define this as a Label method on cue.Selector? 142 + func labelForSelector(sel cue.Selector) (ast.Label, error) { 143 + switch sel.LabelType() { 144 + case cue.StringLabel, cue.DefinitionLabel, cue.HiddenLabel, cue.HiddenDefinitionLabel: 145 + str := sel.String() 146 + switch { 147 + case strings.HasPrefix(str, `"`): 148 + // It's quoted for a reason, so maintain the quotes. 149 + return &ast.BasicLit{ 150 + Kind: token.STRING, 151 + Value: str, 152 + }, nil 153 + case ast.IsValidIdent(str): 154 + return ast.NewIdent(str), nil 155 + } 156 + // Should never happen. 157 + return nil, fmt.Errorf("cannot form expression for selector %q", sel) 158 + default: 159 + return nil, fmt.Errorf("cannot form label for selector %q with type %v", sel, sel.LabelType()) 160 + } 161 + } 162 + 163 + // TODO remove this when we can use [slices.SortedFunc] and [maps.Keys]. 164 + func sortedKeys[K comparable, V any](m map[K]V, cmp func(K, K) int) []K { 165 + ks := make([]K, 0, len(m)) 166 + for k := range m { 167 + ks = append(ks, k) 168 + } 169 + slices.SortFunc(ks, cmp) 170 + return ks 171 + }