main.go 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297
  1. // Copyright 2021 the Kilo authors
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. package main
  15. import (
  16. "bytes"
  17. "fmt"
  18. "go/ast"
  19. "go/doc"
  20. "go/parser"
  21. "go/token"
  22. "os"
  23. "reflect"
  24. "strings"
  25. )
  26. const (
  27. firstParagraph = `# API
  28. This document is a reference of the API types introduced by Kilo.
  29. > Note this document is generated from code comments. When contributing a change to this document, please do so by changing the code comments.`
  30. )
  31. var (
  32. links = map[string]string{
  33. "metav1.ObjectMeta": "https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.21/#objectmeta-v1-meta",
  34. "metav1.ListMeta": "https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.21/#listmeta-v1-meta",
  35. "metav1.LabelSelector": "https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.21/#labelselector-v1-meta",
  36. "v1.ResourceRequirements": "https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.21/#resourcerequirements-v1-core",
  37. "v1.LocalObjectReference": "https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.21/#localobjectreference-v1-core",
  38. "v1.SecretKeySelector": "https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.21/#secretkeyselector-v1-core",
  39. "v1.PersistentVolumeClaim": "https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.21/#persistentvolumeclaim-v1-core",
  40. "v1.EmptyDirVolumeSource": "https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.21/#emptydirvolumesource-v1-core",
  41. "apiextensionsv1.JSON": "https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.21/#json-v1-apiextensions-k8s-io",
  42. }
  43. selfLinks = map[string]string{}
  44. typesDoc = map[string]KubeTypes{}
  45. )
  46. func toSectionLink(name string) string {
  47. name = strings.ToLower(name)
  48. name = strings.Replace(name, " ", "-", -1)
  49. return name
  50. }
  51. func printTOC(types []KubeTypes) {
  52. fmt.Printf("\n## Table of Contents\n")
  53. for _, t := range types {
  54. strukt := t[0]
  55. if len(t) > 1 {
  56. fmt.Printf("* [%s](#%s)\n", strukt.Name, toSectionLink(strukt.Name))
  57. }
  58. }
  59. }
  60. func printAPIDocs(paths []string) {
  61. fmt.Println(firstParagraph)
  62. types := ParseDocumentationFrom(paths)
  63. for _, t := range types {
  64. strukt := t[0]
  65. selfLinks[strukt.Name] = "#" + strings.ToLower(strukt.Name)
  66. typesDoc[toLink(strukt.Name)] = t[1:]
  67. }
  68. // we need to parse once more to now add the self links and the inlined fields
  69. types = ParseDocumentationFrom(paths)
  70. printTOC(types)
  71. for _, t := range types {
  72. strukt := t[0]
  73. if len(t) > 1 {
  74. fmt.Printf("\n## %s\n\n%s\n\n", strukt.Name, strukt.Doc)
  75. fmt.Println("| Field | Description | Scheme | Required |")
  76. fmt.Println("| ----- | ----------- | ------ | -------- |")
  77. fields := t[1:]
  78. for _, f := range fields {
  79. fmt.Println("|", f.Name, "|", f.Doc, "|", f.Type, "|", f.Mandatory, "|")
  80. }
  81. fmt.Println("")
  82. fmt.Println("[Back to TOC](#table-of-contents)")
  83. }
  84. }
  85. }
  86. // Pair of strings. We need the name of fields and the doc.
  87. type Pair struct {
  88. Name, Doc, Type string
  89. Mandatory bool
  90. }
  91. // KubeTypes is an array to represent all available types in a parsed file. [0] is for the type itself
  92. type KubeTypes []Pair
  93. // ParseDocumentationFrom gets all types' documentation and returns them as an
  94. // array. Each type is again represented as an array (we have to use arrays as we
  95. // need to be sure for the order of the fields). This function returns fields and
  96. // struct definitions that have no documentation as {name, ""}.
  97. func ParseDocumentationFrom(srcs []string) []KubeTypes {
  98. var docForTypes []KubeTypes
  99. for _, src := range srcs {
  100. pkg := astFrom(src)
  101. for _, kubType := range pkg.Types {
  102. if structType, ok := kubType.Decl.Specs[0].(*ast.TypeSpec).Type.(*ast.StructType); ok {
  103. var ks KubeTypes
  104. ks = append(ks, Pair{kubType.Name, fmtRawDoc(kubType.Doc), "", false})
  105. for _, field := range structType.Fields.List {
  106. // Skip fields that are not tagged.
  107. if field.Tag == nil {
  108. os.Stderr.WriteString(fmt.Sprintf("Tag is nil, skipping field: %v of type %v\n", field, field.Type))
  109. continue
  110. }
  111. // Treat inlined fields separately as we don't want the original types to appear in the doc.
  112. if isInlined(field) {
  113. // Skip external types, as we don't want their content to be part of the API documentation.
  114. if isInternalType(field.Type) {
  115. ks = append(ks, typesDoc[fieldType(field.Type)]...)
  116. }
  117. continue
  118. }
  119. typeString := fieldType(field.Type)
  120. fieldMandatory := fieldRequired(field)
  121. if n := fieldName(field); n != "-" {
  122. fieldDoc := fmtRawDoc(field.Doc.Text())
  123. ks = append(ks, Pair{n, fieldDoc, typeString, fieldMandatory})
  124. }
  125. }
  126. docForTypes = append(docForTypes, ks)
  127. }
  128. }
  129. }
  130. return docForTypes
  131. }
  132. func astFrom(filePath string) *doc.Package {
  133. fset := token.NewFileSet()
  134. m := make(map[string]*ast.File)
  135. f, err := parser.ParseFile(fset, filePath, nil, parser.ParseComments)
  136. if err != nil {
  137. fmt.Println(err)
  138. return nil
  139. }
  140. m[filePath] = f
  141. apkg, _ := ast.NewPackage(fset, m, nil, nil)
  142. return doc.New(apkg, "", 0)
  143. }
  144. func fmtRawDoc(rawDoc string) string {
  145. var buffer bytes.Buffer
  146. delPrevChar := func() {
  147. if buffer.Len() > 0 {
  148. buffer.Truncate(buffer.Len() - 1) // Delete the last " " or "\n"
  149. }
  150. }
  151. // Ignore all lines after ---
  152. rawDoc = strings.Split(rawDoc, "---")[0]
  153. for _, line := range strings.Split(rawDoc, "\n") {
  154. line = strings.TrimRight(line, " ")
  155. leading := strings.TrimLeft(line, " ")
  156. switch {
  157. case len(line) == 0: // Keep paragraphs
  158. delPrevChar()
  159. buffer.WriteString("\n\n")
  160. case strings.HasPrefix(leading, "TODO"): // Ignore one line TODOs
  161. case strings.HasPrefix(leading, "+"): // Ignore instructions to go2idl
  162. default:
  163. if strings.HasPrefix(line, " ") || strings.HasPrefix(line, "\t") {
  164. delPrevChar()
  165. line = "\n" + line + "\n" // Replace it with newline. This is useful when we have a line with: "Example:\n\tJSON-someting..."
  166. } else {
  167. line += " "
  168. }
  169. buffer.WriteString(line)
  170. }
  171. }
  172. postDoc := strings.TrimRight(buffer.String(), "\n")
  173. postDoc = strings.Replace(postDoc, "\\\"", "\"", -1) // replace user's \" to "
  174. postDoc = strings.Replace(postDoc, "\"", "\\\"", -1) // Escape "
  175. postDoc = strings.Replace(postDoc, "\n", "\\n", -1)
  176. postDoc = strings.Replace(postDoc, "\t", "\\t", -1)
  177. postDoc = strings.Replace(postDoc, "|", "\\|", -1)
  178. return postDoc
  179. }
  180. func toLink(typeName string) string {
  181. selfLink, hasSelfLink := selfLinks[typeName]
  182. if hasSelfLink {
  183. return wrapInLink(typeName, selfLink)
  184. }
  185. link, hasLink := links[typeName]
  186. if hasLink {
  187. return wrapInLink(typeName, link)
  188. }
  189. return typeName
  190. }
  191. func wrapInLink(text, link string) string {
  192. return fmt.Sprintf("[%s](%s)", text, link)
  193. }
  194. func isInlined(field *ast.Field) bool {
  195. jsonTag := reflect.StructTag(field.Tag.Value[1 : len(field.Tag.Value)-1]).Get("json") // Delete first and last quotation
  196. return strings.Contains(jsonTag, "inline")
  197. }
  198. func isInternalType(typ ast.Expr) bool {
  199. switch typ := typ.(type) {
  200. case *ast.SelectorExpr:
  201. pkg := typ.X.(*ast.Ident)
  202. return strings.HasPrefix(pkg.Name, "monitoring")
  203. case *ast.StarExpr:
  204. return isInternalType(typ.X)
  205. case *ast.ArrayType:
  206. return isInternalType(typ.Elt)
  207. case *ast.MapType:
  208. return isInternalType(typ.Key) && isInternalType(typ.Value)
  209. default:
  210. return true
  211. }
  212. }
  213. // fieldName returns the name of the field as it should appear in JSON format
  214. // "-" indicates that this field is not part of the JSON representation
  215. func fieldName(field *ast.Field) string {
  216. jsonTag := reflect.StructTag(field.Tag.Value[1 : len(field.Tag.Value)-1]).Get("json") // Delete first and last quotation
  217. jsonTag = strings.Split(jsonTag, ",")[0] // This can return "-"
  218. if jsonTag == "" {
  219. if field.Names != nil {
  220. return field.Names[0].Name
  221. }
  222. return field.Type.(*ast.Ident).Name
  223. }
  224. return jsonTag
  225. }
  226. // fieldRequired returns whether a field is a required field.
  227. func fieldRequired(field *ast.Field) bool {
  228. jsonTag := ""
  229. if field.Tag != nil {
  230. jsonTag = reflect.StructTag(field.Tag.Value[1 : len(field.Tag.Value)-1]).Get("json") // Delete first and last quotation
  231. return !strings.Contains(jsonTag, "omitempty")
  232. }
  233. return false
  234. }
  235. func fieldType(typ ast.Expr) string {
  236. switch typ := typ.(type) {
  237. case *ast.Ident:
  238. return toLink(typ.Name)
  239. case *ast.StarExpr:
  240. return "*" + toLink(fieldType(typ.X))
  241. case *ast.SelectorExpr:
  242. pkg := typ.X.(*ast.Ident)
  243. t := typ.Sel
  244. return toLink(pkg.Name + "." + t.Name)
  245. case *ast.ArrayType:
  246. return "[]" + toLink(fieldType(typ.Elt))
  247. case *ast.MapType:
  248. return "map[" + toLink(fieldType(typ.Key)) + "]" + toLink(fieldType(typ.Value))
  249. default:
  250. return ""
  251. }
  252. }
  253. func main() {
  254. printAPIDocs(os.Args[1:])
  255. }