Skip to content

Commit

Permalink
feat: add file prompts and mocks
Browse files Browse the repository at this point in the history
  • Loading branch information
ahmad-ibra committed Jun 11, 2024
1 parent e3ee453 commit 4c5a635
Show file tree
Hide file tree
Showing 2 changed files with 168 additions and 0 deletions.
136 changes: 136 additions & 0 deletions prompts/file.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
package prompts

import (
"bytes"
"errors"
"fmt"
"os"
"os/exec"
"strings"
"time"

"github.com/spectrocloud-labs/prompts-tui/prompts/mocks"
)

var (
editorBinary = "vi"
editorPath = ""
GetCmdExecutor = getEditorExecutor
)

func init() {
visual := os.Getenv("VISUAL")
editor := os.Getenv("EDITOR")
if visual != "" {
editorBinary = visual
logger.Info(fmt.Sprintf("Detected VISUAL env var. Overrode default editor (vi) with %s.", editorBinary))
} else if editor != "" {
editorBinary = editor
logger.Info(fmt.Sprintf("Detected EDITOR env var. Overrode default editor (vi) with %s.", editorBinary))
}
var err error
editorPath, err = exec.LookPath(editorBinary)
if err != nil {
logger.Info(fmt.Sprintf("Error: %s not found on PATH. Either install vi or export VISUAL or EDITOR to an editor of your choosing.", editorBinary))
os.Exit(1)
}
}

func getEditorExecutor(editor, filename string) mocks.CommandExecutor {
cmd := exec.Command(editor, filename)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
return cmd
}

func EditFile(initialContent []byte) ([]byte, error) {
tmpFile, err := os.CreateTemp(os.TempDir(), "validator")
if err != nil {
return nil, err
}
filename := tmpFile.Name()
if err := tmpFile.Close(); err != nil {
return nil, err
}

if initialContent != nil {
if err := os.WriteFile(filename, initialContent, 0600); err != nil {
return nil, err
}
}

cmd := GetCmdExecutor(editorPath, filename)
if err := cmd.Start(); err != nil {
return nil, err
}
if err := cmd.Wait(); err != nil {
return nil, err
}

data, err := os.ReadFile(filename) //#nosec
if err != nil {
return nil, err
}
if err := os.Remove(filename); err != nil {
return nil, err
}
return data, nil
}

// EditFileValidated prompts a user to edit a file with a predefined prompt, initial content, and separator.
// An optional validation function can be specified to validate the content of each line.
// Entries within the file must be newline-separated. Additionally, a minimum number of entries can be specified.
// The values on each line are joined by the separator and returned to the caller.
func EditFileValidated(prompt, content, separator string, validate func(input string) error, minEntries int) (string, error) {

Check failure on line 85 in prompts/file.go

View workflow job for this annotation

GitHub Actions / lint-and-test

calculated cyclomatic complexity for function EditFileValidated is 15, max is 12 (cyclop)
if separator == "" {
return "", errors.New("a non-empty separator is required")
}

for {
var partsBytes []byte
if content != "" {
parts := bytes.Split([]byte(content), []byte(separator))
partsBytes = bytes.Join(parts, []byte("\n"))
}

partsBytes, err := EditFile(append([]byte(prompt), partsBytes...))
if err != nil {
return content, err
}
lines := strings.Split(string(partsBytes), "\n")

// Parse final lines, skipping comments and optionally validating each line
finalLines := make([]string, 0)
for _, l := range lines {
l = strings.TrimSpace(l)
if l != "" && !strings.HasPrefix(l, "#") {
if validate != nil {
if err = validate(l); err != nil {
break
}
}
finalLines = append(finalLines, l)
}
}

if err != nil && errors.Is(err, ValidationError) {
// for integration tests, return the error
if os.Getenv("IS_TEST") == "true" {
return "", err
}
// otherwise, we assume the validation function logged
// a meaningful error message and let the user try again
time.Sleep(5 * time.Second)
continue
}
if minEntries > 0 && len(finalLines) < minEntries {
logger.Info(fmt.Sprintf("Error editing file: %d or more entries are required", minEntries))
time.Sleep(5 * time.Second)
continue
}

content = strings.TrimRight(strings.Join(finalLines, separator), separator)
return content, err
}
}
32 changes: 32 additions & 0 deletions prompts/mocks/file_editor_mocks.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package mocks

import (
"os"
)

type CommandExecutor interface {
Start() error
Wait() error
}

type MockFileEditor struct {
FileContents []string
filename string
}

func (m *MockFileEditor) Start() error {
if err := os.WriteFile(m.filename, []byte(m.FileContents[0]), 0600); err != nil {
return err
}
m.FileContents = m.FileContents[1:]
return nil
}

func (m *MockFileEditor) Wait() error {
return nil
}

func (m *MockFileEditor) GetCmdExecutor(vimPath string, filename string) CommandExecutor {
m.filename = filename
return m
}

0 comments on commit 4c5a635

Please sign in to comment.