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
93 changes: 61 additions & 32 deletions docparse/jsonschema.go
Original file line number Diff line number Diff line change
Expand Up @@ -430,38 +430,8 @@ start:

// Maps
case *ast.MapType:
// As far as I can find there is no obvious/elegant way to represent
// this in JSON schema, so it's just an object.
p.Type = "object"
vtyp, vpkg, err := findTypeIdent(typ.Value, pkg)
if err != nil {
// we cannot find a mapping to a concrete type,
// so we cannot define the type of the maps -> ?
dbg("ERR FOUND MapType: %s", err.Error())
return &p, nil
}
if generics != nil && generics[vtyp.Name] != "" {
vtyp.Name = generics[vtyp.Name]
}
if isPrimitive(vtyp.Name) {
// we are done, no need for a lookup of a custom type
if vtyp.Name != "any" {
p.AdditionalProperties = &Schema{Type: JSONSchemaType(vtyp.Name)}
}
return &p, nil
}

_, lref, err := lookupTypeAndRef(ref.File, vpkg, vtyp.Name)
if err == nil {
// found additional properties
p.AdditionalProperties = &Schema{Reference: lref}
// Make sure the reference is added to `prog.References`:
_, err := GetReference(prog, ref.Context, false, lref, ref.File)
if err != nil {
dbg("ERR, Could not find additionalProperties Reference: %s", err.Error())
}
} else {
dbg("ERR, Could not find additionalProperties: %s", err.Error())
if err := resolveMap(prog, ref, pkg, &p, typ, generics); err != nil {
return nil, err
}
return &p, nil

Expand Down Expand Up @@ -751,6 +721,65 @@ func lookupTypeAndRef(file, pkg, name string) (string, string, error) {
return t, sRef, nil
}

// resolveMap fills p with an `object` schema describing a Go map. Where we can
// identify the value type it's attached as `additionalProperties`; otherwise p
// is left as an open object (what Swagger 2 gives us in the absence of better
// information). Slice value types are handled explicitly so that e.g.
// `map[K][]T` doesn't lose its element type.
func resolveMap(
prog *Program,
ref Reference,
pkg string,
p *Schema,
typ *ast.MapType,
generics map[string]string,
) error {
p.Type = "object"

switch v := dropTypePointers(typ.Value).(type) {
case *ast.ArrayType:
items := &Schema{Type: "array"}
if err := resolveArray(prog, ref, pkg, items, v.Elt, false, generics); err != nil {
return fmt.Errorf("resolveMap resolveArray: %v", err)
}
p.AdditionalProperties = items
return nil
case *ast.MapType:
inner := &Schema{}
if err := resolveMap(prog, ref, pkg, inner, v, generics); err != nil {
return fmt.Errorf("resolveMap nested: %v", err)
}
p.AdditionalProperties = inner
return nil
}

vtyp, vpkg, err := findTypeIdent(typ.Value, pkg)
if err != nil {
dbg("ERR FOUND MapType: %s", err.Error())
return nil
}
if generics != nil && generics[vtyp.Name] != "" {
vtyp.Name = generics[vtyp.Name]
}
if isPrimitive(JSONSchemaType(vtyp.Name)) {
if vtyp.Name != "any" {
p.AdditionalProperties = &Schema{Type: JSONSchemaType(vtyp.Name)}
}
return nil
}

_, lref, err := lookupTypeAndRef(ref.File, vpkg, vtyp.Name)
if err != nil {
dbg("ERR, Could not find additionalProperties: %s", err.Error())
return nil
}
p.AdditionalProperties = &Schema{Reference: lref}
if _, err := GetReference(prog, ref.Context, false, lref, ref.File); err != nil {
dbg("ERR, Could not find additionalProperties Reference: %s", err.Error())
}
return nil
}

func resolveArray(
prog *Program,
ref Reference,
Expand Down
31 changes: 18 additions & 13 deletions openapi2/openapi2.go
Original file line number Diff line number Diff line change
Expand Up @@ -514,26 +514,31 @@ func appendIfNotExists(xs []string, y string) []string {
func prefixPropertyReferences(properties map[string]*docparse.Schema, getRef func(string) string) {
var rm []string
for k, s := range properties {
if s.Reference != "" {
s.Reference = getRef(s.Reference)
}
if s.Items != nil && s.Items.Reference != "" {
s.Items.Reference = getRef(s.Items.Reference)
}
if s.AdditionalProperties != nil && s.AdditionalProperties.Reference != "" {
s.AdditionalProperties.Reference = getRef(s.AdditionalProperties.Reference)
}
prefixSchemaReferences(s, getRef)

if s.OmitDoc {
rm = append(rm, k)
}

if s.Properties != nil {
prefixPropertyReferences(s.Properties, getRef)
}
}

for _, r := range rm {
delete(properties, r)
}
}

// prefixSchemaReferences rewrites all `$ref` strings inside a schema (including
// nested items, additionalProperties, and properties) to their fully-qualified
// `#/definitions/...` form.
func prefixSchemaReferences(s *docparse.Schema, getRef func(string) string) {
if s == nil {
return
}
if s.Reference != "" {
s.Reference = getRef(s.Reference)
}
prefixSchemaReferences(s.Items, getRef)
prefixSchemaReferences(s.AdditionalProperties, getRef)
if s.Properties != nil {
prefixPropertyReferences(s.Properties, getRef)
}
}
15 changes: 10 additions & 5 deletions testdata/openapi2/src/struct-map/in.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,16 @@ package path
import "struct-map/otherpkg"

type resp struct {
Basic map[string]interface{} `json:"basic"` // Basic comment.
Basic2 map[string]any `json:"basic2"` // Basic2 comment.
Custom myMap `json:"custom"` // Custom comment.
Struct aStruct `json:"aStruct"` // Struct comment.
OtherStruct otherpkg.OtherStruct `json:"otherStruct"` // OtherStruct comment.
Basic map[string]interface{} `json:"basic"` // Basic comment.
Basic2 map[string]any `json:"basic2"` // Basic2 comment.
Custom myMap `json:"custom"` // Custom comment.
Struct aStruct `json:"aStruct"` // Struct comment.
OtherStruct otherpkg.OtherStruct `json:"otherStruct"` // OtherStruct comment.
PrimSlices map[string][]string `json:"primSlices"` // Map of primitive slices.
StructSlice map[int64][]aStruct `json:"structSlice"` // Map of local struct slices.
OtherSlice map[string][]otherpkg.OtherStruct `json:"otherSlice"` // Map of cross-package struct slices.
NestedPrim map[string]map[string]int `json:"nestedPrim"` // Nested map of primitives.
NestedMap map[string]map[string]otherpkg.OtherStruct `json:"nestedMap"` // Nested map of cross-package structs.
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the same happens for a map of maps but honestly I don't know if it should be addressed.
maps are too opened for web apis and feel lazy

Suggested change
}
OtherMap map[string]map[string]otherpkg.OtherStruct `json:"otherMap"` // Other map comment.
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added nested map support there, although you'd be best positioned to know how it might impact performance .


// Comments are lost here as its just in the doc as an object.
Expand Down
39 changes: 39 additions & 0 deletions testdata/openapi2/src/struct-map/want.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,16 @@ definitions:
map:
description: Map contains some random data :)
type: object
additionalProperties:
type: object
struct-map.aStruct:
title: aStruct
type: object
properties:
bar:
type: object
additionalProperties:
type: object
foo:
type: string
struct-map.resp:
Expand All @@ -51,5 +55,40 @@ definitions:
type: object
additionalProperties:
type: string
nestedMap:
description: Nested map of cross-package structs.
type: object
additionalProperties:
type: object
additionalProperties:
$ref: '#/definitions/otherpkg.OtherStruct'
nestedPrim:
description: Nested map of primitives.
type: object
additionalProperties:
type: object
additionalProperties:
type: integer
otherSlice:
description: Map of cross-package struct slices.
type: object
additionalProperties:
type: array
items:
$ref: '#/definitions/otherpkg.OtherStruct'
otherStruct:
$ref: '#/definitions/otherpkg.OtherStruct'
primSlices:
description: Map of primitive slices.
type: object
additionalProperties:
type: array
items:
type: string
structSlice:
description: Map of local struct slices.
type: object
additionalProperties:
type: array
items:
$ref: '#/definitions/struct-map.aStruct'
Loading