cmd/cue/cmd: support importing .proto files

The import command now supports .proto files.

It generates a file foo.proto.cue for a file foo.proto.

Change-Id: I6670563c149f54b8d10ee550226403cb50e3e84d
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/2001
Reviewed-by: Marcel van Lohuizen <mpvl@google.com>
diff --git a/cmd/cue/cmd/import.go b/cmd/cue/cmd/import.go
index bc4bf2d..05f016d 100644
--- a/cmd/cue/cmd/import.go
+++ b/cmd/cue/cmd/import.go
@@ -38,6 +38,7 @@
 	"cuelang.org/go/cue/parser"
 	"cuelang.org/go/cue/token"
 	"cuelang.org/go/internal"
+	"cuelang.org/go/internal/protobuf"
 	"cuelang.org/go/internal/third_party/yaml"
 	"github.com/spf13/cobra"
 	"golang.org/x/sync/errgroup"
@@ -51,9 +52,10 @@
 
 The following file formats are currently supported:
 
-  Format     Extensions
-    JSON       .json .jsonl .ndjson
-    YAML       .yaml .yml
+  Format       Extensions
+	JSON       .json .jsonl .ndjson
+	YAML       .yaml .yml
+	protobuf   .proto
 
 Files can either be specified explicitly, or inferred from the specified
 packages. In either case, the file extension is replaced with .cue. It will
@@ -211,6 +213,8 @@
 	parseStrings = importCmd.Flags().BoolP("recursive", "R", false, "recursively parse string values")
 
 	importCmd.Flags().String("fix", "", "apply given fix")
+
+	protoPaths = importCmd.Flags().StringArrayP("proto_path", "I", nil, "paths in which to search for imports")
 }
 
 var (
@@ -223,18 +227,22 @@
 	list         *bool
 	files        *bool
 	parseStrings *bool
+	protoPaths   *[]string
 )
 
-type importFunc func(path string, r io.Reader) ([]ast.Expr, error)
+type importStreamFunc func(path string, r io.Reader) ([]ast.Expr, error)
+type importFileFunc func(path string, r io.Reader) (*ast.File, error)
 
 type encodingInfo struct {
-	fn  importFunc
-	typ string
+	fnStream importStreamFunc
+	fnFile   importFileFunc
+	typ      string
 }
 
 var (
-	jsonEnc = &encodingInfo{handleJSON, "json"}
-	yamlEnc = &encodingInfo{handleYAML, "yaml"}
+	jsonEnc     = &encodingInfo{fnStream: handleJSON, typ: "json"}
+	yamlEnc     = &encodingInfo{fnStream: handleYAML, typ: "yaml"}
+	protodefEnc = &encodingInfo{fnFile: handleProtoDef, typ: "proto"}
 )
 
 func getExtInfo(ext string) *encodingInfo {
@@ -247,6 +255,8 @@
 		return jsonEnc
 	case "yaml":
 		return yamlEnc
+	case "protobuf":
+		return protodefEnc
 	}
 	return nil
 }
@@ -324,14 +334,42 @@
 	ext := filepath.Ext(filename)
 	handler := getExtInfo(ext)
 
-	if handler == nil {
+	switch {
+	case handler == nil:
 		return fmt.Errorf("unsupported extension %q", ext)
+
+	case handler.fnFile != nil:
+		file, err := handler.fnFile(filename, f)
+		if err != nil {
+			return err
+		}
+		file.Filename = filename
+		return processFile(cmd, file)
+
+	case handler.fnStream != nil:
+		objs, err := handler.fnStream(filename, f)
+		if err != nil {
+			return err
+		}
+		return processStream(cmd, pkg, filename, objs)
+
+	default:
+		panic("incorrect handler")
 	}
-	objs, err := handler.fn(filename, f)
-	if err != nil {
+}
+
+func processFile(cmd *cobra.Command, file *ast.File) (err error) {
+	name := file.Filename + ".cue"
+
+	buf := &bytes.Buffer{}
+	if err := format.Node(buf, file); err != nil {
 		return err
 	}
 
+	return ioutil.WriteFile(name, buf.Bytes(), 0644)
+}
+
+func processStream(cmd *cobra.Command, pkg, filename string, objs []ast.Expr) error {
 	if *files {
 		for i, f := range objs {
 			err := combineExpressions(cmd, pkg, newName(filename, i), f)
@@ -619,6 +657,10 @@
 	return objects, nil
 }
 
+func handleProtoDef(path string, r io.Reader) (f *ast.File, err error) {
+	return protobuf.Parse(path, r, &protobuf.Config{Paths: *protoPaths})
+}
+
 type hoister struct {
 	fields   map[string]bool
 	altNames map[string]*ast.Ident
diff --git a/cue/encoding/encoding.go b/cue/encoding/encoding.go
index c5f2ca6..8d4fe22 100644
--- a/cue/encoding/encoding.go
+++ b/cue/encoding/encoding.go
@@ -31,7 +31,7 @@
 
 // All returns all known encodings.
 func All() []*Encoding {
-	return []*Encoding{jsonEnc, yamlEnc}
+	return []*Encoding{jsonEnc, yamlEnc, protodefEnc}
 }
 
 // MapExtension returns the likely encoding for a given file extension.
@@ -40,8 +40,9 @@
 }
 
 var (
-	jsonEnc = &Encoding{name: "json"}
-	yamlEnc = &Encoding{name: "yaml"}
+	jsonEnc     = &Encoding{name: "json"}
+	yamlEnc     = &Encoding{name: "yaml"}
+	protodefEnc = &Encoding{name: "protobuf"}
 )
 
 // extensions maps a file extension to a Kind.
@@ -51,4 +52,5 @@
 	".ndjson": jsonEnc,
 	".yaml":   yamlEnc,
 	".yml":    yamlEnc,
+	".proto":  protodefEnc,
 }
diff --git a/internal/protobuf/parse.go b/internal/protobuf/parse.go
index 8b8ede9..19eb4ec 100644
--- a/internal/protobuf/parse.go
+++ b/internal/protobuf/parse.go
@@ -65,7 +65,7 @@
 		switch x := recover().(type) {
 		case nil:
 		case protoError:
-			err = &ProtoError{
+			err = &Error{
 				Filename: filename,
 				Path:     strings.Join(p.path, "."),
 				Err:      x.error,
diff --git a/internal/protobuf/protobuf.go b/internal/protobuf/protobuf.go
index 4280d9c..958e6fb 100644
--- a/internal/protobuf/protobuf.go
+++ b/internal/protobuf/protobuf.go
@@ -52,16 +52,16 @@
 	return p.file, nil
 }
 
-// ProtoError describes the location and cause of an error.
-type ProtoError struct {
+// Error describes the location and cause of an error.
+type Error struct {
 	Filename string
 	Path     string
 	Err      error
 }
 
-func (p *ProtoError) Unwrap() error { return p.Err }
+func (p *Error) Unwrap() error { return p.Err }
 
-func (p *ProtoError) Error() string {
+func (p *Error) Error() string {
 	if p.Path == "" {
 		return fmt.Sprintf("parse of file %q failed: %v", p.Filename, p.Err)
 	}