Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add --output-stream(-o), --no-api-output(-n) option #8

Merged
merged 1 commit into from
May 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading