Skip to content

Commit

Permalink
feat: adds prompt for overwriting when scaffold is rerun
Browse files Browse the repository at this point in the history
Currently running `scaffold init` a second time results in an error
message being propogated to the user as the cli output. This is
harmless, as the error regards the files being created already existing,
but not a great UX.

This adds promptui to run an interactive prompt for overwriting the
files. If the user selects to overwite, the command behaves as though it
were run with the `-force` option. If the user doesn't, it ends the
command but doesn't return an error.

Other related changes introduced by this change:

 * The cli app is wrapped in a new VervetApp struct, allowing config
   options specific to vervet but not to the CLI interface to be set--
   one of these options is a prompting interface.
 * The vervet app is added to the context passed into the cli app, so
   that vervet specific info can be used in the commands called by the
   cli app.

Fixes #56
  • Loading branch information
jcsackett committed Nov 12, 2021
1 parent df6018b commit 8d20aec
Show file tree
Hide file tree
Showing 9 changed files with 183 additions and 12 deletions.
44 changes: 44 additions & 0 deletions cmd/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,16 @@
package cmd

import (
"context"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"time"

"github.com/manifoldco/promptui"
"github.com/urfave/cli/v2"
)

Expand All @@ -16,6 +20,7 @@ type VervetParams struct {
Stdin io.ReadCloser
Stdout io.WriteCloser
Stderr io.WriteCloser
Prompt VervetPrompt
}

// VervetApp contains the cli Application.
Expand All @@ -24,6 +29,21 @@ type VervetApp struct {
Params VervetParams
}

// VervetPrompt defines the interface for interactive prompts in vervet.
type VervetPrompt interface {
Confirm(label string) (bool, error)
}

type runKey string

var vervetKey = runKey("vervet")

// Run runs the cli.App with the Vervet config params.
func (v *VervetApp) Run(args []string) error {
context := context.WithValue(context.Background(), vervetKey, v)
return v.App.RunContext(context, args)
}

// NewApp returns a new VervetApp with the provided params.
func NewApp(vp VervetParams) *VervetApp {
return &VervetApp{
Expand Down Expand Up @@ -149,11 +169,35 @@ func NewApp(vp VervetParams) *VervetApp {
}
}

// Prompt is the default interactive prompt for vervet.
type Prompt struct{}

// Confirm implements VervetPrompt.Confirm
func (p Prompt) Confirm(label string) (bool, error) {
prompt := promptui.Prompt{
Label: fmt.Sprintf("%v (y/N)?", label),
Default: "N",
Validate: func(input string) error {
input = strings.ToLower(input)
if input != "n" && input != "y" {
return errors.New("you must pick y or n")
}
return nil
},
}
result, err := prompt.Run()
if err != nil {
return false, err
}
return (result == "y"), nil
}

// Vervet is the vervet application with the CLI application.
var Vervet = NewApp(VervetParams{
Stdin: os.Stdin,
Stdout: os.Stdout,
Stderr: os.Stderr,
Prompt: Prompt{},
})

func absPath(path string) (string, error) {
Expand Down
6 changes: 3 additions & 3 deletions cmd/compiler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import (
func TestCompile(t *testing.T) {
c := qt.New(t)
dstDir := c.TempDir()
err := cmd.Vervet.App.Run([]string{"vervet", "compile", testdata.Path("resources"), dstDir})
err := cmd.Vervet.Run([]string{"vervet", "compile", testdata.Path("resources"), dstDir})
c.Assert(err, qt.IsNil)
tests := []struct {
version string
Expand Down Expand Up @@ -49,7 +49,7 @@ func TestCompile(t *testing.T) {
func TestCompileInclude(t *testing.T) {
c := qt.New(t)
dstDir := c.TempDir()
err := cmd.Vervet.App.Run([]string{"vervet", "compile", "-I", testdata.Path("resources/include.yaml"), testdata.Path("resources"), dstDir})
err := cmd.Vervet.Run([]string{"vervet", "compile", "-I", testdata.Path("resources/include.yaml"), testdata.Path("resources"), dstDir})
c.Assert(err, qt.IsNil)

tests := []struct {
Expand Down Expand Up @@ -87,6 +87,6 @@ func TestCompileInclude(t *testing.T) {
func TestCompileConflict(t *testing.T) {
c := qt.New(t)
dstDir := c.TempDir()
err := cmd.Vervet.App.Run([]string{"vervet", "compile", "../testdata/conflict", dstDir})
err := cmd.Vervet.Run([]string{"vervet", "compile", "../testdata/conflict", dstDir})
c.Assert(err, qt.ErrorMatches, `failed to load spec versions: conflict: .*`)
}
24 changes: 23 additions & 1 deletion cmd/scaffold.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package cmd

import (
"errors"
"fmt"
"os"
"path/filepath"
Expand Down Expand Up @@ -33,7 +34,28 @@ func ScaffoldInit(ctx *cli.Context) error {
return err
}
err = sc.Organize()
if err != nil {
if err == scaffold.ErrAlreadyInitialized {
// If the project files already exist, prompt the user to see if they want to overwrite them.
// TODO: replace using Context.Value directly with a helper func to be used in other commands.
vervetApp, ok := ctx.Context.Value(vervetKey).(*VervetApp)
if !ok {
return errors.New("could not retrieve vervet app from context")
}
prompt := vervetApp.Params.Prompt
overwrite, err := prompt.Confirm("Scaffold already initialized; do you want to overwrite")
if err != nil {
return err
}
if overwrite {
forceFn := scaffold.Force(true)
forceFn(sc)
// If an error happens with --force enabled, something new has gone wrong.
if err = sc.Organize(); err != nil {
return err
}
}
return nil
} else if err != nil {
return err
}
err = sc.Init()
Expand Down
86 changes: 82 additions & 4 deletions cmd/scaffold_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package cmd_test
import (
"io/ioutil"
"os"
"strings"
"testing"

qt "github.com/frankban/quicktest"
Expand All @@ -11,15 +12,92 @@ import (
"github.com/snyk/vervet/testdata"
)

var vervetConfigFile = "./.vervet.yaml"

type testPrompt struct {
ReturnVal bool
}

func (tp *testPrompt) Confirm(label string) (bool, error) {
return tp.ReturnVal, nil
}

var filemark = "bad wolf"

// markFile adds a string to a file so we can check if that file is being overwritten.
func markTestFile(filename string) error {
// Write a string to the file; we should see this string removed when
f, err := os.OpenFile(filename, os.O_WRONLY, 0644)
if err != nil {
return err
}
defer f.Close()

if err != nil {
return err
}
_, err = f.Write([]byte(filemark))
return err
}

// markInFile checks if the filemark is present, determining if the file has been
// overwritten.
func markInFile(filename string) (bool, error) {
content, err := ioutil.ReadFile(vervetConfigFile)
if err != nil {
return false, err
}
return strings.Contains(string(content), "bad wolf"), nil
}

func TestScaffold(t *testing.T) {
c := qt.New(t)
dstDir := c.TempDir()
cd(c, dstDir)
// Create an API project from a scaffold
err := cmd.Vervet.App.Run([]string{"vervet", "scaffold", "init", testdata.Path("test-scaffold")})

prompt := testPrompt{}
testApp := cmd.NewApp(cmd.VervetParams{
Stdin: os.Stdin,
Stdout: os.Stdout,
Stderr: os.Stderr,
Prompt: &prompt,
})

// Running init creates the project files.
err := testApp.Run([]string{"vervet", "scaffold", "init", testdata.Path("test-scaffold")})
c.Assert(err, qt.IsNil)

// Rerunning init asks the user if they want to overwrite; if they say no
// the command ends...
prompt.ReturnVal = false
err = markTestFile(vervetConfigFile)
c.Assert(err, qt.IsNil)
err = testApp.Run([]string{"vervet", "scaffold", "init", testdata.Path("test-scaffold")})
c.Assert(err, qt.IsNil)
fileMarked, err := markInFile(vervetConfigFile)
c.Assert(err, qt.IsNil)
c.Assert(fileMarked, qt.IsTrue)

// ...if the user selects yes, it will overwrite the project files.
prompt.ReturnVal = true
err = testApp.Run([]string{"vervet", "scaffold", "init", testdata.Path("test-scaffold")})
c.Assert(err, qt.IsNil)
fileMarked, err = markInFile(vervetConfigFile)
c.Assert(err, qt.IsNil)
c.Assert(fileMarked, qt.IsFalse)

// Rerunning init with the force option overwrites the project files.
prompt.ReturnVal = false
err = markTestFile(vervetConfigFile)
c.Assert(err, qt.IsNil)
// Generate a new resource version in the project
err = cmd.Vervet.App.Run([]string{"vervet", "version", "new", "--version", "2021-10-01", "v3", "foo"})
err = testApp.Run([]string{"vervet", "scaffold", "init", "--force", testdata.Path("test-scaffold")})
c.Assert(err, qt.IsNil)
fileMarked, err = markInFile(vervetConfigFile)
c.Assert(err, qt.IsNil)
c.Assert(fileMarked, qt.IsFalse)

// A new resource version can be generated in the project after initialization has completed.
err = testApp.Run([]string{"vervet", "version", "new", "--version", "2021-10-01", "v3", "foo"})
c.Assert(err, qt.IsNil)
for _, item := range []string{".vervet/templates/README.tmpl", ".vervet.yaml", ".vervet/extras/foo", ".vervet/extras/bar/bar"} {
_, err = os.Stat(item)
Expand Down
6 changes: 3 additions & 3 deletions cmd/version_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ func TestVersionFiles(t *testing.T) {
defer output.Close()
c.Patch(&os.Stdout, output)
cd(c, testdata.Path("."))
err = cmd.Vervet.App.Run([]string{"vervet", "version", "files"})
err = cmd.Vervet.Run([]string{"vervet", "version", "files"})
c.Assert(err, qt.IsNil)
})
out, err := ioutil.ReadFile(tmpFile)
Expand All @@ -64,7 +64,7 @@ func TestVersionList(t *testing.T) {
defer output.Close()
c.Patch(&os.Stdout, output)
cd(c, testdata.Path("."))
err = cmd.Vervet.App.Run([]string{"vervet", "version", "list"})
err = cmd.Vervet.Run([]string{"vervet", "version", "list"})
c.Assert(err, qt.IsNil)
})
out, err := ioutil.ReadFile(tmpFile)
Expand Down Expand Up @@ -95,7 +95,7 @@ func TestVersionNew(t *testing.T) {
copyToDir(c, testdata.Path(".vervet/resource/version/index.ts.tmpl"), versionTemplateDir)
copyToDir(c, testdata.Path(".vervet/resource/version/spec.yaml.tmpl"), versionTemplateDir)
cd(c, projectDir)
err := cmd.Vervet.App.Run([]string{"vervet", "version", "new", "testdata", "foo"})
err := cmd.Vervet.Run([]string{"vervet", "version", "new", "testdata", "foo"})
c.Assert(err, qt.IsNil)
versions, err := vervet.LoadResourceVersions(filepath.Join(projectDir, "generated", "foo"))
c.Assert(err, qt.IsNil)
Expand Down
2 changes: 1 addition & 1 deletion cmd/vervet/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (
)

func main() {
err := cmd.Vervet.App.Run(os.Args)
err := cmd.Vervet.Run(os.Args)
if err != nil {
log.Fatal(err)
}
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ require (
github.com/google/go-cmp v0.5.5
github.com/google/uuid v1.3.0
github.com/mailru/easyjson v0.7.7 // indirect
github.com/manifoldco/promptui v0.9.0
github.com/mattn/go-runewidth v0.0.13 // indirect
github.com/mitchellh/reflectwalk v1.0.2
github.com/olekukonko/tablewriter v0.0.5
Expand Down
10 changes: 10 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/bmatcuk/doublestar/v4 v4.0.2 h1:X0krlUVAVmtr2cRoTqR8aDMrDqnB36ht8wpWTiQ3jsA=
github.com/bmatcuk/doublestar/v4 v4.0.2/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/cpuguy83/go-md2man/v2 v2.0.1 h1:r/myEWzV9lfsM1tFLgDyu0atFtJ1fXn261LKYj/3DxU=
github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
Expand Down Expand Up @@ -38,6 +44,8 @@ github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA=
github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
Expand All @@ -63,6 +71,8 @@ github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5Cc
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M=
github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b h1:MQE+LT/ABUuuvEZ+YQAMSXindAdUh7slEmAkup74op4=
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
Expand Down
16 changes: 16 additions & 0 deletions internal/scaffold/scaffold.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ import (
"github.com/ghodss/yaml"
)

// ErrAlreadyInitialized is used when scaffolding is being run on a project that is already setup.
var ErrAlreadyInitialized = fmt.Errorf("project files already exist")

// Scaffold defines a Vervet API project scaffold.
type Scaffold struct {
dst, src string
Expand Down Expand Up @@ -85,6 +88,19 @@ func New(dst, src string, options ...Option) (*Scaffold, error) {
func (s *Scaffold) Organize() error {
for dstItem, srcItem := range s.manifest.Organize {
dstPath := filepath.Join(s.dst, dstItem)
// If we're not force overwriting, check if files already exist.
if !s.force {
_, err := os.Stat(dstPath)
if err == nil {
// Project files already exist.
return ErrAlreadyInitialized
}
if !os.IsNotExist(err) {
// Something else went wrong; the file not existing is the desired
// state.
return err
}
}
srcPath := filepath.Join(s.src, srcItem)
err := s.copyItem(dstPath, srcPath)
if err != nil {
Expand Down

0 comments on commit 8d20aec

Please sign in to comment.