diff --git a/docparse/jsonschema.go b/docparse/jsonschema.go index bfc76eb..9c7d45a 100644 --- a/docparse/jsonschema.go +++ b/docparse/jsonschema.go @@ -385,24 +385,19 @@ start: pkg = pkgSel.Name name = typ.Sel - lookup := pkg + "." + name.Name - t, f := MapType(prog, lookup) - if t == "" { - // Only check for canonicalType if this isn't mapped. - canon, err := canonicalType(ref.File, pkgSel.Name, typ.Sel) - if err != nil { - return nil, fmt.Errorf("cannot get canonical type: %v", err) - } - if canon != nil { - sw = canon - goto start - } + // Try map-types with the short package alias first. + if applyMapType(prog, &p, pkg+"."+name.Name) { + return &p, nil } - p.Format = f - if t != "" { - p.Type = JSONSchemaType(t) - return &p, nil + // Only check for canonicalType if this isn't mapped. + canon, err := canonicalType(ref.File, pkgSel.Name, typ.Sel) + if err != nil { + return nil, fmt.Errorf("cannot get canonical type: %v", err) + } + if canon != nil { + sw = canon + goto start } // Deal with array. @@ -416,6 +411,13 @@ start: pkg = importPath } + // Retry map-types with the resolved full import path so + // fully-qualified config keys (e.g. + // `github.com/foo/bar/view.Date`) also match selector references. + if applyMapType(prog, &p, importPath+"."+name.Name) { + return &p, nil + } + if resolvType, ok := ts.Type.(*ast.ArrayType); ok { isEnum := p.Type == "enum" p.Type = "array" @@ -761,6 +763,9 @@ func resolveArray( asw := typ var name *ast.Ident + // importPath, when set, is the fully-qualified import path of the element + // type; used to retry map-types lookups with the full-path key. + var importPath string arrayStart: switch typ := asw.(type) { @@ -803,6 +808,9 @@ arrayStart: // the switch. p.Items.Type = "" name = typ + // Bare ident: the element is declared in ref.Package, so the + // full-path key equals ref.Package.typ.Name. + importPath = ref.Package // "pkg.foo" case *ast.SelectorExpr: @@ -817,12 +825,13 @@ arrayStart: name = typ.Sel // handle import aliases - _, _, importPath, err := findType(ref.File, pkg, name.Name) + _, _, resolved, err := findType(ref.File, pkg, name.Name) if err != nil { return fmt.Errorf("resolveArray: findType: %v", err) } - if !strings.HasSuffix(importPath, pkg) { - pkg = importPath + importPath = resolved + if !strings.HasSuffix(resolved, pkg) { + pkg = resolved } case *ast.MapType: @@ -833,8 +842,26 @@ arrayStart: return fmt.Errorf("fieldToSchema: unknown array type: %T", typ) } - // Check if the type resolves to a Go primitive. + // Honor map-types config for slice elements. Try the pkg-qualified + // lookup first, then the fully-qualified form, so both short keys + // like `view.Date` and fully-qualified keys match. lookup := pkg + "." + name.Name + fullLookup := importPath + "." + name.Name + for _, key := range []string{lookup, fullLookup} { + if key == "." { + continue + } + if mt, mf := MapType(prog, key); mt != "" { + items := &Schema{Type: JSONSchemaType(mt)} + if mf != "" { + items.Format = mf + } + p.Items = items + return nil + } + } + + // Check if the type resolves to a Go primitive. t, err := getTypeInfo(prog, lookup, ref.File) if err != nil { return err diff --git a/docparse/jsonschema_test.go b/docparse/jsonschema_test.go index cf74ac8..7b0047a 100644 --- a/docparse/jsonschema_test.go +++ b/docparse/jsonschema_test.go @@ -75,6 +75,76 @@ func TestFieldToProperty(t *testing.T) { }) } + t.Run("mapped types", func(t *testing.T) { + cases := []struct { + name string + mapTypes map[string]string + want map[string]*Schema + }{ + { + name: "short selector key", + mapTypes: map[string]string{ + "mail.Address": "string", + "a.bar": "string", + }, + want: map[string]*Schema{ + "b": {Type: "string"}, + "bSlice": {Type: "array", Items: &Schema{Type: "string"}}, + "pkg": {Type: "string"}, + "pkgSlice": {Type: "array", Items: &Schema{Type: "string"}}, + }, + }, + { + name: "fully-qualified key", + mapTypes: map[string]string{ + "net/mail.Address": "string", + "a.bar": "string", + }, + want: map[string]*Schema{ + "b": {Type: "string"}, + "bSlice": {Type: "array", Items: &Schema{Type: "string"}}, + "pkg": {Type: "string"}, + "pkgSlice": {Type: "array", Items: &Schema{Type: "string"}}, + }, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + ts, _, _, err := findType("./testdata/src/a/a.go", "a", "mapped") + if err != nil { + t.Fatalf("could not parse file: %v", err) + } + st, ok := ts.Type.(*ast.StructType) + if !ok { + t.Fatal("not a struct?!") + } + + prog := NewProgram(false) + prog.Config.MapTypes = tc.mapTypes + + for _, f := range st.Fields.List { + name := f.Names[0].Name + out, err := fieldToSchema(prog, name, "json", Reference{ + Package: "a", + File: "./testdata/src/a/a.go", + Context: "req", + }, f, nil) + if err != nil { + t.Fatalf("%s: %v", name, err) + } + w, ok := tc.want[name] + if !ok { + t.Fatalf("no expected schema for %s", name) + } + if d := diff.Diff(w, out); d != "" { + t.Errorf("%s: %v", name, d) + } + } + }) + } + }) + t.Run("nested", func(t *testing.T) { prog := NewProgram(false) ts, _, _, err := findType("./testdata/src/a/a.go", "a", "nested") diff --git a/docparse/testdata/src/a/a.go b/docparse/testdata/src/a/a.go index 0984578..a4b0d4a 100644 --- a/docparse/testdata/src/a/a.go +++ b/docparse/testdata/src/a/a.go @@ -44,6 +44,15 @@ type nested struct { deeper refAnother } +// mapped exercises map-types resolution against both bare-ident and +// selector references, in both single-field and slice-element form. +type mapped struct { + b bar + bSlice []bar + pkg mail.Address + pkgSlice []mail.Address +} + type customStrs []customStr type customStr string