From e90f06d77c27a0ac620cf6f2388da4b6f1d1478b Mon Sep 17 00:00:00 2001 From: Jamie Tanna Date: Fri, 4 Aug 2023 09:59:23 +0100 Subject: [PATCH] Fix: Generate models for all JSON media types As highlighted in #1168, we have some cases where JSON-compatible media types aren't having their respective models generated, as we previously only looked for `application/json`. To resolve this, we can make sure that any media types that are compatible with JSON (via our utility method) can have their types generated. This is a fix for #1168, which could also be deemed a feature. However, as this has broken in the upgrade from v1.12.x to v1.13.x, this is being treated as a feature. Closes #1168. --- internal/test/issues/issue-1168/api.gen.go | 162 ++++++++++++++++++ internal/test/issues/issue-1168/doc.go | 3 + .../test/issues/issue-1168/server.config.yaml | 5 + internal/test/issues/issue-1168/spec.yaml | 95 ++++++++++ pkg/codegen/codegen.go | 40 +++-- 5 files changed, 292 insertions(+), 13 deletions(-) create mode 100644 internal/test/issues/issue-1168/api.gen.go create mode 100644 internal/test/issues/issue-1168/doc.go create mode 100644 internal/test/issues/issue-1168/server.config.yaml create mode 100644 internal/test/issues/issue-1168/spec.yaml diff --git a/internal/test/issues/issue-1168/api.gen.go b/internal/test/issues/issue-1168/api.gen.go new file mode 100644 index 000000000..e83f9be28 --- /dev/null +++ b/internal/test/issues/issue-1168/api.gen.go @@ -0,0 +1,162 @@ +// Package issue1168 provides primitives to interact with the openapi HTTP API. +// +// Code generated by github.com/deepmap/oapi-codegen version (devel) DO NOT EDIT. +package issue1168 + +import ( + "encoding/json" + "fmt" +) + +// ProblemDetails defines model for ProblemDetails. +type ProblemDetails struct { + // Detail A human readable explanation specific to this occurrence of the problem. + Detail *string `json:"detail,omitempty"` + + // Instance An absolute URI that identifies the specific occurrence of the problem. It may or may not yield further information if dereferenced. + Instance *string `json:"instance,omitempty"` + + // Status The HTTP status code generated by the origin server for this occurrence of the problem. + Status *int32 `json:"status,omitempty"` + + // Title A short, summary of the problem type. Written in english and readable for engineers (usually not suited for non technical stakeholders and not localized); example: Service Unavailable + Title *string `json:"title,omitempty"` + + // Type An absolute URI that identifies the problem type. When dereferenced, it SHOULD provide human-readable documentation for the problem type (e.g., using HTML). + Type *string `json:"type,omitempty"` + AdditionalProperties map[string]interface{} `json:"-"` +} + +// Misc400Error defines model for Misc400Error. +type Misc400Error = ProblemDetails + +// Misc404Error defines model for Misc404Error. +type Misc404Error = ProblemDetails + +// Getter for additional properties for ProblemDetails. Returns the specified +// element and whether it was found +func (a ProblemDetails) Get(fieldName string) (value interface{}, found bool) { + if a.AdditionalProperties != nil { + value, found = a.AdditionalProperties[fieldName] + } + return +} + +// Setter for additional properties for ProblemDetails +func (a *ProblemDetails) Set(fieldName string, value interface{}) { + if a.AdditionalProperties == nil { + a.AdditionalProperties = make(map[string]interface{}) + } + a.AdditionalProperties[fieldName] = value +} + +// Override default JSON handling for ProblemDetails to handle AdditionalProperties +func (a *ProblemDetails) UnmarshalJSON(b []byte) error { + object := make(map[string]json.RawMessage) + err := json.Unmarshal(b, &object) + if err != nil { + return err + } + + if raw, found := object["detail"]; found { + err = json.Unmarshal(raw, &a.Detail) + if err != nil { + return fmt.Errorf("error reading 'detail': %w", err) + } + delete(object, "detail") + } + + if raw, found := object["instance"]; found { + err = json.Unmarshal(raw, &a.Instance) + if err != nil { + return fmt.Errorf("error reading 'instance': %w", err) + } + delete(object, "instance") + } + + if raw, found := object["status"]; found { + err = json.Unmarshal(raw, &a.Status) + if err != nil { + return fmt.Errorf("error reading 'status': %w", err) + } + delete(object, "status") + } + + if raw, found := object["title"]; found { + err = json.Unmarshal(raw, &a.Title) + if err != nil { + return fmt.Errorf("error reading 'title': %w", err) + } + delete(object, "title") + } + + if raw, found := object["type"]; found { + err = json.Unmarshal(raw, &a.Type) + if err != nil { + return fmt.Errorf("error reading 'type': %w", err) + } + delete(object, "type") + } + + if len(object) != 0 { + a.AdditionalProperties = make(map[string]interface{}) + for fieldName, fieldBuf := range object { + var fieldVal interface{} + err := json.Unmarshal(fieldBuf, &fieldVal) + if err != nil { + return fmt.Errorf("error unmarshaling field %s: %w", fieldName, err) + } + a.AdditionalProperties[fieldName] = fieldVal + } + } + return nil +} + +// Override default JSON handling for ProblemDetails to handle AdditionalProperties +func (a ProblemDetails) MarshalJSON() ([]byte, error) { + var err error + object := make(map[string]json.RawMessage) + + if a.Detail != nil { + object["detail"], err = json.Marshal(a.Detail) + if err != nil { + return nil, fmt.Errorf("error marshaling 'detail': %w", err) + } + } + + if a.Instance != nil { + object["instance"], err = json.Marshal(a.Instance) + if err != nil { + return nil, fmt.Errorf("error marshaling 'instance': %w", err) + } + } + + if a.Status != nil { + object["status"], err = json.Marshal(a.Status) + if err != nil { + return nil, fmt.Errorf("error marshaling 'status': %w", err) + } + } + + if a.Title != nil { + object["title"], err = json.Marshal(a.Title) + if err != nil { + return nil, fmt.Errorf("error marshaling 'title': %w", err) + } + } + + if a.Type != nil { + object["type"], err = json.Marshal(a.Type) + if err != nil { + return nil, fmt.Errorf("error marshaling 'type': %w", err) + } + } + + for fieldName, field := range a.AdditionalProperties { + object[fieldName], err = json.Marshal(field) + if err != nil { + return nil, fmt.Errorf("error marshaling '%s': %w", fieldName, err) + } + } + return json.Marshal(object) +} diff --git a/internal/test/issues/issue-1168/doc.go b/internal/test/issues/issue-1168/doc.go new file mode 100644 index 000000000..9916b7b85 --- /dev/null +++ b/internal/test/issues/issue-1168/doc.go @@ -0,0 +1,3 @@ +package issue1168 + +//go:generate go run github.com/deepmap/oapi-codegen/cmd/oapi-codegen --config=server.config.yaml spec.yaml diff --git a/internal/test/issues/issue-1168/server.config.yaml b/internal/test/issues/issue-1168/server.config.yaml new file mode 100644 index 000000000..7d70ed842 --- /dev/null +++ b/internal/test/issues/issue-1168/server.config.yaml @@ -0,0 +1,5 @@ +package: issue1168 +output: + api.gen.go +generate: + models: true diff --git a/internal/test/issues/issue-1168/spec.yaml b/internal/test/issues/issue-1168/spec.yaml new file mode 100644 index 000000000..72af58325 --- /dev/null +++ b/internal/test/issues/issue-1168/spec.yaml @@ -0,0 +1,95 @@ +--- +# Copyright 2023 Coros, Corp. All Rights Reserved. +# +# Apache 2.0 Licensed; the portion included from +# https://github.com/zalando/problem/ is MIT licensed +# +openapi: 3.0.3 +info: + version: 1.0.0 + title: Test + description: Provides access to things + license: + name: Apache License 2.0 + url: http://www.example.com/XXX + +servers: + - url: https://www.example.com/api + description: Production environment + +paths: + /v1/test: + get: + summary: Get it + responses: + '200': + description: Successful response + content: + text/plain: + type: string + '400': + $ref: '#/components/responses/Misc400Error' + '404': + $ref: '#/components/responses/Misc404Error' + +components: + responses: + Misc400Error: + description: Bad request + content: + application/problem+json: + schema: + $ref: '#/components/schemas/ProblemDetails' + Misc404Error: + description: Not found + content: + application/problem+json: + schema: + $ref: '#/components/schemas/ProblemDetails' + + schemas: + # Fetched from https://opensource.zalando.com/problem/schema.yaml + # and slightly modified. + # Part of https://github.com/zalando/problem/; MIT License + ProblemDetails: + type: object + additionalProperties: true + properties: + type: + type: string + format: uri + description: >- + An absolute URI that identifies the problem type. When dereferenced, + it SHOULD provide human-readable documentation for the problem type + (e.g., using HTML). + default: 'about:blank' + example: 'https://zalando.github.io/problem/constraint-violation' + title: + type: string + description: >- + A short, summary of the problem type. Written in english and readable + for engineers (usually not suited for non technical stakeholders and + not localized); example: Service Unavailable + status: + type: integer + format: int32 + description: >- + The HTTP status code generated by the origin server for this occurrence + of the problem. + minimum: 100 + maximum: 600 + exclusiveMaximum: true + example: 503 + detail: + type: string + description: >- + A human readable explanation specific to this occurrence of the + problem. + example: Connection to database timed out + instance: + type: string + format: uri + description: >- + An absolute URI that identifies the specific occurrence of the problem. + It may or may not yield further information if dereferenced. + # diff --git a/pkg/codegen/codegen.go b/pkg/codegen/codegen.go index 2c3f0d02d..45891aadf 100644 --- a/pkg/codegen/codegen.go +++ b/pkg/codegen/codegen.go @@ -28,6 +28,7 @@ import ( "strings" "text/template" + "github.com/deepmap/oapi-codegen/pkg/util" "github.com/getkin/kin-openapi/openapi3" "golang.org/x/tools/imports" ) @@ -540,12 +541,15 @@ func GenerateTypesForResponses(t *template.Template, responses openapi3.Response responseOrRef := responses[responseName] // We have to generate the response object. We're only going to - // handle application/json media types here. Other responses should + // handle media types that conform to JSON. Other responses should // simply be specified as strings or byte arrays. response := responseOrRef.Value - jsonResponse, found := response.Content["application/json"] - if found { - goType, err := GenerateGoSchema(jsonResponse.Schema, []string{responseName}) + for mediaType, response := range response.Content { + if !util.IsMediaTypeJson(mediaType) { + continue + } + + goType, err := GenerateGoSchema(response.Schema, []string{responseName}) if err != nil { return nil, fmt.Errorf("error generating Go type for schema in response %s: %w", responseName, err) } @@ -569,6 +573,7 @@ func GenerateTypesForResponses(t *template.Template, responses openapi3.Response } typeDef.TypeName = SchemaNameToTypeName(refType) } + types = append(types, typeDef) } } @@ -586,9 +591,12 @@ func GenerateTypesForRequestBodies(t *template.Template, bodies map[string]*open // As for responses, we will only generate Go code for JSON bodies, // the other body formats are up to the user. response := requestBodyRef.Value - jsonBody, found := response.Content["application/json"] - if found { - goType, err := GenerateGoSchema(jsonBody.Schema, []string{requestBodyName}) + for mediaType, body := range response.Content { + if !util.IsMediaTypeJson(mediaType) { + continue + } + + goType, err := GenerateGoSchema(body.Schema, []string{requestBodyName}) if err != nil { return nil, fmt.Errorf("error generating Go type for schema in body %s: %w", requestBodyName, err) } @@ -1048,9 +1056,12 @@ func GetRequestBodiesImports(bodies map[string]*openapi3.RequestBodyRef) (map[st res := map[string]goImport{} for _, r := range bodies { response := r.Value - jsonBody, found := response.Content["application/json"] - if found { - imprts, err := GoSchemaImports(jsonBody.Schema) + for mediaType, body := range response.Content { + if !util.IsMediaTypeJson(mediaType) { + continue + } + + imprts, err := GoSchemaImports(body.Schema) if err != nil { return nil, err } @@ -1064,9 +1075,12 @@ func GetResponsesImports(responses map[string]*openapi3.ResponseRef) (map[string res := map[string]goImport{} for _, r := range responses { response := r.Value - jsonResponse, found := response.Content["application/json"] - if found { - imprts, err := GoSchemaImports(jsonResponse.Schema) + for mediaType, body := range response.Content { + if !util.IsMediaTypeJson(mediaType) { + continue + } + + imprts, err := GoSchemaImports(body.Schema) if err != nil { return nil, err }