From 1e97b5b8004ee8436df19aadd2303bbd0e81a3d7 Mon Sep 17 00:00:00 2001 From: Ryan Koval Date: Tue, 13 Jul 2021 00:15:45 -0500 Subject: [PATCH] added code generator for searcher --- generate_new_searcher.sh | 6 + generators/searcher/file_writers.go | 135 +++++++++++++++++++ generators/searcher/main.go | 201 ++++++++++++++++++++++++++++ go.mod | 1 + go.sum | 2 + util/file_tools.go | 46 +++++++ 6 files changed, 391 insertions(+) create mode 100755 generate_new_searcher.sh create mode 100644 generators/searcher/file_writers.go create mode 100644 generators/searcher/main.go create mode 100644 util/file_tools.go diff --git a/generate_new_searcher.sh b/generate_new_searcher.sh new file mode 100755 index 00000000..8957e257 --- /dev/null +++ b/generate_new_searcher.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +set -e +go run generators/searcher/*.go $@ +go fmt ./... +./generate.sh +go mod tidy \ No newline at end of file diff --git a/generators/searcher/file_writers.go b/generators/searcher/file_writers.go new file mode 100644 index 00000000..f3d24183 --- /dev/null +++ b/generators/searcher/file_writers.go @@ -0,0 +1,135 @@ +package main + +import ( + "fmt" + "regexp" + "strings" + + "github.com/rkoval/alfred-aws-console-services-workflow/util" +) + +func appendToGenny(searcherNamer SearcherNamer) { + regex := regexp.MustCompile(`(go:generate genny.*)(")`) + typeName := searcherNamer.OperationDefinition.Package + "." + searcherNamer.OperationDefinition.Item + replacement := fmt.Sprintf("$1,%s$2", typeName) + util.ModifyFileWithRegexReplace("caching/caching.go", regex, replacement, typeName) +} + +func appendToSearchers(searcherNamer SearcherNamer) { + regex := regexp.MustCompile(`(\nvar SearchersByServiceId)`) + structInitializer := "&" + searcherNamer.StructName + "{}" + replacement := fmt.Sprintf("var %s = %s\n$1", searcherNamer.StructInstanceName, structInitializer) + replacedContent := util.ModifyFileWithRegexReplace("searchers/searchers_by_service_id.go", regex, replacement, structInitializer) + if replacedContent == "" { + return + } + + regex = regexp.MustCompile(`(,\n)(})`) + replacement = fmt.Sprintf("$1\t\"%ss\": %s$1$2", searcherNamer.NameSnakeCase, searcherNamer.StructInstanceName) + replacedContent = util.ModifyFileWithRegexReplace("searchers/searchers_by_service_id.go", regex, replacement, "") + + if !strings.Contains(replacedContent, fmt.Sprintf("\"%s\"", searcherNamer.ServiceLower)) { + // append root service if this is the first one we're populating + regex = regexp.MustCompile(`(,\n)(})`) + replacement = fmt.Sprintf("$1\t\"%s\": %s$1$2", searcherNamer.ServiceLower, searcherNamer.StructInstanceName) + util.ModifyFileWithRegexReplace("searchers/searchers_by_service_id.go", regex, replacement, "") + } +} + +func writeSearcherFile(searcherNamer SearcherNamer) { + templateString := `package searchers + +import ( + "context" + "fmt" + "log" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/{{ .OperationDefinition.Package }}" + "github.com/aws/aws-sdk-go-v2/service/{{ .OperationDefinition.Package }}/types" + aw "github.com/deanishe/awgo" + "github.com/rkoval/alfred-aws-console-services-workflow/awsworkflow" + "github.com/rkoval/alfred-aws-console-services-workflow/caching" + "github.com/rkoval/alfred-aws-console-services-workflow/util" +) + +type {{ .StructName }} struct{} + +func (s {{ .StructName }}) Search(wf *aw.Workflow, query string, cfg aws.Config, forceFetch bool, fullQuery string) error { + cacheName := util.GetCurrentFilename() + entities := caching.Load{{ .OperationDefinition.PackageTitle }}{{ .OperationDefinition.Item }}ArrayFromCache(wf, cfg, cacheName, s.fetch, forceFetch, fullQuery) + for _, entity := range entities { + s.addToWorkflow(wf, query, cfg, entity) + } + return nil +} + +func (s {{ .StructName }}) fetch(cfg aws.Config) ([]types.{{ .OperationDefinition.Item }}, error) { + client := {{ .OperationDefinition.Package }}.NewFromConfig(cfg) + + entities := []types.{{ .OperationDefinition.Item }}{} + {{if .OperationDefinition.PageInputToken }}pageToken := "" + for { {{ end }} + params := &{{ .OperationDefinition.Package }}.{{ .OperationDefinition.FunctionInput }}{ + {{if .OperationDefinition.PageSize }}{{ .OperationDefinition.PageSize }}: aws.Int32(1000),{{ end }} + } + {{if .OperationDefinition.PageInputToken }}if pageToken != "" { + params.{{ .OperationDefinition.PageInputToken }} = &pageToken + }{{ end }} + resp, err := client.{{ .OperationDefinition.FunctionName }}(context.TODO(), params) + + if err != nil { + return nil, err + } + + for _, entity := range resp.{{ .OperationDefinition.Items }} { + entities = append(entities, entity) + } + + {{if .OperationDefinition.PageOutputToken }}if resp.{{ .OperationDefinition.PageOutputToken }} != nil { + pageToken = *resp.{{ .OperationDefinition.PageOutputToken }} + } else { + break + }{{ end }} + {{if .OperationDefinition.PageInputToken }} }{{ end }} + + return entities, nil +} + +func (s {{ .StructName }}) addToWorkflow(wf *aw.Workflow, query string, config aws.Config, entity types.{{ .OperationDefinition.Item }}) { + log.Println("entity", entity) // TODO remove me + + title := entity.Name + subtitle := "" + + // TODO fill me out + + util.NewURLItem(wf, title). + Subtitle(subtitle). + Arg(fmt.Sprintf( + "https://%s.console.aws.amazon.com/{{ .ServiceLower }}/{{ .EntityLower }}s/?region=%s&tab=overview", + config.Region, + config.Region, + )). + Icon(awsworkflow.GetImageIcon("{{ .ServiceLower }}")). + Valid(true) +}` + + util.WriteTemplateToFile("searcher_file", templateString, fmt.Sprintf("searchers/%ss.go", searcherNamer.NameSnakeCase), searcherNamer) +} + +func writeSearcherTestFile(searcherNamer SearcherNamer) { + templateString := `package searchers + +import ( + "testing" + + "github.com/rkoval/alfred-aws-console-services-workflow/util" +) + +func Test{{ .StructName }}(t *testing.T) { + TestSearcher(t, {{ .StructName }}{}, util.GetCurrentFilename()) +}` + + util.WriteTemplateToFile("searcher_test_file", templateString, fmt.Sprintf("searchers/%ss_test.go", searcherNamer.NameSnakeCase), searcherNamer) +} diff --git a/generators/searcher/main.go b/generators/searcher/main.go new file mode 100644 index 00000000..46c2e20c --- /dev/null +++ b/generators/searcher/main.go @@ -0,0 +1,201 @@ +package main + +import ( + "encoding/json" + "errors" + "flag" + "fmt" + "log" + "os" + "os/exec" + "path/filepath" + "regexp" + "strings" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/iancoleman/strcase" + "github.com/rkoval/alfred-aws-console-services-workflow/parsers" +) + +func init() { + awsServices := parsers.ParseConsoleServicesYml("./console-services.yml") + for _, awsService := range awsServices { + if awsService.ShortName != "" { + strcase.ConfigureAcronym(awsService.ShortName, awsService.ShortName) + } + } + flag.Parse() +} + +type OperationDefinition struct { + Package string + PackageTitle string + FunctionName string + FunctionInput string + Item string + Items string + PageInputToken string + PageOutputToken string + PageSize string +} + +type SearcherNamer struct { + ServiceTitle string + ServiceLower string + EntityTitle string + EntityLower string + Name string + NameLower string + NameCamelCase string + NameSnakeCase string + StructName string + StructInstanceName string + OperationDefinition +} + +func NewSearcherNamer(service, entity string, operationDefinition OperationDefinition) SearcherNamer { + if "s" == entity[len(entity)-1:] { + log.Fatalf("Entity should be singular for casing to work properly") + } + + serviceTitle := strings.Title(service) + entityTitle := strings.Title(entity) + serviceLower := strings.ToLower(service) + name := serviceTitle + entityTitle + + return SearcherNamer{ + ServiceTitle: serviceTitle, + ServiceLower: serviceLower, + EntityTitle: entityTitle, + EntityLower: strings.ToLower(entity), + Name: name, + NameLower: strings.ToLower(name), + NameCamelCase: strcase.ToCamel(name), + NameSnakeCase: strcase.ToSnake(name), + StructName: name + "Searcher", + StructInstanceName: serviceLower + entityTitle + "Searcher", + OperationDefinition: operationDefinition, + } +} + +func main() { + args := flag.Args() + if len(args) < 3 { + usage() + } + + operation := args[2] + pkg, functionName := parseOperation(operation) + goGetPkg(pkg) + + operationDefinition := getOperationDefinition(operation, pkg, functionName) + searcherNamer := NewSearcherNamer(args[0], args[1], operationDefinition) + + appendToGenny(searcherNamer) + appendToSearchers(searcherNamer) + writeSearcherFile(searcherNamer) + writeSearcherTestFile(searcherNamer) +} + +func goGetPkg(pkg string) { + cmd := exec.Command("go", "get", "github.com/aws/"+aws.SDKName+"/service/"+pkg) + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + err := cmd.Run() + if err != nil { + panic(err) + } +} + +func parseOperation(operation string) (string, string) { + operationNameRegex := regexp.MustCompile("com.amazonaws.([a-z]+)#([a-zA-Z]+)") + matches := operationNameRegex.FindStringSubmatch(operation) + if len(matches) != 3 { + log.Fatalln("operation argument must have the form \"com.amazonaws.pkg#FunctionName\"") + } + return matches[1], matches[2] +} + +func getOperationDefinition(operation, pkg, functionName string) OperationDefinition { + gopath, exists := os.LookupEnv("GOPATH") + if !exists { + userHome, err := os.UserHomeDir() + if err != nil { + panic(err) + } + gopath += userHome + "/go" + } + + globPath := gopath + "/pkg/mod/github.com/aws/" + aws.SDKName + "@v" + aws.SDKVersion + "/codegen/sdk-codegen/aws-models/" + pkg + ".*.json" + matches, err := filepath.Glob(globPath) + if err != nil { + panic(err) + } + if len(matches) <= 0 { + panic(errors.New("Unable to find a file with glob \"" + globPath + "\"")) + } else if len(matches) >= 2 { + panic(errors.New("More than one file with glob \"" + globPath + "\"")) + } + filename := matches[0] + + apiJsonRaw, err := os.ReadFile(filename) + if err != nil { + panic(err) + } + + var j interface{} + err = json.Unmarshal(apiJsonRaw, &j) + if err != nil { + panic(err) + } + + definition := getJsonPath(j, "shapes", operation).(map[string]interface{}) + + _, functionInput := parseOperation(getJsonPath(definition, "input", "target").(string)) + functionOutputShape := getJsonPath(definition, "output", "target").(string) + paginated := getJsonPath(definition, "traits", "smithy.api#paginated").(map[string]interface{}) + items := paginated["items"].(string) + + functionOutputItemsShape := getJsonPath(j, "shapes", functionOutputShape, "members", items, "target").(string) + _, item := parseOperation(getJsonPath(j, "shapes", functionOutputItemsShape, "member", "target").(string)) + + operationDefinition := OperationDefinition{ + Package: pkg, + PackageTitle: strings.Title(pkg), + FunctionName: functionName, + FunctionInput: functionInput, + Item: item, + Items: items, + } + + pageInputToken := paginated["inputToken"] + if pageInputToken != nil { + operationDefinition.PageInputToken = pageInputToken.(string) + } + pageOutputToken := paginated["outputToken"] + if pageOutputToken != nil { + operationDefinition.PageOutputToken = pageOutputToken.(string) + } + pageSize := paginated["pageSize"] + if pageSize != nil { + operationDefinition.PageSize = pageSize.(string) + } + + return operationDefinition +} + +func getJsonPath(json interface{}, keys ...string) interface{} { + value := json + for _, key := range keys { + value = value.(map[string]interface{})[key] + } + + return value +} + +func usage() { + flag.Usage() + fmt.Println("go run searcher.go Service Entity com.amazonaws.package#functionName") + os.Exit(1) +} diff --git a/go.mod b/go.mod index fe90c626..0a003a0f 100644 --- a/go.mod +++ b/go.mod @@ -18,6 +18,7 @@ require ( github.com/cheekybits/genny v1.0.0 github.com/deanishe/awgo v0.25.0 github.com/dnaeon/go-vcr v1.0.1 + github.com/iancoleman/strcase v0.1.3 github.com/stretchr/testify v1.6.1 golang.org/x/text v0.3.6 // indirect golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect diff --git a/go.sum b/go.sum index 429d61c5..99976b28 100644 --- a/go.sum +++ b/go.sum @@ -54,6 +54,8 @@ github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyG github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= github.com/google/go-cmp v0.5.4 h1:L8R9j+yAqZuZjsqh/z+F1NCffTKKLShY6zXTItVIZ8M= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/iancoleman/strcase v0.1.3 h1:dJBk1m2/qjL1twPLf68JND55vvivMupZ4wIzE8CTdBw= +github.com/iancoleman/strcase v0.1.3/go.mod h1:SK73tn/9oHe+/Y0h39VT4UCxmurVJkR5NA7kMEAOgSE= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= diff --git a/util/file_tools.go b/util/file_tools.go new file mode 100644 index 00000000..63f810bc --- /dev/null +++ b/util/file_tools.go @@ -0,0 +1,46 @@ +package util + +import ( + "io/ioutil" + "os" + "regexp" + "strings" + "text/template" +) + +func WriteTemplateToFile(templateName, templateString, fileName string, data interface{}) { + t, err := template.New(templateName).Parse(templateString) + if err != nil { + panic(err) + } + + file, err := os.Create(fileName) + if err != nil { + panic(err) + } + defer file.Close() + + err = t.Execute(file, data) + if err != nil { + panic(err) + } +} + +func ModifyFileWithRegexReplace(filename string, regex *regexp.Regexp, replacement string, ignoreIfContains string) string { + c, err := ioutil.ReadFile(filename) + if err != nil { + panic(err) + } + content := string(c) + + if ignoreIfContains != "" && strings.Contains(content, ignoreIfContains) { + return "" + } + + replacedContent := regex.ReplaceAllString(content, replacement) + err = ioutil.WriteFile(filename, []byte(replacedContent), 0600) + if err != nil { + panic(err) + } + return replacedContent +}