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
67 changes: 47 additions & 20 deletions docparse/jsonschema.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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"
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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:
Expand All @@ -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:
Expand All @@ -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
Expand Down
70 changes: 70 additions & 0 deletions docparse/jsonschema_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
9 changes: 9 additions & 0 deletions docparse/testdata/src/a/a.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading