Skip to content

Commit

Permalink
read comments from fields of anonymous structs
Browse files Browse the repository at this point in the history
  • Loading branch information
arvidfm committed Jul 11, 2022
1 parent d9664d9 commit 7e14961
Show file tree
Hide file tree
Showing 2 changed files with 100 additions and 85 deletions.
120 changes: 66 additions & 54 deletions comment_extractor.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,31 +2,43 @@ package jsonschema

import (
"fmt"
"io/fs"
gopath "path"
"path/filepath"
"strings"

"go/ast"
"go/doc"
"go/parser"
"go/token"
"io/fs"
gopath "path"
"path/filepath"
"strings"
)

// ExtractGoComments will read all the go files contained in the provided path,
// including sub-directories, in order to generate a dictionary of comments
// associated with Types and Fields. The results will be added to the `commentsMap`
// provided in the parameters and expected to be used for Schema "description" fields.
//
// The `go/parser` library is used to extract all the comments and unfortunately doesn't
// have a built-in way to determine the fully qualified name of a package. The `base` paremeter,
// the URL used to import that package, is thus required to be able to match reflected types.
//
// When parsing type comments, we use the `go/doc`'s Synopsis method to extract the first phrase
// only. Field comments, which tend to be much shorter, will include everything.
func ExtractGoComments(base, path string, commentMap map[string]string) error {
func handleType(expr ast.Expr, breadcrumb string, comments map[string]string) {
switch t := expr.(type) {
case *ast.StructType:
for _, field := range t.Fields.List {
for _, name := range field.Names {
if !ast.IsExported(name.Name) {
continue
}

b := fmt.Sprintf("%s.%s", breadcrumb, name.Name)
comments[b] = strings.TrimSpace(field.Doc.Text())
handleType(field.Type, b, comments)
}
}
case *ast.ArrayType:
handleType(t.Elt, fmt.Sprintf("%s.[]", breadcrumb), comments)
case *ast.MapType:
handleType(t.Key, fmt.Sprintf("%s.[key]", breadcrumb), comments)
handleType(t.Value, fmt.Sprintf("%s.[value]", breadcrumb), comments)
case *ast.StarExpr:
handleType(t.X, breadcrumb, comments)
}
}

func getPackages(base, path string) (map[string]*ast.Package, error) {
fset := token.NewFileSet()
dict := make(map[string][]*ast.Package)
dict := make(map[string]*ast.Package)
err := filepath.Walk(path, func(path string, info fs.FileInfo, err error) error {
if err != nil {
return err
Expand All @@ -36,55 +48,55 @@ func ExtractGoComments(base, path string, commentMap map[string]string) error {
if err != nil {
return err
}
for _, v := range d {
// paths may have multiple packages, like for tests
k := gopath.Join(base, path)
dict[k] = append(dict[k], v)
for pkgName, v := range d {
k := gopath.Join(base, gopath.Dir(path), pkgName)
dict[k] = v
}
}
return nil
})
return dict, err
}

// ExtractGoComments will read all the go files contained in the provided path,
// including sub-directories, in order to generate a dictionary of comments
// associated with Types and Fields. The results will be added to the `commentsMap`
// provided in the parameters and expected to be used for Schema "description" fields.
//
// The `go/parser` library is used to extract all the comments and unfortunately doesn't
// have a built-in way to determine the fully qualified name of a package. The `base` paremeter,
// the URL used to import that package, is thus required to be able to match reflected types.
//
// When parsing type comments, we use the `go/doc`'s Synopsis method to extract the first phrase
// only. Field comments, which tend to be much shorter, will include everything.
func ExtractGoComments(base, path string, commentMap map[string]string) error {
pkgs, err := getPackages(base, path)
if err != nil {
return err
}

for pkg, p := range dict {
for _, f := range p {
gtxt := ""
typ := ""
ast.Inspect(f, func(n ast.Node) bool {
switch x := n.(type) {
case *ast.TypeSpec:
typ = x.Name.String()
if !ast.IsExported(typ) {
typ = ""
} else {
txt := x.Doc.Text()
if txt == "" && gtxt != "" {
txt = gtxt
gtxt = ""
}
txt = doc.Synopsis(txt)
commentMap[fmt.Sprintf("%s.%s", pkg, typ)] = strings.TrimSpace(txt)
}
case *ast.Field:
txt := x.Doc.Text()
if typ != "" && txt != "" {
for _, n := range x.Names {
if ast.IsExported(n.String()) {
k := fmt.Sprintf("%s.%s.%s", pkg, typ, n)
commentMap[k] = strings.TrimSpace(txt)
for qualifiedName, pkg := range pkgs {
for _, file := range pkg.Files {
for _, decl := range file.Decls {
if d, ok := decl.(*ast.GenDecl); ok {
for _, spec := range d.Specs {
if s, ok := spec.(*ast.TypeSpec); ok {
if !ast.IsExported(s.Name.Name) {
continue
}

breadcrumb := fmt.Sprintf("%s.%s", qualifiedName, s.Name.Name)
txt := s.Doc.Text()
if txt == "" {
txt = d.Doc.Text()
}
commentMap[breadcrumb] = strings.TrimSpace(doc.Synopsis(txt))
handleType(s.Type, breadcrumb, commentMap)
}
}
case *ast.GenDecl:
// remember for the next type
gtxt = x.Doc.Text()
}
return true
})
}
}
}

return nil
}
65 changes: 34 additions & 31 deletions reflect.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ package jsonschema
import (
"bytes"
"encoding/json"
"fmt"
"net"
"net/url"
"reflect"
Expand Down Expand Up @@ -234,7 +235,7 @@ func (r *Reflector) ReflectFromType(t reflect.Type) *Schema {
s := new(Schema)
definitions := Definitions{}
s.Definitions = definitions
bs := r.reflectTypeToSchemaWithID(definitions, t)
bs := r.reflectTypeToSchemaWithID(definitions, t, "")
if r.ExpandedStruct {
*s = *definitions[name]
delete(definitions, name)
Expand Down Expand Up @@ -297,7 +298,7 @@ func (r *Reflector) SetBaseSchemaID(id string) {
r.BaseSchemaID = ID(id)
}

func (r *Reflector) refOrReflectTypeToSchema(definitions Definitions, t reflect.Type) *Schema {
func (r *Reflector) refOrReflectTypeToSchema(definitions Definitions, t reflect.Type, breadcrumb string) *Schema {
id := r.lookupID(t)
if id != EmptyID {
return &Schema{
Expand All @@ -310,11 +311,11 @@ func (r *Reflector) refOrReflectTypeToSchema(definitions Definitions, t reflect.
return def
}

return r.reflectTypeToSchemaWithID(definitions, t)
return r.reflectTypeToSchemaWithID(definitions, t, breadcrumb)
}

func (r *Reflector) reflectTypeToSchemaWithID(defs Definitions, t reflect.Type) *Schema {
s := r.reflectTypeToSchema(defs, t)
func (r *Reflector) reflectTypeToSchemaWithID(defs Definitions, t reflect.Type, breadcrumb string) *Schema {
s := r.reflectTypeToSchema(defs, t, breadcrumb)
if s != nil {
if r.Lookup != nil {
id := r.Lookup(t)
Expand All @@ -326,10 +327,10 @@ func (r *Reflector) reflectTypeToSchemaWithID(defs Definitions, t reflect.Type)
return s
}

func (r *Reflector) reflectTypeToSchema(definitions Definitions, t reflect.Type) *Schema {
func (r *Reflector) reflectTypeToSchema(definitions Definitions, t reflect.Type, breadcrumb string) *Schema {
// only try to reflect non-pointers
if t.Kind() == reflect.Ptr {
return r.refOrReflectTypeToSchema(definitions, t.Elem())
return r.refOrReflectTypeToSchema(definitions, t.Elem(), breadcrumb)
}

// Do any pre-definitions exist?
Expand Down Expand Up @@ -365,15 +366,20 @@ func (r *Reflector) reflectTypeToSchema(definitions Definitions, t reflect.Type)
return st
}

if t.Name() != "" {
// reset breadcrumb for non-anonymous types
breadcrumb = fullyQualifiedTypeName(t)
}

switch t.Kind() {
case reflect.Struct:
r.reflectStruct(definitions, t, st)
r.reflectStruct(definitions, t, st, breadcrumb)

case reflect.Slice, reflect.Array:
r.reflectSliceOrArray(definitions, t, st)
r.reflectSliceOrArray(definitions, t, st, breadcrumb)

case reflect.Map:
r.reflectMap(definitions, t, st)
r.reflectMap(definitions, t, st, breadcrumb)

case reflect.Interface:
// empty
Expand Down Expand Up @@ -422,15 +428,15 @@ func (r *Reflector) reflectCustomSchema(definitions Definitions, t reflect.Type)
return nil
}

func (r *Reflector) reflectSliceOrArray(definitions Definitions, t reflect.Type, st *Schema) {
func (r *Reflector) reflectSliceOrArray(definitions Definitions, t reflect.Type, st *Schema, breadcrumb string) {
if t == rawMessageType {
return
}

r.addDefinition(definitions, t, st)

if st.Description == "" {
st.Description = r.lookupComment(t, "")
st.Description = r.lookupComment(breadcrumb)
}

if t.Kind() == reflect.Array {
Expand All @@ -443,35 +449,36 @@ func (r *Reflector) reflectSliceOrArray(definitions Definitions, t reflect.Type,
st.ContentEncoding = "base64"
} else {
st.Type = "array"
st.Items = r.refOrReflectTypeToSchema(definitions, t.Elem())
st.Items = r.refOrReflectTypeToSchema(definitions, t.Elem(), fmt.Sprintf("%s.[]", breadcrumb))
}
}

func (r *Reflector) reflectMap(definitions Definitions, t reflect.Type, st *Schema) {
func (r *Reflector) reflectMap(definitions Definitions, t reflect.Type, st *Schema, breadcrumb string) {
r.addDefinition(definitions, t, st)

st.Type = "object"
if st.Description == "" {
st.Description = r.lookupComment(t, "")
st.Description = r.lookupComment(breadcrumb)
}

valueBreadcrumb := fmt.Sprintf("%s.[value]", breadcrumb)
switch t.Key().Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
st.PatternProperties = map[string]*Schema{
"^[0-9]+$": r.refOrReflectTypeToSchema(definitions, t.Elem()),
"^[0-9]+$": r.refOrReflectTypeToSchema(definitions, t.Elem(), valueBreadcrumb),
}
st.AdditionalProperties = FalseSchema
return
}
if t.Elem().Kind() != reflect.Interface {
st.PatternProperties = map[string]*Schema{
".*": r.refOrReflectTypeToSchema(definitions, t.Elem()),
".*": r.refOrReflectTypeToSchema(definitions, t.Elem(), valueBreadcrumb),
}
}
}

// Reflects a struct to a JSON Schema type.
func (r *Reflector) reflectStruct(definitions Definitions, t reflect.Type, s *Schema) {
func (r *Reflector) reflectStruct(definitions Definitions, t reflect.Type, s *Schema, breadcrumb string) {
// Handle special types
switch t {
case timeType: // date-time RFC section 7.3.1
Expand All @@ -487,7 +494,7 @@ func (r *Reflector) reflectStruct(definitions Definitions, t reflect.Type, s *Sc
r.addDefinition(definitions, t, s)
s.Type = "object"
s.Properties = orderedmap.New()
s.Description = r.lookupComment(t, "")
s.Description = r.lookupComment(breadcrumb)
if r.AssignAnchor {
s.Anchor = t.Name()
}
Expand All @@ -503,11 +510,11 @@ func (r *Reflector) reflectStruct(definitions Definitions, t reflect.Type, s *Sc
}
}
if !ignored {
r.reflectStructFields(s, definitions, t)
r.reflectStructFields(s, definitions, t, breadcrumb)
}
}

func (r *Reflector) reflectStructFields(st *Schema, definitions Definitions, t reflect.Type) {
func (r *Reflector) reflectStructFields(st *Schema, definitions Definitions, t reflect.Type, breadcrumb string) {
if t.Kind() == reflect.Ptr {
t = t.Elem()
}
Expand All @@ -528,15 +535,16 @@ func (r *Reflector) reflectStructFields(st *Schema, definitions Definitions, t r
// current type should inherit properties of anonymous one
if name == "" {
if shouldEmbed {
r.reflectStructFields(st, definitions, f.Type)
r.reflectStructFields(st, definitions, f.Type, fullyQualifiedTypeName(f.Type))
}
return
}

property := r.refOrReflectTypeToSchema(definitions, f.Type)
fieldBreadcrumb := fmt.Sprintf("%s.%s", breadcrumb, f.Name)
property := r.refOrReflectTypeToSchema(definitions, f.Type, fieldBreadcrumb)
property.structKeywordsFromTags(f, st, name)
if property.Description == "" {
property.Description = r.lookupComment(t, f.Name)
property.Description = r.lookupComment(fieldBreadcrumb)
}
if getFieldDocString != nil {
property.Description = getFieldDocString(f.Name)
Expand Down Expand Up @@ -572,17 +580,12 @@ func (r *Reflector) reflectStructFields(st *Schema, definitions Definitions, t r
}
}

func (r *Reflector) lookupComment(t reflect.Type, name string) string {
func (r *Reflector) lookupComment(breadcrumb string) string {
if r.CommentMap == nil {
return ""
}

n := fullyQualifiedTypeName(t)
if name != "" {
n = n + "." + name
}

return r.CommentMap[n]
return r.CommentMap[breadcrumb]
}

// addDefinition will append the provided schema. If needed, an ID and anchor will also be added.
Expand Down

0 comments on commit 7e14961

Please sign in to comment.