encoding/protobuf: add Builder API
A Builder parses a collection of .proto file and
organizes them according to a CUE package
layout. The exising Parse function is expressed
in this new API.
Issue #5
Change-Id: I980e7654d2b666dd1c637ad6d80f513096907a0b
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/2364
Reviewed-by: Marcel van Lohuizen <mpvl@golang.org>
diff --git a/encoding/protobuf/parse.go b/encoding/protobuf/parse.go
index 0753199..ad49a04 100644
--- a/encoding/protobuf/parse.go
+++ b/encoding/protobuf/parse.go
@@ -24,21 +24,31 @@
"strconv"
"strings"
"text/scanner"
+ "unicode"
"cuelang.org/go/cue/ast"
+ "cuelang.org/go/cue/errors"
"cuelang.org/go/cue/parser"
"cuelang.org/go/cue/token"
"cuelang.org/go/internal/source"
"github.com/emicklei/proto"
- "golang.org/x/xerrors"
)
-type sharedState struct {
- paths []string
-}
+func (s *Builder) parse(filename string, src interface{}) (p *protoConverter, err error) {
+ if filename == "" {
+ return nil, errors.Newf(token.NoPos, "empty filename")
+ }
+ if r, ok := s.fileCache[filename]; ok {
+ return r.p, r.err
+ }
+ defer func() {
+ s.fileCache[filename] = result{p, err}
+ }()
-func (s *sharedState) parse(filename string, src interface{}) (p *protoConverter, err error) {
b, err := source.Read(filename, src)
+ if err != nil {
+ return nil, err
+ }
parser := proto.NewParser(bytes.NewReader(b))
if filename != "" {
@@ -46,14 +56,19 @@
}
d, err := parser.Parse()
if err != nil {
- return nil, xerrors.Errorf("protobuf: %w", err)
+ return nil, errors.Newf(token.NoPos, "protobuf: %v", err)
}
+ tfile := token.NewFile(filename, 0, len(b))
+ tfile.SetLinesForContent(b)
+
p = &protoConverter{
+ id: filename,
state: s,
- tfile: token.NewFile(filename, 0, len(b)),
+ tfile: tfile,
used: map[string]bool{},
symbols: map[string]bool{},
+ aliases: map[string]string{},
}
defer func() {
@@ -105,7 +120,9 @@
for _, e := range d.Elements {
switch x := e.(type) {
case *proto.Import:
- p.doImport(x)
+ if err := p.doImport(x); err != nil {
+ return nil, err
+ }
}
}
@@ -121,11 +138,14 @@
used = append(used, k)
}
sort.Strings(used)
+ p.sorted = used
for _, v := range used {
- imports.Specs = append(imports.Specs, &ast.ImportSpec{
+ spec := &ast.ImportSpec{
Path: &ast.BasicLit{Kind: token.STRING, Value: strconv.Quote(v)},
- })
+ }
+ imports.Specs = append(imports.Specs, spec)
+ p.file.Imports = append(p.file.Imports, spec)
}
if len(imports.Specs) == 0 {
@@ -138,11 +158,12 @@
// A protoConverter converts a proto definition to CUE. Proto files map to
// CUE files one to one.
type protoConverter struct {
- state *sharedState
+ state *Builder
tfile *token.File
proto3 bool
+ id string
protoPkg string
goPkg string
goPkgPath string
@@ -151,23 +172,26 @@
file *ast.File
inBody bool
- imports map[string]string
- used map[string]bool
+ sorted []string
+ used map[string]bool
path []string
scope []map[string]mapping // for symbols resolution within package.
symbols map[string]bool // symbols provided by package
+ aliases map[string]string // for shadowed packages
}
type mapping struct {
- ref string
- pkg *protoConverter
+ ref string
+ alias string // alias for the type, if exists.
+ pkg *protoConverter
}
type pkgInfo struct {
importPath string // the import path
goPath string // The Go import path
shortName string // Used for the cue package path, default is base of goPath
+ protoName string // the protobuf package name
}
func (p *protoConverter) toCUEPos(pos scanner.Position) token.Pos {
@@ -210,13 +234,52 @@
p.scope = p.scope[:len(p.scope)-1]
}
+func (p *protoConverter) uniqueTop(name string) string {
+ if len(p.path) == 0 {
+ return name
+ }
+ a := strings.SplitN(name, ".", 2)
+ if p.path[len(p.path)-1] == a[0] {
+ first := a[0]
+ alias, ok := p.aliases[first]
+ if !ok {
+ // TODO: this is likely to be okay, but find something better.
+ alias = "__" + first
+ p.file.Decls = append(p.file.Decls, &ast.Alias{
+ Ident: ast.NewIdent(alias),
+ Expr: ast.NewIdent(first),
+ })
+ p.aliases[first] = alias
+ }
+ if len(a) > 1 {
+ alias += "." + a[1]
+ }
+ return alias
+ }
+ return name
+}
+
+func (p *protoConverter) toExpr(pos scanner.Position, name string) (expr ast.Expr) {
+ a := strings.Split(name, ".")
+ for i, s := range a {
+ if i == 0 {
+ expr = &ast.Ident{NamePos: p.toCUEPos(pos), Name: s}
+ continue
+ }
+ expr = &ast.SelectorExpr{X: expr, Sel: ast.NewIdent(s)}
+ }
+ return expr
+}
+
func (p *protoConverter) resolve(pos scanner.Position, name string, options []*proto.Option) string {
if strings.HasPrefix(name, ".") {
return p.resolveTopScope(pos, name[1:], options)
}
for i := len(p.scope) - 1; i > 0; i-- {
if m, ok := p.scope[i][name]; ok {
- return m.ref
+ cueName := m.ref
+ cueName = strings.Replace(m.ref, ".", "_", -1)
+ return cueName
}
}
return p.resolveTopScope(pos, name, options)
@@ -232,8 +295,10 @@
if m, ok := p.scope[0][name[:i]]; ok {
if m.pkg != nil {
p.used[m.pkg.goPkgPath] = true
+ // TODO: do something more principled.
}
- return m.ref + name[i:]
+ cueName := strings.Replace(name[i:], ".", "_", -1)
+ return p.uniqueTop(m.ref + cueName)
}
}
if s, ok := protoToCUE(name, options); ok {
@@ -243,9 +308,9 @@
return ""
}
-func (p *protoConverter) doImport(v *proto.Import) {
+func (p *protoConverter) doImport(v *proto.Import) error {
if v.Filename == "cue/cue.proto" {
- return
+ return nil
}
filename := ""
@@ -259,6 +324,12 @@
break
}
+ if filename == "" {
+ err := errors.Newf(p.toCUEPos(v.Position), "could not find import %q", v.Filename)
+ p.state.addErr(err)
+ return err
+ }
+
p.mapBuiltinPackage(v.Position, v.Filename, filename == "")
imp, err := p.state.parse(filename, nil)
@@ -284,7 +355,7 @@
if imp.goPkgPath == p.goPkgPath {
pkg = nil
}
- p.scope[0][ref] = mapping{prefix + k, pkg}
+ p.scope[0][ref] = mapping{prefix + k, "", pkg}
}
}
if len(pkgNamespace) == 0 {
@@ -296,6 +367,7 @@
pkgNamespace = pkgNamespace[1:]
curNamespace = curNamespace[1:]
}
+ return nil
}
func (p *protoConverter) stringLit(pos scanner.Position, s string) *ast.BasicLit {
@@ -353,7 +425,10 @@
case *proto.Import:
// already handled.
- case *proto.Extensions:
+ case *proto.Service:
+ // TODO: handle services.
+
+ case *proto.Extensions, *proto.Reserved:
// no need to handle
default:
@@ -362,6 +437,11 @@
}
func (p *protoConverter) message(v *proto.Message) {
+ if v.IsExtend {
+ // TODO: we are not handling extensions as for now.
+ return
+ }
+
defer func(saved []string) { p.path = saved }(p.path)
p.path = append(p.path, v.Name)
@@ -383,7 +463,6 @@
f := &ast.Field{Label: ref, Value: s}
addComments(f, 1, v.Comment, nil)
- // In CUE a message is always defined at the top level.
p.file.Decls = append(p.file.Decls, f)
for i, e := range v.Elements {
@@ -408,12 +487,15 @@
}
case *proto.MapField:
+ defer func(saved []string) { p.path = saved }(p.path)
+ p.path = append(p.path, x.Name)
+
f := &ast.Field{}
// All keys are converted to strings.
// TODO: support integer keys.
f.Label = &ast.TemplateLabel{Ident: ast.NewIdent("_")}
- f.Value = ast.NewIdent(p.resolve(x.Position, x.Type, x.Options))
+ f.Value = p.toExpr(x.Position, p.resolve(x.Position, x.Type, x.Options))
name := p.ident(x.Position, x.Name)
f = &ast.Field{
@@ -440,11 +522,11 @@
case *proto.Oneof:
p.oneOf(x)
- case *proto.Extensions:
+ case *proto.Extensions, *proto.Reserved:
// no need to handle
default:
- failf(scanner.Position{}, "unsupported type %T", v)
+ failf(scanner.Position{}, "unsupported field type %T", v)
}
}
@@ -467,12 +549,16 @@
// Enums are always defined at the top level. The name of a nested enum
// will be prefixed with the name of its parent and an underscore.
func (p *protoConverter) enum(x *proto.Enum) {
+
if len(x.Elements) == 0 {
failf(x.Position, "empty enum")
}
name := p.subref(x.Position, x.Name)
+ defer func(saved []string) { p.path = saved }(p.path)
+ p.path = append(p.path, x.Name)
+
p.addNames(x.Elements)
if len(p.path) == 0 {
@@ -491,6 +577,9 @@
d := &ast.Field{Label: valueName, Value: valueMap}
// addComments(valueMap, 1, x.Comment, nil)
+ if strings.Contains(name.Name, "google") {
+ panic(name.Name)
+ }
p.file.Decls = append(p.file.Decls, enum, d)
// The line comments for an enum field need to attach after the '|', which
@@ -570,13 +659,16 @@
}
func (p *protoConverter) parseField(s *ast.StructLit, i int, x *proto.Field) *ast.Field {
+ defer func(saved []string) { p.path = saved }(p.path)
+ p.path = append(p.path, x.Name)
+
f := &ast.Field{}
addComments(f, i, x.Comment, x.InlineComment)
name := p.ident(x.Position, x.Name)
f.Label = name
typ := p.resolve(x.Position, x.Type, x.Options)
- f.Value = ast.NewIdent(typ)
+ f.Value = p.toExpr(x.Position, typ)
s.Elts = append(s.Elts, f)
o := optionParser{message: s, field: f}
@@ -635,7 +727,34 @@
// TODO: should CUE support nested attributes?
source := o.Constant.SourceRepresentation()
- p.tags += "," + quote("option("+o.Name+","+source+")")
+ p.tags += ","
+ switch source {
+ case "true":
+ p.tags += quoteOption(o.Name)
+ default:
+ p.tags += quoteOption(o.Name + "=" + source)
+ }
}
}
}
+
+func quoteOption(s string) string {
+ needQuote := false
+ for _, r := range s {
+ if !unicode.In(r, unicode.L, unicode.N) {
+ needQuote = true
+ break
+ }
+ }
+ if !needQuote {
+ return s
+ }
+ if !strings.ContainsAny(s, `"\`) {
+ return strconv.Quote(s)
+ }
+ esc := `\#`
+ for strings.Contains(s, esc) {
+ esc += "#"
+ }
+ return esc[1:] + `"` + s + `"` + esc[1:]
+}