Skip to content

Commit

Permalink
Add namespaces, resources and operations subcommands (stripe#87)
Browse files Browse the repository at this point in the history
  • Loading branch information
ob-stripe authored Aug 16, 2019
1 parent cb22ac3 commit 1bce608
Show file tree
Hide file tree
Showing 25 changed files with 1,181 additions and 318 deletions.
3 changes: 1 addition & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ require (
github.com/briandowns/spinner v1.6.1
github.com/golang/protobuf v1.3.2 // indirect
github.com/gorilla/websocket v1.4.0
github.com/iancoleman/strcase v0.0.0-20190422225806-e506e3ef7365
github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect
github.com/logrusorgru/aurora v0.0.0-20190428105938-cea283e61946
github.com/magiconair/properties v1.8.1 // indirect
Expand All @@ -15,8 +16,6 @@ require (
github.com/onsi/ginkgo v1.8.0 // indirect
github.com/onsi/gomega v1.5.0 // indirect
github.com/pelletier/go-toml v1.4.0 // indirect
github.com/shurcooL/httpfs v0.0.0-20190707220628-8d4bc4ba7749 // indirect
github.com/shurcooL/vfsgen v0.0.0-20181202132449-6a9ea43bcacd
github.com/sirupsen/logrus v1.4.2
github.com/spf13/afero v1.2.2 // indirect
github.com/spf13/cobra v0.0.5
Expand Down
6 changes: 2 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/iancoleman/strcase v0.0.0-20190422225806-e506e3ef7365 h1:ECW73yc9MY7935nNYXUkK7Dz17YuSUI9yqRqYS8aBww=
github.com/iancoleman/strcase v0.0.0-20190422225806-e506e3ef7365/go.mod h1:SK73tn/9oHe+/Y0h39VT4UCxmurVJkR5NA7kMEAOgSE=
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
Expand Down Expand Up @@ -113,10 +115,6 @@ github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7z
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
github.com/shurcooL/httpfs v0.0.0-20190707220628-8d4bc4ba7749 h1:bUGsEnyNbVPw06Bs80sCeARAlK8lhwqGyi6UT8ymuGk=
github.com/shurcooL/httpfs v0.0.0-20190707220628-8d4bc4ba7749/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg=
github.com/shurcooL/vfsgen v0.0.0-20181202132449-6a9ea43bcacd h1:ug7PpSOB5RBPK1Kg6qskGBoP3Vnj/aNYFTznWvlkGo0=
github.com/shurcooL/vfsgen v0.0.0-20181202132449-6a9ea43bcacd/go.mod h1:TrYk7fJVaAttu97ZZKrO9UbRa8izdowaMIZcxYMbVaw=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
Expand Down
2 changes: 1 addition & 1 deletion pkg/cmd/delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ To delete a charge:
RunE: gc.reqs.RunRequestsCmd,
}

gc.reqs.InitFlags()
gc.reqs.InitFlags(true)

return gc
}
149 changes: 149 additions & 0 deletions pkg/cmd/gen_resources_cmds.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
//+build gen_resources

package main

import (
"bytes"
"fmt"
"go/format"
"io/ioutil"
"strings"
"text/template"

"github.com/iancoleman/strcase"

"github.com/stripe/stripe-cli/pkg/cmd/resource"
"github.com/stripe/stripe-cli/pkg/spec"
)

type TemplateData struct {
Namespaces map[string]*NamespaceData
}

type NamespaceData struct {
Resources map[string]*ResourceData
}

type ResourceData struct {
Operations map[string]*OperationData
}

type OperationData struct {
Path string
HTTPVerb string
}

const (
pathStripeSpec = "../../api/openapi-spec/spec3.sdk.json"

pathTemplate = "resources_cmds.go.tpl"

pathOutput = "resources_cmds.go"
)

func main() {
// This is the script that generates the `resources.go` file from the
// OpenAPI spec file.

// Load the spec and prepare the template data
templateData, err := getTemplateData()
if err != nil {
panic(err)
}

// Load the template with a custom function map
tmpl := template.Must(template.
// Note that the template name MUST match the file name
New(pathTemplate).
Funcs(template.FuncMap{
// The `ToCamel` function is used to turn snake_case strings to
// CamelCase strings. The template uses this to form Go variable
// names.
"ToCamel": strcase.ToCamel,
}).
ParseFiles(pathTemplate))

// Execute the template
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
fmt.Printf("writing %s\n", pathOutput)
err = ioutil.WriteFile(pathOutput, formatted, 0644)
if err != nil {
panic(err)
}
}

func getTemplateData() (*TemplateData, error) {
data := &TemplateData{
Namespaces: make(map[string]*NamespaceData),
}

// Load the JSON OpenAPI spec
stripeAPI, err := spec.LoadSpec(pathStripeSpec)
if err != nil {
return nil, err
}

// Iterate over every resource schema
for name, schema := range stripeAPI.Components.Schemas {
// Skip resources that don't have any operations
if schema.XStripeOperations == nil {
continue
}

nsName, resName := parseSchemaName(name)

// Iterate over every operation for the resource
for _, op := range *schema.XStripeOperations {
// We're only implementing "service" operations
if op.MethodOn != "service" {
continue
}

// If we haven't seen the namespace before, initialize it
if _, ok := data.Namespaces[nsName]; !ok {
data.Namespaces[nsName] = &NamespaceData{
Resources: make(map[string]*ResourceData),
}
}

// If we haven't seen the resource before, initialize it
resCmdName := resource.GetResourceCmdName(resName)
if _, ok := data.Namespaces[nsName].Resources[resCmdName]; !ok {
data.Namespaces[nsName].Resources[resCmdName] = &ResourceData{
Operations: make(map[string]*OperationData),
}
}

// If we haven't seen the operation before, initialize it
if _, ok := data.Namespaces[nsName].Resources[resCmdName].Operations[op.MethodName]; !ok {
data.Namespaces[nsName].Resources[resCmdName].Operations[op.MethodName] = &OperationData{
Path: op.Path,
HTTPVerb: string(op.Operation),
}
}
}

}

return data, nil
}

func parseSchemaName(name string) (string, string) {
if strings.Contains(name, ".") {
components := strings.SplitN(name, ".", 2)
return components[0], components[1]
}
return "", name
}
2 changes: 1 addition & 1 deletion pkg/cmd/get.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ To get 50 charges:
RunE: gc.reqs.RunRequestsCmd,
}

gc.reqs.InitFlags()
gc.reqs.InitFlags(true)

return gc
}
2 changes: 1 addition & 1 deletion pkg/cmd/post.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ https://stripe.com/docs/api
RunE: gc.reqs.RunRequestsCmd,
}

gc.reqs.InitFlags()
gc.reqs.InitFlags(true)

return gc
}
90 changes: 90 additions & 0 deletions pkg/cmd/resource/namespace.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package resource

import (
"fmt"

"github.com/spf13/cobra"

"github.com/stripe/stripe-cli/pkg/ansi"
)

//
// Public types
//

// NamespaceCmd represents namespace commands. Namespace commands are top-level
// commands that are simply containers for resource commands.
//
// Example of namespaces: `issuing`, `radar`, `terminal`.
type NamespaceCmd struct {
Cmd *cobra.Command
Name string
ResourceCmds map[string]*ResourceCmd
}

//
// Public functions
//

// NewNamespaceCmd returns a new NamespaceCmd.
func NewNamespaceCmd(rootCmd *cobra.Command, namespaceName string) *NamespaceCmd {
cmd := &cobra.Command{
Use: namespaceName,
Annotations: make(map[string]string),
}
cmd.SetUsageTemplate(namespaceUsageTemplate())

// For non-namespaced resources, we create a namespace command with the
// empty string as its name so we can group the resource commands in its
// ResourceCmds map, but we don't actually add the Cobra command as a
// subcommand.
if namespaceName != "" {
rootCmd.AddCommand(cmd)
rootCmd.Annotations[namespaceName] = "namespace"
}

return &NamespaceCmd{
Cmd: cmd,
Name: namespaceName,
ResourceCmds: make(map[string]*ResourceCmd),
}
}

//
// Private functions
//

func namespaceUsageTemplate() string {
return fmt.Sprintf(`%s{{if .Runnable}}
{{.UseLine}}{{end}}{{if .HasAvailableSubCommands}}
{{.CommandPath}} <resource> <operation> [parameters...]{{end}}{{if gt (len .Aliases) 0}}
%s
{{.NameAndAliases}}{{end}}{{if .HasExample}}
%s
{{.Example}}{{end}}{{if .HasAvailableSubCommands}}
%s{{range .Commands}}{{if (or .IsAvailableCommand (eq .Name "help"))}}
{{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableLocalFlags}}
%s
{{WrappedLocalFlagUsages . | trimTrailingWhitespaces}}{{end}}{{if .HasAvailableInheritedFlags}}
%s
{{WrappedInheritedFlagUsages . | trimTrailingWhitespaces}}{{end}}{{if .HasHelpSubCommands}}
%s{{range .Commands}}{{if .IsAdditionalHelpTopicCommand}}
{{rpad .CommandPath .CommandPathPadding}} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableSubCommands}}
Use "{{.CommandPath}} [command] --help" for more information about a command.{{end}}
`,
ansi.Bold("Usage:"),
ansi.Bold("Aliases:"),
ansi.Bold("Examples:"),
ansi.Bold("Available Resources:"),
ansi.Bold("Flags:"),
ansi.Bold("Global Flags:"),
ansi.Bold("Additional help topics:"),
)
}
33 changes: 33 additions & 0 deletions pkg/cmd/resource/namespace_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package resource

import (
"testing"

"github.com/spf13/cobra"
"github.com/stretchr/testify/assert"
)

func TestNewNamespaceCmd_NonEmptyName(t *testing.T) {
rootCmd := &cobra.Command{Annotations: make(map[string]string)}

nsc := NewNamespaceCmd(rootCmd, "foo")

assert.Equal(t, "foo", nsc.Name)
assert.True(t, rootCmd.HasSubCommands())
val, ok := rootCmd.Annotations["foo"]
assert.True(t, ok)
assert.Equal(t, "namespace", val)
assert.Contains(t, nsc.Cmd.UsageTemplate(), "Available Resources")
}

func TestNewNamespaceCmd_EmptyName(t *testing.T) {
rootCmd := &cobra.Command{Annotations: make(map[string]string)}

nsc := NewNamespaceCmd(rootCmd, "")

assert.Equal(t, "", nsc.Name)
assert.False(t, rootCmd.HasSubCommands())
_, ok := rootCmd.Annotations[""]
assert.False(t, ok)
assert.Contains(t, nsc.Cmd.UsageTemplate(), "Available Resources")
}
Loading

0 comments on commit 1bce608

Please sign in to comment.