cmd/cue/cmd: move export to filetypes

Change-Id: Iea5d2e61758971566c4a0eab694b2ce397f0843a
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/5023
Reviewed-by: Marcel van Lohuizen <mpvl@golang.org>
diff --git a/cmd/cue/cmd/eval.go b/cmd/cue/cmd/eval.go
index 9318d94..3781d01 100644
--- a/cmd/cue/cmd/eval.go
+++ b/cmd/cue/cmd/eval.go
@@ -83,7 +83,8 @@
 )
 
 func runEval(cmd *Command, args []string) error {
-	instances := buildFromArgs(cmd, args)
+	b, err := parseArgs(cmd, args, nil)
+	exitOnErr(cmd, err, false)
 
 	var exprs []ast.Expr
 	for _, e := range flagExpression.StringArray(cmd) {
@@ -104,6 +105,7 @@
 		}
 	}
 
+	instances := b.instances()
 	for _, inst := range instances {
 		// TODO: use ImportPath or some other sanitized path.
 		if len(instances) > 1 {
diff --git a/cmd/cue/cmd/export.go b/cmd/cue/cmd/export.go
index 9403070..63e6ccd 100644
--- a/cmd/cue/cmd/export.go
+++ b/cmd/cue/cmd/export.go
@@ -15,16 +15,12 @@
 package cmd
 
 import (
-	"encoding/json"
-	"fmt"
-	"io"
-
 	"github.com/spf13/cobra"
 
-	"cuelang.org/go/cue"
 	"cuelang.org/go/cue/ast"
 	"cuelang.org/go/cue/parser"
-	"cuelang.org/go/pkg/encoding/yaml"
+	"cuelang.org/go/internal/encoding"
+	"cuelang.org/go/internal/filetypes"
 )
 
 // newExportCmd creates and export command
@@ -104,7 +100,8 @@
 }
 
 func runExport(cmd *Command, args []string) error {
-	instances := buildFromArgs(cmd, args)
+	b, err := parseArgs(cmd, args, nil)
+	exitOnErr(cmd, err, true)
 	w := cmd.OutOrStdout()
 
 	var exprs []ast.Expr
@@ -116,72 +113,30 @@
 		exprs = append(exprs, expr)
 	}
 
-	count := 0
+	format := flagMedia.String(cmd) + ":-"
+	f, err := filetypes.ParseFile(format, filetypes.Export)
+	exitOnErr(cmd, err, true)
 
-	for _, inst := range instances {
-		root := inst.Value()
+	cfg := &encoding.Config{
+		Out: w,
+	}
+
+	enc, err := encoding.NewEncoder(f, cfg)
+	exitOnErr(cmd, err, true)
+	defer enc.Close()
+
+	for _, inst := range b.instances() {
 		if exprs == nil {
-			err := exportValue(cmd, w, root, count)
-			exitIfErr(cmd, inst, err, true)
-			count++
+			err = enc.Encode(inst.Value())
+			exitOnErr(cmd, err, true)
 			continue
 		}
 		for _, e := range exprs {
 			v := inst.Eval(e)
-			exitIfErr(cmd, inst, v.Err(), true)
-			err := exportValue(cmd, w, v, count)
-			count++
-			exitIfErr(cmd, inst, err, true)
+			exitOnErr(cmd, v.Err(), true)
+			err = enc.Encode(v)
+			exitOnErr(cmd, err, true)
 		}
 	}
 	return nil
 }
-
-func exportValue(cmd *Command, w io.Writer, v cue.Value, i int) error {
-	switch media := flagMedia.String(cmd); media {
-	case "json":
-		return outputJSON(cmd, w, v)
-	case "text":
-		return outputText(w, v)
-	case "yaml":
-		return outputYAML(w, v, i)
-	default:
-		return fmt.Errorf("export: unknown format %q", media)
-	}
-}
-
-func outputJSON(cmd *Command, w io.Writer, v cue.Value) error {
-	e := json.NewEncoder(w)
-	e.SetIndent("", "    ")
-	e.SetEscapeHTML(flagEscape.Bool(cmd))
-
-	err := e.Encode(v)
-	if err != nil {
-		if x, ok := err.(*json.MarshalerError); ok {
-			err = x.Err
-		}
-		return err
-	}
-	return nil
-}
-
-func outputText(w io.Writer, v cue.Value) error {
-	str, err := v.String()
-	if err != nil {
-		return err
-	}
-	_, err = fmt.Fprint(w, str)
-	return err
-}
-
-func outputYAML(w io.Writer, v cue.Value, i int) error {
-	if i > 0 {
-		fmt.Fprintln(w, "---")
-	}
-	str, err := yaml.Marshal(v)
-	if err != nil {
-		return err
-	}
-	_, err = fmt.Fprint(w, str)
-	return err
-}
diff --git a/internal/encoding/encoder.go b/internal/encoding/encoder.go
new file mode 100644
index 0000000..305a13e
--- /dev/null
+++ b/internal/encoding/encoder.go
@@ -0,0 +1,184 @@
+// Copyright 2020 CUE Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package encoding
+
+import (
+	"encoding/json"
+	"fmt"
+	"io"
+	"os"
+
+	"cuelang.org/go/cue"
+	"cuelang.org/go/cue/ast"
+	"cuelang.org/go/cue/build"
+	"cuelang.org/go/cue/errors"
+	"cuelang.org/go/cue/token"
+	"cuelang.org/go/pkg/encoding/yaml"
+)
+
+// file.Format
+// file.Info
+
+// An Encoder converts CUE to various file formats, including CUE itself.
+// An Encoder allows
+type Encoder struct {
+	cfg       *Config
+	closer    io.Closer
+	interpret func(cue.Value) (*ast.File, error)
+	encFile   func(*ast.File) error
+	encValue  func(cue.Value) error
+	encInst   func(*cue.Instance) error
+}
+
+func (e Encoder) Close() error {
+	if e.closer == nil {
+		return nil
+	}
+	return e.closer.Close()
+}
+
+// NewEncoder writes content to the file with the given specification.
+func NewEncoder(f *build.File, cfg *Config) (*Encoder, error) {
+	w, closer, err := writer(f, cfg)
+	if err != nil {
+		return nil, err
+	}
+	e := &Encoder{
+		cfg:    cfg,
+		closer: closer,
+	}
+
+	switch f.Interpretation {
+	case "":
+	// case build.OpenAPI:
+	// 	// TODO: get encoding options
+	// 	cfg := openapi.Config{}
+	// 	i.interpret = func(inst *cue.Instance) (*ast.File, error) {
+	// 		return openapi.Generate(inst, cfg)
+	// 	}
+	// case build.JSONSchema:
+	// 	// TODO: get encoding options
+	// 	cfg := openapi.Config{}
+	// 	i.interpret = func(inst *cue.Instance) (*ast.File, error) {
+	// 		return jsonschmea.Generate(inst, cfg)
+	// 	}
+	default:
+		return nil, fmt.Errorf("unsupported interpretation %q", f.Interpretation)
+	}
+
+	switch f.Encoding {
+	case build.JSON, build.JSONL:
+		// SetEscapeHTML
+		d := json.NewEncoder(w)
+		d.SetIndent("", "    ")
+		d.SetEscapeHTML(cfg.EscapeHTML)
+		e.encValue = func(v cue.Value) error {
+			err := d.Encode(v)
+			if x, ok := err.(*json.MarshalerError); ok {
+				err = x.Err
+			}
+			return err
+		}
+
+	case build.YAML:
+		streamed := false
+		e.encValue = func(v cue.Value) error {
+			if streamed {
+				fmt.Fprintln(w, "---")
+			}
+			streamed = true
+
+			str, err := yaml.Marshal(v)
+			if err != nil {
+				return err
+			}
+			_, err = fmt.Fprint(w, str)
+			return err
+		}
+
+	case build.Text:
+		e.encValue = func(v cue.Value) error {
+			str, err := v.String()
+			if err != nil {
+				return err
+			}
+			_, err = fmt.Fprint(w, str)
+			return err
+		}
+
+	default:
+		return nil, fmt.Errorf("unsupported encoding %q", f.Encoding)
+	}
+
+	return e, nil
+}
+
+func (e *Encoder) EncodeFile(f *ast.File) error {
+	return e.encodeFile(f, e.interpret)
+}
+
+func (e *Encoder) EncodeExpr(x ast.Expr) error {
+	return e.EncodeFile(toFile(x))
+}
+
+func (e *Encoder) Encode(v cue.Value) error {
+	if e.interpret != nil {
+		f, err := e.interpret(v)
+		if err != nil {
+			return err
+		}
+		return e.encodeFile(f, nil)
+	}
+	if e.encValue != nil {
+		return e.encValue(v)
+	}
+	return e.encFile(valueToFile(v))
+}
+
+func (e *Encoder) encodeFile(f *ast.File, interpret func(cue.Value) (*ast.File, error)) error {
+	if interpret == nil && e.encFile != nil {
+		return e.encFile(f)
+	}
+	var r cue.Runtime
+	inst, err := r.CompileFile(f)
+	if err != nil {
+		return err
+	}
+	if interpret != nil {
+		return e.Encode(inst.Value())
+	}
+	return e.encValue(inst.Value())
+}
+
+func writer(f *build.File, cfg *Config) (io.Writer, io.Closer, error) {
+	if cfg.Out != nil {
+		return cfg.Out, nil, nil
+	}
+	path := f.Filename
+	if path == "-" {
+		if cfg.Stdout == nil {
+			return os.Stdout, nil, nil
+		}
+		return cfg.Stdout, nil, nil
+	}
+	if !cfg.Force {
+		if _, err := os.Stat(path); err == nil {
+			return nil, nil, errors.Wrapf(os.ErrExist, token.NoPos,
+				"error writing %q", path)
+		}
+	}
+	w, err := os.Create(path)
+	return w, w, err
+}
diff --git a/internal/encoding/encoding.go b/internal/encoding/encoding.go
index a032bde..300a3f1 100644
--- a/internal/encoding/encoding.go
+++ b/internal/encoding/encoding.go
@@ -22,10 +22,12 @@
 	"os"
 	"strings"
 
+	"cuelang.org/go/cue"
 	"cuelang.org/go/cue/ast"
 	"cuelang.org/go/cue/build"
 	"cuelang.org/go/encoding/json"
 	"cuelang.org/go/encoding/protobuf"
+	"cuelang.org/go/internal/filetypes"
 	"cuelang.org/go/internal/third_party/yaml"
 )
 
@@ -51,22 +53,35 @@
 	}
 }
 
-func (i *Decoder) File() *ast.File {
-	if i.file != nil {
-		return i.file
-	}
-	switch x := i.expr.(type) {
+func toFile(x ast.Expr) *ast.File {
+	switch x := x.(type) {
 	case nil:
 		return nil
 	case *ast.StructLit:
 		return &ast.File{Decls: x.Elts}
 	default:
-		return &ast.File{
-			Decls: []ast.Decl{&ast.EmbedDecl{Expr: i.expr}},
-		}
+		return &ast.File{Decls: []ast.Decl{&ast.EmbedDecl{Expr: x}}}
 	}
 }
 
+func valueToFile(v cue.Value) *ast.File {
+	switch x := v.Syntax().(type) {
+	case *ast.File:
+		return x
+	case ast.Expr:
+		return toFile(x)
+	default:
+		panic("unrreachable")
+	}
+}
+
+func (i *Decoder) File() *ast.File {
+	if i.file != nil {
+		return i.file
+	}
+	return toFile(i.expr)
+}
+
 func (i *Decoder) Err() error {
 	if i.err == io.EOF {
 		return nil
@@ -79,9 +94,17 @@
 }
 
 type Config struct {
-	Stdin     io.Reader
-	Stdout    io.Writer
-	ProtoPath []string
+	Mode filetypes.Mode
+
+	// Out specifies an overwrite destination.
+	Out    io.Writer
+	Stdin  io.Reader
+	Stdout io.Writer
+
+	Force bool // overwrite existing files.
+
+	EscapeHTML bool
+	ProtoPath  []string
 }
 
 // NewDecoder returns a stream of non-rooted data expressions. The encoding