Skip to content

Commit

Permalink
added code generator for searcher
Browse files Browse the repository at this point in the history
  • Loading branch information
rkoval committed Jul 13, 2021
1 parent eeb3a0c commit 1e97b5b
Show file tree
Hide file tree
Showing 6 changed files with 391 additions and 0 deletions.
6 changes: 6 additions & 0 deletions generate_new_searcher.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#!/usr/bin/env bash
set -e
go run generators/searcher/*.go $@
go fmt ./...
./generate.sh
go mod tidy
135 changes: 135 additions & 0 deletions generators/searcher/file_writers.go
Original file line number Diff line number Diff line change
@@ -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)
}
201 changes: 201 additions & 0 deletions generators/searcher/main.go
Original file line number Diff line number Diff line change
@@ -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)
}
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
Loading

0 comments on commit 1e97b5b

Please sign in to comment.