Skip to content

Commit

Permalink
add --output-stream(-o), --no-api-output(-n) option
Browse files Browse the repository at this point in the history
  • Loading branch information
fujiwara committed May 20, 2024
1 parent 40945ae commit 8b95ce3
Show file tree
Hide file tree
Showing 5 changed files with 109 additions and 34 deletions.
30 changes: 23 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,11 +80,13 @@ Arguments:
[<input>] 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.
Expand Down Expand Up @@ -123,20 +125,34 @@ $ 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
```

[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.
Expand Down
8 changes: 6 additions & 2 deletions cmd/aws-sdk-client-gen-gen/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
}
`
Expand Down
49 changes: 31 additions & 18 deletions cmd/aws-sdk-client-gen/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
Expand Down Expand Up @@ -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)
Expand Down
36 changes: 29 additions & 7 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
}

Expand Down
20 changes: 20 additions & 0 deletions param.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ type clientMethodParam struct {
InputBytes json.RawMessage
InputReader io.Reader
InputReaderLength *int64
OutputWriter io.Writer

awsCfg aws.Config
cleanup []func() error
Expand All @@ -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 {
Expand Down

0 comments on commit 8b95ce3

Please sign in to comment.