cuelang.org/go@v0.13.0/encoding/jsonschema/structbuilder.go (about) 1 package jsonschema 2 3 import ( 4 "cmp" 5 "fmt" 6 "maps" 7 "slices" 8 9 "cuelang.org/go/cue" 10 "cuelang.org/go/cue/ast" 11 "cuelang.org/go/cue/token" 12 ) 13 14 // structBuilder builds a struct value incrementally by 15 // putting values for its component paths. 16 // The [structBuilder.getRef] method can be used 17 // to obtain reliable references into the resulting struct. 18 type structBuilder struct { 19 root structBuilderNode 20 21 // refIdents records all the identifiers that refer to entries 22 // at the top level of the struct, keyed by the selector 23 // they're referring to. 24 // 25 // The [Ident.Node] field needs to refer to the field value rather 26 // than the field label, and we don't know that until the syntax 27 // method has been invoked, so we fix up the [Ident.Node] fields when 28 // that happens. 29 refIdents map[cue.Selector][]*ast.Ident 30 31 // rootRefIdents is like refIdents but for references to the 32 // struct root itself. 33 rootRefIdents []*ast.Ident 34 } 35 36 // structBuilderNode represents one node in the tree of values 37 // being built. 38 type structBuilderNode struct { 39 // value holds the value associated with the node, if any. 40 // This does not include entries added underneath it by 41 // [structBuilder.put]. 42 value ast.Expr 43 44 // comment holds any doc comment associated with the value. 45 comment *ast.CommentGroup 46 47 // entries holds the children of this node, keyed by the 48 // name of each child's struct field selector. 49 entries map[cue.Selector]*structBuilderNode 50 } 51 52 // put associates value with the given path. It reports whether 53 // the value was successfully put, returning false if a value 54 // already exists for the path. 55 func (b *structBuilder) put(p cue.Path, value ast.Expr, comment *ast.CommentGroup) bool { 56 e := b.entryForPath(p) 57 if e.value != nil { 58 // redefinition 59 return false 60 } 61 e.value = value 62 e.comment = comment 63 return true 64 } 65 66 const rootIdentName = "_schema" 67 68 // getRef returns CUE syntax for a reference to the path p within b. 69 // It ensures that, if possible, the identifier at the start of the 70 // reference expression has the correct target node. 71 func (b *structBuilder) getRef(p cue.Path) (ast.Expr, error) { 72 if err := p.Err(); err != nil { 73 return nil, fmt.Errorf("invalid path %v", p) 74 } 75 sels := p.Selectors() 76 if len(sels) == 0 { 77 // There's no natural name for the root element, 78 // so use an arbitrary one. 79 ref := ast.NewIdent(rootIdentName) 80 81 b.rootRefIdents = append(b.rootRefIdents, ref) 82 return ref, nil 83 } 84 base, err := labelForSelector(sels[0]) 85 if err != nil { 86 return nil, err 87 } 88 baseExpr, ok := base.(*ast.Ident) 89 if !ok { 90 return nil, fmt.Errorf("initial element of path %q must be expressed as an identifier", p) 91 } 92 // The base identifier needs to refer to the 93 // first element of the path; the rest doesn't matter. 94 if b.refIdents == nil { 95 b.refIdents = make(map[cue.Selector][]*ast.Ident) 96 } 97 b.refIdents[sels[0]] = append(b.refIdents[sels[0]], baseExpr) 98 return pathRefSyntax(cue.MakePath(sels[1:]...), baseExpr) 99 } 100 101 func (b *structBuilder) entryForPath(p cue.Path) *structBuilderNode { 102 if err := p.Err(); err != nil { 103 panic(fmt.Errorf("invalid path %v", p)) 104 } 105 sels := p.Selectors() 106 107 n := &b.root 108 for _, sel := range sels { 109 if n.entries == nil { 110 n.entries = make(map[cue.Selector]*structBuilderNode) 111 } 112 n1, ok := n.entries[sel] 113 if !ok { 114 n1 = &structBuilderNode{} 115 n.entries[sel] = n1 116 } 117 n = n1 118 } 119 return n 120 } 121 122 // syntax returns an expression for the whole struct. 123 func (b *structBuilder) syntax() (*ast.File, error) { 124 var db declBuilder 125 if err := b.appendDecls(&b.root, &db); err != nil { 126 return nil, err 127 } 128 // Fix up references (we don't need to do this if the root is a single 129 // expression, because that only happens when there's nothing 130 // to refer to). 131 for _, decl := range db.decls { 132 if f, ok := decl.(*ast.Field); ok { 133 for _, ident := range b.refIdents[selectorForLabel(f.Label)] { 134 ident.Node = f.Value 135 } 136 } 137 } 138 139 var f *ast.File 140 if len(b.rootRefIdents) == 0 { 141 // No reference to root, so can use declarations as they are. 142 f = &ast.File{ 143 Decls: db.decls, 144 } 145 } else { 146 rootExpr := exprFromDecls(db.decls) 147 // Fix up references to the root node. 148 for _, ident := range b.rootRefIdents { 149 ident.Node = rootExpr 150 } 151 rootRef, err := b.getRef(cue.Path{}) 152 if err != nil { 153 return nil, err 154 } 155 f = &ast.File{ 156 Decls: []ast.Decl{ 157 &ast.EmbedDecl{Expr: rootRef}, 158 &ast.Field{ 159 Label: ast.NewIdent(rootIdentName), 160 Value: rootExpr, 161 }, 162 }, 163 } 164 } 165 if b.root.comment != nil { 166 // If Doc is true, as it is for comments on fields, 167 // then the CUE formatting will join it to any import 168 // directives, which is not what we want, as then 169 // it will no longer appear as a comment on the file. 170 // So set Doc to false to prevent that happening. 171 b.root.comment.Doc = false 172 ast.SetComments(f, []*ast.CommentGroup{b.root.comment}) 173 } 174 175 return f, nil 176 } 177 178 func (b *structBuilder) appendDecls(n *structBuilderNode, db *declBuilder) (_err error) { 179 if n.value != nil { 180 if len(n.entries) > 0 { 181 // We've got a value associated with this node and also some entries inside it. 182 // We need to make a struct literal to hold the value and those entries 183 // because the value might be scalar and 184 // #x: string 185 // #x: #y: bool 186 // is not allowed. 187 // 188 // So make a new declBuilder instance with a fresh empty path 189 // to build the declarations to put inside a struct literal. 190 db0 := db 191 db = &declBuilder{} 192 defer func() { 193 if _err != nil { 194 return 195 } 196 db0.decls, _err = appendField(db0.decls, cue.MakePath(db0.path...), exprFromDecls(db.decls), n.comment) 197 }() 198 } 199 // Note: when the path is empty, we rely on the outer level 200 // to add any doc comment required. 201 db.decls, _err = appendField(db.decls, cue.MakePath(db.path...), n.value, n.comment) 202 if _err != nil { 203 return _err 204 } 205 } 206 for _, sel := range slices.SortedFunc(maps.Keys(n.entries), cmpSelector) { 207 entry := n.entries[sel] 208 db.pushPath(sel) 209 err := b.appendDecls(entry, db) 210 db.popPath() 211 if err != nil { 212 return err 213 } 214 } 215 return nil 216 } 217 218 type declBuilder struct { 219 decls []ast.Decl 220 path []cue.Selector 221 } 222 223 func (b *declBuilder) pushPath(sel cue.Selector) { 224 b.path = append(b.path, sel) 225 } 226 227 func (b *declBuilder) popPath() { 228 b.path = b.path[:len(b.path)-1] 229 } 230 231 func exprFromDecls(decls []ast.Decl) ast.Expr { 232 if len(decls) == 1 { 233 if decl, ok := decls[0].(*ast.EmbedDecl); ok { 234 // It's a single embedded expression which we can use directly. 235 return decl.Expr 236 } 237 } 238 return &ast.StructLit{ 239 Elts: decls, 240 } 241 } 242 243 func appendDeclsExpr(decls []ast.Decl, expr ast.Expr) []ast.Decl { 244 switch expr := expr.(type) { 245 case *ast.StructLit: 246 decls = append(decls, expr.Elts...) 247 default: 248 elt := &ast.EmbedDecl{Expr: expr} 249 ast.SetRelPos(elt, token.NewSection) 250 decls = append(decls, elt) 251 } 252 return decls 253 } 254 255 func appendField(decls []ast.Decl, path cue.Path, v ast.Expr, comment *ast.CommentGroup) ([]ast.Decl, error) { 256 if len(path.Selectors()) == 0 { 257 return appendDeclsExpr(decls, v), nil 258 } 259 expr, err := exprAtPath(path, v) 260 if err != nil { 261 return nil, err 262 } 263 // exprAtPath will always return a struct literal with exactly 264 // one element when the path is non-empty. 265 structLit := expr.(*ast.StructLit) 266 elt := structLit.Elts[0] 267 if comment != nil { 268 ast.SetComments(elt, []*ast.CommentGroup{comment}) 269 } 270 ast.SetRelPos(elt, token.NewSection) 271 return append(decls, elt), nil 272 } 273 274 func cmpSelector(s1, s2 cue.Selector) int { 275 if s1 == s2 { 276 // Avoid String allocation when we can. 277 return 0 278 } 279 if c := cmp.Compare(s1.Type(), s2.Type()); c != 0 { 280 return c 281 } 282 return cmp.Compare(s1.String(), s2.String()) 283 }