ekristen/aws-nuke

View on GitHub
tools/generate-docs/docs.go

Summary

Maintainability
B
4 hrs
Test Coverage
package main

import (
    "bytes"
    "embed"
    "fmt"
    "os"
    "path/filepath"
    "regexp"
    "slices"
    "strings"
    "text/template"

    "github.com/sirupsen/logrus"
    "github.com/urfave/cli/v2"

    "golang.org/x/text/cases"
    "golang.org/x/text/language"

    "github.com/ekristen/libnuke/pkg/docs"
    "github.com/ekristen/libnuke/pkg/registry"

    "github.com/ekristen/aws-nuke/v3/pkg/common"

    _ "github.com/ekristen/aws-nuke/v3/resources"
)

//go:embed files/*
var ResourceTemplates embed.FS

var SkipCamelCase = []string{
    "ELBv2",
    "IoT",
}

type TemplateData struct {
    Name                string
    Description         string
    Properties          map[string]string
    Settings            []string
    DependsOn           []string
    DeprecatedAliases   []string
    AlternativeResource string
}

func execute(c *cli.Context) error { //nolint:funlen,gocyclo
    var regs registry.Registrations

    if c.String("resource") == "all" {
        regs = registry.GetRegistrations()
    } else if c.String("resource") != "" {
        regs = registry.Registrations{
            c.String("resource"): registry.GetRegistration(c.String("resource")),
        }
    }

    for _, reg := range regs {
        if reg.Resource == nil {
            continue
        }

        data := TemplateData{
            Name:                reg.Name,
            Properties:          docs.GeneratePropertiesMap(reg.Resource),
            Settings:            reg.Settings,
            DependsOn:           reg.DependsOn,
            DeprecatedAliases:   reg.DeprecatedAliases,
            AlternativeResource: reg.AlternativeResource,
        }

        rawTmpl, err := ResourceTemplates.ReadFile("files/resource.gomd")
        if err != nil {
            return err
        }

        funcMap := template.FuncMap{
            "KebabCase":      KebabCase,
            "SplitCamelCase": SplitCamelCase,
            "ToLower":        toLower,
        }

        tmpl, err := template.New("example").Funcs(funcMap).Parse(string(rawTmpl))
        if err != nil {
            return err
        }

        var buf bytes.Buffer

        err = tmpl.Execute(&buf, data)
        if err != nil {
            return err
        }

        if c.Bool("write-to-disk") {
            filename := KebabCase(strings.ToLower(SplitCamelCase(reg.Name)))
            filename = strings.Replace(filename, "io-t", "iot-", 1)
            filename = strings.Replace(filename, "iamsaml-", "iam-saml-", 1)
            filename = strings.Replace(filename, "wa-fv2-", "wafv2-", 1)
            filename = strings.Replace(filename, "f-sx", "fsx-", 1)
            filename = strings.Replace(filename, "el-bv2-", "elbv2-", 1)
            filename = strings.Replace(filename, "x-ray-", "xray-", 1)
            filename = strings.Replace(filename, "x-ray-", "xray-", 1)

            err := os.WriteFile(
                fmt.Sprintf("docs/resources/%s.md", filename), buf.Bytes(), 0600)
            if err != nil {
                return err
            }

            fmt.Printf("Wrote docs/resources/%s.md\n", filename)

            continue
        }

        fmt.Println(buf.String())
    }

    mkdocs, err := os.ReadFile("mkdocs.yml")
    if err != nil {
        return err
    }

    resources, err := filepath.Glob("docs/resources/*.md")
    if err != nil {
        return err
    }

    var newResources []string

    for _, resource := range resources {
        if strings.Contains(resource, "overview") {
            continue
        }

        newResource := strings.Replace(resource, "docs/", "", 1)
        filename := filepath.Base(resource)
        title := strings.Join(strings.Split(filename, "-"), " ")
        title = strings.Replace(title, ".md", "", 1)
        title = cases.Title(language.English).String(title)
        title = strings.Replace(title, "Ec2", "EC2", 1)
        title = strings.Replace(title, "Iam", "IAM", 1)
        title = strings.Replace(title, "Sqs", "SQS", 1)
        title = strings.Replace(title, "Ssm", "SSM", 1)
        title = strings.Replace(title, "Kms", "KMS", 1)
        title = strings.Replace(title, "Ebs", "EBS", 1)
        title = strings.Replace(title, "Efs", "EFS", 1)
        title = strings.Replace(title, "Rds", "RDS", 1)
        title = strings.Replace(title, "Vpc", "VPC", 1)
        title = strings.Replace(title, "Acmpca", "ACMPCA", 1)
        title = strings.Replace(title, "Acm", "ACM", 1)
        title = strings.Replace(title, "Sns", "SNS", 1)
        title = strings.Replace(title, "Elb", "ELB", 1)
        title = strings.Replace(title, "Eks", "EKS", 1)
        title = strings.Replace(title, "Aws", "AWS", 1)
        title = strings.Replace(title, "Waf", "WAF", 1)
        title = strings.Replace(title, "Xray", "XRay", 1)

        newResources = append(newResources, fmt.Sprintf("%s: %s", title, newResource))
    }

    slices.Sort(newResources)
    newResources = append([]string{"Overview: resources/overview.md"}, newResources...)
    newMkdocs := updateResources(string(mkdocs), newResources)

    if c.Bool("write-to-disk") {
        err := os.WriteFile("mkdocs.yml", []byte(newMkdocs), 0600)
        if err != nil {
            return err
        }

        fmt.Println("Wrote mkdocs.yml")

        return nil
    }

    fmt.Println(newMkdocs)

    return nil
}

func main() {
    flags := []cli.Flag{
        &cli.StringFlag{
            Name:  "resource",
            Value: "all",
        },
        &cli.BoolFlag{
            Name:    "write-to-disk",
            Aliases: []string{"write"},
        },
    }

    defer func() {
        if r := recover(); r != nil {
            // log panics forces exit
            if _, ok := r.(*logrus.Entry); ok {
                os.Exit(1)
            }
            panic(r)
        }
    }()

    app := cli.NewApp()
    app.Name = "generate-docs"
    app.Usage = "generate resource docs from code"
    app.Version = common.AppVersion.Summary
    app.Authors = []*cli.Author{
        {
            Name:  "Erik Kristensen",
            Email: "erik@erikkristensen.com",
        },
    }
    app.Flags = flags
    app.Action = execute

    if err := app.Run(os.Args); err != nil {
        logrus.Fatal(err)
    }
}

var (
    spaces      = regexp.MustCompile(`\s+`)
    nonAlphaNum = regexp.MustCompile(`[^\pL\pN]+`)
)

// KebabCase -
func KebabCase(in string) string {
    s := casePrepare(in)
    return spaces.ReplaceAllString(s, "-")
}

func casePrepare(in string) string {
    in = strings.TrimSpace(in)
    s := strings.ToLower(in)
    // make sure the first letter remains lower- or upper-cased
    s = strings.Replace(s, string(s[0]), string(in[0]), 1)
    s = nonAlphaNum.ReplaceAllString(s, " ")
    return strings.TrimSpace(s)
}

func toLower(in string) string {
    return strings.ToLower(in)
}

func SplitCamelCase(input string) string {
    if slices.Contains(SkipCamelCase, input) {
        return input
    }

    // Regular expression to find boundaries between lowercase and uppercase letters,
    // and between sequences of uppercase letters followed by lowercase letters.
    re := regexp.MustCompile(`([a-z])([A-Z0-9])|([A-Z]+)([A-Z][a-z])|(\d)([A-Z])`)
    // Replace boundaries with a space followed by the uppercase letter.

    boundaries := re.ReplaceAllString(input, "${1}${3}${5} ${2}${4}${6}")
    boundaries = strings.Replace(boundaries, "io-t", "iot-", 1)
    boundaries = strings.Replace(boundaries, "iamsaml-", "iam-saml-", 1)
    boundaries = strings.Replace(boundaries, "wa-fv2-", "wafv2-", 1)
    boundaries = strings.Replace(boundaries, "f-sx", "fsx-", 1)
    boundaries = strings.Replace(boundaries, "el-bv2-", "elbv2-", 1)
    boundaries = strings.Replace(boundaries, "x-ray-", "xray-", 1)
    boundaries = strings.Replace(boundaries, "x-ray-", "xray-", 1)

    return boundaries
}

// Function to update the 'Resources' section with new list values
func updateResources(markdown string, newResources []string) string {
    // Define the regex to match the 'Resources:' section and the following list
    re := regexp.MustCompile(`(?ms)(^\s*- Resources:(?:\n\s*-\s.+)+)`)

    // Join new resource list as markdown format
    newList := "  - Resources:\n"
    for _, resource := range newResources {
        newList += fmt.Sprintf("    - %s\n", resource)
    }

    // Replace the matched 'Resources' list with the new list
    updatedMarkdown := re.ReplaceAllString(markdown, newList)
    return updatedMarkdown
}