diff --git a/docparse/jsonschema.go b/docparse/jsonschema.go index 9c7d45a..77f263d 100644 --- a/docparse/jsonschema.go +++ b/docparse/jsonschema.go @@ -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 @@ -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, diff --git a/openapi2/openapi2.go b/openapi2/openapi2.go index 2ab0d08..ac3b30e 100644 --- a/openapi2/openapi2.go +++ b/openapi2/openapi2.go @@ -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) + } +} diff --git a/testdata/openapi2/src/struct-map/in.go b/testdata/openapi2/src/struct-map/in.go index ec3ebd1..05e5576 100644 --- a/testdata/openapi2/src/struct-map/in.go +++ b/testdata/openapi2/src/struct-map/in.go @@ -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. } // Comments are lost here as its just in the doc as an object. diff --git a/testdata/openapi2/src/struct-map/want.yaml b/testdata/openapi2/src/struct-map/want.yaml index a2ea06a..095e8fe 100644 --- a/testdata/openapi2/src/struct-map/want.yaml +++ b/testdata/openapi2/src/struct-map/want.yaml @@ -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: @@ -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'