Skip to content

Commit

Permalink
Merge pull request #2 from techiecaro/cli-completion-local
Browse files Browse the repository at this point in the history
Cli completion for local and S3
  • Loading branch information
mrfuxi authored Nov 23, 2021
2 parents 3dee2ec + ff010aa commit 90d4e87
Show file tree
Hide file tree
Showing 11 changed files with 829 additions and 49 deletions.
42 changes: 42 additions & 0 deletions cli/cli.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package cli

import (
"net/url"
"techiecaro/remblob/core"

"github.com/willabides/kongplete"
)

type editCmd struct {
SourcePath *url.URL `arg:"" name:"source_path" help:"Location of the file to edit." predictor:"path"`
DestinationPath *url.URL `arg:"" name:"destination_path" optional:"" help:"Final location of the edited file, if different." predictor:"path"`
}

func (e editCmd) GetDestinationPath() *url.URL {
if e.DestinationPath != nil {
return e.DestinationPath
}
return e.SourcePath
}

func (e editCmd) Run() error {
return core.Edit(*e.SourcePath, *e.GetDestinationPath())
}

type viewCmd struct {
SourcePath *url.URL `arg:"" name:"source_path" help:"Location of the file to view." predictor:"path"`
}

func (v viewCmd) Run() error {
return core.View(*v.SourcePath)
}

var Cli struct {
Edit editCmd `cmd help:"Edits a remote blob and optionally stores it elsewhere."`
View viewCmd `cmd help:"Views a remote blob."`

// Competion
// Use hidden empty command so the Kong does not complain on prompt
Hidden struct{} `cmd:"" help:"A hidden command" hidden:""`
InstallCompletions kongplete.InstallCompletions `cmd:"" help:"install shell completions"`
}
55 changes: 55 additions & 0 deletions cli/completion.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package cli

import (
"fmt"
"net/url"
"os"
"techiecaro/remblob/storage"

"github.com/alecthomas/kong"
"github.com/posener/complete"
"github.com/willabides/kongplete"
)

func NewPathPredictor() complete.Predictor {
return PathPredictor(storage.GetFileListerPrefixes())
}

type PathPredictor []string

func (p PathPredictor) Predict(args complete.Args) []string {

pathPredictions := p.matchFileLister(args.Last)

return append(p, pathPredictions...)
}

func (p PathPredictor) matchFileLister(pattern string) []string {
if pattern == "" {
return []string{}
}

prefixURL, err := url.Parse(pattern)
if err != nil {
fmt.Fprintf(os.Stderr, "Can't parse %s\n", prefixURL)
return []string{}
}

lister := storage.GetFileLister(*prefixURL)
matchesURL := lister(*prefixURL)
matches := make([]string, len(matchesURL))
for i, match := range matchesURL {
matches[i] = match.String()
}

return matches
}

// AddCompletion adds cli completion to an exising Kong parer
func AddCompletion(parser *kong.Kong) {
// Run kongplete.Complete to handle completion requests
kongplete.Complete(
parser,
kongplete.WithPredictor("path", NewPathPredictor()),
)
}
90 changes: 90 additions & 0 deletions cli/completion_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package cli_test

import (
"os"
"path"
"techiecaro/remblob/cli"
"testing"

"github.com/posener/complete"
"github.com/stretchr/testify/assert"
)

func createTestFileStructure(t *testing.T) string {
dir := t.TempDir()

var files = []string{"1.txt", "2.txt", "a/a1.txt"}

for _, name := range files {
fullPath := path.Join(dir, name)
os.MkdirAll(path.Dir(fullPath), 0700)
if _, err := os.Create(fullPath); err != nil {
t.Fatal(err)
}
}

return dir
}

func mustGetCWD(t *testing.T) string {
dir, err := os.Getwd()
if err != nil {
t.Fatal(err)
}
return dir
}

func TestPathPredictor(t *testing.T) {
cases := []struct {
prefix string
expected []string
}{
{
prefix: "",
expected: []string{"./", "file://", "s3://"},
},
{
prefix: ".",
expected: []string{"./", "file://", "s3://", "./1.txt", "./2.txt", "./a"},
},
{
prefix: "a/",
expected: []string{"./", "file://", "s3://", "a/a1.txt"},
},
{
prefix: "./a/",
expected: []string{"./", "file://", "s3://", "./a/a1.txt"},
},
{
prefix: "file://",
expected: []string{"./", "file://", "s3://", "file://1.txt", "file://2.txt", "file://a"},
},
{
prefix: "file://a",
expected: []string{"./", "file://", "s3://", "file://a/a1.txt"},
},
}

for _, tc := range cases {
t.Run(tc.prefix, func(t *testing.T) {
dir := createTestFileStructure(t)

cwd := mustGetCWD(t)
os.Chdir(dir)
defer os.Chdir(cwd)

args := complete.Args{
Last: tc.prefix,
All: []string{"not-in-use"},
Completed: []string{"not-in-use"},
LastCompleted: "not-in-use",
}

predictor := cli.NewPathPredictor()

suggestions := predictor.Predict(args)

assert.Equal(t, tc.expected, suggestions, "Invalid prompt")
})
}
}
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ require (
github.com/aws/aws-sdk-go-v2/config v1.8.1
github.com/aws/aws-sdk-go-v2/service/s3 v1.12.0
github.com/posener/complete v1.2.3
github.com/stretchr/testify v1.7.0 // indirect
github.com/willabides/kongplete v0.2.0
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
)
38 changes: 4 additions & 34 deletions main.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
package main

import (
"net/url"
"os"
"techiecaro/remblob/core"
"techiecaro/remblob/cli"

"github.com/alecthomas/kong"
"github.com/willabides/kongplete"
)

const appName = "remblob"
Expand All @@ -19,44 +17,16 @@ const appDescription = `
remblob view s3://a-bucket/path/blob.json
`

type editCmd struct {
SourcePath *url.URL `arg:"" name:"source_path" help:"Location of the file to edit."`
DestinationPath *url.URL `arg:"" name:"destination_path" optional:"" help:"Final location of the edited file, if different."`
}

func (e editCmd) GetDestinationPath() *url.URL {
if e.DestinationPath != nil {
return e.DestinationPath
}
return e.SourcePath
}

func (e editCmd) Run() error {
return core.Edit(*e.SourcePath, *e.GetDestinationPath())
}

type viewCmd struct {
SourcePath *url.URL `arg:"" name:"source_path" help:"Location of the file to view."`
}

func (v viewCmd) Run() error {
return core.View(*v.SourcePath)
}

var cli struct {
Edit editCmd `cmd help:"Edits a remote blob and optionally stores it elsewhere."`
View viewCmd `cmd help:"Views a remote blob."`
InstallCompletions kongplete.InstallCompletions `cmd:"" help:"install shell completions"`
}

func main() {
parser := kong.Must(
&cli,
&cli.Cli,
kong.Name(appName),
kong.Description(appDescription),
kong.UsageOnError(),
)

cli.AddCompletion(parser)

ctx, err := parser.Parse(os.Args[1:])
parser.FatalIfErrorf(err)

Expand Down
81 changes: 74 additions & 7 deletions storage/interface.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package storage

import (
"log"
"net/url"
"sort"
)

type FileStorage interface {
Expand All @@ -10,13 +12,78 @@ type FileStorage interface {
Close() error
}

type fileStorageBuilder func(url.URL) FileStorage
type FileLister func(url.URL) []url.URL

type registrationInfo struct {
storage fileStorageBuilder
lister FileLister
prefixes []string
completionPrompts []string
}

// fileStorageRegister registers available implementations.
var fileStorageRegister = make(map[string]registrationInfo)

func registerFileStorage(registration registrationInfo) {
for _, prefix := range registration.prefixes {
uriPrefix, err := url.Parse(prefix)
if err != nil {
log.Fatalf("Registration of %s can't progress. Can't parse it", prefix)
}

if _, ok := fileStorageRegister[uriPrefix.Scheme]; ok {
log.Fatalf("FileStorage with scheme %s already registered", uriPrefix.Scheme)
}
fileStorageRegister[uriPrefix.Scheme] = registration
}
}

func EmptyFileLister(prefix url.URL) []url.URL {
return []url.URL{}
}

func GetFileStorage(uri url.URL) FileStorage {
switch uri.Scheme {
case "":
return getLocalFileStorage(uri)
case "s3":
return getS3FileStorage(uri)
default:
panic("Can not handle this uri")
if info, ok := fileStorageRegister[uri.Scheme]; ok {
return info.storage(uri)
}

panic("Can not handle this uri")
}

func GetFileListerPrefixes() []string {
uniquePrefixes := map[string]bool{}
for _, info := range fileStorageRegister {
for _, prefix := range info.prefixes {
if prefix == "" {
continue
}
uniquePrefixes[prefix] = true
}
for _, prompt := range info.completionPrompts {
uniquePrefixes[prompt] = true
}
}

keys := make([]string, len(uniquePrefixes))
i := 0
for prefix := range uniquePrefixes {
keys[i] = prefix
i++
}

sort.Strings(keys)
return keys
}

func GetFileLister(prefix url.URL) FileLister {
lister := EmptyFileLister

if info, ok := fileStorageRegister[prefix.Scheme]; ok {
if info.lister != nil {
lister = info.lister
}
}

return lister
}
16 changes: 16 additions & 0 deletions storage/interface_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package storage_test

import (
"techiecaro/remblob/storage"
"testing"

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

func TestGetFileListerPrefixes(t *testing.T) {
prefixes := storage.GetFileListerPrefixes()

expected := []string{"./", "file://", "s3://"}

assert.Equal(t, expected, prefixes, "Invalid prefixes")
}
Loading

0 comments on commit 90d4e87

Please sign in to comment.