github.com/1aal/kubeblocks@v0.0.0-20231107070852-e1c03e598921/hack/docgen/api/main.go (about)

     1  /*
     2  Copyright (C) 2022-2023 ApeCloud Co., Ltd
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  /*
    18  Fork from https://github.com/ahmetb/gen-crd-api-reference-docs
    19  */
    20  
    21  package main
    22  
    23  import (
    24  	"bytes"
    25  	"encoding/json"
    26  	"flag"
    27  	"fmt"
    28  	"html/template"
    29  	"io"
    30  	"net/http"
    31  	"os"
    32  	"path/filepath"
    33  	"reflect"
    34  	"regexp"
    35  	"sort"
    36  	"strconv"
    37  	"strings"
    38  	texttemplate "text/template"
    39  	"time"
    40  	"unicode"
    41  
    42  	"golang.org/x/text/cases"
    43  	"golang.org/x/text/language"
    44  
    45  	"github.com/pkg/errors"
    46  	"github.com/russross/blackfriday/v2"
    47  	"k8s.io/gengo/parser"
    48  	"k8s.io/gengo/types"
    49  	"k8s.io/klog"
    50  )
    51  
    52  var (
    53  	flConfig      = flag.String("config", "", "path to config file")
    54  	flAPIDir      = flag.String("api-dir", "", "api directory (or import path), point this to pkg/apis")
    55  	flTemplateDir = flag.String("template-dir", "template", "path to template/ dir")
    56  
    57  	flHTTPAddr = flag.String("http-addr", "", "start an HTTP server on specified addr to view the result (e.g. :8080)")
    58  	flOutFile  = flag.String("out-file", "", "path to output file to save the result")
    59  
    60  	apiOrder = map[string]int{"cluster": 1, "backup": 2, "add-on": 3}
    61  )
    62  
    63  const (
    64  	docCommentForceIncludes = "// +gencrdrefdocs:force"
    65  )
    66  
    67  type generatorConfig struct {
    68  	// HiddenMemberFields hides fields with specified names on all types.
    69  	HiddenMemberFields []string `json:"hideMemberFields"`
    70  
    71  	// HideTypePatterns hides types matching the specified patterns from the
    72  	// output.
    73  	HideTypePatterns []string `json:"hideTypePatterns"`
    74  
    75  	// ExternalPackages lists recognized external package references and how to
    76  	// link to them.
    77  	ExternalPackages []externalPackage `json:"externalPackages"`
    78  
    79  	// TypeDisplayNamePrefixOverrides is a mapping of how to override displayed
    80  	// name for types with certain prefixes with what value.
    81  	TypeDisplayNamePrefixOverrides map[string]string `json:"typeDisplayNamePrefixOverrides"`
    82  
    83  	// MarkdownDisabled controls markdown rendering for comment lines.
    84  	MarkdownDisabled bool `json:"markdownDisabled"`
    85  }
    86  
    87  type externalPackage struct {
    88  	TypeMatchPrefix string `json:"typeMatchPrefix"`
    89  	DocsURLTemplate string `json:"docsURLTemplate"`
    90  }
    91  
    92  type apiPackage struct {
    93  	apiGroup   string
    94  	apiVersion string
    95  	GoPackages []*types.Package
    96  	Types      []*types.Type // because multiple 'types.Package's can add types to an apiVersion
    97  	Constants  []*types.Type
    98  }
    99  
   100  func (v *apiPackage) identifier() string { return fmt.Sprintf("%s/%s", v.apiGroup, v.apiVersion) }
   101  
   102  func init() {
   103  	klog.InitFlags(nil)
   104  	err := flag.Set("alsologtostderr", "true")
   105  	if err != nil {
   106  		return
   107  	}
   108  	flag.Parse()
   109  
   110  	if *flConfig == "" {
   111  		panic("-config not specified")
   112  	}
   113  	if *flAPIDir == "" {
   114  		panic("-api-dir not specified")
   115  	}
   116  	if *flHTTPAddr == "" && *flOutFile == "" {
   117  		panic("-out-file or -http-addr must be specified")
   118  	}
   119  	if *flHTTPAddr != "" && *flOutFile != "" {
   120  		panic("only -out-file or -http-addr can be specified")
   121  	}
   122  	if err := resolveTemplateDir(*flTemplateDir); err != nil {
   123  		panic(err)
   124  	}
   125  
   126  }
   127  
   128  func resolveTemplateDir(dir string) error {
   129  	path, err := filepath.Abs(dir)
   130  	if err != nil {
   131  		return err
   132  	}
   133  	if fi, err := os.Stat(path); err != nil {
   134  		return errors.Wrapf(err, "cannot read the %s directory", path)
   135  	} else if !fi.IsDir() {
   136  		return errors.Errorf("%s path is not a directory", path)
   137  	}
   138  	return nil
   139  }
   140  
   141  func main() {
   142  	defer klog.Flush()
   143  
   144  	f, err := os.Open(*flConfig)
   145  	if err != nil {
   146  		klog.Fatalf("failed to open config file: %+v", err)
   147  	}
   148  	d := json.NewDecoder(f)
   149  	d.DisallowUnknownFields()
   150  	var config generatorConfig
   151  	if err := d.Decode(&config); err != nil {
   152  		klog.Fatalf("failed to parse config file: %+v", err)
   153  	}
   154  
   155  	klog.Infof("parsing go packages in directory %s", *flAPIDir)
   156  	pkgs, err := parseAPIPackages()
   157  	if err != nil {
   158  		klog.Fatal(err)
   159  	}
   160  	if len(pkgs) == 0 {
   161  		klog.Fatalf("no API packages found in %s", *flAPIDir)
   162  	}
   163  
   164  	apiPackages, err := combineAPIPackages(pkgs)
   165  	if err != nil {
   166  		klog.Fatal(err)
   167  	}
   168  
   169  	mkOutput := func() (string, error) {
   170  		var b bytes.Buffer
   171  		err := render(&b, apiPackages, config)
   172  		if err != nil {
   173  			return "", errors.Wrap(err, "failed to render the result")
   174  		}
   175  
   176  		// remove trailing whitespace from each html line for markdown renderers
   177  		s := regexp.MustCompile(`(?m)^\s+`).ReplaceAllString(b.String(), "")
   178  		return s, nil
   179  	}
   180  
   181  	if *flOutFile != "" {
   182  		dir := filepath.Dir(*flOutFile)
   183  		if err := os.MkdirAll(dir, 0755); err != nil {
   184  			klog.Fatalf("failed to create dir %s: %v", dir, err)
   185  		}
   186  		s, err := mkOutput()
   187  		if err != nil {
   188  			klog.Fatalf("failed: %+v", err)
   189  		}
   190  		if err := os.WriteFile(*flOutFile, []byte(s), 0644); err != nil {
   191  			klog.Fatalf("failed to write to out file: %v", err)
   192  		}
   193  		klog.Infof("written to %s", *flOutFile)
   194  	}
   195  
   196  	if *flHTTPAddr != "" {
   197  		h := func(w http.ResponseWriter, r *http.Request) {
   198  			now := time.Now()
   199  			defer func() { klog.Infof("request took %v", time.Since(now)) }()
   200  			s, err := mkOutput()
   201  			if err != nil {
   202  				_, _ = fmt.Fprintf(w, "error: %+v", err)
   203  				klog.Warningf("failed: %+v", err)
   204  			}
   205  			if _, err := fmt.Fprint(w, s); err != nil {
   206  				klog.Warningf("response write error: %v", err)
   207  			}
   208  		}
   209  		http.HandleFunc("/", h)
   210  		klog.Infof("server listening at %s", *flHTTPAddr)
   211  		klog.Fatal(http.ListenAndServe(*flHTTPAddr, nil))
   212  	}
   213  }
   214  
   215  // groupName extracts the "//+groupName" meta-comment from the specified
   216  // package's comments, or returns empty string if it cannot be found.
   217  func groupName(pkg *types.Package) string {
   218  	m := types.ExtractCommentTags("+", pkg.Comments)
   219  	v := m["groupName"]
   220  	if len(v) == 1 {
   221  		return v[0]
   222  	}
   223  	return ""
   224  }
   225  
   226  func parseAPIPackages() ([]*types.Package, error) {
   227  	b := parser.New()
   228  	// the following will silently fail (turn on -v=4 to see logs)
   229  	if err := b.AddDirRecursive(*flAPIDir); err != nil {
   230  		return nil, err
   231  	}
   232  	scan, err := b.FindTypes()
   233  	if err != nil {
   234  		return nil, errors.Wrap(err, "failed to parse pkgs and types")
   235  	}
   236  	var pkgNames []string
   237  	for p := range scan {
   238  		pkg := scan[p]
   239  		klog.V(3).Infof("trying package=%v groupName=%s", p, groupName(pkg))
   240  
   241  		// Do not pick up packages that are in vendor/ as API packages. (This
   242  		// happened in knative/eventing-sources/vendor/..., where a package
   243  		// matched the pattern, but it didn't have a compatible import path).
   244  		if isVendorPackage(pkg) {
   245  			klog.V(3).Infof("package=%v coming from vendor/, ignoring.", p)
   246  			continue
   247  		}
   248  
   249  		if groupName(pkg) != "" && len(pkg.Types) > 0 || containsString(pkg.DocComments, docCommentForceIncludes) {
   250  			klog.V(3).Infof("package=%v has groupName and has types", p)
   251  			pkgNames = append(pkgNames, p)
   252  		}
   253  	}
   254  	sort.Strings(pkgNames)
   255  	var pkgs []*types.Package
   256  	for _, p := range pkgNames {
   257  		klog.Infof("using package=%s", p)
   258  		pkgs = append(pkgs, scan[p])
   259  	}
   260  	return pkgs, nil
   261  }
   262  
   263  func containsString(sl []string, str string) bool {
   264  	for _, s := range sl {
   265  		if str == s {
   266  			return true
   267  		}
   268  	}
   269  	return false
   270  }
   271  
   272  // combineAPIPackages groups the Go packages by the <apiGroup+apiVersion> they
   273  // offer, and combines the types in them.
   274  func combineAPIPackages(pkgs []*types.Package) ([]*apiPackage, error) {
   275  	pkgMap := make(map[string]*apiPackage)
   276  	var pkgIds []string
   277  
   278  	flattenTypes := func(typeMap map[string]*types.Type) []*types.Type {
   279  		typeList := make([]*types.Type, 0, len(typeMap))
   280  
   281  		for _, t := range typeMap {
   282  			typeList = append(typeList, t)
   283  		}
   284  
   285  		return typeList
   286  	}
   287  
   288  	for _, pkg := range pkgs {
   289  		apiGroup, apiVersion, err := apiVersionForPackage(pkg)
   290  		if err != nil {
   291  			return nil, errors.Wrapf(err, "could not get apiVersion for package %s", pkg.Path)
   292  		}
   293  
   294  		id := fmt.Sprintf("%s/%s", apiGroup, apiVersion)
   295  		v, ok := pkgMap[id]
   296  		if !ok {
   297  			pkgMap[id] = &apiPackage{
   298  				apiGroup:   apiGroup,
   299  				apiVersion: apiVersion,
   300  				Types:      flattenTypes(pkg.Types),
   301  				Constants:  flattenTypes(pkg.Constants),
   302  				GoPackages: []*types.Package{pkg},
   303  			}
   304  			pkgIds = append(pkgIds, id)
   305  		} else {
   306  			v.Types = append(v.Types, flattenTypes(pkg.Types)...)
   307  			v.Constants = append(v.Constants, flattenTypes(pkg.Constants)...)
   308  			v.GoPackages = append(v.GoPackages, pkg)
   309  		}
   310  	}
   311  
   312  	sort.Strings(pkgIds)
   313  
   314  	out := make([]*apiPackage, 0, len(pkgMap))
   315  	for _, id := range pkgIds {
   316  		out = append(out, pkgMap[id])
   317  	}
   318  	return out, nil
   319  }
   320  
   321  // isVendorPackage determines if package is coming from vendor/ dir.
   322  func isVendorPackage(pkg *types.Package) bool {
   323  	vendorPattern := string(os.PathSeparator) + "vendor" + string(os.PathSeparator)
   324  	return strings.Contains(pkg.SourcePath, vendorPattern)
   325  }
   326  
   327  func findTypeReferences(pkgs []*apiPackage) map[*types.Type][]*types.Type {
   328  	m := make(map[*types.Type][]*types.Type)
   329  	for _, pkg := range pkgs {
   330  		for _, typ := range pkg.Types {
   331  			for _, member := range typ.Members {
   332  				t := member.Type
   333  				t = tryDereference(t)
   334  				m[t] = append(m[t], typ)
   335  			}
   336  		}
   337  	}
   338  	return m
   339  }
   340  
   341  func isExportedType(t *types.Type) bool {
   342  	return strings.Contains(strings.Join(t.SecondClosestCommentLines, "\n"), "+genclient") || strings.Contains(strings.Join(t.SecondClosestCommentLines, "\n"), "+kubebuilder:object:root=true")
   343  }
   344  
   345  func fieldName(m types.Member) string {
   346  	v := reflect.StructTag(m.Tags).Get("json")
   347  	v = strings.TrimSuffix(v, ",omitempty")
   348  	v = strings.TrimSuffix(v, ",inline")
   349  	if v != "" {
   350  		return v
   351  	}
   352  	return m.Name
   353  }
   354  
   355  func fieldEmbedded(m types.Member) bool {
   356  	return strings.Contains(reflect.StructTag(m.Tags).Get("json"), ",inline")
   357  }
   358  
   359  func isLocalType(t *types.Type, typePkgMap map[*types.Type]*apiPackage) bool {
   360  	t = tryDereference(t)
   361  	_, ok := typePkgMap[t]
   362  	return ok
   363  }
   364  
   365  func renderComments(s []string, markdown bool) string {
   366  	s = filterCommentTags(s)
   367  	doc := strings.Join(s, "\n")
   368  
   369  	if markdown {
   370  		doc = string(blackfriday.Run([]byte(doc)))
   371  		doc = strings.ReplaceAll(doc, "\n", string(template.HTML("<br />")))
   372  		doc = strings.ReplaceAll(doc, "{", string(template.HTML("&#123;")))
   373  		doc = strings.ReplaceAll(doc, "}", string(template.HTML("&#125;")))
   374  		return doc
   375  	}
   376  	return nl2br(doc)
   377  }
   378  
   379  func safe(s string) template.HTML { return template.HTML(s) }
   380  
   381  func toTitle(s string) string { return cases.Title(language.English).String(s) }
   382  
   383  func nl2br(s string) string {
   384  	return strings.ReplaceAll(s, "\n\n", string(template.HTML("<br/><br/>")))
   385  }
   386  
   387  func hiddenMember(m types.Member, c generatorConfig) bool {
   388  	for _, v := range c.HiddenMemberFields {
   389  		if m.Name == v {
   390  			return true
   391  		}
   392  	}
   393  	return false
   394  }
   395  
   396  func typeIdentifier(t *types.Type) string {
   397  	t = tryDereference(t)
   398  	return t.Name.String() // {PackagePath.Name}
   399  }
   400  
   401  // apiGroupForType looks up apiGroup for the given type
   402  func apiGroupForType(t *types.Type, typePkgMap map[*types.Type]*apiPackage) string {
   403  	t = tryDereference(t)
   404  
   405  	v := typePkgMap[t]
   406  	if v == nil {
   407  		klog.Warningf("WARNING: cannot read apiVersion for %s from type=>pkg map", t.Name.String())
   408  		return "<UNKNOWN_API_GROUP>"
   409  	}
   410  
   411  	return v.identifier()
   412  }
   413  
   414  // anchorIDForLocalType returns the #anchor string for the local type
   415  func anchorIDForLocalType(t *types.Type, typePkgMap map[*types.Type]*apiPackage) string {
   416  	return fmt.Sprintf("%s.%s", apiGroupForType(t, typePkgMap), t.Name.Name)
   417  }
   418  
   419  // linkForType returns an anchor to the type if it can be generated. returns
   420  // empty string if it is not a local type or unrecognized external type.
   421  func linkForType(t *types.Type, c generatorConfig, typePkgMap map[*types.Type]*apiPackage) (string, error) {
   422  	t = tryDereference(t) // dereference kind=Pointer
   423  
   424  	if isLocalType(t, typePkgMap) {
   425  		return "#" + anchorIDForLocalType(t, typePkgMap), nil
   426  	}
   427  
   428  	var arrIndex = func(a []string, i int) string {
   429  		return a[(len(a)+i)%len(a)]
   430  	}
   431  
   432  	// types like k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta,
   433  	// k8s.io/api/core/v1.Container, k8s.io/api/autoscaling/v1.CrossVersionObjectReference,
   434  	// github.com/knative/build/pkg/apis/build/v1alpha1.BuildSpec
   435  	if t.Kind == types.Struct || t.Kind == types.Pointer || t.Kind == types.Interface || t.Kind == types.Alias {
   436  		id := typeIdentifier(t)                        // gives {{ImportPath.Identifier}} for type
   437  		segments := strings.Split(t.Name.Package, "/") // to parse [meta, v1] from "k8s.io/apimachinery/pkg/apis/meta/v1"
   438  
   439  		for _, v := range c.ExternalPackages {
   440  			r, err := regexp.Compile(v.TypeMatchPrefix)
   441  			if err != nil {
   442  				return "", errors.Wrapf(err, "pattern %q failed to compile", v.TypeMatchPrefix)
   443  			}
   444  			if r.MatchString(id) {
   445  				tpl, err := texttemplate.New("").Funcs(map[string]interface{}{
   446  					"lower":    strings.ToLower,
   447  					"arrIndex": arrIndex,
   448  				}).Parse(v.DocsURLTemplate)
   449  				if err != nil {
   450  					return "", errors.Wrap(err, "docs URL template failed to parse")
   451  				}
   452  
   453  				var b bytes.Buffer
   454  				if err := tpl.
   455  					Execute(&b, map[string]interface{}{
   456  						"TypeIdentifier":  t.Name.Name,
   457  						"PackagePath":     t.Name.Package,
   458  						"PackageSegments": segments,
   459  					}); err != nil {
   460  					return "", errors.Wrap(err, "docs url template execution error")
   461  				}
   462  				return b.String(), nil
   463  			}
   464  		}
   465  		klog.Warningf("not found external link source for type %v", t.Name)
   466  	}
   467  	return "", nil
   468  }
   469  
   470  // tryDereference returns the underlying type when t is a pointer, map, or slice.
   471  func tryDereference(t *types.Type) *types.Type {
   472  	for t.Elem != nil {
   473  		t = t.Elem
   474  	}
   475  	return t
   476  }
   477  
   478  // finalUnderlyingTypeOf walks the type hierarchy for t and returns
   479  // its base type (i.e. the type that has no further underlying type).
   480  func finalUnderlyingTypeOf(t *types.Type) *types.Type {
   481  	for {
   482  		if t.Underlying == nil {
   483  			return t
   484  		}
   485  
   486  		t = t.Underlying
   487  	}
   488  }
   489  
   490  func typeDisplayName(t *types.Type, c generatorConfig, typePkgMap map[*types.Type]*apiPackage) string {
   491  	s := typeIdentifier(t)
   492  
   493  	if isLocalType(t, typePkgMap) {
   494  		s = tryDereference(t).Name.Name
   495  	}
   496  
   497  	if t.Kind == types.Pointer {
   498  		s = strings.TrimLeft(s, "*")
   499  	}
   500  
   501  	switch t.Kind {
   502  	case types.Struct,
   503  		types.Interface,
   504  		types.Alias,
   505  		types.Pointer,
   506  		types.Slice,
   507  		types.Builtin:
   508  		// noop
   509  	case types.Map:
   510  		// return original name
   511  		return t.Name.Name
   512  	case types.DeclarationOf:
   513  		// For constants, we want to display the value
   514  		// rather than the name of the constant, since the
   515  		// value is what users will need to write into YAML
   516  		// specs.
   517  		if t.ConstValue != nil {
   518  			u := finalUnderlyingTypeOf(t)
   519  			// Quote string constants to make it clear to the documentation reader.
   520  			if u.Kind == types.Builtin && u.Name.Name == "string" {
   521  				return strconv.Quote(*t.ConstValue)
   522  			}
   523  
   524  			return *t.ConstValue
   525  		}
   526  		klog.Fatalf("type %s is a non-const declaration, which is unhandled", t.Name)
   527  	default:
   528  		klog.Fatalf("type %s has kind=%v which is unhandled", t.Name, t.Kind)
   529  	}
   530  
   531  	// substitute prefix, if registered
   532  	for prefix, replacement := range c.TypeDisplayNamePrefixOverrides {
   533  		if strings.HasPrefix(s, prefix) {
   534  			s = strings.Replace(s, prefix, replacement, 1)
   535  		}
   536  	}
   537  
   538  	if t.Kind == types.Slice {
   539  		s = "[]" + s
   540  	}
   541  
   542  	return s
   543  }
   544  
   545  func hideType(t *types.Type, c generatorConfig) bool {
   546  	for _, pattern := range c.HideTypePatterns {
   547  		if regexp.MustCompile(pattern).MatchString(t.Name.String()) {
   548  			return true
   549  		}
   550  	}
   551  	if !isExportedType(t) && unicode.IsLower(rune(t.Name.Name[0])) {
   552  		// types that start with lowercase
   553  		return true
   554  	}
   555  	return false
   556  }
   557  
   558  func typeReferences(t *types.Type, c generatorConfig, references map[*types.Type][]*types.Type) []*types.Type {
   559  	var out []*types.Type
   560  	m := make(map[*types.Type]struct{})
   561  	for _, ref := range references[t] {
   562  		if !hideType(ref, c) {
   563  			m[ref] = struct{}{}
   564  		}
   565  	}
   566  	for k := range m {
   567  		out = append(out, k)
   568  	}
   569  	sortTypes(out)
   570  	return out
   571  }
   572  
   573  func sortTypes(typs []*types.Type) []*types.Type {
   574  	sort.Slice(typs, func(i, j int) bool {
   575  		t1, t2 := typs[i], typs[j]
   576  		if isExportedType(t1) && !isExportedType(t2) {
   577  			return true
   578  		} else if !isExportedType(t1) && isExportedType(t2) {
   579  			return false
   580  		}
   581  		return t1.Name.String() < t2.Name.String()
   582  	})
   583  	return typs
   584  }
   585  
   586  func visibleTypes(in []*types.Type, c generatorConfig) []*types.Type {
   587  	var out []*types.Type
   588  	for _, t := range in {
   589  		if !hideType(t, c) {
   590  			out = append(out, t)
   591  		}
   592  	}
   593  	return out
   594  }
   595  
   596  func filterCommentTags(comments []string) []string {
   597  	var out []string
   598  	for _, v := range comments {
   599  		if !strings.HasPrefix(strings.TrimSpace(v), "+") {
   600  			out = append(out, v)
   601  		}
   602  	}
   603  	return out
   604  }
   605  
   606  func isOptionalMember(m types.Member) bool {
   607  	tags := types.ExtractCommentTags("+", m.CommentLines)
   608  	_, ok := tags["optional"]
   609  	return ok
   610  }
   611  
   612  func apiVersionForPackage(pkg *types.Package) (string, string, error) {
   613  	group := groupName(pkg)
   614  	version := pkg.Name // assumes basename (i.e. "v1" in "core/v1") is apiVersion
   615  	r := `^v\d+((alpha|beta|api|stable)[a-z0-9]+)?$`
   616  	if !regexp.MustCompile(r).MatchString(version) {
   617  		return "", "", errors.Errorf("cannot infer kubernetes apiVersion of go package %s (basename %q doesn't match expected pattern %s that's used to determine apiVersion)", pkg.Path, version, r)
   618  	}
   619  	return group, version, nil
   620  }
   621  
   622  // extractTypeToPackageMap creates a *types.Type map to apiPackage
   623  func extractTypeToPackageMap(pkgs []*apiPackage) map[*types.Type]*apiPackage {
   624  	out := make(map[*types.Type]*apiPackage)
   625  	for _, ap := range pkgs {
   626  		for _, t := range ap.Types {
   627  			out[t] = ap
   628  		}
   629  		for _, t := range ap.Constants {
   630  			out[t] = ap
   631  		}
   632  	}
   633  	return out
   634  }
   635  
   636  // constantsOfType finds all the constants in pkg that have the
   637  // same underlying type as t. This is intended for use by enum
   638  // type validation, where users need to specify one of a specific
   639  // set of constant values for a field.
   640  func constantsOfType(t *types.Type, pkg *apiPackage) []*types.Type {
   641  	var constants []*types.Type
   642  
   643  	for _, c := range pkg.Constants {
   644  		if c.Underlying == t {
   645  			constants = append(constants, c)
   646  		}
   647  	}
   648  
   649  	return sortTypes(constants)
   650  }
   651  
   652  func getAPIOrder(filename string) int {
   653  	if order, ok := apiOrder[filename]; ok {
   654  		return order
   655  	}
   656  	return 1000
   657  }
   658  
   659  func render(w io.Writer, pkgs []*apiPackage, config generatorConfig) error {
   660  	references := findTypeReferences(pkgs)
   661  	typePkgMap := extractTypeToPackageMap(pkgs)
   662  
   663  	t, err := template.New("").Funcs(map[string]interface{}{
   664  		"isExportedType":     isExportedType,
   665  		"fieldName":          fieldName,
   666  		"fieldEmbedded":      fieldEmbedded,
   667  		"typeIdentifier":     typeIdentifier,
   668  		"typeDisplayName":    func(t *types.Type) string { return typeDisplayName(t, config, typePkgMap) },
   669  		"visibleTypes":       func(t []*types.Type) []*types.Type { return visibleTypes(t, config) },
   670  		"renderComments":     func(s []string) string { return renderComments(s, !config.MarkdownDisabled) },
   671  		"packageDisplayName": func(p *apiPackage) string { return p.identifier() },
   672  		"apiGroup":           func(t *types.Type) string { return apiGroupForType(t, typePkgMap) },
   673  		"packageAnchorID": func(p *apiPackage) string {
   674  			return strings.ReplaceAll(p.identifier(), " ", "")
   675  		},
   676  		"linkForType": func(t *types.Type) string {
   677  			v, err := linkForType(t, config, typePkgMap)
   678  			if err != nil {
   679  				klog.Fatal(errors.Wrapf(err, "error getting link for type=%s", t.Name))
   680  				return ""
   681  			}
   682  			return v
   683  		},
   684  		"anchorIDForType":  func(t *types.Type) string { return anchorIDForLocalType(t, typePkgMap) },
   685  		"safe":             safe,
   686  		"toTitle":          toTitle,
   687  		"sortedTypes":      sortTypes,
   688  		"typeReferences":   func(t *types.Type) []*types.Type { return typeReferences(t, config, references) },
   689  		"hiddenMember":     func(m types.Member) bool { return hiddenMember(m, config) },
   690  		"isLocalType":      isLocalType,
   691  		"isOptionalMember": isOptionalMember,
   692  		"constantsOfType":  func(t *types.Type) []*types.Type { return constantsOfType(t, typePkgMap[t]) },
   693  	}).ParseGlob(filepath.Join(*flTemplateDir, "*.tpl"))
   694  	if err != nil {
   695  		return errors.Wrap(err, "parse error")
   696  	}
   697  
   698  	apiName := strings.Split(filepath.Base(*flOutFile), ".")[0]
   699  	filerOrder := getAPIOrder(apiName)
   700  
   701  	return errors.Wrap(t.ExecuteTemplate(w, "packages", map[string]interface{}{
   702  		"packages":   pkgs,
   703  		"apiName":    apiName,
   704  		"filerOrder": filerOrder,
   705  	}), "template execution error")
   706  }