Skip to content

Commit

Permalink
Refactor stripe samples commands (stripe#652)
Browse files Browse the repository at this point in the history
* Refactor sample commands

* add comments

* Empty string is valid

* Check for more errors

* rewrite sample create as goroutine with channels

* Add comments

* return on error

* add default case for safety
  • Loading branch information
vcheung-stripe authored May 5, 2021
1 parent 3fbf545 commit 7a58548
Show file tree
Hide file tree
Showing 6 changed files with 485 additions and 251 deletions.
216 changes: 125 additions & 91 deletions pkg/cmd/samples/create.go
Original file line number Diff line number Diff line change
@@ -1,22 +1,18 @@
package samples

import (
"errors"
"fmt"
"os"
"os/signal"

log "github.com/sirupsen/logrus"
"github.com/spf13/afero"
"github.com/manifoldco/promptui"
"github.com/spf13/cobra"

"github.com/stripe/stripe-cli/pkg/ansi"
"github.com/stripe/stripe-cli/pkg/config"
gitpkg "github.com/stripe/stripe-cli/pkg/git"
"github.com/stripe/stripe-cli/pkg/samples"
"github.com/stripe/stripe-cli/pkg/validators"
"github.com/stripe/stripe-cli/pkg/version"

"gopkg.in/src-d/go-git.v4"
)

// CreateCmd wraps the `create` command for samples which generates a new
Expand Down Expand Up @@ -59,125 +55,163 @@ func (cc *CreateCmd) runCreateCmd(cmd *cobra.Command, args []string) error {
return nil
}

sample := samples.Samples{
Config: cc.cfg,
Fs: afero.NewOsFs(),
Git: gitpkg.Operations{},
}

if _, ok := sample.GetSamples("create")[args[0]]; !ok {
errorMessage := fmt.Sprintf(`The sample provided is not currently supported by the CLI: %s
To see supported samples, run 'stripe samples list'`, args[0])
return fmt.Errorf(errorMessage)
}

selectedSample := args[0]
color := ansi.Color(os.Stdout)

destination := selectedSample
if len(args) > 1 {
destination = args[1]
}

exists, _ := afero.DirExists(sample.Fs, destination)
if exists {
return fmt.Errorf("Path already exists for: %s", destination)
}

color := ansi.Color(os.Stdout)
spinner := ansi.StartNewSpinner(fmt.Sprintf("Downloading %s", selectedSample), os.Stdout)

if cc.forceRefresh {
err := sample.DeleteCache(selectedSample)
if err != nil {
logger := log.Logger{
Out: os.Stdout,
}

logger.WithFields(log.Fields{
"prefix": "samples.create.forceRefresh",
"error": err,
}).Debug("Could not clear cache")
}
}

// Initialize the selected sample in the local cache directory.
// This will either clone or update the specified sample,
// depending on whether or not it's. Additionally, this
// identifies if the sample has multiple integrations and what
// languages it supports.
err := sample.Initialize(selectedSample)
sampleConfig, err := samples.GetSampleConfig(selectedSample, cc.forceRefresh)
if err != nil {
switch e := err.Error(); e {
case git.NoErrAlreadyUpToDate.Error():
// Repo is already up to date. This isn't a program
// error to continue as normal
break
case git.ErrRepositoryAlreadyExists.Error():
// If the repository already exists and we don't pull
// for some reason, that's fine as we can use the existing
// repository
break
default:
ansi.StopSpinner(spinner, "An error occurred.", os.Stdout)
return err
}
ansi.StopSpinner(spinner, "", os.Stdout)
return err
}

ansi.StopSpinner(spinner, "", os.Stdout)
fmt.Printf("%s %s\n", color.Green("✔"), ansi.Faint("Finished downloading"))

// Once we've initialized the sample in the local cache
// directory, the user needs to select which integration they
// want to work with (if selectedSamplelicable) and which language they
// want to copy
err = sample.SelectOptions()
selectedConfig, err := promptSampleConfig(sampleConfig)
if err != nil {
return err
}

// Setup to intercept ctrl+c
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt)
resultChan := make(chan samples.CreationResult)

go samples.Create(
cc.cfg,
selectedSample,
selectedConfig,
destination,
cc.forceRefresh,
resultChan,
)

for res := range resultChan {
if res.Err != nil {
ansi.StopSpinner(spinner, "", os.Stdout)
return res.Err
}

switch res.State {
case samples.WillCopy:
spinner = ansi.StartNewSpinner(fmt.Sprintf("Copying files over... %s", destination), os.Stdout)
case samples.DidCopy:
ansi.StopSpinner(spinner, "", os.Stdout)
fmt.Printf("%s %s\n", color.Green("✔"), ansi.Faint("Files copied"))
case samples.WillConfigure:
spinner = ansi.StartNewSpinner(fmt.Sprintf("Configuring your code... %s", selectedSample), os.Stdout)
case samples.DidConfigure:
ansi.StopSpinner(spinner, "", os.Stdout)
fmt.Printf("%s %s\n", color.Green("✔"), ansi.Faint("Project configured"))
case samples.Done:
fmt.Println("You're all set. To get started: cd", destination)
if res.PostInstall != "" {
fmt.Println(res.PostInstall)
}
default:
return errors.New("an unknown error occurred during sample creation")
}
}

return nil
}

go func() {
<-c
sample.Cleanup(selectedSample)
os.Exit(1)
}()
// promptSampleConfig prompts the user to select the integration they want to use
// (if available) and the language they want the integration to be.
func promptSampleConfig(sampleConfig *samples.SampleConfig) (*samples.SelectedConfig, error) {
var selectedConfig samples.SelectedConfig

spinner = ansi.StartNewSpinner(fmt.Sprintf("Copying files over... %s", destination), os.Stdout)
// Create the target folder to copy the sample in to. We do
// this here in case any of the steps above fail, minimizing
// the change that we create a dangling empty folder
targetPath, err := sample.MakeFolder(destination)
if err != nil {
return err
if sampleConfig.HasIntegrations() {
integration, err := integrationSelectPrompt(sampleConfig)
if err != nil {
return nil, err
}
selectedConfig.Integration = integration
} else {
selectedConfig.Integration = &sampleConfig.Integrations[0]
}

// Perform the copy of the sample given the selected options
// from the selections above
err = sample.Copy(targetPath)
if selectedConfig.Integration.HasMultipleClients() {
client, err := clientSelectPrompt(selectedConfig.Integration.Clients)
if err != nil {
return nil, err
}
selectedConfig.Client = client
} else {
selectedConfig.Client = ""
}

if selectedConfig.Integration.HasMultipleServers() {
server, err := serverSelectPrompt(selectedConfig.Integration.Servers)
if err != nil {
return nil, err
}
selectedConfig.Server = server
} else {
selectedConfig.Server = ""
}

return &selectedConfig, nil
}

func selectOptions(template, label string, options []string) (string, error) {
color := ansi.Color(os.Stdout)

templates := &promptui.SelectTemplates{
Selected: color.Green("✔").String() + ansi.Faint(fmt.Sprintf(" Selected %s: {{ . | bold }} ", template)),
}
prompt := promptui.Select{
Label: label,
Items: options,
Templates: templates,
}

_, result, err := prompt.Run()

if err != nil {
return err
return "", err
}

ansi.StopSpinner(spinner, "", os.Stdout)
fmt.Printf("%s %s\n", color.Green("✔"), ansi.Faint("Files copied"))
return result, nil
}

func clientSelectPrompt(clients []string) (string, error) {
selected, err := selectOptions("client", "Which client would you like to use", clients)
if err != nil {
return "", err
}

spinner = ansi.StartNewSpinner(fmt.Sprintf("Configuring your code... %s", selectedSample), os.Stdout)
return selected, nil
}

err = sample.ConfigureDotEnv(targetPath)
func integrationSelectPrompt(sc *samples.SampleConfig) (*samples.SampleConfigIntegration, error) {
selected, err := selectOptions("integration", "What type of integration would you like to use", sc.IntegrationNames())
if err != nil {
return err
return nil, err
}

ansi.StopSpinner(spinner, "", os.Stdout)
fmt.Printf("%s %s\n", color.Green("✔"), ansi.Faint("Project configured"))
fmt.Println("You're all set. To get started: cd", destination)
var selectedIntegration *samples.SampleConfigIntegration

if sample.PostInstall() != "" {
fmt.Println(sample.PostInstall())
for i, integration := range sc.Integrations {
if integration.Name == selected {
selectedIntegration = &sc.Integrations[i]
}
}

return nil
return selectedIntegration, nil
}

func serverSelectPrompt(servers []string) (string, error) {
selected, err := selectOptions("server", "What server would you like to use", servers)
if err != nil {
return "", err
}

return selected, nil
}
25 changes: 15 additions & 10 deletions pkg/cmd/samples/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@ package samples

import (
"fmt"
"os"
"sort"

"github.com/spf13/afero"
"github.com/spf13/cobra"

gitpkg "github.com/stripe/stripe-cli/pkg/git"
"github.com/stripe/stripe-cli/pkg/ansi"
"github.com/stripe/stripe-cli/pkg/samples"
"github.com/stripe/stripe-cli/pkg/validators"
)
Expand All @@ -27,22 +27,25 @@ func NewListCmd() *ListCmd {
Short: "List Stripe Samples supported by the CLI",
Long: `A list of available Stripe Sample integrations that can be setup and bootstrap by
the CLI.`,
Run: listCmd.runListCmd,
RunE: listCmd.runListCmd,
}

return listCmd
}

func (lc *ListCmd) runListCmd(cmd *cobra.Command, args []string) {
sample := samples.Samples{
Fs: afero.NewOsFs(),
Git: gitpkg.Operations{},
}

func (lc *ListCmd) runListCmd(cmd *cobra.Command, args []string) error {
fmt.Println("A list of available Stripe Samples:")
fmt.Println()

list := sample.GetSamples("list")
spinner := ansi.StartNewSpinner("Loading...", os.Stdout)

list, err := samples.GetSamples("list")
if err != nil {
ansi.StopSpinner(spinner, "Error: please check your internet connection and try again!", os.Stdout)
return err
}
ansi.StopSpinner(spinner, "", os.Stdout)

names := samples.Names(list)
sort.Strings(names)

Expand All @@ -52,4 +55,6 @@ func (lc *ListCmd) runListCmd(cmd *cobra.Command, args []string) {
fmt.Printf("Repo: %s\n", list[name].URL)
fmt.Println()
}

return nil
}
Loading

0 comments on commit 7a58548

Please sign in to comment.