diff --git a/README.md b/README.md index 813d244..9db8e51 100644 --- a/README.md +++ b/README.md @@ -80,11 +80,13 @@ Arguments: [] input JSON Flags: - -h, --help Show context-sensitive help. - -i, --input-stream=STRING bind input filename or '-' to io.Reader field in the input struct - -c, --compact compact JSON output - -q, --query=STRING JMESPath query to apply to output - -v, --version show version + -h, --help Show context-sensitive help. + -i, --input-stream=STRING bind input filename or '-' to io.Reader field in the input struct + -o, --output-stream=STRING bind output filename or '-' to io.ReadCloser field in the output struct + -n, --no-api-output do not output API response into stdout + -c, --compact compact JSON output + -q, --query=STRING JMESPath query to apply to output + -v, --version show version ``` - `service`: AWS service name. @@ -123,7 +125,7 @@ $ aws-sdk-client-go ecs describe-clusters '{"Cluster":"default"}' #### `--input-stream` option -`--input-stream` option allows you to bind a file or stdin to the input struct. +`--input-stream` (`-i`) option allows you to bind a file or stdin to the input struct. ```console $ aws-sdk-client-go s3 put-object '{"Bucket": "my-bucket", "Key": "my.txt"}' --input-stream my.txt @@ -131,12 +133,26 @@ $ aws-sdk-client-go s3 put-object '{"Bucket": "my-bucket", "Key": "my.txt"}' --i [s3#PutObjectInput](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/s3#PutObjectInput) has `Body` field of `io.Reader`. `--input-stream` option binds the file to the field. -When the input struct has only one field of `io.Reader`, `aws-sdk-client-go` reads the file and binds it to the field automatically. (At now, all SDK input structs have only one field of `io.Reader`.) +When the input struct has only one field of `io.Reader`, `aws-sdk-client-go` reads the file and binds it to the field automatically. (Currently, all SDK input structs have at most one io.Reader field.) When the input struct has a "\*Length" field for the size of the content, `aws-sdk-client-go` sets the size of the content to the field automatically. For example, [s3#PutObjectInput](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/s3#PutObjectInput) has `ContentLength` field. If `--input-stream` is "-", `aws-sdk-client-go` reads from stdin. In this case, `aws-sdk-client-go` reads all contents into memory, so it is not suitable for large files. Consider using a file for large content. +#### `--output-stream` option + +`--output-stream` (`-o`) option allows you to bind the `io.ReadCloser` of the API output to a file or stdout. + +```console +$ aws-sdk-client-go s3 get-object '{"Bucket": "my-bucket", "Key": "my.txt"}' --output-stream my.txt +``` + +[s3#GetObjectOutput](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/s3#PutObjectOutput) has `Body` field of `io.ReadeCloser`. `--output-stream` option binds the file to the field. + +When the output struct has only one field of `io.ReadCloser`, `aws-sdk-client-go` copies it to the file automatically. (Currently, all SDK output structs have at most one io.ReadCloser field.) + +If `--output-stream` is "-", `aws-sdk-client-go` writes into stdout. The result of the API also writes to stdout by default. If you don't want to output the result, use `--no-api-output` (`-n`). + #### Query output by JMESPath `--query` option allows you to query the output by JMESPath like the AWS CLI. diff --git a/cmd/aws-sdk-client-gen-gen/main.go b/cmd/aws-sdk-client-gen-gen/main.go index 52b9eaf..70f7dbd 100644 --- a/cmd/aws-sdk-client-gen-gen/main.go +++ b/cmd/aws-sdk-client-gen-gen/main.go @@ -20,12 +20,16 @@ import ( ) func generateAll() { + var err error {{ range $key, $value := .Services }} {{- if eq (len $value) 0 }} - gen("{{ $key }}", reflect.TypeOf({{ $key }}.New({{ $key }}.Options{})), nil) + err = gen("{{ $key }}", reflect.TypeOf({{ $key }}.New({{ $key }}.Options{})), nil) {{- else }} - gen("{{ $key }}", reflect.TypeOf({{ $key }}.New({{ $key }}.Options{})), {{ printf "%#v" $value }}) + err = gen("{{ $key }}", reflect.TypeOf({{ $key }}.New({{ $key }}.Options{})), {{ printf "%#v" $value }}) {{- end }} + if err != nil { + panic("failed to generate {{ $key }}" + err.Error()) + } {{ end }} } ` diff --git a/cmd/aws-sdk-client-gen/main.go b/cmd/aws-sdk-client-gen/main.go index dce6c31..8eea352 100644 --- a/cmd/aws-sdk-client-gen/main.go +++ b/cmd/aws-sdk-client-gen/main.go @@ -28,21 +28,32 @@ import ( func {{ $.PkgName }}_{{ .Name }}(ctx context.Context, p *clientMethodParam) (any, error) { svc := {{ $.PkgName }}.NewFromConfig(p.awsCfg) var in {{ .Input }} - {{ if .InputReaderLengthField }} + {{- if .InputReaderLengthField }} p.mustInject("{{ .InputReaderLengthField }}", p.InputReaderLength) - {{ end }} - + {{- end }} if err := json.Unmarshal(p.InputBytes, &in); err != nil { return nil, fmt.Errorf("failed to unmarshal request: %w", err) } - if p.InputReader != nil { + if err := p.validate("{{ $.PkgName }}.{{ .Name }}", "{{ .InputReaderField }}", "{{ .OutputReadCloserField }}"); err != nil { + return nil, err + } {{- if .InputReaderField }} + if p.InputReader != nil { in.{{ .InputReaderField }} = p.InputReader - {{- else }} - return nil, fmt.Errorf("{{ $.PkgName }}.{{ .Name }}Input has no io.Reader field") + } {{- end }} + {{- if .OutputReadCloserField }} + if out, err := svc.{{ .Name }}(ctx, &in); err != nil { + return nil, err + } else { + if err := p.Output(out.{{ .OutputReadCloserField }}); err != nil { + return nil, err + } + return out, nil } + {{- else }} return svc.{{ .Name }}(ctx, &in) + {{- end }} } {{ end }} @@ -99,24 +110,26 @@ func gen(pkgName string, clientType reflect.Type, genNames []string) error { } } } + + var outputReadCloserField string + output := method.Type.Out(0).Elem() + for j := 0; j < output.NumField(); j++ { + field := output.Field(j) + if t := field.Type.String(); t == "io.ReadCloser" { + log.Printf("found %s field in %s.%sOutput %s %s", t, pkgName, method.Name, field.Name, t) + if outputReadCloserField != "" { + return fmt.Errorf("found multiple io.ReadCloser fields in %s.%sOutput", pkgName, method.Name) + } + outputReadCloserField = field.Name + } + } methods = append(methods, map[string]string{ "Name": method.Name, "Input": strings.TrimPrefix(params[2], "*"), "InputReaderField": inputReaderField, "InputReaderLengthField": inputReaderLengthField, + "OutputReadCloserField": outputReadCloserField, }) - /* - output := method.Type.Out(0) - if output.Kind() == reflect.Ptr { - output = output.Elem() - } - for j := 0; j < output.NumField(); j++ { - field := output.Field(j) - if t := field.Type.String(); strings.Contains(t, "io.") { - log.Printf("found %s field in %s.%sOutput %s %s", t, pkgName, method.Name, field.Name, t) - } - } - */ } tmpl, err := template.New("clientGen").Parse(templateStr) diff --git a/main.go b/main.go index 65e8084..d8cfe30 100644 --- a/main.go +++ b/main.go @@ -27,10 +27,12 @@ type CLI struct { Method string `arg:"" help:"method name" default:""` Input string `arg:"" help:"input JSON" default:"{}"` - InputStream string `short:"i" help:"bind input filename or '-' to io.Reader field in the input struct"` - Compact bool `short:"c" help:"compact JSON output"` - Query string `short:"q" help:"JMESPath query to apply to output"` - Version bool `short:"v" help:"show version"` + InputStream string `short:"i" help:"bind input filename or '-' to io.Reader field in the input struct"` + OutputStream string `short:"o" help:"bind output filename or '-' to io.ReadCloser field in the output struct"` + NoAPIOutput bool `short:"n" help:"do not output API response into stdout"` + Compact bool `short:"c" help:"compact JSON output"` + Query string `short:"q" help:"JMESPath query to apply to output"` + Version bool `short:"v" help:"show version"` w io.Writer } @@ -82,6 +84,10 @@ func (c *CLI) CallMethod(ctx context.Context) error { return err } + if c.NoAPIOutput { + return nil + } + if c.Query != "" { out, err = jmespath.Search(c.Query, out) if err != nil { @@ -110,9 +116,10 @@ func (c *CLI) clientMethodParam(ctx context.Context) (*clientMethodParam, error) return nil, err } p := &clientMethodParam{ - awsCfg: awsCfg, - InputBytes: json.RawMessage(c.Input), - InputReader: nil, + awsCfg: awsCfg, + InputBytes: json.RawMessage(c.Input), + InputReader: nil, + OutputWriter: nil, } switch c.InputStream { @@ -140,6 +147,21 @@ func (c *CLI) clientMethodParam(ctx context.Context) (*clientMethodParam, error) } p.InputReaderLength = aws.Int64(st.Size()) } + + switch c.OutputStream { + case "": + // do nothing + case "-": // stdout + p.OutputWriter = os.Stdout + default: + f, err := os.Create(c.OutputStream) + if err != nil { + return nil, fmt.Errorf("failed to create output file: %w", err) + } + p.OutputWriter = f + p.cleanup = append(p.cleanup, f.Close) + } + return p, nil } diff --git a/param.go b/param.go index d18bf1c..bb1316d 100644 --- a/param.go +++ b/param.go @@ -14,6 +14,7 @@ type clientMethodParam struct { InputBytes json.RawMessage InputReader io.Reader InputReaderLength *int64 + OutputWriter io.Writer awsCfg aws.Config cleanup []func() error @@ -27,6 +28,25 @@ func (p *clientMethodParam) Cleanup() { } } +func (p *clientMethodParam) Output(src io.ReadCloser) error { + if p.OutputWriter == nil { + return nil + } + defer src.Close() + _, err := io.Copy(p.OutputWriter, src) + return err +} + +func (p *clientMethodParam) validate(name, inputReaderField, outputReadCloserField string) error { + if p.InputReader != nil && inputReaderField == "" { + return fmt.Errorf("%sInput has not io.Reader field", name) + } + if p.OutputWriter != nil && outputReadCloserField == "" { + return fmt.Errorf("%sOutput has not io.ReadCloser field", name) + } + return nil +} + func (p *clientMethodParam) mustInject(field string, value *int64) { v := make(map[string]any) if err := json.Unmarshal(p.InputBytes, &v); err != nil {