Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,70 @@ func (s *ErrorResponse) encodeFields(e *jx.Encoder) {
- Special values: `null`, empty strings (`""`), zero values (`0`, `false`)
- Complex types: `object`, `array` (when specified in schema)

## Multiple Response Content Types

For responses with multiple possible content types, a parameter is automatically added to represent the HTTP Accept header.
If desired, the generated server can use this parameter to pick a response format, and the generated client can use this parameter to set the Accept header.
Existing parameters remain untouched, and if a parameter is explicitly defined for the Accept header, no automatic parameter is generated.

Given the following OpenAPI spec:

```yaml
openapi: 3.0.3
paths:
/multipleContentTypes:
get:
operationId: multipleContentTypes
responses:
"200":
description: "OK"
content:
application/octet-stream:
schema:
type: string
format: binary
application/json:
schema:
type: object
properties:
data:
type: string
required:
- "data"
```

The server can handle the Accept header like this:

```go
func (h HandlerImplementation) MultipleContentTypesWithoutParameters(ctx context.Context, params api.MultipleContentTypesParams) (api.MultipleContentTypesRes, error) {
if params.Accept.MatchesContentType(api.MediaTypeApplicationOctetStream) {
return &api.MultipleContentTypesOKApplicationOctetStream{
Data: bytes.NewBufferString("byte content"),
}, nil
} else if params.Accept.MatchesContentType(api.MediaTypeApplicationJSON) {
return &api.MultipleContentTypesOKApplicationJSON{
Data: "json data",
}, nil
} else {
// Local error type which may be used in convenient errors to generate an appropriate response
return nil, errNotAcceptable
}
}
```

And the client can set it like this:

```go
r, err := client.MultipleContentTypes(ctx, api.MultipleContentTypesParams{
Accept: http.AcceptHeaderNew(api.MediaTypeApplicationOctetStream),
})
res, ok := r.(*api.MultipleContentTypesOKApplicationOctetStream)
if !ok {
// Wrong type
}
// Handle content
```

## Extension properties

OpenAPI enables [Specification Extensions](https://spec.openapis.org/oas/v3.1.0#specification-extensions),
Expand Down
81 changes: 81 additions & 0 deletions _testdata/positive/http_responses_accept.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
{
"openapi": "3.0.3",
"info": {
"title": "title",
"version": "v0.1.0"
},
"paths": {
"/multipleContentTypesWithParameters": {
"get": {
"operationId": "multipleContentTypesWithParameters",
"parameters": [
{
"name": "q",
"in": "query",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Ok",
"content": {
"application/octet-stream": {
"schema": {
"type": "string",
"format": "binary"
}
},
"application/json": {
"schema": {
"type": "object",
"properties": {
"data": {
"type": "string"
}
},
"required": [
"data"
]
}
}
}
}
}
}
},
"/multipleContentTypesWithoutParameters": {
"get": {
"operationId": "multipleContentTypesWithoutParameters",
"responses": {
"200": {
"description": "Ok",
"content": {
"application/octet-stream": {
"schema": {
"type": "string",
"format": "binary"
}
},
"application/json": {
"schema": {
"type": "object",
"properties": {
"data": {
"type": "string"
}
},
"required": [
"data"
]
}
}
}
}
}
}
}
}
}
11 changes: 11 additions & 0 deletions gen/_template/mediatypes.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{{ define "mediatypes" }}
{{- /*gotype: github.com/ogen-go/ogen/gen.TemplateConfig*/ -}}
{{ template "header" $ }}

const (
{{- range $op := $.MediaTypes }}
MediaType{{ $op.Name }} string = {{ quote $op.Value }}
{{- end }}
)

{{ end }}
51 changes: 51 additions & 0 deletions gen/gen_operation.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (

"github.com/ogen-go/ogen/gen/ir"
"github.com/ogen-go/ogen/internal/xslices"
"github.com/ogen-go/ogen/jsonschema"
"github.com/ogen-go/ogen/openapi"
)

Expand Down Expand Up @@ -67,6 +68,56 @@ func (g *Generator) generateOperation(ctx *genctx, webhookName string, spec *ope
return nil, errors.Wrap(err, "security")
}

// If we are not generating RAW data (no parameter parsing means no parameter structure is available):
if !op.HasRawResponse() {
isAcceptHeader := func(param *ir.Parameter) bool {
return param.Spec.In.Header() && param.Spec.Name == "Accept"
}
// If there is no manual specification of the Accept parameter
if _, ok := xslices.FindFunc(op.Params, isAcceptHeader); !ok {
supportsMultipleMediaTypes := false
// And at least one operation defines multiple media types
for _, statusCode := range spec.Responses.StatusCode {
if len(statusCode.Content) > 1 {
supportsMultipleMediaTypes = true
break
}
}
if supportsMultipleMediaTypes {
mediaTypes := map[string]any{}
for _, statusCode := range spec.Responses.StatusCode {
for mediaType := range statusCode.Content {
mediaTypes[mediaType] = nil
}
}

mediaTypeType, ok := ctx.global.types["AcceptHeader"]
if !ok {
mediaTypeType = &ir.Type{
Doc: "Auto-generated parameter for the Accept header",
Kind: ir.KindStruct,
Name: "ht.AcceptHeader",
}
}

acceptParam := &ir.Parameter{
Name: "Accept",
Type: mediaTypeType,
Spec: &openapi.Parameter{
Name: "Accept",
Description: "Auto-generated parameter for the Accept header",
Schema: &jsonschema.Schema{
Type: jsonschema.String,
},
In: openapi.LocationHeader,
},
}

op.Params = append(op.Params, acceptParam)
}
}
}

return op, nil
}

Expand Down
33 changes: 33 additions & 0 deletions gen/generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ type Generator struct {
defaultOperations []*ir.Operation // Operations without an operation group.
operationGroups []*ir.OperationGroup
webhooks []*ir.Operation
mediaTypes map[ir.ContentType]*ir.MediaType
securities map[string]*ir.Security
tstorage *tstorage
errType *ir.Response
Expand Down Expand Up @@ -109,6 +110,7 @@ func NewGenerator(spec *ogen.Spec, opts Options) (*Generator, error) {
servers: nil,
operations: nil,
webhooks: nil,
mediaTypes: map[ir.ContentType]*ir.MediaType{},
securities: map[string]*ir.Security{},
tstorage: newTStorage(),
errType: nil,
Expand Down Expand Up @@ -203,6 +205,21 @@ func (g *Generator) makeOps(ops []*openapi.Operation) error {
return err
}

// Collect all media types used in responses to generate constants
for _, response := range op.Responses.StatusCode {
for contentType := range response.Contents {
if _, ok := g.mediaTypes[contentType]; !ok {
constantName, err := pascalNonEmpty(string(contentType))
if err != nil {
return errors.Wrap(err, "gather media types")
}
g.mediaTypes[contentType] = &ir.MediaType{
Name: constantName,
Value: contentType,
}
}
}
}
g.operations = append(g.operations, op)
}

Expand Down Expand Up @@ -298,6 +315,12 @@ func sortOperations(ops []*ir.Operation) {
})
}

func sortMediaTypes(types []*ir.MediaType) {
slices.SortStableFunc(types, func(a, b *ir.MediaType) int {
return strings.Compare(a.Name, b.Name)
})
}

func groupOperations(ops []*ir.Operation) (
defaultOperations []*ir.Operation,
operationGroups []*ir.OperationGroup,
Expand Down Expand Up @@ -336,6 +359,16 @@ func (g *Generator) Operations() []*ir.Operation {
return g.operations
}

// MediaTypes returns generated media type constants.
func (g *Generator) MediaTypes() []*ir.MediaType {
mediaTypesSorted := make([]*ir.MediaType, 0, len(g.mediaTypes))
for _, mt := range g.mediaTypes {
mediaTypesSorted = append(mediaTypesSorted, mt)
}
sortMediaTypes(mediaTypesSorted)
return mediaTypesSorted
}

// Webhooks returns generated webhooks.
func (g *Generator) Webhooks() []*ir.Operation {
return g.webhooks
Expand Down
5 changes: 5 additions & 0 deletions gen/ir/operation.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ type OperationGroup struct {
Operations []*Operation
}

type MediaType struct {
Name string // Generated constant name
Value ContentType // Actual media type, e.g. application/xml
}

// OTELAttribute represents OpenTelemetry attribute defined by otelogen package.
type OTELAttribute struct {
// Key is a name of the attribute constructor in otelogen package.
Expand Down
3 changes: 3 additions & 0 deletions gen/write.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ type TemplateConfig struct {
DefaultOperations []*ir.Operation
OperationGroups []*ir.OperationGroup
Webhooks []*ir.Operation
MediaTypes []*ir.MediaType
Types map[string]*ir.Type
Interfaces map[string]*ir.Type
Error *ir.Response
Expand Down Expand Up @@ -261,6 +262,7 @@ func (g *Generator) WriteSource(fs FileSystem, pkgName string) error {
DefaultOperations: g.defaultOperations,
OperationGroups: g.operationGroups,
Webhooks: g.webhooks,
MediaTypes: g.MediaTypes(),
Types: types,
Interfaces: interfaces,
Error: g.errType,
Expand Down Expand Up @@ -344,6 +346,7 @@ func (g *Generator) WriteSource(fs FileSystem, pkgName string) error {
{"unimplemented", features.Has(OgenUnimplemented) && genServer},
{"labeler", features.Has(OgenOtel) && genServer},
{"operations", (genClient || genServer)},
{"mediatypes", (genClient || genServer)},
} {
if !t.enabled {
continue
Expand Down
Loading