1package jsonschema
2
3import (
4 "fmt"
5 "io/fs"
6 gopath "path"
7 "path/filepath"
8 "reflect"
9 "strings"
10
11 "go/ast"
12 "go/doc"
13 "go/parser"
14 "go/token"
15)
16
17type commentOptions struct {
18 fullObjectText bool // use the first sentence only?
19}
20
21// CommentOption allows for special configuration options when preparing Go
22// source files for comment extraction.
23type CommentOption func(*commentOptions)
24
25// WithFullComment will configure the comment extraction to process to use an
26// object type's full comment text instead of just the synopsis.
27func WithFullComment() CommentOption {
28 return func(o *commentOptions) {
29 o.fullObjectText = true
30 }
31}
32
33// AddGoComments will update the reflectors comment map with all the comments
34// found in the provided source directories including sub-directories, in order to
35// generate a dictionary of comments associated with Types and Fields. The results
36// will be added to the `Reflect.CommentMap` ready to use with Schema "description"
37// fields.
38//
39// The `go/parser` library is used to extract all the comments and unfortunately doesn't
40// have a built-in way to determine the fully qualified name of a package. The `base`
41// parameter, the URL used to import that package, is thus required to be able to match
42// reflected types.
43//
44// When parsing type comments, by default we use the `go/doc`'s Synopsis method to extract
45// the first phrase only. Field comments, which tend to be much shorter, will include everything.
46// This behavior can be changed by using the `WithFullComment` option.
47func (r *Reflector) AddGoComments(base, path string, opts ...CommentOption) error {
48 if r.CommentMap == nil {
49 r.CommentMap = make(map[string]string)
50 }
51 co := new(commentOptions)
52 for _, opt := range opts {
53 opt(co)
54 }
55
56 return r.extractGoComments(base, path, r.CommentMap, co)
57}
58
59func (r *Reflector) extractGoComments(base, path string, commentMap map[string]string, opts *commentOptions) error {
60 fset := token.NewFileSet()
61 dict := make(map[string][]*ast.Package)
62 err := filepath.Walk(path, func(path string, info fs.FileInfo, err error) error {
63 if err != nil {
64 return err
65 }
66 if info.IsDir() {
67 d, err := parser.ParseDir(fset, path, nil, parser.ParseComments)
68 if err != nil {
69 return err
70 }
71 for _, v := range d {
72 // paths may have multiple packages, like for tests
73 k := gopath.Join(base, path)
74 dict[k] = append(dict[k], v)
75 }
76 }
77 return nil
78 })
79 if err != nil {
80 return err
81 }
82
83 for pkg, p := range dict {
84 for _, f := range p {
85 gtxt := ""
86 typ := ""
87 ast.Inspect(f, func(n ast.Node) bool {
88 switch x := n.(type) {
89 case *ast.TypeSpec:
90 typ = x.Name.String()
91 if !ast.IsExported(typ) {
92 typ = ""
93 } else {
94 txt := x.Doc.Text()
95 if txt == "" && gtxt != "" {
96 txt = gtxt
97 gtxt = ""
98 }
99 if !opts.fullObjectText {
100 txt = doc.Synopsis(txt)
101 }
102 commentMap[fmt.Sprintf("%s.%s", pkg, typ)] = strings.TrimSpace(txt)
103 }
104 case *ast.Field:
105 txt := x.Doc.Text()
106 if txt == "" {
107 txt = x.Comment.Text()
108 }
109 if typ != "" && txt != "" {
110 for _, n := range x.Names {
111 if ast.IsExported(n.String()) {
112 k := fmt.Sprintf("%s.%s.%s", pkg, typ, n)
113 commentMap[k] = strings.TrimSpace(txt)
114 }
115 }
116 }
117 case *ast.GenDecl:
118 // remember for the next type
119 gtxt = x.Doc.Text()
120 }
121 return true
122 })
123 }
124 }
125
126 return nil
127}
128
129func (r *Reflector) lookupComment(t reflect.Type, name string) string {
130 if r.LookupComment != nil {
131 if comment := r.LookupComment(t, name); comment != "" {
132 return comment
133 }
134 }
135
136 if r.CommentMap == nil {
137 return ""
138 }
139
140 n := fullyQualifiedTypeName(t)
141 if name != "" {
142 n = n + "." + name
143 }
144
145 return r.CommentMap[n]
146}