blob: f09f641782208cbb5890055b38b28299eddab7d4 [file] [log] [blame]
/*
Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file distributed with
this work for additional information regarding copyright ownership.
The ASF licenses this file to You 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 generators
import (
"context"
"fmt"
"io"
"io/ioutil"
"os"
"path"
"reflect"
"regexp"
"sort"
"strings"
v1 "github.com/apache/camel-k/pkg/apis/camel/v1"
"github.com/apache/camel-k/pkg/trait"
"k8s.io/gengo/args"
"k8s.io/gengo/generator"
"k8s.io/gengo/types"
)
const (
tagTrait = "+camel-k:trait"
adocCommonMarkerStart = "// Start of autogenerated code - DO NOT EDIT!"
adocCommonMarkerEnd = "// End of autogenerated code - DO NOT EDIT!"
adocDescriptionMarkerStart = adocCommonMarkerStart + " (description)"
adocDescriptionMarkerEnd = adocCommonMarkerEnd + " (description)"
adocConfigurationMarkerStart = adocCommonMarkerStart + " (configuration)"
adocConfigurationMarkerEnd = adocCommonMarkerEnd + " (configuration)"
adocNavMarkerStart = adocCommonMarkerStart + " (trait-nav)"
adocNavMarkerEnd = adocCommonMarkerEnd + " (trait-nav)"
adocListMarkerStart = adocCommonMarkerStart + " (trait-list)"
adocListMarkerEnd = adocCommonMarkerEnd + " (trait-list)"
)
var (
tagTraitID = regexp.MustCompile(fmt.Sprintf("%s=([a-z0-9-]+)", regexp.QuoteMeta(tagTrait)))
)
// traitDocGen produces documentation about traits
type traitDocGen struct {
generator.DefaultGen
arguments *args.GeneratorArgs
generatedTraitFiles []string
}
// NewTraitDocGen --
func NewTraitDocGen(arguments *args.GeneratorArgs) generator.Generator {
return &traitDocGen{
DefaultGen: generator.DefaultGen{},
arguments: arguments,
}
}
func (g *traitDocGen) Filename() string {
return "zz_generated_doc.go"
}
func (g *traitDocGen) Filter(context *generator.Context, t *types.Type) bool {
for _, c := range t.CommentLines {
if strings.Contains(c, tagTrait) {
return true
}
}
return false
}
func (g *traitDocGen) GenerateType(context *generator.Context, t *types.Type, out io.Writer) error {
docDir := g.arguments.CustomArgs.(*CustomArgs).DocDir
traitPath := g.arguments.CustomArgs.(*CustomArgs).TraitPath
traitID := getTraitID(t)
traitFile := traitID + ".adoc"
filename := path.Join(docDir, traitPath, traitFile)
g.generatedTraitFiles = append(g.generatedTraitFiles, traitFile)
file, content, err := readFile(filename)
if err != nil {
return err
}
defer file.Close()
writeTitle(traitID, &content)
writeDescription(t, traitID, &content)
writeFields(t, traitID, &content)
return writeFile(file, content)
}
func (g *traitDocGen) Finalize(c *generator.Context, w io.Writer) error {
if err := g.FinalizeNav(c); err != nil {
return err
}
return g.FinalizeList(c)
}
func (g *traitDocGen) FinalizeNav(*generator.Context) error {
docDir := g.arguments.CustomArgs.(*CustomArgs).DocDir
navPath := g.arguments.CustomArgs.(*CustomArgs).NavPath
filename := path.Join(docDir, navPath)
file, content, err := readFile(filename)
if err != nil {
return err
}
defer file.Close()
pre, post := split(content, adocNavMarkerStart, adocNavMarkerEnd)
content = append([]string(nil), pre...)
content = append(content, adocNavMarkerStart)
sort.Strings(g.generatedTraitFiles)
for _, t := range g.generatedTraitFiles {
name := traitNameFromFile(t)
content = append(content, "** xref:traits/"+t+"["+name+"]")
}
content = append(content, adocNavMarkerEnd)
content = append(content, post...)
return writeFile(file, content)
}
func (g *traitDocGen) FinalizeList(*generator.Context) error {
docDir := g.arguments.CustomArgs.(*CustomArgs).DocDir
listPath := g.arguments.CustomArgs.(*CustomArgs).ListPath
filename := path.Join(docDir, listPath)
file, content, err := readFile(filename)
if err != nil {
return err
}
defer file.Close()
pre, post := split(content, adocListMarkerStart, adocListMarkerEnd)
content = append([]string(nil), pre...)
content = append(content, adocListMarkerStart)
sort.Strings(g.generatedTraitFiles)
for _, t := range g.generatedTraitFiles {
name := traitNameFromFile(t)
content = append(content, "* xref:traits/"+t+"["+name+" Trait]")
}
content = append(content, adocListMarkerEnd)
content = append(content, post...)
return writeFile(file, content)
}
func traitNameFromFile(file string) string {
name := strings.TrimSuffix(file, ".adoc")
name = strings.ReplaceAll(name, "trait", "")
name = strings.ReplaceAll(name, "-", " ")
name = strings.Trim(name, " ")
name = strings.Title(name)
return name
}
func writeTitle(traitID string, content *[]string) {
res := append([]string(nil), *content...)
for _, s := range res {
if strings.HasPrefix(s, "= ") {
// Already has a title
return
}
}
res = append([]string{"= " + strings.Title(strings.ReplaceAll(traitID, "-", " ")) + " Trait"}, res...)
*content = res
}
func writeDescription(t *types.Type, traitID string, content *[]string) {
pre, post := split(*content, adocDescriptionMarkerStart, adocDescriptionMarkerEnd)
res := append([]string(nil), pre...)
res = append(res, adocDescriptionMarkerStart)
res = append(res, filterOutTagsAndComments(t.CommentLines)...)
profiles := strings.Join(determineProfiles(traitID), ", ")
res = append(res, "", fmt.Sprintf("This trait is available in the following profiles: **%s**.", profiles))
if isPlatformTrait(traitID) {
res = append(res, "", fmt.Sprintf("WARNING: The %s trait is a *platform trait*: disabling it may compromise the platform functionality.", traitID))
}
res = append(res, "", adocDescriptionMarkerEnd)
res = append(res, post...)
*content = res
}
func writeFields(t *types.Type, traitID string, content *[]string) {
pre, post := split(*content, adocConfigurationMarkerStart, adocConfigurationMarkerEnd)
res := append([]string(nil), pre...)
res = append(res, adocConfigurationMarkerStart, "== Configuration", "")
res = append(res, "Trait properties can be specified when running any integration with the CLI:")
res = append(res, "```")
if len(t.Members) > 1 {
res = append(res, fmt.Sprintf("kamel run --trait %s.[key]=[value] --trait %s.[key2]=[value2] integration.groovy", traitID, traitID))
} else {
res = append(res, fmt.Sprintf("kamel run --trait %s.[key]=[value] integration.groovy", traitID))
}
res = append(res, "```")
res = append(res, "The following configuration options are available:", "")
res = append(res, "[cols=\"2,1,5a\"]", "|===")
res = append(res, "|Property | Type | Description", "")
writeMembers(t, traitID, &res)
res = append(res, "|===", "", adocConfigurationMarkerEnd)
res = append(res, post...)
*content = res
}
func writeMembers(t *types.Type, traitID string, content *[]string) {
res := append([]string(nil), *content...)
for _, m := range t.Members {
prop := reflect.StructTag(m.Tags).Get("property")
if prop != "" {
if strings.Contains(prop, "squash") {
writeMembers(m.Type, traitID, &res)
} else {
res = append(res, "| "+traitID+"."+prop)
res = append(res, "| "+strings.TrimPrefix(m.Type.Name.Name, "*"))
first := true
for _, l := range filterOutTagsAndComments(m.CommentLines) {
if first {
res = append(res, "| "+l)
first = false
} else {
res = append(res, l)
}
}
res = append(res, "")
}
}
}
*content = res
}
func getTraitID(t *types.Type) string {
for _, s := range t.CommentLines {
if strings.Contains(s, tagTrait) {
matches := tagTraitID.FindStringSubmatch(s)
if len(matches) < 2 {
panic(fmt.Sprintf("unable to extract trait ID from tag line `%s`", s))
}
return matches[1]
}
}
panic(fmt.Sprintf("trait ID not found in type %s", t.Name.Name))
}
func filterOutTagsAndComments(comments []string) []string {
res := make([]string, 0, len(comments))
for _, l := range comments {
if !strings.HasPrefix(strings.TrimLeft(l, " \t"), "+") &&
!strings.HasPrefix(strings.TrimLeft(l, " \t"), "TODO:") {
res = append(res, l)
}
}
return res
}
func split(doc []string, startMarker, endMarker string) (pre []string, post []string) {
if len(doc) == 0 {
return nil, nil
}
idx := len(doc)
for i, s := range doc {
if s == startMarker {
idx = i
break
}
}
idy := len(doc)
for j, s := range doc {
if j > idx && s == endMarker {
idy = j
break
}
}
pre = doc[0:idx]
if idy < len(doc) {
post = doc[idy+1:]
}
return pre, post
}
func readFile(filename string) (file *os.File, content []string, err error) {
if file, err = os.OpenFile(filename, os.O_RDWR|os.O_CREATE, 0777); err != nil {
return file, content, err
}
bytes, err := ioutil.ReadFile(filename)
if err != nil {
return file, content, err
}
content = strings.Split(string(bytes), "\n")
return file, content, nil
}
func writeFile(file *os.File, content []string) error {
if err := file.Truncate(0); err != nil {
return err
}
max := 0
for i, line := range content {
if line != "" {
max = i
}
}
for i, line := range content {
if i <= max {
if _, err := file.WriteString(line + "\n"); err != nil {
return err
}
}
}
return nil
}
func isPlatformTrait(traitID string) bool {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
catalog := trait.NewCatalog(ctx, nil)
t := catalog.GetTrait(traitID)
return t.IsPlatformTrait()
}
func determineProfiles(traitID string) (profiles []string) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
catalog := trait.NewCatalog(ctx, nil)
for _, p := range v1.AllTraitProfiles {
traits := catalog.TraitsForProfile(p)
for _, t := range traits {
if string(t.ID()) == traitID {
profiles = append(profiles, string(p))
}
}
}
return profiles
}