diff --git a/.github/actions/setup-go/action.yml b/.github/actions/setup-go/action.yml index f82d2ed..2f43bae 100644 --- a/.github/actions/setup-go/action.yml +++ b/.github/actions/setup-go/action.yml @@ -7,7 +7,7 @@ inputs: runs: using: composite steps: - - uses: stainless-api/retrieve-github-access-token@v1 + - uses: stainless-api/retrieve-github-access-token@1f03f929b746c5b03dcdafa2bebbb18ca5672e1a # v1.0.0 if: github.repository == 'stainless-sdks/courier-cli' id: get_token with: @@ -20,7 +20,7 @@ runs: run: git config --global url."https://x-access-token:${{ steps.get_token.outputs.github_access_token }}@github.com/stainless-sdks/courier-go".insteadOf "https://github.com/stainless-sdks/courier-go" - name: Setup go - uses: actions/setup-go@v5 + uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5.6.0 with: go-version-file: ./go.mod diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f1aa995..e2ee0a6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,7 +25,7 @@ jobs: if: (github.event_name == 'push' || github.event.pull_request.head.repo.fork) && (github.event_name != 'push' || github.event.head_commit.message != 'codegen metadata') steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: ./.github/actions/setup-go with: @@ -51,7 +51,7 @@ jobs: runs-on: ${{ github.repository == 'stainless-sdks/courier-cli' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} if: (github.event_name == 'push' || github.event.pull_request.head.repo.fork) && (github.event_name != 'push' || github.event.head_commit.message != 'codegen metadata') steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: ./.github/actions/setup-go with: @@ -66,7 +66,7 @@ jobs: run: ./scripts/bootstrap - name: Run goreleaser - uses: goreleaser/goreleaser-action@v6.1.0 + uses: goreleaser/goreleaser-action@9ed2f89a662bf1735a48bc8557fd212fa902bebf # v6.1.0 with: version: latest args: release --snapshot --clean --skip=publish @@ -78,7 +78,7 @@ jobs: github.repository == 'stainless-sdks/courier-cli' && !startsWith(github.ref, 'refs/heads/stl/') id: github-oidc - uses: actions/github-script@v8 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 with: script: core.setOutput('github_token', await core.getIDToken()); @@ -98,7 +98,7 @@ jobs: runs-on: ${{ github.repository == 'stainless-sdks/courier-cli' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} if: github.event_name == 'push' || github.event.pull_request.head.repo.fork steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: ./.github/actions/setup-go with: diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index cfa2f18..267bb2b 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -10,20 +10,21 @@ on: push: tags: - "v*" + workflow_dispatch: {} jobs: goreleaser: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 - name: Set up Go - uses: actions/setup-go@v5 + uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5.6.0 with: go-version-file: "go.mod" - name: Run GoReleaser - uses: goreleaser/goreleaser-action@v6.1.0 + uses: goreleaser/goreleaser-action@9ed2f89a662bf1735a48bc8557fd212fa902bebf # v6.1.0 with: version: latest args: release --clean diff --git a/.github/workflows/release-doctor.yml b/.github/workflows/release-doctor.yml index 423c57f..ffec563 100644 --- a/.github/workflows/release-doctor.yml +++ b/.github/workflows/release-doctor.yml @@ -12,7 +12,7 @@ jobs: if: github.repository == 'trycourier/courier-cli' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch' || startsWith(github.head_ref, 'release-please') || github.head_ref == 'next') steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Check release environment run: | diff --git a/.release-please-manifest.json b/.release-please-manifest.json index acdf570..bf0d036 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "3.4.2" + ".": "3.5.0" } \ No newline at end of file diff --git a/.stats.yml b/.stats.yml index 998581c..17321d4 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 103 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/courier%2Fcourier-0f56d7860a1be3ffaea5dea291b20ac92e31c94d54e41cc8a30462dacf2f2d37.yml -openapi_spec_hash: 7e7c6f963e83c5f626c09efa322f476a -config_hash: afcc4f6f8c33ca3f338589e32e086f56 +configured_endpoints: 117 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/courier/courier-8e7ad3d889c555ff9c381518b627b24b85e3eb7376bdc3689adc7a96ec78e537.yml +openapi_spec_hash: 53b3680aae719487c56efaa782bbe5b2 +config_hash: 10bd597dd6cc89023541bc551b6532b8 diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e0004b..b8d3766 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,44 @@ # Changelog +## 3.5.0 (2026-05-19) + +Full Changelog: [v3.4.2...v3.5.0](https://github.com/trycourier/courier-cli/compare/v3.4.2...v3.5.0) + +### Features + +* [C-18380] Journeys API reference: copy + naming cleanup ([3e5f0cd](https://github.com/trycourier/courier-cli/commit/3e5f0cd321b17598cf89d2e9fcc0a364bbc379d2)) +* [SUP-607] Add DELETE endpoint for Courier Create tenant templates ([5ad24c4](https://github.com/trycourier/courier-cli/commit/5ad24c484316bfbfe4e89a50ea3f98d55c897ea2)) +* **api:** add journeys methods and journeys:templates resource ([f4afb09](https://github.com/trycourier/courier-cli/commit/f4afb0962c0d224f65d5c3d2061629787ada86b1)) +* **cli:** add `--raw-output`/`-r` option to print raw (non-JSON) strings ([780a9c0](https://github.com/trycourier/courier-cli/commit/780a9c0e5dc5f889e1756e00b7712b58fd1c2cf6)) +* **cli:** alias parameters in data with `x-stainless-cli-data-alias` ([4c52102](https://github.com/trycourier/courier-cli/commit/4c5210271156ddab475383aa6d512d915c43e7b6)) +* **cli:** send filename and content type when reading input from files ([8de2857](https://github.com/trycourier/courier-cli/commit/8de28579f6a831bfdc78ec33b16bcf31c55d06bb)) +* support passing path and query params over stdin ([29f9b3f](https://github.com/trycourier/courier-cli/commit/29f9b3f993fbd72360b1ce65e77b26e03bbfce8f)) + + +### Bug Fixes + +* **cli:** correctly load zsh autocompletion ([c58ffcd](https://github.com/trycourier/courier-cli/commit/c58ffcd91a7c0fb5255b638a34a61c2c8146759c)) +* flags for nullable body scalar fields are strictly typed ([41b8096](https://github.com/trycourier/courier-cli/commit/41b809656f1b5a1d4ba73b91e9c60533f032e5f0)) + + +### Chores + +* **ci:** support manually triggering release workflow ([99e6955](https://github.com/trycourier/courier-cli/commit/99e6955357951c874d777c4a26a920d602b7ec72)) +* **cli:** fall back to JSON when using default "explore" with non-TTY ([09aeebd](https://github.com/trycourier/courier-cli/commit/09aeebd03bb1402bd278940cdeb8ea69b01517b6)) +* **cli:** switch long lists of positional args over to param structs ([389c820](https://github.com/trycourier/courier-cli/commit/389c8207c4c54f657dfd310ce5ab1d8d751cc357)) +* **cli:** use `ShowJSONOpts` as argument to `formatJSON` instead of many positionals ([747c709](https://github.com/trycourier/courier-cli/commit/747c7090e140eb34ff022c5d9e42454618efe5c4)) +* **internal:** codegen related update ([b152ea0](https://github.com/trycourier/courier-cli/commit/b152ea0392fc2ee9857dde8affe7746cc07fa0cd)) +* **internal:** codegen related update ([cd851ac](https://github.com/trycourier/courier-cli/commit/cd851acc33bb7aa94374078ce84ca0e69064eec3)) +* **internal:** codegen related update ([0f66682](https://github.com/trycourier/courier-cli/commit/0f66682369f79620f487884c535e41ad96304d95)) +* **internal:** codegen related update ([8f3872f](https://github.com/trycourier/courier-cli/commit/8f3872f856ca43e04183c8f965f8e0e7ec1e0177)) +* **internal:** more robust bootstrap script ([3934247](https://github.com/trycourier/courier-cli/commit/39342478242318700e674e0ed42b1950e7bce5a1)) +* redact api-key headers in debug logs ([26b9617](https://github.com/trycourier/courier-cli/commit/26b96175c773dae96c8eb7a877126c309f71040f)) + + +### Documentation + +* **api:** update notification parameter descriptions ([40e76a4](https://github.com/trycourier/courier-cli/commit/40e76a403eab7c2c445e532fcbce8494b9cef78a)) + ## 3.4.2 (2026-04-14) Full Changelog: [v3.4.1...v3.4.2](https://github.com/trycourier/courier-cli/compare/v3.4.1...v3.4.2) diff --git a/cmd/courier/main.go b/cmd/courier/main.go index d1de87e..b0197a2 100644 --- a/cmd/courier/main.go +++ b/cmd/courier/main.go @@ -43,7 +43,12 @@ func main() { fmt.Fprintf(os.Stderr, "%s %q: %d %s\n", apierr.Request.Method, apierr.Request.URL, apierr.Response.StatusCode, http.StatusText(apierr.Response.StatusCode)) format := app.String("format-error") json := gjson.Parse(apierr.RawJSON()) - show_err := cmd.ShowJSON(os.Stdout, "Error", json, format, app.String("transform-error")) + show_err := cmd.ShowJSON(json, cmd.ShowJSONOpts{ + ExplicitFormat: app.IsSet("format-error"), + Format: format, + Title: "Error", + Transform: app.String("transform-error"), + }) if show_err != nil { // Just print the original error: fmt.Fprintf(os.Stderr, "%s\n", err.Error()) diff --git a/internal/autocomplete/shellscripts/zsh_autocomplete.zsh b/internal/autocomplete/shellscripts/zsh_autocomplete.zsh index 4d4bdcd..d937171 100644 --- a/internal/autocomplete/shellscripts/zsh_autocomplete.zsh +++ b/internal/autocomplete/shellscripts/zsh_autocomplete.zsh @@ -1,5 +1,4 @@ -#!/bin/zsh -compdef ____APPNAME___zsh_autocomplete __APPNAME__ +#compdef __APPNAME__ ____APPNAME___zsh_autocomplete() { @@ -44,3 +43,14 @@ ____APPNAME___zsh_autocomplete() { ;; esac } + +# When installed in fpath (e.g., via Homebrew's zsh_completion stanza), this file +# is autoloaded as the function ___APPNAME__ and its body becomes that function's +# body. Detect that case via funcstack and dispatch to the completion function. +# When sourced (e.g., `source <(__APPNAME__ @completion zsh)`), register the +# function with compdef instead. +if [[ "${funcstack[1]}" = "___APPNAME__" ]]; then + ____APPNAME___zsh_autocomplete "$@" +else + compdef ____APPNAME___zsh_autocomplete __APPNAME__ +fi diff --git a/internal/debugmiddleware/debug_middleware.go b/internal/debugmiddleware/debug_middleware.go index f07b93b..647f1de 100644 --- a/internal/debugmiddleware/debug_middleware.go +++ b/internal/debugmiddleware/debug_middleware.go @@ -21,7 +21,12 @@ const redactedPlaceholder = "" // Headers known to contain sensitive information like an API key. Note that this exclude `Authorization`, // which is handled specially in `redactRequest` below. -var sensitiveHeaders = []string{} +var sensitiveHeaders = []string{ + "api-key", + "x-api-key", + "cookie", + "set-cookie", +} // RequestLogger is a middleware that logs HTTP requests and responses. type RequestLogger struct { diff --git a/internal/requestflag/innerflag.go b/internal/requestflag/innerflag.go index 102624f..528915f 100644 --- a/internal/requestflag/innerflag.go +++ b/internal/requestflag/innerflag.go @@ -14,7 +14,8 @@ import ( type InnerFlag[ T []any | []map[string]any | []DateTimeValue | []DateValue | []TimeValue | []string | []float64 | []int64 | []bool | any | map[string]any | DateTimeValue | DateValue | TimeValue | - string | float64 | int64 | bool, + string | float64 | int64 | bool | + *string | *float64 | *int64 | *bool | *DateTimeValue | *DateValue | *TimeValue, ] struct { Name string // name of the flag DefaultText string // default text of the flag for usage purposes @@ -22,14 +23,35 @@ type InnerFlag[ Aliases []string // aliases that are allowed for this flag Validator func(T) error // custom function to validate this flag value - OuterFlag cli.Flag // The flag on which this inner flag will set values - InnerField string // The inner field which this flag will set + OuterFlag cli.Flag // The flag on which this inner flag will set values + InnerField string // The inner field which this flag will set + DataAliases []string // alternate names recognized in YAML values passed as the outer flag + + // OuterIsArrayOfObjects tells an untyped outer flag (Flag[any], used for nullable + // complex schemas) to seed its underlying value as []map[string]any rather than + // map[string]any before SetInnerField runs. The hint is ignored for typed outer + // flags whose zero value already carries a dispatchable reflect.Kind. + OuterIsArrayOfObjects bool +} + +// GetDataAliases returns the aliases recognized when parsing inner field keys from piped or flag YAML. +func (f *InnerFlag[T]) GetDataAliases() []string { + return f.DataAliases +} + +// GetInnerField returns the API field name that this inner flag sets on its outer flag's value. +// For example, the flag --parent.foo targeting a parameter whose OpenAPI property name is "foo" +// would return "foo". This is distinct from the flag's CLI name and from any DataAliases entries. +func (f *InnerFlag[T]) GetInnerField() string { + return f.InnerField } type HasOuterFlag interface { cli.Flag SetOuterFlag(cli.Flag) GetOuterFlag() cli.Flag + GetInnerField() string + GetDataAliases() []string } func (f *InnerFlag[T]) SetOuterFlag(flag cli.Flag) { @@ -61,6 +83,10 @@ func (f *InnerFlag[T]) Set(name string, rawVal string) error { } } + if seeder, ok := f.OuterFlag.(InnerFieldSeeder); ok { + seeder.SeedInnerCollection(f.OuterIsArrayOfObjects) + } + if settableInnerField, ok := f.OuterFlag.(SettableInnerField); ok { settableInnerField.SetInnerField(f.InnerField, parsedValue) } else { @@ -121,6 +147,9 @@ func (f *InnerFlag[T]) TypeName() string { if ty == nil { return "" } + if ty.Kind() == reflect.Pointer { + ty = ty.Elem() + } // Get base type name with special handling for built-in types getTypeName := func(t reflect.Type) string { diff --git a/internal/requestflag/requestflag.go b/internal/requestflag/requestflag.go index bdef64f..77c4f1f 100644 --- a/internal/requestflag/requestflag.go +++ b/internal/requestflag/requestflag.go @@ -1,6 +1,7 @@ package requestflag import ( + "encoding/json" "fmt" "reflect" "strconv" @@ -12,13 +13,38 @@ import ( "github.com/urfave/cli/v3" ) +// formatForFlagSet converts a Go value parsed from YAML/JSON stdin data into a string +// that flag.Set (and thus parseCLIArg) can parse correctly for each flag type. +// Strings are returned as-is (parseCLIArg[string] assigns the raw value directly, so +// JSON-quoting must be avoided). Scalars use %v. Complex types (maps, slices) are +// JSON-encoded, which the yaml.Unmarshal default branch in parseCLIArg can parse. +func formatForFlagSet(val any) (string, error) { + switch v := val.(type) { + case string: + return v, nil + case bool, int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64: + return fmt.Sprintf("%v", val), nil + default: + b, err := json.Marshal(val) + if err != nil { + return "", fmt.Errorf("cannot format value %T for flag.Set: %w", val, err) + } + return string(b), nil + } +} + // Flag [T] is a generic flag base which can be used to implement the most // common interfaces used by urfave/cli. Additionally, it allows specifying // where in an HTTP request the flag values should be placed (e.g. query, body, etc.). +// +// Pointer-to-primitive type parameters (e.g. *string) are used for flags whose underlying +// schema is nullable. They give flags a tri-state: unset (excluded from the request), +// set to the literal "null" (nil pointer → JSON null), or set to a value (*v → JSON value). type Flag[ T []any | []map[string]any | []DateTimeValue | []DateValue | []TimeValue | []string | []float64 | []int64 | []bool | any | map[string]any | DateTimeValue | DateValue | TimeValue | - string | float64 | int64 | bool, + string | float64 | int64 | bool | + *string | *float64 | *int64 | *bool | *DateTimeValue | *DateValue | *TimeValue, ] struct { Name string // name of the flag Category string // category of the flag, if any @@ -36,6 +62,7 @@ type Flag[ HeaderPath string // location in the request header to put this flag's value BodyPath string // location in the request body to put this flag's value BodyRoot bool // if true, then use this value as the entire request body + PathParam string // name of the URL path parameter this flag's value maps to // Const, when true, marks this flag as a constant. The flag's Default value is used as the fixed value // and always included in the request (IsSet returns true). The user can still see and override the flag, @@ -48,6 +75,10 @@ type Flag[ // binary` in the OpenAPI spec. FileInput bool + // DataAliases is a list of alternate names for this parameter recognized when parsing piped YAML/JSON + // input. Values keyed by any alias are translated to the canonical API name before being sent. + DataAliases []string + // unexported fields for internal use count int // number of times the flag has been set hasBeenSet bool // whether the flag has been set from env or file @@ -63,8 +94,10 @@ type InRequest interface { GetQueryPath() string GetHeaderPath() string GetBodyPath() string + GetPathParam() string IsBodyRoot() bool IsFileInput() bool + GetDataAliases() []string } func (f Flag[T]) GetQueryPath() string { @@ -79,6 +112,10 @@ func (f Flag[T]) GetBodyPath() string { return f.BodyPath } +func (f Flag[T]) GetPathParam() string { + return f.PathParam +} + func (f Flag[T]) IsBodyRoot() bool { return f.BodyRoot } @@ -87,6 +124,10 @@ func (f Flag[T]) IsFileInput() bool { return f.FileInput } +func (f Flag[T]) GetDataAliases() []string { + return f.DataAliases +} + // The values that will be sent in different parts of a request. type RequestContents struct { Queries map[string]any @@ -94,7 +135,91 @@ type RequestContents struct { Body any } -// Extract query parameters, headers, and body values from command flags. +// ApplyStdinDataToFlags sets flag values from a parsed stdin data map for flags that have not already been +// set via the command line. This allows piped YAML/JSON data to satisfy path, query, and header parameters. +// Body parameters are excluded: they are already handled by the maps.Copy merge in flagOptions. +// For each unset flag, if the parsed data map contains a key matching the flag's QueryPath, HeaderPath, or +// PathParam (or any of its DataAliases), the flag is set to that value via flag.Set. +// +// Inner flags (those with an outer flag) are also handled: if the outer flag's body path key exists in the +// data map and contains a nested map with a key matching the inner flag's field (or aliases), the inner +// flag is set from that nested value. +func ApplyStdinDataToFlags(cmd *cli.Command, data map[string]any) error { + for _, flag := range cmd.Flags { + if flag.IsSet() { + continue + } + + // Handle inner flags: look for their value nested under the outer flag's body path. + if inner, ok := flag.(HasOuterFlag); ok { + outer, outerOk := inner.GetOuterFlag().(InRequest) + if !outerOk || outer.GetBodyPath() == "" { + continue + } + nested, ok := data[outer.GetBodyPath()].(map[string]any) + if !ok { + continue + } + innerField := inner.GetInnerField() + val, found := nested[innerField] + if !found { + for _, alias := range inner.GetDataAliases() { + if alias != "" && alias != innerField { + if v, ok := nested[alias]; ok { + val, found = v, true + break + } + } + } + } + if !found { + continue + } + setVal, err := formatForFlagSet(val) + if err != nil { + return fmt.Errorf("cannot format piped value for flag %q: %w", flag.Names()[0], err) + } + if err := flag.Set(flag.Names()[0], setVal); err != nil { + return fmt.Errorf("cannot set flag %q from piped data: %w", flag.Names()[0], err) + } + continue + } + + inReq, ok := flag.(InRequest) + if !ok { + continue + } + + // Try each request location in turn, checking the canonical path key and all aliases. + // Body params are excluded: they are already handled by the maps.Copy merge in flagOptions. + for _, path := range []string{inReq.GetQueryPath(), inReq.GetHeaderPath(), inReq.GetPathParam()} { + if path == "" { + continue + } + var val any + var found bool + for _, key := range append([]string{path}, inReq.GetDataAliases()...) { + if v, ok := data[key]; ok { + val, found = v, true + break + } + } + if !found { + continue + } + setVal, err := formatForFlagSet(val) + if err != nil { + return fmt.Errorf("cannot format piped value for flag %q: %w", flag.Names()[0], err) + } + if err := flag.Set(flag.Names()[0], setVal); err != nil { + return fmt.Errorf("cannot set flag %q from piped data: %w", flag.Names()[0], err) + } + break + } + } + return nil +} + func ExtractRequestContents(cmd *cli.Command) RequestContents { bodyMap := make(map[string]any) res := RequestContents{ @@ -277,7 +402,7 @@ func (f *Flag[T]) IsRequired() bool { } // Intentionally don't use `f.Required`, because request flags may be passed // over stdin as well as by flag. - if f.BodyPath != "" || f.BodyRoot { + if f.BodyPath != "" || f.BodyRoot || f.PathParam != "" || f.QueryPath != "" || f.HeaderPath != "" { return false } return f.Required @@ -332,6 +457,11 @@ func (f *Flag[T]) TypeName() string { if ty == nil { return "" } + // Deref pointer-typed flags so --help surfaces the pointee kind (e.g. "string"), not + // Go's pointer syntax. + if ty.Kind() == reflect.Pointer { + ty = ty.Elem() + } // Get base type name with special handling for built-in types getTypeName := func(t reflect.Type) string { @@ -387,6 +517,8 @@ func (f *Flag[T]) IsMultiValueFlag() bool { } func (f *Flag[T]) IsBoolFlag() bool { + // Flag[*bool] is deliberately not treated as a bool flag — the pointer form needs an + // explicit value (`--foo true`, `--foo null`) to disambiguate the tri-state. _, isBool := any(f.Default).(bool) return isBool } @@ -410,7 +542,8 @@ func (f Flag[T]) IsLocal() bool { type cliValue[ T []any | []map[string]any | []DateTimeValue | []DateValue | []TimeValue | []string | []float64 | []int64 | []bool | any | map[string]any | DateTimeValue | DateValue | TimeValue | string | - float64 | int64 | bool, + float64 | int64 | bool | + *string | *float64 | *int64 | *bool | *DateTimeValue | *DateValue | *TimeValue, ] struct { value T } @@ -420,12 +553,27 @@ type cliValue[ func parseCLIArg[ T []any | []map[string]any | []DateTimeValue | []DateValue | []TimeValue | []string | []float64 | []int64 | []bool | any | map[string]any | DateTimeValue | DateValue | TimeValue | string | - float64 | int64 | bool, + float64 | int64 | bool | + *string | *float64 | *int64 | *bool | *DateTimeValue | *DateValue | *TimeValue, ](value string) (T, error) { var parsedValue any var err error var empty T + + if value == "null" { + switch any(empty).(type) { + // Pointer-to-primitive: explicit nil gives the tri-state its "null" state + // (unset / null / value). Without this, numeric flags would fail to parse + // "null" and string flags would accept the literal word as a raw value. + case *string, *int64, *float64, *bool, *DateValue, *DateTimeValue, *TimeValue: + return empty, nil + // Maps marshal nil as JSON null natively; short-circuit avoids a YAML round-trip. + case map[string]any: + return empty, nil + } + } + switch any(empty).(type) { case string: parsedValue = value @@ -456,6 +604,48 @@ func parseCLIArg[ parsedValue = t } + // Pointer-to-primitive flags reach here only when `value != "null"`; we parse the + // pointee type and return its address so JSON marshaling emits the underlying value. + case *string: + v := value + parsedValue = &v + case *int64: + var v int64 + v, err = strconv.ParseInt(value, 0, 64) + if err == nil { + parsedValue = &v + } + case *float64: + var v float64 + v, err = strconv.ParseFloat(value, 64) + if err == nil { + parsedValue = &v + } + case *bool: + var v bool + v, err = strconv.ParseBool(value) + if err == nil { + parsedValue = &v + } + case *DateTimeValue: + var dt DateTimeValue + err = (&dt).Parse(value) + if err == nil { + parsedValue = &dt + } + case *DateValue: + var d DateValue + err = (&d).Parse(value) + if err == nil { + parsedValue = &d + } + case *TimeValue: + var t TimeValue + err = (&t).Parse(value) + if err == nil { + parsedValue = &t + } + default: if strings.HasPrefix(value, "@") { // File literals like @file.txt should work here @@ -492,6 +682,13 @@ func parseCLIArg[ } +// Ptr returns a pointer to its argument. It is used to initialize `Default` on pointer-typed +// Flag values, since Go does not allow taking the address of a composite literal's element +// or of an untyped constant. +func Ptr[T any](v T) *T { + return &v +} + // Assuming this string failed to parse as valid YAML, this function will // return true for strings that can reasonably be interpreted as a string literal, // like identifiers (`foo_bar`), UUIDs (`945b2f0c-8e89-487a-b02c-f851c69ea459`), @@ -585,6 +782,15 @@ func (c *cliValue[T]) String() string { // For basic types, use standard string representation return fmt.Sprintf("%v", v) + case *string, *int64, *float64, *bool, *DateTimeValue, *DateValue, *TimeValue: + // Pointer-to-primitive: nil renders as "null" (the CLI literal that produces it); + // non-nil derefs to the pointee's standard representation. + rv := reflect.ValueOf(v) + if rv.IsNil() { + return "null" + } + return fmt.Sprintf("%v", rv.Elem().Interface()) + default: // For complex types, convert to YAML yamlBytes, err := yaml.MarshalWithOptions(c.value, yaml.Flow(true)) @@ -696,6 +902,15 @@ type SettableInnerField interface { SetInnerField(string, any) } +// InnerFieldSeeder lets an InnerFlag prepare its outer flag's underlying value +// before dispatching SetInnerField. This is only meaningful for Flag[any] — +// the codegen output for nullable complex schemas — whose untyped-nil zero +// value would otherwise have no reflect.Kind for the inner-field switch to +// dispatch on. +type InnerFieldSeeder interface { + SeedInnerCollection(isArrayOfObjects bool) +} + func (f *Flag[T]) SetInnerField(field string, val any) { if f.value == nil { f.value = &cliValue[T]{} @@ -709,6 +924,33 @@ func (f *Flag[T]) SetInnerField(field string, val any) { } } +// SeedInnerCollection initializes a Flag[any]'s underlying value as an empty +// map[string]any or []map[string]any so subsequent SetInnerField calls have a +// dispatchable reflect.Kind. For typed Flag[T] this is a no-op: the type +// assertion fails and the existing reflect.Kind on the typed-nil zero value +// already routes correctly. +func (f *Flag[T]) SeedInnerCollection(isArrayOfObjects bool) { + if f.value == nil { + f.value = &cliValue[T]{} + } + cv, ok := f.value.(*cliValue[T]) + if !ok { + return + } + if reflect.ValueOf(cv.value).Kind() != reflect.Invalid { + return + } + if isArrayOfObjects { + if seed, ok := any([]map[string]any{}).(T); ok { + cv.value = seed + } + return + } + if seed, ok := any(map[string]any{}).(T); ok { + cv.value = seed + } +} + func (c *cliValue[T]) SetInnerField(field string, val any) { flagVal := c.value flagValReflect := reflect.ValueOf(flagVal) diff --git a/internal/requestflag/requestflag_test.go b/internal/requestflag/requestflag_test.go index 0e86e07..779bd57 100644 --- a/internal/requestflag/requestflag_test.go +++ b/internal/requestflag/requestflag_test.go @@ -1,6 +1,7 @@ package requestflag import ( + "encoding/json" "fmt" "testing" "time" @@ -616,6 +617,178 @@ func TestYamlHandling(t *testing.T) { }) } +// TestNullLiteralHandling pins how each Flag[T] type handles the literal value "null" +// when passed via the CLI. Pointer-typed flags serialize nil as JSON null, which is how +// nullable body fields (`anyOf: [T, null]` / `{nullable: true}`) let users clear a field +// via `--foo null`. Non-pointer primitive flags treat "null" as a raw value — these are +// non-nullable schemas where explicit null has no API semantics anyway. +func TestNullLiteralHandling(t *testing.T) { + t.Parallel() + + assertJSONBody := func(t *testing.T, value any, expected string) { + t.Helper() + body, err := json.Marshal(map[string]any{"foo": value}) + assert.NoError(t, err) + assert.JSONEq(t, expected, string(body)) + } + + t.Run("Flag[any] null sends JSON null", func(t *testing.T) { + t.Parallel() + cv := &cliValue[any]{} + assert.NoError(t, cv.Set("null")) + assertJSONBody(t, cv.Get(), `{"foo":null}`) + }) + + t.Run("Flag[string] null is the raw string \"null\"", func(t *testing.T) { + t.Parallel() + cv := &cliValue[string]{} + assert.NoError(t, cv.Set("null")) + assertJSONBody(t, cv.Get(), `{"foo":"null"}`) + }) + + t.Run("Flag[int64] null errors", func(t *testing.T) { + t.Parallel() + cv := &cliValue[int64]{} + assert.Error(t, cv.Set("null")) + }) + + t.Run("Flag[*string] null sends JSON null", func(t *testing.T) { + t.Parallel() + cv := &cliValue[*string]{} + assert.NoError(t, cv.Set("null")) + assertJSONBody(t, cv.Get(), `{"foo":null}`) + }) + + t.Run("Flag[*string] value sends the string", func(t *testing.T) { + t.Parallel() + cv := &cliValue[*string]{} + assert.NoError(t, cv.Set("1.1")) + assertJSONBody(t, cv.Get(), `{"foo":"1.1"}`) + }) + + t.Run("Flag[*int64] null sends JSON null", func(t *testing.T) { + t.Parallel() + cv := &cliValue[*int64]{} + assert.NoError(t, cv.Set("null")) + assertJSONBody(t, cv.Get(), `{"foo":null}`) + }) + + t.Run("Flag[*int64] value sends the integer", func(t *testing.T) { + t.Parallel() + cv := &cliValue[*int64]{} + assert.NoError(t, cv.Set("42")) + assertJSONBody(t, cv.Get(), `{"foo":42}`) + }) + + t.Run("Flag[*int64] invalid value errors", func(t *testing.T) { + t.Parallel() + cv := &cliValue[*int64]{} + assert.Error(t, cv.Set("not-an-int")) + }) + + t.Run("Flag[*bool] null sends JSON null", func(t *testing.T) { + t.Parallel() + cv := &cliValue[*bool]{} + assert.NoError(t, cv.Set("null")) + assertJSONBody(t, cv.Get(), `{"foo":null}`) + }) + + t.Run("Flag[*bool] value sends the boolean", func(t *testing.T) { + t.Parallel() + cv := &cliValue[*bool]{} + assert.NoError(t, cv.Set("true")) + assertJSONBody(t, cv.Get(), `{"foo":true}`) + }) + + t.Run("Flag[*float64] null sends JSON null", func(t *testing.T) { + t.Parallel() + cv := &cliValue[*float64]{} + assert.NoError(t, cv.Set("null")) + assertJSONBody(t, cv.Get(), `{"foo":null}`) + }) + + t.Run("Flag[*float64] value sends the float", func(t *testing.T) { + t.Parallel() + cv := &cliValue[*float64]{} + assert.NoError(t, cv.Set("1.5")) + assertJSONBody(t, cv.Get(), `{"foo":1.5}`) + }) + + t.Run("Flag[*float64] invalid value errors", func(t *testing.T) { + t.Parallel() + cv := &cliValue[*float64]{} + assert.Error(t, cv.Set("not-a-float")) + }) + + t.Run("Flag[*DateValue] null sends JSON null", func(t *testing.T) { + t.Parallel() + cv := &cliValue[*DateValue]{} + assert.NoError(t, cv.Set("null")) + assertJSONBody(t, cv.Get(), `{"foo":null}`) + }) + + t.Run("Flag[*DateValue] value sends the date", func(t *testing.T) { + t.Parallel() + cv := &cliValue[*DateValue]{} + assert.NoError(t, cv.Set("2023-05-15")) + assertJSONBody(t, cv.Get(), `{"foo":"2023-05-15"}`) + }) + + t.Run("Flag[*DateValue] invalid value errors", func(t *testing.T) { + t.Parallel() + cv := &cliValue[*DateValue]{} + assert.Error(t, cv.Set("not-a-date")) + }) + + t.Run("Flag[*DateTimeValue] null sends JSON null", func(t *testing.T) { + t.Parallel() + cv := &cliValue[*DateTimeValue]{} + assert.NoError(t, cv.Set("null")) + assertJSONBody(t, cv.Get(), `{"foo":null}`) + }) + + t.Run("Flag[*DateTimeValue] value sends the datetime", func(t *testing.T) { + t.Parallel() + cv := &cliValue[*DateTimeValue]{} + assert.NoError(t, cv.Set("2023-05-15T14:30:45Z")) + assertJSONBody(t, cv.Get(), `{"foo":"2023-05-15T14:30:45Z"}`) + }) + + t.Run("Flag[*DateTimeValue] invalid value errors", func(t *testing.T) { + t.Parallel() + cv := &cliValue[*DateTimeValue]{} + assert.Error(t, cv.Set("not-a-datetime")) + }) + + t.Run("Flag[*TimeValue] null sends JSON null", func(t *testing.T) { + t.Parallel() + cv := &cliValue[*TimeValue]{} + assert.NoError(t, cv.Set("null")) + assertJSONBody(t, cv.Get(), `{"foo":null}`) + }) + + t.Run("Flag[*TimeValue] value sends the time", func(t *testing.T) { + t.Parallel() + cv := &cliValue[*TimeValue]{} + assert.NoError(t, cv.Set("14:30:45")) + assertJSONBody(t, cv.Get(), `{"foo":"14:30:45"}`) + }) + + t.Run("Flag[*TimeValue] invalid value errors", func(t *testing.T) { + t.Parallel() + cv := &cliValue[*TimeValue]{} + assert.Error(t, cv.Set("not-a-time")) + }) + + // Nullable maps don't need pointer wrapping — a nil map already marshals as JSON null. + t.Run("Flag[map[string]any] null sends JSON null", func(t *testing.T) { + t.Parallel() + cv := &cliValue[map[string]any]{} + assert.NoError(t, cv.Set("null")) + assertJSONBody(t, cv.Get(), `{"foo":null}`) + }) +} + func TestFlagTypeNames(t *testing.T) { t.Parallel() @@ -646,3 +819,409 @@ func TestFlagTypeNames(t *testing.T) { }) } } + +// TestInnerFlagDispatchOnUntypedFlag pins inner-flag behavior for `Flag[any]`, +// which is the codegen output for nullable complex schemas (`anyOf: [T, null]` +// or `{nullable: true}`). The untyped-nil zero value carries no reflect.Kind, +// so SetInnerField has nowhere to dispatch the assignment — without explicit +// help the inner-field value silently drops. +func TestInnerFlagDispatchOnUntypedFlag(t *testing.T) { + t.Parallel() + + t.Run("nullable array of objects appends element from inner flag", func(t *testing.T) { + t.Parallel() + outer := &Flag[any]{Name: "mcp-server"} + assert.NoError(t, outer.PreParse()) + + nameFlag := &InnerFlag[string]{ + Name: "mcp-server.name", InnerField: "name", + OuterFlag: outer, OuterIsArrayOfObjects: true, + } + assert.NoError(t, nameFlag.Set("mcp-server.name", "first")) + + body, err := json.Marshal(map[string]any{"foo": outer.Get()}) + assert.NoError(t, err) + assert.JSONEq(t, `{"foo":[{"name":"first"}]}`, string(body)) + }) + + t.Run("nullable object sets field from inner flag", func(t *testing.T) { + t.Parallel() + outer := &Flag[any]{Name: "metadata"} + assert.NoError(t, outer.PreParse()) + + keyFlag := &InnerFlag[string]{ + Name: "metadata.key", InnerField: "key", OuterFlag: outer, + } + assert.NoError(t, keyFlag.Set("metadata.key", "value")) + + body, err := json.Marshal(map[string]any{"foo": outer.Get()}) + assert.NoError(t, err) + assert.JSONEq(t, `{"foo":{"key":"value"}}`, string(body)) + }) + + t.Run("multiple inner flags merge into the trailing element", func(t *testing.T) { + t.Parallel() + outer := &Flag[any]{Name: "mcp-server"} + assert.NoError(t, outer.PreParse()) + + nameFlag := &InnerFlag[string]{ + Name: "mcp-server.name", InnerField: "name", + OuterFlag: outer, OuterIsArrayOfObjects: true, + } + urlFlag := &InnerFlag[string]{ + Name: "mcp-server.url", InnerField: "url", + OuterFlag: outer, OuterIsArrayOfObjects: true, + } + assert.NoError(t, nameFlag.Set("mcp-server.name", "first")) + assert.NoError(t, urlFlag.Set("mcp-server.url", "https://example.com")) + + body, err := json.Marshal(map[string]any{"foo": outer.Get()}) + assert.NoError(t, err) + assert.JSONEq(t, `{"foo":[{"name":"first","url":"https://example.com"}]}`, string(body)) + }) +} + +func TestApplyStdinDataToFlags(t *testing.T) { + t.Parallel() + + t.Run("sets query path flag from piped data", func(t *testing.T) { + t.Parallel() + + flag := &Flag[string]{ + Name: "account-id", + QueryPath: "account_id", + } + assert.NoError(t, flag.PreParse()) + + data := map[string]any{"account_id": "acct_123"} + cmd := &cli.Command{Flags: []cli.Flag{flag}} + assert.NoError(t, ApplyStdinDataToFlags(cmd, data)) + + assert.True(t, flag.IsSet()) + assert.Equal(t, "acct_123", flag.Get()) + }) + + t.Run("sets header path flag from piped data", func(t *testing.T) { + t.Parallel() + + flag := &Flag[string]{ + Name: "idempotency-key", + HeaderPath: "Idempotency-Key", + } + assert.NoError(t, flag.PreParse()) + + data := map[string]any{"Idempotency-Key": "key-xyz"} + cmd := &cli.Command{Flags: []cli.Flag{flag}} + assert.NoError(t, ApplyStdinDataToFlags(cmd, data)) + + assert.True(t, flag.IsSet()) + assert.Equal(t, "key-xyz", flag.Get()) + }) + + t.Run("does not set body path flag from piped data", func(t *testing.T) { + t.Parallel() + + // Body params are handled by the maps.Copy merge in flagOptions, not by ApplyStdinDataToFlags. + flag := &Flag[string]{ + Name: "message", + BodyPath: "message", + } + assert.NoError(t, flag.PreParse()) + + data := map[string]any{"message": "hello world"} + cmd := &cli.Command{Flags: []cli.Flag{flag}} + assert.NoError(t, ApplyStdinDataToFlags(cmd, data)) + + assert.False(t, flag.IsSet()) + }) + + t.Run("does not override flag already set via CLI", func(t *testing.T) { + t.Parallel() + + flag := &Flag[string]{ + Name: "account-id", + QueryPath: "account_id", + } + assert.NoError(t, flag.PreParse()) + assert.NoError(t, flag.Set("account-id", "explicit_value")) + + data := map[string]any{"account_id": "piped_value"} + cmd := &cli.Command{Flags: []cli.Flag{flag}} + assert.NoError(t, ApplyStdinDataToFlags(cmd, data)) + + // The explicitly-set value should win. + assert.Equal(t, "explicit_value", flag.Get()) + }) + + t.Run("sets integer query flag from piped data", func(t *testing.T) { + t.Parallel() + + flag := &Flag[int64]{ + Name: "page-size", + QueryPath: "page_size", + } + assert.NoError(t, flag.PreParse()) + + data := map[string]any{"page_size": int64(50)} + cmd := &cli.Command{Flags: []cli.Flag{flag}} + assert.NoError(t, ApplyStdinDataToFlags(cmd, data)) + + assert.True(t, flag.IsSet()) + assert.Equal(t, int64(50), flag.Get()) + }) + + t.Run("sets boolean query flag from piped data", func(t *testing.T) { + t.Parallel() + + flag := &Flag[bool]{ + Name: "include-deleted", + QueryPath: "include_deleted", + } + assert.NoError(t, flag.PreParse()) + + data := map[string]any{"include_deleted": true} + cmd := &cli.Command{Flags: []cli.Flag{flag}} + assert.NoError(t, ApplyStdinDataToFlags(cmd, data)) + + assert.True(t, flag.IsSet()) + assert.Equal(t, true, flag.Get()) + }) + + t.Run("resolves query path flag via data alias", func(t *testing.T) { + t.Parallel() + + flag := &Flag[string]{ + Name: "account-id", + QueryPath: "account_id", + DataAliases: []string{"accountId", "account"}, + } + assert.NoError(t, flag.PreParse()) + + // Use one of the aliases as the key in piped data. + data := map[string]any{"accountId": "acct_alias"} + cmd := &cli.Command{Flags: []cli.Flag{flag}} + assert.NoError(t, ApplyStdinDataToFlags(cmd, data)) + + assert.True(t, flag.IsSet()) + assert.Equal(t, "acct_alias", flag.Get()) + }) + + t.Run("does not set body path flag via data alias", func(t *testing.T) { + t.Parallel() + + // Body params are handled by the maps.Copy merge in flagOptions, not by ApplyStdinDataToFlags. + flag := &Flag[string]{ + Name: "user-name", + BodyPath: "user_name", + DataAliases: []string{"userName", "username"}, + } + assert.NoError(t, flag.PreParse()) + + data := map[string]any{"userName": "alice"} + cmd := &cli.Command{Flags: []cli.Flag{flag}} + assert.NoError(t, ApplyStdinDataToFlags(cmd, data)) + + assert.False(t, flag.IsSet()) + }) + + t.Run("ignores flags with no matching key in piped data", func(t *testing.T) { + t.Parallel() + + flag := &Flag[string]{ + Name: "account-id", + QueryPath: "account_id", + } + assert.NoError(t, flag.PreParse()) + + data := map[string]any{"other_key": "value"} + cmd := &cli.Command{Flags: []cli.Flag{flag}} + assert.NoError(t, ApplyStdinDataToFlags(cmd, data)) + + assert.False(t, flag.IsSet()) + }) + + t.Run("ignores flags with no path set", func(t *testing.T) { + t.Parallel() + + flag := &Flag[string]{ + Name: "some-flag", + // No QueryPath, HeaderPath, or BodyPath + } + assert.NoError(t, flag.PreParse()) + + data := map[string]any{"some-flag": "value"} + cmd := &cli.Command{Flags: []cli.Flag{flag}} + assert.NoError(t, ApplyStdinDataToFlags(cmd, data)) + + assert.False(t, flag.IsSet()) + }) + + t.Run("handles multiple flags from piped data", func(t *testing.T) { + t.Parallel() + + accountFlag := &Flag[string]{ + Name: "account-id", + QueryPath: "account_id", + } + limitFlag := &Flag[int64]{ + Name: "limit", + QueryPath: "limit", + } + assert.NoError(t, accountFlag.PreParse()) + assert.NoError(t, limitFlag.PreParse()) + + data := map[string]any{ + "account_id": "acct_abc", + "limit": int64(25), + } + cmd := &cli.Command{Flags: []cli.Flag{accountFlag, limitFlag}} + assert.NoError(t, ApplyStdinDataToFlags(cmd, data)) + + assert.True(t, accountFlag.IsSet()) + assert.Equal(t, "acct_abc", accountFlag.Get()) + assert.True(t, limitFlag.IsSet()) + assert.Equal(t, int64(25), limitFlag.Get()) + }) + + t.Run("sets inner flag from nested piped data under outer body path", func(t *testing.T) { + t.Parallel() + + outer := &Flag[map[string]any]{ + Name: "address", + BodyPath: "address", + } + assert.NoError(t, outer.PreParse()) + + cityInner := &InnerFlag[string]{ + Name: "address.city", + InnerField: "city", + OuterFlag: outer, + } + + data := map[string]any{ + "address": map[string]any{"city": "San Francisco"}, + } + cmd := &cli.Command{Flags: []cli.Flag{outer, cityInner}} + assert.NoError(t, ApplyStdinDataToFlags(cmd, data)) + + // InnerFlag.IsSet() is always false by design; verify the value was written + // into the outer flag's underlying map instead. + outerVal, ok := outer.Get().(map[string]any) + assert.True(t, ok, "expected outer flag value to be map[string]any, got %T", outer.Get()) + assert.Equal(t, "San Francisco", outerVal["city"]) + }) + + t.Run("sets inner flag via data alias in nested piped data", func(t *testing.T) { + t.Parallel() + + outer := &Flag[map[string]any]{ + Name: "address", + BodyPath: "address", + } + assert.NoError(t, outer.PreParse()) + + cityInner := &InnerFlag[string]{ + Name: "address.city", + InnerField: "city", + DataAliases: []string{"cityName"}, + OuterFlag: outer, + } + + // Use the alias in piped data. + data := map[string]any{ + "address": map[string]any{"cityName": "Portland"}, + } + cmd := &cli.Command{Flags: []cli.Flag{outer, cityInner}} + assert.NoError(t, ApplyStdinDataToFlags(cmd, data)) + + // InnerFlag.IsSet() is always false by design; verify the value was written + // into the outer flag's underlying map instead. + outerVal, ok := outer.Get().(map[string]any) + assert.True(t, ok, "expected outer flag value to be map[string]any, got %T", outer.Get()) + assert.Equal(t, "Portland", outerVal["city"]) + }) + + t.Run("does not set inner flag when outer flag has no body path", func(t *testing.T) { + t.Parallel() + + outer := &Flag[map[string]any]{ + Name: "options", + // No BodyPath set + } + assert.NoError(t, outer.PreParse()) + + inner := &InnerFlag[string]{ + Name: "options.key", + InnerField: "key", + OuterFlag: outer, + } + + data := map[string]any{ + "options": map[string]any{"key": "value"}, + } + cmd := &cli.Command{Flags: []cli.Flag{outer, inner}} + assert.NoError(t, ApplyStdinDataToFlags(cmd, data)) + + assert.False(t, inner.IsSet()) + }) + + t.Run("does not set inner flag when piped data has no nested map for outer path", func(t *testing.T) { + t.Parallel() + + outer := &Flag[map[string]any]{ + Name: "address", + BodyPath: "address", + } + assert.NoError(t, outer.PreParse()) + + inner := &InnerFlag[string]{ + Name: "address.city", + InnerField: "city", + OuterFlag: outer, + } + + // The outer body path key is missing from the piped data. + data := map[string]any{"other": "value"} + cmd := &cli.Command{Flags: []cli.Flag{outer, inner}} + assert.NoError(t, ApplyStdinDataToFlags(cmd, data)) + + assert.False(t, inner.IsSet()) + }) + + t.Run("canonical path key takes precedence over alias when both are present", func(t *testing.T) { + t.Parallel() + + flag := &Flag[string]{ + Name: "account-id", + QueryPath: "account_id", + DataAliases: []string{"accountId"}, + } + assert.NoError(t, flag.PreParse()) + + // Both canonical and alias present — canonical should win because it's checked first. + data := map[string]any{ + "account_id": "canonical_value", + "accountId": "alias_value", + } + cmd := &cli.Command{Flags: []cli.Flag{flag}} + assert.NoError(t, ApplyStdinDataToFlags(cmd, data)) + + assert.True(t, flag.IsSet()) + assert.Equal(t, "canonical_value", flag.Get()) + }) + + t.Run("empty data map does not set any flags", func(t *testing.T) { + t.Parallel() + + flag := &Flag[string]{ + Name: "account-id", + QueryPath: "account_id", + } + assert.NoError(t, flag.PreParse()) + + cmd := &cli.Command{Flags: []cli.Flag{flag}} + assert.NoError(t, ApplyStdinDataToFlags(cmd, map[string]any{})) + + assert.False(t, flag.IsSet()) + }) +} diff --git a/pkg/cmd/audience.go b/pkg/cmd/audience.go index 9867947..7a7ff8f 100644 --- a/pkg/cmd/audience.go +++ b/pkg/cmd/audience.go @@ -5,7 +5,6 @@ package cmd import ( "context" "fmt" - "os" "github.com/tidwall/gjson" "github.com/trycourier/courier-cli/v3/internal/apiquery" @@ -21,8 +20,9 @@ var audiencesRetrieve = cli.Command{ Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "audience-id", - Required: true, + Name: "audience-id", + Required: true, + PathParam: "audience_id", }, }, Action: handleAudiencesRetrieve, @@ -35,10 +35,11 @@ var audiencesUpdate = requestflag.WithInnerFlags(cli.Command{ Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "audience-id", - Required: true, + Name: "audience-id", + Required: true, + PathParam: "audience_id", }, - &requestflag.Flag[any]{ + &requestflag.Flag[*string]{ Name: "description", Usage: "A description of the audience", BodyPath: "description", @@ -48,12 +49,12 @@ var audiencesUpdate = requestflag.WithInnerFlags(cli.Command{ Usage: "Filter configuration for audience membership containing an array of filter rules", BodyPath: "filter", }, - &requestflag.Flag[any]{ + &requestflag.Flag[*string]{ Name: "name", Usage: "The name of the audience", BodyPath: "name", }, - &requestflag.Flag[any]{ + &requestflag.Flag[*string]{ Name: "operator", Usage: "The logical operator (AND/OR) for the top-level filter", BodyPath: "operator", @@ -76,7 +77,7 @@ var audiencesList = cli.Command{ Usage: "Get the audiences associated with the authorization token.", Suggest: true, Flags: []cli.Flag{ - &requestflag.Flag[any]{ + &requestflag.Flag[*string]{ Name: "cursor", Usage: "A unique identifier that allows for fetching the next set of audiences", QueryPath: "cursor", @@ -92,8 +93,9 @@ var audiencesDelete = cli.Command{ Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "audience-id", - Required: true, + Name: "audience-id", + Required: true, + PathParam: "audience_id", }, }, Action: handleAudiencesDelete, @@ -106,10 +108,11 @@ var audiencesListMembers = cli.Command{ Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "audience-id", - Required: true, + Name: "audience-id", + Required: true, + PathParam: "audience_id", }, - &requestflag.Flag[any]{ + &requestflag.Flag[*string]{ Name: "cursor", Usage: "A unique identifier that allows for fetching the next set of members", QueryPath: "cursor", @@ -150,8 +153,15 @@ func handleAudiencesRetrieve(ctx context.Context, cmd *cli.Command) error { obj := gjson.ParseBytes(res) format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") - return ShowJSON(os.Stdout, "audiences retrieve", obj, format, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "audiences retrieve", + Transform: transform, + }) } func handleAudiencesUpdate(ctx context.Context, cmd *cli.Command) error { @@ -165,8 +175,6 @@ func handleAudiencesUpdate(ctx context.Context, cmd *cli.Command) error { return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := courier.AudienceUpdateParams{} - options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -178,6 +186,8 @@ func handleAudiencesUpdate(ctx context.Context, cmd *cli.Command) error { return err } + params := courier.AudienceUpdateParams{} + var res []byte options = append(options, option.WithResponseBodyInto(&res)) _, err = client.Audiences.Update( @@ -192,8 +202,15 @@ func handleAudiencesUpdate(ctx context.Context, cmd *cli.Command) error { obj := gjson.ParseBytes(res) format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") - return ShowJSON(os.Stdout, "audiences update", obj, format, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "audiences update", + Transform: transform, + }) } func handleAudiencesList(ctx context.Context, cmd *cli.Command) error { @@ -204,8 +221,6 @@ func handleAudiencesList(ctx context.Context, cmd *cli.Command) error { return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := courier.AudienceListParams{} - options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -217,6 +232,8 @@ func handleAudiencesList(ctx context.Context, cmd *cli.Command) error { return err } + params := courier.AudienceListParams{} + var res []byte options = append(options, option.WithResponseBodyInto(&res)) _, err = client.Audiences.List(ctx, params, options...) @@ -226,8 +243,15 @@ func handleAudiencesList(ctx context.Context, cmd *cli.Command) error { obj := gjson.ParseBytes(res) format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") - return ShowJSON(os.Stdout, "audiences list", obj, format, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "audiences list", + Transform: transform, + }) } func handleAudiencesDelete(ctx context.Context, cmd *cli.Command) error { @@ -266,8 +290,6 @@ func handleAudiencesListMembers(ctx context.Context, cmd *cli.Command) error { return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := courier.AudienceListMembersParams{} - options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -279,6 +301,8 @@ func handleAudiencesListMembers(ctx context.Context, cmd *cli.Command) error { return err } + params := courier.AudienceListMembersParams{} + var res []byte options = append(options, option.WithResponseBodyInto(&res)) _, err = client.Audiences.ListMembers( @@ -293,6 +317,13 @@ func handleAudiencesListMembers(ctx context.Context, cmd *cli.Command) error { obj := gjson.ParseBytes(res) format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") - return ShowJSON(os.Stdout, "audiences list-members", obj, format, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "audiences list-members", + Transform: transform, + }) } diff --git a/pkg/cmd/auditevent.go b/pkg/cmd/auditevent.go index 96fcdca..559731e 100644 --- a/pkg/cmd/auditevent.go +++ b/pkg/cmd/auditevent.go @@ -5,7 +5,6 @@ package cmd import ( "context" "fmt" - "os" "github.com/tidwall/gjson" "github.com/trycourier/courier-cli/v3/internal/apiquery" @@ -21,8 +20,9 @@ var auditEventsRetrieve = cli.Command{ Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "audit-event-id", - Required: true, + Name: "audit-event-id", + Required: true, + PathParam: "audit-event-id", }, }, Action: handleAuditEventsRetrieve, @@ -34,7 +34,7 @@ var auditEventsList = cli.Command{ Usage: "Fetch the list of audit events", Suggest: true, Flags: []cli.Flag{ - &requestflag.Flag[any]{ + &requestflag.Flag[*string]{ Name: "cursor", Usage: "A unique identifier that allows for fetching the next set of audit events.", QueryPath: "cursor", @@ -75,8 +75,15 @@ func handleAuditEventsRetrieve(ctx context.Context, cmd *cli.Command) error { obj := gjson.ParseBytes(res) format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") - return ShowJSON(os.Stdout, "audit-events retrieve", obj, format, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "audit-events retrieve", + Transform: transform, + }) } func handleAuditEventsList(ctx context.Context, cmd *cli.Command) error { @@ -87,8 +94,6 @@ func handleAuditEventsList(ctx context.Context, cmd *cli.Command) error { return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := courier.AuditEventListParams{} - options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -100,6 +105,8 @@ func handleAuditEventsList(ctx context.Context, cmd *cli.Command) error { return err } + params := courier.AuditEventListParams{} + var res []byte options = append(options, option.WithResponseBodyInto(&res)) _, err = client.AuditEvents.List(ctx, params, options...) @@ -109,6 +116,13 @@ func handleAuditEventsList(ctx context.Context, cmd *cli.Command) error { obj := gjson.ParseBytes(res) format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") - return ShowJSON(os.Stdout, "audit-events list", obj, format, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "audit-events list", + Transform: transform, + }) } diff --git a/pkg/cmd/auth.go b/pkg/cmd/auth.go index 8926ff2..3cc9182 100644 --- a/pkg/cmd/auth.go +++ b/pkg/cmd/auth.go @@ -5,7 +5,6 @@ package cmd import ( "context" "fmt" - "os" "github.com/tidwall/gjson" "github.com/trycourier/courier-cli/v3/internal/apiquery" @@ -45,8 +44,6 @@ func handleAuthIssueToken(ctx context.Context, cmd *cli.Command) error { return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := courier.AuthIssueTokenParams{} - options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -58,6 +55,8 @@ func handleAuthIssueToken(ctx context.Context, cmd *cli.Command) error { return err } + params := courier.AuthIssueTokenParams{} + var res []byte options = append(options, option.WithResponseBodyInto(&res)) _, err = client.Auth.IssueToken(ctx, params, options...) @@ -67,6 +66,13 @@ func handleAuthIssueToken(ctx context.Context, cmd *cli.Command) error { obj := gjson.ParseBytes(res) format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") - return ShowJSON(os.Stdout, "auth issue-token", obj, format, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "auth issue-token", + Transform: transform, + }) } diff --git a/pkg/cmd/automation.go b/pkg/cmd/automation.go index d486175..dddde80 100644 --- a/pkg/cmd/automation.go +++ b/pkg/cmd/automation.go @@ -5,7 +5,6 @@ package cmd import ( "context" "fmt" - "os" "github.com/tidwall/gjson" "github.com/trycourier/courier-cli/v3/internal/apiquery" @@ -44,8 +43,6 @@ func handleAutomationsList(ctx context.Context, cmd *cli.Command) error { return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := courier.AutomationListParams{} - options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -57,6 +54,8 @@ func handleAutomationsList(ctx context.Context, cmd *cli.Command) error { return err } + params := courier.AutomationListParams{} + var res []byte options = append(options, option.WithResponseBodyInto(&res)) _, err = client.Automations.List(ctx, params, options...) @@ -66,6 +65,13 @@ func handleAutomationsList(ctx context.Context, cmd *cli.Command) error { obj := gjson.ParseBytes(res) format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") - return ShowJSON(os.Stdout, "automations list", obj, format, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "automations list", + Transform: transform, + }) } diff --git a/pkg/cmd/automationinvoke.go b/pkg/cmd/automationinvoke.go index e3c4921..e05f527 100644 --- a/pkg/cmd/automationinvoke.go +++ b/pkg/cmd/automationinvoke.go @@ -5,7 +5,6 @@ package cmd import ( "context" "fmt" - "os" "github.com/tidwall/gjson" "github.com/trycourier/courier-cli/v3/internal/apiquery" @@ -25,7 +24,7 @@ var automationsInvokeInvokeAdHoc = requestflag.WithInnerFlags(cli.Command{ Required: true, BodyPath: "automation", }, - &requestflag.Flag[any]{ + &requestflag.Flag[*string]{ Name: "brand", BodyPath: "brand", }, @@ -37,11 +36,11 @@ var automationsInvokeInvokeAdHoc = requestflag.WithInnerFlags(cli.Command{ Name: "profile", BodyPath: "profile", }, - &requestflag.Flag[any]{ + &requestflag.Flag[*string]{ Name: "recipient", BodyPath: "recipient", }, - &requestflag.Flag[any]{ + &requestflag.Flag[*string]{ Name: "template", BodyPath: "template", }, @@ -54,7 +53,7 @@ var automationsInvokeInvokeAdHoc = requestflag.WithInnerFlags(cli.Command{ Name: "automation.steps", InnerField: "steps", }, - &requestflag.InnerFlag[any]{ + &requestflag.InnerFlag[*string]{ Name: "automation.cancelation-token", InnerField: "cancelation_token", }, @@ -67,15 +66,16 @@ var automationsInvokeInvokeByTemplate = cli.Command{ Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "template-id", - Required: true, + Name: "template-id", + Required: true, + PathParam: "templateId", }, - &requestflag.Flag[any]{ + &requestflag.Flag[*string]{ Name: "recipient", Required: true, BodyPath: "recipient", }, - &requestflag.Flag[any]{ + &requestflag.Flag[*string]{ Name: "brand", BodyPath: "brand", }, @@ -87,7 +87,7 @@ var automationsInvokeInvokeByTemplate = cli.Command{ Name: "profile", BodyPath: "profile", }, - &requestflag.Flag[any]{ + &requestflag.Flag[*string]{ Name: "template", BodyPath: "template", }, @@ -104,8 +104,6 @@ func handleAutomationsInvokeInvokeAdHoc(ctx context.Context, cmd *cli.Command) e return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := courier.AutomationInvokeInvokeAdHocParams{} - options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -117,6 +115,8 @@ func handleAutomationsInvokeInvokeAdHoc(ctx context.Context, cmd *cli.Command) e return err } + params := courier.AutomationInvokeInvokeAdHocParams{} + var res []byte options = append(options, option.WithResponseBodyInto(&res)) _, err = client.Automations.Invoke.InvokeAdHoc(ctx, params, options...) @@ -126,8 +126,15 @@ func handleAutomationsInvokeInvokeAdHoc(ctx context.Context, cmd *cli.Command) e obj := gjson.ParseBytes(res) format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") - return ShowJSON(os.Stdout, "automations:invoke invoke-ad-hoc", obj, format, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "automations:invoke invoke-ad-hoc", + Transform: transform, + }) } func handleAutomationsInvokeInvokeByTemplate(ctx context.Context, cmd *cli.Command) error { @@ -141,8 +148,6 @@ func handleAutomationsInvokeInvokeByTemplate(ctx context.Context, cmd *cli.Comma return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := courier.AutomationInvokeInvokeByTemplateParams{} - options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -154,6 +159,8 @@ func handleAutomationsInvokeInvokeByTemplate(ctx context.Context, cmd *cli.Comma return err } + params := courier.AutomationInvokeInvokeByTemplateParams{} + var res []byte options = append(options, option.WithResponseBodyInto(&res)) _, err = client.Automations.Invoke.InvokeByTemplate( @@ -168,6 +175,13 @@ func handleAutomationsInvokeInvokeByTemplate(ctx context.Context, cmd *cli.Comma obj := gjson.ParseBytes(res) format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") - return ShowJSON(os.Stdout, "automations:invoke invoke-by-template", obj, format, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "automations:invoke invoke-by-template", + Transform: transform, + }) } diff --git a/pkg/cmd/brand.go b/pkg/cmd/brand.go index 03a6d7b..f2180e5 100644 --- a/pkg/cmd/brand.go +++ b/pkg/cmd/brand.go @@ -5,7 +5,6 @@ package cmd import ( "context" "fmt" - "os" "github.com/tidwall/gjson" "github.com/trycourier/courier-cli/v3/internal/apiquery" @@ -25,7 +24,7 @@ var brandsCreate = requestflag.WithInnerFlags(cli.Command{ Required: true, BodyPath: "name", }, - &requestflag.Flag[any]{ + &requestflag.Flag[*string]{ Name: "id", BodyPath: "id", }, @@ -69,8 +68,9 @@ var brandsRetrieve = cli.Command{ Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "brand-id", - Required: true, + Name: "brand-id", + Required: true, + PathParam: "brand_id", }, }, Action: handleBrandsRetrieve, @@ -83,8 +83,9 @@ var brandsUpdate = requestflag.WithInnerFlags(cli.Command{ Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "brand-id", - Required: true, + Name: "brand-id", + Required: true, + PathParam: "brand_id", }, &requestflag.Flag[string]{ Name: "name", @@ -131,7 +132,7 @@ var brandsList = cli.Command{ Usage: "Get the list of brands.", Suggest: true, Flags: []cli.Flag{ - &requestflag.Flag[any]{ + &requestflag.Flag[*string]{ Name: "cursor", Usage: "A unique identifier that allows for fetching the next set of brands.", QueryPath: "cursor", @@ -147,8 +148,9 @@ var brandsDelete = cli.Command{ Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "brand-id", - Required: true, + Name: "brand-id", + Required: true, + PathParam: "brand_id", }, }, Action: handleBrandsDelete, @@ -163,8 +165,6 @@ func handleBrandsCreate(ctx context.Context, cmd *cli.Command) error { return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := courier.BrandNewParams{} - options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -176,6 +176,8 @@ func handleBrandsCreate(ctx context.Context, cmd *cli.Command) error { return err } + params := courier.BrandNewParams{} + var res []byte options = append(options, option.WithResponseBodyInto(&res)) _, err = client.Brands.New(ctx, params, options...) @@ -185,8 +187,15 @@ func handleBrandsCreate(ctx context.Context, cmd *cli.Command) error { obj := gjson.ParseBytes(res) format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") - return ShowJSON(os.Stdout, "brands create", obj, format, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "brands create", + Transform: transform, + }) } func handleBrandsRetrieve(ctx context.Context, cmd *cli.Command) error { @@ -220,8 +229,15 @@ func handleBrandsRetrieve(ctx context.Context, cmd *cli.Command) error { obj := gjson.ParseBytes(res) format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") - return ShowJSON(os.Stdout, "brands retrieve", obj, format, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "brands retrieve", + Transform: transform, + }) } func handleBrandsUpdate(ctx context.Context, cmd *cli.Command) error { @@ -235,8 +251,6 @@ func handleBrandsUpdate(ctx context.Context, cmd *cli.Command) error { return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := courier.BrandUpdateParams{} - options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -248,6 +262,8 @@ func handleBrandsUpdate(ctx context.Context, cmd *cli.Command) error { return err } + params := courier.BrandUpdateParams{} + var res []byte options = append(options, option.WithResponseBodyInto(&res)) _, err = client.Brands.Update( @@ -262,8 +278,15 @@ func handleBrandsUpdate(ctx context.Context, cmd *cli.Command) error { obj := gjson.ParseBytes(res) format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") - return ShowJSON(os.Stdout, "brands update", obj, format, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "brands update", + Transform: transform, + }) } func handleBrandsList(ctx context.Context, cmd *cli.Command) error { @@ -274,8 +297,6 @@ func handleBrandsList(ctx context.Context, cmd *cli.Command) error { return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := courier.BrandListParams{} - options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -287,6 +308,8 @@ func handleBrandsList(ctx context.Context, cmd *cli.Command) error { return err } + params := courier.BrandListParams{} + var res []byte options = append(options, option.WithResponseBodyInto(&res)) _, err = client.Brands.List(ctx, params, options...) @@ -296,8 +319,15 @@ func handleBrandsList(ctx context.Context, cmd *cli.Command) error { obj := gjson.ParseBytes(res) format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") - return ShowJSON(os.Stdout, "brands list", obj, format, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "brands list", + Transform: transform, + }) } func handleBrandsDelete(ctx context.Context, cmd *cli.Command) error { diff --git a/pkg/cmd/bulk.go b/pkg/cmd/bulk.go index 3663ff9..2fd110c 100644 --- a/pkg/cmd/bulk.go +++ b/pkg/cmd/bulk.go @@ -5,7 +5,6 @@ package cmd import ( "context" "fmt" - "os" "github.com/tidwall/gjson" "github.com/trycourier/courier-cli/v3/internal/apiquery" @@ -21,8 +20,9 @@ var bulkAddUsers = requestflag.WithInnerFlags(cli.Command{ Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "job-id", - Required: true, + Name: "job-id", + Required: true, + PathParam: "job_id", }, &requestflag.Flag[[]map[string]any]{ Name: "user", @@ -48,7 +48,7 @@ var bulkAddUsers = requestflag.WithInnerFlags(cli.Command{ Usage: "User profile information. For email-based bulk jobs, `profile.email` is required \nfor provider routing to determine if the message can be delivered. The email \naddress should be provided here rather than in `to.email`.\n", InnerField: "profile", }, - &requestflag.InnerFlag[any]{ + &requestflag.InnerFlag[*string]{ Name: "user.recipient", Usage: "User ID (legacy field, use profile or to.user_id instead)", InnerField: "recipient", @@ -81,11 +81,11 @@ var bulkCreateJob = requestflag.WithInnerFlags(cli.Command{ Usage: "Event ID or Notification ID (required). Can be either a \nNotification ID (e.g., \"FRH3QXM9E34W4RKP7MRC8NZ1T8V8\") or a custom Event ID \n(e.g., \"welcome-email\") mapped to a notification.\n", InnerField: "event", }, - &requestflag.InnerFlag[any]{ + &requestflag.InnerFlag[*string]{ Name: "message.brand", InnerField: "brand", }, - &requestflag.InnerFlag[any]{ + &requestflag.InnerFlag[map[string]any]{ Name: "message.content", Usage: "Elemental content (optional, for V2 format). When provided, this will be used \ninstead of the notification associated with the `event` field.\n", InnerField: "content", @@ -102,7 +102,7 @@ var bulkCreateJob = requestflag.WithInnerFlags(cli.Command{ Name: "message.override", InnerField: "override", }, - &requestflag.InnerFlag[any]{ + &requestflag.InnerFlag[*string]{ Name: "message.template", Usage: "Notification ID or template ID (optional, for V2 format). When provided, \nthis will be used instead of the notification associated with the `event` field.\n", InnerField: "template", @@ -116,10 +116,11 @@ var bulkListUsers = cli.Command{ Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "job-id", - Required: true, + Name: "job-id", + Required: true, + PathParam: "job_id", }, - &requestflag.Flag[any]{ + &requestflag.Flag[*string]{ Name: "cursor", Usage: "A unique identifier that allows for fetching the next set of users added to the bulk job", QueryPath: "cursor", @@ -135,8 +136,9 @@ var bulkRetrieveJob = cli.Command{ Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "job-id", - Required: true, + Name: "job-id", + Required: true, + PathParam: "job_id", }, }, Action: handleBulkRetrieveJob, @@ -149,8 +151,9 @@ var bulkRunJob = cli.Command{ Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "job-id", - Required: true, + Name: "job-id", + Required: true, + PathParam: "job_id", }, }, Action: handleBulkRunJob, @@ -168,8 +171,6 @@ func handleBulkAddUsers(ctx context.Context, cmd *cli.Command) error { return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := courier.BulkAddUsersParams{} - options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -181,6 +182,8 @@ func handleBulkAddUsers(ctx context.Context, cmd *cli.Command) error { return err } + params := courier.BulkAddUsersParams{} + return client.Bulk.AddUsers( ctx, cmd.Value("job-id").(string), @@ -197,8 +200,6 @@ func handleBulkCreateJob(ctx context.Context, cmd *cli.Command) error { return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := courier.BulkNewJobParams{} - options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -210,6 +211,8 @@ func handleBulkCreateJob(ctx context.Context, cmd *cli.Command) error { return err } + params := courier.BulkNewJobParams{} + var res []byte options = append(options, option.WithResponseBodyInto(&res)) _, err = client.Bulk.NewJob(ctx, params, options...) @@ -219,8 +222,15 @@ func handleBulkCreateJob(ctx context.Context, cmd *cli.Command) error { obj := gjson.ParseBytes(res) format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") - return ShowJSON(os.Stdout, "bulk create-job", obj, format, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "bulk create-job", + Transform: transform, + }) } func handleBulkListUsers(ctx context.Context, cmd *cli.Command) error { @@ -234,8 +244,6 @@ func handleBulkListUsers(ctx context.Context, cmd *cli.Command) error { return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := courier.BulkListUsersParams{} - options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -247,6 +255,8 @@ func handleBulkListUsers(ctx context.Context, cmd *cli.Command) error { return err } + params := courier.BulkListUsersParams{} + var res []byte options = append(options, option.WithResponseBodyInto(&res)) _, err = client.Bulk.ListUsers( @@ -261,8 +271,15 @@ func handleBulkListUsers(ctx context.Context, cmd *cli.Command) error { obj := gjson.ParseBytes(res) format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") - return ShowJSON(os.Stdout, "bulk list-users", obj, format, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "bulk list-users", + Transform: transform, + }) } func handleBulkRetrieveJob(ctx context.Context, cmd *cli.Command) error { @@ -296,8 +313,15 @@ func handleBulkRetrieveJob(ctx context.Context, cmd *cli.Command) error { obj := gjson.ParseBytes(res) format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") - return ShowJSON(os.Stdout, "bulk retrieve-job", obj, format, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "bulk retrieve-job", + Transform: transform, + }) } func handleBulkRunJob(ctx context.Context, cmd *cli.Command) error { diff --git a/pkg/cmd/cmd.go b/pkg/cmd/cmd.go index afe432e..027b1ef 100644 --- a/pkg/cmd/cmd.go +++ b/pkg/cmd/cmd.go @@ -73,6 +73,11 @@ func init() { Name: "transform-error", Usage: "The GJSON transformation for errors.", }, + &cli.BoolFlag{ + Name: "raw-output", + Aliases: []string{"r"}, + Usage: "If the result is a string, print it without JSON quotes. This can be useful for making output transforms talk to non-JSON-based systems.", + }, &requestflag.Flag[string]{ Name: "api-key", Sources: cli.EnvVars("COURIER_API_KEY"), @@ -158,8 +163,28 @@ func init() { Category: "API RESOURCE", Suggest: true, Commands: []*cli.Command{ + &journeysCreate, + &journeysRetrieve, &journeysList, + &journeysArchive, &journeysInvoke, + &journeysListVersions, + &journeysPublish, + &journeysReplace, + }, + }, + { + Name: "journeys:templates", + Category: "API RESOURCE", + Suggest: true, + Commands: []*cli.Command{ + &journeysTemplatesCreate, + &journeysTemplatesRetrieve, + &journeysTemplatesList, + &journeysTemplatesArchive, + &journeysTemplatesListVersions, + &journeysTemplatesPublish, + &journeysTemplatesReplace, }, }, { @@ -329,6 +354,7 @@ func init() { Commands: []*cli.Command{ &tenantsTemplatesRetrieve, &tenantsTemplatesList, + &tenantsTemplatesDelete, &tenantsTemplatesPublish, &tenantsTemplatesReplace, }, diff --git a/pkg/cmd/cmdutil.go b/pkg/cmd/cmdutil.go index d74609f..5aed463 100644 --- a/pkg/cmd/cmdutil.go +++ b/pkg/cmd/cmdutil.go @@ -311,21 +311,29 @@ func shouldUseColors(w io.Writer) bool { return isTerminal(w) } -func formatJSON(expectedOutput *os.File, title string, res gjson.Result, format string, transform string) ([]byte, error) { - if transform != "" { - transformed := res.Get(transform) +func formatJSON(res gjson.Result, opts ShowJSONOpts) ([]byte, error) { + if opts.Transform != "" { + transformed := res.Get(opts.Transform) if transformed.Exists() { res = transformed } } - switch strings.ToLower(format) { + // Modeled after `jq -r` (`--raw-output`): if the result is a string, print it without JSON quotes so that + // it's easier to pipe into other programs. + if opts.RawOutput && res.Type == gjson.String { + return []byte(res.Str + "\n"), nil + } + switch strings.ToLower(opts.Format) { case "auto": - return formatJSON(expectedOutput, title, res, "json", "") + autoOpts := opts + autoOpts.Format = "json" + autoOpts.Transform = "" + return formatJSON(res, autoOpts) case "pretty": - return []byte(jsonview.RenderJSON(title, res) + "\n"), nil + return []byte(jsonview.RenderJSON(opts.Title, res) + "\n"), nil case "json": prettyJSON := pretty.Pretty([]byte(res.Raw)) - if shouldUseColors(expectedOutput) { + if shouldUseColors(opts.Stdout) { return pretty.Color(prettyJSON, pretty.TerminalStyle), nil } else { return prettyJSON, nil @@ -333,7 +341,7 @@ func formatJSON(expectedOutput *os.File, title string, res gjson.Result, format case "jsonl": // @ugly is gjson syntax for "no whitespace", so it fits on one line oneLineJSON := res.Get("@ugly").Raw - if shouldUseColors(expectedOutput) { + if shouldUseColors(opts.Stdout) { bytes := append(pretty.Color([]byte(oneLineJSON), pretty.TerminalStyle), '\n') return bytes, nil } else { @@ -347,34 +355,67 @@ func formatJSON(expectedOutput *os.File, title string, res gjson.Result, format if err := json2yaml.Convert(&yaml, input); err != nil { return nil, err } - _, err := expectedOutput.Write([]byte(yaml.String())) + _, err := opts.Stdout.Write([]byte(yaml.String())) return nil, err default: - return nil, fmt.Errorf("Invalid format: %s, valid formats are: %s", format, strings.Join(OutputFormats, ", ")) + return nil, fmt.Errorf("Invalid format: %s, valid formats are: %s", opts.Format, strings.Join(OutputFormats, ", ")) } } -// Display JSON to the user in various different formats -func ShowJSON(out *os.File, title string, res gjson.Result, format string, transform string) error { - if transform != "" { - transformed := res.Get(transform) - if transformed.Exists() { - res = transformed - } +const warningExploreNotSupported = "Warning: Output format 'explore' not supported for non-terminal output; falling back to 'json'\n" + +// ShowJSONOpts configures how JSON output is displayed. +type ShowJSONOpts struct { + ExplicitFormat bool // true if the user explicitly passed --format + Format string // output format (auto, explore, json, jsonl, pretty, raw, yaml) + RawOutput bool // like jq -r: print strings without JSON quotes + Stderr io.Writer // stderr for warnings; injectable for testing; defaults to os.Stderr + Stdout *os.File // stdout (or pager); injectable for testing; defaults to os.Stdout + Title string // display title + Transform string // GJSON path to extract before displaying +} + +func (o *ShowJSONOpts) setDefaults() { + if o.Stderr == nil { + o.Stderr = os.Stderr + } + if o.Stdout == nil { + o.Stdout = os.Stdout } +} + +// ShowJSON displays a single JSON result to the user. +func ShowJSON(res gjson.Result, opts ShowJSONOpts) error { + opts.setDefaults() - switch strings.ToLower(format) { + switch strings.ToLower(opts.Format) { case "auto": - return ShowJSON(out, title, res, "json", "") + autoOpts := opts + autoOpts.Format = "json" + return ShowJSON(res, autoOpts) case "explore": - return jsonview.ExploreJSON(title, res) + if !isTerminal(opts.Stdout) { + if opts.ExplicitFormat { + fmt.Fprint(opts.Stderr, warningExploreNotSupported) + } + jsonOpts := opts + jsonOpts.Format = "json" + return ShowJSON(res, jsonOpts) + } + if opts.Transform != "" { + transformed := res.Get(opts.Transform) + if transformed.Exists() { + res = transformed + } + } + return jsonview.ExploreJSON(opts.Title, res) default: - bytes, err := formatJSON(out, title, res, format, transform) + bytes, err := formatJSON(res, opts) if err != nil { return err } - _, err = out.Write(bytes) + _, err = opts.Stdout.Write(bytes) return err } } @@ -388,12 +429,18 @@ type hasRawJSON interface { RawJSON() string } -// For an iterator over different value types, display its values to the user in -// different formats. -// -1 is used to signal no limit of items to display -func ShowJSONIterator[T any](stdout *os.File, title string, iter jsonview.Iterator[T], format string, transform string, itemsToDisplay int64) error { - if format == "explore" { - return jsonview.ExploreJSONStream(title, iter) +// ShowJSONIterator displays an iterator of values to the user. Use itemsToDisplay = -1 for no limit. +func ShowJSONIterator[T any](iter jsonview.Iterator[T], itemsToDisplay int64, opts ShowJSONOpts) error { + opts.setDefaults() + + if opts.Format == "explore" { + if isTerminal(opts.Stdout) { + return jsonview.ExploreJSONStream(opts.Title, iter) + } + if opts.ExplicitFormat { + fmt.Fprint(opts.Stderr, warningExploreNotSupported) + } + opts.Format = "json" } terminalWidth, terminalHeight, err := term.GetSize(os.Stdout.Fd()) @@ -419,7 +466,7 @@ func ShowJSONIterator[T any](stdout *os.File, title string, iter jsonview.Iterat } obj = gjson.ParseBytes(jsonData) } - json, err := formatJSON(stdout, title, obj, format, transform) + json, err := formatJSON(obj, opts) if err != nil { return err } @@ -436,7 +483,7 @@ func ShowJSONIterator[T any](stdout *os.File, title string, iter jsonview.Iterat } if !usePager { - _, err := stdout.Write(output) + _, err := opts.Stdout.Write(output) if err != nil { return err } @@ -444,13 +491,15 @@ func ShowJSONIterator[T any](stdout *os.File, title string, iter jsonview.Iterat return iter.Err() } - return streamOutput(title, func(pager *os.File) error { - // Write the output we used during the initial terminal size computation + return streamOutput(opts.Title, func(pager *os.File) error { _, err := pager.Write(output) if err != nil { return err } + pagerOpts := opts + pagerOpts.Stdout = pager + for iter.Next() { if itemsToDisplay == 0 { break @@ -466,7 +515,7 @@ func ShowJSONIterator[T any](stdout *os.File, title string, iter jsonview.Iterat } obj = gjson.ParseBytes(jsonData) } - if err := ShowJSON(pager, title, obj, format, transform); err != nil { + if err := ShowJSON(obj, pagerOpts); err != nil { return err } itemsToDisplay -= 1 diff --git a/pkg/cmd/cmdutil_test.go b/pkg/cmd/cmdutil_test.go index 4a7c832..dca40be 100644 --- a/pkg/cmd/cmdutil_test.go +++ b/pkg/cmd/cmdutil_test.go @@ -159,7 +159,7 @@ func TestFormatJSON(t *testing.T) { t.Parallel() res := gjson.Parse(`{"id":"abc123","name":"test"}`) - formatted, err := formatJSON(os.Stdout, "test", res, "raw", "id") + formatted, err := formatJSON(res, ShowJSONOpts{Format: "raw", Stdout: os.Stdout, Transform: "id"}) require.NoError(t, err) require.Equal(t, `"abc123"`+"\n", string(formatted)) }) @@ -168,7 +168,7 @@ func TestFormatJSON(t *testing.T) { t.Parallel() res := gjson.Parse(`{"id":"abc123","name":"test"}`) - formatted, err := formatJSON(os.Stdout, "test", res, "raw", "") + formatted, err := formatJSON(res, ShowJSONOpts{Format: "raw", Stdout: os.Stdout}) require.NoError(t, err) require.Equal(t, `{"id":"abc123","name":"test"}`+"\n", string(formatted)) }) @@ -177,7 +177,7 @@ func TestFormatJSON(t *testing.T) { t.Parallel() res := gjson.Parse(`{"data":{"items":[1,2,3]}}`) - formatted, err := formatJSON(os.Stdout, "test", res, "raw", "data.items") + formatted, err := formatJSON(res, ShowJSONOpts{Format: "raw", Stdout: os.Stdout, Transform: "data.items"}) require.NoError(t, err) require.Equal(t, "[1,2,3]\n", string(formatted)) }) @@ -186,11 +186,40 @@ func TestFormatJSON(t *testing.T) { t.Parallel() res := gjson.Parse(`{"id":"abc123"}`) - formatted, err := formatJSON(os.Stdout, "test", res, "raw", "missing") + formatted, err := formatJSON(res, ShowJSONOpts{Format: "raw", Stdout: os.Stdout, Transform: "missing"}) require.NoError(t, err) // Transform path doesn't exist, so original result is returned require.Equal(t, `{"id":"abc123"}`+"\n", string(formatted)) }) + + t.Run("RawOutputString", func(t *testing.T) { + t.Parallel() + + res := gjson.Parse(`{"id":"abc123","name":"test"}`) + formatted, err := formatJSON(res, ShowJSONOpts{Format: "json", Stdout: os.Stdout, Transform: "id", RawOutput: true}) + require.NoError(t, err) + require.Equal(t, "abc123\n", string(formatted)) + }) + + t.Run("RawOutputNonString", func(t *testing.T) { + t.Parallel() + + // --raw-output has no effect on non-string values + res := gjson.Parse(`{"count":42}`) + formatted, err := formatJSON(res, ShowJSONOpts{Format: "raw", Stdout: os.Stdout, Transform: "count", RawOutput: true}) + require.NoError(t, err) + require.Equal(t, "42\n", string(formatted)) + }) + + t.Run("RawOutputObject", func(t *testing.T) { + t.Parallel() + + // --raw-output has no effect on objects + res := gjson.Parse(`{"nested":{"a":1}}`) + formatted, err := formatJSON(res, ShowJSONOpts{Format: "raw", Stdout: os.Stdout, Transform: "nested", RawOutput: true}) + require.NoError(t, err) + require.Equal(t, `{"a":1}`+"\n", string(formatted)) + }) } func TestShowJSONIterator(t *testing.T) { @@ -231,6 +260,89 @@ func TestShowJSONIterator(t *testing.T) { }) } +func TestExploreFallback(t *testing.T) { + t.Parallel() + + t.Run("ShowJSONFallsBackToJsonOnNonTTY", func(t *testing.T) { + t.Parallel() + + // os.Pipe() produces a *os.File that isn't a terminal, so explore should fall back. + r, w, err := os.Pipe() + require.NoError(t, err) + defer r.Close() + + var stderr bytes.Buffer + res := gjson.Parse(`{"id":"abc"}`) + err = ShowJSON(res, ShowJSONOpts{ + Format: "explore", + Stderr: &stderr, + Stdout: w, + Title: "test", + }) + w.Close() + require.NoError(t, err) + + var buf bytes.Buffer + _, _ = buf.ReadFrom(r) + assert.Contains(t, buf.String(), `"id"`) + assert.Contains(t, buf.String(), `"abc"`) + }) + + t.Run("ShowJSONIteratorFallsBackToJsonOnNonTTY", func(t *testing.T) { + t.Parallel() + + iter := &sliceIterator[map[string]any]{items: []map[string]any{ + {"id": "abc"}, + }} + captured := captureShowJSONIterator(t, iter, "explore", "", -1) + assert.Contains(t, captured, `"id"`) + assert.Contains(t, captured, `"abc"`) + }) + + t.Run("ShowJSONWarnsWhenExplicitFormatOnNonTTY", func(t *testing.T) { + t.Parallel() + + r, w, err := os.Pipe() + require.NoError(t, err) + defer r.Close() + + var stderr bytes.Buffer + res := gjson.Parse(`{"id":"abc"}`) + err = ShowJSON(res, ShowJSONOpts{ + ExplicitFormat: true, + Format: "explore", + Stderr: &stderr, + Stdout: w, + Title: "test", + }) + w.Close() + require.NoError(t, err) + + assert.Equal(t, warningExploreNotSupported, stderr.String()) + }) + + t.Run("ShowJSONSilentWhenDefaultFormatOnNonTTY", func(t *testing.T) { + t.Parallel() + + r, w, err := os.Pipe() + require.NoError(t, err) + defer r.Close() + + var stderr bytes.Buffer + res := gjson.Parse(`{"id":"abc"}`) + err = ShowJSON(res, ShowJSONOpts{ + Format: "explore", + Stderr: &stderr, + Stdout: w, + Title: "test", + }) + w.Close() + require.NoError(t, err) + + assert.Empty(t, stderr.String(), "no warning expected when format was not explicit") + }) +} + // sliceIterator is a simple iterator over a slice for testing. type sliceIterator[T any] struct { index int @@ -260,7 +372,13 @@ func captureShowJSONIterator[T any](t *testing.T, iter jsonview.Iterator[T], for require.NoError(t, err) defer r.Close() - err = ShowJSONIterator(w, "test", iter, format, transform, itemsToDisplay) + err = ShowJSONIterator(iter, itemsToDisplay, ShowJSONOpts{ + Format: format, + Stderr: io.Discard, + Stdout: w, + Title: "test", + Transform: transform, + }) w.Close() require.NoError(t, err) diff --git a/pkg/cmd/flagoptions.go b/pkg/cmd/flagoptions.go index a64588a..e0d84cc 100644 --- a/pkg/cmd/flagoptions.go +++ b/pkg/cmd/flagoptions.go @@ -7,9 +7,11 @@ import ( "fmt" "io" "maps" + "mime" "mime/multipart" "net/http" "os" + "path/filepath" "reflect" "strings" "unicode/utf8" @@ -36,7 +38,14 @@ const ( type FileEmbedStyle int const ( + // EmbedText reads referenced files fully into memory and substitutes the file's contents back into the + // value as a string. Binary files are base64-encoded. Used for JSON request bodies and for headers and + // query parameters, where the file contents need to be serialized inline. EmbedText FileEmbedStyle = iota + + // EmbedIOReader replaces file references with an io.Reader that streams the file's contents. Used for + // `multipart/form-data` and `application/octet-stream` request bodies, where files are uploaded as binary + // parts rather than embedded into a text value. EmbedIOReader ) @@ -142,6 +151,20 @@ func embedFilesValue(v reflect.Value, embedStyle FileEmbedStyle, stdin *onceStdi if s == "" { return v, nil } + if embedStyle == EmbedIOReader { + if isStdinPath(s) { + r, err := stdin.read() + if err != nil { + return v, err + } + return reflect.ValueOf(io.NopCloser(r)), nil + } + upload, err := openFileUpload(s) + if err != nil { + return v, err + } + return reflect.ValueOf(upload), nil + } if isStdinPath(s) { content, err := stdin.readAll() if err != nil { @@ -250,7 +273,7 @@ func embedFilesValue(v reflect.Value, embedStyle FileEmbedStyle, stdin *onceStdi return reflect.ValueOf(io.NopCloser(r)), nil } - file, err := os.Open(filename) + upload, err := openFileUpload(filename) if err != nil { if !expectsFile { // For strings that start with "@" and don't look like a filename, return the string @@ -258,7 +281,7 @@ func embedFilesValue(v reflect.Value, embedStyle FileEmbedStyle, stdin *onceStdi } return v, err } - return reflect.ValueOf(file), nil + return reflect.ValueOf(upload), nil } } return v, nil @@ -309,8 +332,14 @@ func flagOptions( requestContents := requestflag.ExtractRequestContents(cmd) + // Translate inner-field aliases in YAML values that came from flags (e.g. + // `--parent '{"alias": val}'` resolving to the canonical inner field). + if bodyMap, ok := requestContents.Body.(map[string]any); ok { + applyDataAliases(cmd, bodyMap) + } + stdinConsumedByPipe := false - if (bodyType == MultipartFormEncoded || bodyType == ApplicationJSON) && !ignoreStdin && isInputPiped() { + if bodyType != ApplicationOctetStream && !ignoreStdin && isInputPiped() { pipeData, err := io.ReadAll(os.Stdin) if err != nil { return nil, err @@ -323,16 +352,46 @@ func flagOptions( return nil, fmt.Errorf("Failed to parse piped data as YAML/JSON:\n%w", err) } if bodyMap, ok := bodyData.(map[string]any); ok { - if flagMap, ok := requestContents.Body.(map[string]any); ok { - maps.Copy(bodyMap, flagMap) - requestContents.Body = bodyMap + applyDataAliases(cmd, bodyMap) + // Apply any matching keys from the piped data to path, query, and header flags + // that have not already been set via the command line. + if err := requestflag.ApplyStdinDataToFlags(cmd, bodyMap); err != nil { + return nil, err + } + // Re-extract request contents now that flags may have been updated. + requestContents = requestflag.ExtractRequestContents(cmd) + // Remove keys that were consumed as query, header, or path params so they + // don't also leak into the request body via the maps.Copy merge below. + // We delete both the canonical key and any aliases since the user may have + // piped data using an alias name rather than the canonical API name. + for _, flag := range cmd.Flags { + inReq, ok := flag.(requestflag.InRequest) + if !ok || !flag.IsSet() { + continue + } + if inReq.GetQueryPath() != "" || inReq.GetHeaderPath() != "" || inReq.GetPathParam() != "" { + delete(bodyMap, inReq.GetQueryPath()) + delete(bodyMap, inReq.GetHeaderPath()) + delete(bodyMap, inReq.GetPathParam()) + for _, alias := range inReq.GetDataAliases() { + delete(bodyMap, alias) + } + } + } + if bodyType != EmptyBody { + if flagMap, ok := requestContents.Body.(map[string]any); ok { + maps.Copy(bodyMap, flagMap) + requestContents.Body = bodyMap + } else { + bodyData = requestContents.Body + } + } + } else if bodyType != EmptyBody { + if flagMap, ok := requestContents.Body.(map[string]any); ok && len(flagMap) > 0 { + return nil, fmt.Errorf("Cannot merge flags with a body that is not a map: %v", bodyData) } else { - bodyData = requestContents.Body + requestContents.Body = bodyData } - } else if flagMap, ok := requestContents.Body.(map[string]any); ok && len(flagMap) > 0 { - return nil, fmt.Errorf("Cannot merge flags with a body that is not a map: %v", bodyData) - } else { - requestContents.Body = bodyData } } } @@ -340,7 +399,6 @@ func flagOptions( if missingFlags := requestflag.GetMissingRequiredFlags(cmd, requestContents.Body); len(missingFlags) > 0 { if len(missingFlags) == 1 { return nil, fmt.Errorf("Required flag %q not set\nRun '%s --help' for usage information", missingFlags[0].Names()[0], cmd.FullName()) - } else { names := []string{} for _, flag := range missingFlags { @@ -485,6 +543,84 @@ func flagOptions( // as a file path without needing the "@" prefix. type FilePathValue string +// fileUpload wraps an io.Reader with filename and content-type metadata for +// use as a multipart form part. The apiform encoder detects the Filename and +// ContentType methods and uses them to populate the Content-Disposition +// filename and the Content-Type header on the part. +type fileUpload struct { + io.Reader // apiform checks for reader and reads its contents during encode + filename string + contentType string +} + +func (f fileUpload) Filename() string { return f.filename } +func (f fileUpload) ContentType() string { return f.contentType } +func (f fileUpload) Close() error { + if c, ok := f.Reader.(io.Closer); ok { + return c.Close() + } + return nil +} + +// openFileUpload opens the file at path and returns a fileUpload whose filename +// is the path's basename and whose content type is derived from the file +// extension (falling back to application/octet-stream when unknown). +func openFileUpload(path string) (fileUpload, error) { + file, err := os.Open(path) + if err != nil { + return fileUpload{}, err + } + contentType := mime.TypeByExtension(filepath.Ext(path)) + if contentType == "" { + contentType = "application/octet-stream" + } + return fileUpload{ + Reader: file, + filename: filepath.Base(path), + contentType: contentType, + }, nil +} + +// applyDataAliases rewrites keys in a body map based on flag `DataAliases` metadata. For top-level flags, +// `{alias: value}` becomes `{canonical: value}`. For inner flags (those registered under an outer flag +// via WithInnerFlags), the alias translation is also applied to the nested map under the outer flag's +// body path, so values like `--parent '{"alias": val}'` resolve to the canonical inner field name. +func applyDataAliases(cmd *cli.Command, bodyMap map[string]any) { + for _, flag := range cmd.Flags { + // Inner flags: rewrite aliases inside the nested map under the outer flag's body path. + if inner, ok := flag.(requestflag.HasOuterFlag); ok { + outer, outerOk := inner.GetOuterFlag().(requestflag.InRequest) + if !outerOk { + continue + } + if nested, ok := bodyMap[outer.GetBodyPath()].(map[string]any); ok && inner.GetInnerField() != "" { + rewriteAliases(nested, inner.GetInnerField(), inner.GetDataAliases()) + } + continue + } + // Top-level flags: rewrite aliases in the body map. + if inReq, ok := flag.(requestflag.InRequest); ok && inReq.GetBodyPath() != "" { + rewriteAliases(bodyMap, inReq.GetBodyPath(), inReq.GetDataAliases()) + } + } +} + +// rewriteAliases replaces each alias key in m with the canonical key, preserving the value. The +// "canonical" key is the name the API itself expects (the OpenAPI property/field name) — e.g. for +// a top-level flag, the parameter's BodyPath; for an inner flag, the inner field name. Aliases are +// the user-facing alternate names declared via x-stainless-cli-data-alias. +func rewriteAliases(m map[string]any, canonical string, aliases []string) { + for _, alias := range aliases { + if alias == "" || alias == canonical { + continue + } + if val, exists := m[alias]; exists { + m[canonical] = val + delete(m, alias) + } + } +} + // wrapFileInputValues replaces string values for FileInput flags (type: string, format: binary) with // FilePathValue sentinel values. embedFilesValue recognizes FilePathValue and reads the file contents // directly, so the user doesn't need to type the "@" prefix. This handles both values set via explicit diff --git a/pkg/cmd/flagoptions_test.go b/pkg/cmd/flagoptions_test.go index 039b9ff..00734ca 100644 --- a/pkg/cmd/flagoptions_test.go +++ b/pkg/cmd/flagoptions_test.go @@ -8,7 +8,6 @@ import ( "strings" "testing" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -31,7 +30,7 @@ func TestIsUTF8TextFile(t *testing.T) { } for _, tt := range tests { - assert.Equal(t, tt.expected, isUTF8TextFile(tt.content)) + require.Equal(t, tt.expected, isUTF8TextFile(tt.content)) } } @@ -226,10 +225,10 @@ func TestEmbedFiles(t *testing.T) { got, err := embedFiles(tt.input, EmbedText, nil) if tt.wantErr { - assert.Error(t, err) + require.Error(t, err) } else { require.NoError(t, err) - assert.Equal(t, tt.want, got) + require.Equal(t, tt.want, got) } }) @@ -238,7 +237,7 @@ func TestEmbedFiles(t *testing.T) { _, err := embedFiles(tt.input, EmbedIOReader, nil) if tt.wantErr { - assert.Error(t, err) + require.Error(t, err) } else { require.NoError(t, err) } @@ -333,6 +332,56 @@ func TestEmbedFilesStdin(t *testing.T) { }) } +// TestEmbedFilesUploadMetadata verifies that EmbedIOReader mode wraps file readers with filename and +// content-type metadata so the multipart encoder populates `Content-Disposition` and `Content-Type` headers. +func TestEmbedFilesUploadMetadata(t *testing.T) { + t.Parallel() + + tmpDir := t.TempDir() + writeTestFile(t, tmpDir, "hello.txt", "hi") + writeTestFile(t, tmpDir, "page.html", "") + writeTestFile(t, tmpDir, "blob.bin", "\x00\x01") + + cases := []struct { + basename string + wantContentType string + }{ + {"hello.txt", "text/plain; charset=utf-8"}, + {"page.html", "text/html; charset=utf-8"}, + {"blob.bin", "application/octet-stream"}, + } + + for _, tc := range cases { + t.Run("AtPrefix_"+tc.basename, func(t *testing.T) { + t.Parallel() + + path := filepath.Join(tmpDir, tc.basename) + withEmbedded, err := embedFiles(map[string]any{"file": "@" + path}, EmbedIOReader, nil) + require.NoError(t, err) + + upload, ok := withEmbedded.(map[string]any)["file"].(fileUpload) + require.True(t, ok, "expected fileUpload, got %T", withEmbedded.(map[string]any)["file"]) + require.Equal(t, tc.basename, upload.Filename()) + require.Equal(t, upload.ContentType(), tc.wantContentType) + require.NoError(t, upload.Close()) + }) + + t.Run("FilePathValue_"+tc.basename, func(t *testing.T) { + t.Parallel() + + path := filepath.Join(tmpDir, tc.basename) + withEmbedded, err := embedFiles(map[string]any{"file": FilePathValue(path)}, EmbedIOReader, nil) + require.NoError(t, err) + + upload, ok := withEmbedded.(map[string]any)["file"].(fileUpload) + require.True(t, ok, "expected fileUpload, got %T", withEmbedded.(map[string]any)["file"]) + require.Equal(t, tc.basename, upload.Filename()) + require.Equal(t, upload.ContentType(), tc.wantContentType) + require.NoError(t, upload.Close()) + }) + } +} + func writeTestFile(t *testing.T, dir, filename, content string) { t.Helper() diff --git a/pkg/cmd/inbound.go b/pkg/cmd/inbound.go index 4d47c00..8be3d1f 100644 --- a/pkg/cmd/inbound.go +++ b/pkg/cmd/inbound.go @@ -5,7 +5,6 @@ package cmd import ( "context" "fmt" - "os" "github.com/tidwall/gjson" "github.com/trycourier/courier-cli/v3/internal/apiquery" @@ -43,7 +42,7 @@ var inboundTrackEvent = cli.Command{ Required: true, BodyPath: "type", }, - &requestflag.Flag[any]{ + &requestflag.Flag[*string]{ Name: "user-id", Usage: "The user id associated with the track", BodyPath: "userId", @@ -61,8 +60,6 @@ func handleInboundTrackEvent(ctx context.Context, cmd *cli.Command) error { return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := courier.InboundTrackEventParams{} - options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -74,6 +71,8 @@ func handleInboundTrackEvent(ctx context.Context, cmd *cli.Command) error { return err } + params := courier.InboundTrackEventParams{} + var res []byte options = append(options, option.WithResponseBodyInto(&res)) _, err = client.Inbound.TrackEvent(ctx, params, options...) @@ -83,6 +82,13 @@ func handleInboundTrackEvent(ctx context.Context, cmd *cli.Command) error { obj := gjson.ParseBytes(res) format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") - return ShowJSON(os.Stdout, "inbound track-event", obj, format, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "inbound track-event", + Transform: transform, + }) } diff --git a/pkg/cmd/journey.go b/pkg/cmd/journey.go index c9ee498..242e2ba 100644 --- a/pkg/cmd/journey.go +++ b/pkg/cmd/journey.go @@ -5,7 +5,6 @@ package cmd import ( "context" "fmt" - "os" "github.com/tidwall/gjson" "github.com/trycourier/courier-cli/v3/internal/apiquery" @@ -15,6 +14,55 @@ import ( "github.com/urfave/cli/v3" ) +var journeysCreate = cli.Command{ + Name: "create", + Usage: "Create a journey. Defaults to `DRAFT` state; pass `state: \"PUBLISHED\"` to\npublish on create. Send nodes are not allowed on `POST`. The standard flow is:\ncreate the journey shell here, add notification templates with\n`POST /journeys/{templateId}/templates`, then wire them into the journey with\n`PUT /journeys/{templateId}`. Call `POST /journeys/{templateId}/publish` to\npublish a draft after the fact.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "name", + Required: true, + BodyPath: "name", + }, + &requestflag.Flag[[]map[string]any]{ + Name: "node", + Required: true, + BodyPath: "nodes", + }, + &requestflag.Flag[bool]{ + Name: "enabled", + BodyPath: "enabled", + }, + &requestflag.Flag[string]{ + Name: "state", + Usage: "Lifecycle state of a journey.", + BodyPath: "state", + }, + }, + Action: handleJourneysCreate, + HideHelpCommand: true, +} + +var journeysRetrieve = cli.Command{ + Name: "retrieve", + Usage: "Fetch a journey by id. Pass `?version=draft` (default `published`) to retrieve\nthe working draft, or `?version=vN` to retrieve a historical version.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "template-id", + Required: true, + PathParam: "templateId", + }, + &requestflag.Flag[string]{ + Name: "version", + Usage: "Version selector: `draft`, `published` (default), or `vN`.", + QueryPath: "version", + }, + }, + Action: handleJourneysRetrieve, + HideHelpCommand: true, +} + var journeysList = cli.Command{ Name: "list", Usage: "Get the list of journeys.", @@ -36,14 +84,30 @@ var journeysList = cli.Command{ HideHelpCommand: true, } +var journeysArchive = cli.Command{ + Name: "archive", + Usage: "Archive a journey. Archived journeys cannot be invoked. Existing journey runs\ncontinue to completion.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "template-id", + Required: true, + PathParam: "templateId", + }, + }, + Action: handleJourneysArchive, + HideHelpCommand: true, +} + var journeysInvoke = cli.Command{ Name: "invoke", - Usage: "Invoke a journey run from a journey template.", + Usage: "Invoke a journey by id or alias to start a new run. The response includes a\n`runId` identifying the run.", Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "template-id", - Required: true, + Name: "template-id", + Required: true, + PathParam: "templateId", }, &requestflag.Flag[map[string]any]{ Name: "data", @@ -65,7 +129,75 @@ var journeysInvoke = cli.Command{ HideHelpCommand: true, } -func handleJourneysList(ctx context.Context, cmd *cli.Command) error { +var journeysListVersions = cli.Command{ + Name: "list-versions", + Usage: "List published versions of a journey, ordered most recent first.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "template-id", + Required: true, + PathParam: "templateId", + }, + }, + Action: handleJourneysListVersions, + HideHelpCommand: true, +} + +var journeysPublish = cli.Command{ + Name: "publish", + Usage: "Publish the current draft as a new version. Body is optional; pass\n`{ \"version\": \"vN\" }` to roll back to a prior version instead. Returns 404 if\nthe journey has no draft to publish.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "template-id", + Required: true, + PathParam: "templateId", + }, + &requestflag.Flag[string]{ + Name: "version", + BodyPath: "version", + }, + }, + Action: handleJourneysPublish, + HideHelpCommand: true, +} + +var journeysReplace = cli.Command{ + Name: "replace", + Usage: "Replace the journey draft. Updates the working draft only; call\n`POST /journeys/{templateId}/publish` to make it live, or pass\n`state: \"PUBLISHED\"` in this request to publish immediately. Send-node\n`template` ids must already exist and be scoped to this journey, and node ids\nmust not be claimed by another journey.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "template-id", + Required: true, + PathParam: "templateId", + }, + &requestflag.Flag[string]{ + Name: "name", + Required: true, + BodyPath: "name", + }, + &requestflag.Flag[[]map[string]any]{ + Name: "node", + Required: true, + BodyPath: "nodes", + }, + &requestflag.Flag[bool]{ + Name: "enabled", + BodyPath: "enabled", + }, + &requestflag.Flag[string]{ + Name: "state", + Usage: "Lifecycle state of a journey.", + BodyPath: "state", + }, + }, + Action: handleJourneysReplace, + HideHelpCommand: true, +} + +func handleJourneysCreate(ctx context.Context, cmd *cli.Command) error { client := courier.NewClient(getDefaultRequestOptions(cmd)...) unusedArgs := cmd.Args().Slice() @@ -73,7 +205,95 @@ func handleJourneysList(ctx context.Context, cmd *cli.Command) error { return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := courier.JourneyListParams{} + options, err := flagOptions( + cmd, + apiquery.NestedQueryFormatBrackets, + apiquery.ArrayQueryFormatComma, + ApplicationJSON, + false, + ) + if err != nil { + return err + } + + params := courier.JourneyNewParams{} + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Journeys.New(ctx, params, options...) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") + transform := cmd.Root().String("transform") + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "journeys create", + Transform: transform, + }) +} + +func handleJourneysRetrieve(ctx context.Context, cmd *cli.Command) error { + client := courier.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("template-id") && len(unusedArgs) > 0 { + cmd.Set("template-id", unusedArgs[0]) + unusedArgs = unusedArgs[1:] + } + if len(unusedArgs) > 0 { + return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) + } + + options, err := flagOptions( + cmd, + apiquery.NestedQueryFormatBrackets, + apiquery.ArrayQueryFormatComma, + EmptyBody, + false, + ) + if err != nil { + return err + } + + params := courier.JourneyGetParams{} + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Journeys.Get( + ctx, + cmd.Value("template-id").(string), + params, + options..., + ) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") + transform := cmd.Root().String("transform") + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "journeys retrieve", + Transform: transform, + }) +} + +func handleJourneysList(ctx context.Context, cmd *cli.Command) error { + client := courier.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + + if len(unusedArgs) > 0 { + return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) + } options, err := flagOptions( cmd, @@ -86,6 +306,8 @@ func handleJourneysList(ctx context.Context, cmd *cli.Command) error { return err } + params := courier.JourneyListParams{} + var res []byte options = append(options, option.WithResponseBodyInto(&res)) _, err = client.Journeys.List(ctx, params, options...) @@ -95,8 +317,40 @@ func handleJourneysList(ctx context.Context, cmd *cli.Command) error { obj := gjson.ParseBytes(res) format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") - return ShowJSON(os.Stdout, "journeys list", obj, format, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "journeys list", + Transform: transform, + }) +} + +func handleJourneysArchive(ctx context.Context, cmd *cli.Command) error { + client := courier.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("template-id") && len(unusedArgs) > 0 { + cmd.Set("template-id", unusedArgs[0]) + unusedArgs = unusedArgs[1:] + } + if len(unusedArgs) > 0 { + return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) + } + + options, err := flagOptions( + cmd, + apiquery.NestedQueryFormatBrackets, + apiquery.ArrayQueryFormatComma, + EmptyBody, + false, + ) + if err != nil { + return err + } + + return client.Journeys.Archive(ctx, cmd.Value("template-id").(string), options...) } func handleJourneysInvoke(ctx context.Context, cmd *cli.Command) error { @@ -110,8 +364,97 @@ func handleJourneysInvoke(ctx context.Context, cmd *cli.Command) error { return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } + options, err := flagOptions( + cmd, + apiquery.NestedQueryFormatBrackets, + apiquery.ArrayQueryFormatComma, + ApplicationJSON, + false, + ) + if err != nil { + return err + } + params := courier.JourneyInvokeParams{} + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Journeys.Invoke( + ctx, + cmd.Value("template-id").(string), + params, + options..., + ) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") + transform := cmd.Root().String("transform") + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "journeys invoke", + Transform: transform, + }) +} + +func handleJourneysListVersions(ctx context.Context, cmd *cli.Command) error { + client := courier.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("template-id") && len(unusedArgs) > 0 { + cmd.Set("template-id", unusedArgs[0]) + unusedArgs = unusedArgs[1:] + } + if len(unusedArgs) > 0 { + return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) + } + + options, err := flagOptions( + cmd, + apiquery.NestedQueryFormatBrackets, + apiquery.ArrayQueryFormatComma, + EmptyBody, + false, + ) + if err != nil { + return err + } + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Journeys.ListVersions(ctx, cmd.Value("template-id").(string), options...) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") + transform := cmd.Root().String("transform") + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "journeys list-versions", + Transform: transform, + }) +} + +func handleJourneysPublish(ctx context.Context, cmd *cli.Command) error { + client := courier.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("template-id") && len(unusedArgs) > 0 { + cmd.Set("template-id", unusedArgs[0]) + unusedArgs = unusedArgs[1:] + } + if len(unusedArgs) > 0 { + return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) + } + options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -123,9 +466,60 @@ func handleJourneysInvoke(ctx context.Context, cmd *cli.Command) error { return err } + params := courier.JourneyPublishParams{} + var res []byte options = append(options, option.WithResponseBodyInto(&res)) - _, err = client.Journeys.Invoke( + _, err = client.Journeys.Publish( + ctx, + cmd.Value("template-id").(string), + params, + options..., + ) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") + transform := cmd.Root().String("transform") + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "journeys publish", + Transform: transform, + }) +} + +func handleJourneysReplace(ctx context.Context, cmd *cli.Command) error { + client := courier.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("template-id") && len(unusedArgs) > 0 { + cmd.Set("template-id", unusedArgs[0]) + unusedArgs = unusedArgs[1:] + } + if len(unusedArgs) > 0 { + return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) + } + + options, err := flagOptions( + cmd, + apiquery.NestedQueryFormatBrackets, + apiquery.ArrayQueryFormatComma, + ApplicationJSON, + false, + ) + if err != nil { + return err + } + + params := courier.JourneyReplaceParams{} + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Journeys.Replace( ctx, cmd.Value("template-id").(string), params, @@ -137,6 +531,13 @@ func handleJourneysInvoke(ctx context.Context, cmd *cli.Command) error { obj := gjson.ParseBytes(res) format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") - return ShowJSON(os.Stdout, "journeys invoke", obj, format, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "journeys replace", + Transform: transform, + }) } diff --git a/pkg/cmd/journey_test.go b/pkg/cmd/journey_test.go index 7363d74..b04f235 100644 --- a/pkg/cmd/journey_test.go +++ b/pkg/cmd/journey_test.go @@ -8,6 +8,65 @@ import ( "github.com/trycourier/courier-cli/v3/internal/mocktest" ) +func TestJourneysCreate(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "journeys", "create", + "--name", "Welcome Journey", + "--node", "{trigger_type: api-invoke, type: trigger, id: trigger-1, conditions: [string, string], schema: {foo: bar}}", + "--node", "{trigger_type: api-invoke, type: trigger, id: send-1, conditions: [string, string], schema: {foo: bar}}", + "--enabled=true", + "--state", "DRAFT", + ) + }) + + t.Run("piping data", func(t *testing.T) { + // Test piping YAML data over stdin + pipeData := []byte("" + + "name: Welcome Journey\n" + + "nodes:\n" + + " - trigger_type: api-invoke\n" + + " type: trigger\n" + + " id: trigger-1\n" + + " conditions:\n" + + " - string\n" + + " - string\n" + + " schema:\n" + + " foo: bar\n" + + " - trigger_type: api-invoke\n" + + " type: trigger\n" + + " id: send-1\n" + + " conditions:\n" + + " - string\n" + + " - string\n" + + " schema:\n" + + " foo: bar\n" + + "enabled: true\n" + + "state: DRAFT\n") + mocktest.TestRunMockTestWithPipeAndFlags( + t, pipeData, + "--api-key", "string", + "journeys", "create", + ) + }) +} + +func TestJourneysRetrieve(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "journeys", "retrieve", + "--template-id", "x", + "--version", "published", + ) + }) +} + func TestJourneysList(t *testing.T) { t.Skip("Mock server tests are disabled") t.Run("regular flags", func(t *testing.T) { @@ -21,6 +80,18 @@ func TestJourneysList(t *testing.T) { }) } +func TestJourneysArchive(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "journeys", "archive", + "--template-id", "x", + ) + }) +} + func TestJourneysInvoke(t *testing.T) { t.Skip("Mock server tests are disabled") t.Run("regular flags", func(t *testing.T) { @@ -52,3 +123,78 @@ func TestJourneysInvoke(t *testing.T) { ) }) } + +func TestJourneysListVersions(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "journeys", "list-versions", + "--template-id", "x", + ) + }) +} + +func TestJourneysPublish(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "journeys", "publish", + "--template-id", "x", + "--version", "v321669910225", + ) + }) + + t.Run("piping data", func(t *testing.T) { + // Test piping YAML data over stdin + pipeData := []byte("version: v321669910225") + mocktest.TestRunMockTestWithPipeAndFlags( + t, pipeData, + "--api-key", "string", + "journeys", "publish", + "--template-id", "x", + ) + }) +} + +func TestJourneysReplace(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "journeys", "replace", + "--template-id", "x", + "--name", "Welcome Journey v2", + "--node", "{trigger_type: api-invoke, type: trigger, id: x, conditions: [string, string], schema: {foo: bar}}", + "--enabled=true", + "--state", "DRAFT", + ) + }) + + t.Run("piping data", func(t *testing.T) { + // Test piping YAML data over stdin + pipeData := []byte("" + + "name: Welcome Journey v2\n" + + "nodes:\n" + + " - trigger_type: api-invoke\n" + + " type: trigger\n" + + " id: x\n" + + " conditions:\n" + + " - string\n" + + " - string\n" + + " schema:\n" + + " foo: bar\n" + + "enabled: true\n" + + "state: DRAFT\n") + mocktest.TestRunMockTestWithPipeAndFlags( + t, pipeData, + "--api-key", "string", + "journeys", "replace", + "--template-id", "x", + ) + }) +} diff --git a/pkg/cmd/journeytemplate.go b/pkg/cmd/journeytemplate.go new file mode 100644 index 0000000..2bb484b --- /dev/null +++ b/pkg/cmd/journeytemplate.go @@ -0,0 +1,551 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package cmd + +import ( + "context" + "fmt" + + "github.com/tidwall/gjson" + "github.com/trycourier/courier-cli/v3/internal/apiquery" + "github.com/trycourier/courier-cli/v3/internal/requestflag" + "github.com/trycourier/courier-go/v4" + "github.com/trycourier/courier-go/v4/option" + "github.com/urfave/cli/v3" +) + +var journeysTemplatesCreate = requestflag.WithInnerFlags(cli.Command{ + Name: "create", + Usage: "Create a notification template scoped to this journey. Defaults to `DRAFT`\nstate; pass `state: \"PUBLISHED\"` to publish on create.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "template-id", + Required: true, + PathParam: "templateId", + }, + &requestflag.Flag[string]{ + Name: "channel", + Required: true, + BodyPath: "channel", + }, + &requestflag.Flag[map[string]any]{ + Name: "notification", + Required: true, + BodyPath: "notification", + }, + &requestflag.Flag[string]{ + Name: "provider-key", + BodyPath: "providerKey", + }, + &requestflag.Flag[string]{ + Name: "state", + BodyPath: "state", + }, + }, + Action: handleJourneysTemplatesCreate, + HideHelpCommand: true, +}, map[string][]requestflag.HasOuterFlag{ + "notification": { + &requestflag.InnerFlag[map[string]any]{ + Name: "notification.brand", + InnerField: "brand", + }, + &requestflag.InnerFlag[map[string]any]{ + Name: "notification.content", + InnerField: "content", + }, + &requestflag.InnerFlag[string]{ + Name: "notification.name", + InnerField: "name", + }, + &requestflag.InnerFlag[map[string]any]{ + Name: "notification.subscription", + InnerField: "subscription", + }, + &requestflag.InnerFlag[[]string]{ + Name: "notification.tags", + InnerField: "tags", + }, + }, +}) + +var journeysTemplatesRetrieve = cli.Command{ + Name: "retrieve", + Usage: "Fetch a journey-scoped notification template by id. Pass `?version=draft`\n(default `published`) to retrieve the working draft, or `?version=vN` for a\nhistorical version.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "template-id", + Required: true, + PathParam: "templateId", + }, + &requestflag.Flag[string]{ + Name: "notification-id", + Required: true, + PathParam: "notificationId", + }, + }, + Action: handleJourneysTemplatesRetrieve, + HideHelpCommand: true, +} + +var journeysTemplatesList = cli.Command{ + Name: "list", + Usage: "List notification templates scoped to this journey. Journey-scoped notification\ntemplates can only be referenced from `send` nodes within the same journey.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "template-id", + Required: true, + PathParam: "templateId", + }, + &requestflag.Flag[string]{ + Name: "cursor", + Usage: "Pagination cursor from a prior response.", + QueryPath: "cursor", + }, + &requestflag.Flag[int64]{ + Name: "limit", + Usage: "Page size. Minimum 1, maximum 100.", + QueryPath: "limit", + }, + }, + Action: handleJourneysTemplatesList, + HideHelpCommand: true, +} + +var journeysTemplatesArchive = cli.Command{ + Name: "archive", + Usage: "Archive the journey-scoped notification template. Archived templates cannot be\nsent.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "template-id", + Required: true, + PathParam: "templateId", + }, + &requestflag.Flag[string]{ + Name: "notification-id", + Required: true, + PathParam: "notificationId", + }, + }, + Action: handleJourneysTemplatesArchive, + HideHelpCommand: true, +} + +var journeysTemplatesListVersions = cli.Command{ + Name: "list-versions", + Usage: "List published versions of the journey-scoped notification template, ordered\nmost recent first.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "template-id", + Required: true, + PathParam: "templateId", + }, + &requestflag.Flag[string]{ + Name: "notification-id", + Required: true, + PathParam: "notificationId", + }, + }, + Action: handleJourneysTemplatesListVersions, + HideHelpCommand: true, +} + +var journeysTemplatesPublish = cli.Command{ + Name: "publish", + Usage: "Publish the current draft of the journey-scoped notification template as a new\nversion. Optionally roll back to a prior version by passing\n`{ \"version\": \"vN\" }`.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "template-id", + Required: true, + PathParam: "templateId", + }, + &requestflag.Flag[string]{ + Name: "notification-id", + Required: true, + PathParam: "notificationId", + }, + &requestflag.Flag[string]{ + Name: "version", + BodyPath: "version", + }, + }, + Action: handleJourneysTemplatesPublish, + HideHelpCommand: true, +} + +var journeysTemplatesReplace = requestflag.WithInnerFlags(cli.Command{ + Name: "replace", + Usage: "Replace the journey-scoped notification template draft.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "template-id", + Required: true, + PathParam: "templateId", + }, + &requestflag.Flag[string]{ + Name: "notification-id", + Required: true, + PathParam: "notificationId", + }, + &requestflag.Flag[map[string]any]{ + Name: "notification", + Required: true, + BodyPath: "notification", + }, + &requestflag.Flag[string]{ + Name: "state", + BodyPath: "state", + }, + }, + Action: handleJourneysTemplatesReplace, + HideHelpCommand: true, +}, map[string][]requestflag.HasOuterFlag{ + "notification": { + &requestflag.InnerFlag[map[string]any]{ + Name: "notification.brand", + InnerField: "brand", + }, + &requestflag.InnerFlag[map[string]any]{ + Name: "notification.content", + InnerField: "content", + }, + &requestflag.InnerFlag[string]{ + Name: "notification.name", + InnerField: "name", + }, + &requestflag.InnerFlag[map[string]any]{ + Name: "notification.subscription", + InnerField: "subscription", + }, + &requestflag.InnerFlag[[]string]{ + Name: "notification.tags", + InnerField: "tags", + }, + }, +}) + +func handleJourneysTemplatesCreate(ctx context.Context, cmd *cli.Command) error { + client := courier.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("template-id") && len(unusedArgs) > 0 { + cmd.Set("template-id", unusedArgs[0]) + unusedArgs = unusedArgs[1:] + } + if len(unusedArgs) > 0 { + return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) + } + + options, err := flagOptions( + cmd, + apiquery.NestedQueryFormatBrackets, + apiquery.ArrayQueryFormatComma, + ApplicationJSON, + false, + ) + if err != nil { + return err + } + + params := courier.JourneyTemplateNewParams{} + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Journeys.Templates.New( + ctx, + cmd.Value("template-id").(string), + params, + options..., + ) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") + transform := cmd.Root().String("transform") + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "journeys:templates create", + Transform: transform, + }) +} + +func handleJourneysTemplatesRetrieve(ctx context.Context, cmd *cli.Command) error { + client := courier.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("notification-id") && len(unusedArgs) > 0 { + cmd.Set("notification-id", unusedArgs[0]) + unusedArgs = unusedArgs[1:] + } + if len(unusedArgs) > 0 { + return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) + } + + options, err := flagOptions( + cmd, + apiquery.NestedQueryFormatBrackets, + apiquery.ArrayQueryFormatComma, + EmptyBody, + false, + ) + if err != nil { + return err + } + + params := courier.JourneyTemplateGetParams{ + TemplateID: cmd.Value("template-id").(string), + } + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Journeys.Templates.Get( + ctx, + cmd.Value("notification-id").(string), + params, + options..., + ) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") + transform := cmd.Root().String("transform") + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "journeys:templates retrieve", + Transform: transform, + }) +} + +func handleJourneysTemplatesList(ctx context.Context, cmd *cli.Command) error { + client := courier.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("template-id") && len(unusedArgs) > 0 { + cmd.Set("template-id", unusedArgs[0]) + unusedArgs = unusedArgs[1:] + } + if len(unusedArgs) > 0 { + return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) + } + + options, err := flagOptions( + cmd, + apiquery.NestedQueryFormatBrackets, + apiquery.ArrayQueryFormatComma, + EmptyBody, + false, + ) + if err != nil { + return err + } + + params := courier.JourneyTemplateListParams{} + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Journeys.Templates.List( + ctx, + cmd.Value("template-id").(string), + params, + options..., + ) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") + transform := cmd.Root().String("transform") + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "journeys:templates list", + Transform: transform, + }) +} + +func handleJourneysTemplatesArchive(ctx context.Context, cmd *cli.Command) error { + client := courier.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("notification-id") && len(unusedArgs) > 0 { + cmd.Set("notification-id", unusedArgs[0]) + unusedArgs = unusedArgs[1:] + } + if len(unusedArgs) > 0 { + return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) + } + + options, err := flagOptions( + cmd, + apiquery.NestedQueryFormatBrackets, + apiquery.ArrayQueryFormatComma, + EmptyBody, + false, + ) + if err != nil { + return err + } + + params := courier.JourneyTemplateArchiveParams{ + TemplateID: cmd.Value("template-id").(string), + } + + return client.Journeys.Templates.Archive( + ctx, + cmd.Value("notification-id").(string), + params, + options..., + ) +} + +func handleJourneysTemplatesListVersions(ctx context.Context, cmd *cli.Command) error { + client := courier.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("notification-id") && len(unusedArgs) > 0 { + cmd.Set("notification-id", unusedArgs[0]) + unusedArgs = unusedArgs[1:] + } + if len(unusedArgs) > 0 { + return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) + } + + options, err := flagOptions( + cmd, + apiquery.NestedQueryFormatBrackets, + apiquery.ArrayQueryFormatComma, + EmptyBody, + false, + ) + if err != nil { + return err + } + + params := courier.JourneyTemplateListVersionsParams{ + TemplateID: cmd.Value("template-id").(string), + } + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Journeys.Templates.ListVersions( + ctx, + cmd.Value("notification-id").(string), + params, + options..., + ) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") + transform := cmd.Root().String("transform") + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "journeys:templates list-versions", + Transform: transform, + }) +} + +func handleJourneysTemplatesPublish(ctx context.Context, cmd *cli.Command) error { + client := courier.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("notification-id") && len(unusedArgs) > 0 { + cmd.Set("notification-id", unusedArgs[0]) + unusedArgs = unusedArgs[1:] + } + if len(unusedArgs) > 0 { + return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) + } + + options, err := flagOptions( + cmd, + apiquery.NestedQueryFormatBrackets, + apiquery.ArrayQueryFormatComma, + ApplicationJSON, + false, + ) + if err != nil { + return err + } + + params := courier.JourneyTemplatePublishParams{ + TemplateID: cmd.Value("template-id").(string), + } + + return client.Journeys.Templates.Publish( + ctx, + cmd.Value("notification-id").(string), + params, + options..., + ) +} + +func handleJourneysTemplatesReplace(ctx context.Context, cmd *cli.Command) error { + client := courier.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("notification-id") && len(unusedArgs) > 0 { + cmd.Set("notification-id", unusedArgs[0]) + unusedArgs = unusedArgs[1:] + } + if len(unusedArgs) > 0 { + return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) + } + + options, err := flagOptions( + cmd, + apiquery.NestedQueryFormatBrackets, + apiquery.ArrayQueryFormatComma, + ApplicationJSON, + false, + ) + if err != nil { + return err + } + + params := courier.JourneyTemplateReplaceParams{ + TemplateID: cmd.Value("template-id").(string), + } + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Journeys.Templates.Replace( + ctx, + cmd.Value("notification-id").(string), + params, + options..., + ) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") + transform := cmd.Root().String("transform") + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "journeys:templates replace", + Transform: transform, + }) +} diff --git a/pkg/cmd/journeytemplate_test.go b/pkg/cmd/journeytemplate_test.go new file mode 100644 index 0000000..67b11c8 --- /dev/null +++ b/pkg/cmd/journeytemplate_test.go @@ -0,0 +1,224 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package cmd + +import ( + "testing" + + "github.com/trycourier/courier-cli/v3/internal/mocktest" + "github.com/trycourier/courier-cli/v3/internal/requestflag" +) + +func TestJourneysTemplatesCreate(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "journeys:templates", "create", + "--template-id", "x", + "--channel", "email", + "--notification", "{brand: {id: id}, content: {elements: [{channels: [string], if: if, loop: loop, ref: ref, type: text}], version: '2022-01-01', scope: default}, name: Welcome email, subscription: {topic_id: topic_id}, tags: [string]}", + "--provider-key", "x", + "--state", "state", + ) + }) + + t.Run("inner flags", func(t *testing.T) { + // Check that inner flags have been set up correctly + requestflag.CheckInnerFlags(journeysTemplatesCreate) + + // Alternative argument passing style using inner flags + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "journeys:templates", "create", + "--template-id", "x", + "--channel", "email", + "--notification.brand", "{id: id}", + "--notification.content", "{elements: [{channels: [string], if: if, loop: loop, ref: ref, type: text}], version: '2022-01-01', scope: default}", + "--notification.name", "Welcome email", + "--notification.subscription", "{topic_id: topic_id}", + "--notification.tags", "[string]", + "--provider-key", "x", + "--state", "state", + ) + }) + + t.Run("piping data", func(t *testing.T) { + // Test piping YAML data over stdin + pipeData := []byte("" + + "channel: email\n" + + "notification:\n" + + " brand:\n" + + " id: id\n" + + " content:\n" + + " elements:\n" + + " - channels:\n" + + " - string\n" + + " if: if\n" + + " loop: loop\n" + + " ref: ref\n" + + " type: text\n" + + " version: '2022-01-01'\n" + + " scope: default\n" + + " name: Welcome email\n" + + " subscription:\n" + + " topic_id: topic_id\n" + + " tags:\n" + + " - string\n" + + "providerKey: x\n" + + "state: state\n") + mocktest.TestRunMockTestWithPipeAndFlags( + t, pipeData, + "--api-key", "string", + "journeys:templates", "create", + "--template-id", "x", + ) + }) +} + +func TestJourneysTemplatesRetrieve(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "journeys:templates", "retrieve", + "--template-id", "x", + "--notification-id", "x", + ) + }) +} + +func TestJourneysTemplatesList(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "journeys:templates", "list", + "--template-id", "x", + "--cursor", "cursor", + "--limit", "1", + ) + }) +} + +func TestJourneysTemplatesArchive(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "journeys:templates", "archive", + "--template-id", "x", + "--notification-id", "x", + ) + }) +} + +func TestJourneysTemplatesListVersions(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "journeys:templates", "list-versions", + "--template-id", "x", + "--notification-id", "x", + ) + }) +} + +func TestJourneysTemplatesPublish(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "journeys:templates", "publish", + "--template-id", "x", + "--notification-id", "x", + "--version", "v321669910225", + ) + }) + + t.Run("piping data", func(t *testing.T) { + // Test piping YAML data over stdin + pipeData := []byte("version: v321669910225") + mocktest.TestRunMockTestWithPipeAndFlags( + t, pipeData, + "--api-key", "string", + "journeys:templates", "publish", + "--template-id", "x", + "--notification-id", "x", + ) + }) +} + +func TestJourneysTemplatesReplace(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "journeys:templates", "replace", + "--template-id", "x", + "--notification-id", "x", + "--notification", "{brand: {id: id}, content: {elements: [{channels: [string], if: if, loop: loop, ref: ref, type: text}], version: '2022-01-01', scope: default}, name: name, subscription: {topic_id: topic_id}, tags: [string]}", + "--state", "state", + ) + }) + + t.Run("inner flags", func(t *testing.T) { + // Check that inner flags have been set up correctly + requestflag.CheckInnerFlags(journeysTemplatesReplace) + + // Alternative argument passing style using inner flags + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "journeys:templates", "replace", + "--template-id", "x", + "--notification-id", "x", + "--notification.brand", "{id: id}", + "--notification.content", "{elements: [{channels: [string], if: if, loop: loop, ref: ref, type: text}], version: '2022-01-01', scope: default}", + "--notification.name", "name", + "--notification.subscription", "{topic_id: topic_id}", + "--notification.tags", "[string]", + "--state", "state", + ) + }) + + t.Run("piping data", func(t *testing.T) { + // Test piping YAML data over stdin + pipeData := []byte("" + + "notification:\n" + + " brand:\n" + + " id: id\n" + + " content:\n" + + " elements:\n" + + " - channels:\n" + + " - string\n" + + " if: if\n" + + " loop: loop\n" + + " ref: ref\n" + + " type: text\n" + + " version: '2022-01-01'\n" + + " scope: default\n" + + " name: name\n" + + " subscription:\n" + + " topic_id: topic_id\n" + + " tags:\n" + + " - string\n" + + "state: state\n") + mocktest.TestRunMockTestWithPipeAndFlags( + t, pipeData, + "--api-key", "string", + "journeys:templates", "replace", + "--template-id", "x", + "--notification-id", "x", + ) + }) +} diff --git a/pkg/cmd/list.go b/pkg/cmd/list.go index 9bd98f6..28ac970 100644 --- a/pkg/cmd/list.go +++ b/pkg/cmd/list.go @@ -5,7 +5,6 @@ package cmd import ( "context" "fmt" - "os" "github.com/tidwall/gjson" "github.com/trycourier/courier-cli/v3/internal/apiquery" @@ -21,8 +20,9 @@ var listsRetrieve = cli.Command{ Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "list-id", - Required: true, + Name: "list-id", + Required: true, + PathParam: "list_id", }, }, Action: handleListsRetrieve, @@ -35,8 +35,9 @@ var listsUpdate = requestflag.WithInnerFlags(cli.Command{ Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "list-id", - Required: true, + Name: "list-id", + Required: true, + PathParam: "list_id", }, &requestflag.Flag[string]{ Name: "name", @@ -68,12 +69,12 @@ var listsList = cli.Command{ Usage: "Returns all of the lists, with the ability to filter based on a pattern.", Suggest: true, Flags: []cli.Flag{ - &requestflag.Flag[any]{ + &requestflag.Flag[*string]{ Name: "cursor", Usage: "A unique identifier that allows for fetching the next page of lists.", QueryPath: "cursor", }, - &requestflag.Flag[any]{ + &requestflag.Flag[*string]{ Name: "pattern", Usage: "\"A pattern used to filter the list items returned. Pattern types supported: exact match on `list_id` or a pattern of one or more pattern parts. you may replace a part with either: `*` to match all parts in that position, or `**` to signify a wildcard `endsWith` pattern match.\"", QueryPath: "pattern", @@ -89,8 +90,9 @@ var listsDelete = cli.Command{ Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "list-id", - Required: true, + Name: "list-id", + Required: true, + PathParam: "list_id", }, }, Action: handleListsDelete, @@ -103,8 +105,9 @@ var listsRestore = cli.Command{ Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "list-id", - Required: true, + Name: "list-id", + Required: true, + PathParam: "list_id", }, }, Action: handleListsRestore, @@ -142,8 +145,15 @@ func handleListsRetrieve(ctx context.Context, cmd *cli.Command) error { obj := gjson.ParseBytes(res) format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") - return ShowJSON(os.Stdout, "lists retrieve", obj, format, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "lists retrieve", + Transform: transform, + }) } func handleListsUpdate(ctx context.Context, cmd *cli.Command) error { @@ -157,8 +167,6 @@ func handleListsUpdate(ctx context.Context, cmd *cli.Command) error { return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := courier.ListUpdateParams{} - options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -170,6 +178,8 @@ func handleListsUpdate(ctx context.Context, cmd *cli.Command) error { return err } + params := courier.ListUpdateParams{} + return client.Lists.Update( ctx, cmd.Value("list-id").(string), @@ -186,8 +196,6 @@ func handleListsList(ctx context.Context, cmd *cli.Command) error { return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := courier.ListListParams{} - options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -199,6 +207,8 @@ func handleListsList(ctx context.Context, cmd *cli.Command) error { return err } + params := courier.ListListParams{} + var res []byte options = append(options, option.WithResponseBodyInto(&res)) _, err = client.Lists.List(ctx, params, options...) @@ -208,8 +218,15 @@ func handleListsList(ctx context.Context, cmd *cli.Command) error { obj := gjson.ParseBytes(res) format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") - return ShowJSON(os.Stdout, "lists list", obj, format, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "lists list", + Transform: transform, + }) } func handleListsDelete(ctx context.Context, cmd *cli.Command) error { @@ -248,8 +265,6 @@ func handleListsRestore(ctx context.Context, cmd *cli.Command) error { return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := courier.ListRestoreParams{} - options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -261,6 +276,8 @@ func handleListsRestore(ctx context.Context, cmd *cli.Command) error { return err } + params := courier.ListRestoreParams{} + return client.Lists.Restore( ctx, cmd.Value("list-id").(string), diff --git a/pkg/cmd/listsubscription.go b/pkg/cmd/listsubscription.go index 683c80a..992b75c 100644 --- a/pkg/cmd/listsubscription.go +++ b/pkg/cmd/listsubscription.go @@ -5,7 +5,6 @@ package cmd import ( "context" "fmt" - "os" "github.com/tidwall/gjson" "github.com/trycourier/courier-cli/v3/internal/apiquery" @@ -21,10 +20,11 @@ var listsSubscriptionsList = cli.Command{ Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "list-id", - Required: true, + Name: "list-id", + Required: true, + PathParam: "list_id", }, - &requestflag.Flag[any]{ + &requestflag.Flag[*string]{ Name: "cursor", Usage: "A unique identifier that allows for fetching the next set of list subscriptions", QueryPath: "cursor", @@ -40,8 +40,9 @@ var listsSubscriptionsAdd = requestflag.WithInnerFlags(cli.Command{ Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "list-id", - Required: true, + Name: "list-id", + Required: true, + PathParam: "list_id", }, &requestflag.Flag[[]map[string]any]{ Name: "recipient", @@ -70,8 +71,9 @@ var listsSubscriptionsSubscribe = requestflag.WithInnerFlags(cli.Command{ Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "list-id", - Required: true, + Name: "list-id", + Required: true, + PathParam: "list_id", }, &requestflag.Flag[[]map[string]any]{ Name: "recipient", @@ -100,12 +102,14 @@ var listsSubscriptionsSubscribeUser = requestflag.WithInnerFlags(cli.Command{ Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "list-id", - Required: true, + Name: "list-id", + Required: true, + PathParam: "list_id", }, &requestflag.Flag[string]{ - Name: "user-id", - Required: true, + Name: "user-id", + Required: true, + PathParam: "user_id", }, &requestflag.Flag[map[string]any]{ Name: "preferences", @@ -133,12 +137,14 @@ var listsSubscriptionsUnsubscribeUser = cli.Command{ Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "list-id", - Required: true, + Name: "list-id", + Required: true, + PathParam: "list_id", }, &requestflag.Flag[string]{ - Name: "user-id", - Required: true, + Name: "user-id", + Required: true, + PathParam: "user_id", }, }, Action: handleListsSubscriptionsUnsubscribeUser, @@ -156,8 +162,6 @@ func handleListsSubscriptionsList(ctx context.Context, cmd *cli.Command) error { return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := courier.ListSubscriptionListParams{} - options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -169,6 +173,8 @@ func handleListsSubscriptionsList(ctx context.Context, cmd *cli.Command) error { return err } + params := courier.ListSubscriptionListParams{} + var res []byte options = append(options, option.WithResponseBodyInto(&res)) _, err = client.Lists.Subscriptions.List( @@ -183,8 +189,15 @@ func handleListsSubscriptionsList(ctx context.Context, cmd *cli.Command) error { obj := gjson.ParseBytes(res) format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") - return ShowJSON(os.Stdout, "lists:subscriptions list", obj, format, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "lists:subscriptions list", + Transform: transform, + }) } func handleListsSubscriptionsAdd(ctx context.Context, cmd *cli.Command) error { @@ -198,8 +211,6 @@ func handleListsSubscriptionsAdd(ctx context.Context, cmd *cli.Command) error { return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := courier.ListSubscriptionAddParams{} - options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -211,6 +222,8 @@ func handleListsSubscriptionsAdd(ctx context.Context, cmd *cli.Command) error { return err } + params := courier.ListSubscriptionAddParams{} + return client.Lists.Subscriptions.Add( ctx, cmd.Value("list-id").(string), @@ -230,8 +243,6 @@ func handleListsSubscriptionsSubscribe(ctx context.Context, cmd *cli.Command) er return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := courier.ListSubscriptionSubscribeParams{} - options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -243,6 +254,8 @@ func handleListsSubscriptionsSubscribe(ctx context.Context, cmd *cli.Command) er return err } + params := courier.ListSubscriptionSubscribeParams{} + return client.Lists.Subscriptions.Subscribe( ctx, cmd.Value("list-id").(string), @@ -262,10 +275,6 @@ func handleListsSubscriptionsSubscribeUser(ctx context.Context, cmd *cli.Command return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := courier.ListSubscriptionSubscribeUserParams{ - ListID: cmd.Value("list-id").(string), - } - options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -277,6 +286,10 @@ func handleListsSubscriptionsSubscribeUser(ctx context.Context, cmd *cli.Command return err } + params := courier.ListSubscriptionSubscribeUserParams{ + ListID: cmd.Value("list-id").(string), + } + return client.Lists.Subscriptions.SubscribeUser( ctx, cmd.Value("user-id").(string), @@ -296,10 +309,6 @@ func handleListsSubscriptionsUnsubscribeUser(ctx context.Context, cmd *cli.Comma return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := courier.ListSubscriptionUnsubscribeUserParams{ - ListID: cmd.Value("list-id").(string), - } - options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -311,6 +320,10 @@ func handleListsSubscriptionsUnsubscribeUser(ctx context.Context, cmd *cli.Comma return err } + params := courier.ListSubscriptionUnsubscribeUserParams{ + ListID: cmd.Value("list-id").(string), + } + return client.Lists.Subscriptions.UnsubscribeUser( ctx, cmd.Value("user-id").(string), diff --git a/pkg/cmd/message.go b/pkg/cmd/message.go index 5a4462e..1797afb 100644 --- a/pkg/cmd/message.go +++ b/pkg/cmd/message.go @@ -5,7 +5,6 @@ package cmd import ( "context" "fmt" - "os" "github.com/tidwall/gjson" "github.com/trycourier/courier-cli/v3/internal/apiquery" @@ -21,8 +20,9 @@ var messagesRetrieve = cli.Command{ Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "message-id", - Required: true, + Name: "message-id", + Required: true, + PathParam: "message_id", }, }, Action: handleMessagesRetrieve, @@ -34,72 +34,72 @@ var messagesList = cli.Command{ Usage: "Fetch the statuses of messages you've previously sent.", Suggest: true, Flags: []cli.Flag{ - &requestflag.Flag[any]{ + &requestflag.Flag[*bool]{ Name: "archived", Usage: "A boolean value that indicates whether archived messages should be included in the response.", QueryPath: "archived", }, - &requestflag.Flag[any]{ + &requestflag.Flag[*string]{ Name: "cursor", Usage: "A unique identifier that allows for fetching the next set of messages.", QueryPath: "cursor", }, - &requestflag.Flag[any]{ + &requestflag.Flag[*string]{ Name: "enqueued-after", Usage: "The enqueued datetime of a message to filter out messages received before.", QueryPath: "enqueued_after", }, - &requestflag.Flag[any]{ + &requestflag.Flag[*string]{ Name: "event", Usage: "A unique identifier representing the event that was used to send the event.", QueryPath: "event", }, - &requestflag.Flag[any]{ + &requestflag.Flag[*string]{ Name: "list", Usage: "A unique identifier representing the list the message was sent to.", QueryPath: "list", }, - &requestflag.Flag[any]{ + &requestflag.Flag[*string]{ Name: "message-id", Usage: "A unique identifier representing the message_id returned from either /send or /send/list.", QueryPath: "messageId", }, - &requestflag.Flag[any]{ + &requestflag.Flag[*string]{ Name: "notification", Usage: "A unique identifier representing the notification that was used to send the event.", QueryPath: "notification", }, - &requestflag.Flag[[]any]{ + &requestflag.Flag[[]string]{ Name: "provider", Usage: "The key assocated to the provider you want to filter on. E.g., sendgrid, inbox, twilio, slack, msteams, etc. Allows multiple values to be set in query parameters.", QueryPath: "provider", }, - &requestflag.Flag[any]{ + &requestflag.Flag[*string]{ Name: "recipient", Usage: "A unique identifier representing the recipient associated with the requested profile.", QueryPath: "recipient", }, - &requestflag.Flag[[]any]{ + &requestflag.Flag[[]string]{ Name: "status", Usage: "An indicator of the current status of the message. Allows multiple values to be set in query parameters.", QueryPath: "status", }, - &requestflag.Flag[[]any]{ + &requestflag.Flag[[]string]{ Name: "tag", Usage: "A tag placed in the metadata.tags during a notification send. Allows multiple values to be set in query parameters.", QueryPath: "tag", }, - &requestflag.Flag[any]{ + &requestflag.Flag[*string]{ Name: "tags", Usage: "A comma delimited list of 'tags'. Messages will be returned if they match any of the tags passed in.", QueryPath: "tags", }, - &requestflag.Flag[any]{ + &requestflag.Flag[*string]{ Name: "tenant-id", Usage: "Messages sent with the context of a Tenant", QueryPath: "tenant_id", }, - &requestflag.Flag[any]{ + &requestflag.Flag[*string]{ Name: "trace-id", Usage: "The unique identifier used to trace the requests", QueryPath: "traceId", @@ -115,8 +115,9 @@ var messagesCancel = cli.Command{ Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "message-id", - Required: true, + Name: "message-id", + Required: true, + PathParam: "message_id", }, }, Action: handleMessagesCancel, @@ -129,8 +130,9 @@ var messagesContent = cli.Command{ Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "message-id", - Required: true, + Name: "message-id", + Required: true, + PathParam: "message_id", }, }, Action: handleMessagesContent, @@ -143,10 +145,11 @@ var messagesHistory = cli.Command{ Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "message-id", - Required: true, + Name: "message-id", + Required: true, + PathParam: "message_id", }, - &requestflag.Flag[any]{ + &requestflag.Flag[*string]{ Name: "type", Usage: "A supported Message History type that will filter the events returned.", QueryPath: "type", @@ -187,8 +190,15 @@ func handleMessagesRetrieve(ctx context.Context, cmd *cli.Command) error { obj := gjson.ParseBytes(res) format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") - return ShowJSON(os.Stdout, "messages retrieve", obj, format, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "messages retrieve", + Transform: transform, + }) } func handleMessagesList(ctx context.Context, cmd *cli.Command) error { @@ -199,8 +209,6 @@ func handleMessagesList(ctx context.Context, cmd *cli.Command) error { return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := courier.MessageListParams{} - options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -212,6 +220,8 @@ func handleMessagesList(ctx context.Context, cmd *cli.Command) error { return err } + params := courier.MessageListParams{} + var res []byte options = append(options, option.WithResponseBodyInto(&res)) _, err = client.Messages.List(ctx, params, options...) @@ -221,8 +231,15 @@ func handleMessagesList(ctx context.Context, cmd *cli.Command) error { obj := gjson.ParseBytes(res) format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") - return ShowJSON(os.Stdout, "messages list", obj, format, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "messages list", + Transform: transform, + }) } func handleMessagesCancel(ctx context.Context, cmd *cli.Command) error { @@ -256,8 +273,15 @@ func handleMessagesCancel(ctx context.Context, cmd *cli.Command) error { obj := gjson.ParseBytes(res) format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") - return ShowJSON(os.Stdout, "messages cancel", obj, format, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "messages cancel", + Transform: transform, + }) } func handleMessagesContent(ctx context.Context, cmd *cli.Command) error { @@ -291,8 +315,15 @@ func handleMessagesContent(ctx context.Context, cmd *cli.Command) error { obj := gjson.ParseBytes(res) format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") - return ShowJSON(os.Stdout, "messages content", obj, format, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "messages content", + Transform: transform, + }) } func handleMessagesHistory(ctx context.Context, cmd *cli.Command) error { @@ -306,8 +337,6 @@ func handleMessagesHistory(ctx context.Context, cmd *cli.Command) error { return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := courier.MessageHistoryParams{} - options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -319,6 +348,8 @@ func handleMessagesHistory(ctx context.Context, cmd *cli.Command) error { return err } + params := courier.MessageHistoryParams{} + var res []byte options = append(options, option.WithResponseBodyInto(&res)) _, err = client.Messages.History( @@ -333,6 +364,13 @@ func handleMessagesHistory(ctx context.Context, cmd *cli.Command) error { obj := gjson.ParseBytes(res) format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") - return ShowJSON(os.Stdout, "messages history", obj, format, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "messages history", + Transform: transform, + }) } diff --git a/pkg/cmd/notification.go b/pkg/cmd/notification.go index b1b75d5..c6b6c92 100644 --- a/pkg/cmd/notification.go +++ b/pkg/cmd/notification.go @@ -5,7 +5,6 @@ package cmd import ( "context" "fmt" - "os" "github.com/tidwall/gjson" "github.com/trycourier/courier-cli/v3/internal/apiquery" @@ -22,7 +21,7 @@ var notificationsCreate = requestflag.WithInnerFlags(cli.Command{ Flags: []cli.Flag{ &requestflag.Flag[map[string]any]{ Name: "notification", - Usage: "Full document shape used in POST and PUT request bodies, and returned inside the GET response envelope.", + Usage: "Core template fields used in POST and PUT request bodies (nested under a `notification` key) and returned at the top level in responses.", Required: true, BodyPath: "notification", }, @@ -75,8 +74,9 @@ var notificationsRetrieve = cli.Command{ Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "id", - Required: true, + Name: "id", + Required: true, + PathParam: "id", }, &requestflag.Flag[string]{ Name: "version", @@ -93,7 +93,7 @@ var notificationsList = cli.Command{ Usage: "List notification templates in your workspace.", Suggest: true, Flags: []cli.Flag{ - &requestflag.Flag[any]{ + &requestflag.Flag[*string]{ Name: "cursor", Usage: "Opaque pagination cursor from a previous response. Omit for the first page.", QueryPath: "cursor", @@ -103,7 +103,7 @@ var notificationsList = cli.Command{ Usage: "Filter to templates linked to this event map ID.", QueryPath: "event_id", }, - &requestflag.Flag[any]{ + &requestflag.Flag[*bool]{ Name: "notes", Usage: "Include template notes in the response. Only applies to legacy templates.", QueryPath: "notes", @@ -119,8 +119,9 @@ var notificationsArchive = cli.Command{ Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "id", - Required: true, + Name: "id", + Required: true, + PathParam: "id", }, }, Action: handleNotificationsArchive, @@ -133,8 +134,9 @@ var notificationsListVersions = cli.Command{ Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "id", - Required: true, + Name: "id", + Required: true, + PathParam: "id", }, &requestflag.Flag[string]{ Name: "cursor", @@ -158,8 +160,9 @@ var notificationsPublish = cli.Command{ Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "id", - Required: true, + Name: "id", + Required: true, + PathParam: "id", }, &requestflag.Flag[string]{ Name: "version", @@ -177,8 +180,9 @@ var notificationsPutContent = requestflag.WithInnerFlags(cli.Command{ Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "id", - Required: true, + Name: "id", + Required: true, + PathParam: "id", }, &requestflag.Flag[map[string]any]{ Name: "content", @@ -215,12 +219,14 @@ var notificationsPutElement = cli.Command{ Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "id", - Required: true, + Name: "id", + Required: true, + PathParam: "id", }, &requestflag.Flag[string]{ - Name: "element-id", - Required: true, + Name: "element-id", + Required: true, + PathParam: "elementId", }, &requestflag.Flag[string]{ Name: "type", @@ -265,12 +271,14 @@ var notificationsPutLocale = requestflag.WithInnerFlags(cli.Command{ Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "id", - Required: true, + Name: "id", + Required: true, + PathParam: "id", }, &requestflag.Flag[string]{ - Name: "locale-id", - Required: true, + Name: "locale-id", + Required: true, + PathParam: "localeId", }, &requestflag.Flag[[]map[string]any]{ Name: "element", @@ -303,12 +311,13 @@ var notificationsReplace = requestflag.WithInnerFlags(cli.Command{ Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "id", - Required: true, + Name: "id", + Required: true, + PathParam: "id", }, &requestflag.Flag[map[string]any]{ Name: "notification", - Usage: "Full document shape used in POST and PUT request bodies, and returned inside the GET response envelope.", + Usage: "Core template fields used in POST and PUT request bodies (nested under a `notification` key) and returned at the top level in responses.", Required: true, BodyPath: "notification", }, @@ -361,8 +370,9 @@ var notificationsRetrieveContent = cli.Command{ Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "id", - Required: true, + Name: "id", + Required: true, + PathParam: "id", }, &requestflag.Flag[string]{ Name: "version", @@ -382,8 +392,6 @@ func handleNotificationsCreate(ctx context.Context, cmd *cli.Command) error { return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := courier.NotificationNewParams{} - options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -395,6 +403,8 @@ func handleNotificationsCreate(ctx context.Context, cmd *cli.Command) error { return err } + params := courier.NotificationNewParams{} + var res []byte options = append(options, option.WithResponseBodyInto(&res)) _, err = client.Notifications.New(ctx, params, options...) @@ -404,8 +414,15 @@ func handleNotificationsCreate(ctx context.Context, cmd *cli.Command) error { obj := gjson.ParseBytes(res) format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") - return ShowJSON(os.Stdout, "notifications create", obj, format, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "notifications create", + Transform: transform, + }) } func handleNotificationsRetrieve(ctx context.Context, cmd *cli.Command) error { @@ -419,8 +436,6 @@ func handleNotificationsRetrieve(ctx context.Context, cmd *cli.Command) error { return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := courier.NotificationGetParams{} - options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -432,6 +447,8 @@ func handleNotificationsRetrieve(ctx context.Context, cmd *cli.Command) error { return err } + params := courier.NotificationGetParams{} + var res []byte options = append(options, option.WithResponseBodyInto(&res)) _, err = client.Notifications.Get( @@ -446,8 +463,15 @@ func handleNotificationsRetrieve(ctx context.Context, cmd *cli.Command) error { obj := gjson.ParseBytes(res) format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") - return ShowJSON(os.Stdout, "notifications retrieve", obj, format, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "notifications retrieve", + Transform: transform, + }) } func handleNotificationsList(ctx context.Context, cmd *cli.Command) error { @@ -458,8 +482,6 @@ func handleNotificationsList(ctx context.Context, cmd *cli.Command) error { return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := courier.NotificationListParams{} - options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -471,6 +493,8 @@ func handleNotificationsList(ctx context.Context, cmd *cli.Command) error { return err } + params := courier.NotificationListParams{} + var res []byte options = append(options, option.WithResponseBodyInto(&res)) _, err = client.Notifications.List(ctx, params, options...) @@ -480,8 +504,15 @@ func handleNotificationsList(ctx context.Context, cmd *cli.Command) error { obj := gjson.ParseBytes(res) format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") - return ShowJSON(os.Stdout, "notifications list", obj, format, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "notifications list", + Transform: transform, + }) } func handleNotificationsArchive(ctx context.Context, cmd *cli.Command) error { @@ -520,8 +551,6 @@ func handleNotificationsListVersions(ctx context.Context, cmd *cli.Command) erro return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := courier.NotificationListVersionsParams{} - options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -533,6 +562,8 @@ func handleNotificationsListVersions(ctx context.Context, cmd *cli.Command) erro return err } + params := courier.NotificationListVersionsParams{} + var res []byte options = append(options, option.WithResponseBodyInto(&res)) _, err = client.Notifications.ListVersions( @@ -547,8 +578,15 @@ func handleNotificationsListVersions(ctx context.Context, cmd *cli.Command) erro obj := gjson.ParseBytes(res) format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") - return ShowJSON(os.Stdout, "notifications list-versions", obj, format, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "notifications list-versions", + Transform: transform, + }) } func handleNotificationsPublish(ctx context.Context, cmd *cli.Command) error { @@ -562,8 +600,6 @@ func handleNotificationsPublish(ctx context.Context, cmd *cli.Command) error { return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := courier.NotificationPublishParams{} - options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -575,6 +611,8 @@ func handleNotificationsPublish(ctx context.Context, cmd *cli.Command) error { return err } + params := courier.NotificationPublishParams{} + return client.Notifications.Publish( ctx, cmd.Value("id").(string), @@ -594,8 +632,6 @@ func handleNotificationsPutContent(ctx context.Context, cmd *cli.Command) error return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := courier.NotificationPutContentParams{} - options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -607,6 +643,8 @@ func handleNotificationsPutContent(ctx context.Context, cmd *cli.Command) error return err } + params := courier.NotificationPutContentParams{} + var res []byte options = append(options, option.WithResponseBodyInto(&res)) _, err = client.Notifications.PutContent( @@ -621,8 +659,15 @@ func handleNotificationsPutContent(ctx context.Context, cmd *cli.Command) error obj := gjson.ParseBytes(res) format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") - return ShowJSON(os.Stdout, "notifications put-content", obj, format, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "notifications put-content", + Transform: transform, + }) } func handleNotificationsPutElement(ctx context.Context, cmd *cli.Command) error { @@ -636,10 +681,6 @@ func handleNotificationsPutElement(ctx context.Context, cmd *cli.Command) error return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := courier.NotificationPutElementParams{ - ID: cmd.Value("id").(string), - } - options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -651,6 +692,10 @@ func handleNotificationsPutElement(ctx context.Context, cmd *cli.Command) error return err } + params := courier.NotificationPutElementParams{ + ID: cmd.Value("id").(string), + } + var res []byte options = append(options, option.WithResponseBodyInto(&res)) _, err = client.Notifications.PutElement( @@ -665,8 +710,15 @@ func handleNotificationsPutElement(ctx context.Context, cmd *cli.Command) error obj := gjson.ParseBytes(res) format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") - return ShowJSON(os.Stdout, "notifications put-element", obj, format, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "notifications put-element", + Transform: transform, + }) } func handleNotificationsPutLocale(ctx context.Context, cmd *cli.Command) error { @@ -680,10 +732,6 @@ func handleNotificationsPutLocale(ctx context.Context, cmd *cli.Command) error { return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := courier.NotificationPutLocaleParams{ - ID: cmd.Value("id").(string), - } - options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -695,6 +743,10 @@ func handleNotificationsPutLocale(ctx context.Context, cmd *cli.Command) error { return err } + params := courier.NotificationPutLocaleParams{ + ID: cmd.Value("id").(string), + } + var res []byte options = append(options, option.WithResponseBodyInto(&res)) _, err = client.Notifications.PutLocale( @@ -709,8 +761,15 @@ func handleNotificationsPutLocale(ctx context.Context, cmd *cli.Command) error { obj := gjson.ParseBytes(res) format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") - return ShowJSON(os.Stdout, "notifications put-locale", obj, format, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "notifications put-locale", + Transform: transform, + }) } func handleNotificationsReplace(ctx context.Context, cmd *cli.Command) error { @@ -724,8 +783,6 @@ func handleNotificationsReplace(ctx context.Context, cmd *cli.Command) error { return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := courier.NotificationReplaceParams{} - options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -737,6 +794,8 @@ func handleNotificationsReplace(ctx context.Context, cmd *cli.Command) error { return err } + params := courier.NotificationReplaceParams{} + var res []byte options = append(options, option.WithResponseBodyInto(&res)) _, err = client.Notifications.Replace( @@ -751,8 +810,15 @@ func handleNotificationsReplace(ctx context.Context, cmd *cli.Command) error { obj := gjson.ParseBytes(res) format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") - return ShowJSON(os.Stdout, "notifications replace", obj, format, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "notifications replace", + Transform: transform, + }) } func handleNotificationsRetrieveContent(ctx context.Context, cmd *cli.Command) error { @@ -766,8 +832,6 @@ func handleNotificationsRetrieveContent(ctx context.Context, cmd *cli.Command) e return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := courier.NotificationGetContentParams{} - options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -779,6 +843,8 @@ func handleNotificationsRetrieveContent(ctx context.Context, cmd *cli.Command) e return err } + params := courier.NotificationGetContentParams{} + var res []byte options = append(options, option.WithResponseBodyInto(&res)) _, err = client.Notifications.GetContent( @@ -793,6 +859,13 @@ func handleNotificationsRetrieveContent(ctx context.Context, cmd *cli.Command) e obj := gjson.ParseBytes(res) format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") - return ShowJSON(os.Stdout, "notifications retrieve-content", obj, format, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "notifications retrieve-content", + Transform: transform, + }) } diff --git a/pkg/cmd/notificationcheck.go b/pkg/cmd/notificationcheck.go index 11eb60d..a25e6ff 100644 --- a/pkg/cmd/notificationcheck.go +++ b/pkg/cmd/notificationcheck.go @@ -5,7 +5,6 @@ package cmd import ( "context" "fmt" - "os" "github.com/tidwall/gjson" "github.com/trycourier/courier-cli/v3/internal/apiquery" @@ -21,12 +20,14 @@ var notificationsChecksUpdate = requestflag.WithInnerFlags(cli.Command{ Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "id", - Required: true, + Name: "id", + Required: true, + PathParam: "id", }, &requestflag.Flag[string]{ - Name: "submission-id", - Required: true, + Name: "submission-id", + Required: true, + PathParam: "submissionId", }, &requestflag.Flag[[]map[string]any]{ Name: "check", @@ -61,12 +62,14 @@ var notificationsChecksList = cli.Command{ Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "id", - Required: true, + Name: "id", + Required: true, + PathParam: "id", }, &requestflag.Flag[string]{ - Name: "submission-id", - Required: true, + Name: "submission-id", + Required: true, + PathParam: "submissionId", }, }, Action: handleNotificationsChecksList, @@ -79,12 +82,14 @@ var notificationsChecksDelete = cli.Command{ Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "id", - Required: true, + Name: "id", + Required: true, + PathParam: "id", }, &requestflag.Flag[string]{ - Name: "submission-id", - Required: true, + Name: "submission-id", + Required: true, + PathParam: "submissionId", }, }, Action: handleNotificationsChecksDelete, @@ -102,10 +107,6 @@ func handleNotificationsChecksUpdate(ctx context.Context, cmd *cli.Command) erro return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := courier.NotificationCheckUpdateParams{ - ID: cmd.Value("id").(string), - } - options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -117,6 +118,10 @@ func handleNotificationsChecksUpdate(ctx context.Context, cmd *cli.Command) erro return err } + params := courier.NotificationCheckUpdateParams{ + ID: cmd.Value("id").(string), + } + var res []byte options = append(options, option.WithResponseBodyInto(&res)) _, err = client.Notifications.Checks.Update( @@ -131,8 +136,15 @@ func handleNotificationsChecksUpdate(ctx context.Context, cmd *cli.Command) erro obj := gjson.ParseBytes(res) format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") - return ShowJSON(os.Stdout, "notifications:checks update", obj, format, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "notifications:checks update", + Transform: transform, + }) } func handleNotificationsChecksList(ctx context.Context, cmd *cli.Command) error { @@ -146,10 +158,6 @@ func handleNotificationsChecksList(ctx context.Context, cmd *cli.Command) error return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := courier.NotificationCheckListParams{ - ID: cmd.Value("id").(string), - } - options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -161,6 +169,10 @@ func handleNotificationsChecksList(ctx context.Context, cmd *cli.Command) error return err } + params := courier.NotificationCheckListParams{ + ID: cmd.Value("id").(string), + } + var res []byte options = append(options, option.WithResponseBodyInto(&res)) _, err = client.Notifications.Checks.List( @@ -175,8 +187,15 @@ func handleNotificationsChecksList(ctx context.Context, cmd *cli.Command) error obj := gjson.ParseBytes(res) format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") - return ShowJSON(os.Stdout, "notifications:checks list", obj, format, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "notifications:checks list", + Transform: transform, + }) } func handleNotificationsChecksDelete(ctx context.Context, cmd *cli.Command) error { @@ -190,10 +209,6 @@ func handleNotificationsChecksDelete(ctx context.Context, cmd *cli.Command) erro return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := courier.NotificationCheckDeleteParams{ - ID: cmd.Value("id").(string), - } - options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -205,6 +220,10 @@ func handleNotificationsChecksDelete(ctx context.Context, cmd *cli.Command) erro return err } + params := courier.NotificationCheckDeleteParams{ + ID: cmd.Value("id").(string), + } + return client.Notifications.Checks.Delete( ctx, cmd.Value("submission-id").(string), diff --git a/pkg/cmd/profile.go b/pkg/cmd/profile.go index 56eca9f..3b0ee25 100644 --- a/pkg/cmd/profile.go +++ b/pkg/cmd/profile.go @@ -5,7 +5,6 @@ package cmd import ( "context" "fmt" - "os" "github.com/tidwall/gjson" "github.com/trycourier/courier-cli/v3/internal/apiquery" @@ -21,8 +20,9 @@ var profilesCreate = cli.Command{ Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "user-id", - Required: true, + Name: "user-id", + Required: true, + PathParam: "user_id", }, &requestflag.Flag[map[string]any]{ Name: "profile", @@ -40,8 +40,9 @@ var profilesRetrieve = cli.Command{ Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "user-id", - Required: true, + Name: "user-id", + Required: true, + PathParam: "user_id", }, }, Action: handleProfilesRetrieve, @@ -54,8 +55,9 @@ var profilesUpdate = requestflag.WithInnerFlags(cli.Command{ Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "user-id", - Required: true, + Name: "user-id", + Required: true, + PathParam: "user_id", }, &requestflag.Flag[[]map[string]any]{ Name: "patch", @@ -92,8 +94,9 @@ var profilesDelete = cli.Command{ Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "user-id", - Required: true, + Name: "user-id", + Required: true, + PathParam: "user_id", }, }, Action: handleProfilesDelete, @@ -106,8 +109,9 @@ var profilesReplace = cli.Command{ Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "user-id", - Required: true, + Name: "user-id", + Required: true, + PathParam: "user_id", }, &requestflag.Flag[map[string]any]{ Name: "profile", @@ -130,8 +134,6 @@ func handleProfilesCreate(ctx context.Context, cmd *cli.Command) error { return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := courier.ProfileNewParams{} - options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -143,6 +145,8 @@ func handleProfilesCreate(ctx context.Context, cmd *cli.Command) error { return err } + params := courier.ProfileNewParams{} + var res []byte options = append(options, option.WithResponseBodyInto(&res)) _, err = client.Profiles.New( @@ -157,8 +161,15 @@ func handleProfilesCreate(ctx context.Context, cmd *cli.Command) error { obj := gjson.ParseBytes(res) format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") - return ShowJSON(os.Stdout, "profiles create", obj, format, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "profiles create", + Transform: transform, + }) } func handleProfilesRetrieve(ctx context.Context, cmd *cli.Command) error { @@ -192,8 +203,15 @@ func handleProfilesRetrieve(ctx context.Context, cmd *cli.Command) error { obj := gjson.ParseBytes(res) format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") - return ShowJSON(os.Stdout, "profiles retrieve", obj, format, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "profiles retrieve", + Transform: transform, + }) } func handleProfilesUpdate(ctx context.Context, cmd *cli.Command) error { @@ -207,8 +225,6 @@ func handleProfilesUpdate(ctx context.Context, cmd *cli.Command) error { return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := courier.ProfileUpdateParams{} - options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -220,6 +236,8 @@ func handleProfilesUpdate(ctx context.Context, cmd *cli.Command) error { return err } + params := courier.ProfileUpdateParams{} + return client.Profiles.Update( ctx, cmd.Value("user-id").(string), @@ -264,8 +282,6 @@ func handleProfilesReplace(ctx context.Context, cmd *cli.Command) error { return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := courier.ProfileReplaceParams{} - options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -277,6 +293,8 @@ func handleProfilesReplace(ctx context.Context, cmd *cli.Command) error { return err } + params := courier.ProfileReplaceParams{} + var res []byte options = append(options, option.WithResponseBodyInto(&res)) _, err = client.Profiles.Replace( @@ -291,6 +309,13 @@ func handleProfilesReplace(ctx context.Context, cmd *cli.Command) error { obj := gjson.ParseBytes(res) format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") - return ShowJSON(os.Stdout, "profiles replace", obj, format, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "profiles replace", + Transform: transform, + }) } diff --git a/pkg/cmd/profilelist.go b/pkg/cmd/profilelist.go index 8155ed9..d4ec913 100644 --- a/pkg/cmd/profilelist.go +++ b/pkg/cmd/profilelist.go @@ -5,7 +5,6 @@ package cmd import ( "context" "fmt" - "os" "github.com/tidwall/gjson" "github.com/trycourier/courier-cli/v3/internal/apiquery" @@ -21,10 +20,11 @@ var profilesListsRetrieve = cli.Command{ Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "user-id", - Required: true, + Name: "user-id", + Required: true, + PathParam: "user_id", }, - &requestflag.Flag[any]{ + &requestflag.Flag[*string]{ Name: "cursor", Usage: "A unique identifier that allows for fetching the next set of message statuses.", QueryPath: "cursor", @@ -40,8 +40,9 @@ var profilesListsDelete = cli.Command{ Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "user-id", - Required: true, + Name: "user-id", + Required: true, + PathParam: "user_id", }, }, Action: handleProfilesListsDelete, @@ -54,8 +55,9 @@ var profilesListsSubscribe = requestflag.WithInnerFlags(cli.Command{ Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "user-id", - Required: true, + Name: "user-id", + Required: true, + PathParam: "user_id", }, &requestflag.Flag[[]map[string]any]{ Name: "list", @@ -89,8 +91,6 @@ func handleProfilesListsRetrieve(ctx context.Context, cmd *cli.Command) error { return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := courier.ProfileListGetParams{} - options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -102,6 +102,8 @@ func handleProfilesListsRetrieve(ctx context.Context, cmd *cli.Command) error { return err } + params := courier.ProfileListGetParams{} + var res []byte options = append(options, option.WithResponseBodyInto(&res)) _, err = client.Profiles.Lists.Get( @@ -116,8 +118,15 @@ func handleProfilesListsRetrieve(ctx context.Context, cmd *cli.Command) error { obj := gjson.ParseBytes(res) format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") - return ShowJSON(os.Stdout, "profiles:lists retrieve", obj, format, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "profiles:lists retrieve", + Transform: transform, + }) } func handleProfilesListsDelete(ctx context.Context, cmd *cli.Command) error { @@ -151,8 +160,15 @@ func handleProfilesListsDelete(ctx context.Context, cmd *cli.Command) error { obj := gjson.ParseBytes(res) format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") - return ShowJSON(os.Stdout, "profiles:lists delete", obj, format, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "profiles:lists delete", + Transform: transform, + }) } func handleProfilesListsSubscribe(ctx context.Context, cmd *cli.Command) error { @@ -166,8 +182,6 @@ func handleProfilesListsSubscribe(ctx context.Context, cmd *cli.Command) error { return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := courier.ProfileListSubscribeParams{} - options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -179,6 +193,8 @@ func handleProfilesListsSubscribe(ctx context.Context, cmd *cli.Command) error { return err } + params := courier.ProfileListSubscribeParams{} + var res []byte options = append(options, option.WithResponseBodyInto(&res)) _, err = client.Profiles.Lists.Subscribe( @@ -193,6 +209,13 @@ func handleProfilesListsSubscribe(ctx context.Context, cmd *cli.Command) error { obj := gjson.ParseBytes(res) format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") - return ShowJSON(os.Stdout, "profiles:lists subscribe", obj, format, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "profiles:lists subscribe", + Transform: transform, + }) } diff --git a/pkg/cmd/provider.go b/pkg/cmd/provider.go index 3a237aa..ca5eddd 100644 --- a/pkg/cmd/provider.go +++ b/pkg/cmd/provider.go @@ -5,7 +5,6 @@ package cmd import ( "context" "fmt" - "os" "github.com/tidwall/gjson" "github.com/trycourier/courier-cli/v3/internal/apiquery" @@ -52,8 +51,9 @@ var providersRetrieve = cli.Command{ Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "id", - Required: true, + Name: "id", + Required: true, + PathParam: "id", }, }, Action: handleProvidersRetrieve, @@ -66,8 +66,9 @@ var providersUpdate = cli.Command{ Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "id", - Required: true, + Name: "id", + Required: true, + PathParam: "id", }, &requestflag.Flag[string]{ Name: "provider", @@ -116,8 +117,9 @@ var providersDelete = cli.Command{ Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "id", - Required: true, + Name: "id", + Required: true, + PathParam: "id", }, }, Action: handleProvidersDelete, @@ -132,8 +134,6 @@ func handleProvidersCreate(ctx context.Context, cmd *cli.Command) error { return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := courier.ProviderNewParams{} - options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -145,6 +145,8 @@ func handleProvidersCreate(ctx context.Context, cmd *cli.Command) error { return err } + params := courier.ProviderNewParams{} + var res []byte options = append(options, option.WithResponseBodyInto(&res)) _, err = client.Providers.New(ctx, params, options...) @@ -154,8 +156,15 @@ func handleProvidersCreate(ctx context.Context, cmd *cli.Command) error { obj := gjson.ParseBytes(res) format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") - return ShowJSON(os.Stdout, "providers create", obj, format, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "providers create", + Transform: transform, + }) } func handleProvidersRetrieve(ctx context.Context, cmd *cli.Command) error { @@ -189,8 +198,15 @@ func handleProvidersRetrieve(ctx context.Context, cmd *cli.Command) error { obj := gjson.ParseBytes(res) format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") - return ShowJSON(os.Stdout, "providers retrieve", obj, format, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "providers retrieve", + Transform: transform, + }) } func handleProvidersUpdate(ctx context.Context, cmd *cli.Command) error { @@ -204,8 +220,6 @@ func handleProvidersUpdate(ctx context.Context, cmd *cli.Command) error { return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := courier.ProviderUpdateParams{} - options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -217,6 +231,8 @@ func handleProvidersUpdate(ctx context.Context, cmd *cli.Command) error { return err } + params := courier.ProviderUpdateParams{} + var res []byte options = append(options, option.WithResponseBodyInto(&res)) _, err = client.Providers.Update( @@ -231,8 +247,15 @@ func handleProvidersUpdate(ctx context.Context, cmd *cli.Command) error { obj := gjson.ParseBytes(res) format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") - return ShowJSON(os.Stdout, "providers update", obj, format, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "providers update", + Transform: transform, + }) } func handleProvidersList(ctx context.Context, cmd *cli.Command) error { @@ -243,8 +266,6 @@ func handleProvidersList(ctx context.Context, cmd *cli.Command) error { return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := courier.ProviderListParams{} - options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -256,6 +277,8 @@ func handleProvidersList(ctx context.Context, cmd *cli.Command) error { return err } + params := courier.ProviderListParams{} + var res []byte options = append(options, option.WithResponseBodyInto(&res)) _, err = client.Providers.List(ctx, params, options...) @@ -265,8 +288,15 @@ func handleProvidersList(ctx context.Context, cmd *cli.Command) error { obj := gjson.ParseBytes(res) format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") - return ShowJSON(os.Stdout, "providers list", obj, format, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "providers list", + Transform: transform, + }) } func handleProvidersDelete(ctx context.Context, cmd *cli.Command) error { diff --git a/pkg/cmd/providercatalog.go b/pkg/cmd/providercatalog.go index 5eda0fa..d0283e8 100644 --- a/pkg/cmd/providercatalog.go +++ b/pkg/cmd/providercatalog.go @@ -5,7 +5,6 @@ package cmd import ( "context" "fmt" - "os" "github.com/tidwall/gjson" "github.com/trycourier/courier-cli/v3/internal/apiquery" @@ -48,8 +47,6 @@ func handleProvidersCatalogList(ctx context.Context, cmd *cli.Command) error { return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := courier.ProviderCatalogListParams{} - options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -61,6 +58,8 @@ func handleProvidersCatalogList(ctx context.Context, cmd *cli.Command) error { return err } + params := courier.ProviderCatalogListParams{} + var res []byte options = append(options, option.WithResponseBodyInto(&res)) _, err = client.Providers.Catalog.List(ctx, params, options...) @@ -70,6 +69,13 @@ func handleProvidersCatalogList(ctx context.Context, cmd *cli.Command) error { obj := gjson.ParseBytes(res) format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") - return ShowJSON(os.Stdout, "providers:catalog list", obj, format, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "providers:catalog list", + Transform: transform, + }) } diff --git a/pkg/cmd/request.go b/pkg/cmd/request.go index ff00256..723e450 100644 --- a/pkg/cmd/request.go +++ b/pkg/cmd/request.go @@ -18,8 +18,9 @@ var requestsArchive = cli.Command{ Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "request-id", - Required: true, + Name: "request-id", + Required: true, + PathParam: "request_id", }, }, Action: handleRequestsArchive, diff --git a/pkg/cmd/routingstrategy.go b/pkg/cmd/routingstrategy.go index f03d2fe..69dc848 100644 --- a/pkg/cmd/routingstrategy.go +++ b/pkg/cmd/routingstrategy.go @@ -5,7 +5,6 @@ package cmd import ( "context" "fmt" - "os" "github.com/tidwall/gjson" "github.com/trycourier/courier-cli/v3/internal/apiquery" @@ -35,7 +34,7 @@ var routingStrategiesCreate = requestflag.WithInnerFlags(cli.Command{ Name: "channels", BodyPath: "channels", }, - &requestflag.Flag[any]{ + &requestflag.Flag[*string]{ Name: "description", Usage: "Optional description of the routing strategy.", BodyPath: "description", @@ -72,8 +71,9 @@ var routingStrategiesRetrieve = cli.Command{ Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "id", - Required: true, + Name: "id", + Required: true, + PathParam: "id", }, }, Action: handleRoutingStrategiesRetrieve, @@ -85,7 +85,7 @@ var routingStrategiesList = cli.Command{ Usage: "List routing strategies in your workspace. Returns metadata only (no\nrouting/channels/providers content). Use GET /routing-strategies/{id} for full\ndetails.", Suggest: true, Flags: []cli.Flag{ - &requestflag.Flag[any]{ + &requestflag.Flag[*string]{ Name: "cursor", Usage: "Opaque pagination cursor from a previous response. Omit for the first page.", QueryPath: "cursor", @@ -107,8 +107,9 @@ var routingStrategiesArchive = cli.Command{ Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "id", - Required: true, + Name: "id", + Required: true, + PathParam: "id", }, }, Action: handleRoutingStrategiesArchive, @@ -121,10 +122,11 @@ var routingStrategiesListNotifications = cli.Command{ Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "id", - Required: true, + Name: "id", + Required: true, + PathParam: "id", }, - &requestflag.Flag[any]{ + &requestflag.Flag[*string]{ Name: "cursor", Usage: "Opaque pagination cursor from a previous response. Omit for the first page.", QueryPath: "cursor", @@ -146,8 +148,9 @@ var routingStrategiesReplace = requestflag.WithInnerFlags(cli.Command{ Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "id", - Required: true, + Name: "id", + Required: true, + PathParam: "id", }, &requestflag.Flag[string]{ Name: "name", @@ -164,7 +167,7 @@ var routingStrategiesReplace = requestflag.WithInnerFlags(cli.Command{ Name: "channels", BodyPath: "channels", }, - &requestflag.Flag[any]{ + &requestflag.Flag[*string]{ Name: "description", Usage: "Optional description. Omit or null to clear.", BodyPath: "description", @@ -203,8 +206,6 @@ func handleRoutingStrategiesCreate(ctx context.Context, cmd *cli.Command) error return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := courier.RoutingStrategyNewParams{} - options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -216,6 +217,8 @@ func handleRoutingStrategiesCreate(ctx context.Context, cmd *cli.Command) error return err } + params := courier.RoutingStrategyNewParams{} + var res []byte options = append(options, option.WithResponseBodyInto(&res)) _, err = client.RoutingStrategies.New(ctx, params, options...) @@ -225,8 +228,15 @@ func handleRoutingStrategiesCreate(ctx context.Context, cmd *cli.Command) error obj := gjson.ParseBytes(res) format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") - return ShowJSON(os.Stdout, "routing-strategies create", obj, format, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "routing-strategies create", + Transform: transform, + }) } func handleRoutingStrategiesRetrieve(ctx context.Context, cmd *cli.Command) error { @@ -260,8 +270,15 @@ func handleRoutingStrategiesRetrieve(ctx context.Context, cmd *cli.Command) erro obj := gjson.ParseBytes(res) format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") - return ShowJSON(os.Stdout, "routing-strategies retrieve", obj, format, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "routing-strategies retrieve", + Transform: transform, + }) } func handleRoutingStrategiesList(ctx context.Context, cmd *cli.Command) error { @@ -272,8 +289,6 @@ func handleRoutingStrategiesList(ctx context.Context, cmd *cli.Command) error { return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := courier.RoutingStrategyListParams{} - options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -285,6 +300,8 @@ func handleRoutingStrategiesList(ctx context.Context, cmd *cli.Command) error { return err } + params := courier.RoutingStrategyListParams{} + var res []byte options = append(options, option.WithResponseBodyInto(&res)) _, err = client.RoutingStrategies.List(ctx, params, options...) @@ -294,8 +311,15 @@ func handleRoutingStrategiesList(ctx context.Context, cmd *cli.Command) error { obj := gjson.ParseBytes(res) format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") - return ShowJSON(os.Stdout, "routing-strategies list", obj, format, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "routing-strategies list", + Transform: transform, + }) } func handleRoutingStrategiesArchive(ctx context.Context, cmd *cli.Command) error { @@ -334,8 +358,6 @@ func handleRoutingStrategiesListNotifications(ctx context.Context, cmd *cli.Comm return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := courier.RoutingStrategyListNotificationsParams{} - options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -347,6 +369,8 @@ func handleRoutingStrategiesListNotifications(ctx context.Context, cmd *cli.Comm return err } + params := courier.RoutingStrategyListNotificationsParams{} + var res []byte options = append(options, option.WithResponseBodyInto(&res)) _, err = client.RoutingStrategies.ListNotifications( @@ -361,8 +385,15 @@ func handleRoutingStrategiesListNotifications(ctx context.Context, cmd *cli.Comm obj := gjson.ParseBytes(res) format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") - return ShowJSON(os.Stdout, "routing-strategies list-notifications", obj, format, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "routing-strategies list-notifications", + Transform: transform, + }) } func handleRoutingStrategiesReplace(ctx context.Context, cmd *cli.Command) error { @@ -376,8 +407,6 @@ func handleRoutingStrategiesReplace(ctx context.Context, cmd *cli.Command) error return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := courier.RoutingStrategyReplaceParams{} - options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -389,6 +418,8 @@ func handleRoutingStrategiesReplace(ctx context.Context, cmd *cli.Command) error return err } + params := courier.RoutingStrategyReplaceParams{} + var res []byte options = append(options, option.WithResponseBodyInto(&res)) _, err = client.RoutingStrategies.Replace( @@ -403,6 +434,13 @@ func handleRoutingStrategiesReplace(ctx context.Context, cmd *cli.Command) error obj := gjson.ParseBytes(res) format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") - return ShowJSON(os.Stdout, "routing-strategies replace", obj, format, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "routing-strategies replace", + Transform: transform, + }) } diff --git a/pkg/cmd/send.go b/pkg/cmd/send.go index 3478ded..c11da4a 100644 --- a/pkg/cmd/send.go +++ b/pkg/cmd/send.go @@ -5,7 +5,6 @@ package cmd import ( "context" "fmt" - "os" "github.com/tidwall/gjson" "github.com/trycourier/courier-cli/v3/internal/apiquery" @@ -31,7 +30,7 @@ var sendMessage = requestflag.WithInnerFlags(cli.Command{ HideHelpCommand: true, }, map[string][]requestflag.HasOuterFlag{ "message": { - &requestflag.InnerFlag[any]{ + &requestflag.InnerFlag[*string]{ Name: "message.brand-id", InnerField: "brand_id", }, @@ -77,7 +76,7 @@ var sendMessage = requestflag.WithInnerFlags(cli.Command{ Usage: "Customize which channels/providers Courier may deliver the message through.", InnerField: "routing", }, - &requestflag.InnerFlag[any]{ + &requestflag.InnerFlag[*string]{ Name: "message.template", InnerField: "template", }, @@ -101,8 +100,6 @@ func handleSendMessage(ctx context.Context, cmd *cli.Command) error { return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := courier.SendMessageParams{} - options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -114,6 +111,8 @@ func handleSendMessage(ctx context.Context, cmd *cli.Command) error { return err } + params := courier.SendMessageParams{} + var res []byte options = append(options, option.WithResponseBodyInto(&res)) _, err = client.Send.Message(ctx, params, options...) @@ -123,6 +122,13 @@ func handleSendMessage(ctx context.Context, cmd *cli.Command) error { obj := gjson.ParseBytes(res) format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") - return ShowJSON(os.Stdout, "send message", obj, format, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "send message", + Transform: transform, + }) } diff --git a/pkg/cmd/tenant.go b/pkg/cmd/tenant.go index 52c014a..2d19db9 100644 --- a/pkg/cmd/tenant.go +++ b/pkg/cmd/tenant.go @@ -5,7 +5,6 @@ package cmd import ( "context" "fmt" - "os" "github.com/tidwall/gjson" "github.com/trycourier/courier-cli/v3/internal/apiquery" @@ -21,8 +20,9 @@ var tenantsRetrieve = cli.Command{ Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "tenant-id", - Required: true, + Name: "tenant-id", + Required: true, + PathParam: "tenant_id", }, }, Action: handleTenantsRetrieve, @@ -35,8 +35,9 @@ var tenantsUpdate = requestflag.WithInnerFlags(cli.Command{ Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "tenant-id", - Required: true, + Name: "tenant-id", + Required: true, + PathParam: "tenant_id", }, &requestflag.Flag[string]{ Name: "name", @@ -44,7 +45,7 @@ var tenantsUpdate = requestflag.WithInnerFlags(cli.Command{ Required: true, BodyPath: "name", }, - &requestflag.Flag[any]{ + &requestflag.Flag[*string]{ Name: "brand-id", Usage: "Brand to be used for the account when one is not specified by the send call.", BodyPath: "brand_id", @@ -53,7 +54,7 @@ var tenantsUpdate = requestflag.WithInnerFlags(cli.Command{ Name: "default-preferences", BodyPath: "default_preferences", }, - &requestflag.Flag[any]{ + &requestflag.Flag[*string]{ Name: "parent-tenant-id", Usage: "Tenant's parent id (if any).", BodyPath: "parent_tenant_id", @@ -85,17 +86,17 @@ var tenantsList = cli.Command{ Usage: "Get a List of Tenants", Suggest: true, Flags: []cli.Flag{ - &requestflag.Flag[any]{ + &requestflag.Flag[*string]{ Name: "cursor", Usage: "Continue the pagination with the next cursor", QueryPath: "cursor", }, - &requestflag.Flag[any]{ + &requestflag.Flag[*int64]{ Name: "limit", Usage: "The number of tenants to return \n(defaults to 20, maximum value of 100)", QueryPath: "limit", }, - &requestflag.Flag[any]{ + &requestflag.Flag[*string]{ Name: "parent-tenant-id", Usage: "Filter the list of tenants by parent_id", QueryPath: "parent_tenant_id", @@ -111,8 +112,9 @@ var tenantsDelete = cli.Command{ Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "tenant-id", - Required: true, + Name: "tenant-id", + Required: true, + PathParam: "tenant_id", }, }, Action: handleTenantsDelete, @@ -125,15 +127,16 @@ var tenantsListUsers = cli.Command{ Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "tenant-id", - Required: true, + Name: "tenant-id", + Required: true, + PathParam: "tenant_id", }, - &requestflag.Flag[any]{ + &requestflag.Flag[*string]{ Name: "cursor", Usage: "Continue the pagination with the next cursor", QueryPath: "cursor", }, - &requestflag.Flag[any]{ + &requestflag.Flag[*int64]{ Name: "limit", Usage: "The number of accounts to return \n(defaults to 20, maximum value of 100)", QueryPath: "limit", @@ -174,8 +177,15 @@ func handleTenantsRetrieve(ctx context.Context, cmd *cli.Command) error { obj := gjson.ParseBytes(res) format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") - return ShowJSON(os.Stdout, "tenants retrieve", obj, format, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "tenants retrieve", + Transform: transform, + }) } func handleTenantsUpdate(ctx context.Context, cmd *cli.Command) error { @@ -189,8 +199,6 @@ func handleTenantsUpdate(ctx context.Context, cmd *cli.Command) error { return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := courier.TenantUpdateParams{} - options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -202,6 +210,8 @@ func handleTenantsUpdate(ctx context.Context, cmd *cli.Command) error { return err } + params := courier.TenantUpdateParams{} + var res []byte options = append(options, option.WithResponseBodyInto(&res)) _, err = client.Tenants.Update( @@ -216,8 +226,15 @@ func handleTenantsUpdate(ctx context.Context, cmd *cli.Command) error { obj := gjson.ParseBytes(res) format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") - return ShowJSON(os.Stdout, "tenants update", obj, format, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "tenants update", + Transform: transform, + }) } func handleTenantsList(ctx context.Context, cmd *cli.Command) error { @@ -228,8 +245,6 @@ func handleTenantsList(ctx context.Context, cmd *cli.Command) error { return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := courier.TenantListParams{} - options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -241,6 +256,8 @@ func handleTenantsList(ctx context.Context, cmd *cli.Command) error { return err } + params := courier.TenantListParams{} + var res []byte options = append(options, option.WithResponseBodyInto(&res)) _, err = client.Tenants.List(ctx, params, options...) @@ -250,8 +267,15 @@ func handleTenantsList(ctx context.Context, cmd *cli.Command) error { obj := gjson.ParseBytes(res) format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") - return ShowJSON(os.Stdout, "tenants list", obj, format, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "tenants list", + Transform: transform, + }) } func handleTenantsDelete(ctx context.Context, cmd *cli.Command) error { @@ -290,8 +314,6 @@ func handleTenantsListUsers(ctx context.Context, cmd *cli.Command) error { return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := courier.TenantListUsersParams{} - options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -303,6 +325,8 @@ func handleTenantsListUsers(ctx context.Context, cmd *cli.Command) error { return err } + params := courier.TenantListUsersParams{} + var res []byte options = append(options, option.WithResponseBodyInto(&res)) _, err = client.Tenants.ListUsers( @@ -317,6 +341,13 @@ func handleTenantsListUsers(ctx context.Context, cmd *cli.Command) error { obj := gjson.ParseBytes(res) format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") - return ShowJSON(os.Stdout, "tenants list-users", obj, format, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "tenants list-users", + Transform: transform, + }) } diff --git a/pkg/cmd/tenantpreferenceitem.go b/pkg/cmd/tenantpreferenceitem.go index c718f64..1be7117 100644 --- a/pkg/cmd/tenantpreferenceitem.go +++ b/pkg/cmd/tenantpreferenceitem.go @@ -18,12 +18,14 @@ var tenantsPreferencesItemsUpdate = cli.Command{ Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "tenant-id", - Required: true, + Name: "tenant-id", + Required: true, + PathParam: "tenant_id", }, &requestflag.Flag[string]{ - Name: "topic-id", - Required: true, + Name: "topic-id", + Required: true, + PathParam: "topic_id", }, &requestflag.Flag[string]{ Name: "status", @@ -36,7 +38,7 @@ var tenantsPreferencesItemsUpdate = cli.Command{ Usage: "The default channels to send to this tenant when has_custom_routing is enabled", BodyPath: "custom_routing", }, - &requestflag.Flag[any]{ + &requestflag.Flag[*bool]{ Name: "has-custom-routing", Usage: "Override channel routing with custom preferences. This will override any template preferences that are set, but a user can still customize their preferences", BodyPath: "has_custom_routing", @@ -52,12 +54,14 @@ var tenantsPreferencesItemsDelete = cli.Command{ Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "tenant-id", - Required: true, + Name: "tenant-id", + Required: true, + PathParam: "tenant_id", }, &requestflag.Flag[string]{ - Name: "topic-id", - Required: true, + Name: "topic-id", + Required: true, + PathParam: "topic_id", }, }, Action: handleTenantsPreferencesItemsDelete, @@ -75,10 +79,6 @@ func handleTenantsPreferencesItemsUpdate(ctx context.Context, cmd *cli.Command) return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := courier.TenantPreferenceItemUpdateParams{ - TenantID: cmd.Value("tenant-id").(string), - } - options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -90,6 +90,10 @@ func handleTenantsPreferencesItemsUpdate(ctx context.Context, cmd *cli.Command) return err } + params := courier.TenantPreferenceItemUpdateParams{ + TenantID: cmd.Value("tenant-id").(string), + } + return client.Tenants.Preferences.Items.Update( ctx, cmd.Value("topic-id").(string), @@ -109,10 +113,6 @@ func handleTenantsPreferencesItemsDelete(ctx context.Context, cmd *cli.Command) return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := courier.TenantPreferenceItemDeleteParams{ - TenantID: cmd.Value("tenant-id").(string), - } - options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -124,6 +124,10 @@ func handleTenantsPreferencesItemsDelete(ctx context.Context, cmd *cli.Command) return err } + params := courier.TenantPreferenceItemDeleteParams{ + TenantID: cmd.Value("tenant-id").(string), + } + return client.Tenants.Preferences.Items.Delete( ctx, cmd.Value("topic-id").(string), diff --git a/pkg/cmd/tenanttemplate.go b/pkg/cmd/tenanttemplate.go index b5bbee3..0234351 100644 --- a/pkg/cmd/tenanttemplate.go +++ b/pkg/cmd/tenanttemplate.go @@ -5,7 +5,6 @@ package cmd import ( "context" "fmt" - "os" "github.com/tidwall/gjson" "github.com/trycourier/courier-cli/v3/internal/apiquery" @@ -21,12 +20,14 @@ var tenantsTemplatesRetrieve = cli.Command{ Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "tenant-id", - Required: true, + Name: "tenant-id", + Required: true, + PathParam: "tenant_id", }, &requestflag.Flag[string]{ - Name: "template-id", - Required: true, + Name: "template-id", + Required: true, + PathParam: "template_id", }, }, Action: handleTenantsTemplatesRetrieve, @@ -39,15 +40,16 @@ var tenantsTemplatesList = cli.Command{ Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "tenant-id", - Required: true, + Name: "tenant-id", + Required: true, + PathParam: "tenant_id", }, - &requestflag.Flag[any]{ + &requestflag.Flag[*string]{ Name: "cursor", Usage: "Continue the pagination with the next cursor", QueryPath: "cursor", }, - &requestflag.Flag[any]{ + &requestflag.Flag[*int64]{ Name: "limit", Usage: "The number of templates to return (defaults to 20, maximum value of 100)", QueryPath: "limit", @@ -57,18 +59,40 @@ var tenantsTemplatesList = cli.Command{ HideHelpCommand: true, } +var tenantsTemplatesDelete = cli.Command{ + Name: "delete", + Usage: "Deletes the tenant's notification template with the given `template_id`.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "tenant-id", + Required: true, + PathParam: "tenant_id", + }, + &requestflag.Flag[string]{ + Name: "template-id", + Required: true, + PathParam: "template_id", + }, + }, + Action: handleTenantsTemplatesDelete, + HideHelpCommand: true, +} + var tenantsTemplatesPublish = cli.Command{ Name: "publish", Usage: "Publishes a specific version of a notification template for a tenant.", Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "tenant-id", - Required: true, + Name: "tenant-id", + Required: true, + PathParam: "tenant_id", }, &requestflag.Flag[string]{ - Name: "template-id", - Required: true, + Name: "template-id", + Required: true, + PathParam: "template_id", }, &requestflag.Flag[string]{ Name: "version", @@ -87,12 +111,14 @@ var tenantsTemplatesReplace = requestflag.WithInnerFlags(cli.Command{ Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "tenant-id", - Required: true, + Name: "tenant-id", + Required: true, + PathParam: "tenant_id", }, &requestflag.Flag[string]{ - Name: "template-id", - Required: true, + Name: "template-id", + Required: true, + PathParam: "template_id", }, &requestflag.Flag[map[string]any]{ Name: "template", @@ -141,10 +167,6 @@ func handleTenantsTemplatesRetrieve(ctx context.Context, cmd *cli.Command) error return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := courier.TenantTemplateGetParams{ - TenantID: cmd.Value("tenant-id").(string), - } - options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -156,6 +178,10 @@ func handleTenantsTemplatesRetrieve(ctx context.Context, cmd *cli.Command) error return err } + params := courier.TenantTemplateGetParams{ + TenantID: cmd.Value("tenant-id").(string), + } + var res []byte options = append(options, option.WithResponseBodyInto(&res)) _, err = client.Tenants.Templates.Get( @@ -170,8 +196,15 @@ func handleTenantsTemplatesRetrieve(ctx context.Context, cmd *cli.Command) error obj := gjson.ParseBytes(res) format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") - return ShowJSON(os.Stdout, "tenants:templates retrieve", obj, format, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "tenants:templates retrieve", + Transform: transform, + }) } func handleTenantsTemplatesList(ctx context.Context, cmd *cli.Command) error { @@ -185,8 +218,6 @@ func handleTenantsTemplatesList(ctx context.Context, cmd *cli.Command) error { return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := courier.TenantTemplateListParams{} - options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -198,6 +229,8 @@ func handleTenantsTemplatesList(ctx context.Context, cmd *cli.Command) error { return err } + params := courier.TenantTemplateListParams{} + var res []byte options = append(options, option.WithResponseBodyInto(&res)) _, err = client.Tenants.Templates.List( @@ -212,11 +245,18 @@ func handleTenantsTemplatesList(ctx context.Context, cmd *cli.Command) error { obj := gjson.ParseBytes(res) format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") - return ShowJSON(os.Stdout, "tenants:templates list", obj, format, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "tenants:templates list", + Transform: transform, + }) } -func handleTenantsTemplatesPublish(ctx context.Context, cmd *cli.Command) error { +func handleTenantsTemplatesDelete(ctx context.Context, cmd *cli.Command) error { client := courier.NewClient(getDefaultRequestOptions(cmd)...) unusedArgs := cmd.Args().Slice() if !cmd.IsSet("template-id") && len(unusedArgs) > 0 { @@ -227,10 +267,40 @@ func handleTenantsTemplatesPublish(ctx context.Context, cmd *cli.Command) error return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := courier.TenantTemplatePublishParams{ + options, err := flagOptions( + cmd, + apiquery.NestedQueryFormatBrackets, + apiquery.ArrayQueryFormatComma, + EmptyBody, + false, + ) + if err != nil { + return err + } + + params := courier.TenantTemplateDeleteParams{ TenantID: cmd.Value("tenant-id").(string), } + return client.Tenants.Templates.Delete( + ctx, + cmd.Value("template-id").(string), + params, + options..., + ) +} + +func handleTenantsTemplatesPublish(ctx context.Context, cmd *cli.Command) error { + client := courier.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("template-id") && len(unusedArgs) > 0 { + cmd.Set("template-id", unusedArgs[0]) + unusedArgs = unusedArgs[1:] + } + if len(unusedArgs) > 0 { + return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) + } + options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -242,6 +312,10 @@ func handleTenantsTemplatesPublish(ctx context.Context, cmd *cli.Command) error return err } + params := courier.TenantTemplatePublishParams{ + TenantID: cmd.Value("tenant-id").(string), + } + var res []byte options = append(options, option.WithResponseBodyInto(&res)) _, err = client.Tenants.Templates.Publish( @@ -256,8 +330,15 @@ func handleTenantsTemplatesPublish(ctx context.Context, cmd *cli.Command) error obj := gjson.ParseBytes(res) format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") - return ShowJSON(os.Stdout, "tenants:templates publish", obj, format, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "tenants:templates publish", + Transform: transform, + }) } func handleTenantsTemplatesReplace(ctx context.Context, cmd *cli.Command) error { @@ -271,10 +352,6 @@ func handleTenantsTemplatesReplace(ctx context.Context, cmd *cli.Command) error return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := courier.TenantTemplateReplaceParams{ - TenantID: cmd.Value("tenant-id").(string), - } - options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -286,6 +363,10 @@ func handleTenantsTemplatesReplace(ctx context.Context, cmd *cli.Command) error return err } + params := courier.TenantTemplateReplaceParams{ + TenantID: cmd.Value("tenant-id").(string), + } + var res []byte options = append(options, option.WithResponseBodyInto(&res)) _, err = client.Tenants.Templates.Replace( @@ -300,6 +381,13 @@ func handleTenantsTemplatesReplace(ctx context.Context, cmd *cli.Command) error obj := gjson.ParseBytes(res) format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") - return ShowJSON(os.Stdout, "tenants:templates replace", obj, format, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "tenants:templates replace", + Transform: transform, + }) } diff --git a/pkg/cmd/tenanttemplate_test.go b/pkg/cmd/tenanttemplate_test.go index 22c58df..f580be6 100644 --- a/pkg/cmd/tenanttemplate_test.go +++ b/pkg/cmd/tenanttemplate_test.go @@ -36,6 +36,19 @@ func TestTenantsTemplatesList(t *testing.T) { }) } +func TestTenantsTemplatesDelete(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "tenants:templates", "delete", + "--tenant-id", "tenant_id", + "--template-id", "template_id", + ) + }) +} + func TestTenantsTemplatesPublish(t *testing.T) { t.Skip("Mock server tests are disabled") t.Run("regular flags", func(t *testing.T) { diff --git a/pkg/cmd/tenanttemplateversion.go b/pkg/cmd/tenanttemplateversion.go index 0f50a89..2002b98 100644 --- a/pkg/cmd/tenanttemplateversion.go +++ b/pkg/cmd/tenanttemplateversion.go @@ -5,7 +5,6 @@ package cmd import ( "context" "fmt" - "os" "github.com/tidwall/gjson" "github.com/trycourier/courier-cli/v3/internal/apiquery" @@ -21,16 +20,19 @@ var tenantsTemplatesVersionsRetrieve = cli.Command{ Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "tenant-id", - Required: true, + Name: "tenant-id", + Required: true, + PathParam: "tenant_id", }, &requestflag.Flag[string]{ - Name: "template-id", - Required: true, + Name: "template-id", + Required: true, + PathParam: "template_id", }, &requestflag.Flag[string]{ - Name: "version", - Required: true, + Name: "version", + Required: true, + PathParam: "version", }, }, Action: handleTenantsTemplatesVersionsRetrieve, @@ -48,11 +50,6 @@ func handleTenantsTemplatesVersionsRetrieve(ctx context.Context, cmd *cli.Comman return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := courier.TenantTemplateVersionGetParams{ - TenantID: cmd.Value("tenant-id").(string), - TemplateID: cmd.Value("template-id").(string), - } - options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -64,6 +61,11 @@ func handleTenantsTemplatesVersionsRetrieve(ctx context.Context, cmd *cli.Comman return err } + params := courier.TenantTemplateVersionGetParams{ + TenantID: cmd.Value("tenant-id").(string), + TemplateID: cmd.Value("template-id").(string), + } + var res []byte options = append(options, option.WithResponseBodyInto(&res)) _, err = client.Tenants.Templates.Versions.Get( @@ -78,6 +80,13 @@ func handleTenantsTemplatesVersionsRetrieve(ctx context.Context, cmd *cli.Comman obj := gjson.ParseBytes(res) format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") - return ShowJSON(os.Stdout, "tenants:templates:versions retrieve", obj, format, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "tenants:templates:versions retrieve", + Transform: transform, + }) } diff --git a/pkg/cmd/translation.go b/pkg/cmd/translation.go index 86fa59e..eda1899 100644 --- a/pkg/cmd/translation.go +++ b/pkg/cmd/translation.go @@ -5,7 +5,6 @@ package cmd import ( "context" "fmt" - "os" "github.com/tidwall/gjson" "github.com/trycourier/courier-cli/v3/internal/apiquery" @@ -21,12 +20,14 @@ var translationsRetrieve = cli.Command{ Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "domain", - Required: true, + Name: "domain", + Required: true, + PathParam: "domain", }, &requestflag.Flag[string]{ - Name: "locale", - Required: true, + Name: "locale", + Required: true, + PathParam: "locale", }, }, Action: handleTranslationsRetrieve, @@ -39,12 +40,14 @@ var translationsUpdate = cli.Command{ Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "domain", - Required: true, + Name: "domain", + Required: true, + PathParam: "domain", }, &requestflag.Flag[string]{ - Name: "locale", - Required: true, + Name: "locale", + Required: true, + PathParam: "locale", }, &requestflag.Flag[string]{ Name: "body", @@ -67,10 +70,6 @@ func handleTranslationsRetrieve(ctx context.Context, cmd *cli.Command) error { return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := courier.TranslationGetParams{ - Domain: cmd.Value("domain").(string), - } - options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -82,6 +81,10 @@ func handleTranslationsRetrieve(ctx context.Context, cmd *cli.Command) error { return err } + params := courier.TranslationGetParams{ + Domain: cmd.Value("domain").(string), + } + var res []byte options = append(options, option.WithResponseBodyInto(&res)) _, err = client.Translations.Get( @@ -96,8 +99,15 @@ func handleTranslationsRetrieve(ctx context.Context, cmd *cli.Command) error { obj := gjson.ParseBytes(res) format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") - return ShowJSON(os.Stdout, "translations retrieve", obj, format, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "translations retrieve", + Transform: transform, + }) } func handleTranslationsUpdate(ctx context.Context, cmd *cli.Command) error { @@ -111,10 +121,6 @@ func handleTranslationsUpdate(ctx context.Context, cmd *cli.Command) error { return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := courier.TranslationUpdateParams{ - Domain: cmd.Value("domain").(string), - } - options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -126,6 +132,10 @@ func handleTranslationsUpdate(ctx context.Context, cmd *cli.Command) error { return err } + params := courier.TranslationUpdateParams{ + Domain: cmd.Value("domain").(string), + } + return client.Translations.Update( ctx, cmd.Value("locale").(string), diff --git a/pkg/cmd/userpreference.go b/pkg/cmd/userpreference.go index 121c8b8..9965737 100644 --- a/pkg/cmd/userpreference.go +++ b/pkg/cmd/userpreference.go @@ -5,7 +5,6 @@ package cmd import ( "context" "fmt" - "os" "github.com/tidwall/gjson" "github.com/trycourier/courier-cli/v3/internal/apiquery" @@ -21,10 +20,11 @@ var usersPreferencesRetrieve = cli.Command{ Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "user-id", - Required: true, + Name: "user-id", + Required: true, + PathParam: "user_id", }, - &requestflag.Flag[any]{ + &requestflag.Flag[*string]{ Name: "tenant-id", Usage: "Query the preferences of a user for this specific tenant context.", QueryPath: "tenant_id", @@ -40,14 +40,16 @@ var usersPreferencesRetrieveTopic = cli.Command{ Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "user-id", - Required: true, + Name: "user-id", + Required: true, + PathParam: "user_id", }, &requestflag.Flag[string]{ - Name: "topic-id", - Required: true, + Name: "topic-id", + Required: true, + PathParam: "topic_id", }, - &requestflag.Flag[any]{ + &requestflag.Flag[*string]{ Name: "tenant-id", Usage: "Query the preferences of a user for this specific tenant context.", QueryPath: "tenant_id", @@ -63,19 +65,21 @@ var usersPreferencesUpdateOrCreateTopic = requestflag.WithInnerFlags(cli.Command Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "user-id", - Required: true, + Name: "user-id", + Required: true, + PathParam: "user_id", }, &requestflag.Flag[string]{ - Name: "topic-id", - Required: true, + Name: "topic-id", + Required: true, + PathParam: "topic_id", }, &requestflag.Flag[map[string]any]{ Name: "topic", Required: true, BodyPath: "topic", }, - &requestflag.Flag[any]{ + &requestflag.Flag[*string]{ Name: "tenant-id", Usage: "Update the preferences of a user for this specific tenant context.", QueryPath: "tenant_id", @@ -95,7 +99,7 @@ var usersPreferencesUpdateOrCreateTopic = requestflag.WithInnerFlags(cli.Command Usage: "The Channels a user has chosen to receive notifications through for this topic", InnerField: "custom_routing", }, - &requestflag.InnerFlag[any]{ + &requestflag.InnerFlag[*bool]{ Name: "topic.has-custom-routing", InnerField: "has_custom_routing", }, @@ -113,8 +117,6 @@ func handleUsersPreferencesRetrieve(ctx context.Context, cmd *cli.Command) error return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := courier.UserPreferenceGetParams{} - options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -126,6 +128,8 @@ func handleUsersPreferencesRetrieve(ctx context.Context, cmd *cli.Command) error return err } + params := courier.UserPreferenceGetParams{} + var res []byte options = append(options, option.WithResponseBodyInto(&res)) _, err = client.Users.Preferences.Get( @@ -140,8 +144,15 @@ func handleUsersPreferencesRetrieve(ctx context.Context, cmd *cli.Command) error obj := gjson.ParseBytes(res) format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") - return ShowJSON(os.Stdout, "users:preferences retrieve", obj, format, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "users:preferences retrieve", + Transform: transform, + }) } func handleUsersPreferencesRetrieveTopic(ctx context.Context, cmd *cli.Command) error { @@ -155,10 +166,6 @@ func handleUsersPreferencesRetrieveTopic(ctx context.Context, cmd *cli.Command) return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := courier.UserPreferenceGetTopicParams{ - UserID: cmd.Value("user-id").(string), - } - options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -170,6 +177,10 @@ func handleUsersPreferencesRetrieveTopic(ctx context.Context, cmd *cli.Command) return err } + params := courier.UserPreferenceGetTopicParams{ + UserID: cmd.Value("user-id").(string), + } + var res []byte options = append(options, option.WithResponseBodyInto(&res)) _, err = client.Users.Preferences.GetTopic( @@ -184,8 +195,15 @@ func handleUsersPreferencesRetrieveTopic(ctx context.Context, cmd *cli.Command) obj := gjson.ParseBytes(res) format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") - return ShowJSON(os.Stdout, "users:preferences retrieve-topic", obj, format, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "users:preferences retrieve-topic", + Transform: transform, + }) } func handleUsersPreferencesUpdateOrCreateTopic(ctx context.Context, cmd *cli.Command) error { @@ -199,10 +217,6 @@ func handleUsersPreferencesUpdateOrCreateTopic(ctx context.Context, cmd *cli.Com return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := courier.UserPreferenceUpdateOrNewTopicParams{ - UserID: cmd.Value("user-id").(string), - } - options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -214,6 +228,10 @@ func handleUsersPreferencesUpdateOrCreateTopic(ctx context.Context, cmd *cli.Com return err } + params := courier.UserPreferenceUpdateOrNewTopicParams{ + UserID: cmd.Value("user-id").(string), + } + var res []byte options = append(options, option.WithResponseBodyInto(&res)) _, err = client.Users.Preferences.UpdateOrNewTopic( @@ -228,6 +246,13 @@ func handleUsersPreferencesUpdateOrCreateTopic(ctx context.Context, cmd *cli.Com obj := gjson.ParseBytes(res) format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") - return ShowJSON(os.Stdout, "users:preferences update-or-create-topic", obj, format, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "users:preferences update-or-create-topic", + Transform: transform, + }) } diff --git a/pkg/cmd/usertenant.go b/pkg/cmd/usertenant.go index 2d2f153..b4c8ee6 100644 --- a/pkg/cmd/usertenant.go +++ b/pkg/cmd/usertenant.go @@ -5,7 +5,6 @@ package cmd import ( "context" "fmt" - "os" "github.com/tidwall/gjson" "github.com/trycourier/courier-cli/v3/internal/apiquery" @@ -21,15 +20,16 @@ var usersTenantsList = cli.Command{ Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "user-id", - Required: true, + Name: "user-id", + Required: true, + PathParam: "user_id", }, - &requestflag.Flag[any]{ + &requestflag.Flag[*string]{ Name: "cursor", Usage: "Continue the pagination with the next cursor", QueryPath: "cursor", }, - &requestflag.Flag[any]{ + &requestflag.Flag[*int64]{ Name: "limit", Usage: "The number of accounts to return \n(defaults to 20, maximum value of 100)", QueryPath: "limit", @@ -45,8 +45,9 @@ var usersTenantsAddMultiple = requestflag.WithInnerFlags(cli.Command{ Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "user-id", - Required: true, + Name: "user-id", + Required: true, + PathParam: "user_id", }, &requestflag.Flag[[]map[string]any]{ Name: "tenant", @@ -68,12 +69,12 @@ var usersTenantsAddMultiple = requestflag.WithInnerFlags(cli.Command{ Usage: "Additional metadata to be applied to a user profile when used in a tenant context", InnerField: "profile", }, - &requestflag.InnerFlag[any]{ + &requestflag.InnerFlag[*string]{ Name: "tenant.type", Usage: `Allowed values: "user".`, InnerField: "type", }, - &requestflag.InnerFlag[any]{ + &requestflag.InnerFlag[*string]{ Name: "tenant.user-id", Usage: "User ID for the association between tenant and user", InnerField: "user_id", @@ -87,12 +88,14 @@ var usersTenantsAddSingle = cli.Command{ Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "user-id", - Required: true, + Name: "user-id", + Required: true, + PathParam: "user_id", }, &requestflag.Flag[string]{ - Name: "tenant-id", - Required: true, + Name: "tenant-id", + Required: true, + PathParam: "tenant_id", }, &requestflag.Flag[map[string]any]{ Name: "profile", @@ -109,8 +112,9 @@ var usersTenantsRemoveAll = cli.Command{ Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "user-id", - Required: true, + Name: "user-id", + Required: true, + PathParam: "user_id", }, }, Action: handleUsersTenantsRemoveAll, @@ -123,12 +127,14 @@ var usersTenantsRemoveSingle = cli.Command{ Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "user-id", - Required: true, + Name: "user-id", + Required: true, + PathParam: "user_id", }, &requestflag.Flag[string]{ - Name: "tenant-id", - Required: true, + Name: "tenant-id", + Required: true, + PathParam: "tenant_id", }, }, Action: handleUsersTenantsRemoveSingle, @@ -146,8 +152,6 @@ func handleUsersTenantsList(ctx context.Context, cmd *cli.Command) error { return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := courier.UserTenantListParams{} - options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -159,6 +163,8 @@ func handleUsersTenantsList(ctx context.Context, cmd *cli.Command) error { return err } + params := courier.UserTenantListParams{} + var res []byte options = append(options, option.WithResponseBodyInto(&res)) _, err = client.Users.Tenants.List( @@ -173,8 +179,15 @@ func handleUsersTenantsList(ctx context.Context, cmd *cli.Command) error { obj := gjson.ParseBytes(res) format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") - return ShowJSON(os.Stdout, "users:tenants list", obj, format, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "users:tenants list", + Transform: transform, + }) } func handleUsersTenantsAddMultiple(ctx context.Context, cmd *cli.Command) error { @@ -188,8 +201,6 @@ func handleUsersTenantsAddMultiple(ctx context.Context, cmd *cli.Command) error return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := courier.UserTenantAddMultipleParams{} - options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -201,6 +212,8 @@ func handleUsersTenantsAddMultiple(ctx context.Context, cmd *cli.Command) error return err } + params := courier.UserTenantAddMultipleParams{} + return client.Users.Tenants.AddMultiple( ctx, cmd.Value("user-id").(string), @@ -220,10 +233,6 @@ func handleUsersTenantsAddSingle(ctx context.Context, cmd *cli.Command) error { return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := courier.UserTenantAddSingleParams{ - UserID: cmd.Value("user-id").(string), - } - options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -235,6 +244,10 @@ func handleUsersTenantsAddSingle(ctx context.Context, cmd *cli.Command) error { return err } + params := courier.UserTenantAddSingleParams{ + UserID: cmd.Value("user-id").(string), + } + return client.Users.Tenants.AddSingle( ctx, cmd.Value("tenant-id").(string), @@ -279,10 +292,6 @@ func handleUsersTenantsRemoveSingle(ctx context.Context, cmd *cli.Command) error return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := courier.UserTenantRemoveSingleParams{ - UserID: cmd.Value("user-id").(string), - } - options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -294,6 +303,10 @@ func handleUsersTenantsRemoveSingle(ctx context.Context, cmd *cli.Command) error return err } + params := courier.UserTenantRemoveSingleParams{ + UserID: cmd.Value("user-id").(string), + } + return client.Users.Tenants.RemoveSingle( ctx, cmd.Value("tenant-id").(string), diff --git a/pkg/cmd/usertoken.go b/pkg/cmd/usertoken.go index 877f05e..3ac0d84 100644 --- a/pkg/cmd/usertoken.go +++ b/pkg/cmd/usertoken.go @@ -5,7 +5,6 @@ package cmd import ( "context" "fmt" - "os" "github.com/tidwall/gjson" "github.com/trycourier/courier-cli/v3/internal/apiquery" @@ -21,12 +20,14 @@ var usersTokensRetrieve = cli.Command{ Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "user-id", - Required: true, + Name: "user-id", + Required: true, + PathParam: "user_id", }, &requestflag.Flag[string]{ - Name: "token", - Required: true, + Name: "token", + Required: true, + PathParam: "token", }, }, Action: handleUsersTokensRetrieve, @@ -39,12 +40,14 @@ var usersTokensUpdate = requestflag.WithInnerFlags(cli.Command{ Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "user-id", - Required: true, + Name: "user-id", + Required: true, + PathParam: "user_id", }, &requestflag.Flag[string]{ - Name: "token", - Required: true, + Name: "token", + Required: true, + PathParam: "token", }, &requestflag.Flag[[]map[string]any]{ Name: "patch", @@ -66,7 +69,7 @@ var usersTokensUpdate = requestflag.WithInnerFlags(cli.Command{ Usage: "The JSON path specifying the part of the profile to operate on.", InnerField: "path", }, - &requestflag.InnerFlag[any]{ + &requestflag.InnerFlag[*string]{ Name: "patch.value", Usage: "The value for the operation.", InnerField: "value", @@ -80,8 +83,9 @@ var usersTokensList = cli.Command{ Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "user-id", - Required: true, + Name: "user-id", + Required: true, + PathParam: "user_id", }, }, Action: handleUsersTokensList, @@ -94,12 +98,14 @@ var usersTokensDelete = cli.Command{ Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "user-id", - Required: true, + Name: "user-id", + Required: true, + PathParam: "user_id", }, &requestflag.Flag[string]{ - Name: "token", - Required: true, + Name: "token", + Required: true, + PathParam: "token", }, }, Action: handleUsersTokensDelete, @@ -112,8 +118,9 @@ var usersTokensAddMultiple = cli.Command{ Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "user-id", - Required: true, + Name: "user-id", + Required: true, + PathParam: "user_id", }, }, Action: handleUsersTokensAddMultiple, @@ -126,12 +133,14 @@ var usersTokensAddSingle = requestflag.WithInnerFlags(cli.Command{ Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "user-id", - Required: true, + Name: "user-id", + Required: true, + PathParam: "user_id", }, &requestflag.Flag[string]{ - Name: "token", - Required: true, + Name: "token", + Required: true, + PathParam: "token", }, &requestflag.Flag[string]{ Name: "provider-key", @@ -164,54 +173,54 @@ var usersTokensAddSingle = requestflag.WithInnerFlags(cli.Command{ HideHelpCommand: true, }, map[string][]requestflag.HasOuterFlag{ "device": { - &requestflag.InnerFlag[any]{ + &requestflag.InnerFlag[*string]{ Name: "device.ad-id", Usage: "Id of the advertising identifier", InnerField: "ad_id", }, - &requestflag.InnerFlag[any]{ + &requestflag.InnerFlag[*string]{ Name: "device.app-id", Usage: "Id of the application the token is used for", InnerField: "app_id", }, - &requestflag.InnerFlag[any]{ + &requestflag.InnerFlag[*string]{ Name: "device.device-id", Usage: "Id of the device the token is associated with", InnerField: "device_id", }, - &requestflag.InnerFlag[any]{ + &requestflag.InnerFlag[*string]{ Name: "device.manufacturer", Usage: "The device manufacturer", InnerField: "manufacturer", }, - &requestflag.InnerFlag[any]{ + &requestflag.InnerFlag[*string]{ Name: "device.model", Usage: "The device model", InnerField: "model", }, - &requestflag.InnerFlag[any]{ + &requestflag.InnerFlag[*string]{ Name: "device.platform", Usage: "The device platform i.e. android, ios, web", InnerField: "platform", }, }, "tracking": { - &requestflag.InnerFlag[any]{ + &requestflag.InnerFlag[*string]{ Name: "tracking.ip", Usage: "The IP address of the device", InnerField: "ip", }, - &requestflag.InnerFlag[any]{ + &requestflag.InnerFlag[*string]{ Name: "tracking.lat", Usage: "The latitude of the device", InnerField: "lat", }, - &requestflag.InnerFlag[any]{ + &requestflag.InnerFlag[*string]{ Name: "tracking.long", Usage: "The longitude of the device", InnerField: "long", }, - &requestflag.InnerFlag[any]{ + &requestflag.InnerFlag[*string]{ Name: "tracking.os-version", Usage: "The operating system version", InnerField: "os_version", @@ -230,10 +239,6 @@ func handleUsersTokensRetrieve(ctx context.Context, cmd *cli.Command) error { return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := courier.UserTokenGetParams{ - UserID: cmd.Value("user-id").(string), - } - options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -245,6 +250,10 @@ func handleUsersTokensRetrieve(ctx context.Context, cmd *cli.Command) error { return err } + params := courier.UserTokenGetParams{ + UserID: cmd.Value("user-id").(string), + } + var res []byte options = append(options, option.WithResponseBodyInto(&res)) _, err = client.Users.Tokens.Get( @@ -259,8 +268,15 @@ func handleUsersTokensRetrieve(ctx context.Context, cmd *cli.Command) error { obj := gjson.ParseBytes(res) format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") - return ShowJSON(os.Stdout, "users:tokens retrieve", obj, format, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "users:tokens retrieve", + Transform: transform, + }) } func handleUsersTokensUpdate(ctx context.Context, cmd *cli.Command) error { @@ -274,10 +290,6 @@ func handleUsersTokensUpdate(ctx context.Context, cmd *cli.Command) error { return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := courier.UserTokenUpdateParams{ - UserID: cmd.Value("user-id").(string), - } - options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -289,6 +301,10 @@ func handleUsersTokensUpdate(ctx context.Context, cmd *cli.Command) error { return err } + params := courier.UserTokenUpdateParams{ + UserID: cmd.Value("user-id").(string), + } + return client.Users.Tokens.Update( ctx, cmd.Value("token").(string), @@ -328,8 +344,15 @@ func handleUsersTokensList(ctx context.Context, cmd *cli.Command) error { obj := gjson.ParseBytes(res) format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") - return ShowJSON(os.Stdout, "users:tokens list", obj, format, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "users:tokens list", + Transform: transform, + }) } func handleUsersTokensDelete(ctx context.Context, cmd *cli.Command) error { @@ -343,10 +366,6 @@ func handleUsersTokensDelete(ctx context.Context, cmd *cli.Command) error { return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := courier.UserTokenDeleteParams{ - UserID: cmd.Value("user-id").(string), - } - options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -358,6 +377,10 @@ func handleUsersTokensDelete(ctx context.Context, cmd *cli.Command) error { return err } + params := courier.UserTokenDeleteParams{ + UserID: cmd.Value("user-id").(string), + } + return client.Users.Tokens.Delete( ctx, cmd.Value("token").(string), @@ -402,10 +425,6 @@ func handleUsersTokensAddSingle(ctx context.Context, cmd *cli.Command) error { return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := courier.UserTokenAddSingleParams{ - UserID: cmd.Value("user-id").(string), - } - options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -417,6 +436,10 @@ func handleUsersTokensAddSingle(ctx context.Context, cmd *cli.Command) error { return err } + params := courier.UserTokenAddSingleParams{ + UserID: cmd.Value("user-id").(string), + } + return client.Users.Tokens.AddSingle( ctx, cmd.Value("token").(string), diff --git a/pkg/cmd/version.go b/pkg/cmd/version.go index a648943..1b99840 100644 --- a/pkg/cmd/version.go +++ b/pkg/cmd/version.go @@ -2,4 +2,4 @@ package cmd -const Version = "3.4.2" // x-release-please-version +const Version = "3.5.0" // x-release-please-version diff --git a/scripts/bootstrap b/scripts/bootstrap index 9ebb7d3..bbc786d 100755 --- a/scripts/bootstrap +++ b/scripts/bootstrap @@ -4,7 +4,7 @@ set -e cd "$(dirname "$0")/.." -if [ -f "Brewfile" ] && [ "$(uname -s)" = "Darwin" ] && [ "$SKIP_BREW" != "1" ] && [ -t 0 ]; then +if [ -f "Brewfile" ] && [ "$(uname -s)" = "Darwin" ] && [ "${SKIP_BREW:-}" != "1" ] && [ -t 0 ]; then brew bundle check >/dev/null 2>&1 || { echo -n "==> Install Homebrew dependencies? (y/N): " read -r response