From 957684fae2ba51dd2e33a1b8920053ff4c157111 Mon Sep 17 00:00:00 2001 From: Jesse Bouwman Date: Sat, 21 Aug 2021 11:47:54 -0700 Subject: [PATCH] Add Command.DisplayCLI method signature Following the approach described in https://github.com/ipfs/go-ipfs-cmds/issues/115, define a new method signature on `Command` that supports full processing of the `Response` object when text encoding is requested. Add an encoding check and dispatch to DisplayCLI in local, http client, and http handler code paths. Unblocks resolution of `encoding` option processing in multiple go-ipfs issues. - https://github.com/ipfs/go-ipfs/issues/7050 json encoding for `ls` - https://github.com/ipfs/go-ipfs/issues/1121 json encoding for `add` - https://github.com/ipfs/go-ipfs/issues/5594 json encoding for `stats bw` --- cli/parse.go | 2 +- cli/run.go | 2 +- command.go | 11 +++++++++++ executor.go | 30 ++++++++++++++++++++++++++++++ http/client.go | 6 ++++++ http/responseemitter.go | 2 +- 6 files changed, 50 insertions(+), 3 deletions(-) diff --git a/cli/parse.go b/cli/parse.go index 0ebe3a8c..e8538867 100644 --- a/cli/parse.go +++ b/cli/parse.go @@ -48,7 +48,7 @@ func Parse(ctx context.Context, input []string, stdin *os.File, root *cmds.Comma // if no encoding was specified by user, default to plaintext encoding // (if command doesn't support plaintext, use JSON instead) if enc := req.Options[cmds.EncLong]; enc == "" { - if req.Command.Encoders != nil && req.Command.Encoders[cmds.Text] != nil { + if req.Command.HasText() { req.SetOption(cmds.EncLong, cmds.Text) } else { req.SetOption(cmds.EncLong, cmds.JSON) diff --git a/cli/run.go b/cli/run.go index 46b68280..eca364e6 100644 --- a/cli/run.go +++ b/cli/run.go @@ -123,7 +123,7 @@ func Run(ctx context.Context, root *cmds.Command, encType := cmds.EncodingType(encTypeStr) // use JSON if text was requested but the command doesn't have a text-encoder - if _, ok := cmd.Encoders[encType]; encType == cmds.Text && !ok { + if encType == cmds.Text && !cmd.HasText() { req.Options[cmds.EncLong] = cmds.JSON } diff --git a/command.go b/command.go index f31b3de2..14852ece 100644 --- a/command.go +++ b/command.go @@ -11,6 +11,7 @@ package cmds import ( "errors" "fmt" + "io" "strings" files "github.com/ipfs/go-ipfs-files" @@ -66,6 +67,11 @@ type Command struct { // encoding. Encoders EncoderMap + // DisplayCLI provides console output in cases requiring + // access to a full response object rather than individual + // result values. It is always run in the local process. + DisplayCLI func(res Response, stdout, stderr io.Writer) error + // Helptext is the command's help text. Helptext HelpText @@ -194,6 +200,11 @@ func (c *Command) Resolve(pth []string) ([]*Command, error) { return cmds, nil } +// HasText is true if the Command has direct support for text output +func (c *Command) HasText() bool { + return c.DisplayCLI != nil || (c.Encoders != nil && c.Encoders[Text] != nil) +} + // Get resolves and returns the Command addressed by path func (c *Command) Get(path []string) (*Command, error) { cmds, err := c.Resolve(path) diff --git a/executor.go b/executor.go index 5bcfb7a5..9ea40406 100644 --- a/executor.go +++ b/executor.go @@ -2,6 +2,9 @@ package cmds import ( "context" + "io" + "io/ioutil" + "os" ) type Executor interface { @@ -32,6 +35,33 @@ type executor struct { root *Command } +// GetLocalEncoder provides special treatment for text encoding +// when Command.DisplayCLI field is non-nil, by defining an +// Encoder that delegates to a nested emitter that consumes a Response +// and writes to the underlying io.Writer using DisplayCLI. +func GetLocalEncoder(req *Request, w io.Writer, def EncodingType) (EncodingType, Encoder, error) { + encType, enc, err := GetEncoder(req, w, def) + if err != nil { + return encType, nil, err + } + + if req.Command.DisplayCLI != nil && encType == Text { + emitter, response := NewChanResponsePair(req) + go req.Command.DisplayCLI(response, w, ioutil.Discard) + return encType, &emitterEncoder{emitter: emitter}, nil + } + + return encType, enc, nil +} + +type emitterEncoder struct { + emitter ResponseEmitter +} + +func (enc *emitterEncoder) Encode(value interface{}) error { + return enc.emitter.Emit(value) +} + func (x *executor) Execute(req *Request, re ResponseEmitter, env Environment) error { cmd := req.Command diff --git a/http/client.go b/http/client.go index d19c9fb9..0a0589e4 100644 --- a/http/client.go +++ b/http/client.go @@ -7,6 +7,7 @@ import ( "net" "net/http" "net/url" + "os" "strings" "github.com/ipfs/go-ipfs-cmds" @@ -132,6 +133,11 @@ func (c *client) Execute(req *cmds.Request, re cmds.ResponseEmitter, env cmds.En } } + if cmd.DisplayCLI != nil && + cmds.GetEncoding(req, cmds.Undefined) == cmds.Text { + return cmd.DisplayCLI(res, os.Stdout, os.Stderr) + } + return cmds.Copy(re, res) } diff --git a/http/responseemitter.go b/http/responseemitter.go index e5d424e7..9c99da53 100644 --- a/http/responseemitter.go +++ b/http/responseemitter.go @@ -27,7 +27,7 @@ var ( // NewResponseEmitter returns a new ResponseEmitter. func NewResponseEmitter(w http.ResponseWriter, method string, req *cmds.Request, opts ...ResponseEmitterOption) (ResponseEmitter, error) { - encType, enc, err := cmds.GetEncoder(req, w, cmds.JSON) + encType, enc, err := cmds.GetLocalEncoder(req, w, cmds.JSON) if err != nil { return nil, err }