diff --git a/README.md b/README.md index ab80d37f..822cb934 100644 --- a/README.md +++ b/README.md @@ -262,6 +262,12 @@ data (TYPE_BYTES) => \u65e5\u672c\u8a9e } ``` +Or add the flag `--bytes-from-file` to read bytes from the provided relative path +``` +> call UnaryBytes --bytes-from-file +data (TYPE_BYTES) => ../relative/path/to/file +``` + ### Client streaming RPC Client streaming RPC accepts some requests and then returns only one response. Finish request inputting with CTRL-D diff --git a/e2e/testdata/fixtures/teste2e_repl-call_--help.golden b/e2e/testdata/fixtures/teste2e_repl-call_--help.golden index 99c8cd71..3de07264 100644 --- a/e2e/testdata/fixtures/teste2e_repl-call_--help.golden +++ b/e2e/testdata/fixtures/teste2e_repl-call_--help.golden @@ -1,6 +1,7 @@ usage: call Options: - --dig-manually prompt asks whether to dig down if it encountered to a message field - --enrich enrich response output includes header, message, trailer and status + --bytes-from-file interpret TYPE_BYTES input as a relative path to a file + --dig-manually prompt asks whether to dig down if it encountered to a message field + --enrich enrich response output includes header, message, trailer and status diff --git a/fill/filler.go b/fill/filler.go index e44a840c..01f521fe 100644 --- a/fill/filler.go +++ b/fill/filler.go @@ -18,14 +18,20 @@ type Filler interface { Fill(v interface{}) error } +// InteractiveFillerOpts +// If DigManually is true, Fill asks whether to dig down if it encountered to a message field. +// If BytesFromFile is true, Fill will read the contents of the file from the provided relative path +type InteractiveFillerOpts struct { + DigManually, BytesFromFile bool +} + // Filler tries to correspond input text to a struct interactively. type InteractiveFiller interface { // Fill receives a struct v and corresponds input that is have internally to the struct. - // If digManually is true, Fill asks whether to dig down if it encountered to a message field. // Fill may return these errors: // // - io.EOF: At the end of input. // - ErrCodecMismatch: If v isn't a supported type. // - Fill(v interface{}, digManually bool) error + Fill(v interface{}, opts InteractiveFillerOpts) error } diff --git a/fill/proto/interactive_filler.go b/fill/proto/interactive_filler.go index f624e5fc..298aa69d 100644 --- a/fill/proto/interactive_filler.go +++ b/fill/proto/interactive_filler.go @@ -3,6 +3,7 @@ package proto import ( "fmt" "io" + "io/ioutil" "strings" "github.com/golang/protobuf/protoc-gen-go/descriptor" @@ -20,7 +21,8 @@ type InteractiveFiller struct { prefixFormat string state promptInputterState - digManually bool + digManually bool + bytesFromFile bool } // NewInteractiveFiller instantiates a new filler that fills each field interactively. @@ -35,8 +37,9 @@ func NewInteractiveFiller(prompt prompt.Prompt, prefixFormat string) *Interactiv // Fill let you input each field interactively by using a prompt. v will be set field values inputted by a prompt. // // Note that Fill resets the previous state when it is called again. -func (f *InteractiveFiller) Fill(v interface{}, digManually bool) error { - f.digManually = digManually +func (f *InteractiveFiller) Fill(v interface{}, opts fill.InteractiveFillerOpts) error { + f.digManually = opts.DigManually + f.bytesFromFile = opts.BytesFromFile msg, ok := v.(*dynamic.Message) if !ok { @@ -213,7 +216,7 @@ func (f *InteractiveFiller) inputField(dmsg *dynamic.Message, field *desc.FieldD f.state.color.Next() default: // Normal fields. f.prompt.SetPrefix(f.makePrefix(field)) - v, err := f.inputPrimitiveField(field) + v, err := f.inputPrimitiveField(field.GetType()) if err != nil { return err } @@ -301,7 +304,7 @@ func (f *InteractiveFiller) inputRepeatedField(dmsg *dynamic.Message, field *des // inputPrimitiveField reads an input and converts it to a Go type. // If CTRL+d is entered, inputPrimitiveField returns io.EOF. -func (f *InteractiveFiller) inputPrimitiveField(field *desc.FieldDescriptor) (interface{}, error) { +func (f *InteractiveFiller) inputPrimitiveField(fieldType descriptor.FieldDescriptorProto_Type) (interface{}, error) { in, err := f.prompt.Input() if errors.Is(err, io.EOF) { return "", io.EOF @@ -310,7 +313,28 @@ func (f *InteractiveFiller) inputPrimitiveField(field *desc.FieldDescriptor) (in return "", errors.Wrap(err, "failed to read user input") } - return convertValue(in, descriptor.FieldDescriptorProto_Type(descriptor.FieldDescriptorProto_Type_value[field.GetType().String()])) + v, err := convertValue(in, fieldType) + if err != nil { + return nil, err + } + + if fieldType == descriptor.FieldDescriptorProto_TYPE_BYTES { + return f.processBytesInput(v) + } + + return v, err +} + +func (f *InteractiveFiller) processBytesInput(v interface{}) (interface{}, error) { + if _, ok := v.([]byte); !ok { + return nil, errors.New("value is not of type bytes") + } + + if f.bytesFromFile { + return readFileFromRelativePath(string(v.([]byte))) + } + + return v, nil } func (f *InteractiveFiller) isSelectedOneOf(field *desc.FieldDescriptor) bool { @@ -438,3 +462,10 @@ func makePrefix(s string, field *desc.FieldDescriptor, ancestor []string, ancest func isOneOfField(field *desc.FieldDescriptor) bool { return field.GetOneOf() != nil } + +func readFileFromRelativePath(path string) ([]byte, error) { + if path == "" { + return nil, nil + } + return ioutil.ReadFile(path) +} diff --git a/fill/proto/interactive_filler_test.go b/fill/proto/interactive_filler_test.go index 7f4f546b..b466c870 100644 --- a/fill/proto/interactive_filler_test.go +++ b/fill/proto/interactive_filler_test.go @@ -1,16 +1,72 @@ -package proto_test +package proto import ( "testing" + "github.com/golang/protobuf/protoc-gen-go/descriptor" "github.com/ktr0731/evans/fill" - "github.com/ktr0731/evans/fill/proto" + "github.com/ktr0731/evans/prompt" ) +type testPrompt struct { + prompt.Prompt + + input string +} + +func (t *testPrompt) Input() (string, error) { + return t.input, nil +} + func TestInteractiveProtoFiller(t *testing.T) { - f := proto.NewInteractiveFiller(nil, "") - err := f.Fill("invalid type", false) + f := NewInteractiveFiller(nil, "") + err := f.Fill("invalid type", fill.InteractiveFillerOpts{}) if err != fill.ErrCodecMismatch { t.Errorf("must return fill.ErrCodecMismatch because the arg is invalid type, but got: %s", err) } + + tp := &testPrompt{ + input: "../../go.mod", + } + + f = NewInteractiveFiller(tp, "") + f.bytesFromFile = true + + var v interface{} + v, err = f.inputPrimitiveField(descriptor.FieldDescriptorProto_TYPE_BYTES) + if err != nil { + t.Error(err) + } + + if _, ok := v.([]byte); !ok { + t.Errorf("value should be of type []byte") + } + + fileContent, err := readFileFromRelativePath(tp.input) + if err != nil { + t.Error(err) + } + + if len(v.([]byte)) != len(fileContent) { + t.Error("contents should have the same length") + } + + tp = &testPrompt{ + input: "\\x6f\\x67\\x69\\x73\\x6f", + } + + f = NewInteractiveFiller(tp, "") + + v, err = f.inputPrimitiveField(descriptor.FieldDescriptorProto_TYPE_BYTES) + if err != nil { + t.Error(err) + } + + if _, ok := v.([]byte); !ok { + t.Errorf("value should be of type []byte") + } + + if string(v.([]byte)) != "ogiso" { + t.Error("unequal content") + } } diff --git a/repl/commands.go b/repl/commands.go index 2d7d2797..1ab1f341 100644 --- a/repl/commands.go +++ b/repl/commands.go @@ -155,7 +155,7 @@ func (c *showCommand) Run(w io.Writer, args []string) error { } type callCommand struct { - enrich, digManually bool + enrich, digManually, bytesFromFile bool } func (c *callCommand) FlagSet() (*pflag.FlagSet, bool) { @@ -163,6 +163,7 @@ func (c *callCommand) FlagSet() (*pflag.FlagSet, bool) { fs.Usage = func() {} // Disable help output when an error occurred. fs.BoolVar(&c.enrich, "enrich", false, "enrich response output includes header, message, trailer and status") fs.BoolVar(&c.digManually, "dig-manually", false, "prompt asks whether to dig down if it encountered to a message field") + fs.BoolVar(&c.bytesFromFile, "bytes-from-file", false, "interpret TYPE_BYTES input as a relative path to a file") return fs, true } @@ -195,7 +196,9 @@ func (c *callCommand) Run(w io.Writer, args []string) error { }, ) - err := usecase.CallRPCInteractively(context.Background(), w, args[0], c.digManually) + // here we create the request context + // we also add the call command flags here + err := usecase.CallRPCInteractively(context.Background(), w, args[0], c.digManually, c.bytesFromFile) if errors.Is(err, io.EOF) { return errors.New("inputting canceled") } diff --git a/usecase/call_rpc.go b/usecase/call_rpc.go index e95b74c5..4d725869 100644 --- a/usecase/call_rpc.go +++ b/usecase/call_rpc.go @@ -383,14 +383,14 @@ func (f *interactiveFiller) Fill(v interface{}) error { return f.fillFunc(v) } -func CallRPCInteractively(ctx context.Context, w io.Writer, rpcName string, digManually bool) error { - return dm.CallRPCInteractively(ctx, w, rpcName, digManually) +func CallRPCInteractively(ctx context.Context, w io.Writer, rpcName string, digManually, bytesFromFile bool) error { + return dm.CallRPCInteractively(ctx, w, rpcName, digManually, bytesFromFile) } -func (m *dependencyManager) CallRPCInteractively(ctx context.Context, w io.Writer, rpcName string, digManually bool) error { +func (m *dependencyManager) CallRPCInteractively(ctx context.Context, w io.Writer, rpcName string, digManually, bytesFromFile bool) error { return m.CallRPC(ctx, w, rpcName, &interactiveFiller{ fillFunc: func(v interface{}) error { - return m.interactiveFiller.Fill(v, digManually) + return m.interactiveFiller.Fill(v, fill.InteractiveFillerOpts{DigManually: digManually, BytesFromFile: bytesFromFile}) }, }) }