cmd/cue/cmd: factor out task code

Move to directories in which the corresponding
.cue files will be put.

Updates #39

Change-Id: If3cb498b9e3fe6e10905c4d461c2942d9fbbd997
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/1923
Reviewed-by: Marcel van Lohuizen <mpvl@google.com>
diff --git a/cmd/cue/cmd/custom.go b/cmd/cue/cmd/custom.go
index 1866ff2..48c9f7c 100644
--- a/cmd/cue/cmd/custom.go
+++ b/cmd/cue/cmd/custom.go
@@ -26,14 +26,15 @@
 	"net/http"
 	"net/http/httptest"
 	"os"
-	"os/exec"
-	"strings"
 	"sync"
 
 	"cuelang.org/go/cue"
+	itask "cuelang.org/go/internal/task"
+	_ "cuelang.org/go/pkg/tool/cli" // Register tasks
+	_ "cuelang.org/go/pkg/tool/exec"
+	_ "cuelang.org/go/pkg/tool/http"
 	"github.com/spf13/cobra"
 	"golang.org/x/sync/errgroup"
-	"golang.org/x/xerrors"
 )
 
 const (
@@ -187,7 +188,7 @@
 			m.Lock()
 			obj := tasks.Lookup(t.name)
 			m.Unlock()
-			update, err := t.Run(ctx, obj)
+			update, err := t.Run(&itask.Context{ctx, stdout, stderr}, obj)
 			if err == nil && update != nil {
 				m.Lock()
 				root, err = root.Fill(update, spec.taskPath(t.name)...)
@@ -242,7 +243,7 @@
 }
 
 type task struct {
-	Runner
+	itask.Runner
 
 	index int
 	name  string
@@ -255,8 +256,8 @@
 	if err != nil {
 		return nil, err
 	}
-	rf, ok := runners[kind]
-	if !ok {
+	rf := itask.Lookup(kind)
+	if rf == nil {
 		return nil, fmt.Errorf("runner of kind %q not found", kind)
 	}
 	runner, err := rf(v)
@@ -272,202 +273,17 @@
 	}, nil
 }
 
-// A Runner defines a command type.
-type Runner interface {
-	// Init is called with the original configuration before any task is run.
-	// As a result, the configuration may be incomplete, but allows some
-	// validation before tasks are kicked off.
-	// Init(v cue.Value)
-
-	// Runner runs given the current value and returns a new value which is to
-	// be unified with the original result.
-	Run(ctx context.Context, v cue.Value) (results interface{}, err error)
-}
-
-// A RunnerFunc creates a Runner.
-type RunnerFunc func(v cue.Value) (Runner, error)
-
-var runners = map[string]RunnerFunc{
-	"print":      newPrintCmd,
-	"exec":       newExecCmd,
-	"http":       newHTTPCmd,
-	"testserver": newTestServerCmd,
-}
-
-type printCmd struct{}
-
-func newPrintCmd(v cue.Value) (Runner, error) {
-	return &printCmd{}, nil
-}
-
-func (c *printCmd) Run(ctx context.Context, v cue.Value) (res interface{}, err error) {
-	str, err := v.Lookup("text").String()
-	if err != nil {
-		return nil, err
-	}
-	fmt.Fprintln(stdout, str)
-	return nil, nil
-}
-
-type execCmd struct{}
-
-func newExecCmd(v cue.Value) (Runner, error) {
-	return &execCmd{}, nil
-}
-
-func (c *execCmd) Run(ctx context.Context, v cue.Value) (res interface{}, err error) {
-	// TODO: set environment variables, if defined.
-	var bin string
-	var args []string
-	doc := ""
-	switch v := v.Lookup("cmd"); v.Kind() {
-	case cue.StringKind:
-		str, _ := v.String()
-		if str == "" {
-			return cue.Value{}, errors.New("empty command")
-		}
-		doc = str
-		list := strings.Fields(str)
-		bin = list[0]
-		for _, s := range list[1:] {
-			args = append(args, s)
-		}
-
-	case cue.ListKind:
-		list, _ := v.List()
-		if !list.Next() {
-			return cue.Value{}, errors.New("empty command list")
-		}
-		bin, err = list.Value().String()
-		if err != nil {
-			return cue.Value{}, err
-		}
-		doc += bin
-		for list.Next() {
-			str, err := list.Value().String()
-			if err != nil {
-				return cue.Value{}, err
-			}
-			args = append(args, str)
-			doc += " " + str
-		}
-	}
-
-	cmd := exec.CommandContext(ctx, bin, args...)
-
-	if v := v.Lookup("stdin"); v.IsValid() {
-		if cmd.Stdin, err = v.Reader(); err != nil {
-			return nil, fmt.Errorf("cue: %v", err)
-		}
-	}
-	captureOut := v.Lookup("stdout").Exists()
-	if !captureOut {
-		cmd.Stdout = stdout
-	}
-	captureErr := v.Lookup("stderr").Exists()
-	if !captureErr {
-		cmd.Stderr = stderr
-	}
-
-	update := map[string]interface{}{}
-	if captureOut {
-		var stdout []byte
-		stdout, err = cmd.Output()
-		update["stdout"] = string(stdout)
-	} else {
-		err = cmd.Run()
-	}
-	update["success"] = err == nil
-	if err != nil {
-		if exit := (*exec.ExitError)(nil); xerrors.As(err, &exit) && captureErr {
-			update["stderr"] = string(exit.Stderr)
-		} else {
-			update = nil
-		}
-		err = fmt.Errorf("command %q failed: %v", doc, err)
-	}
-	return update, err
-}
-
-type httpCmd struct{}
-
-func newHTTPCmd(v cue.Value) (Runner, error) {
-	return &httpCmd{}, nil
-}
-
-func (c *httpCmd) Run(ctx context.Context, v cue.Value) (res interface{}, err error) {
-	// v.Validate()
-	var header, trailer http.Header
-	method := lookupString(v, "method")
-	u := lookupString(v, "url")
-	var r io.Reader
-	if obj := v.Lookup("request"); v.Exists() {
-		if v := obj.Lookup("body"); v.Exists() {
-			r, err = v.Reader()
-			if err != nil {
-				return nil, err
-			}
-		}
-		if header, err = parseHeaders(obj, "header"); err != nil {
-			return nil, err
-		}
-		if trailer, err = parseHeaders(obj, "trailer"); err != nil {
-			return nil, err
-		}
-	}
-	req, err := http.NewRequest(method, u, r)
-	if err != nil {
-		return nil, err
-	}
-	req.Header = header
-	req.Trailer = trailer
-
-	// TODO:
-	//  - retry logic
-	//  - TLS certs
-	resp, err := http.DefaultClient.Do(req)
-	if err != nil {
-		return nil, err
-	}
-	defer resp.Body.Close()
-	b, err := ioutil.ReadAll(resp.Body)
-	// parse response body and headers
-	return map[string]interface{}{
-		"response": map[string]interface{}{
-			"body":    string(b),
-			"header":  resp.Header,
-			"trailer": resp.Trailer,
-		},
-	}, err
-}
-
-func parseHeaders(obj cue.Value, label string) (http.Header, error) {
-	m := obj.Lookup(label)
-	if !m.Exists() {
-		return nil, nil
-	}
-	iter, err := m.Fields()
-	if err != nil {
-		return nil, err
-	}
-	var h http.Header
-	for iter.Next() {
-		str, err := iter.Value().String()
-		if err != nil {
-			return nil, err
-		}
-		h.Add(iter.Label(), str)
-	}
-	return h, nil
-}
-
 func isValid(v cue.Value) bool {
 	return v.Kind() == cue.BottomKind
 }
 
+func init() {
+	itask.Register("testserver", newTestServerCmd)
+}
+
 var testOnce sync.Once
 
-func newTestServerCmd(v cue.Value) (Runner, error) {
+func newTestServerCmd(v cue.Value) (itask.Runner, error) {
 	server := ""
 	testOnce.Do(func() {
 		s := httptest.NewServer(http.HandlerFunc(
@@ -487,6 +303,6 @@
 
 type testServerCmd string
 
-func (s testServerCmd) Run(ctx context.Context, v cue.Value) (x interface{}, err error) {
+func (s testServerCmd) Run(ctx *itask.Context, v cue.Value) (x interface{}, err error) {
 	return map[string]interface{}{"url": string(s)}, nil
 }
diff --git a/cue/gen.go b/cue/gen.go
index 683ea44..e5c2b26 100644
--- a/cue/gen.go
+++ b/cue/gen.go
@@ -320,6 +320,11 @@
 	}
 	if n := len(types); n != 1 && (n != 2 || types[1] != "error") {
 		fmt.Printf("Dropped func %s.%s: must have one return value or a value and an error %v\n", g.defaultPkg, x.Name.Name, types)
+		return
+	}
+
+	if !ast.IsExported(x.Name.Name) || x.Recv != nil {
+		return
 	}
 
 	g.sep()
diff --git a/internal/task/task.go b/internal/task/task.go
new file mode 100644
index 0000000..b494a1a
--- /dev/null
+++ b/internal/task/task.go
@@ -0,0 +1,62 @@
+// Copyright 2019 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 task provides a registry for tasks to be used by commands.
+package task
+
+import (
+	"context"
+	"io"
+	"sync"
+
+	"cuelang.org/go/cue"
+)
+
+// A Context provides context for running a task.
+type Context struct {
+	Context context.Context
+	Stdout  io.Writer
+	Stderr  io.Writer
+}
+
+// A RunnerFunc creates a Runner.
+type RunnerFunc func(v cue.Value) (Runner, error)
+
+// A Runner defines a command type.
+type Runner interface {
+	// Init is called with the original configuration before any task is run.
+	// As a result, the configuration may be incomplete, but allows some
+	// validation before tasks are kicked off.
+	// Init(v cue.Value)
+
+	// Runner runs given the current value and returns a new value which is to
+	// be unified with the original result.
+	Run(ctx *Context, v cue.Value) (results interface{}, err error)
+}
+
+// Register registers a task for cue commands.
+func Register(key string, f RunnerFunc) {
+	runners.Store(key, f)
+}
+
+// Lookup returns the RunnerFunc for a key.
+func Lookup(key string) RunnerFunc {
+	v, ok := runners.Load(key)
+	if !ok {
+		return nil
+	}
+	return v.(RunnerFunc)
+}
+
+var runners sync.Map
diff --git a/pkg/tool/cli/cli.go b/pkg/tool/cli/cli.go
new file mode 100644
index 0000000..1a5fb26
--- /dev/null
+++ b/pkg/tool/cli/cli.go
@@ -0,0 +1,45 @@
+// Copyright 2019 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 cli provides tasks dealing with a console.
+package cli
+
+import (
+	"fmt"
+
+	"cuelang.org/go/cue"
+	"cuelang.org/go/internal/task"
+)
+
+func init() {
+	task.Register("tool/cli.Print", newPrintCmd)
+
+	// For backwards compatibility.
+	task.Register("print", newPrintCmd)
+}
+
+type printCmd struct{}
+
+func newPrintCmd(v cue.Value) (task.Runner, error) {
+	return &printCmd{}, nil
+}
+
+func (c *printCmd) Run(ctx *task.Context, v cue.Value) (res interface{}, err error) {
+	str, err := v.Lookup("text").String()
+	if err != nil {
+		return nil, err
+	}
+	fmt.Fprintln(ctx.Stdout, str)
+	return nil, nil
+}
diff --git a/pkg/tool/exec/exec.go b/pkg/tool/exec/exec.go
new file mode 100644
index 0000000..169508e
--- /dev/null
+++ b/pkg/tool/exec/exec.go
@@ -0,0 +1,114 @@
+// Copyright 2019 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 exec defines tasks for running commands.
+package exec
+
+import (
+	"errors"
+	"fmt"
+	"os/exec"
+	"strings"
+
+	"cuelang.org/go/cue"
+	"cuelang.org/go/internal/task"
+	"golang.org/x/xerrors"
+)
+
+func init() {
+	task.Register("tool/exec.Run", newExecCmd)
+
+	// For backwards compatibility.
+	task.Register("exec", newExecCmd)
+}
+
+type execCmd struct{}
+
+func newExecCmd(v cue.Value) (task.Runner, error) {
+	return &execCmd{}, nil
+}
+
+func (c *execCmd) Run(ctx *task.Context, v cue.Value) (res interface{}, err error) {
+	// TODO: set environment variables, if defined.
+	var bin string
+	var args []string
+	doc := ""
+	switch v := v.Lookup("cmd"); v.Kind() {
+	case cue.StringKind:
+		str, _ := v.String()
+		if str == "" {
+			return cue.Value{}, errors.New("empty command")
+		}
+		doc = str
+		list := strings.Fields(str)
+		bin = list[0]
+		for _, s := range list[1:] {
+			args = append(args, s)
+		}
+
+	case cue.ListKind:
+		list, _ := v.List()
+		if !list.Next() {
+			return cue.Value{}, errors.New("empty command list")
+		}
+		bin, err = list.Value().String()
+		if err != nil {
+			return cue.Value{}, err
+		}
+		doc += bin
+		for list.Next() {
+			str, err := list.Value().String()
+			if err != nil {
+				return cue.Value{}, err
+			}
+			args = append(args, str)
+			doc += " " + str
+		}
+	}
+
+	cmd := exec.CommandContext(ctx.Context, bin, args...)
+
+	if v := v.Lookup("stdin"); v.IsValid() {
+		if cmd.Stdin, err = v.Reader(); err != nil {
+			return nil, fmt.Errorf("cue: %v", err)
+		}
+	}
+	captureOut := v.Lookup("stdout").Exists()
+	if !captureOut {
+		cmd.Stdout = ctx.Stdout
+	}
+	captureErr := v.Lookup("stderr").Exists()
+	if !captureErr {
+		cmd.Stderr = ctx.Stderr
+	}
+
+	update := map[string]interface{}{}
+	if captureOut {
+		var stdout []byte
+		stdout, err = cmd.Output()
+		update["stdout"] = string(stdout)
+	} else {
+		err = cmd.Run()
+	}
+	update["success"] = err == nil
+	if err != nil {
+		if exit := (*exec.ExitError)(nil); xerrors.As(err, &exit) && captureErr {
+			update["stderr"] = string(exit.Stderr)
+		} else {
+			update = nil
+		}
+		err = fmt.Errorf("command %q failed: %v", doc, err)
+	}
+	return update, err
+}
diff --git a/pkg/tool/http/http.go b/pkg/tool/http/http.go
new file mode 100644
index 0000000..65dfdbd
--- /dev/null
+++ b/pkg/tool/http/http.go
@@ -0,0 +1,112 @@
+// Copyright 2019 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 http provides tasks related to the HTTP protocol.
+package http
+
+import (
+	"io"
+	"io/ioutil"
+	"net/http"
+
+	"cuelang.org/go/cue"
+	"cuelang.org/go/internal/task"
+)
+
+func init() {
+	task.Register("tool/http.Do", newHTTPCmd)
+
+	// For backwards compatibility.
+	task.Register("http", newHTTPCmd)
+}
+
+type httpCmd struct{}
+
+func newHTTPCmd(v cue.Value) (task.Runner, error) {
+	return &httpCmd{}, nil
+}
+
+func lookupString(obj cue.Value, key string) string {
+	str, err := obj.Lookup(key).String()
+	if err != nil {
+		return ""
+	}
+	return str
+}
+
+func (c *httpCmd) Run(ctx *task.Context, v cue.Value) (res interface{}, err error) {
+	// v.Validate()
+	var header, trailer http.Header
+	method := lookupString(v, "method")
+	u := lookupString(v, "url")
+	var r io.Reader
+	if obj := v.Lookup("request"); v.Exists() {
+		if v := obj.Lookup("body"); v.Exists() {
+			r, err = v.Reader()
+			if err != nil {
+				return nil, err
+			}
+		}
+		if header, err = parseHeaders(obj, "header"); err != nil {
+			return nil, err
+		}
+		if trailer, err = parseHeaders(obj, "trailer"); err != nil {
+			return nil, err
+		}
+	}
+	req, err := http.NewRequest(method, u, r)
+	if err != nil {
+		return nil, err
+	}
+	req.Header = header
+	req.Trailer = trailer
+
+	// TODO:
+	//  - retry logic
+	//  - TLS certs
+	resp, err := http.DefaultClient.Do(req)
+	if err != nil {
+		return nil, err
+	}
+	defer resp.Body.Close()
+	b, err := ioutil.ReadAll(resp.Body)
+	// parse response body and headers
+	return map[string]interface{}{
+		"response": map[string]interface{}{
+			"body":    string(b),
+			"header":  resp.Header,
+			"trailer": resp.Trailer,
+		},
+	}, err
+}
+
+func parseHeaders(obj cue.Value, label string) (http.Header, error) {
+	m := obj.Lookup(label)
+	if !m.Exists() {
+		return nil, nil
+	}
+	iter, err := m.Fields()
+	if err != nil {
+		return nil, err
+	}
+	var h http.Header
+	for iter.Next() {
+		str, err := iter.Value().String()
+		if err != nil {
+			return nil, err
+		}
+		h.Add(iter.Label(), str)
+	}
+	return h, nil
+}