cmd/gen-docs/main.go: auto generate docs for CRD
The new make command `make gen-docs` is introduced. It will build a markdown file from the CRD introduced by Kilo. The generation of the docs is a requirement for building the website. Signed-off-by: leonnicolas <leonloechner@gmx.de>
This commit is contained in:
		
							
								
								
									
										297
									
								
								cmd/gen-docs/main.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										297
									
								
								cmd/gen-docs/main.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,297 @@ | ||||
| // 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. | ||||
|  | ||||
| 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", | ||||
| 		"metav1.LabelSelector":     "https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.21/#labelselector-v1-meta", | ||||
| 		"v1.ResourceRequirements":  "https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.21/#resourcerequirements-v1-core", | ||||
| 		"v1.LocalObjectReference":  "https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.21/#localobjectreference-v1-core", | ||||
| 		"v1.SecretKeySelector":     "https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.21/#secretkeyselector-v1-core", | ||||
| 		"v1.PersistentVolumeClaim": "https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.21/#persistentvolumeclaim-v1-core", | ||||
| 		"v1.EmptyDirVolumeSource":  "https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.21/#emptydirvolumesource-v1-core", | ||||
| 		"apiextensionsv1.JSON":     "https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.21/#json-v1-apiextensions-k8s-io", | ||||
| 	} | ||||
|  | ||||
| 	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 for 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:]) | ||||
| } | ||||
		Reference in New Issue
	
	Block a user