github.com/banzaicloud/operator-tools@v0.28.10/pkg/docgen/docgen.go (about) 1 // Copyright © 2020 Banzai Cloud 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 docgen 16 17 import ( 18 "bytes" 19 "fmt" 20 "go/ast" 21 "go/parser" 22 "go/printer" 23 "go/token" 24 "os" 25 filepath2 "path/filepath" 26 "regexp" 27 "strings" 28 29 "emperror.dev/errors" 30 "github.com/go-logr/logr" 31 ) 32 33 type DocItem struct { 34 Name string 35 SourcePath string 36 DestPath string 37 Category string 38 DefaultValueFromTagExtractor func(string) string 39 } 40 41 type DocItems []DocItem 42 43 type Doc struct { 44 Item DocItem 45 DisplayName string 46 Content string 47 Version string 48 Url string 49 Desc string 50 Status string 51 52 RootNode *ast.File 53 Logger logr.Logger 54 } 55 56 func (d *Doc) Append(line string) { 57 if d != nil { 58 d.Content = d.Content + line + "\n" 59 } 60 } 61 62 func NewDoc(item DocItem, log logr.Logger) *Doc { 63 return &Doc{ 64 Item: item, 65 Logger: log, 66 } 67 } 68 69 func GetDocumentParser(source DocItem, log logr.Logger) *Doc { 70 fileSet := token.NewFileSet() 71 node, err := parser.ParseFile(fileSet, source.SourcePath, nil, parser.ParseComments) 72 if err != nil { 73 log.Error(err, "Error!") 74 } 75 newDoc := &Doc{ 76 Item: source, 77 RootNode: node, 78 Logger: log, 79 } 80 return newDoc 81 } 82 83 func (d *Doc) Generate() error { 84 if d == nil { 85 return nil 86 } 87 if d.RootNode != nil { 88 ast.Inspect(d.RootNode, d.visitNode) 89 d.Logger.V(2).Info("DocumentRoot not present skipping parse") 90 } 91 err := os.MkdirAll(d.Item.DestPath, os.ModePerm) 92 if err != nil { 93 return errors.WrapIf(err, "failed to create destination directory") 94 } 95 filepath := filepath2.Join(d.Item.DestPath, d.Item.Name+".md") 96 f, err := os.Create(filepath) 97 if err != nil { 98 return errors.WrapIf(err, "failed to create destination file") 99 } 100 101 _, err = f.WriteString(d.Content) 102 if err != nil { 103 return errors.WrapIf(errors.WrapIf(f.Close(), "failed to close file"), "failed to write content") 104 } 105 106 return errors.WrapIf(f.Close(), "failed to close file") 107 } 108 109 func (d *Doc) visitNode(n ast.Node) bool { 110 generic, ok := n.(*ast.GenDecl) 111 if ok { 112 typeName, ok := generic.Specs[0].(*ast.TypeSpec) 113 if ok { 114 _, ok := typeName.Type.(*ast.InterfaceType) 115 if ok && strings.HasPrefix(typeName.Name.Name, "_hugo") { 116 d.Append("---") 117 d.Append(fmt.Sprintf("title: %s", GetPrefixedValue(getTypeDocs(generic, true), `\+name:\"(.*)\"`))) 118 d.Append(fmt.Sprintf("weight: %s", GetPrefixedValue(getTypeDocs(generic, true), `\+weight:\"(.*)\"`))) 119 d.Append("generated_file: true") 120 d.Append("---\n") 121 } 122 if ok && strings.HasPrefix(typeName.Name.Name, "_doc") { 123 d.Append(fmt.Sprintf("# %s", getTypeName(generic, d.Item.Name))) 124 d.Append("## Overview") 125 d.Append(getTypeDocs(generic, false)) 126 d.Append("## Configuration") 127 } 128 if ok && strings.HasPrefix(typeName.Name.Name, "_meta") { 129 d.DisplayName = GetPrefixedValue(getTypeDocs(generic, true), `\+name:\"(.*)\"`) 130 d.Url = GetPrefixedValue(getTypeDocs(generic, true), `\+url:\"(.*)\"`) 131 d.Version = GetPrefixedValue(getTypeDocs(generic, true), `\+version:\"(.*)\"`) 132 d.Desc = GetPrefixedValue(getTypeDocs(generic, true), `\+description:\"(.*)\"`) 133 d.Status = GetPrefixedValue(getTypeDocs(generic, true), `\+status:\"(.*)\"`) 134 } 135 if d.DisplayName == "" { 136 d.DisplayName = typeName.Name.Name 137 } 138 if ok && strings.HasPrefix(typeName.Name.Name, "_exp") { 139 d.Append(getTypeDocs(generic, false)) 140 d.Append("---") 141 } 142 structure, ok := typeName.Type.(*ast.StructType) 143 if ok && typeName.Name.IsExported() { 144 d.Append(fmt.Sprintf("## %s", getTypeName(generic, typeName.Name.Name))) 145 d.Append("") // Adds a line-break for markdown formatting 146 if getTypeDocs(generic, true) != "" { 147 d.Append(fmt.Sprintf("%s", getTypeDocs(generic, true))) 148 } 149 for i, item := range structure.Fields.List { 150 name, com, def, required, err := d.getValuesFromItem(item) 151 if err != nil { 152 panic(errors.WrapIff(err, "failed to get values for field #%d for type %s", i, typeName.Name.Name)) 153 } 154 155 required_string := "" 156 if required == "No" { 157 required_string = ", optional" 158 } else if required == "Yes" { 159 required_string = ", required" 160 } 161 162 anchor := strings.ToLower(getTypeName(generic, typeName.Name.Name) + "-" + name) 163 d.Append(fmt.Sprintf("### %s (%s%s) {#%s}", name, d.normaliseType(item.Type), required_string, anchor)) 164 d.Append("") 165 if com != "" { 166 d.Append(fmt.Sprintf("%s", com)) 167 d.Append("") 168 } 169 d.Append(fmt.Sprintf("Default: %s", def)) 170 d.Append("") // Adds a line-break for markdown formatting 171 } 172 d.Append("") // Adds a line-break for markdown formatting 173 } 174 } 175 } 176 177 return true 178 } 179 180 func (d *Doc) normaliseType(fieldType ast.Expr) string { 181 fset := token.NewFileSet() 182 var typeNameBuf bytes.Buffer 183 err := printer.Fprint(&typeNameBuf, fset, fieldType) 184 if err != nil { 185 d.Logger.Error(err, "error getting type") 186 } 187 return typeNameBuf.String() 188 } 189 190 func GetPrefixedValue(origin, expression string) string { 191 r := regexp.MustCompile(expression) 192 result := r.FindStringSubmatch(origin) 193 if len(result) > 1 { 194 return result[1] 195 } 196 return "" 197 } 198 199 func getTypeName(generic *ast.GenDecl, defaultName string) string { 200 structName := generic.Doc.Text() 201 result := GetPrefixedValue(structName, `\+docName:\"(.*)\"`) 202 if result != "" { 203 return result 204 } 205 return defaultName 206 } 207 208 func getTypeDocs(generic *ast.GenDecl, trimSpace bool) string { 209 comment := "" 210 if generic.Doc != nil { 211 for _, line := range generic.Doc.List { 212 newLine := strings.TrimPrefix(line.Text, "//") 213 if trimSpace { 214 newLine = strings.TrimSpace(newLine) 215 } 216 if !strings.HasPrefix(strings.TrimSpace(newLine), "+kubebuilder") && 217 !strings.HasPrefix(strings.TrimSpace(newLine), "nolint") && 218 !strings.HasPrefix(strings.TrimSpace(newLine), "+docName") { 219 comment += newLine + "\n" 220 } 221 } 222 } 223 return comment 224 } 225 226 func getLink(def string) string { 227 result := GetPrefixedValue(def, `\+docLink:\"(.*)\"`) 228 if result != "" { 229 url := strings.Split(result, ",") 230 def = strings.Replace(def, fmt.Sprintf("+docLink:\"%s\"", result), fmt.Sprintf("[%s](%s)", url[0], url[1]), 1) 231 } 232 return def 233 } 234 235 func formatRequired(r bool) string { 236 if r { 237 return "Yes" 238 } 239 return "No" 240 } 241 242 func (d *Doc) getValuesFromItem(item *ast.Field) (name, comment, def, required string, err error) { 243 commentWithDefault := "" 244 if item.Doc != nil { 245 // Process comments of objects that become ### level headings 246 isCodeBlock := false 247 for _, line := range item.Doc.List { 248 newLine := strings.TrimPrefix(line.Text, "//") 249 250 if strings.HasPrefix(newLine, " {{< highlight") { 251 commentWithDefault += "\n" 252 isCodeBlock = true 253 } 254 255 // Do not trim spaces when processing a code block to keep indentation, trim only one character 256 if !(isCodeBlock) { 257 newLine = strings.TrimSpace(newLine) 258 } else { 259 newLine = strings.TrimPrefix(newLine, " ") 260 } 261 262 if !strings.HasPrefix(newLine, "+kubebuilder") { 263 // Keep newlines in code blocks, but join body text 264 if isCodeBlock { 265 commentWithDefault += newLine + "\n" 266 } else { 267 commentWithDefault += newLine + " " 268 } 269 // Detect the end of code blocks 270 if isCodeBlock && strings.HasPrefix(newLine, " {{< /highlight") { 271 isCodeBlock = false 272 } 273 } 274 } 275 } 276 if item.Tag == nil { 277 return "", "", "", "", errors.Errorf("field has no tag defined: %+v", item) 278 } 279 tag := item.Tag.Value 280 tagResult := "" 281 if d.Item.DefaultValueFromTagExtractor != nil { 282 tagResult = d.Item.DefaultValueFromTagExtractor(tag) 283 } 284 nameResult := GetPrefixedValue(tag, `json:\"([^,\"]*).*\"`) 285 required = formatRequired(!strings.Contains(GetPrefixedValue(tag, `json:\"(.*)\"`), "omitempty")) 286 if tagResult != "" { 287 return nameResult, getLink(commentWithDefault), tagResult, required, nil 288 } 289 result := GetPrefixedValue(commentWithDefault, `\(default:(.*)\)`) 290 if result != "" { 291 ignore := fmt.Sprintf("(default:%s)", result) 292 comment = strings.Replace(commentWithDefault, ignore, "", 1) 293 return nameResult, comment, getLink(result), required, nil 294 } 295 296 return nameResult, getLink(commentWithDefault), "-", required, nil 297 }