cmd/cue/cmd: move import to filetypes

Change-Id: I30e2a473f26e14ae231dda0f6a589427c993d288
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/5021
Reviewed-by: Marcel van Lohuizen <mpvl@golang.org>
diff --git a/cmd/cue/cmd/import.go b/cmd/cue/cmd/import.go
index e79ae38..ddd96d4 100644
--- a/cmd/cue/cmd/import.go
+++ b/cmd/cue/cmd/import.go
@@ -31,7 +31,7 @@
 
 	"cuelang.org/go/cue/ast"
 	"cuelang.org/go/cue/ast/astutil"
-	"cuelang.org/go/cue/encoding"
+	"cuelang.org/go/cue/build"
 	"cuelang.org/go/cue/format"
 	"cuelang.org/go/cue/literal"
 	"cuelang.org/go/cue/load"
@@ -40,6 +40,7 @@
 	"cuelang.org/go/encoding/json"
 	"cuelang.org/go/encoding/protobuf"
 	"cuelang.org/go/internal"
+	"cuelang.org/go/internal/encoding"
 	"cuelang.org/go/internal/third_party/yaml"
 )
 
@@ -247,93 +248,56 @@
 	flagWithContext flagName = "with-context"
 )
 
-type importStreamFunc func(path string, r io.Reader) ([]ast.Expr, error)
-type importFileFunc func(cmd *Command, path string, r io.Reader) (*ast.File, error)
+// TODO: factor out rooting of orphaned files.
 
-type encodingInfo struct {
-	fnStream importStreamFunc
-	fnFile   importFileFunc
-	typ      string
-}
-
-var (
-	jsonEnc     = &encodingInfo{fnStream: handleJSON, typ: "json"}
-	yamlEnc     = &encodingInfo{fnStream: handleYAML, typ: "yaml"}
-	protodefEnc = &encodingInfo{fnFile: handleProtoDef, typ: "proto"}
-)
-
-func getExtInfo(ext string) *encodingInfo {
-	enc := encoding.MapExtension(ext)
-	if enc == nil {
-		return nil
-	}
-	switch enc.Name() {
-	case "json":
-		return jsonEnc
-	case "yaml":
-		return yamlEnc
-	case "protobuf":
-		return protodefEnc
-	}
-	return nil
-}
-
-func runImport(cmd *Command, args []string) error {
+func runImport(cmd *Command, args []string) (err error) {
 	var group errgroup.Group
 
 	pkgFlag := flagPackage.String(cmd)
 
-	group.Go(func() (err error) {
-		if len(args) > 0 && len(filepath.Ext(args[0])) > len(".") {
-			for _, a := range args {
-				group.Go(func() error { return handleFile(cmd, pkgFlag, a) })
-			}
-			return nil
+	done := map[string]bool{}
+
+	b, err := parseArgs(cmd, args, &load.Config{DataFiles: true})
+	if err != nil {
+		return err
+	}
+	builds := b.insts
+	if b.orphanInstance != nil {
+		builds = append(builds, b.orphanInstance)
+	}
+	for _, pkg := range builds {
+		pkgName := pkgFlag
+		if pkgName == "" {
+			pkgName = pkg.PkgName
 		}
-
-		done := map[string]bool{}
-
-		inst := load.Instances(args, &load.Config{DataFiles: true})
-		for _, pkg := range inst {
-			pkgName := pkgFlag
-			if pkgName == "" {
-				pkgName = pkg.PkgName
-			}
-			if pkgName == "" && len(inst) > 1 {
-				return fmt.Errorf("must specify package name with the -p flag")
-			}
-			dir := pkg.Dir
-			if err := pkg.Err; err != nil {
-				return err
-			}
-			if done[dir] {
-				continue
-			}
-			done[dir] = true
-
-			files, err := ioutil.ReadDir(dir)
-			if err != nil {
-				return err
-			}
-			for _, file := range files {
-				ext := filepath.Ext(file.Name())
-				typ := flagType.String(cmd)
-				if enc := getExtInfo(ext); enc == nil || (typ != "" && typ != enc.typ) {
-					continue
-				}
-				path := filepath.Join(dir, file.Name())
-				group.Go(func() error { return handleFile(cmd, pkgName, path) })
-			}
+		// TODO: allow if there is a unique package name.
+		if pkgName == "" && len(builds) > 1 {
+			err = fmt.Errorf("must specify package name with the -p flag")
+			break
 		}
-		return nil
-	})
+		if err = pkg.Err; err != nil {
+			break
+		}
+		if done[pkg.Dir] {
+			continue
+		}
+		done[pkg.Dir] = true
 
-	err := group.Wait()
+		for _, f := range pkg.OrphanedFiles {
+			f := f // capture range var
+			group.Go(func() error { return handleFile(cmd, pkgName, f) })
+		}
+	}
+
+	err2 := group.Wait()
 	exitOnErr(cmd, err, true)
+	exitOnErr(cmd, err2, true)
 	return nil
 }
 
-func handleFile(cmd *Command, pkg, filename string) (err error) {
+func handleFile(cmd *Command, pkg string, file *build.File) (err error) {
+	filename := file.Filename
+	// filter file names
 	re, err := regexp.Compile(flagGlob.String(cmd))
 	if err != nil {
 		return err
@@ -341,37 +305,31 @@
 	if !re.MatchString(filepath.Base(filename)) {
 		return nil
 	}
-	f, err := os.Open(filename)
-	if err != nil {
-		return fmt.Errorf("error opening file: %v", err)
-	}
-	defer f.Close()
 
-	ext := filepath.Ext(filename)
-	handler := getExtInfo(ext)
-
-	switch {
-	case handler == nil:
-		return fmt.Errorf("unsupported extension %q", ext)
-
-	case handler.fnFile != nil:
-		file, err := handler.fnFile(cmd, filename, f)
-		if err != nil {
+	// TODO: consider unifying the two modes.
+	var objs []ast.Expr
+	i := encoding.NewDecoder(file, &encoding.Config{
+		Stdin:     stdin,
+		Stdout:    stdout,
+		ProtoPath: flagProtoPath.StringArray(cmd),
+	})
+	defer i.Close()
+	for ; !i.Done(); i.Next() {
+		if expr := i.Expr(); expr != nil {
+			objs = append(objs, expr)
+			continue
+		}
+		if err := processFile(cmd, i.File()); err != nil {
 			return err
 		}
-		file.Filename = filename
-		return processFile(cmd, file)
+	}
 
-	case handler.fnStream != nil:
-		objs, err := handler.fnStream(filename, f)
-		if err != nil {
+	if len(objs) > 0 {
+		if err := processStream(cmd, pkg, filename, objs); err != nil {
 			return err
 		}
-		return processStream(cmd, pkg, filename, objs)
-
-	default:
-		panic("incorrect handler")
 	}
+	return i.Err()
 }
 
 func processFile(cmd *Command, file *ast.File) (err error) {
@@ -587,40 +545,6 @@
 	return filename
 }
 
-func handleJSON(path string, r io.Reader) (objects []ast.Expr, err error) {
-	d := json.NewDecoder(nil, path, r)
-
-	for {
-		expr, err := d.Extract()
-		if err == io.EOF {
-			break
-		}
-		if err != nil {
-			return nil, err
-		}
-		objects = append(objects, expr)
-	}
-	return objects, nil
-}
-
-func handleYAML(path string, r io.Reader) (objects []ast.Expr, err error) {
-	d, err := yaml.NewDecoder(path, r)
-	if err != nil {
-		return nil, err
-	}
-	for i := 0; ; i++ {
-		expr, err := d.Decode()
-		if err == io.EOF {
-			break
-		}
-		if err != nil {
-			return nil, err
-		}
-		objects = append(objects, expr)
-	}
-	return objects, nil
-}
-
 func handleProtoDef(cmd *Command, path string, r io.Reader) (f *ast.File, err error) {
 	return protobuf.Extract(path, r, &protobuf.Config{Paths: flagProtoPath.StringArray(cmd)})
 }
@@ -674,7 +598,7 @@
 				return false
 			}
 
-			pkg := c.Import("encoding/" + enc.typ)
+			pkg := c.Import("encoding/" + enc)
 			if pkg == nil {
 				return false
 			}
@@ -696,32 +620,32 @@
 	})
 }
 
-func tryParse(str string) (s ast.Expr, format *encodingInfo) {
+func tryParse(str string) (s ast.Expr, pkg string) {
 	b := []byte(str)
 	if json.Valid(b) {
 		expr, err := parser.ParseExpr("", b)
 		if err != nil {
 			// TODO: report error
-			return nil, nil
+			return nil, ""
 		}
 		switch expr.(type) {
 		case *ast.StructLit, *ast.ListLit:
 		default:
-			return nil, nil
+			return nil, ""
 		}
-		return expr, jsonEnc
+		return expr, "json"
 	}
 
 	if expr, err := yaml.Unmarshal("", b); err == nil {
 		switch expr.(type) {
 		case *ast.StructLit, *ast.ListLit:
 		default:
-			return nil, nil
+			return nil, ""
 		}
-		return expr, yamlEnc
+		return expr, "yaml"
 	}
 
-	return nil, nil
+	return nil, ""
 }
 
 func (h *hoister) uniqueName(base, prefix, typ string) string {