diff --git a/.changesets/1790922409-830b3652.yaml b/.changesets/1790922409-830b3652.yaml new file mode 100644 index 00000000..e4cef780 --- /dev/null +++ b/.changesets/1790922409-830b3652.yaml @@ -0,0 +1,10 @@ +id: 1790922409-830b3652 +features: + - examples +targets: + - cli +type: fix +bump: patch +description: preserve CLI examples for empty and optional request bodies, omit redundant empty JSON/form body flags, and retain required body validation +author: TristanSpeakEasy +date: "2026-10-02" diff --git a/pkg/generate/snapshots/cli_empty_body_go_test.go b/pkg/generate/snapshots/cli_empty_body_go_test.go new file mode 100644 index 00000000..f6cf0e22 --- /dev/null +++ b/pkg/generate/snapshots/cli_empty_body_go_test.go @@ -0,0 +1,269 @@ +package snapshots + +import ( + "fmt" + "os" + "path/filepath" + "regexp" + "testing" + + "github.com/speakeasy-api/openapi-generation/v2/pkg/generate/snapshots/snaptest" + "github.com/stretchr/testify/require" +) + +const cliEmptyBodySpec = `openapi: 3.0.3 +info: + title: Widget API + version: 1.0.0 +servers: + - url: https://api.example.com +x-test-operation: &mixedOperation + description: An optional empty object body accompanies a required path parameter. + tags: [widgets] + security: [] + parameters: + - name: id + in: path + required: true + example: widget_123 + schema: + type: string + requestBody: &emptyBody + content: + application/json: + schema: + $ref: '#/components/schemas/EmptyBody' + responses: &responses + '200': + description: Accepted +paths: + /widgets/{id}/optional: + post: + <<: *mixedOperation + operationId: optional + /widgets/{id}/optionalexample: + post: + <<: *mixedOperation + operationId: optionalexample + description: An explicit empty object example does not require an optional body. + requestBody: + content: + application/json: + example: {} + schema: + $ref: '#/components/schemas/EmptyBody' + /widgets/{id}/required: + post: + <<: *mixedOperation + operationId: required + description: A required empty object body accepts an empty object without invented properties. + requestBody: + <<: *emptyBody + required: true + /widgets/standaloneoptional: + post: + <<: *mixedOperation + operationId: standaloneoptional + description: An optional empty object body is the only operation input. + parameters: [] + /widgets/standalonerequired: + post: + <<: *mixedOperation + operationId: standalonerequired + description: A required empty object body is the only operation input. + parameters: [] + requestBody: + <<: *emptyBody + required: true + /widgets/{id}/formoptional: + post: + <<: *mixedOperation + operationId: formoptional + description: An optional empty form object accompanies a required path parameter. + requestBody: &emptyFormBody + content: + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/EmptyBody' + /widgets/{id}/formrequired: + post: + <<: *mixedOperation + operationId: formrequired + description: A required empty form object accepts an empty object without invented properties. + requestBody: + <<: *emptyFormBody + required: true + /widgets/{id}/scalaroptional: + post: + <<: *mixedOperation + operationId: scalaroptional + description: An optional scalar body without an object example does not need an invented object payload. + requestBody: + content: + application/json: + schema: + type: string + /widgets/{id}/nullablerequired: + post: + <<: *mixedOperation + operationId: nullablerequired + description: A nullable empty object body does not need an invented payload. + requestBody: + required: true + content: + application/json: + schema: + type: object + nullable: true + properties: {} + /widgets/{id}/nullableoptional: + post: + <<: *mixedOperation + operationId: nullableoptional + description: An optional nullable empty object body preserves the distinction between omission and null. + requestBody: + content: + application/json: + schema: + type: object + nullable: true + properties: {} + /widgets/standalonenullable: + post: + <<: *mixedOperation + operationId: standalonenullable + description: A nullable empty object body is the only operation input. + parameters: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + nullable: true + properties: {} + /widgets/{id}/maprequired: + post: + <<: *mixedOperation + operationId: maprequired + description: A map body retains its whole-body input flag. + requestBody: + required: true + content: + application/json: + schema: + type: object + additionalProperties: + type: string + /widgets/{id}/unionrequired: + post: + <<: *mixedOperation + operationId: unionrequired + description: A union body retains its whole-body input flag and explicit example. + requestBody: + required: true + content: + application/json: + example: + label: sample + schema: + oneOf: + - type: object + required: [label] + properties: + label: + type: string + - type: string + /widgets/parameter: + get: + operationId: parameter + description: An empty object query parameter remains a JSON input rather than a body wrapper. + tags: [widgets] + security: [] + parameters: + - name: filter + in: query + schema: + $ref: '#/components/schemas/EmptyBody' + responses: *responses +components: + schemas: + EmptyBody: + description: An object with no declared properties. + type: object + properties: {} +` + +func TestSnapCLIEmptyBodyExamplesAndFlags(t *testing.T) { + for _, style := range []string{"compact", "full"} { + t.Run(style, func(t *testing.T) { + shouldCompile := false + snaptest.DoTestSnapshot(t, snaptest.Options{ + Spec: cliEmptyBodySpec, + GenYaml: fmt.Sprintf(`cli: + packageName: github.com/example/widget-cli + cliName: widget-cli + envVarPrefix: WIDGET + helpStyle: %s +`, style), + ShouldCompile: &shouldCompile, + AfterGenerate: func(t *testing.T, outputDir string) { + t.Helper() + readCommand := func(name string) string { + t.Helper() + content, err := os.ReadFile(filepath.Join(outputDir, "internal", "cli", "widgets", name+".go")) + require.NoError(t, err) + return string(content) + } + examplePattern := regexp.MustCompile(`Example:\s*"([^"\n]*)"`) + for _, name := range []string{"optional", "optionalexample", "scalaroptional", "formoptional", "nullablerequired"} { + command := readCommand(name) + require.Regexp(t, examplePattern, command) + require.Equal(t, " widget-cli widgets "+name+" --id widget_123", examplePattern.FindStringSubmatch(command)[1]) + } + for _, name := range []string{"optional", "optionalexample", "required", "formoptional", "formrequired"} { + command := readCommand(name) + require.Contains(t, command, `cmd.Flags().String("body",`) + require.NotContains(t, command, `FlagName: "body-param"`) + } + usageSource, err := os.ReadFile(filepath.Join(outputDir, "internal", "usage", "schema.go")) + require.NoError(t, err) + usagePattern := regexp.MustCompile(`"widgets (optional|optionalexample|required|formoptional|formrequired)":\s*"(.*)"`) + usageSchemas := usagePattern.FindAllStringSubmatch(string(usageSource), -1) + require.Len(t, usageSchemas, 5) + for _, schema := range usageSchemas { + require.Contains(t, schema[2], "--body ") + require.NotContains(t, schema[2], "--body-param") + } + for _, name := range []string{"required", "formrequired"} { + command := readCommand(name) + require.Regexp(t, examplePattern, command) + require.Equal(t, " widget-cli widgets "+name+" --id widget_123 --body '{}'", examplePattern.FindStringSubmatch(command)[1]) + require.Contains(t, command, `PromptFlagSpec{Required: true, Kind: "json", BodyFlag: true}`) + } + standaloneOptional := readCommand("standaloneoptional") + require.Regexp(t, examplePattern, standaloneOptional) + require.Equal(t, " widget-cli widgets standaloneoptional", examplePattern.FindStringSubmatch(standaloneOptional)[1]) + standaloneNullable := readCommand("standalonenullable") + require.Regexp(t, examplePattern, standaloneNullable) + require.Equal(t, " widget-cli widgets standalonenullable", examplePattern.FindStringSubmatch(standaloneNullable)[1]) + require.NotContains(t, readCommand("optional"), "alternative to individual flags") + nullableOptional := readCommand("nullableoptional") + require.Contains(t, nullableOptional, "Kind: flagutil.FlagKindJSON") + require.Contains(t, nullableOptional, "alternative to individual flags") + standaloneRequired := readCommand("standalonerequired") + require.Contains(t, standaloneRequired, "--empty-body '{}'") + for _, command := range []string{standaloneOptional, standaloneRequired} { + require.Contains(t, command, `cmd.Flags().String("empty-body",`) + require.Contains(t, command, "flagutil.BuildRequestBody[") + } + require.Contains(t, readCommand("parameter"), `FlagName: "filter"`) + require.Contains(t, readCommand("parameter"), "Kind: flagutil.FlagKindJSON") + require.Contains(t, readCommand("maprequired"), `FlagName: "body-param"`) + require.Contains(t, readCommand("unionrequired"), "Kind: flagutil.FlagKindUnion") + require.Contains(t, readCommand("unionrequired"), `--body '{\"label\":\"sample\"}'`) + }, + }) + }) + } +} diff --git a/templates/templates/cli/README.md b/templates/templates/cli/README.md index 83a791f4..99fa3d06 100644 --- a/templates/templates/cli/README.md +++ b/templates/templates/cli/README.md @@ -1248,6 +1248,8 @@ Declared commands may add a nested `help:` map with the closed keys `defaults`, Compact operation examples suppress only generator-synthesized invocations containing its own angle-bracket value placeholders or fallback `{"key": "value"}` body. Spec examples, manifest examples, and hand-written `Example` text are never filtered. An intent with no authored example gets a bare invocation only when it has no unresolved required positional or declared flag. +In both help styles, `includes/descriptions.ts` omits invented payloads for optional or nullable, unexpanded request bodies without usable body examples, preserving examples for required parameters. Empty JSON/form request-body classes use `'{}'` when required rather than an object with invented properties. Mixed parameter/body operations with an empty JSON/form body register only the existing `--body` flag for that body, not a redundant `--body-param` metadata flag, unless the body uses a nullable/optional wrapper. Wrapped bodies retain their individual JSON flag and the `--body` help describes that alternative. Required empty bodies mark `--body` as required for prompting, and `BuildRequest` rejects omission or JSON `null` while accepting `'{}'` from the flag or stdin. The metadata, whole-body flag lookup, and `--usage` generators share the same field filter (`includes/metadata.ts`, `includes/usage.ts`). Body-only operations retain their whole-body flag, named from the type for shared/component models or from the request field for operation-specific models. Empty non-body classes, maps, unions, and nullable/optional wrappers retain their existing input flags. + The compact layout and `Just works:` heading are runtime help contracts. Cobra markdown generation keeps its existing `Examples` heading and operation-doc footer, and the explorer continues to read `Short`, `Long`, and `Example` without a heading rewrite. Because synthesized-operation filtering happens while `templateCmdExample` builds the command's `Example`, a rejected synthesized example is absent from docs and the explorer too; authored examples remain unchanged everywhere. Custom commands that currently append Defaults/Machine/Globals prose to `Example` should migrate that prose to the annotations above to avoid a duplicate footer, set `speakeasy_help_footer: "false"` while retaining their own footer, or choose `helpStyle: full` during migration. The KDL schema is built at generation time from the command tree in `includes/usage.ts` and emitted at runtime by `usage.EmitSchema()`. It includes command names, aliases, help text, flags, defaults, config metadata, and the config file location. Generated flags include the same auto-assigned short forms as the parser and `--help` (for example, `flag "-i --id "` when `--id` receives `-i`). Shorthand assignment still reserves inherited global letters and the pagination `-a`; route-dispatch intents expose only shorthands for the non-body flags they register. diff --git a/templates/templates/cli/auxiliary/internal/flagutil/metadata.go.stmpl b/templates/templates/cli/auxiliary/internal/flagutil/metadata.go.stmpl index 1cfc9221..205cc5d1 100644 --- a/templates/templates/cli/auxiliary/internal/flagutil/metadata.go.stmpl +++ b/templates/templates/cli/auxiliary/internal/flagutil/metadata.go.stmpl @@ -662,8 +662,16 @@ func BuildRequest[T any](cmd *cobra.Command, meta []FlagMeta, bodyFieldPath stri v := reflect.ValueOf(&req).Elem() bodyPrePopulated := false hasRequestBody := bodyFieldPath != "" || bodyFlagName != "" || !isJSONSerialized(v.Type()) + bodyRequired := false + if flag := cmd.Flags().Lookup(bodyFlagName); flag != nil { + values := flag.Annotations[AnnotationRequired] + bodyRequired = len(values) > 0 && values[0] == "true" + } decodeBody := func(data []byte, source string) error { + if bodyRequired && bytes.Equal(bytes.TrimSpace(data), []byte("null")) { + return WithCLIValidation(fmt.Errorf("invalid value for --%s: null; the body is required", bodyFlagName)) + } u := bodyUnionMeta(meta, bodyFieldPath) if bodyFieldPath != "" { bodyField, err := navigateToField(v, bodyFieldPath) @@ -723,6 +731,10 @@ func BuildRequest[T any](cmd *cobra.Command, meta []FlagMeta, bodyFieldPath stri } } + if bodyRequired && !bodyPrePopulated { + return nil, &MissingRequiredFlagError{FlagName: bodyFlagName, Detail: "(or provide via stdin)"} + } + // When body provided via --body flag or stdin, relax Required checks for body fields // so builders don't error for fields already populated if bodyPrePopulated { diff --git a/templates/templates/cli/includes/descriptions.ts b/templates/templates/cli/includes/descriptions.ts index 21e1f6e7..ed7a37c4 100644 --- a/templates/templates/cli/includes/descriptions.ts +++ b/templates/templates/cli/includes/descriptions.ts @@ -438,8 +438,12 @@ function templateCmdExample(op: Operation): string { Value: `--${flagName} '${JSON.stringify(ex.Value)}'`, SynthesizedPlaceholder: ex.Synthesized, })); - } else { - pushPart(`--${flagName} '{"key": "value"}'`, true); + } else if (!bodyField.Optional && !bodyField.Nullable) { + if (isEmptyRequestBodyClass(bodyField)) { + pushPart(`--${flagName} '{}'`); + } else { + pushPart(`--${flagName} '{"key": "value"}'`, true); + } } } } else if (op.Request.RequestBody) { @@ -478,8 +482,12 @@ function templateCmdExample(op: Operation): string { Value: `--${flagName} '${JSON.stringify(ex.Value)}'`, SynthesizedPlaceholder: ex.Synthesized, })); - } else { - pushPart(`--${flagName} '{"key": "value"}'`, true); + } else if (!bodyField.Optional && !bodyField.Nullable) { + if (isEmptyRequestBodyClass(bodyField)) { + pushPart(`--${flagName} '{}'`); + } else { + pushPart(`--${flagName} '{"key": "value"}'`, true); + } } } else if (bodyField && isMultipartMixedOp(op)) { // Multipart bodies have no whole-body JSON flag. Keep examples runnable diff --git a/templates/templates/cli/includes/metadata.ts b/templates/templates/cli/includes/metadata.ts index 45ed1c70..5a9d9ffa 100644 --- a/templates/templates/cli/includes/metadata.ts +++ b/templates/templates/cli/includes/metadata.ts @@ -48,6 +48,15 @@ function operationHasFlagMetadata(op: Operation): boolean { } registerTemplateFunc("operationHasFlagMetadata", operationHasFlagMetadata); +function getMixedRequestMetadataFields(op: Operation): FieldDef[] { + const fields = op.Request.Field.Type.Fields || []; + if (!templateHasBodyFlag(op)) return fields; + return fields.filter( + (field: FieldDef) => + !isEmptyRequestBodyClass(field) || isNullableOptionalWrapped(field), + ); +} + // buildOperationMetadataEntries constructs the FlagMeta entry strings for an // operation — the single source for both the emitted metadata var and any // validation that must mirror it (e.g. auto-shorthand collision checks). @@ -76,7 +85,7 @@ function buildOperationMetadataEntries( return null; // Complex JSON IsRequestBody uses BuildRequestBody, no metadata var } } else { - const fields = op.Request.Field.Type.Fields; + const fields = getMixedRequestMetadataFields(op); hadCandidateFields = fields.some((f: FieldDef) => !f.Const); collectMetadataFromFields(fields, "", "", entries); } @@ -774,12 +783,7 @@ function wholeBodyFlagName(op: Operation): string { const bodyFieldPath = getBodyFieldPath(op); if (!bodyFieldPath) return ""; const entries: string[] = []; - collectMetadataFromFields( - op.Request.Field.Type.Fields || [], - "", - "", - entries, - ); + collectMetadataFromFields(getMixedRequestMetadataFields(op), "", "", entries); // Entries are Go literals that always open with the top-level FlagName and // FieldPath (buildMetaEntryForField / the union entry); match that prefix so // nested variant fields cannot be mistaken for the body field. @@ -967,12 +971,28 @@ function templateHasBodyFlag(op: Operation): boolean { } registerTemplateFunc("templateHasBodyFlag", templateHasBodyFlag); +function templateEmptyBodyRequired(op: Operation): boolean { + if (!op.Request || op.Request.IsRequestBody || !templateHasBodyFlag(op)) { + return false; + } + return (op.Request.Field.Type.Fields || []).some( + (field: FieldDef) => + isEmptyRequestBodyClass(field) && !field.Optional && !field.Nullable, + ); +} +registerTemplateFunc("templateEmptyBodyRequired", templateEmptyBodyRequired); + /** * Generate the description for the --body flag. */ function templateBodyFlagDescription(op: Operation): string { - let description = - "Request body as JSON (alternative to individual flags). Can also be provided via stdin; @path reads a file, @- reads stdin to EOF."; + const hasEmptyBody = + op.Request?.RequestBody && + isEmptyRequestBodyClass(op.Request.RequestBody) && + !isNullableOptionalWrapped(op.Request.RequestBody); + let description = hasEmptyBody + ? "Request body as JSON. Can also be provided via stdin; @path reads a file, @- reads stdin to EOF." + : "Request body as JSON (alternative to individual flags). Can also be provided via stdin; @path reads a file, @- reads stdin to EOF."; if (hasBodySchemaForOp(op)) { description += " Use --schema to print the exact JSON Schema."; } diff --git a/templates/templates/cli/includes/test-workflows.ts b/templates/templates/cli/includes/test-workflows.ts index e0178262..0c607c3e 100644 --- a/templates/templates/cli/includes/test-workflows.ts +++ b/templates/templates/cli/includes/test-workflows.ts @@ -264,7 +264,10 @@ function templateCLIStepCode(usageContext: UsageContext): string { if (operation.Request.RequestBody) { bodyHandled = true; const reqBodyField = operation.Request.RequestBody; - const flagName = getRequestBodyFlagName(reqBodyField); + const flagName = + !operation.Request.IsRequestBody && templateHasBodyFlag(operation) + ? "body" + : getRequestBodyFlagName(reqBodyField); const bodyIsExpandable = shouldExpandNestedField(reqBodyField); const example = findExampleWithFallback( operation.Request.Examples ?? [], diff --git a/templates/templates/cli/includes/tests.ts b/templates/templates/cli/includes/tests.ts index e04941d5..71e1e71c 100644 --- a/templates/templates/cli/includes/tests.ts +++ b/templates/templates/cli/includes/tests.ts @@ -1132,6 +1132,28 @@ function templateCLIArgs(usageContext: UsageContext): string { continue; } + if ( + isEmptyRequestBodyClass(field) && + templateHasBodyFlag(operation) && + !isNullableOptionalWrapped(field) + ) { + if ( + bodyExamplePayload !== undefined || + (!field.Optional && !field.Nullable) + ) { + pushFlagArg( + args, + "body", + goStringLiteral( + JSON.stringify( + bodyExamplePayload === undefined ? {} : bodyExamplePayload, + ), + ), + ); + } + continue; + } + // Check if this is a body struct field that gets expanded into nested flags // (no param annotation + class type + expanded by CLI command via shouldExpandNestedField) // Also handle multipart body fields which are expanded via collectMultipartMetadata diff --git a/templates/templates/cli/includes/usage.ts b/templates/templates/cli/includes/usage.ts index 50ddb1af..7ae3c8e1 100644 --- a/templates/templates/cli/includes/usage.ts +++ b/templates/templates/cli/includes/usage.ts @@ -500,7 +500,7 @@ function getOperationBodyFieldUsageFlags(op: Operation): UsageFlagDef[] { walkFields(op.Request.RequestBody.Type.Fields || [], ""); } } else { - walkFields(op.Request.Field.Type.Fields || [], ""); + walkFields(getMixedRequestMetadataFields(op), ""); } return withAutoShorthands(op, flags, false); diff --git a/templates/templates/cli/includes/utils.ts b/templates/templates/cli/includes/utils.ts index 40afe16c..882bcee9 100644 --- a/templates/templates/cli/includes/utils.ts +++ b/templates/templates/cli/includes/utils.ts @@ -57,6 +57,21 @@ function getInputClassType(field: FieldDef): InputClassType { } registerTemplateFunc("getInputClassType", getInputClassType); +function isEmptyRequestBodyClass(field: FieldDef): boolean { + if ( + field.Type.Type.toString() !== "class" || + (field.Type.Fields || []).length !== 0 || + !field.Annotations?.Has("request") + ) { + return false; + } + const requestAnno = field.Annotations.Get("request") as RequestAnnotation; + return ( + matchContentType(requestAnno.MediaType, "application/json") || + matchContentType(requestAnno.MediaType, "application/x-www-form-urlencoded") + ); +} + function isNullableOptionalWrapped(field: FieldDef): boolean { return Boolean( field.Nullable && diff --git a/templates/templates/cli/opcmd.go.stmpl b/templates/templates/cli/opcmd.go.stmpl index 7cee08b9..eb42fdec 100644 --- a/templates/templates/cli/opcmd.go.stmpl +++ b/templates/templates/cli/opcmd.go.stmpl @@ -92,7 +92,7 @@ func {{$initFuncName}}(parent *cobra.Command) error { {{- if templateHasBodyFlag $op}} cmd.Flags().String("body", "", "{{templateBodyFlagDescription $op}}") {{- addImport "fmt"}} - _ = flagutil.AnnotatePromptFlag(cmd, "body", flagutil.PromptFlagSpec{Kind: "json", BodyFlag: true}) + _ = flagutil.AnnotatePromptFlag(cmd, "body", flagutil.PromptFlagSpec{Required: {{templateEmptyBodyRequired $op}}, Kind: "json", BodyFlag: true}) cmd.Annotations[flagutil.AnnotationWholeBodyFlag] = "body" if err := flagutil.AnnotateBodyFields(cmd, {{templateMetaVarName $op}}, "{{templateBodyFieldPath $op}}", "body"); err != nil { return fmt.Errorf("annotate body fields for {{sanitizeCLICommand $op.GetID}}: %w", err) diff --git a/templates/templates/cli/tests/primary/bodyinput_test.go.stmpl b/templates/templates/cli/tests/primary/bodyinput_test.go.stmpl index 9e3f1071..2528f078 100644 --- a/templates/templates/cli/tests/primary/bodyinput_test.go.stmpl +++ b/templates/templates/cli/tests/primary/bodyinput_test.go.stmpl @@ -3,9 +3,11 @@ package tests import ( "os" "path/filepath" + "strings" "testing" "time" + "github.com/spf13/cobra" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -13,6 +15,45 @@ import ( "{{getGolangPackage}}/internal/output" ) +func TestRequiredEmptyMixedBody(t *testing.T) { + type request struct { + Body struct{} `json:"body"` + } + for _, tc := range []struct { + name string + body string + setBody bool + stdin string + required bool + wantError bool + }{ + {name: "required omitted", required: true, wantError: true}, + {name: "required flag", body: "{}", setBody: true, required: true}, + {name: "required stdin", stdin: "{}", required: true}, + {name: "required empty flag", setBody: true, required: true, wantError: true}, + {name: "required null flag", body: "null", setBody: true, required: true, wantError: true}, + {name: "required null stdin", stdin: "null", required: true, wantError: true}, + {name: "optional omitted"}, + } { + t.Run(tc.name, func(t *testing.T) { + cmd := &cobra.Command{} + cmd.SetIn(strings.NewReader(tc.stdin)) + cmd.Flags().String("body", "", "Request body as JSON") + require.NoError(t, flagutil.AnnotatePromptFlag(cmd, "body", flagutil.PromptFlagSpec{Required: tc.required, Kind: "json", BodyFlag: true})) + if tc.setBody { + require.NoError(t, cmd.Flags().Set("body", tc.body)) + } + _, err := flagutil.BuildRequest[request](cmd, nil, "Body", "body") + if tc.wantError { + require.Error(t, err) + assert.Contains(t, err.Error(), "--body") + } else { + require.NoError(t, err) + } + }) + } +} + // withSilentStdinPipe swaps os.Stdin for the read end of a pipe whose write // end stays open and never produces data — the shape agent runtimes and exec // wrappers hand a CLI — and restores it when the test ends. diff --git a/zSDKs/sdk-cli/.speakeasy/gen.lock b/zSDKs/sdk-cli/.speakeasy/gen.lock index 363a844e..bb5f18dc 100644 --- a/zSDKs/sdk-cli/.speakeasy/gen.lock +++ b/zSDKs/sdk-cli/.speakeasy/gen.lock @@ -62,15 +62,15 @@ trackedFiles: internal/cli/auth.go: last_write_checksum: sha1:6eb9d73f31d4922ceeea044a1d92e46821cc04eb internal/cli/binaryandstringupload.go: - last_write_checksum: sha1:c47f65053b62472a62de6a8f0de0cc6ce28d63f1 + last_write_checksum: sha1:20d8028b7618e73dcf1763372f9145ea325b32b0 internal/cli/chat.go: last_write_checksum: sha1:3d1fa09fa2658d3c8800cecafa30f677180a327f internal/cli/configure.go: last_write_checksum: sha1:ad89cfd7fd30f6395fc006ee54d3f2e640160c21 internal/cli/createuser.go: - last_write_checksum: sha1:dfd3a094a5e4e9f70d9aa051b3edee96a36c799f + last_write_checksum: sha1:5b60000059db85dcf4c90f5e67c37b7fff3cf852 internal/cli/createwithunion.go: - last_write_checksum: sha1:65128825f54a5666104684573d4e257300f7d14e + last_write_checksum: sha1:b0f2c0ce986fe2bee70d002373e9e1e78be84671 internal/cli/deleteuser.go: last_write_checksum: sha1:e6876685816745a290da3903a03a3afe8f0812e7 internal/cli/getasset.go: @@ -86,7 +86,7 @@ trackedFiles: internal/cli/geterroronlyexample.go: last_write_checksum: sha1:31437c3769f79c492aa0e7a7f173a9f73413c102 internal/cli/getfullyflattenedrequest.go: - last_write_checksum: sha1:d8a6bbf230b9cd2e96720d5cf44ee6a7c44bd0b3 + last_write_checksum: sha1:81f559c8f866ccf39e63d0a6ce592993188a6246 internal/cli/getnamedprimitiveunion.go: last_write_checksum: sha1:663d361d6a46c5d729350af9afe88d4ee9b266e3 internal/cli/getnestedintegerstring.go: @@ -136,7 +136,7 @@ trackedFiles: internal/cli/masking.go: last_write_checksum: sha1:5a69fa055d628c1b8507cd123f3cd18309aa9a53 internal/cli/namespacetests/conflicts/createnamespaceconflict.go: - last_write_checksum: sha1:f0b084215afa94936b2cb5cbfe50a8d32e44b07a + last_write_checksum: sha1:1ef390ab92c1aa4416b370734d02f567eaa9fdd1 internal/cli/namespacetests/conflicts/getnamespaceconflict.go: last_write_checksum: sha1:8730dacebc73d9b364b77196c2ff47e7c84d36af internal/cli/namespacetests/conflicts/getpetowners.go: @@ -144,7 +144,7 @@ trackedFiles: internal/cli/namespacetests/conflicts/gettriplenamespaceconflict.go: last_write_checksum: sha1:6e70cde7b555f23e0f22b6b1281794ab6ba6eb5a internal/cli/namespacetests/conflicts/putnamespaceconflict.go: - last_write_checksum: sha1:51e7b258d5587d45cffa6d99a9836ab1589513a8 + last_write_checksum: sha1:4a200a397b2f268275eb3db62bf6fbce70fe66df internal/cli/namespacetests/conflicts/root.go: last_write_checksum: sha1:02a213614562bf56b1d4655435039c5932c35732 internal/cli/namespacetests/root.go: @@ -154,7 +154,7 @@ trackedFiles: internal/cli/namespacetests/singlebar/root.go: last_write_checksum: sha1:9588b75e31fc6974aa5c46bf6ab37200a37b2c8b internal/cli/namespacetests/singlefoo/createsinglenamespacefoopet.go: - last_write_checksum: sha1:f1c2a663ddf1e5f6354149ceb2df3489055fcb36 + last_write_checksum: sha1:cd178ef84ed126fa22d0099ae51d74a6e717dd0c internal/cli/namespacetests/singlefoo/getsinglenamespacefoopet.go: last_write_checksum: sha1:e042198f9fa2f8105c8c39824edcc823bbc5e305 internal/cli/namespacetests/singlefoo/root.go: @@ -176,11 +176,11 @@ trackedFiles: internal/cli/operationwithleadingandtrailingunderscores.go: last_write_checksum: sha1:cf1e07bbdf0dabdb9fffd15c9e3f47a018a59f80 internal/cli/parenthesesinpathallowed.go: - last_write_checksum: sha1:adee137db3ff5c047823800c99412ecab849f706 + last_write_checksum: sha1:044a262b30d750b1a82bc33ab31a70195cab61dd internal/cli/postfile.go: last_write_checksum: sha1:f67d5ddcaa3ea921513ee6f97fe5eb8db9580aae internal/cli/renderasset.go: - last_write_checksum: sha1:66703b4e61cf22941907d9a373dd4e75587f7e0e + last_write_checksum: sha1:7b64189912eaa0e2c11bdbc91f39d3d035bbc8b3 internal/cli/root.go: last_write_checksum: sha1:4efdf5323f50a1dc87bd20d3ffe86af0804da2fa internal/cli/tag1/auth.go: @@ -196,21 +196,21 @@ trackedFiles: internal/cli/tag1/root.go: last_write_checksum: sha1:cfe60d600cb9609d9609056bfce8fb7c97134b41 internal/cli/testendpoint.go: - last_write_checksum: sha1:264efb2b7a2045e94fbec8f4976db14e1018b9d8 + last_write_checksum: sha1:d4ea63754634f3cb79d9603f3abfd7730968ceb8 internal/cli/testenumformats.go: - last_write_checksum: sha1:fc0f5c836a4b4ad9d47fd1a903e963a847aec9b2 + last_write_checksum: sha1:3ed815fb0d60772beecf29e251238985400badd4 internal/cli/testgroup/root.go: last_write_checksum: sha1:e252ec79b7dccbf3cedb78d27ed2f3f7a4cd819f internal/cli/testgroup/tag2/posttest.go: - last_write_checksum: sha1:3cb9926f6fcfa49cdcfff47066a1a0346ec82468 + last_write_checksum: sha1:1cf593c40b1e581cc5063acd177d2ecbd6ba30ae internal/cli/testgroup/tag2/root.go: last_write_checksum: sha1:333e0d1e2947411048be5e44359d41a779a8051d internal/cli/testgroup/tag3/posttest.go: - last_write_checksum: sha1:85afaaa2325b939b4299596689986d3cb98649f5 + last_write_checksum: sha1:67fc1540b046c7acb34da3d95898fdff4590da67 internal/cli/testgroup/tag3/root.go: last_write_checksum: sha1:544b5d37f18bcd87ed2b878deca97b3dba8da1e7 internal/cli/updateuser.go: - last_write_checksum: sha1:1b05be0bffcc0bc864e880d820bdf5f1ada3c29e + last_write_checksum: sha1:703a7dfa5c3601afeff9e803735a4110c118ef71 internal/cli/urlvalidationstresstest.go: last_write_checksum: sha1:088e04431ff4780ee3220d86bf8f423def61f7f6 internal/cli/validate.go: @@ -246,7 +246,7 @@ trackedFiles: internal/flagutil/flags.go: last_write_checksum: sha1:7a255e684a2ea0367f68a8683b9167c56a0f4235 internal/flagutil/metadata.go: - last_write_checksum: sha1:f2dae1449af37ccf1929900338cffde1f07b1220 + last_write_checksum: sha1:1936c82d8e36a6612364827ffc05dcbea0e1dd8f internal/flagutil/preset.go: last_write_checksum: sha1:a580b7d4b00cf469415bc53d40f4981d60091423 internal/interactive/interactive.go: diff --git a/zSDKs/sdk-cli/internal/cli/binaryandstringupload.go b/zSDKs/sdk-cli/internal/cli/binaryandstringupload.go index 0d6051ca..b854b8e5 100644 --- a/zSDKs/sdk-cli/internal/cli/binaryandstringupload.go +++ b/zSDKs/sdk-cli/internal/cli/binaryandstringupload.go @@ -37,7 +37,7 @@ func initBinaryAndStringUploadCmd(parent *cobra.Command) error { return fmt.Errorf("invalid metadata for binary-and-string-upload: %w", err) } cmd.Flags().String("body", "", "Request body as JSON (alternative to individual flags). Can also be provided via stdin; @path reads a file, @- reads stdin to EOF. Use --schema to print the exact JSON Schema.") - _ = flagutil.AnnotatePromptFlag(cmd, "body", flagutil.PromptFlagSpec{Kind: "json", BodyFlag: true}) + _ = flagutil.AnnotatePromptFlag(cmd, "body", flagutil.PromptFlagSpec{Required: false, Kind: "json", BodyFlag: true}) cmd.Annotations[flagutil.AnnotationWholeBodyFlag] = "body" if err := flagutil.AnnotateBodyFields(cmd, binaryAndStringUploadCmdMeta, "", "body"); err != nil { return fmt.Errorf("annotate body fields for binary-and-string-upload: %w", err) diff --git a/zSDKs/sdk-cli/internal/cli/createuser.go b/zSDKs/sdk-cli/internal/cli/createuser.go index f582a73b..7efb60a4 100644 --- a/zSDKs/sdk-cli/internal/cli/createuser.go +++ b/zSDKs/sdk-cli/internal/cli/createuser.go @@ -46,7 +46,7 @@ func initCreateUserCmd(parent *cobra.Command) error { return fmt.Errorf("invalid metadata for create-user: %w", err) } cmd.Flags().String("body", "", "Request body as JSON (alternative to individual flags). Can also be provided via stdin; @path reads a file, @- reads stdin to EOF. Use --schema to print the exact JSON Schema.") - _ = flagutil.AnnotatePromptFlag(cmd, "body", flagutil.PromptFlagSpec{Kind: "json", BodyFlag: true}) + _ = flagutil.AnnotatePromptFlag(cmd, "body", flagutil.PromptFlagSpec{Required: false, Kind: "json", BodyFlag: true}) cmd.Annotations[flagutil.AnnotationWholeBodyFlag] = "body" if err := flagutil.AnnotateBodyFields(cmd, createUserCmdMeta, "", "body"); err != nil { return fmt.Errorf("annotate body fields for create-user: %w", err) diff --git a/zSDKs/sdk-cli/internal/cli/createwithunion.go b/zSDKs/sdk-cli/internal/cli/createwithunion.go index f9cd395b..22acaa0b 100644 --- a/zSDKs/sdk-cli/internal/cli/createwithunion.go +++ b/zSDKs/sdk-cli/internal/cli/createwithunion.go @@ -49,7 +49,7 @@ func initCreateWithUnionCmd(parent *cobra.Command) error { return fmt.Errorf("invalid metadata for create-with-union: %w", err) } cmd.Flags().String("body", "", "Request body as JSON (alternative to individual flags). Can also be provided via stdin; @path reads a file, @- reads stdin to EOF. Use --schema to print the exact JSON Schema.") - _ = flagutil.AnnotatePromptFlag(cmd, "body", flagutil.PromptFlagSpec{Kind: "json", BodyFlag: true}) + _ = flagutil.AnnotatePromptFlag(cmd, "body", flagutil.PromptFlagSpec{Required: false, Kind: "json", BodyFlag: true}) cmd.Annotations[flagutil.AnnotationWholeBodyFlag] = "body" if err := flagutil.AnnotateBodyFields(cmd, createWithUnionCmdMeta, "ShapeRequest", "body"); err != nil { return fmt.Errorf("annotate body fields for create-with-union: %w", err) diff --git a/zSDKs/sdk-cli/internal/cli/getfullyflattenedrequest.go b/zSDKs/sdk-cli/internal/cli/getfullyflattenedrequest.go index 470d8ba1..e2337cc2 100644 --- a/zSDKs/sdk-cli/internal/cli/getfullyflattenedrequest.go +++ b/zSDKs/sdk-cli/internal/cli/getfullyflattenedrequest.go @@ -42,7 +42,7 @@ func initGetFullyFlattenedRequestCmd(parent *cobra.Command) error { return fmt.Errorf("invalid metadata for get-fully-flattened-request: %w", err) } cmd.Flags().String("body", "", "Request body as JSON (alternative to individual flags). Can also be provided via stdin; @path reads a file, @- reads stdin to EOF. Use --schema to print the exact JSON Schema.") - _ = flagutil.AnnotatePromptFlag(cmd, "body", flagutil.PromptFlagSpec{Kind: "json", BodyFlag: true}) + _ = flagutil.AnnotatePromptFlag(cmd, "body", flagutil.PromptFlagSpec{Required: false, Kind: "json", BodyFlag: true}) cmd.Annotations[flagutil.AnnotationWholeBodyFlag] = "body" if err := flagutil.AnnotateBodyFields(cmd, getFullyFlattenedRequestCmdMeta, "RequestBody", "body"); err != nil { return fmt.Errorf("annotate body fields for get-fully-flattened-request: %w", err) diff --git a/zSDKs/sdk-cli/internal/cli/namespacetests/conflicts/createnamespaceconflict.go b/zSDKs/sdk-cli/internal/cli/namespacetests/conflicts/createnamespaceconflict.go index fab351a7..876b2379 100644 --- a/zSDKs/sdk-cli/internal/cli/namespacetests/conflicts/createnamespaceconflict.go +++ b/zSDKs/sdk-cli/internal/cli/namespacetests/conflicts/createnamespaceconflict.go @@ -44,7 +44,7 @@ func initCreateNamespaceConflictCmd(parent *cobra.Command) error { return fmt.Errorf("invalid metadata for create-namespace-conflict: %w", err) } cmd.Flags().String("body", "", "Request body as JSON (alternative to individual flags). Can also be provided via stdin; @path reads a file, @- reads stdin to EOF. Use --schema to print the exact JSON Schema.") - _ = flagutil.AnnotatePromptFlag(cmd, "body", flagutil.PromptFlagSpec{Kind: "json", BodyFlag: true}) + _ = flagutil.AnnotatePromptFlag(cmd, "body", flagutil.PromptFlagSpec{Required: false, Kind: "json", BodyFlag: true}) cmd.Annotations[flagutil.AnnotationWholeBodyFlag] = "body" if err := flagutil.AnnotateBodyFields(cmd, createNamespaceConflictCmdMeta, "", "body"); err != nil { return fmt.Errorf("annotate body fields for create-namespace-conflict: %w", err) diff --git a/zSDKs/sdk-cli/internal/cli/namespacetests/conflicts/putnamespaceconflict.go b/zSDKs/sdk-cli/internal/cli/namespacetests/conflicts/putnamespaceconflict.go index 8ef15aab..df7365b0 100644 --- a/zSDKs/sdk-cli/internal/cli/namespacetests/conflicts/putnamespaceconflict.go +++ b/zSDKs/sdk-cli/internal/cli/namespacetests/conflicts/putnamespaceconflict.go @@ -40,7 +40,7 @@ func initPutNamespaceConflictCmd(parent *cobra.Command) error { return fmt.Errorf("invalid metadata for put-namespace-conflict: %w", err) } cmd.Flags().String("body", "", "Request body as JSON (alternative to individual flags). Can also be provided via stdin; @path reads a file, @- reads stdin to EOF. Use --schema to print the exact JSON Schema.") - _ = flagutil.AnnotatePromptFlag(cmd, "body", flagutil.PromptFlagSpec{Kind: "json", BodyFlag: true}) + _ = flagutil.AnnotatePromptFlag(cmd, "body", flagutil.PromptFlagSpec{Required: false, Kind: "json", BodyFlag: true}) cmd.Annotations[flagutil.AnnotationWholeBodyFlag] = "body" if err := flagutil.AnnotateBodyFields(cmd, putNamespaceConflictCmdMeta, "", "body"); err != nil { return fmt.Errorf("annotate body fields for put-namespace-conflict: %w", err) diff --git a/zSDKs/sdk-cli/internal/cli/namespacetests/singlefoo/createsinglenamespacefoopet.go b/zSDKs/sdk-cli/internal/cli/namespacetests/singlefoo/createsinglenamespacefoopet.go index f87e52fb..8d977f30 100644 --- a/zSDKs/sdk-cli/internal/cli/namespacetests/singlefoo/createsinglenamespacefoopet.go +++ b/zSDKs/sdk-cli/internal/cli/namespacetests/singlefoo/createsinglenamespacefoopet.go @@ -44,7 +44,7 @@ func initCreateSingleNamespaceFooPetCmd(parent *cobra.Command) error { return fmt.Errorf("invalid metadata for create-single-namespace-foo-pet: %w", err) } cmd.Flags().String("body", "", "Request body as JSON (alternative to individual flags). Can also be provided via stdin; @path reads a file, @- reads stdin to EOF. Use --schema to print the exact JSON Schema.") - _ = flagutil.AnnotatePromptFlag(cmd, "body", flagutil.PromptFlagSpec{Kind: "json", BodyFlag: true}) + _ = flagutil.AnnotatePromptFlag(cmd, "body", flagutil.PromptFlagSpec{Required: false, Kind: "json", BodyFlag: true}) cmd.Annotations[flagutil.AnnotationWholeBodyFlag] = "body" if err := flagutil.AnnotateBodyFields(cmd, createSingleNamespaceFooPetCmdMeta, "", "body"); err != nil { return fmt.Errorf("annotate body fields for create-single-namespace-foo-pet: %w", err) diff --git a/zSDKs/sdk-cli/internal/cli/parenthesesinpathallowed.go b/zSDKs/sdk-cli/internal/cli/parenthesesinpathallowed.go index c7661fe6..a35e2cef 100644 --- a/zSDKs/sdk-cli/internal/cli/parenthesesinpathallowed.go +++ b/zSDKs/sdk-cli/internal/cli/parenthesesinpathallowed.go @@ -41,7 +41,7 @@ func initParenthesesInPathAllowedCmd(parent *cobra.Command) error { return fmt.Errorf("invalid metadata for parentheses-in-path-allowed: %w", err) } cmd.Flags().String("body", "", "Request body as JSON (alternative to individual flags). Can also be provided via stdin; @path reads a file, @- reads stdin to EOF. Use --schema to print the exact JSON Schema.") - _ = flagutil.AnnotatePromptFlag(cmd, "body", flagutil.PromptFlagSpec{Kind: "json", BodyFlag: true}) + _ = flagutil.AnnotatePromptFlag(cmd, "body", flagutil.PromptFlagSpec{Required: false, Kind: "json", BodyFlag: true}) cmd.Annotations[flagutil.AnnotationWholeBodyFlag] = "body" if err := flagutil.AnnotateBodyFields(cmd, parenthesesInPathAllowedCmdMeta, "TemplateBracesTest", "body"); err != nil { return fmt.Errorf("annotate body fields for parentheses-in-path-allowed: %w", err) diff --git a/zSDKs/sdk-cli/internal/cli/renderasset.go b/zSDKs/sdk-cli/internal/cli/renderasset.go index 0861dc8c..ca496bb2 100644 --- a/zSDKs/sdk-cli/internal/cli/renderasset.go +++ b/zSDKs/sdk-cli/internal/cli/renderasset.go @@ -37,7 +37,7 @@ func initRenderAssetCmd(parent *cobra.Command) error { return fmt.Errorf("invalid metadata for render-asset: %w", err) } cmd.Flags().String("body", "", "Request body as JSON (alternative to individual flags). Can also be provided via stdin; @path reads a file, @- reads stdin to EOF. Use --schema to print the exact JSON Schema.") - _ = flagutil.AnnotatePromptFlag(cmd, "body", flagutil.PromptFlagSpec{Kind: "json", BodyFlag: true}) + _ = flagutil.AnnotatePromptFlag(cmd, "body", flagutil.PromptFlagSpec{Required: false, Kind: "json", BodyFlag: true}) cmd.Annotations[flagutil.AnnotationWholeBodyFlag] = "body" if err := flagutil.AnnotateBodyFields(cmd, renderAssetCmdMeta, "", "body"); err != nil { return fmt.Errorf("annotate body fields for render-asset: %w", err) diff --git a/zSDKs/sdk-cli/internal/cli/testendpoint.go b/zSDKs/sdk-cli/internal/cli/testendpoint.go index 603db0e6..1c3acddf 100644 --- a/zSDKs/sdk-cli/internal/cli/testendpoint.go +++ b/zSDKs/sdk-cli/internal/cli/testendpoint.go @@ -38,7 +38,7 @@ func initTestEndpointCmd(parent *cobra.Command) error { return fmt.Errorf("invalid metadata for test-endpoint: %w", err) } cmd.Flags().String("body", "", "Request body as JSON (alternative to individual flags). Can also be provided via stdin; @path reads a file, @- reads stdin to EOF. Use --schema to print the exact JSON Schema.") - _ = flagutil.AnnotatePromptFlag(cmd, "body", flagutil.PromptFlagSpec{Kind: "json", BodyFlag: true}) + _ = flagutil.AnnotatePromptFlag(cmd, "body", flagutil.PromptFlagSpec{Required: false, Kind: "json", BodyFlag: true}) cmd.Annotations[flagutil.AnnotationWholeBodyFlag] = "body" if err := flagutil.AnnotateBodyFields(cmd, testEndpointCmdMeta, "RequestBody", "body"); err != nil { return fmt.Errorf("annotate body fields for test-endpoint: %w", err) diff --git a/zSDKs/sdk-cli/internal/cli/testenumformats.go b/zSDKs/sdk-cli/internal/cli/testenumformats.go index c2829c29..6e980ebb 100644 --- a/zSDKs/sdk-cli/internal/cli/testenumformats.go +++ b/zSDKs/sdk-cli/internal/cli/testenumformats.go @@ -40,7 +40,7 @@ func initTestEnumFormatsCmd(parent *cobra.Command) error { return fmt.Errorf("invalid metadata for test-enum-formats: %w", err) } cmd.Flags().String("body", "", "Request body as JSON (alternative to individual flags). Can also be provided via stdin; @path reads a file, @- reads stdin to EOF. Use --schema to print the exact JSON Schema.") - _ = flagutil.AnnotatePromptFlag(cmd, "body", flagutil.PromptFlagSpec{Kind: "json", BodyFlag: true}) + _ = flagutil.AnnotatePromptFlag(cmd, "body", flagutil.PromptFlagSpec{Required: false, Kind: "json", BodyFlag: true}) cmd.Annotations[flagutil.AnnotationWholeBodyFlag] = "body" if err := flagutil.AnnotateBodyFields(cmd, testEnumFormatsCmdMeta, "", "body"); err != nil { return fmt.Errorf("annotate body fields for test-enum-formats: %w", err) diff --git a/zSDKs/sdk-cli/internal/cli/testgroup/tag2/posttest.go b/zSDKs/sdk-cli/internal/cli/testgroup/tag2/posttest.go index b9a2b980..bf480252 100644 --- a/zSDKs/sdk-cli/internal/cli/testgroup/tag2/posttest.go +++ b/zSDKs/sdk-cli/internal/cli/testgroup/tag2/posttest.go @@ -36,7 +36,7 @@ func initPostTestCmd(parent *cobra.Command) error { return fmt.Errorf("invalid metadata for post-test: %w", err) } cmd.Flags().String("body", "", "Request body as JSON (alternative to individual flags). Can also be provided via stdin; @path reads a file, @- reads stdin to EOF. Use --schema to print the exact JSON Schema.") - _ = flagutil.AnnotatePromptFlag(cmd, "body", flagutil.PromptFlagSpec{Kind: "json", BodyFlag: true}) + _ = flagutil.AnnotatePromptFlag(cmd, "body", flagutil.PromptFlagSpec{Required: false, Kind: "json", BodyFlag: true}) cmd.Annotations[flagutil.AnnotationWholeBodyFlag] = "body" if err := flagutil.AnnotateBodyFields(cmd, postTestCmdMeta, "Test2Request", "body"); err != nil { return fmt.Errorf("annotate body fields for post-test: %w", err) diff --git a/zSDKs/sdk-cli/internal/cli/testgroup/tag3/posttest.go b/zSDKs/sdk-cli/internal/cli/testgroup/tag3/posttest.go index 57e6f7fe..7e44ef34 100644 --- a/zSDKs/sdk-cli/internal/cli/testgroup/tag3/posttest.go +++ b/zSDKs/sdk-cli/internal/cli/testgroup/tag3/posttest.go @@ -36,7 +36,7 @@ func initPostTestCmd(parent *cobra.Command) error { return fmt.Errorf("invalid metadata for post-test: %w", err) } cmd.Flags().String("body", "", "Request body as JSON (alternative to individual flags). Can also be provided via stdin; @path reads a file, @- reads stdin to EOF. Use --schema to print the exact JSON Schema.") - _ = flagutil.AnnotatePromptFlag(cmd, "body", flagutil.PromptFlagSpec{Kind: "json", BodyFlag: true}) + _ = flagutil.AnnotatePromptFlag(cmd, "body", flagutil.PromptFlagSpec{Required: false, Kind: "json", BodyFlag: true}) cmd.Annotations[flagutil.AnnotationWholeBodyFlag] = "body" if err := flagutil.AnnotateBodyFields(cmd, postTestCmdMeta, "Test2Request", "body"); err != nil { return fmt.Errorf("annotate body fields for post-test: %w", err) diff --git a/zSDKs/sdk-cli/internal/cli/updateuser.go b/zSDKs/sdk-cli/internal/cli/updateuser.go index 34407f7c..43338982 100644 --- a/zSDKs/sdk-cli/internal/cli/updateuser.go +++ b/zSDKs/sdk-cli/internal/cli/updateuser.go @@ -47,7 +47,7 @@ func initUpdateUserCmd(parent *cobra.Command) error { return fmt.Errorf("invalid metadata for update-user: %w", err) } cmd.Flags().String("body", "", "Request body as JSON (alternative to individual flags). Can also be provided via stdin; @path reads a file, @- reads stdin to EOF. Use --schema to print the exact JSON Schema.") - _ = flagutil.AnnotatePromptFlag(cmd, "body", flagutil.PromptFlagSpec{Kind: "json", BodyFlag: true}) + _ = flagutil.AnnotatePromptFlag(cmd, "body", flagutil.PromptFlagSpec{Required: false, Kind: "json", BodyFlag: true}) cmd.Annotations[flagutil.AnnotationWholeBodyFlag] = "body" if err := flagutil.AnnotateBodyFields(cmd, updateUserCmdMeta, "User", "body"); err != nil { return fmt.Errorf("annotate body fields for update-user: %w", err) diff --git a/zSDKs/sdk-cli/internal/flagutil/metadata.go b/zSDKs/sdk-cli/internal/flagutil/metadata.go index e91fc06c..b93279e2 100644 --- a/zSDKs/sdk-cli/internal/flagutil/metadata.go +++ b/zSDKs/sdk-cli/internal/flagutil/metadata.go @@ -664,8 +664,16 @@ func BuildRequest[T any](cmd *cobra.Command, meta []FlagMeta, bodyFieldPath stri v := reflect.ValueOf(&req).Elem() bodyPrePopulated := false hasRequestBody := bodyFieldPath != "" || bodyFlagName != "" || !isJSONSerialized(v.Type()) + bodyRequired := false + if flag := cmd.Flags().Lookup(bodyFlagName); flag != nil { + values := flag.Annotations[AnnotationRequired] + bodyRequired = len(values) > 0 && values[0] == "true" + } decodeBody := func(data []byte, source string) error { + if bodyRequired && bytes.Equal(bytes.TrimSpace(data), []byte("null")) { + return WithCLIValidation(fmt.Errorf("invalid value for --%s: null; the body is required", bodyFlagName)) + } u := bodyUnionMeta(meta, bodyFieldPath) if bodyFieldPath != "" { bodyField, err := navigateToField(v, bodyFieldPath) @@ -725,6 +733,10 @@ func BuildRequest[T any](cmd *cobra.Command, meta []FlagMeta, bodyFieldPath stri } } + if bodyRequired && !bodyPrePopulated { + return nil, &MissingRequiredFlagError{FlagName: bodyFlagName, Detail: "(or provide via stdin)"} + } + // When body provided via --body flag or stdin, relax Required checks for body fields // so builders don't error for fields already populated if bodyPrePopulated {