cue/ast: add NewStruct helper

Change-Id: Ie6acb637416a2be06325362346b6aad0bc702ed7
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/4902
Reviewed-by: Marcel van Lohuizen <mpvl@golang.org>
diff --git a/cmd/cue/cmd/import.go b/cmd/cue/cmd/import.go
index cc74748..fdf4164 100644
--- a/cmd/cue/cmd/import.go
+++ b/cmd/cue/cmd/import.go
@@ -481,7 +481,7 @@
 			f.Decls = append(f.Decls, field)
 			for _, e := range pathElems[1:] {
 				newField := &ast.Field{Label: e}
-				newVal := &ast.StructLit{Elts: []ast.Decl{newField}}
+				newVal := ast.NewStruct(newField)
 				field.Value = newVal
 				field = newField
 			}
diff --git a/cue/ast/ast.go b/cue/ast/ast.go
index 4138969..d723e24 100644
--- a/cue/ast/ast.go
+++ b/cue/ast/ast.go
@@ -17,6 +17,7 @@
 package ast // import "cuelang.org/go/cue/ast"
 
 import (
+	"fmt"
 	"strconv"
 	"strings"
 
@@ -461,6 +462,71 @@
 	expr
 }
 
+// NewStruct creates a struct from the given fields.
+// A field is either a *Field, an *Elipsis or a Label, optionally followed by a
+// a token.OPTION to indicate the field is optional, optionally followed by a
+// token.ISA to indicate the field is a defintion followed by an expression
+// for the field value.
+// It will panic if a values not matching these patterns are given.
+// Useful for ASTs generated by code other than the CUE parser.
+func NewStruct(fields ...interface{}) *StructLit {
+	s := &StructLit{}
+	for i := 0; i < len(fields); i++ {
+		var (
+			label    Label
+			optional = token.NoPos
+			tok      = token.ILLEGAL
+			expr     Expr
+		)
+
+		switch x := fields[i].(type) {
+		case *Field:
+			s.Elts = append(s.Elts, x)
+			continue
+		case *Ellipsis:
+			s.Elts = append(s.Elts, x)
+			continue
+		case Label:
+			label = x
+		case string:
+			label = NewString(x)
+		default:
+			panic(fmt.Sprintf("unsupported label type %T", x))
+		}
+
+	inner:
+		for i++; i < len(fields); i++ {
+			switch x := (fields[i]).(type) {
+			case Expr:
+				expr = x
+				break inner
+			case token.Token:
+				switch x {
+				case token.ISA:
+					tok = x
+				case token.OPTION:
+					optional = token.Blank.Pos()
+				case token.COLON, token.ILLEGAL:
+				default:
+					panic(fmt.Sprintf("invalid token %s", x))
+				}
+			default:
+				panic(fmt.Sprintf("unsupported expression type %T", x))
+			}
+		}
+		if expr == nil {
+			panic("label not matched with expression")
+		}
+		s.Elts = append(s.Elts, &Field{
+			Label:    label,
+			Optional: optional,
+			Token:    tok,
+			Value:    expr,
+		})
+	}
+	return s
+}
+
 // A ListLit node represents a literal list.
 type ListLit struct {
 	Lbrack token.Pos // position of "["
diff --git a/cue/ast/astutil/apply_test.go b/cue/ast/astutil/apply_test.go
index c9d7241..09bc8f4 100644
--- a/cue/ast/astutil/apply_test.go
+++ b/cue/ast/astutil/apply_test.go
@@ -116,12 +116,7 @@
 				default:
 					c.InsertAfter(astutil.ApplyRecursively(&ast.Field{
 						Label: ast.NewIdent("iam"),
-						Value: &ast.StructLit{Elts: []ast.Decl{
-							&ast.Field{
-								Label: ast.NewIdent("here"),
-								Value: ast.NewIdent("new"),
-							},
-						}},
+						Value: ast.NewStruct(ast.NewIdent("here"), ast.NewIdent("new")),
 					}))
 				case "iam":
 					c.InsertAfter(&ast.Field{
diff --git a/cue/format/node_test.go b/cue/format/node_test.go
index b544f3b..caf366d 100644
--- a/cue/format/node_test.go
+++ b/cue/format/node_test.go
@@ -34,13 +34,9 @@
 		out  string
 	}{{
 		desc: "label sequence for definition",
-		node: &ast.Field{Label: ident("foo"), Value: &ast.StructLit{
-			Elts: []ast.Decl{&ast.Field{
-				Label: ident("bar"),
-				Token: token.ISA,
-				Value: &ast.StructLit{},
-			}},
-		}},
+		node: &ast.Field{Label: ident("foo"), Value: ast.NewStruct(
+			ident("bar"), token.ISA, &ast.StructLit{},
+		)},
 		// Force a new struct.
 		out: `foo: bar :: {
 }`,
diff --git a/encoding/jsonschema/constraints.go b/encoding/jsonschema/constraints.go
index c4c5add..985c904 100644
--- a/encoding/jsonschema/constraints.go
+++ b/encoding/jsonschema/constraints.go
@@ -108,7 +108,7 @@
 		}
 		f = &ast.Field{
 			Label: ast.NewIdent(rootDefs),
-			Value: &ast.StructLit{Elts: []ast.Decl{f}},
+			Value: ast.NewStruct(f),
 		}
 		ast.SetRelPos(f, token.NewSection)
 		s.definitions = append(s.definitions, f)
@@ -386,10 +386,7 @@
 	p0d("propertyNames", 6, func(n cue.Value, s *state) {
 		s.kind |= cue.StructKind
 		// [=~pattern]: _
-		s.add(&ast.StructLit{Elts: []ast.Decl{&ast.Field{
-			Label: ast.NewList(s.schema(n)),
-			Value: ast.NewIdent("_"),
-		}}})
+		s.add(ast.NewStruct(ast.NewList(s.schema(n)), ast.NewIdent("_")))
 	}),
 
 	p0("minProperties", func(n cue.Value, s *state) {
diff --git a/encoding/jsonschema/decode.go b/encoding/jsonschema/decode.go
index c644ff0..afa502d 100644
--- a/encoding/jsonschema/decode.go
+++ b/encoding/jsonschema/decode.go
@@ -226,9 +226,7 @@
 				add(ast.NewList(&ast.Ellipsis{}))
 			}
 		case "object":
-			elps := &ast.Ellipsis{}
-			st := &ast.StructLit{Elts: []ast.Decl{elps}}
-			add(st)
+			add(ast.NewStruct(&ast.Ellipsis{}))
 		default:
 			s.errf(n, "unknown type %q", n)
 		}
diff --git a/encoding/protobuf/parse.go b/encoding/protobuf/parse.go
index b35e6ae..2cca457 100644
--- a/encoding/protobuf/parse.go
+++ b/encoding/protobuf/parse.go
@@ -528,7 +528,7 @@
 		name := p.ident(x.Position, x.Name)
 		f = &ast.Field{
 			Label: name,
-			Value: &ast.StructLit{Elts: []ast.Decl{f}},
+			Value: ast.NewStruct(f),
 		}
 		addComments(f, i, x.Comment, x.InlineComment)