From 9f21ae21e8b7d13de7007851ae2ce9f5d121324b Mon Sep 17 00:00:00 2001 From: Anastasis Andronidis Date: Sun, 5 Jul 2015 19:15:13 +0200 Subject: [PATCH] swagger type documentation generator --- cmd/genswaggertypedocs/swagger_type_docs.go | 73 +++++++ hack/verify-flags/known-flags.txt | 1 + pkg/runtime/swagger_doc_generator.go | 228 ++++++++++++++++++++ pkg/runtime/swagger_doc_generator_test.go | 43 ++++ 4 files changed, 345 insertions(+) create mode 100644 cmd/genswaggertypedocs/swagger_type_docs.go create mode 100644 pkg/runtime/swagger_doc_generator.go create mode 100644 pkg/runtime/swagger_doc_generator_test.go diff --git a/cmd/genswaggertypedocs/swagger_type_docs.go b/cmd/genswaggertypedocs/swagger_type_docs.go new file mode 100644 index 00000000000..afdbf78b68c --- /dev/null +++ b/cmd/genswaggertypedocs/swagger_type_docs.go @@ -0,0 +1,73 @@ +/* +Copyright 2015 The Kubernetes Authors All rights reserved. + +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 ( + "fmt" + "io" + "os" + "runtime" + + pkg_runtime "k8s.io/kubernetes/pkg/runtime" + + "github.com/golang/glog" + flag "github.com/spf13/pflag" +) + +var ( + functionDest = flag.StringP("func-dest", "f", "-", "Output for swagger functions; '-' means stdout (default)") + typeSrc = flag.StringP("type-src", "s", "", "From where we are going to read the types") + verify = flag.BoolP("verify", "v", false, "Verifies if the given type-src file has documentation for every type") +) + +func main() { + runtime.GOMAXPROCS(runtime.NumCPU()) + flag.Parse() + + if *typeSrc == "" { + glog.Fatalf("Please define -s flag as it is the source file") + } + + var funcOut io.Writer + if *functionDest == "-" { + funcOut = os.Stdout + } else { + file, err := os.Create(*functionDest) + if err != nil { + glog.Fatalf("Couldn't open %v: %v", *functionDest, err) + } + defer file.Close() + funcOut = file + } + + docsForTypes := pkg_runtime.ParseDocumentationFrom(*typeSrc) + + if *verify == true { + rc, err := pkg_runtime.VerifySwaggerDocsExist(docsForTypes, funcOut) + if err != nil { + fmt.Fprintf(os.Stderr, "Error in verification process: %s\n", err) + } + os.Exit(rc) + } + + if docsForTypes != nil && len(docsForTypes) > 0 { + if err := pkg_runtime.WriteSwaggerDocFunc(docsForTypes, funcOut); err != nil { + fmt.Fprintf(os.Stderr, "Error when writing swagger documentation functions: %s\n", err) + os.Exit(-1) + } + } +} diff --git a/hack/verify-flags/known-flags.txt b/hack/verify-flags/known-flags.txt index f175adb3bc7..52fc5407acb 100644 --- a/hack/verify-flags/known-flags.txt +++ b/hack/verify-flags/known-flags.txt @@ -238,6 +238,7 @@ tls-cert-file tls-private-key-file token-auth-file ttl-secs +type-src unix-socket update-period upgrade-target diff --git a/pkg/runtime/swagger_doc_generator.go b/pkg/runtime/swagger_doc_generator.go new file mode 100644 index 00000000000..829ce7b7106 --- /dev/null +++ b/pkg/runtime/swagger_doc_generator.go @@ -0,0 +1,228 @@ +/* +Copyright 2015 The Kubernetes Authors All rights reserved. + +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 runtime + +import ( + "bytes" + "fmt" + "go/ast" + "go/doc" + "go/parser" + "go/token" + "io" + "reflect" + "strings" +) + +// Pair of strings. We keed the name of fields and the doc +type Pair struct { + Name, Doc string +} + +// KubeTypes is an array to represent all available types in a parsed file. [0] is for the type itself +type KubeTypes []Pair + +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, " ") + + if line == "" { // Keep paragraphs + delPrevChar() + buffer.WriteString("\n\n") + } else if !strings.HasPrefix(strings.TrimLeft(line, " "), "TODO") { // Ignore one line TODOs + 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) + + return postDoc +} + +// 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 := "" + if field.Tag != nil { + jsonTag = reflect.StructTag(field.Tag.Value[1 : len(field.Tag.Value)-1]).Get("json") // Delete first and last quotation + if strings.Contains(jsonTag, "inline") { + return "-" + } + } + + 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 +} + +func writeFuncHeader(b *buffer, structName string, indent int) { + s := fmt.Sprintf("var map_%s = map[string]string {\n", structName) + b.addLine(s, indent) +} + +func writeFuncFooter(b *buffer, structName string, indent int) { + b.addLine("}\n", indent) // Closes the map definition + + s := fmt.Sprintf("func (%s) SwaggerDoc() map[string]string {\n", structName) + b.addLine(s, indent) + s = fmt.Sprintf("return map_%s\n", structName) + b.addLine(s, indent+1) + b.addLine("}\n", indent) // Closes the function definition +} + +func writeMapBody(b *buffer, kubeType []Pair, indent int) { + format := "\"%s\": \"%s\",\n" + for _, pair := range kubeType { + s := fmt.Sprintf(format, pair.Name, pair.Doc) + b.addLine(s, indent+2) + } +} + +// 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(src string) []KubeTypes { + var docForTypes []KubeTypes + + 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)}) + + for _, field := range structType.Fields.List { + if n := fieldName(field); n != "-" { + fieldDoc := fmtRawDoc(field.Doc.Text()) + ks = append(ks, Pair{n, fieldDoc}) + } + } + docForTypes = append(docForTypes, ks) + } + } + + return docForTypes +} + +// WriteSwaggerDocFunc writes a declaration of a function as a string. This function is used in +// Swagger as a documentation source for structs and theirs fields +func WriteSwaggerDocFunc(kubeTypes []KubeTypes, w io.Writer) error { + for _, kubeType := range kubeTypes { + structName := kubeType[0].Name + kubeType[0].Name = "" + + // Ignore empty documentation + docfulTypes := make(KubeTypes, 0, len(kubeType)) + for _, pair := range kubeType { + if pair.Doc != "" { + docfulTypes = append(docfulTypes, pair) + } + } + + if len(docfulTypes) == 0 { + continue // If no fields and the struct have documentation, skip the function definition + } + + indent := 0 + buffer := newBuffer() + + writeFuncHeader(buffer, structName, indent) + writeMapBody(buffer, docfulTypes, indent) + writeFuncFooter(buffer, structName, indent) + buffer.addLine("\n", 0) + + if err := buffer.flushLines(w); err != nil { + return err + } + } + + return nil +} + +// VerifySwaggerDocsExist writes in a io.Writer a list of structs and fields that +// are missing of documentation. +func VerifySwaggerDocsExist(kubeTypes []KubeTypes, w io.Writer) (int, error) { + missingDocs := 0 + buffer := newBuffer() + + for _, kubeType := range kubeTypes { + structName := kubeType[0].Name + if kubeType[0].Doc == "" { + format := "Missing documentation for the struct itself: %s\n" + s := fmt.Sprintf(format, structName) + buffer.addLine(s, 0) + missingDocs++ + } + kubeType = kubeType[1:] // Skip struct definition + + for _, pair := range kubeType { // Iterate only the fields + if pair.Doc == "" { + format := "In struct: %s, field documentation is missing: %s\n" + s := fmt.Sprintf(format, structName, pair.Name) + buffer.addLine(s, 0) + missingDocs++ + } + } + } + + if err := buffer.flushLines(w); err != nil { + return -1, err + } + return missingDocs, nil +} diff --git a/pkg/runtime/swagger_doc_generator_test.go b/pkg/runtime/swagger_doc_generator_test.go new file mode 100644 index 00000000000..095dddff58b --- /dev/null +++ b/pkg/runtime/swagger_doc_generator_test.go @@ -0,0 +1,43 @@ +/* +Copyright 2015 The Kubernetes Authors All rights reserved. + +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 runtime + +import ( + "testing" +) + +func TestFmtRawDoc(t *testing.T) { + tests := []struct { + t, expected string + }{ + {"aaa\n --- asd\n TODO: tooooodo\n toooodoooooo\n", "aaa"}, + {"aaa\nasd\n TODO: tooooodo\nbbbb\n --- toooodoooooo\n", "aaa asd bbbb"}, + {" TODO: tooooodo\n", ""}, + {"Par1\n\nPar2\n\n", "Par1\\n\\nPar2"}, + {"", ""}, + {" ", ""}, + {" \n", ""}, + {" \n\n ", ""}, + {"Example:\n\tl1\n\t\tl2\n", "Example:\\n\\tl1\\n\\t\\tl2"}, + } + + for _, test := range tests { + if o := fmtRawDoc(test.t); o != test.expected { + t.Fatalf("Expected: %q, got %q", test.expected, o) + } + } +}