Skip to content

Commit

Permalink
feat: create and generate template from template data for teams (#238)
Browse files Browse the repository at this point in the history
* create and generate template from template data

* add supports semantic patch to template and data

* fix test

* fix more tests

* update template
  • Loading branch information
k3llymariee authored May 3, 2024
1 parent f7bf857 commit bf2f0a1
Show file tree
Hide file tree
Showing 17 changed files with 748 additions and 164 deletions.
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"
"github.com/spf13/cobra"
"github.com/spf13/viper"

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

type ResourceData struct {
Name string
DisplayName string
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)
}

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.
//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

0 comments on commit bf2f0a1

Please sign in to comment.