|
| 1 | +// Copyright 2020 The prometheus-operator Authors and the FoundationDB project 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 | + |
| 15 | +package main |
| 16 | + |
| 17 | +import ( |
| 18 | + "bytes" |
| 19 | + "fmt" |
| 20 | + "go/ast" |
| 21 | + "go/doc" |
| 22 | + "go/parser" |
| 23 | + "go/token" |
| 24 | + "reflect" |
| 25 | + "strings" |
| 26 | +) |
| 27 | + |
| 28 | +const ( |
| 29 | + firstParagraph = `<br> |
| 30 | +# API Docs |
| 31 | +This Document documents the types introduced by the FoundationDB Operator to be consumed by users. |
| 32 | +> Note this document is generated from code comments. When contributing a change to this document please do so by changing the code comments.` |
| 33 | +) |
| 34 | + |
| 35 | +var ( |
| 36 | + links = map[string]string{ |
| 37 | + "metav1.ObjectMeta": "https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.17/#objectmeta-v1-meta", |
| 38 | + "metav1.ListMeta": "https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.17/#listmeta-v1-meta", |
| 39 | + "metav1.LabelSelector": "https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.17/#labelselector-v1-meta", |
| 40 | + "corev1.ResourceRequirements": "https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.17/#resourcerequirements-v1-core", |
| 41 | + "corev1.LocalObjectReference": "https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.17/#localobjectreference-v1-core", |
| 42 | + "corev1.SecretKeySelector": "https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.17/#secretkeyselector-v1-core", |
| 43 | + "corev1.PersistentVolumeClaim": "https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.17/#persistentvolumeclaim-v1-core", |
| 44 | + "corev1.EmptyDirVolumeSource": "https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.17/#emptydirvolumesource-v1-core", |
| 45 | + "corev1.Container": "https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.17/#container-v1-core", |
| 46 | + "corev1.PodSecurityContext": "https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.17/#podsecuritycontext-v1-core", |
| 47 | + "corev1.SecurityContext": "https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.17/#securitycontext-v1-core", |
| 48 | + "corev1.EnvVar": "https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.17/#envvar-v1-core", |
| 49 | + "corev1.VolumeMount": "https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.17/#volumemount-v1-core", |
| 50 | + "corev1.PodTemplateSpec": "https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.17/#podtemplatespec-v1-core", |
| 51 | + "corev1.ConfigMap": "https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.17/#configmap-v1-core", |
| 52 | + } |
| 53 | + |
| 54 | + selfLinks = map[string]string{} |
| 55 | +) |
| 56 | + |
| 57 | +func toSectionLink(name string) string { |
| 58 | + name = strings.ToLower(name) |
| 59 | + name = strings.Replace(name, " ", "-", -1) |
| 60 | + return name |
| 61 | +} |
| 62 | + |
| 63 | +func printTOC(types []KubeTypes) { |
| 64 | + fmt.Printf("\n## Table of Contents\n") |
| 65 | + for _, t := range types { |
| 66 | + strukt := t[0] |
| 67 | + if len(t) > 1 { |
| 68 | + fmt.Printf("* [%s](#%s)\n", strukt.Name, toSectionLink(strukt.Name)) |
| 69 | + } |
| 70 | + } |
| 71 | +} |
| 72 | + |
| 73 | +func printAPIDocs(paths []string) { |
| 74 | + fmt.Println(firstParagraph) |
| 75 | + |
| 76 | + types := ParseDocumentationFrom(paths) |
| 77 | + for _, t := range types { |
| 78 | + strukt := t[0] |
| 79 | + selfLinks[strukt.Name] = "#" + strings.ToLower(strukt.Name) |
| 80 | + } |
| 81 | + |
| 82 | + // we need to parse once more to now add the self links |
| 83 | + types = ParseDocumentationFrom(paths) |
| 84 | + |
| 85 | + printTOC(types) |
| 86 | + |
| 87 | + for _, t := range types { |
| 88 | + strukt := t[0] |
| 89 | + if len(t) > 1 { |
| 90 | + fmt.Printf("\n## %s\n\n%s\n\n", strukt.Name, strukt.Doc) |
| 91 | + |
| 92 | + fmt.Println("| Field | Description | Scheme | Required |") |
| 93 | + fmt.Println("| ----- | ----------- | ------ | -------- |") |
| 94 | + fields := t[1:(len(t))] |
| 95 | + for _, f := range fields { |
| 96 | + fmt.Println("|", f.Name, "|", f.Doc, "|", f.Type, "|", f.Mandatory, "|") |
| 97 | + } |
| 98 | + fmt.Println("") |
| 99 | + fmt.Println("[Back to TOC](#table-of-contents)") |
| 100 | + } |
| 101 | + } |
| 102 | +} |
| 103 | + |
| 104 | +// Pair of strings. We keed the name of fields and the doc |
| 105 | +type Pair struct { |
| 106 | + Name, Doc, Type string |
| 107 | + Mandatory bool |
| 108 | +} |
| 109 | + |
| 110 | +// KubeTypes is an array to represent all available types in a parsed file. [0] is for the type itself |
| 111 | +type KubeTypes []Pair |
| 112 | + |
| 113 | +// ParseDocumentationFrom gets all types' documentation and returns them as an |
| 114 | +// array. Each type is again represented as an array (we have to use arrays as we |
| 115 | +// need to be sure for the order of the fields). This function returns fields and |
| 116 | +// struct definitions that have no documentation as {name, ""}. |
| 117 | +func ParseDocumentationFrom(srcs []string) []KubeTypes { |
| 118 | + var docForTypes []KubeTypes |
| 119 | + |
| 120 | + for _, src := range srcs { |
| 121 | + pkg := astFrom(src) |
| 122 | + |
| 123 | + for _, kubType := range pkg.Types { |
| 124 | + if structType, ok := kubType.Decl.Specs[0].(*ast.TypeSpec).Type.(*ast.StructType); ok { |
| 125 | + var ks KubeTypes |
| 126 | + ks = append(ks, Pair{kubType.Name, fmtRawDoc(kubType.Doc), "", false}) |
| 127 | + |
| 128 | + for _, field := range structType.Fields.List { |
| 129 | + typeString := fieldType(field.Type) |
| 130 | + fieldMandatory := fieldRequired(field) |
| 131 | + if n := fieldName(field); n != "-" { |
| 132 | + fieldDoc := fmtRawDoc(field.Doc.Text()) |
| 133 | + ks = append(ks, Pair{n, fieldDoc, typeString, fieldMandatory}) |
| 134 | + } |
| 135 | + } |
| 136 | + docForTypes = append(docForTypes, ks) |
| 137 | + } |
| 138 | + } |
| 139 | + } |
| 140 | + |
| 141 | + return docForTypes |
| 142 | +} |
| 143 | + |
| 144 | +func astFrom(filePath string) *doc.Package { |
| 145 | + fset := token.NewFileSet() |
| 146 | + m := make(map[string]*ast.File) |
| 147 | + |
| 148 | + f, err := parser.ParseFile(fset, filePath, nil, parser.ParseComments) |
| 149 | + if err != nil { |
| 150 | + fmt.Println(err) |
| 151 | + return nil |
| 152 | + } |
| 153 | + |
| 154 | + m[filePath] = f |
| 155 | + apkg, _ := ast.NewPackage(fset, m, nil, nil) |
| 156 | + |
| 157 | + return doc.New(apkg, "", 0) |
| 158 | +} |
| 159 | + |
| 160 | +func fmtRawDoc(rawDoc string) string { |
| 161 | + var buffer bytes.Buffer |
| 162 | + delPrevChar := func() { |
| 163 | + if buffer.Len() > 0 { |
| 164 | + buffer.Truncate(buffer.Len() - 1) // Delete the last " " or "\n" |
| 165 | + } |
| 166 | + } |
| 167 | + |
| 168 | + // Ignore all lines after --- |
| 169 | + rawDoc = strings.Split(rawDoc, "---")[0] |
| 170 | + |
| 171 | + for _, line := range strings.Split(rawDoc, "\n") { |
| 172 | + line = strings.TrimRight(line, " ") |
| 173 | + leading := strings.TrimLeft(line, " ") |
| 174 | + switch { |
| 175 | + case len(line) == 0: // Keep paragraphs |
| 176 | + delPrevChar() |
| 177 | + buffer.WriteString("\n\n") |
| 178 | + case strings.HasPrefix(leading, "TODO"): // Ignore one line TODOs |
| 179 | + case strings.HasPrefix(leading, "+"): // Ignore instructions to go2idl |
| 180 | + default: |
| 181 | + if strings.HasPrefix(line, " ") || strings.HasPrefix(line, "\t") { |
| 182 | + delPrevChar() |
| 183 | + line = "\n" + line + "\n" // Replace it with newline. This is useful when we have a line with: "Example:\n\tJSON-someting..." |
| 184 | + } else { |
| 185 | + line += " " |
| 186 | + } |
| 187 | + buffer.WriteString(line) |
| 188 | + } |
| 189 | + } |
| 190 | + |
| 191 | + postDoc := strings.TrimRight(buffer.String(), "\n") |
| 192 | + postDoc = strings.Replace(postDoc, "\\\"", "\"", -1) // replace user's \" to " |
| 193 | + postDoc = strings.Replace(postDoc, "\"", "\\\"", -1) // Escape " |
| 194 | + postDoc = strings.Replace(postDoc, "\n", "\\n", -1) |
| 195 | + postDoc = strings.Replace(postDoc, "\t", "\\t", -1) |
| 196 | + postDoc = strings.Replace(postDoc, "|", "\\|", -1) |
| 197 | + |
| 198 | + return postDoc |
| 199 | +} |
| 200 | + |
| 201 | +func toLink(typeName string) string { |
| 202 | + selfLink, hasSelfLink := selfLinks[typeName] |
| 203 | + if hasSelfLink { |
| 204 | + return wrapInLink(typeName, selfLink) |
| 205 | + } |
| 206 | + |
| 207 | + link, hasLink := links[typeName] |
| 208 | + if hasLink { |
| 209 | + return wrapInLink(typeName, link) |
| 210 | + } |
| 211 | + |
| 212 | + return typeName |
| 213 | +} |
| 214 | + |
| 215 | +func wrapInLink(text, link string) string { |
| 216 | + return fmt.Sprintf("[%s](%s)", text, link) |
| 217 | +} |
| 218 | + |
| 219 | +// fieldName returns the name of the field as it should appear in JSON format |
| 220 | +// "-" indicates that this field is not part of the JSON representation |
| 221 | +func fieldName(field *ast.Field) string { |
| 222 | + jsonTag := "" |
| 223 | + if field.Tag != nil { |
| 224 | + jsonTag = reflect.StructTag(field.Tag.Value[1 : len(field.Tag.Value)-1]).Get("json") // Delete first and last quotation |
| 225 | + if strings.Contains(jsonTag, "inline") { |
| 226 | + return "-" |
| 227 | + } |
| 228 | + } |
| 229 | + |
| 230 | + jsonTag = strings.Split(jsonTag, ",")[0] // This can return "-" |
| 231 | + if jsonTag == "" { |
| 232 | + if field.Names != nil { |
| 233 | + return field.Names[0].Name |
| 234 | + } |
| 235 | + return field.Type.(*ast.Ident).Name |
| 236 | + } |
| 237 | + return jsonTag |
| 238 | +} |
| 239 | + |
| 240 | +// fieldRequired returns whether a field is a required field. |
| 241 | +func fieldRequired(field *ast.Field) bool { |
| 242 | + jsonTag := "" |
| 243 | + if field.Tag != nil { |
| 244 | + jsonTag = reflect.StructTag(field.Tag.Value[1 : len(field.Tag.Value)-1]).Get("json") // Delete first and last quotation |
| 245 | + return !strings.Contains(jsonTag, "omitempty") |
| 246 | + } |
| 247 | + |
| 248 | + return false |
| 249 | +} |
| 250 | + |
| 251 | +func fieldType(typ ast.Expr) string { |
| 252 | + switch typ.(type) { |
| 253 | + case *ast.Ident: |
| 254 | + return toLink(typ.(*ast.Ident).Name) |
| 255 | + case *ast.StarExpr: |
| 256 | + return "*" + toLink(fieldType(typ.(*ast.StarExpr).X)) |
| 257 | + case *ast.SelectorExpr: |
| 258 | + e := typ.(*ast.SelectorExpr) |
| 259 | + pkg := e.X.(*ast.Ident) |
| 260 | + t := e.Sel |
| 261 | + return toLink(pkg.Name + "." + t.Name) |
| 262 | + case *ast.ArrayType: |
| 263 | + return "[]" + toLink(fieldType(typ.(*ast.ArrayType).Elt)) |
| 264 | + case *ast.MapType: |
| 265 | + mapType := typ.(*ast.MapType) |
| 266 | + return "map[" + toLink(fieldType(mapType.Key)) + "]" + toLink(fieldType(mapType.Value)) |
| 267 | + default: |
| 268 | + return "" |
| 269 | + } |
| 270 | +} |
0 commit comments