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
+}