reflect_comments.go 3.7 KB

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