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  }