| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292 |
- // Copyright 2021 the Kilo authors
- //
- // Licensed under the Apache License, Version 2.0 (the "License");
- // you may not use this file except in compliance with the License.
- // You may obtain a copy of the License at
- //
- // http://www.apache.org/licenses/LICENSE-2.0
- //
- // Unless required by applicable law or agreed to in writing, software
- // distributed under the License is distributed on an "AS IS" BASIS,
- // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- // See the License for the specific language governing permissions and
- // limitations under the License.
- // This file was adapted from https://github.com/prometheus-operator/prometheus-operator/blob/master/cmd/po-docgen/api.go.
- package main
- import (
- "bytes"
- "fmt"
- "go/ast"
- "go/doc"
- "go/parser"
- "go/token"
- "os"
- "reflect"
- "strings"
- )
- const (
- firstParagraph = `# API
- This document is a reference of the API types introduced by Kilo.
- > **Note**: this document is generated from code comments. When contributing a change to this document, please do so by changing the code comments.`
- )
- var (
- links = map[string]string{
- "metav1.ObjectMeta": "https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.21/#objectmeta-v1-meta",
- "metav1.ListMeta": "https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.21/#listmeta-v1-meta",
- }
- selfLinks = map[string]string{}
- typesDoc = map[string]KubeTypes{}
- )
- func toSectionLink(name string) string {
- name = strings.ToLower(name)
- name = strings.Replace(name, " ", "-", -1)
- return name
- }
- func printTOC(types []KubeTypes) {
- fmt.Printf("\n## Table of Contents\n")
- for _, t := range types {
- strukt := t[0]
- if len(t) > 1 {
- fmt.Printf("* [%s](#%s)\n", strukt.Name, toSectionLink(strukt.Name))
- }
- }
- }
- func printAPIDocs(paths []string) {
- fmt.Println(firstParagraph)
- types := parseDocumentationFrom(paths)
- for _, t := range types {
- strukt := t[0]
- selfLinks[strukt.Name] = "#" + strings.ToLower(strukt.Name)
- typesDoc[toLink(strukt.Name)] = t[1:]
- }
- // we need to parse once more to now add the self links and the inlined fields
- types = parseDocumentationFrom(paths)
- printTOC(types)
- for _, t := range types {
- strukt := t[0]
- if len(t) > 1 {
- fmt.Printf("\n## %s\n\n%s\n\n", strukt.Name, strukt.Doc)
- fmt.Println("| Field | Description | Scheme | Required |")
- fmt.Println("| ----- | ----------- | ------ | -------- |")
- fields := t[1:]
- for _, f := range fields {
- fmt.Println("|", f.Name, "|", f.Doc, "|", f.Type, "|", f.Mandatory, "|")
- }
- fmt.Println("")
- fmt.Println("[Back to TOC](#table-of-contents)")
- }
- }
- }
- // Pair of strings. We need the name of fields and the doc.
- type Pair struct {
- Name, Doc, Type string
- Mandatory bool
- }
- // KubeTypes is an array to represent all available types in a parsed file. [0] is for the type itself
- type KubeTypes []Pair
- // parseDocumentationFrom gets all types' documentation and returns them as an
- // array. Each type is again represented as an array (we have to use arrays as we
- // need to be sure of the order of the fields). This function returns fields and
- // struct definitions that have no documentation as {name, ""}.
- func parseDocumentationFrom(srcs []string) []KubeTypes {
- var docForTypes []KubeTypes
- for _, src := range srcs {
- pkg := astFrom(src)
- for _, kubType := range pkg.Types {
- if structType, ok := kubType.Decl.Specs[0].(*ast.TypeSpec).Type.(*ast.StructType); ok {
- var ks KubeTypes
- ks = append(ks, Pair{kubType.Name, fmtRawDoc(kubType.Doc), "", false})
- for _, field := range structType.Fields.List {
- // Skip fields that are not tagged.
- if field.Tag == nil {
- os.Stderr.WriteString(fmt.Sprintf("Tag is nil, skipping field: %v of type %v\n", field, field.Type))
- continue
- }
- // Treat inlined fields separately as we don't want the original types to appear in the doc.
- if isInlined(field) {
- // Skip external types, as we don't want their content to be part of the API documentation.
- if isInternalType(field.Type) {
- ks = append(ks, typesDoc[fieldType(field.Type)]...)
- }
- continue
- }
- typeString := fieldType(field.Type)
- fieldMandatory := fieldRequired(field)
- if n := fieldName(field); n != "-" {
- fieldDoc := fmtRawDoc(field.Doc.Text())
- ks = append(ks, Pair{n, fieldDoc, typeString, fieldMandatory})
- }
- }
- docForTypes = append(docForTypes, ks)
- }
- }
- }
- return docForTypes
- }
- func astFrom(filePath string) *doc.Package {
- fset := token.NewFileSet()
- m := make(map[string]*ast.File)
- f, err := parser.ParseFile(fset, filePath, nil, parser.ParseComments)
- if err != nil {
- fmt.Println(err)
- return nil
- }
- m[filePath] = f
- apkg, _ := ast.NewPackage(fset, m, nil, nil)
- return doc.New(apkg, "", 0)
- }
- func fmtRawDoc(rawDoc string) string {
- var buffer bytes.Buffer
- delPrevChar := func() {
- if buffer.Len() > 0 {
- buffer.Truncate(buffer.Len() - 1) // Delete the last " " or "\n"
- }
- }
- // Ignore all lines after ---
- rawDoc = strings.Split(rawDoc, "---")[0]
- for _, line := range strings.Split(rawDoc, "\n") {
- line = strings.TrimRight(line, " ")
- leading := strings.TrimLeft(line, " ")
- switch {
- case len(line) == 0: // Keep paragraphs
- delPrevChar()
- buffer.WriteString("\n\n")
- case strings.HasPrefix(leading, "TODO"): // Ignore one line TODOs
- case strings.HasPrefix(leading, "+"): // Ignore instructions to go2idl
- default:
- if strings.HasPrefix(line, " ") || strings.HasPrefix(line, "\t") {
- delPrevChar()
- line = "\n" + line + "\n" // Replace it with newline. This is useful when we have a line with: "Example:\n\tJSON-someting..."
- } else {
- line += " "
- }
- buffer.WriteString(line)
- }
- }
- postDoc := strings.TrimRight(buffer.String(), "\n")
- postDoc = strings.Replace(postDoc, "\\\"", "\"", -1) // replace user's \" to "
- postDoc = strings.Replace(postDoc, "\"", "\\\"", -1) // Escape "
- postDoc = strings.Replace(postDoc, "\n", "\\n", -1)
- postDoc = strings.Replace(postDoc, "\t", "\\t", -1)
- postDoc = strings.Replace(postDoc, "|", "\\|", -1)
- return postDoc
- }
- func toLink(typeName string) string {
- selfLink, hasSelfLink := selfLinks[typeName]
- if hasSelfLink {
- return wrapInLink(typeName, selfLink)
- }
- link, hasLink := links[typeName]
- if hasLink {
- return wrapInLink(typeName, link)
- }
- return typeName
- }
- func wrapInLink(text, link string) string {
- return fmt.Sprintf("[%s](%s)", text, link)
- }
- func isInlined(field *ast.Field) bool {
- jsonTag := reflect.StructTag(field.Tag.Value[1 : len(field.Tag.Value)-1]).Get("json") // Delete first and last quotation
- return strings.Contains(jsonTag, "inline")
- }
- func isInternalType(typ ast.Expr) bool {
- switch typ := typ.(type) {
- case *ast.SelectorExpr:
- pkg := typ.X.(*ast.Ident)
- return strings.HasPrefix(pkg.Name, "monitoring")
- case *ast.StarExpr:
- return isInternalType(typ.X)
- case *ast.ArrayType:
- return isInternalType(typ.Elt)
- case *ast.MapType:
- return isInternalType(typ.Key) && isInternalType(typ.Value)
- default:
- return true
- }
- }
- // fieldName returns the name of the field as it should appear in JSON format
- // "-" indicates that this field is not part of the JSON representation
- func fieldName(field *ast.Field) string {
- jsonTag := reflect.StructTag(field.Tag.Value[1 : len(field.Tag.Value)-1]).Get("json") // Delete first and last quotation
- jsonTag = strings.Split(jsonTag, ",")[0] // This can return "-"
- if jsonTag == "" {
- if field.Names != nil {
- return field.Names[0].Name
- }
- return field.Type.(*ast.Ident).Name
- }
- return jsonTag
- }
- // fieldRequired returns whether a field is a required field.
- func fieldRequired(field *ast.Field) bool {
- jsonTag := ""
- if field.Tag != nil {
- jsonTag = reflect.StructTag(field.Tag.Value[1 : len(field.Tag.Value)-1]).Get("json") // Delete first and last quotation
- return !strings.Contains(jsonTag, "omitempty")
- }
- return false
- }
- func fieldType(typ ast.Expr) string {
- switch typ := typ.(type) {
- case *ast.Ident:
- return toLink(typ.Name)
- case *ast.StarExpr:
- return "*" + toLink(fieldType(typ.X))
- case *ast.SelectorExpr:
- pkg := typ.X.(*ast.Ident)
- t := typ.Sel
- return toLink(pkg.Name + "." + t.Name)
- case *ast.ArrayType:
- return "[]" + toLink(fieldType(typ.Elt))
- case *ast.MapType:
- return "map[" + toLink(fieldType(typ.Key)) + "]" + toLink(fieldType(typ.Value))
- default:
- return ""
- }
- }
- func main() {
- printAPIDocs(os.Args[1:])
- }
|