Skip to content
Merged
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
3 changes: 3 additions & 0 deletions docparse/docparse.go
Original file line number Diff line number Diff line change
Expand Up @@ -574,6 +574,9 @@ func MapType(prog *Program, in string) (kind, format string) {
if v, ok := prog.Config.MapTypes[in]; ok {
kind = v
}
if kind == "" {
return
}
if v, ok := prog.Config.MapFormats[in]; ok {
format = v
}
Expand Down
66 changes: 66 additions & 0 deletions docparse/jsonschema.go
Original file line number Diff line number Diff line change
Expand Up @@ -475,6 +475,11 @@ start:
default:
return nil, fmt.Errorf("unknown generic type: %T", typ.X)
}
if mapped, err := genericMapTypeLookup(prog, &p, genericsPkg, genericsIdent.Name, ref, typ.Index); err != nil {
return nil, err
} else if mapped {
return &p, nil
}
if err := fillGenericsSchema(prog, &p, tagName, ref, genericsPkg, genericsIdent, generics, typ.Index); err != nil {
return nil, fmt.Errorf("generic fieldToSchema: %v", err)
}
Expand All @@ -497,6 +502,11 @@ start:
default:
return nil, fmt.Errorf("unknown generic type: %T", typ.X)
}
if mapped, err := genericMapTypeLookup(prog, &p, genericsPkg, genericsIdent.Name, ref, typ.Indices...); err != nil {
return nil, err
} else if mapped {
return &p, nil
}
err = fillGenericsSchema(prog, &p, tagName, ref, genericsPkg, genericsIdent, generics, typ.Indices...)
if err != nil {
return nil, fmt.Errorf("generic fieldToSchema: %v", err)
Expand Down Expand Up @@ -534,6 +544,62 @@ start:
return &p, nil
}

// applyMapType checks if key has a map-types entry and, if so, writes the
// mapped type and optional format into p. Returns true when a mapping was found.
func applyMapType(prog *Program, p *Schema, key string) bool {
mappedType, mappedFormat := MapType(prog, key)
if mappedType == "" {
return false
}
p.Type = JSONSchemaType(mappedType)
if mappedFormat != "" {
p.Format = mappedFormat
}
return true
}

// genericMapTypeLookup checks if a generic type has a map-types override in the
// config. It first tries the full instantiated key (e.g. "pkg.Foo[Bar]"), then
// falls back to the main type key (e.g. "pkg.Foo"). Returns true if a mapping
// was found and the schema was populated.
func genericMapTypeLookup(
prog *Program,
p *Schema,
genericsPkg string,
genericsName string,
ref Reference,
indices ...ast.Expr,
) (bool, error) {
mainKey := genericsPkg + "." + genericsName

var args []string
for _, idx := range indices {
arg, argPkg, err := findTypeIdent(idx, ref.Package)
if err != nil {
// Unresolvable type arguments are normal for partial or external
// packages; skip the full-key lookup and fall through to the main key.
args = nil
break
}
if argPkg != ref.Package {
args = append(args, argPkg+"."+arg.Name)
} else {
args = append(args, arg.Name)
}
}

// Try full instantiated key first, e.g. "pkg.Foo[string, Bar]".
if len(args) > 0 {
fullKey := mainKey + "[" + strings.Join(args, ", ") + "]"
if applyMapType(prog, p, fullKey) {
return true, nil
}
}

// Fall back to main type key, e.g. "pkg.Foo".
return applyMapType(prog, p, mainKey), nil
}

// fillGenericsSchema fills the schema with the generic type information. As the
// types can be different for every generics declaration they will need to be a
// anonymous object in the schema output instead of a reusable reference.
Expand Down
31 changes: 31 additions & 0 deletions testdata/openapi2/src/generics-map-types/in.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package generics_map_types

// Wrapper wraps a value with metadata.
type Wrapper[T any] struct {
// Value is the wrapped value.
Value T
// Extra is additional metadata.
Extra string
}

// GroupBy groups results by a field.
type GroupBy[T any] struct {
// Field is the grouping field.
Field T
// Label is the display label.
Label string
}

type reqRef struct {
// FullKey is mapped via full instantiated key Wrapper[string].
FullKey Wrapper[string]
// MainKey is mapped via main type key GroupBy (matches all instantiations).
MainKey GroupBy[int]
// Unmapped is not in map-types so it expands as an object.
Unmapped Wrapper[int]
}

// POST /path
//
// Request body: reqRef
// Response 200: {empty}
9 changes: 9 additions & 0 deletions testdata/openapi2/src/generics-map-types/test.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Map specific generic instantiation to a primitive type + format.
# Full key: "pkg.Type[ArgType]"
map-types
generics-map-types.Wrapper[string] string
generics-map-types.GroupBy string

# Map format for the full key.
map-format
generics-map-types.Wrapper[string] date-time
47 changes: 47 additions & 0 deletions testdata/openapi2/src/generics-map-types/want.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
swagger: "2.0"
info:
title: x
version: x
consumes:
- application/json
produces:
- application/json
paths:
/path:
post:
operationId: POST_path
consumes:
- application/json
produces:
- application/json
parameters:
- name: generics-map-types.reqRef
in: body
required: true
schema:
$ref: '#/definitions/generics-map-types.reqRef'
responses:
200:
description: 200 OK (no data)
definitions:
generics-map-types.reqRef:
title: reqRef
type: object
properties:
FullKey:
description: FullKey is mapped via full instantiated key Wrapper[string].
type: string
format: date-time
MainKey:
description: MainKey is mapped via main type key GroupBy (matches all instantiations).
type: string
Unmapped:
description: Unmapped is not in map-types so it expands as an object.
type: object
properties:
Extra:
description: Extra is additional metadata.
type: string
Value:
description: Value is the wrapped value.
type: integer
Loading