cuelang.org/go@v0.13.0/encoding/protobuf/parse.go (about)

     1  // Copyright 2019 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 protobuf
    16  
    17  import (
    18  	"bytes"
    19  	"fmt"
    20  	"os"
    21  	"path"
    22  	"path/filepath"
    23  	"slices"
    24  	"strconv"
    25  	"strings"
    26  	"text/scanner"
    27  	"unicode"
    28  
    29  	"github.com/emicklei/proto"
    30  
    31  	"cuelang.org/go/cue/ast"
    32  	"cuelang.org/go/cue/ast/astutil"
    33  	"cuelang.org/go/cue/errors"
    34  	"cuelang.org/go/cue/literal"
    35  	"cuelang.org/go/cue/parser"
    36  	"cuelang.org/go/cue/token"
    37  	"cuelang.org/go/internal/source"
    38  )
    39  
    40  func (s *Extractor) parse(filename string, src interface{}) (p *protoConverter, err error) {
    41  	if filename == "" {
    42  		return nil, errors.Newf(token.NoPos, "empty filename")
    43  	}
    44  	if r, ok := s.fileCache[filename]; ok {
    45  		return r.p, r.err
    46  	}
    47  	defer func() {
    48  		s.fileCache[filename] = result{p, err}
    49  	}()
    50  
    51  	b, err := source.ReadAll(filename, src)
    52  	if err != nil {
    53  		return nil, err
    54  	}
    55  
    56  	parser := proto.NewParser(bytes.NewReader(b))
    57  	if filename != "" {
    58  		parser.Filename(filename)
    59  	}
    60  	d, err := parser.Parse()
    61  	if err != nil {
    62  		return nil, errors.Newf(token.NoPos, "protobuf: %v", err)
    63  	}
    64  
    65  	tfile := token.NewFile(filename, -1, len(b))
    66  	tfile.SetLinesForContent(b)
    67  
    68  	p = &protoConverter{
    69  		id:       filename,
    70  		state:    s,
    71  		tfile:    tfile,
    72  		imported: map[string]bool{},
    73  		symbols:  map[string]bool{},
    74  	}
    75  
    76  	defer func() {
    77  		switch x := recover().(type) {
    78  		case nil:
    79  		case protoError:
    80  			err = &protobufError{
    81  				path: p.path,
    82  				pos:  p.toCUEPos(x.pos),
    83  				err:  x.error,
    84  			}
    85  		default:
    86  			panic(x)
    87  		}
    88  	}()
    89  
    90  	p.file = &ast.File{Filename: filename}
    91  
    92  	p.addNames(d.Elements)
    93  
    94  	// Parse package definitions.
    95  	for _, e := range d.Elements {
    96  		switch x := e.(type) {
    97  		case *proto.Package:
    98  			p.protoPkg = x.Name
    99  		case *proto.Option:
   100  			if x.Name == "go_package" {
   101  				str, err := strconv.Unquote(x.Constant.SourceRepresentation())
   102  				if err != nil {
   103  					failf(x.Position, "unquoting package filed: %v", err)
   104  				}
   105  				split := strings.Split(str, ";")
   106  				switch {
   107  				case strings.Contains(split[0], "."):
   108  					p.cuePkgPath = split[0]
   109  					switch len(split) {
   110  					case 1:
   111  						p.shortPkgName = path.Base(str)
   112  					case 2:
   113  						p.shortPkgName = split[1]
   114  					default:
   115  						failf(x.Position, "unexpected ';' in %q", str)
   116  					}
   117  
   118  				case len(split) == 1:
   119  					p.shortPkgName = split[0]
   120  
   121  				default:
   122  					failf(x.Position, "malformed go_package clause %s", str)
   123  				}
   124  				// name.AddComment(comment(x.Comment, true))
   125  				// name.AddComment(comment(x.InlineComment, false))
   126  			}
   127  		}
   128  	}
   129  
   130  	if name := p.shortName(); name != "" {
   131  		p.file.Decls = append(p.file.Decls, &ast.Package{Name: ast.NewIdent(name)})
   132  	}
   133  
   134  	for _, e := range d.Elements {
   135  		switch x := e.(type) {
   136  		case *proto.Import:
   137  			if err := p.doImport(x); err != nil {
   138  				return nil, err
   139  			}
   140  		}
   141  	}
   142  
   143  	for _, e := range d.Elements {
   144  		p.topElement(e)
   145  	}
   146  
   147  	err = astutil.Sanitize(p.file)
   148  
   149  	return p, err
   150  }
   151  
   152  // A protoConverter converts a proto definition to CUE. Proto files map to
   153  // CUE files one to one.
   154  type protoConverter struct {
   155  	state *Extractor
   156  	tfile *token.File
   157  
   158  	proto3 bool
   159  
   160  	id           string
   161  	protoPkg     string
   162  	shortPkgName string
   163  	cuePkgPath   string
   164  
   165  	file    *ast.File
   166  	current *ast.StructLit
   167  
   168  	imported map[string]bool
   169  
   170  	path    []string
   171  	scope   []map[string]mapping // for symbols resolution within package.
   172  	symbols map[string]bool      // symbols provided by package
   173  }
   174  
   175  type mapping struct {
   176  	cue func() ast.Expr // needs to be a new copy as position changes
   177  	pkg *protoConverter
   178  }
   179  
   180  func (p *protoConverter) qualifiedImportPath() string {
   181  	s := p.importPath()
   182  	if short := p.shortPkgName; short != "" && short != path.Base(s) {
   183  		s += ":" + short
   184  	}
   185  	return s
   186  }
   187  
   188  func (p *protoConverter) importPath() string {
   189  	if p.cuePkgPath == "" && p.protoPkg != "" {
   190  		dir := strings.Replace(p.protoPkg, ".", "/", -1)
   191  		p.cuePkgPath = path.Join("googleapis.com", dir)
   192  	}
   193  	return p.cuePkgPath
   194  }
   195  
   196  func (p *protoConverter) shortName() string {
   197  	if p.state.pkgName != "" {
   198  		return p.state.pkgName
   199  	}
   200  	if p.shortPkgName == "" && p.protoPkg != "" {
   201  		split := strings.Split(p.protoPkg, ".")
   202  		p.shortPkgName = split[len(split)-1]
   203  	}
   204  	return p.shortPkgName
   205  }
   206  
   207  func (p *protoConverter) toCUEPos(pos scanner.Position) token.Pos {
   208  	return p.tfile.Pos(pos.Offset, 0)
   209  }
   210  
   211  func (p *protoConverter) addRef(pos scanner.Position, name string, cue func() ast.Expr) {
   212  	top := p.scope[len(p.scope)-1]
   213  	if _, ok := top[name]; ok {
   214  		failf(pos, "entity %q already defined", name)
   215  	}
   216  	top[name] = mapping{cue: cue}
   217  }
   218  
   219  func (p *protoConverter) addNames(elems []proto.Visitee) {
   220  	p.scope = append(p.scope, map[string]mapping{})
   221  	for _, e := range elems {
   222  		var pos scanner.Position
   223  		var name string
   224  		switch x := e.(type) {
   225  		case *proto.Message:
   226  			if x.IsExtend {
   227  				continue
   228  			}
   229  			name = x.Name
   230  			pos = x.Position
   231  		case *proto.Enum:
   232  			name = x.Name
   233  			pos = x.Position
   234  		case *proto.NormalField:
   235  			name = x.Name
   236  			pos = x.Position
   237  		case *proto.MapField:
   238  			name = x.Name
   239  			pos = x.Position
   240  		case *proto.Oneof:
   241  			name = x.Name
   242  			pos = x.Position
   243  		default:
   244  			continue
   245  		}
   246  		sym := strings.Join(append(p.path, name), ".")
   247  		p.symbols[sym] = true
   248  		p.addRef(pos, name, func() ast.Expr { return ast.NewIdent("#" + name) })
   249  	}
   250  }
   251  
   252  func (p *protoConverter) popNames() {
   253  	p.scope = p.scope[:len(p.scope)-1]
   254  }
   255  
   256  func (p *protoConverter) resolve(pos scanner.Position, name string, options []*proto.Option) ast.Expr {
   257  	if expr := protoToCUE(name, options); expr != nil {
   258  		ast.SetPos(expr, p.toCUEPos(pos))
   259  		return expr
   260  	}
   261  	if strings.HasPrefix(name, ".") {
   262  		return p.resolveTopScope(pos, name[1:], options)
   263  	}
   264  	for _, scope := range slices.Backward(p.scope) {
   265  		if m, ok := scope[name]; ok {
   266  			return m.cue()
   267  		}
   268  	}
   269  	expr := p.resolveTopScope(pos, name, options)
   270  	return expr
   271  }
   272  
   273  func (p *protoConverter) resolveTopScope(pos scanner.Position, name string, options []*proto.Option) ast.Expr {
   274  	for i := 0; i < len(name); i++ {
   275  		k := strings.IndexByte(name[i:], '.')
   276  		i += k
   277  		if k == -1 {
   278  			i = len(name)
   279  		}
   280  		if m, ok := p.scope[0][name[:i]]; ok {
   281  			if m.pkg != nil {
   282  				p.imported[m.pkg.qualifiedImportPath()] = true
   283  			}
   284  			expr := m.cue()
   285  			for i < len(name) {
   286  				name = name[i+1:]
   287  				if i = strings.IndexByte(name, '.'); i == -1 {
   288  					i = len(name)
   289  				}
   290  				expr = ast.NewSel(expr, "#"+name[:i])
   291  			}
   292  			ast.SetPos(expr, p.toCUEPos(pos))
   293  			return expr
   294  		}
   295  	}
   296  	failf(pos, "name %q not found", name)
   297  	return nil
   298  }
   299  
   300  func (p *protoConverter) doImport(v *proto.Import) error {
   301  	if v.Filename == "cue/cue.proto" {
   302  		return nil
   303  	}
   304  
   305  	filename := ""
   306  	for _, p := range p.state.paths {
   307  		name := filepath.Join(p, v.Filename)
   308  		_, err := os.Stat(name)
   309  		if err != nil {
   310  			continue
   311  		}
   312  		filename = name
   313  		break
   314  	}
   315  
   316  	if filename == "" {
   317  		err := errors.Newf(p.toCUEPos(v.Position), "could not find import %q", v.Filename)
   318  		p.state.addErr(err)
   319  		return err
   320  	}
   321  
   322  	if !p.mapBuiltinPackage(v.Position, v.Filename, filename == "") {
   323  		return nil
   324  	}
   325  
   326  	imp, err := p.state.parse(filename, nil)
   327  	if err != nil {
   328  		fail(v.Position, err)
   329  	}
   330  
   331  	pkgNamespace := strings.Split(imp.protoPkg, ".")
   332  	curNamespace := strings.Split(p.protoPkg, ".")
   333  	for {
   334  		for k := range imp.symbols {
   335  			ref := k
   336  			if len(pkgNamespace) > 0 {
   337  				ref = strings.Join(append(pkgNamespace, k), ".")
   338  			}
   339  			if _, ok := p.scope[0][ref]; !ok {
   340  				pkg := imp
   341  				a := toCue(k)
   342  
   343  				var f func() ast.Expr
   344  
   345  				if imp.qualifiedImportPath() == p.qualifiedImportPath() {
   346  					pkg = nil
   347  					f = func() ast.Expr { return ast.NewIdent(a[0]) }
   348  				} else {
   349  					f = func() ast.Expr {
   350  						ident := &ast.Ident{
   351  							Name: imp.shortName(),
   352  							Node: ast.NewImport(nil, imp.qualifiedImportPath()),
   353  						}
   354  						return ast.NewSel(ident, a[0])
   355  					}
   356  				}
   357  				p.scope[0][ref] = mapping{f, pkg}
   358  			}
   359  		}
   360  		if len(pkgNamespace) == 0 {
   361  			break
   362  		}
   363  		if len(curNamespace) == 0 || pkgNamespace[0] != curNamespace[0] {
   364  			break
   365  		}
   366  		pkgNamespace = pkgNamespace[1:]
   367  		curNamespace = curNamespace[1:]
   368  	}
   369  	return nil
   370  }
   371  
   372  // TODO: this doesn't work. Do something more principled.
   373  func toCue(name string) []string {
   374  	a := strings.Split(name, ".")
   375  	for i, s := range a {
   376  		a[i] = "#" + s
   377  	}
   378  	return a
   379  }
   380  
   381  func (p *protoConverter) stringLit(pos scanner.Position, s string) *ast.BasicLit {
   382  	return &ast.BasicLit{
   383  		ValuePos: p.toCUEPos(pos),
   384  		Kind:     token.STRING,
   385  		Value:    literal.String.Quote(s)}
   386  }
   387  
   388  func (p *protoConverter) ident(pos scanner.Position, name string) *ast.Ident {
   389  	return &ast.Ident{NamePos: p.toCUEPos(pos), Name: labelName(name)}
   390  }
   391  
   392  func (p *protoConverter) ref(pos scanner.Position) *ast.Ident {
   393  	name := "#" + p.path[len(p.path)-1]
   394  	return &ast.Ident{NamePos: p.toCUEPos(pos), Name: name}
   395  }
   396  
   397  func (p *protoConverter) subref(pos scanner.Position, name string) *ast.Ident {
   398  	return &ast.Ident{
   399  		NamePos: p.toCUEPos(pos),
   400  		Name:    "#" + name,
   401  	}
   402  }
   403  
   404  func (p *protoConverter) addTag(f *ast.Field, body string) {
   405  	tag := "@protobuf(" + body + ")"
   406  	f.Attrs = append(f.Attrs, &ast.Attribute{Text: tag})
   407  }
   408  
   409  func (p *protoConverter) topElement(v proto.Visitee) {
   410  	switch x := v.(type) {
   411  	case *proto.Syntax:
   412  		p.proto3 = x.Value == "proto3"
   413  
   414  	case *proto.Comment:
   415  		addComments(p.file, 0, x, nil)
   416  
   417  	case *proto.Enum:
   418  		p.enum(x)
   419  
   420  	case *proto.Package:
   421  		if doc := x.Doc(); doc != nil {
   422  			addComments(p.file, 0, doc, nil)
   423  		}
   424  
   425  	case *proto.Message:
   426  		p.message(x)
   427  
   428  	case *proto.Option:
   429  	case *proto.Import:
   430  		// already handled.
   431  
   432  	case *proto.Service:
   433  		// TODO: handle services.
   434  
   435  	case *proto.Extensions, *proto.Reserved:
   436  		// no need to handle
   437  
   438  	default:
   439  		failf(scanner.Position{}, "unsupported type %T", x)
   440  	}
   441  }
   442  
   443  func (p *protoConverter) message(v *proto.Message) {
   444  	if v.IsExtend {
   445  		// TODO: we are not handling extensions as for now.
   446  		return
   447  	}
   448  
   449  	defer func(saved []string) { p.path = saved }(p.path)
   450  	p.path = append(p.path, v.Name)
   451  
   452  	p.addNames(v.Elements)
   453  	defer p.popNames()
   454  
   455  	// TODO: handle IsExtend/ proto2
   456  
   457  	s := &ast.StructLit{
   458  		Lbrace: p.toCUEPos(v.Position),
   459  		// TODO: set proto file position.
   460  		Rbrace: token.Newline.Pos(),
   461  	}
   462  
   463  	ref := p.ref(v.Position)
   464  	if v.Comment == nil {
   465  		ref.NamePos = newSection
   466  	}
   467  	f := &ast.Field{Label: ref, Value: s}
   468  	addComments(f, 1, v.Comment, nil)
   469  
   470  	p.addDecl(f)
   471  	defer func(current *ast.StructLit) {
   472  		p.current = current
   473  	}(p.current)
   474  	p.current = s
   475  
   476  	for i, e := range v.Elements {
   477  		p.messageField(s, i, e)
   478  	}
   479  }
   480  
   481  func (p *protoConverter) addDecl(d ast.Decl) {
   482  	if p.current == nil {
   483  		p.file.Decls = append(p.file.Decls, d)
   484  	} else {
   485  		p.current.Elts = append(p.current.Elts, d)
   486  	}
   487  }
   488  
   489  func (p *protoConverter) messageField(s *ast.StructLit, i int, v proto.Visitee) {
   490  	switch x := v.(type) {
   491  	case *proto.Comment:
   492  		s.Elts = append(s.Elts, comment(x, true))
   493  
   494  	case *proto.NormalField:
   495  		f := p.parseField(s, i, x.Field)
   496  
   497  		if x.Repeated {
   498  			f.Value = &ast.ListLit{
   499  				Lbrack: p.toCUEPos(x.Position),
   500  				Elts:   []ast.Expr{&ast.Ellipsis{Type: f.Value}},
   501  			}
   502  		}
   503  
   504  	case *proto.MapField:
   505  		defer func(saved []string) { p.path = saved }(p.path)
   506  		p.path = append(p.path, x.Name)
   507  
   508  		f := &ast.Field{}
   509  
   510  		// All keys are converted to strings.
   511  		// TODO: support integer keys.
   512  		f.Label = ast.NewList(ast.NewIdent("string"))
   513  		f.Value = p.resolve(x.Position, x.Type, x.Options)
   514  
   515  		name := p.ident(x.Position, x.Name)
   516  		f = &ast.Field{
   517  			Label: name,
   518  			Value: ast.NewStruct(f),
   519  		}
   520  		addComments(f, i, x.Comment, x.InlineComment)
   521  
   522  		o := optionParser{message: s, field: f}
   523  		o.tags = fmt.Sprintf(`%d,map[%s]%s`, x.Sequence, x.KeyType, x.Type)
   524  		if x.Name != name.Name {
   525  			o.tags += "," + x.Name
   526  		}
   527  		s.Elts = append(s.Elts, f)
   528  		o.parse(x.Options)
   529  		p.addTag(f, o.tags)
   530  
   531  		if !o.required {
   532  			f.Optional = token.NoSpace.Pos()
   533  		}
   534  
   535  	case *proto.Enum:
   536  		p.enum(x)
   537  
   538  	case *proto.Message:
   539  		p.message(x)
   540  
   541  	case *proto.Oneof:
   542  		p.oneOf(x)
   543  
   544  	case *proto.Extensions, *proto.Reserved:
   545  		// no need to handle
   546  
   547  	case *proto.Option:
   548  		opt := fmt.Sprintf("@protobuf(option %s=%s)", x.Name, x.Constant.Source)
   549  		attr := &ast.Attribute{
   550  			At:   p.toCUEPos(x.Position),
   551  			Text: opt,
   552  		}
   553  		addComments(attr, i, x.Doc(), x.InlineComment)
   554  		s.Elts = append(s.Elts, attr)
   555  
   556  	default:
   557  		failf(scanner.Position{}, "unsupported field type %T", v)
   558  	}
   559  }
   560  
   561  // enum converts a proto enum definition to CUE.
   562  //
   563  // An enum will generate two top-level definitions:
   564  //
   565  //	Enum:
   566  //	  "Value1" |
   567  //	  "Value2" |
   568  //	  "Value3"
   569  //
   570  // and
   571  //
   572  //	Enum_value: {
   573  //	    "Value1": 0
   574  //	    "Value2": 1
   575  //	}
   576  //
   577  // Enums are always defined at the top level. The name of a nested enum
   578  // will be prefixed with the name of its parent and an underscore.
   579  func (p *protoConverter) enum(x *proto.Enum) {
   580  
   581  	if len(x.Elements) == 0 {
   582  		failf(x.Position, "empty enum")
   583  	}
   584  
   585  	name := p.subref(x.Position, x.Name)
   586  
   587  	defer func(saved []string) { p.path = saved }(p.path)
   588  	p.path = append(p.path, x.Name)
   589  
   590  	p.addNames(x.Elements)
   591  
   592  	if len(p.path) == 0 {
   593  		defer func() { p.path = p.path[:0] }()
   594  		p.path = append(p.path, x.Name)
   595  	}
   596  
   597  	// Top-level enum entry.
   598  	enum := &ast.Field{Label: name}
   599  	addComments(enum, 1, x.Comment, nil)
   600  	if p.current != nil && len(p.current.Elts) > 0 {
   601  		ast.SetRelPos(enum, token.NewSection)
   602  	}
   603  
   604  	// Top-level enum values entry.
   605  	valueName := ast.NewIdent(name.Name + "_value")
   606  	valueName.NamePos = newSection
   607  	valueMap := &ast.StructLit{}
   608  	d := &ast.Field{Label: valueName, Value: valueMap}
   609  	// addComments(valueMap, 1, x.Comment, nil)
   610  
   611  	if strings.Contains(name.Name, "google") {
   612  		panic(name.Name)
   613  	}
   614  	p.addDecl(enum)
   615  
   616  	numEnums := 0
   617  	for _, v := range x.Elements {
   618  		if _, ok := v.(*proto.EnumField); ok {
   619  			numEnums++
   620  		}
   621  	}
   622  
   623  	lastSingle := false
   624  
   625  	firstSpace := token.NewSection
   626  
   627  	// The line comments for an enum field need to attach after the '|', which
   628  	// is only known at the next iteration.
   629  	var lastComment *proto.Comment
   630  	for i, v := range x.Elements {
   631  		switch y := v.(type) {
   632  		case *proto.EnumField:
   633  			// Add enum value to map
   634  			intValue := ast.NewLit(token.INT, strconv.Itoa(y.Integer))
   635  			f := &ast.Field{
   636  				Label: p.stringLit(y.Position, y.Name),
   637  				Value: intValue,
   638  			}
   639  			valueMap.Elts = append(valueMap.Elts, f)
   640  
   641  			var e ast.Expr
   642  			switch p.state.enumMode {
   643  			case "int":
   644  				e = ast.NewIdent("#" + y.Name)
   645  				ast.SetRelPos(e, token.Newline)
   646  
   647  				f := &ast.Field{
   648  					Label: ast.NewIdent("#" + y.Name),
   649  					Value: intValue,
   650  				}
   651  				ast.SetRelPos(f, firstSpace)
   652  				firstSpace = token.Newline
   653  				addComments(f, 0, y.Comment, y.InlineComment)
   654  				p.addDecl(f)
   655  
   656  			case "", "json":
   657  				// add to enum disjunction
   658  				value := p.stringLit(y.Position, y.Name)
   659  				embed := &ast.EmbedDecl{Expr: value}
   660  				ast.SetRelPos(embed, token.Blank)
   661  				field := &ast.Field{Label: ast.NewIdent("#enumValue"), Value: intValue}
   662  				st := &ast.StructLit{
   663  					Lbrace: token.Blank.Pos(),
   664  					Elts:   []ast.Decl{embed, field},
   665  				}
   666  
   667  				addComments(embed, 0, y.Comment, y.InlineComment)
   668  				if y.Comment == nil && y.InlineComment == nil {
   669  					ast.SetRelPos(field, token.Blank)
   670  					ast.SetRelPos(field.Label, token.Blank)
   671  					st.Rbrace = token.Blank.Pos()
   672  					if i > 0 && lastSingle {
   673  						st.Lbrace = token.Newline.Pos()
   674  					}
   675  					lastSingle = true
   676  				} else {
   677  					lastSingle = false
   678  				}
   679  				e = st
   680  
   681  			default:
   682  				p.state.errs = errors.Append(p.state.errs,
   683  					errors.Newf(token.NoPos, "unknown enum mode %q", p.state.enumMode))
   684  				return
   685  			}
   686  
   687  			if enum.Value != nil {
   688  				e = &ast.BinaryExpr{X: enum.Value, Op: token.OR, Y: e}
   689  			}
   690  			enum.Value = e
   691  
   692  			// a := fmt.Sprintf("@protobuf(enum,name=%s)", y.Name)
   693  			// f.Attrs = append(f.Attrs, &ast.Attribute{Text: a})
   694  		}
   695  	}
   696  	p.addDecl(d)
   697  	addComments(enum.Value, 1, nil, lastComment)
   698  }
   699  
   700  // oneOf converts a Proto OneOf field to CUE. Note that Protobuf defines
   701  // a oneOf to be at most one of the fields. Rather than making each field
   702  // optional, we define oneOfs as all required fields, but add one more
   703  // disjunction allowing no fields. This makes it easier to constrain the
   704  // result to include at least one of the values.
   705  func (p *protoConverter) oneOf(x *proto.Oneof) {
   706  	s := ast.NewStruct()
   707  	ast.SetRelPos(s, token.Newline)
   708  	embed := &ast.EmbedDecl{Expr: s}
   709  	embed.AddComment(comment(x.Comment, true))
   710  
   711  	p.addDecl(embed)
   712  
   713  	newStruct := func() {
   714  		s = &ast.StructLit{
   715  			// TODO: make this the default in the formatter.
   716  			Rbrace: token.Newline.Pos(),
   717  		}
   718  		embed.Expr = ast.NewBinExpr(token.OR, embed.Expr, s)
   719  	}
   720  	for _, v := range x.Elements {
   721  		switch x := v.(type) {
   722  		case *proto.OneOfField:
   723  			newStruct()
   724  			oneOf := p.parseField(s, 0, x.Field)
   725  			oneOf.Optional = token.NoPos
   726  
   727  		case *proto.Comment:
   728  			cg := comment(x, false)
   729  			ast.SetRelPos(cg, token.NewSection)
   730  			s.Elts = append(s.Elts, cg)
   731  
   732  		default:
   733  			newStruct()
   734  			p.messageField(s, 1, v)
   735  		}
   736  
   737  	}
   738  }
   739  
   740  func (p *protoConverter) parseField(s *ast.StructLit, i int, x *proto.Field) *ast.Field {
   741  	defer func(saved []string) { p.path = saved }(p.path)
   742  	p.path = append(p.path, x.Name)
   743  
   744  	f := &ast.Field{}
   745  	addComments(f, i, x.Comment, x.InlineComment)
   746  
   747  	name := p.ident(x.Position, x.Name)
   748  	f.Label = name
   749  	typ := p.resolve(x.Position, x.Type, x.Options)
   750  	f.Value = typ
   751  	s.Elts = append(s.Elts, f)
   752  
   753  	o := optionParser{message: s, field: f}
   754  
   755  	// body of @protobuf tag: sequence,type[,name=<name>][,...]
   756  	o.tags += fmt.Sprintf("%v,%s", x.Sequence, x.Type)
   757  	if x.Name != name.Name {
   758  		o.tags += ",name=" + x.Name
   759  	}
   760  	o.parse(x.Options)
   761  	p.addTag(f, o.tags)
   762  
   763  	if !o.required {
   764  		f.Optional = token.NoSpace.Pos()
   765  	}
   766  	return f
   767  }
   768  
   769  type optionParser struct {
   770  	message  *ast.StructLit
   771  	field    *ast.Field
   772  	required bool
   773  	tags     string
   774  }
   775  
   776  func (p *optionParser) parse(options []*proto.Option) {
   777  
   778  	// TODO: handle options
   779  	// - translate options to tags
   780  	// - interpret CUE options.
   781  	for _, o := range options {
   782  		switch o.Name {
   783  		case "(cue.opt).required":
   784  			p.required = true
   785  			// TODO: Dropping comments. Maybe add a dummy tag?
   786  
   787  		case "(cue.val)":
   788  			// TODO: set filename and base offset.
   789  			expr, err := parser.ParseExpr("", o.Constant.Source)
   790  			if err != nil {
   791  				failf(o.Position, "invalid cue.val value: %v", err)
   792  			}
   793  			// Any further checks will be done at the end.
   794  			constraint := &ast.Field{Label: p.field.Label, Value: expr}
   795  			addComments(constraint, 1, o.Comment, o.InlineComment)
   796  			p.message.Elts = append(p.message.Elts, constraint)
   797  			if !p.required {
   798  				constraint.Optional = token.NoSpace.Pos()
   799  			}
   800  		case "(google.api.field_behavior)":
   801  			if o.Constant.Source == "REQUIRED" {
   802  				p.required = true
   803  			}
   804  		default:
   805  			// TODO: dropping comments. Maybe add dummy tag?
   806  
   807  			// TODO: should CUE support nested attributes?
   808  			source := o.Constant.SourceRepresentation()
   809  			p.tags += ","
   810  			switch source {
   811  			case "true":
   812  				p.tags += quoteOption(o.Name)
   813  			default:
   814  				p.tags += quoteOption(o.Name + "=" + source)
   815  			}
   816  		}
   817  	}
   818  }
   819  
   820  func quoteOption(s string) string {
   821  	needQuote := false
   822  	for _, r := range s {
   823  		if !unicode.In(r, unicode.L, unicode.N) {
   824  			needQuote = true
   825  			break
   826  		}
   827  	}
   828  	if !needQuote {
   829  		return s
   830  	}
   831  	if !strings.ContainsAny(s, `"\`) {
   832  		return literal.String.Quote(s)
   833  	}
   834  	esc := `\#`
   835  	for strings.Contains(s, esc) {
   836  		esc += "#"
   837  	}
   838  	return esc[1:] + `"` + s + `"` + esc[1:]
   839  }