diff --git a/internal/clientgen/client.go b/internal/clientgen/client.go new file mode 100644 index 0000000..a915fba --- /dev/null +++ b/internal/clientgen/client.go @@ -0,0 +1,251 @@ +// Package clientgen analyzes the language-neutral HTTP contract used by client emitters. +package clientgen + +import ( + "fmt" + "net/http" + "sort" + "strings" + + "github.com/responsibleapi/oasmith/internal/openapi" +) + +// Operation is the normalized client contract for one OpenAPI operation. +type Operation struct { + Route openapi.OperationRoute + RequestBody RequestBody + Responses []Response + Accept string +} + +// RequestBody is exactly one supported request body shape. +type RequestBody struct { + JSON *JSONBody + Raw *RawBody + Multipart *MultipartBody +} + +// JSONBody describes a JSON request body. +type JSONBody struct { + Schema *openapi.Schema + Required bool +} + +// RawBody describes a request body passed through without encoding. +type RawBody struct { + MediaType string + Required bool +} + +// MultipartBody describes the fixed-length sequential multipart shape supported by clients. +type MultipartBody struct { + MediaType string + Required bool + Parts []MultipartPart +} + +// MultipartPart describes one positional multipart part. +type MultipartPart struct { + Title string + Schema *openapi.Schema + ContentType string + Binary bool +} + +// Response describes one concrete response status and its selected representation. +type Response struct { + Status int + MediaType string + Schema *openapi.Schema + SSE bool +} + +// HasBody reports whether the response has a supported typed body. +func (response Response) HasBody() bool { + return response.Schema != nil +} + +// JSON reports whether the response body is JSON encoded. +func (response Response) JSON() bool { + return response.HasBody() && strings.Contains(response.MediaType, "json") +} + +// Text reports whether the response body is text encoded. +func (response Response) Text() bool { + return response.HasBody() && + (strings.HasPrefix(response.MediaType, "text/") || + response.MediaType == "application/xml" || strings.HasSuffix(response.MediaType, "+xml")) +} + +// Analyze normalizes and validates every operation used by generated clients. +func Analyze(doc *openapi.Document) ([]Operation, error) { + routes := doc.Operations() + operations := make([]Operation, 0, len(routes)) + for _, route := range routes { + requestBody, err := analyzeRequestBody(route.Operation) + if err != nil { + return nil, err + } + responses := analyzeResponses(doc, route) + operations = append(operations, Operation{ + Route: route, + RequestBody: requestBody, + Responses: responses, + Accept: operationAccept(responses), + }) + } + return operations, nil +} + +func analyzeRequestBody(operation *openapi.Operation) (RequestBody, error) { + if operation.RequestBody == nil { + return RequestBody{}, nil + } + if media, ok := operation.RequestBody.Content["application/json"]; ok { + if media.Schema == nil { + return RequestBody{}, fmt.Errorf( + "operation %s has unsupported request body: application/json schema is missing", + operation.OperationID, + ) + } + return RequestBody{JSON: &JSONBody{ + Schema: media.Schema, + Required: operation.RequestBody.Required, + }}, nil + } + if multipart, ok := sequentialMultipartBody(operation.RequestBody); ok { + return RequestBody{Multipart: multipart}, nil + } + if mediaType, ok := operation.RawRequestBodyMediaType(); ok && !strings.HasPrefix(mediaType, "multipart/") { + return RequestBody{Raw: &RawBody{ + MediaType: mediaType, + Required: operation.RequestBody.Required, + }}, nil + } + return RequestBody{}, fmt.Errorf("operation %s has unsupported request body", operation.OperationID) +} + +func sequentialMultipartBody(requestBody *openapi.RequestBody) (*MultipartBody, bool) { + mediaTypes := make([]string, 0, len(requestBody.Content)) + for mediaType := range requestBody.Content { + mediaTypes = append(mediaTypes, mediaType) + } + sort.Strings(mediaTypes) + for _, mediaType := range mediaTypes { + media := requestBody.Content[mediaType] + if !strings.HasPrefix(mediaType, "multipart/") || media.Schema == nil || + !media.Schema.Type.Has("array") || len(media.Schema.PrefixItems) != 2 || + len(media.PrefixEncoding) != 2 { + continue + } + minimum, maximum := 0, 0 + if media.Schema.MinItems != nil { + minimum = *media.Schema.MinItems + } + if media.Schema.MaxItems != nil { + maximum = *media.Schema.MaxItems + } + if minimum != 2 || maximum != 2 { + continue + } + body := &MultipartBody{ + MediaType: mediaType, + Required: requestBody.Required, + } + for index, schema := range media.Schema.PrefixItems { + body.Parts = append(body.Parts, MultipartPart{ + Title: schema.Title, + Schema: schema, + ContentType: strings.TrimSpace(media.PrefixEncoding[index].ContentType), + Binary: schema.Type.Has("string") && schema.Format == "binary", + }) + } + return body, true + } + return nil, false +} + +func analyzeResponses(doc *openapi.Document, route openapi.OperationRoute) []Response { + statuses := make([]string, 0, len(route.Operation.Responses)) + for status := range route.Operation.Responses { + statuses = append(statuses, status) + } + sort.Strings(statuses) + responses := make([]Response, 0, len(statuses)) + for _, status := range statuses { + statusCode, ok := parseStatus(status) + if !ok { + continue + } + resolved := doc.ResolveResponse(route.Operation.Responses[status]) + mediaType, media, ok := responseMedia(resolved) + response := Response{Status: statusCode} + if ok { + response.MediaType = mediaType + response.Schema = media.Schema + if mediaType == "text/event-stream" && route.Method == http.MethodGet && isSuccessStatus(statusCode) { + response.Schema = media.ItemSchema + response.SSE = response.Schema != nil + } + } + responses = append(responses, response) + } + return responses +} + +func isSuccessStatus(status int) bool { + return status >= 200 && status < 300 +} + +func responseMedia(response openapi.Response) (string, openapi.MediaType, bool) { + for _, mediaType := range []string{ + "text/event-stream", + "application/json", + "application/rss+xml", + "text/yaml", + "text/plain", + } { + media, ok := response.Content[mediaType] + if ok { + return mediaType, media, true + } + } + mediaTypes := make([]string, 0, len(response.Content)) + for mediaType := range response.Content { + mediaTypes = append(mediaTypes, mediaType) + } + sort.Strings(mediaTypes) + if len(mediaTypes) == 0 { + return "", openapi.MediaType{}, false + } + mediaType := mediaTypes[0] + return mediaType, response.Content[mediaType], true +} + +func operationAccept(responses []Response) string { + for _, response := range responses { + if response.SSE { + return "text/event-stream" + } + } + for _, response := range responses { + if response.MediaType == "application/json" { + return "application/json" + } + } + return "*/*" +} + +func parseStatus(status string) (int, bool) { + if len(status) != 3 { + return 0, false + } + value := 0 + for _, digit := range status { + if digit < '0' || digit > '9' { + return 0, false + } + value = value*10 + int(digit-'0') + } + return value, true +} diff --git a/internal/goemit/client.go b/internal/goemit/client.go index 4600744..4b3bd89 100644 --- a/internal/goemit/client.go +++ b/internal/goemit/client.go @@ -2,9 +2,10 @@ package goemit import ( "fmt" - "sort" + "strconv" "strings" + "github.com/responsibleapi/oasmith/internal/clientgen" "github.com/responsibleapi/oasmith/internal/openapi" ) @@ -26,6 +27,7 @@ type goOperationData struct { Params []goOperationParamData ResponsesType string Responses []goResponseData + BodylessStatuses string Accept string HasJSONBody bool RequiredJSONBody bool @@ -78,13 +80,13 @@ type goResponseData struct { ReconnectableSSE bool } -func goClientTemplateData(doc *openapi.Document, sourcePath string) goClientData { +func goClientTemplateData(doc *openapi.Document, sourcePath string, operations []clientgen.Operation) goClientData { e := emitter{} data := goClientData{ PackageName: packageName(doc, sourcePath), } - for _, route := range doc.Operations() { - operation := e.operationTemplateData(doc, route) + for _, analyzed := range operations { + operation := e.operationTemplateData(doc, analyzed) data.HasJSONBody = data.HasJSONBody || operation.HasJSONBody data.HasMultipartBody = data.HasMultipartBody || operation.MultipartBody != nil for _, response := range operation.Responses { @@ -95,7 +97,8 @@ func goClientTemplateData(doc *openapi.Document, sourcePath string) goClientData return data } -func (e *emitter) operationTemplateData(doc *openapi.Document, route openapi.OperationRoute) goOperationData { +func (e *emitter) operationTemplateData(doc *openapi.Document, analyzed clientgen.Operation) goOperationData { + route := analyzed.Route op := route.Operation data := goOperationData{ ID: openapi.ExportName(op.OperationID), @@ -103,7 +106,7 @@ func (e *emitter) operationTemplateData(doc *openapi.Document, route openapi.Ope Path: route.Path, ParamsType: openapi.ExportName(op.OperationID) + "Params", ResponsesType: openapi.ExportName(op.OperationID) + "Response", - Accept: operationAccept(doc, route.Method, op), + Accept: analyzed.Accept, } for _, param := range op.Parameters { paramType := e.goType(param.Schema) @@ -129,13 +132,13 @@ func (e *emitter) operationTemplateData(doc *openapi.Document, route openapi.Ope }) data.HasQueryParams = data.HasQueryParams || param.In == "query" } - if multipartBody, ok := sequentialMultipartBody(e, op); ok { - data.MultipartBody = multipartBody + if analyzed.RequestBody.Multipart != nil { + data.MultipartBody = e.multipartBodyTemplateData(analyzed.RequestBody.Multipart) data.HasRequestBody = true data.HasParams = true - } else if schema := op.JSONRequestSchema(); schema != nil { - paramType := e.goType(schema) - required := op.RequestBody != nil && op.RequestBody.Required + } else if body := analyzed.RequestBody.JSON; body != nil { + paramType := e.goType(body.Schema) + required := body.Required if !required { paramType = optionalType(paramType) } @@ -149,73 +152,46 @@ func (e *emitter) operationTemplateData(doc *openapi.Document, route openapi.Ope data.HasJSONBody = true data.RequiredJSONBody = required data.HasRequestBody = true - } else if mediaType, ok := op.RawRequestBodyMediaType(); ok { + } else if body := analyzed.RequestBody.Raw; body != nil { data.HasRawBody = true - data.RequiredRawBody = op.RequestBody != nil && op.RequestBody.Required - data.RawBodyMediaType = mediaType + data.RequiredRawBody = body.Required + data.RawBodyMediaType = body.MediaType data.HasRequestBody = true } data.HasParams = len(data.Params) > 0 || data.HasRawBody if data.MultipartBody != nil { data.HasParams = true } - data.Responses = e.operationResponses(doc, route.Method, op) + data.Responses, data.BodylessStatuses = e.operationResponses(analyzed.Responses) for _, response := range data.Responses { data.ReconnectableSSE = data.ReconnectableSSE || response.ReconnectableSSE } return data } -func sequentialMultipartBody(e *emitter, operation *openapi.Operation) (*goMultipartBodyData, bool) { - if operation.RequestBody == nil { - return nil, false - } - mediaTypes := make([]string, 0, len(operation.RequestBody.Content)) - for mediaType := range operation.RequestBody.Content { - mediaTypes = append(mediaTypes, mediaType) - } - sort.Strings(mediaTypes) - for _, mediaType := range mediaTypes { - media := operation.RequestBody.Content[mediaType] - if !strings.HasPrefix(mediaType, "multipart/") || media.Schema == nil || - !media.Schema.Type.Has("array") || len(media.Schema.PrefixItems) != 2 || - len(media.PrefixEncoding) != 2 { - continue - } - minimum, maximum := 0, 0 - if media.Schema.MinItems != nil { - minimum = *media.Schema.MinItems +func (e *emitter) multipartBodyTemplateData(analyzed *clientgen.MultipartBody) *goMultipartBodyData { + body := &goMultipartBodyData{MediaType: analyzed.MediaType} + for index, analyzedPart := range analyzed.Parts { + fieldName := openapi.ExportName(analyzedPart.Title) + if fieldName == "" { + fieldName = fmt.Sprintf("Part%d", index+1) } - if media.Schema.MaxItems != nil { - maximum = *media.Schema.MaxItems + part := goMultipartPartData{ + FieldName: fieldName, + ContentType: analyzedPart.ContentType, } - if minimum != 2 || maximum != 2 { - continue + if analyzedPart.Binary { + part.Binary = true + part.Type = "io.Reader" + part.ContentTypeField = fieldName + "ContentType" + part.AllowedContentType = part.ContentType + } else { + part.JSON = true + part.Type = e.goType(analyzedPart.Schema) } - body := &goMultipartBodyData{MediaType: mediaType} - for index, schema := range media.Schema.PrefixItems { - fieldName := openapi.ExportName(schema.Title) - if fieldName == "" { - fieldName = fmt.Sprintf("Part%d", index+1) - } - part := goMultipartPartData{ - FieldName: fieldName, - ContentType: strings.TrimSpace(media.PrefixEncoding[index].ContentType), - } - if schema.Type.Has("string") && schema.Format == "binary" { - part.Binary = true - part.Type = "io.Reader" - part.ContentTypeField = fieldName + "ContentType" - part.AllowedContentType = part.ContentType - } else { - part.JSON = true - part.Type = e.goType(schema) - } - body.Parts = append(body.Parts, part) - } - return body, true + body.Parts = append(body.Parts, part) } - return nil, false + return body } func schemaIsString(doc *openapi.Document, schema *openapi.Schema) bool { @@ -228,83 +204,24 @@ func schemaIsString(doc *openapi.Document, schema *openapi.Schema) bool { return schema.Type.Has("string") } -func (e *emitter) operationResponses(doc *openapi.Document, method string, operation *openapi.Operation) []goResponseData { - statuses := make([]string, 0, len(operation.Responses)) - for status := range operation.Responses { - statuses = append(statuses, status) - } - sort.Strings(statuses) - responses := make([]goResponseData, 0, len(statuses)) - for _, status := range statuses { - statusCode, ok := parseStatus(status) - if !ok { - continue - } - response := doc.ResolveResponse(operation.Responses[status]) +func (e *emitter) operationResponses(analyzed []clientgen.Response) ([]goResponseData, string) { + responses := make([]goResponseData, 0, len(analyzed)) + var bodylessStatuses []string + for _, response := range analyzed { responseData := goResponseData{ - Status: statusCode, - FieldName: fmt.Sprintf("Status%d", statusCode), + Status: response.Status, + FieldName: fmt.Sprintf("Status%d", response.Status), + Type: e.goType(response.Schema), + Body: response.HasBody(), + JSON: response.JSON(), + Text: response.Text(), + SSE: response.SSE, + ReconnectableSSE: response.SSE, } - mime, media, ok := responseMedia(response) - if ok { - schema := media.Schema - if mime == "text/event-stream" && method == "GET" { - schema = media.ItemSchema - responseData.SSE = true - responseData.ReconnectableSSE = true - } - responseData.Type = e.goType(schema) - responseData.Body = schema != nil - responseData.JSON = strings.Contains(mime, "json") - responseData.Text = strings.HasPrefix(mime, "text/") || mime == "application/rss+xml" + if !responseData.Body { + bodylessStatuses = append(bodylessStatuses, strconv.Itoa(response.Status)) } responses = append(responses, responseData) } - return responses -} - -func responseMedia(response openapi.Response) (string, openapi.MediaType, bool) { - for _, mime := range []string{"text/event-stream", "application/json", "application/rss+xml", "text/yaml", "text/plain"} { - media, ok := response.Content[mime] - if ok { - return mime, media, true - } - } - var mimes []string - for mime := range response.Content { - mimes = append(mimes, mime) - } - sort.Strings(mimes) - if len(mimes) == 0 { - return "", openapi.MediaType{}, false - } - mime := mimes[0] - return mime, response.Content[mime], true -} - -func operationAccept(doc *openapi.Document, method string, operation *openapi.Operation) string { - if doc.OperationHasSSEResponseMethod(method, operation) { - return "text/event-stream" - } - for _, response := range operation.Responses { - resolved := doc.ResolveResponse(response) - if _, ok := resolved.Content["application/json"]; ok { - return "application/json" - } - } - return "*/*" -} - -func parseStatus(status string) (int, bool) { - if len(status) != 3 { - return 0, false - } - value := 0 - for _, digit := range status { - if digit < '0' || digit > '9' { - return 0, false - } - value = value*10 + int(digit-'0') - } - return value, true + return responses, strings.Join(bodylessStatuses, ", ") } diff --git a/internal/goemit/goemit.go b/internal/goemit/goemit.go index d97922b..3e105c9 100644 --- a/internal/goemit/goemit.go +++ b/internal/goemit/goemit.go @@ -10,6 +10,7 @@ import ( "sort" "strings" + "github.com/responsibleapi/oasmith/internal/clientgen" "github.com/responsibleapi/oasmith/internal/emit" "github.com/responsibleapi/oasmith/internal/openapi" ) @@ -47,10 +48,14 @@ func Emit(doc *openapi.Document, opts Options) error { // EmitClient writes Go models and an HTTP client for the supplied OpenAPI document. func EmitClient(doc *openapi.Document, opts Options) error { + operations, err := clientgen.Analyze(doc) + if err != nil { + return fmt.Errorf("analyze client operations: %w", err) + } if err := Emit(doc, opts); err != nil { return err } - raw, err := executeTemplate("client.go", goClientTemplateData(doc, opts.SourcePath)) + raw, err := executeTemplate("client.go", goClientTemplateData(doc, opts.SourcePath, operations)) if err != nil { return err } diff --git a/internal/goemit/templates/client.go.gotmpl b/internal/goemit/templates/client.go.gotmpl index bce8c44..4daf060 100644 --- a/internal/goemit/templates/client.go.gotmpl +++ b/internal/goemit/templates/client.go.gotmpl @@ -658,7 +658,6 @@ func sseRetryValue(frame []byte) (time.Duration, bool) { Raw *http.Response {{range .Responses}}{{if .SSE}} {{.FieldName}} *SSEStream[{{.Type}}] {{else if .Body}} {{.FieldName}} *{{.Type}} -{{else}} {{.FieldName}} bool {{end}}{{end}}} func (c *Client) New{{.ID}}Request(ctx context.Context{{if .HasParams}}, params {{.ParamsType}}{{end}}) (*http.Request, error) { @@ -704,13 +703,13 @@ func (c *Client) New{{.ID}}Request(ctx context.Context{{if .HasParams}}, params if err != nil { return nil, fmt.Errorf("build {{.ID}} multipart body: %w", err) } -{{else if .HasJSONBody}} var requestBody io.Reader -{{range .Params}}{{if .Body}}{{if .Required}} encodedBody, err := json.Marshal(params.{{.FieldName}}) +{{else if .HasJSONBody}}{{if .RequiredJSONBody}}{{range .Params}}{{if .Body}} encodedBody, err := json.Marshal(params.{{.FieldName}}) if err != nil { return nil, fmt.Errorf("encode {{$operation.ID}} JSON body: %w", err) } - requestBody = bytes.NewReader(encodedBody) -{{else}} if params.{{.FieldName}} != nil { + requestBody := bytes.NewReader(encodedBody) +{{end}}{{end}}{{else}} var requestBody io.Reader +{{range .Params}}{{if .Body}} if params.{{.FieldName}} != nil { encodedBody, err := json.Marshal(params.{{.FieldName}}) if err != nil { return nil, fmt.Errorf("encode {{$operation.ID}} JSON body: %w", err) @@ -723,7 +722,8 @@ func (c *Client) New{{.ID}}Request(ctx context.Context{{if .HasParams}}, params return nil, fmt.Errorf("build {{.ID}} request: %w", err) } req.Header.Set("Accept", {{printf "%q" .Accept}}) -{{if .HasJSONBody}} if requestBody != nil { +{{if and .HasJSONBody .RequiredJSONBody}} req.Header.Set("Content-Type", "application/json") +{{else if .HasJSONBody}} if requestBody != nil { req.Header.Set("Content-Type", "application/json") } {{else if .MultipartBody}} req.Header.Set("Content-Type", contentType) @@ -807,7 +807,7 @@ func (c *Client) {{.ID}}(ctx context.Context{{if .HasParams}}, params {{.ParamsT {{end}} result := &{{.ResponsesType}}{StatusCode: res.StatusCode, Raw: res} switch res.StatusCode { -{{range .Responses}} case {{.Status}}: +{{range .Responses}}{{if .Body}} case {{.Status}}: {{if .SSE}} lifecycle.stopTimeout() keepLifecycle = true {{if .ReconnectableSSE}} body := newReconnectingSSEBody( @@ -843,10 +843,10 @@ func (c *Client) {{.ID}}(ctx context.Context{{if .HasParams}}, params {{.ParamsT decoded := {{.Type}}(rawBody) result.{{.FieldName}} = &decoded return result, nil -{{else}} _ = res.Body.Close() - result.{{.FieldName}} = true +{{end}}{{end}}{{end}}{{if .BodylessStatuses}} case {{.BodylessStatuses}}: + _ = res.Body.Close() return result, nil -{{end}}{{end}} default: +{{end}} default: rawBody, readErr := io.ReadAll(io.LimitReader(res.Body, maxDiagnosticBodyBytes)) _ = res.Body.Close() if readErr != nil { diff --git a/internal/tsemit/templates/api.ts.gotmpl b/internal/tsemit/templates/api.ts.gotmpl index 13d9b1d..a7d7095 100644 --- a/internal/tsemit/templates/api.ts.gotmpl +++ b/internal/tsemit/templates/api.ts.gotmpl @@ -661,9 +661,17 @@ async function runInterceptors( {{end}}{{end}}{{end}} const headerParameters: Record = {}; {{if .HasSSE}} headerParameters['Accept'] = 'text/event-stream'; -{{end}}{{if .BodyParamName}} headerParameters['Content-Type'] = 'application/json'; -{{end}}{{if .RawBodyParamName}} headerParameters['Content-Type'] = {{printf "%q" .RawBodyMediaType}}; -{{end}}{{with .MultipartBody}} const multipartBoundary = newMultipartBoundary(); +{{end}}{{with .JSONBody}}{{if .Required}} headerParameters['Content-Type'] = 'application/json'; +{{else}} const requestBody = requestParameters['{{.Name}}']; + if (requestBody !== null && requestBody !== undefined) { + headerParameters['Content-Type'] = 'application/json'; + } +{{end}}{{end}}{{with .RawBody}}{{if .Required}} headerParameters['Content-Type'] = {{printf "%q" .MediaType}}; +{{else}} const requestBody = requestParameters['{{.Name}}']; + if (requestBody !== null && requestBody !== undefined) { + headerParameters['Content-Type'] = {{printf "%q" .MediaType}}; + } +{{end}}{{end}}{{with .MultipartBody}} const multipartBoundary = newMultipartBoundary(); headerParameters['Content-Type'] = {{printf "%q" .MediaType}} + '; boundary=' + multipartBoundary; const multipartBody = new Blob([ {{range .Parts}} '--' + multipartBoundary + '\r\n', @@ -692,10 +700,13 @@ async function runInterceptors( {{end}} return new Request(this.baseURL + {{.PathExpression}}{{if .QueryParams}} + (queryString === '' ? '' : '?' + queryString){{end}}, { method: '{{.Method}}', headers: headerParameters, -{{if .BodyParamName}} body: JSON.stringify(requestParameters['{{.BodyParamName}}']), -{{else if .RawBodyParamName}} body: requestParameters['{{.RawBodyParamName}}'], -{{else if .MultipartBody}} body: multipartBody, -{{end}} }); +{{with .JSONBody}}{{if .Required}} body: JSON.stringify(requestParameters['{{.Name}}']), +{{else}} body: requestBody === null || requestBody === undefined ? undefined : JSON.stringify(requestBody), +{{end}}{{else}}{{with .RawBody}}{{if .Required}} body: requestParameters['{{.Name}}'], +{{else}} body: requestBody, +{{end}}{{else}}{{if .MultipartBody}} body: multipartBody, +{{end}}{{end}}{{end}} + }); } async {{.ID}}Result({{.RawRequestSignature}}initOverrides?: RequestInit): Promise<{{.ResultType}}> { diff --git a/internal/tsemit/tsemit.go b/internal/tsemit/tsemit.go index 32697c5..e049a17 100644 --- a/internal/tsemit/tsemit.go +++ b/internal/tsemit/tsemit.go @@ -7,10 +7,10 @@ import ( "os" "os/exec" "path/filepath" - "slices" "sort" "strings" + "github.com/responsibleapi/oasmith/internal/clientgen" "github.com/responsibleapi/oasmith/internal/emit" "github.com/responsibleapi/oasmith/internal/openapi" ) @@ -186,24 +186,22 @@ func modelsTemplateData(doc *openapi.Document) modelsData { } func apiSource(doc *openapi.Document) (string, error) { - operations := doc.Operations() - for _, route := range operations { - if err := validateRequestBody(route.Operation); err != nil { - return "", err - } + operations, err := clientgen.Analyze(doc) + if err != nil { + return "", fmt.Errorf("analyze client operations: %w", err) } data := apiData{ Imports: modelImports(doc, operations), } - for _, route := range operations { - params := operationParams(route.Operation) + for _, analyzed := range operations { + params := operationParams(analyzed) if len(params) > 0 { data.RequestInterfaces = append(data.RequestInterfaces, requestInterfaceData{ - Name: requestInterfaceName(route.Operation.OperationID), + Name: requestInterfaceName(analyzed.Route.Operation.OperationID), Params: params, }) } - operation := operationTemplateData(doc, route) + operation := operationTemplateData(analyzed) if operation.HasSSE { data.HasSSE = true } @@ -243,14 +241,24 @@ type operationData struct { PositionalSignature string PositionalRequestObject string PathExpression string - BodyParamName string - RawBodyParamName string - RawBodyMediaType string + JSONBody *tsRequestBodyData + RawBody *tsRawRequestBodyData MultipartBody *tsMultipartBodyData QueryParams []opParam HasSSE bool } +type tsRequestBodyData struct { + Name string + Required bool +} + +type tsRawRequestBodyData struct { + Name string + Required bool + MediaType string +} + type tsMultipartBodyData struct { MediaType string Parts []tsMultipartPartData @@ -265,10 +273,11 @@ type tsMultipartPartData struct { AllowedContentType string } -func operationTemplateData(doc *openapi.Document, route openapi.OperationRoute) operationData { +func operationTemplateData(analyzed clientgen.Operation) operationData { + route := analyzed.Route op := route.Operation - params := operationParams(op) - responses := operationResponses(doc, route.Method, op) + params := operationParams(analyzed) + responses := operationResponses(analyzed.Responses) data := operationData{ ID: openapi.LowerCamel(op.OperationID), Method: route.Method, @@ -282,7 +291,7 @@ func operationTemplateData(doc *openapi.Document, route openapi.OperationRoute) PositionalSignature: positionalSignature(params), PositionalRequestObject: positionalRequestObject(params), PathExpression: pathExpression(route.Path, params), - HasSSE: doc.OperationHasSSEResponseMethod(route.Method, op), + HasSSE: analyzed.Accept == "text/event-stream", } for _, param := range params { if param.Required { @@ -300,13 +309,18 @@ func operationTemplateData(doc *openapi.Document, route openapi.OperationRoute) if body := bodyParam(params); body != nil { switch body.Kind { case "body": - data.BodyParamName = body.Name + data.JSONBody = &tsRequestBodyData{Name: body.Name, Required: body.Required} case "rawBody": - data.RawBodyParamName = body.Name - data.RawBodyMediaType, _ = op.RawRequestBodyMediaType() + data.RawBody = &tsRawRequestBodyData{ + Name: body.Name, + Required: body.Required, + MediaType: analyzed.RequestBody.Raw.MediaType, + } } } - data.MultipartBody, _ = sequentialMultipartBody(op) + if analyzed.RequestBody.Multipart != nil { + data.MultipartBody = multipartBodyTemplateData(analyzed.RequestBody.Multipart) + } return data } @@ -329,13 +343,13 @@ type opResponse struct { Text bool } -func modelImports(doc *openapi.Document, operations []openapi.OperationRoute) []string { +func modelImports(doc *openapi.Document, operations []clientgen.Operation) []string { seen := map[string]bool{} - for _, route := range operations { - for _, param := range operationParams(route.Operation) { + for _, operation := range operations { + for _, param := range operationParams(operation) { collectModelNames(doc, param.Type, seen) } - for _, response := range operationResponses(doc, route.Method, route.Operation) { + for _, response := range operationResponses(operation.Responses) { collectModelNames(doc, response.Type, seen) } } @@ -394,7 +408,8 @@ func executeTemplate(name string, data any) (string, error) { return string(raw), err } -func operationParams(operation *openapi.Operation) []opParam { +func operationParams(analyzed clientgen.Operation) []opParam { + operation := analyzed.Route.Operation var params []opParam for _, param := range operation.Parameters { params = append(params, opParam{ @@ -407,133 +422,81 @@ func operationParams(operation *openapi.Operation) []opParam { Slice: param.Schema != nil && param.Schema.IsArray(), }) } - if multipartBody, ok := sequentialMultipartBody(operation); ok { - for _, part := range multipartBody.Parts { + if multipartBody := analyzed.RequestBody.Multipart; multipartBody != nil { + for index, part := range multipartBody.Parts { + name := openapi.LowerCamel(part.Title) + if name == "" { + name = fmt.Sprintf("part%d", index+1) + } params = append(params, opParam{ - Name: part.Name, - WireName: part.Name, - Type: part.Type, + Name: name, + WireName: name, + Type: multipartPartType(part), Required: true, Kind: "multipart", }) } - } else if schema := operation.JSONRequestSchema(); schema != nil { - name := openapi.LowerCamel(openapi.RefName(schema.Ref)) - if name == "" { - name = "body" - } + } else if body := analyzed.RequestBody.JSON; body != nil { params = append(params, opParam{ - Name: name, + Name: "body", WireName: "body", - Type: tsType(schema), - Required: operation.RequestBody.Required, - Optional: optional(operation.RequestBody.Required), + Type: tsType(body.Schema), + Required: body.Required, + Optional: optional(body.Required), Kind: "body", }) - } else if _, ok := operation.RawRequestBodyMediaType(); ok { + } else if body := analyzed.RequestBody.Raw; body != nil { params = append(params, opParam{ Name: "body", WireName: "body", Type: "BodyInit", - Required: operation.RequestBody.Required, - Optional: optional(operation.RequestBody.Required), + Required: body.Required, + Optional: optional(body.Required), Kind: "rawBody", }) } return params } -func validateRequestBody(operation *openapi.Operation) error { - if operation.RequestBody == nil { - return nil - } - if media, ok := operation.RequestBody.Content["application/json"]; ok { - if media.Schema != nil { - return nil - } - return fmt.Errorf("operation %s has unsupported request body: application/json schema is missing", operation.OperationID) - } - if _, ok := sequentialMultipartBody(operation); ok { - return nil - } - if mediaType, ok := operation.RawRequestBodyMediaType(); ok && !strings.HasPrefix(mediaType, "multipart/") { - return nil - } - return fmt.Errorf("operation %s has unsupported request body", operation.OperationID) -} - -func sequentialMultipartBody(operation *openapi.Operation) (*tsMultipartBodyData, bool) { - if operation.RequestBody == nil { - return nil, false - } - mediaTypes := make([]string, 0, len(operation.RequestBody.Content)) - for mediaType := range operation.RequestBody.Content { - mediaTypes = append(mediaTypes, mediaType) - } - sort.Strings(mediaTypes) - for _, mediaType := range mediaTypes { - media := operation.RequestBody.Content[mediaType] - if !strings.HasPrefix(mediaType, "multipart/") || media.Schema == nil || - !media.Schema.Type.Has("array") || len(media.Schema.PrefixItems) != 2 || - len(media.PrefixEncoding) != 2 { - continue - } - minimum, maximum := 0, 0 - if media.Schema.MinItems != nil { - minimum = *media.Schema.MinItems - } - if media.Schema.MaxItems != nil { - maximum = *media.Schema.MaxItems +func multipartBodyTemplateData(analyzed *clientgen.MultipartBody) *tsMultipartBodyData { + body := &tsMultipartBodyData{MediaType: analyzed.MediaType} + for index, analyzedPart := range analyzed.Parts { + name := openapi.LowerCamel(analyzedPart.Title) + if name == "" { + name = fmt.Sprintf("part%d", index+1) } - if minimum != 2 || maximum != 2 { - continue + part := tsMultipartPartData{ + Name: name, + Type: multipartPartType(analyzedPart), + ContentType: analyzedPart.ContentType, + Binary: analyzedPart.Binary, + JSON: !analyzedPart.Binary, } - body := &tsMultipartBodyData{MediaType: mediaType} - for index, schema := range media.Schema.PrefixItems { - name := openapi.LowerCamel(schema.Title) - if name == "" { - name = fmt.Sprintf("part%d", index+1) - } - part := tsMultipartPartData{ - Name: name, - ContentType: strings.TrimSpace(media.PrefixEncoding[index].ContentType), - } - if schema.Type.Has("string") && schema.Format == "binary" { - part.Binary = true - part.Type = "Blob" - part.AllowedContentType = part.ContentType - } else { - part.JSON = true - part.Type = tsType(schema) - } - body.Parts = append(body.Parts, part) + if analyzedPart.Binary { + part.AllowedContentType = part.ContentType } - return body, true + body.Parts = append(body.Parts, part) } - return nil, false + return body } -func operationResponses(doc *openapi.Document, method string, operation *openapi.Operation) []opResponse { - var statuses []string - for status := range operation.Responses { - statuses = append(statuses, status) +func multipartPartType(part clientgen.MultipartPart) string { + if part.Binary { + return "Blob" } - slices.Sort(statuses) - responses := make([]opResponse, 0, len(statuses)) - for _, status := range statuses { - statusCode, ok := parseStatus(status) - if !ok { - continue - } - response := doc.ResolveResponse(operation.Responses[status]) - kind := responseBodyKind(response) + return tsType(part.Schema) +} + +func operationResponses(analyzed []clientgen.Response) []opResponse { + responses := make([]opResponse, 0, len(analyzed)) + for _, response := range analyzed { responses = append(responses, opResponse{ - Status: statusCode, - Type: responseBodyType(response), - Body: responseHasTypedBody(response), - SSE: method == "GET" && responseHasSSEBody(response), - JSON: kind == "json", - Text: kind == "text", + Status: response.Status, + Type: tsType(response.Schema), + Body: response.HasBody(), + SSE: response.SSE, + JSON: response.JSON(), + Text: response.Text(), }) } return responses @@ -579,65 +542,10 @@ func operationResultType(responses []opResponse) string { return strings.Join(variants, " | ") } -func responseBodyType(response openapi.Response) string { - if mt, ok := response.Content["text/event-stream"]; ok && mt.ItemSchema != nil { - return tsType(mt.ItemSchema) - } - if mt, ok := response.Content["application/json"]; ok && mt.Schema != nil { - return tsType(mt.Schema) - } - if mt, ok := response.Content["application/rss+xml"]; ok && mt.Schema != nil { - return tsType(mt.Schema) - } - return "unknown" -} - -func responseHasTypedBody(response openapi.Response) bool { - if mt, ok := response.Content["application/json"]; ok && mt.Schema != nil { - return true - } - if mt, ok := response.Content["application/rss+xml"]; ok && mt.Schema != nil { - return true - } - return false -} - -func responseHasSSEBody(response openapi.Response) bool { - mt, ok := response.Content["text/event-stream"] - return ok && mt.ItemSchema != nil -} - -func responseBodyKind(response openapi.Response) string { - if mt, ok := response.Content["text/event-stream"]; ok && mt.ItemSchema != nil { - return "sse" - } - if mt, ok := response.Content["application/json"]; ok && mt.Schema != nil { - return "json" - } - if mt, ok := response.Content["application/rss+xml"]; ok && mt.Schema != nil { - return "text" - } - return "" -} - func isSuccessStatus(status int) bool { return status >= 200 && status < 300 } -func parseStatus(status string) (int, bool) { - if len(status) != 3 { - return 0, false - } - value := 0 - for _, digit := range status { - if digit < '0' || digit > '9' { - return 0, false - } - value = value*10 + int(digit-'0') - } - return value, true -} - func uniqueStrings(values []string) []string { seen := map[string]bool{} var unique []string diff --git a/oasmith_test.go b/oasmith_test.go index 4c31be5..dc6b8ed 100644 --- a/oasmith_test.go +++ b/oasmith_test.go @@ -227,6 +227,57 @@ func TestGoClientBehavior(t *testing.T) { } } +func TestGeneratedClientsUseCanonicalRequestAndResponseShapes(t *testing.T) { + t.Parallel() + + fixturePath := filepath.Join("testdata", "fixtures", "public-client.yaml") + doc, err := openapi.ParseFile(fixturePath) + if err != nil { + t.Fatalf("parse public client fixture: %v", err) + } + goOut := t.TempDir() + if err := goemit.EmitClient(doc, goemit.Options{OutDir: goOut, SourcePath: fixturePath}); err != nil { + t.Fatalf("emit Go client: %v", err) + } + goRaw, err := os.ReadFile(filepath.Join(goOut, "client.go")) + if err != nil { + t.Fatalf("read Go client: %v", err) + } + goSource := string(goRaw) + for _, want := range []string{ + "requestBody := bytes.NewReader(encodedBody)", + "case 401, 403:", + } { + if !strings.Contains(goSource, want) { + t.Fatalf("generated Go client missing %q", want) + } + } + for _, forbidden := range []string{ + "Status401 bool", + "Status403 bool", + } { + if strings.Contains(goSource, forbidden) { + t.Fatalf("generated Go client contains redundant bodyless response field %q", forbidden) + } + } + + typeScriptOut := t.TempDir() + if err := tsemit.Emit(doc, tsemit.Options{OutDir: typeScriptOut}); err != nil { + t.Fatalf("emit TypeScript client: %v", err) + } + typeScriptRaw, err := os.ReadFile(filepath.Join(typeScriptOut, "api.ts")) + if err != nil { + t.Fatalf("read TypeScript client: %v", err) + } + typeScriptSource := string(typeScriptRaw) + if !strings.Contains(typeScriptSource, "body: CreateThing\n") { + t.Fatal("generated TypeScript client does not expose the canonical body request parameter") + } + if strings.Contains(typeScriptSource, "createThing: CreateThing\n") { + t.Fatal("generated TypeScript client exposes a schema-derived request body parameter") + } +} + func TestGoPackageNameFallsBackToSourceBasename(t *testing.T) { t.Parallel() @@ -549,7 +600,7 @@ components: } } -func TestTypeScriptClientRejectsUnsupportedRequestBody(t *testing.T) { +func TestClientsRejectUnsupportedRequestBody(t *testing.T) { t.Parallel() doc, err := openapi.Parse([]byte(`openapi: 3.2.0 @@ -573,9 +624,30 @@ paths: if err != nil { t.Fatalf("parse unsupported request body fixture: %v", err) } - err = tsemit.Emit(doc, tsemit.Options{OutDir: t.TempDir()}) - if err == nil || !strings.Contains(err.Error(), "operation upload has unsupported request body") { - t.Fatalf("unsupported request body error = %v", err) + for _, testCase := range []struct { + name string + emit func(string) error + }{ + { + name: "Go", + emit: func(outDir string) error { + return goemit.EmitClient(doc, goemit.Options{OutDir: outDir, SourcePath: "unsupported.yaml"}) + }, + }, + { + name: "TypeScript", + emit: func(outDir string) error { + return tsemit.Emit(doc, tsemit.Options{OutDir: outDir}) + }, + }, + } { + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + err := testCase.emit(t.TempDir()) + if err == nil || !strings.Contains(err.Error(), "operation upload has unsupported request body") { + t.Fatalf("unsupported request body error = %v", err) + } + }) } } @@ -1024,7 +1096,7 @@ void describe("TypeScript client queries", () => { notify: true, label: ["alpha beta", "x&y"], xRequestId: "request/1", - createThing: { name: "fixture" }, + body: { name: "fixture" }, }) const url = new URL(request.url) @@ -1045,6 +1117,24 @@ void describe("TypeScript client queries", () => { assert.equal(request.headers.get("Content-Type"), "application/octet-stream") assert.equal(await request.text(), "complete-media") }) + + void test("omits absent optional request bodies and their content types", async () => { + const api = new DefaultApi({ baseURL: "https://example.test" }) + + const absentJSON = api.patchThingRequest({}) + assert.equal(absentJSON.headers.get("Content-Type"), null) + assert.equal(await absentJSON.text(), "") + const presentJSON = api.patchThingRequest({ body: { name: "patched" } }) + assert.equal(presentJSON.headers.get("Content-Type"), "application/json") + assert.equal(await presentJSON.text(), '{"name":"patched"}') + + const absentRaw = api.uploadOptionalMediaRequest({}) + assert.equal(absentRaw.headers.get("Content-Type"), null) + assert.equal(await absentRaw.text(), "") + const presentRaw = api.uploadOptionalMediaRequest({ body: "optional-media" }) + assert.equal(presentRaw.headers.get("Content-Type"), "application/octet-stream") + assert.equal(await presentRaw.text(), "optional-media") + }) }) ` @@ -1186,6 +1276,54 @@ func TestRawRequestBodyAndSharedPathParameters(t *testing.T) { } } +func TestOptionalRequestBodiesAndContentTypes(t *testing.T) { + client, err := NewClient(ClientOptions{BaseURL: "https://example.test"}) + if err != nil { + t.Fatal(err) + } + + absentJSON, err := client.NewPatchThingRequest(context.Background(), PatchThingParams{}) + if err != nil { + t.Fatal(err) + } + if absentJSON.Body != nil || absentJSON.Header.Get("Content-Type") != "" { + t.Fatalf("absent JSON body = %#v, Content-Type = %q", absentJSON.Body, absentJSON.Header.Get("Content-Type")) + } + body := CreateThing{Name: "patched"} + presentJSON, err := client.NewPatchThingRequest(context.Background(), PatchThingParams{Body: &body}) + if err != nil { + t.Fatal(err) + } + jsonBody, err := io.ReadAll(presentJSON.Body) + if err != nil { + t.Fatal(err) + } + if string(jsonBody) != ` + "`" + `{"name":"patched"}` + "`" + ` || presentJSON.Header.Get("Content-Type") != "application/json" { + t.Fatalf("present JSON body = %q, Content-Type = %q", jsonBody, presentJSON.Header.Get("Content-Type")) + } + + absentRaw, err := client.NewUploadOptionalMediaRequest(context.Background(), UploadOptionalMediaParams{}) + if err != nil { + t.Fatal(err) + } + if absentRaw.Body != nil || absentRaw.Header.Get("Content-Type") != "" { + t.Fatalf("absent raw body = %#v, Content-Type = %q", absentRaw.Body, absentRaw.Header.Get("Content-Type")) + } + presentRaw, err := client.NewUploadOptionalMediaRequest(context.Background(), UploadOptionalMediaParams{ + Body: strings.NewReader("optional-media"), + }) + if err != nil { + t.Fatal(err) + } + rawBody, err := io.ReadAll(presentRaw.Body) + if err != nil { + t.Fatal(err) + } + if string(rawBody) != "optional-media" || presentRaw.Header.Get("Content-Type") != "application/octet-stream" { + t.Fatalf("present raw body = %q, Content-Type = %q", rawBody, presentRaw.Header.Get("Content-Type")) + } +} + func TestRequestEditorFailurePreventsTransportExecution(t *testing.T) { sentinel := errors.New("editor failed") transportCalls := 0 diff --git a/testdata/fixtures/public-client.yaml b/testdata/fixtures/public-client.yaml index 1a64b05..0bcfde9 100644 --- a/testdata/fixtures/public-client.yaml +++ b/testdata/fixtures/public-client.yaml @@ -76,6 +76,32 @@ paths: application/json: schema: $ref: "#/components/schemas/Problem" + "401": + description: Unauthorized + "403": + description: Forbidden + /optional-json: + post: + operationId: patchThing + requestBody: + required: false + content: + application/json: + schema: + $ref: "#/components/schemas/CreateThing" + responses: + "204": + description: Patched + /optional-raw: + post: + operationId: uploadOptionalMedia + requestBody: + required: false + content: + application/octet-stream: {} + responses: + "204": + description: Uploaded /events: get: operationId: watchEvents diff --git a/testdata/golden/private-typescript/api.ts b/testdata/golden/private-typescript/api.ts index c8d6579..e3501cb 100644 --- a/testdata/golden/private-typescript/api.ts +++ b/testdata/golden/private-typescript/api.ts @@ -37,6 +37,7 @@ import type { UpdateShow, UploadSessionID, User, + ValidationErr, } from "./types.ts" export type FetchInterceptor = ( @@ -577,31 +578,31 @@ async function runInterceptors( } export interface VerifyEmailRequest { - emailVerification: EmailVerification + body: EmailVerification } export interface CreateGoogleSignupSessionRequest { - googleSignup: GoogleSignup + body: GoogleSignup } export interface CreatePasswordLoginSessionRequest { - passwordAuth: PasswordAuth + body: PasswordAuth } export interface ConfirmPasswordResetRequest { - passwordResetConfirmation: PasswordResetConfirmation + body: PasswordResetConfirmation } export interface RequestPasswordResetRequest { - passwordResetRequest: PasswordResetRequest + body: PasswordResetRequest } export interface CreatePasswordSignupSessionRequest { - passwordAuth: PasswordAuth + body: PasswordAuth } export interface CreateAPIKeyRequest { - createAPIKey: CreateAPIKey + body: CreateAPIKey } export interface ShowPageRequest { @@ -613,34 +614,34 @@ export interface ShowsPageRequest { } export interface CreateTeamRequest { - createTeam: CreateTeam + body: CreateTeam } export interface CreateImageUploadPresignRequest { teamId: TeamID - createImageUploadPresign: CreateImageUploadPresign + body: CreateImageUploadPresign } export interface CreateMediaMultipartUploadRequest { teamId: TeamID - createMediaMultipartUpload: CreateMediaMultipartUpload + body: CreateMediaMultipartUpload } export interface CreateMediaUploadPresignRequest { teamId: TeamID - createMediaUploadPresign: CreateMediaUploadPresign + body: CreateMediaUploadPresign } export interface CompleteMediaMultipartUploadRequest { teamId: TeamID uploadSessionId: UploadSessionID - completeMediaMultipartUpload: CompleteMediaMultipartUpload + body: CompleteMediaMultipartUpload } export interface PresignMediaUploadPartsRequest { teamId: TeamID uploadSessionId: UploadSessionID - presignMediaUploadParts: PresignMediaUploadParts + body: PresignMediaUploadParts } export interface ListShowsRequest { @@ -649,13 +650,13 @@ export interface ListShowsRequest { export interface CreateShowRequest { teamId: TeamID - createShow: CreateShow + body: CreateShow } export interface UpdateShowRequest { teamId: TeamID showId: ShowID - updateShow: UpdateShow + body: UpdateShow } export interface ListEpisodesRequest { @@ -666,14 +667,14 @@ export interface ListEpisodesRequest { export interface CreateEpisodeRequest { teamId: TeamID showId: ShowID - createEpisode: CreateEpisode + body: CreateEpisode } export interface UpdateEpisodeRequest { teamId: TeamID showId: ShowID episodeId: EpisodeID - updateEpisode: UpdateEpisode + body: UpdateEpisode } export interface EpisodeProcessingEventsRequest { @@ -805,12 +806,12 @@ export class DefaultApi { verifyEmailRequest(requestParameters: VerifyEmailRequest): Request { if ( - requestParameters["emailVerification"] === null || - requestParameters["emailVerification"] === undefined + requestParameters["body"] === null || + requestParameters["body"] === undefined ) { throw new RequiredError( - "emailVerification", - 'Required parameter "emailVerification" was null or undefined when calling verifyEmail().', + "body", + 'Required parameter "body" was null or undefined when calling verifyEmail().', ) } @@ -819,7 +820,7 @@ export class DefaultApi { return new Request(this.baseURL + "/auth/email/verify", { method: "POST", headers: headerParameters, - body: JSON.stringify(requestParameters["emailVerification"]), + body: JSON.stringify(requestParameters["body"]), }) } @@ -828,7 +829,7 @@ export class DefaultApi { initOverrides?: RequestInit, ): Promise< | { status: 201; body: Session; raw: Response } - | { status: 400; raw: Response } + | { status: 400; body: ValidationErr; raw: Response } | { status: 401; raw: Response } > { const timedResponse = await this.request( @@ -858,13 +859,10 @@ export class DefaultApi { } async verifyEmail( - emailVerification: EmailVerification, + body: EmailVerification, initOverrides?: RequestInit, ): Promise { - const response = await this.verifyEmailResult( - { emailVerification }, - initOverrides, - ) + const response = await this.verifyEmailResult({ body }, initOverrides) if (response.status === 201) { return response.body } @@ -875,12 +873,12 @@ export class DefaultApi { requestParameters: CreateGoogleSignupSessionRequest, ): Request { if ( - requestParameters["googleSignup"] === null || - requestParameters["googleSignup"] === undefined + requestParameters["body"] === null || + requestParameters["body"] === undefined ) { throw new RequiredError( - "googleSignup", - 'Required parameter "googleSignup" was null or undefined when calling createGoogleSignupSession().', + "body", + 'Required parameter "body" was null or undefined when calling createGoogleSignupSession().', ) } @@ -889,7 +887,7 @@ export class DefaultApi { return new Request(this.baseURL + "/auth/google/signup", { method: "POST", headers: headerParameters, - body: JSON.stringify(requestParameters["googleSignup"]), + body: JSON.stringify(requestParameters["body"]), }) } @@ -898,7 +896,7 @@ export class DefaultApi { initOverrides?: RequestInit, ): Promise< | { status: 201; body: Session; raw: Response } - | { status: 400; raw: Response } + | { status: 400; body: ValidationErr; raw: Response } > { const timedResponse = await this.request( this.createGoogleSignupSessionRequest(requestParameters), @@ -924,11 +922,11 @@ export class DefaultApi { } async createGoogleSignupSession( - googleSignup: GoogleSignup, + body: GoogleSignup, initOverrides?: RequestInit, ): Promise { const response = await this.createGoogleSignupSessionResult( - { googleSignup }, + { body }, initOverrides, ) if (response.status === 201) { @@ -941,12 +939,12 @@ export class DefaultApi { requestParameters: CreatePasswordLoginSessionRequest, ): Request { if ( - requestParameters["passwordAuth"] === null || - requestParameters["passwordAuth"] === undefined + requestParameters["body"] === null || + requestParameters["body"] === undefined ) { throw new RequiredError( - "passwordAuth", - 'Required parameter "passwordAuth" was null or undefined when calling createPasswordLoginSession().', + "body", + 'Required parameter "body" was null or undefined when calling createPasswordLoginSession().', ) } @@ -955,7 +953,7 @@ export class DefaultApi { return new Request(this.baseURL + "/auth/login", { method: "POST", headers: headerParameters, - body: JSON.stringify(requestParameters["passwordAuth"]), + body: JSON.stringify(requestParameters["body"]), }) } @@ -964,7 +962,7 @@ export class DefaultApi { initOverrides?: RequestInit, ): Promise< | { status: 201; body: Session; raw: Response } - | { status: 400; raw: Response } + | { status: 400; body: ValidationErr; raw: Response } | { status: 401; raw: Response } | { status: 403; raw: Response } > { @@ -998,11 +996,11 @@ export class DefaultApi { } async createPasswordLoginSession( - passwordAuth: PasswordAuth, + body: PasswordAuth, initOverrides?: RequestInit, ): Promise { const response = await this.createPasswordLoginSessionResult( - { passwordAuth }, + { body }, initOverrides, ) if (response.status === 201) { @@ -1015,12 +1013,12 @@ export class DefaultApi { requestParameters: ConfirmPasswordResetRequest, ): Request { if ( - requestParameters["passwordResetConfirmation"] === null || - requestParameters["passwordResetConfirmation"] === undefined + requestParameters["body"] === null || + requestParameters["body"] === undefined ) { throw new RequiredError( - "passwordResetConfirmation", - 'Required parameter "passwordResetConfirmation" was null or undefined when calling confirmPasswordReset().', + "body", + 'Required parameter "body" was null or undefined when calling confirmPasswordReset().', ) } @@ -1029,7 +1027,7 @@ export class DefaultApi { return new Request(this.baseURL + "/auth/password/reset/confirm", { method: "POST", headers: headerParameters, - body: JSON.stringify(requestParameters["passwordResetConfirmation"]), + body: JSON.stringify(requestParameters["body"]), }) } @@ -1038,7 +1036,7 @@ export class DefaultApi { initOverrides?: RequestInit, ): Promise< | { status: 201; body: Session; raw: Response } - | { status: 400; raw: Response } + | { status: 400; body: ValidationErr; raw: Response } | { status: 401; raw: Response } > { const timedResponse = await this.request( @@ -1068,11 +1066,11 @@ export class DefaultApi { } async confirmPasswordReset( - passwordResetConfirmation: PasswordResetConfirmation, + body: PasswordResetConfirmation, initOverrides?: RequestInit, ): Promise { const response = await this.confirmPasswordResetResult( - { passwordResetConfirmation }, + { body }, initOverrides, ) if (response.status === 201) { @@ -1085,12 +1083,12 @@ export class DefaultApi { requestParameters: RequestPasswordResetRequest, ): Request { if ( - requestParameters["passwordResetRequest"] === null || - requestParameters["passwordResetRequest"] === undefined + requestParameters["body"] === null || + requestParameters["body"] === undefined ) { throw new RequiredError( - "passwordResetRequest", - 'Required parameter "passwordResetRequest" was null or undefined when calling requestPasswordReset().', + "body", + 'Required parameter "body" was null or undefined when calling requestPasswordReset().', ) } @@ -1099,14 +1097,17 @@ export class DefaultApi { return new Request(this.baseURL + "/auth/password/reset/request", { method: "POST", headers: headerParameters, - body: JSON.stringify(requestParameters["passwordResetRequest"]), + body: JSON.stringify(requestParameters["body"]), }) } async requestPasswordResetResult( requestParameters: RequestPasswordResetRequest, initOverrides?: RequestInit, - ): Promise<{ status: 202; raw: Response } | { status: 400; raw: Response }> { + ): Promise< + | { status: 202; raw: Response } + | { status: 400; body: ValidationErr; raw: Response } + > { const timedResponse = await this.request( this.requestPasswordResetRequest(requestParameters), initOverrides, @@ -1130,11 +1131,11 @@ export class DefaultApi { } async requestPasswordReset( - passwordResetRequest: PasswordResetRequest, + body: PasswordResetRequest, initOverrides?: RequestInit, ): Promise { const response = await this.requestPasswordResetResult( - { passwordResetRequest }, + { body }, initOverrides, ) if (response.status === 202) { @@ -1147,12 +1148,12 @@ export class DefaultApi { requestParameters: CreatePasswordSignupSessionRequest, ): Request { if ( - requestParameters["passwordAuth"] === null || - requestParameters["passwordAuth"] === undefined + requestParameters["body"] === null || + requestParameters["body"] === undefined ) { throw new RequiredError( - "passwordAuth", - 'Required parameter "passwordAuth" was null or undefined when calling createPasswordSignupSession().', + "body", + 'Required parameter "body" was null or undefined when calling createPasswordSignupSession().', ) } @@ -1161,7 +1162,7 @@ export class DefaultApi { return new Request(this.baseURL + "/auth/signup", { method: "POST", headers: headerParameters, - body: JSON.stringify(requestParameters["passwordAuth"]), + body: JSON.stringify(requestParameters["body"]), }) } @@ -1170,7 +1171,7 @@ export class DefaultApi { initOverrides?: RequestInit, ): Promise< | { status: 202; raw: Response } - | { status: 400; raw: Response } + | { status: 400; body: ValidationErr; raw: Response } | { status: 409; raw: Response } > { const timedResponse = await this.request( @@ -1199,11 +1200,11 @@ export class DefaultApi { } async createPasswordSignupSession( - passwordAuth: PasswordAuth, + body: PasswordAuth, initOverrides?: RequestInit, ): Promise { const response = await this.createPasswordSignupSessionResult( - { passwordAuth }, + { body }, initOverrides, ) if (response.status === 202) { @@ -1224,7 +1225,7 @@ export class DefaultApi { initOverrides?: RequestInit, ): Promise< | { status: 200; body: User; raw: Response } - | { status: 400; raw: Response } + | { status: 400; body: ValidationErr; raw: Response } | { status: 401; raw: Response } > { const timedResponse = await this.request( @@ -1273,7 +1274,7 @@ export class DefaultApi { initOverrides?: RequestInit, ): Promise< | { status: 200; body: readonly ListedAPIKey[]; raw: Response } - | { status: 400; raw: Response } + | { status: 400; body: ValidationErr; raw: Response } | { status: 401; raw: Response } > { const timedResponse = await this.request( @@ -1314,12 +1315,12 @@ export class DefaultApi { createAPIKeyRequest(requestParameters: CreateAPIKeyRequest): Request { if ( - requestParameters["createAPIKey"] === null || - requestParameters["createAPIKey"] === undefined + requestParameters["body"] === null || + requestParameters["body"] === undefined ) { throw new RequiredError( - "createAPIKey", - 'Required parameter "createAPIKey" was null or undefined when calling createAPIKey().', + "body", + 'Required parameter "body" was null or undefined when calling createAPIKey().', ) } @@ -1328,7 +1329,7 @@ export class DefaultApi { return new Request(this.baseURL + "/session/api-keys", { method: "POST", headers: headerParameters, - body: JSON.stringify(requestParameters["createAPIKey"]), + body: JSON.stringify(requestParameters["body"]), }) } @@ -1337,7 +1338,7 @@ export class DefaultApi { initOverrides?: RequestInit, ): Promise< | { status: 201; body: CreatedAPIKey; raw: Response } - | { status: 400; raw: Response } + | { status: 400; body: ValidationErr; raw: Response } | { status: 401; raw: Response } > { const timedResponse = await this.request( @@ -1367,13 +1368,10 @@ export class DefaultApi { } async createAPIKey( - createAPIKey: CreateAPIKey, + body: CreateAPIKey, initOverrides?: RequestInit, ): Promise { - const response = await this.createAPIKeyResult( - { createAPIKey }, - initOverrides, - ) + const response = await this.createAPIKeyResult({ body }, initOverrides) if (response.status === 201) { return response.body } @@ -1392,7 +1390,7 @@ export class DefaultApi { initOverrides?: RequestInit, ): Promise< | { status: 200; body: AppShellData; raw: Response } - | { status: 400; raw: Response } + | { status: 400; body: ValidationErr; raw: Response } | { status: 401; raw: Response } > { const timedResponse = await this.request( @@ -1441,7 +1439,7 @@ export class DefaultApi { initOverrides?: RequestInit, ): Promise< | { status: 200; body: Team; raw: Response } - | { status: 400; raw: Response } + | { status: 400; body: ValidationErr; raw: Response } | { status: 401; raw: Response } | { status: 404; raw: Response } > { @@ -1509,7 +1507,7 @@ export class DefaultApi { initOverrides?: RequestInit, ): Promise< | { status: 200; body: ShowPage; raw: Response } - | { status: 400; raw: Response } + | { status: 400; body: ValidationErr; raw: Response } | { status: 401; raw: Response } | { status: 404; raw: Response } > { @@ -1580,7 +1578,7 @@ export class DefaultApi { initOverrides?: RequestInit, ): Promise< | { status: 200; body: ShowsPage; raw: Response } - | { status: 400; raw: Response } + | { status: 400; body: ValidationErr; raw: Response } | { status: 401; raw: Response } | { status: 404; raw: Response } > { @@ -1626,12 +1624,12 @@ export class DefaultApi { createTeamRequest(requestParameters: CreateTeamRequest): Request { if ( - requestParameters["createTeam"] === null || - requestParameters["createTeam"] === undefined + requestParameters["body"] === null || + requestParameters["body"] === undefined ) { throw new RequiredError( - "createTeam", - 'Required parameter "createTeam" was null or undefined when calling createTeam().', + "body", + 'Required parameter "body" was null or undefined when calling createTeam().', ) } @@ -1640,7 +1638,7 @@ export class DefaultApi { return new Request(this.baseURL + "/session/teams", { method: "POST", headers: headerParameters, - body: JSON.stringify(requestParameters["createTeam"]), + body: JSON.stringify(requestParameters["body"]), }) } @@ -1649,7 +1647,7 @@ export class DefaultApi { initOverrides?: RequestInit, ): Promise< | { status: 201; body: Team; raw: Response } - | { status: 400; raw: Response } + | { status: 400; body: ValidationErr; raw: Response } | { status: 401; raw: Response } > { const timedResponse = await this.request( @@ -1679,10 +1677,10 @@ export class DefaultApi { } async createTeam( - createTeam: CreateTeam, + body: CreateTeam, initOverrides?: RequestInit, ): Promise { - const response = await this.createTeamResult({ createTeam }, initOverrides) + const response = await this.createTeamResult({ body }, initOverrides) if (response.status === 201) { return response.body } @@ -1703,12 +1701,12 @@ export class DefaultApi { } if ( - requestParameters["createImageUploadPresign"] === null || - requestParameters["createImageUploadPresign"] === undefined + requestParameters["body"] === null || + requestParameters["body"] === undefined ) { throw new RequiredError( - "createImageUploadPresign", - 'Required parameter "createImageUploadPresign" was null or undefined when calling createImageUploadPresign().', + "body", + 'Required parameter "body" was null or undefined when calling createImageUploadPresign().', ) } @@ -1720,7 +1718,7 @@ export class DefaultApi { { method: "POST", headers: headerParameters, - body: JSON.stringify(requestParameters["createImageUploadPresign"]), + body: JSON.stringify(requestParameters["body"]), }, ) } @@ -1730,7 +1728,7 @@ export class DefaultApi { initOverrides?: RequestInit, ): Promise< | { status: 201; body: CreatedImageUploadPresign; raw: Response } - | { status: 400; raw: Response } + | { status: 400; body: ValidationErr; raw: Response } | { status: 401; raw: Response } | { status: 404; raw: Response } > { @@ -1765,11 +1763,11 @@ export class DefaultApi { async createImageUploadPresign( teamId: TeamID, - createImageUploadPresign: CreateImageUploadPresign, + body: CreateImageUploadPresign, initOverrides?: RequestInit, ): Promise { const response = await this.createImageUploadPresignResult( - { teamId, createImageUploadPresign }, + { teamId, body }, initOverrides, ) if (response.status === 201) { @@ -1792,12 +1790,12 @@ export class DefaultApi { } if ( - requestParameters["createMediaMultipartUpload"] === null || - requestParameters["createMediaMultipartUpload"] === undefined + requestParameters["body"] === null || + requestParameters["body"] === undefined ) { throw new RequiredError( - "createMediaMultipartUpload", - 'Required parameter "createMediaMultipartUpload" was null or undefined when calling createMediaMultipartUpload().', + "body", + 'Required parameter "body" was null or undefined when calling createMediaMultipartUpload().', ) } @@ -1809,7 +1807,7 @@ export class DefaultApi { { method: "POST", headers: headerParameters, - body: JSON.stringify(requestParameters["createMediaMultipartUpload"]), + body: JSON.stringify(requestParameters["body"]), }, ) } @@ -1819,7 +1817,7 @@ export class DefaultApi { initOverrides?: RequestInit, ): Promise< | { status: 201; body: CreatedMediaMultipartUpload; raw: Response } - | { status: 400; raw: Response } + | { status: 400; body: ValidationErr; raw: Response } | { status: 401; raw: Response } | { status: 404; raw: Response } > { @@ -1854,11 +1852,11 @@ export class DefaultApi { async createMediaMultipartUpload( teamId: TeamID, - createMediaMultipartUpload: CreateMediaMultipartUpload, + body: CreateMediaMultipartUpload, initOverrides?: RequestInit, ): Promise { const response = await this.createMediaMultipartUploadResult( - { teamId, createMediaMultipartUpload }, + { teamId, body }, initOverrides, ) if (response.status === 201) { @@ -1881,12 +1879,12 @@ export class DefaultApi { } if ( - requestParameters["createMediaUploadPresign"] === null || - requestParameters["createMediaUploadPresign"] === undefined + requestParameters["body"] === null || + requestParameters["body"] === undefined ) { throw new RequiredError( - "createMediaUploadPresign", - 'Required parameter "createMediaUploadPresign" was null or undefined when calling createMediaUploadPresign().', + "body", + 'Required parameter "body" was null or undefined when calling createMediaUploadPresign().', ) } @@ -1898,7 +1896,7 @@ export class DefaultApi { { method: "POST", headers: headerParameters, - body: JSON.stringify(requestParameters["createMediaUploadPresign"]), + body: JSON.stringify(requestParameters["body"]), }, ) } @@ -1908,7 +1906,7 @@ export class DefaultApi { initOverrides?: RequestInit, ): Promise< | { status: 201; body: CreatedMediaUploadPresign; raw: Response } - | { status: 400; raw: Response } + | { status: 400; body: ValidationErr; raw: Response } | { status: 401; raw: Response } | { status: 404; raw: Response } > { @@ -1943,11 +1941,11 @@ export class DefaultApi { async createMediaUploadPresign( teamId: TeamID, - createMediaUploadPresign: CreateMediaUploadPresign, + body: CreateMediaUploadPresign, initOverrides?: RequestInit, ): Promise { const response = await this.createMediaUploadPresignResult( - { teamId, createMediaUploadPresign }, + { teamId, body }, initOverrides, ) if (response.status === 201) { @@ -1980,12 +1978,12 @@ export class DefaultApi { } if ( - requestParameters["completeMediaMultipartUpload"] === null || - requestParameters["completeMediaMultipartUpload"] === undefined + requestParameters["body"] === null || + requestParameters["body"] === undefined ) { throw new RequiredError( - "completeMediaMultipartUpload", - 'Required parameter "completeMediaMultipartUpload" was null or undefined when calling completeMediaMultipartUpload().', + "body", + 'Required parameter "body" was null or undefined when calling completeMediaMultipartUpload().', ) } @@ -1997,7 +1995,7 @@ export class DefaultApi { { method: "POST", headers: headerParameters, - body: JSON.stringify(requestParameters["completeMediaMultipartUpload"]), + body: JSON.stringify(requestParameters["body"]), }, ) } @@ -2007,7 +2005,7 @@ export class DefaultApi { initOverrides?: RequestInit, ): Promise< | { status: 200; body: CompletedMediaMultipartUpload; raw: Response } - | { status: 400; raw: Response } + | { status: 400; body: ValidationErr; raw: Response } | { status: 401; raw: Response } | { status: 404; raw: Response } > { @@ -2043,11 +2041,11 @@ export class DefaultApi { async completeMediaMultipartUpload( teamId: TeamID, uploadSessionId: UploadSessionID, - completeMediaMultipartUpload: CompleteMediaMultipartUpload, + body: CompleteMediaMultipartUpload, initOverrides?: RequestInit, ): Promise { const response = await this.completeMediaMultipartUploadResult( - { teamId, uploadSessionId, completeMediaMultipartUpload }, + { teamId, uploadSessionId, body }, initOverrides, ) if (response.status === 200) { @@ -2080,12 +2078,12 @@ export class DefaultApi { } if ( - requestParameters["presignMediaUploadParts"] === null || - requestParameters["presignMediaUploadParts"] === undefined + requestParameters["body"] === null || + requestParameters["body"] === undefined ) { throw new RequiredError( - "presignMediaUploadParts", - 'Required parameter "presignMediaUploadParts" was null or undefined when calling presignMediaUploadParts().', + "body", + 'Required parameter "body" was null or undefined when calling presignMediaUploadParts().', ) } @@ -2097,7 +2095,7 @@ export class DefaultApi { { method: "POST", headers: headerParameters, - body: JSON.stringify(requestParameters["presignMediaUploadParts"]), + body: JSON.stringify(requestParameters["body"]), }, ) } @@ -2107,7 +2105,7 @@ export class DefaultApi { initOverrides?: RequestInit, ): Promise< | { status: 201; body: PresignedMediaUploadParts; raw: Response } - | { status: 400; raw: Response } + | { status: 400; body: ValidationErr; raw: Response } | { status: 401; raw: Response } | { status: 404; raw: Response } > { @@ -2143,11 +2141,11 @@ export class DefaultApi { async presignMediaUploadParts( teamId: TeamID, uploadSessionId: UploadSessionID, - presignMediaUploadParts: PresignMediaUploadParts, + body: PresignMediaUploadParts, initOverrides?: RequestInit, ): Promise { const response = await this.presignMediaUploadPartsResult( - { teamId, uploadSessionId, presignMediaUploadParts }, + { teamId, uploadSessionId, body }, initOverrides, ) if (response.status === 201) { @@ -2183,7 +2181,7 @@ export class DefaultApi { initOverrides?: RequestInit, ): Promise< | { status: 200; body: readonly Show[]; raw: Response } - | { status: 400; raw: Response } + | { status: 400; body: ValidationErr; raw: Response } | { status: 401; raw: Response } > { const timedResponse = await this.request( @@ -2235,12 +2233,12 @@ export class DefaultApi { } if ( - requestParameters["createShow"] === null || - requestParameters["createShow"] === undefined + requestParameters["body"] === null || + requestParameters["body"] === undefined ) { throw new RequiredError( - "createShow", - 'Required parameter "createShow" was null or undefined when calling createShow().', + "body", + 'Required parameter "body" was null or undefined when calling createShow().', ) } @@ -2252,7 +2250,7 @@ export class DefaultApi { { method: "POST", headers: headerParameters, - body: JSON.stringify(requestParameters["createShow"]), + body: JSON.stringify(requestParameters["body"]), }, ) } @@ -2262,7 +2260,7 @@ export class DefaultApi { initOverrides?: RequestInit, ): Promise< | { status: 201; body: Show; raw: Response } - | { status: 400; raw: Response } + | { status: 400; body: ValidationErr; raw: Response } | { status: 401; raw: Response } | { status: 403; raw: Response } | { status: 409; raw: Response } @@ -2301,11 +2299,11 @@ export class DefaultApi { async createShow( teamId: TeamID, - createShow: CreateShow, + body: CreateShow, initOverrides?: RequestInit, ): Promise { const response = await this.createShowResult( - { teamId, createShow }, + { teamId, body }, initOverrides, ) if (response.status === 201) { @@ -2336,12 +2334,12 @@ export class DefaultApi { } if ( - requestParameters["updateShow"] === null || - requestParameters["updateShow"] === undefined + requestParameters["body"] === null || + requestParameters["body"] === undefined ) { throw new RequiredError( - "updateShow", - 'Required parameter "updateShow" was null or undefined when calling updateShow().', + "body", + 'Required parameter "body" was null or undefined when calling updateShow().', ) } @@ -2353,7 +2351,7 @@ export class DefaultApi { { method: "PUT", headers: headerParameters, - body: JSON.stringify(requestParameters["updateShow"]), + body: JSON.stringify(requestParameters["body"]), }, ) } @@ -2363,7 +2361,7 @@ export class DefaultApi { initOverrides?: RequestInit, ): Promise< | { status: 200; body: Show; raw: Response } - | { status: 400; raw: Response } + | { status: 400; body: ValidationErr; raw: Response } | { status: 401; raw: Response } > { const timedResponse = await this.request( @@ -2395,11 +2393,11 @@ export class DefaultApi { async updateShow( teamId: TeamID, showId: ShowID, - updateShow: UpdateShow, + body: UpdateShow, initOverrides?: RequestInit, ): Promise { const response = await this.updateShowResult( - { teamId, showId, updateShow }, + { teamId, showId, body }, initOverrides, ) if (response.status === 200) { @@ -2445,7 +2443,7 @@ export class DefaultApi { initOverrides?: RequestInit, ): Promise< | { status: 200; body: readonly Episode[]; raw: Response } - | { status: 400; raw: Response } + | { status: 400; body: ValidationErr; raw: Response } | { status: 401; raw: Response } > { const timedResponse = await this.request( @@ -2511,12 +2509,12 @@ export class DefaultApi { } if ( - requestParameters["createEpisode"] === null || - requestParameters["createEpisode"] === undefined + requestParameters["body"] === null || + requestParameters["body"] === undefined ) { throw new RequiredError( - "createEpisode", - 'Required parameter "createEpisode" was null or undefined when calling createEpisode().', + "body", + 'Required parameter "body" was null or undefined when calling createEpisode().', ) } @@ -2528,7 +2526,7 @@ export class DefaultApi { { method: "POST", headers: headerParameters, - body: JSON.stringify(requestParameters["createEpisode"]), + body: JSON.stringify(requestParameters["body"]), }, ) } @@ -2538,7 +2536,7 @@ export class DefaultApi { initOverrides?: RequestInit, ): Promise< | { status: 201; body: Episode; raw: Response } - | { status: 400; raw: Response } + | { status: 400; body: ValidationErr; raw: Response } | { status: 401; raw: Response } > { const timedResponse = await this.request( @@ -2570,11 +2568,11 @@ export class DefaultApi { async createEpisode( teamId: TeamID, showId: ShowID, - createEpisode: CreateEpisode, + body: CreateEpisode, initOverrides?: RequestInit, ): Promise { const response = await this.createEpisodeResult( - { teamId, showId, createEpisode }, + { teamId, showId, body }, initOverrides, ) if (response.status === 201) { @@ -2615,12 +2613,12 @@ export class DefaultApi { } if ( - requestParameters["updateEpisode"] === null || - requestParameters["updateEpisode"] === undefined + requestParameters["body"] === null || + requestParameters["body"] === undefined ) { throw new RequiredError( - "updateEpisode", - 'Required parameter "updateEpisode" was null or undefined when calling updateEpisode().', + "body", + 'Required parameter "body" was null or undefined when calling updateEpisode().', ) } @@ -2632,7 +2630,7 @@ export class DefaultApi { { method: "PUT", headers: headerParameters, - body: JSON.stringify(requestParameters["updateEpisode"]), + body: JSON.stringify(requestParameters["body"]), }, ) } @@ -2642,7 +2640,7 @@ export class DefaultApi { initOverrides?: RequestInit, ): Promise< | { status: 200; body: Episode; raw: Response } - | { status: 400; raw: Response } + | { status: 400; body: ValidationErr; raw: Response } | { status: 401; raw: Response } > { const timedResponse = await this.request( @@ -2675,11 +2673,11 @@ export class DefaultApi { teamId: TeamID, showId: ShowID, episodeId: EpisodeID, - updateEpisode: UpdateEpisode, + body: UpdateEpisode, initOverrides?: RequestInit, ): Promise { const response = await this.updateEpisodeResult( - { teamId, showId, episodeId, updateEpisode }, + { teamId, showId, episodeId, body }, initOverrides, ) if (response.status === 200) { @@ -2742,7 +2740,7 @@ export class DefaultApi { body: AsyncIterable raw: Response } - | { status: 400; raw: Response } + | { status: 400; body: ValidationErr; raw: Response } | { status: 401; raw: Response } | { status: 403; raw: Response } | { status: 404; raw: Response } @@ -2838,7 +2836,7 @@ export class DefaultApi { initOverrides?: RequestInit, ): Promise< | { status: 200; body: readonly TestEmail[]; raw: Response } - | { status: 400; raw: Response } + | { status: 400; body: ValidationErr; raw: Response } | { status: 404; raw: Response } > { const timedResponse = await this.request( @@ -2905,7 +2903,7 @@ export class DefaultApi { initOverrides?: RequestInit, ): Promise< | { status: 200; body: string; raw: Response } - | { status: 400; raw: Response } + | { status: 400; body: ValidationErr; raw: Response } > { const timedResponse = await this.request( this.renderShowRSSFeedRequest(requestParameters), diff --git a/testdata/golden/public-client-go/client.go b/testdata/golden/public-client-go/client.go index 6c2530c..a435f05 100644 --- a/testdata/golden/public-client-go/client.go +++ b/testdata/golden/public-client-go/client.go @@ -693,6 +693,164 @@ func (c *Client) WatchEvents(ctx context.Context) (*WatchEventsResponse, error) } } +type PatchThingParams struct { + Body *CreateThing +} + +type PatchThingResponse struct { + StatusCode int + Raw *http.Response +} + +func (c *Client) NewPatchThingRequest(ctx context.Context, params PatchThingParams) (*http.Request, error) { + if ctx == nil { + return nil, fmt.Errorf("build PatchThing request: context must not be nil") + } + path := "/optional-json" + + endpoint, err := url.Parse(c.baseURL + path) + if err != nil { + return nil, fmt.Errorf("build PatchThing URL: %w", err) + } + var requestBody io.Reader + if params.Body != nil { + encodedBody, err := json.Marshal(params.Body) + if err != nil { + return nil, fmt.Errorf("encode PatchThing JSON body: %w", err) + } + requestBody = bytes.NewReader(encodedBody) + } + req, err := http.NewRequestWithContext(ctx, "POST", endpoint.String(), requestBody) + if err != nil { + return nil, fmt.Errorf("build PatchThing request: %w", err) + } + req.Header.Set("Accept", "*/*") + if requestBody != nil { + req.Header.Set("Content-Type", "application/json") + } + return req, nil +} + +func (c *Client) PatchThing(ctx context.Context, params PatchThingParams) (*PatchThingResponse, error) { + + req, err := c.NewPatchThingRequest(ctx, params) + if err != nil { + return nil, err + } + responseCtx, lifecycle := c.responseContext(ctx) + req = req.Clone(responseCtx) + keepLifecycle := false + defer func() { + if !keepLifecycle { + lifecycle.close() + } + }() + res, err := c.do(responseCtx, req) + if err != nil { + return nil, fmt.Errorf("execute PatchThing request: %w", err) + } + if res == nil { + return nil, fmt.Errorf("execute PatchThing request: HTTP client returned nil response") + } + + result := &PatchThingResponse{StatusCode: res.StatusCode, Raw: res} + switch res.StatusCode { + case 204: + _ = res.Body.Close() + return result, nil + default: + rawBody, readErr := io.ReadAll(io.LimitReader(res.Body, maxDiagnosticBodyBytes)) + _ = res.Body.Close() + if readErr != nil { + return nil, fmt.Errorf("read unexpected PatchThing response status %d: %w", res.StatusCode, readErr) + } + return nil, &UnexpectedStatusError{ + Method: req.Method, + URL: req.URL.String(), + StatusCode: res.StatusCode, + Body: strings.TrimSpace(string(rawBody)), + } + } +} + +type UploadOptionalMediaParams struct { + Body io.Reader + ContentType string +} + +type UploadOptionalMediaResponse struct { + StatusCode int + Raw *http.Response +} + +func (c *Client) NewUploadOptionalMediaRequest(ctx context.Context, params UploadOptionalMediaParams) (*http.Request, error) { + if ctx == nil { + return nil, fmt.Errorf("build UploadOptionalMedia request: context must not be nil") + } + path := "/optional-raw" + + endpoint, err := url.Parse(c.baseURL + path) + if err != nil { + return nil, fmt.Errorf("build UploadOptionalMedia URL: %w", err) + } + requestBody := params.Body + req, err := http.NewRequestWithContext(ctx, "POST", endpoint.String(), requestBody) + if err != nil { + return nil, fmt.Errorf("build UploadOptionalMedia request: %w", err) + } + req.Header.Set("Accept", "*/*") + if requestBody != nil { + contentType := strings.TrimSpace(params.ContentType) + if contentType == "" { + contentType = "application/octet-stream" + } + req.Header.Set("Content-Type", contentType) + } + return req, nil +} + +func (c *Client) UploadOptionalMedia(ctx context.Context, params UploadOptionalMediaParams) (*UploadOptionalMediaResponse, error) { + + req, err := c.NewUploadOptionalMediaRequest(ctx, params) + if err != nil { + return nil, err + } + responseCtx, lifecycle := c.responseContext(ctx) + req = req.Clone(responseCtx) + keepLifecycle := false + defer func() { + if !keepLifecycle { + lifecycle.close() + } + }() + res, err := c.do(responseCtx, req) + if err != nil { + return nil, fmt.Errorf("execute UploadOptionalMedia request: %w", err) + } + if res == nil { + return nil, fmt.Errorf("execute UploadOptionalMedia request: HTTP client returned nil response") + } + + result := &UploadOptionalMediaResponse{StatusCode: res.StatusCode, Raw: res} + switch res.StatusCode { + case 204: + _ = res.Body.Close() + return result, nil + default: + rawBody, readErr := io.ReadAll(io.LimitReader(res.Body, maxDiagnosticBodyBytes)) + _ = res.Body.Close() + if readErr != nil { + return nil, fmt.Errorf("read unexpected UploadOptionalMedia response status %d: %w", res.StatusCode, readErr) + } + return nil, &UnexpectedStatusError{ + Method: req.Method, + URL: req.URL.String(), + StatusCode: res.StatusCode, + Body: strings.TrimSpace(string(rawBody)), + } + } +} + type CreateThingParams struct { ThingId string Tag *string @@ -737,20 +895,17 @@ func (c *Client) NewCreateThingRequest(ctx context.Context, params CreateThingPa } } endpoint.RawQuery = query.Encode() - var requestBody io.Reader encodedBody, err := json.Marshal(params.Body) if err != nil { return nil, fmt.Errorf("encode CreateThing JSON body: %w", err) } - requestBody = bytes.NewReader(encodedBody) + requestBody := bytes.NewReader(encodedBody) req, err := http.NewRequestWithContext(ctx, "POST", endpoint.String(), requestBody) if err != nil { return nil, fmt.Errorf("build CreateThing request: %w", err) } req.Header.Set("Accept", "application/json") - if requestBody != nil { - req.Header.Set("Content-Type", "application/json") - } + req.Header.Set("Content-Type", "application/json") req.Header.Set("x-request-id", fmt.Sprint(params.XRequestId)) return req, nil } @@ -797,6 +952,9 @@ func (c *Client) CreateThing(ctx context.Context, params CreateThingParams) (*Cr _ = res.Body.Close() result.Status400 = &decoded return result, nil + case 401, 403: + _ = res.Body.Close() + return result, nil default: rawBody, readErr := io.ReadAll(io.LimitReader(res.Body, maxDiagnosticBodyBytes)) _ = res.Body.Close()