Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 35 additions & 2 deletions cmd/resources/gen_resources.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,49 @@
package main

import (
"bytes"
"go/format"
"io/ioutil"
"log"
"text/template"

"ldcli/cmd/resources"
)

const pathSpecFile = "../ld-teams-openapi.json"
const (
pathSpecFile = "../ld-teams-openapi.json"
pathTemplate = "resources/resource_cmds.tmpl"
templateName = "resource_cmds.tmpl"
pathOutput = "resources/resource_cmds.go"
)

func main() {
log.Println("Generating resources...")
_, err := resources.GetTemplateData(pathSpecFile)
templateData, err := resources.GetTemplateData(pathSpecFile)
if err != nil {
panic(err)
}

tmpl, err := template.New(templateName).ParseFiles(pathTemplate)
if err != nil {
panic(err)
}

var result bytes.Buffer
err = tmpl.Execute(&result, templateData)
if err != nil {
panic(err)
}

// Format the output of the template execution
formatted, err := format.Source(result.Bytes())
if err != nil {
panic(err)
}

// Write the formatted source code to disk
log.Printf("writing %s\n", pathOutput)
err = ioutil.WriteFile(pathOutput, formatted, 0644)
if err != nil {
panic(err)
}
Expand Down
4 changes: 1 addition & 3 deletions cmd/resources/gen_resources_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,5 @@ func TestGetTemplateData(t *testing.T) {
err = json.Unmarshal(expectedFromFile, &expected)
require.NoError(t, err)

t.Run("succeeds with single get resource", func(t *testing.T) {
assert.Equal(t, expected, actual)
})
assert.Equal(t, expected, actual)
}
195 changes: 178 additions & 17 deletions cmd/resources/resource_cmds.go

Large diffs are not rendered by default.

45 changes: 45 additions & 0 deletions cmd/resources/resource_cmds.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// This file is generated by gen_resources.go; DO NOT EDIT.

package resources

import (
"github.com/spf13/cobra"

"ldcli/internal/analytics"
"ldcli/internal/resources"
)

func AddAllResourceCmds(rootCmd *cobra.Command, client resources.Client, analyticsTracker analytics.Tracker) {
// Resource commands
{{ range $resName, $resData := .Resources }}
gen_{{ $resName }}ResourceCmd := NewResourceCmd(
rootCmd,
analyticsTracker,
"{{ $resData.Name }}",
"Make requests (list, create, etc.) on {{ $resData.DisplayName }}",
{{ $resData.Description }},
)
{{ end }}

// Operation commands
{{ range $resName, $resData := .Resources }}{{ range $opName, $opData := $resData.Operations }}
NewOperationCmd(gen_{{ $resName }}ResourceCmd, client, OperationData{
Short: {{ $opData.Short }},
Long: {{ $opData.Long }},
Use: "{{ $opData.Use }}",
Params: []Param{ {{ range $param := $opData.Params }}
{
Name: "{{ $param.Name }}",
In: "{{ $param.In }}",
Description: {{ $param.Description }},
Type: "{{ $param.Type }}",
}, {{ end }}
},
HTTPMethod: "{{ $opData.HTTPMethod }}",
RequiresBody: {{ $opData.RequiresBody }},
Path: "{{ $opData.Path }}",
SupportsSemanticPatch: {{ $opData.SupportsSemanticPatch }},
})

{{ end }}{{ end }}
}
4 changes: 2 additions & 2 deletions cmd/resources/resource_cmds_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ func TestCreateTeam(t *testing.T) {
t.Run("help shows postTeam description", func(t *testing.T) {
args := []string{
"teams",
"create",
"post-team", // temporary command name
"--help",
}

Expand All @@ -27,7 +27,7 @@ func TestCreateTeam(t *testing.T) {
t.Skip("TODO: add back when mock client is added")
args := []string{
"teams",
"create",
"post-team", // temporary command name
"--access-token",
"abcd1234",
"--data",
Expand Down
137 changes: 77 additions & 60 deletions cmd/resources/resources.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ import (
"net/url"
"os"
"regexp"
"strconv"
"strings"

"github.com/getkin/kin-openapi/openapi3"
"github.com/iancoleman/strcase"
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hoping it's ok to use this (..stripe does 🙃)

"github.com/spf13/cobra"
"github.com/spf13/viper"

Expand All @@ -29,6 +29,7 @@ type TemplateData struct {

type ResourceData struct {
Name string
DisplayName string
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can also pass in functions to the template to handle converting these values from camel/kebab, which I might do in a refactor

Description string
Operations map[string]OperationData
}
Expand All @@ -52,6 +53,11 @@ type Param struct {
Required bool
}

func jsonString(s string) string {
bs, _ := json.Marshal(s)
return string(bs)
}

Comment on lines +56 to +60
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we have all kinds of markdown stuff in the descriptions of resources and endpoints that was causing the template to throw errors - marshaling to json and returning that string to help!

func GetTemplateData(fileName string) (TemplateData, error) {
rawFile, err := os.ReadFile(fileName)
if err != nil {
Expand All @@ -66,16 +72,25 @@ func GetTemplateData(fileName string) (TemplateData, error) {

resources := make(map[string]ResourceData)
for _, r := range spec.Tags {
resources[r.Name] = ResourceData{
Name: r.Name,
Description: r.Description,
if strings.Contains(r.Name, "(beta)") {
// skip beta resources for now
continue
}
resources[strcase.ToCamel(r.Name)] = ResourceData{
DisplayName: strings.ToLower(r.Name),
Name: strcase.ToKebab(strings.ToLower(r.Name)),
Description: jsonString(r.Description),
Operations: make(map[string]OperationData, 0),
}
}

for path, pathItem := range spec.Paths.Map() {
for method, op := range pathItem.Operations() {
tag := op.Tags[0] // TODO: confirm each op only has one tag
tag := op.Tags[0] // each op only has one tag
if strings.Contains(tag, "(beta)") {
// skip beta resources for now
continue
}
resource, ok := resources[tag]
if !ok {
log.Printf("Matching resource not found for %s operation's tag: %s", op.OperationID, tag)
Expand All @@ -84,24 +99,30 @@ func GetTemplateData(fileName string) (TemplateData, error) {

use := getCmdUse(method, op, spec)

var supportsSemanticPatch bool
if strings.Contains(op.Description, "semantic patch") {
supportsSemanticPatch = true
}

operation := OperationData{
Short: op.Summary,
Long: op.Description,
Use: use,
Params: make([]Param, 0),
HTTPMethod: method,
RequiresBody: method == "PUT" || method == "POST" || method == "PATCH",
Path: path,
Short: jsonString(op.Summary),
Long: jsonString(op.Description),
Use: use,
Params: make([]Param, 0),
HTTPMethod: method,
RequiresBody: method == "PUT" || method == "POST" || method == "PATCH",
Path: path,
SupportsSemanticPatch: supportsSemanticPatch,
}

for _, p := range op.Parameters {
if p.Value != nil {
// TODO: confirm if we only have one type per param b/c somehow this is a slice
types := *p.Value.Schema.Value.Type
param := Param{
Name: p.Value.Name,
Name: strcase.ToKebab(p.Value.Name),
In: p.Value.In,
Description: p.Value.Description,
Description: jsonString(p.Value.Description),
Type: types[0],
Required: p.Value.Required,
}
Expand All @@ -117,39 +138,42 @@ func GetTemplateData(fileName string) (TemplateData, error) {
}

func getCmdUse(method string, op *openapi3.Operation, spec *openapi3.T) string {
methodMap := map[string]string{
"GET": "get",
"POST": "create",
"PUT": "replace", // TODO: confirm this
"DELETE": "delete",
"PATCH": "update",
}

use := methodMap[method]

var schema *openapi3.SchemaRef
for respType, respInfo := range op.Responses.Map() {
respCode, _ := strconv.Atoi(respType)
if respCode < 300 {
for _, s := range respInfo.Value.Content {
schemaName := strings.TrimPrefix(s.Schema.Ref, "#/components/schemas/")
schema = spec.Components.Schemas[schemaName]
}
}
}

if schema == nil {
// probably won't need to keep this logging in but leaving it for debugging purposes
log.Printf("No response type defined for %s", op.OperationID)
} else {
for propName := range schema.Value.Properties {
if propName == "items" {
use = "list"
break
}
}
}
return use
return strcase.ToKebab(op.OperationID)

// TODO: work with operation ID & response type to stripe out resource name and update post -> create, get -> list, etc.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

see comment in main description - going to bring this back in but for now, to remove duplicate subcommands, using just the OperationId

//methodMap := map[string]string{
// "GET": "get",
// "POST": "create",
// "PUT": "replace", // TODO: confirm this
// "DELETE": "delete",
// "PATCH": "update",
//}
//
//use := methodMap[method]
//
//var schema *openapi3.SchemaRef
//for respType, respInfo := range op.Responses.Map() {
// respCode, _ := strconv.Atoi(respType)
// if respCode < 300 {
// for _, s := range respInfo.Value.Content {
// schemaName := strings.TrimPrefix(s.Schema.Ref, "#/components/schemas/")
// schema = spec.Components.Schemas[schemaName]
// }
// }
//}
//
//if schema == nil {
// // probably won't need to keep this logging in but leaving it for debugging purposes
// log.Printf("No response type defined for %s", op.OperationID)
//} else {
// for propName := range schema.Value.Properties {
// if propName == "items" {
// use = "list"
// break
// }
// }
//}
//return use
}

func NewResourceCmd(parentCmd *cobra.Command, analyticsTracker analytics.Tracker, resourceName, shortDescription, longDescription string) *cobra.Command {
Expand Down Expand Up @@ -200,25 +224,18 @@ func (op *OperationCmd) initFlags() error {
}

for _, p := range op.Params {
shorthand := fmt.Sprintf(p.Name[0:1]) // todo: how do we handle potential dupes
// TODO: consider handling these all as strings
switch p.Type {
case "string":
op.cmd.Flags().StringP(p.Name, shorthand, "", p.Description)
case "int":
op.cmd.Flags().IntP(p.Name, shorthand, 0, p.Description)
case "boolean":
op.cmd.Flags().BoolP(p.Name, shorthand, false, p.Description)
}
flagName := strcase.ToKebab(p.Name)

op.cmd.Flags().String(flagName, "", p.Description)

if p.In == "path" {
err := op.cmd.MarkFlagRequired(p.Name)
if p.In == "path" || p.Required {
err := op.cmd.MarkFlagRequired(flagName)
if err != nil {
return err
}
}

err := viper.BindPFlag(p.Name, op.cmd.Flags().Lookup(p.Name))
err := viper.BindPFlag(flagName, op.cmd.Flags().Lookup(flagName))
if err != nil {
return err
}
Expand Down
Loading