This is an automated email from the ASF dual-hosted git repository.

rohit pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/cloudstack-cloudmonkey.git


The following commit(s) were added to refs/heads/master by this push:
     new ff373cd  cli: implement auto-completion for apis
ff373cd is described below

commit ff373cd11187234c77f2ff2bfc68de2d82828433
Author: Rohit Yadav <ro...@apache.org>
AuthorDate: Fri Apr 13 04:43:09 2018 +0530

    cli: implement auto-completion for apis
    
    Signed-off-by: Rohit Yadav <ro...@apache.org>
---
 .gitignore       |   2 +-
 Makefile         |   2 +-
 cli/completer.go | 218 +++++++++++++++++++++++++++++++++++++++----------------
 cli/selector.go  |  47 ++++++++----
 cmd/api.go       |  29 ++++++++
 cmd/network.go   |   4 +-
 config/cache.go  |  69 ++++++++++++++----
 config/config.go |  43 +++++++++++
 config/util.go   |  67 -----------------
 9 files changed, 316 insertions(+), 165 deletions(-)

diff --git a/.gitignore b/.gitignore
index 0cbfde4..67cb950 100644
--- a/.gitignore
+++ b/.gitignore
@@ -21,5 +21,5 @@ dist
 *.exe
 *.test
 *.out
-.gopath~
+.gopath
 .idea
diff --git a/Makefile b/Makefile
index 41184d9..d32b6be 100644
--- a/Makefile
+++ b/Makefile
@@ -20,7 +20,7 @@ PACKAGE  = cloudmonkey
 DATE    ?= $(shell date +%FT%T%z)
 VERSION ?= $(shell git describe --tags --always --dirty --match=v* 2> 
/dev/null || \
                        cat $(CURDIR)/.version 2> /dev/null || echo v0)
-GOPATH   = $(CURDIR)/.gopath~
+GOPATH   = $(CURDIR)/.gopath
 BIN      = $(GOPATH)/bin
 BASE     = $(GOPATH)/src/$(PACKAGE)
 PKGS     = $(or $(PKG),$(shell cd $(BASE) && env GOPATH=$(GOPATH) $(GO) list 
./... | grep -v "^$(PACKAGE)/vendor/"))
diff --git a/cli/completer.go b/cli/completer.go
index ba45391..1a4c0a2 100644
--- a/cli/completer.go
+++ b/cli/completer.go
@@ -35,7 +35,29 @@ type CliCompleter struct {
 
 var completer *CliCompleter
 
-func TrimSpaceLeft(in []rune) []rune {
+func buildApiCacheMap(apiMap map[string][]*config.Api) 
map[string][]*config.Api {
+       for _, cmd := range cmd.AllCommands() {
+               verb := cmd.Name
+               if cmd.SubCommands != nil && len(cmd.SubCommands) > 0 {
+                       for _, scmd := range cmd.SubCommands {
+                               dummyApi := &config.Api{
+                                       Name: scmd,
+                                       Verb: verb,
+                               }
+                               apiMap[verb] = append(apiMap[verb], dummyApi)
+                       }
+               } else {
+                       dummyApi := &config.Api{
+                               Name: "",
+                               Verb: verb,
+                       }
+                       apiMap[verb] = append(apiMap[verb], dummyApi)
+               }
+       }
+       return apiMap
+}
+
+func trimSpaceLeft(in []rune) []rune {
        firstIndex := len(in)
        for i, r := range in {
                if unicode.IsSpace(r) == false {
@@ -65,36 +87,9 @@ func doInternal(line []rune, pos int, lineLen int, argName 
[]rune) (newLine [][]
        return
 }
 
-func (t *CliCompleter) Do(line []rune, pos int) (newLine [][]rune, offset int) 
{
-
-       line = TrimSpaceLeft(line[:pos])
-       lineLen := len(line)
+func (t *CliCompleter) Do(line []rune, pos int) (options [][]rune, offset int) 
{
 
-       apiCache := t.Config.GetCache()
-       apiMap := make(map[string][]*config.Api)
-       for api := range apiCache {
-               verb := apiCache[api].Verb
-               apiMap[verb] = append(apiMap[verb], apiCache[api])
-       }
-
-       for _, cmd := range cmd.AllCommands() {
-               verb := cmd.Name
-               if cmd.SubCommands != nil && len(cmd.SubCommands) > 0 {
-                       for _, scmd := range cmd.SubCommands {
-                               dummyApi := &config.Api{
-                                       Name: scmd,
-                                       Verb: verb,
-                               }
-                               apiMap[verb] = append(apiMap[verb], dummyApi)
-                       }
-               } else {
-                       dummyApi := &config.Api{
-                               Name: "",
-                               Verb: verb,
-                       }
-                       apiMap[verb] = append(apiMap[verb], dummyApi)
-               }
-       }
+       apiMap := buildApiCacheMap(t.Config.GetApiVerbMap())
 
        var verbs []string
        for verb := range apiMap {
@@ -105,56 +100,155 @@ func (t *CliCompleter) Do(line []rune, pos int) (newLine 
[][]rune, offset int) {
        }
        sort.Strings(verbs)
 
-       var verbsFound []string
+       line = trimSpaceLeft(line[:pos])
+
+       // Auto-complete verb
+       var verbFound string
        for _, verb := range verbs {
                search := verb + " "
                if !runes.HasPrefix(line, []rune(search)) {
-                       sLine, sOffset := doInternal(line, pos, lineLen, 
[]rune(search))
-                       newLine = append(newLine, sLine...)
+                       sLine, sOffset := doInternal(line, pos, len(line), 
[]rune(search))
+                       options = append(options, sLine...)
                        offset = sOffset
                } else {
-                       verbsFound = append(verbsFound, verb)
+                       verbFound = verb
+                       break
                }
        }
+       if len(verbFound) == 0 {
+               return
+       }
 
-       apiArg := false
-       for _, verbFound := range verbsFound {
-               search := verbFound + " "
+       // Auto-complete noun
+       var nounFound string
+       line = trimSpaceLeft(line[len(verbFound):])
+       for _, api := range apiMap[verbFound] {
+               search := api.Noun + " "
+               if !runes.HasPrefix(line, []rune(search)) {
+                       sLine, sOffset := doInternal(line, pos, len(line), 
[]rune(search))
+                       options = append(options, sLine...)
+                       offset = sOffset
+               } else {
+                       nounFound = api.Noun
+                       break
+               }
+       }
+       if len(nounFound) == 0 {
+               return
+       }
 
-               nLine := TrimSpaceLeft(line[len(search):])
-               offset = lineLen - len(verbFound) - 1
+       // Find API
+       var apiFound *config.Api
+       for _, api := range apiMap[verbFound] {
+               if api.Noun == nounFound {
+                       apiFound = api
+                       break
+               }
+       }
+       if apiFound == nil {
+               return
+       }
 
-               for _, api := range apiMap[verbFound] {
-                       resource := 
strings.TrimPrefix(strings.ToLower(api.Name), verbFound)
-                       search = resource + " "
+       // Auto-complete api args
+       splitLine := strings.Split(string(line), " ")
+       line = trimSpaceLeft([]rune(splitLine[len(splitLine)-1]))
+       for _, arg := range apiFound.Args {
+               search := arg.Name + "="
+               if !runes.HasPrefix(line, []rune(search)) {
+                       sLine, sOffset := doInternal(line, pos, len(line), 
[]rune(search))
+                       options = append(options, sLine...)
+                       offset = sOffset
+               } else {
+                       if arg.Type == "boolean" {
+                               options = [][]rune{[]rune("true "), 
[]rune("false ")}
+                               offset = 0
+                               return
+                       }
+
+                       var autocompleteApi *config.Api
+                       var relatedNoun string
+                       if arg.Name == "id" || arg.Name == "ids" {
+                               relatedNoun = apiFound.Noun
+                               if apiFound.Verb != "list" {
+                                       relatedNoun += "s"
+                               }
+                       } else if arg.Name == "account" {
+                               relatedNoun = "accounts"
+                       } else {
+                               relatedNoun = 
strings.Replace(strings.Replace(arg.Name, "ids", "", -1), "id", "", -1) + "s"
+                       }
+                       for _, related := range apiMap["list"] {
+                               if relatedNoun == related.Noun {
+                                       autocompleteApi = related
+                                       break
+                               }
+                       }
+
+                       if autocompleteApi == nil {
+                               return nil, 0
+                       }
+
+                       r := cmd.NewRequest(nil, config.NewConfig(), nil, nil)
+                       autocompleteApiArgs := []string{"listall=true"}
+                       if autocompleteApi.Noun == "templates" {
+                               autocompleteApiArgs = 
append(autocompleteApiArgs, "templatefilter=all")
+                       }
+                       response, _ := cmd.NewAPIRequest(r, 
autocompleteApi.Name, autocompleteApiArgs)
 
-                       if runes.HasPrefix(nLine, []rune(search)) {
-                               // FIXME: handle params to API here with = stuff
-                               for _, arg := range api.Args {
-                                       opt := arg.Name + "="
-                                       newLine = append(newLine, []rune(opt))
+                       var autocompleteOptions []SelectOption
+                       for _, v := range response {
+                               switch obj := v.(type) {
+                               case []interface{}:
+                                       if obj == nil {
+                                               break
+                                       }
+                                       for _, item := range obj {
+                                               resource, ok := 
item.(map[string]interface{})
+                                               if !ok {
+                                                       continue
+                                               }
+                                               opt := SelectOption{}
+                                               if resource["id"] != nil {
+                                                       opt.Id = 
resource["id"].(string)
+                                               }
+                                               if resource["name"] != nil {
+                                                       opt.Name = 
resource["name"].(string)
+                                               } else if resource["username"] 
!= nil {
+                                                       opt.Name = 
resource["username"].(string)
+                                               }
+                                               if resource["displaytext"] != 
nil {
+                                                       opt.Detail = 
resource["displaytext"].(string)
+                                               }
+
+                                               autocompleteOptions = 
append(autocompleteOptions, opt)
+                                       }
+                                       break
                                }
-                               if string(nLine[len(nLine)-1]) == "=" {
-                                       apiArg = true
+                       }
+
+                       var selected string
+                       if len(autocompleteOptions) > 1 {
+                               sort.Slice(autocompleteOptions, func(i, j int) 
bool {
+                                       return autocompleteOptions[i].Name < 
autocompleteOptions[j].Name
+                               })
+                               fmt.Println()
+                               selectedOption := 
ShowSelector(autocompleteOptions)
+                               if arg.Name == "account" {
+                                       selected = selectedOption.Name
+                               } else {
+                                       selected = selectedOption.Id
                                }
-                               offset = lineLen - len(verbFound) - 
len(resource) - 1
                        } else {
-                               sLine, _ := doInternal(nLine, pos, len(nLine), 
[]rune(search))
-                               newLine = append(newLine, sLine...)
+                               if len(autocompleteOptions) == 1 {
+                                       selected = autocompleteOptions[0].Id
+                               }
                        }
+                       options = [][]rune{[]rune(selected + " ")}
+                       offset = 0
                }
        }
 
-       // FIXME: pass selector uuid options
-       if apiArg {
-               fmt.Println()
-               option := ShowSelector()
-               // show only one option in autocompletion
-               newLine = [][]rune{[]rune(option)}
-               offset = 0
-       }
-
-       return newLine, offset
+       return options, offset
 }
 
 func NewCompleter(cfg *config.Config) *CliCompleter {
diff --git a/cli/selector.go b/cli/selector.go
index 5cf35af..bb1696e 100644
--- a/cli/selector.go
+++ b/cli/selector.go
@@ -25,23 +25,38 @@ import (
        "github.com/rhtyd/readline"
 )
 
-type SelectOptions struct {
-       Name   string
+type SelectOption struct {
        Id     string
+       Name   string
        Detail string
 }
 
-func ShowSelector() string {
-       options := []SelectOptions{
-               {Name: "Option1", Id: "some-uuid", Detail: "Some Detail"},
-               {Name: "Option2", Id: "some-uuid", Detail: "Some Detail"},
-               {Name: "Option3", Id: "some-uuid", Detail: "Some Detail"},
-               {Name: "Option4", Id: "some-uuid", Detail: "Some Detail"},
-               {Name: "Option5", Id: "some-uuid", Detail: "Some Detail"},
-               {Name: "Option6", Id: "some-uuid", Detail: "Some Detail"},
-               {Name: "Option7", Id: "some-uuid", Detail: "Some Detail"},
-               {Name: "Option8", Id: "some-uuid", Detail: "Some Detail"},
+type Selector struct {
+       InUse bool
+}
+
+var selector Selector
+
+func init() {
+       selector = Selector{
+               InUse: false,
+       }
+}
+
+func (s Selector) lock() {
+       s.InUse = true
+}
+
+func (s Selector) unlock() {
+       s.InUse = false
+}
+
+func ShowSelector(options []SelectOption) SelectOption {
+       if selector.InUse {
+               return SelectOption{}
        }
+       selector.lock()
+       defer selector.unlock()
 
        templates := &promptui.SelectTemplates{
                Label:    "{{ . }}?",
@@ -50,9 +65,9 @@ func ShowSelector() string {
                Selected: "Selected: {{ .Name | cyan }} ({{ .Id | red }})",
                Details: `
 --------- Current Selection ----------
-{{ "Name:" | faint }} {{ .Name }}
 {{ "Id:" | faint }}  {{ .Id }}
-{{ "Detail:" | faint }}  {{ .Detail }}`,
+{{ "Name:" | faint }} {{ .Name }}
+{{ "Description:" | faint }}  {{ .Detail }}`,
        }
 
        searcher := func(input string, index int) bool {
@@ -83,8 +98,8 @@ func ShowSelector() string {
 
        if err != nil {
                fmt.Printf("Prompt failed %v\n", err)
-               return ""
+               return SelectOption{}
        }
 
-       return options[i].Id
+       return options[i]
 }
diff --git a/cmd/api.go b/cmd/api.go
index 4be24e3..ff9d0ce 100644
--- a/cmd/api.go
+++ b/cmd/api.go
@@ -51,6 +51,35 @@ func init() {
                                return errors.New("unknown or unauthorized API: 
" + apiName)
                        }
 
+                       if strings.Contains(strings.Join(apiArgs, " "), "-h") {
+                               fmt.Println("=== Help docs ===")
+                               fmt.Println(api.Name, ":", api.Description)
+                               fmt.Println("Async:", api.Async)
+                               fmt.Println("Required params:", 
strings.Join(api.RequiredArgs, ", "))
+                               for _, arg := range api.Args {
+                                       fmt.Println(arg.Name, "(", arg.Type, 
")", arg.Description)
+                               }
+                               return nil
+                       }
+
+                       var missingArgs []string
+                       for _, required := range api.RequiredArgs {
+                               provided := false
+                               for _, arg := range apiArgs {
+                                       if strings.HasPrefix(arg, required+"=") 
{
+                                               provided = true
+                                       }
+                               }
+                               if !provided {
+                                       missingArgs = append(missingArgs, 
required)
+                               }
+                       }
+
+                       if len(missingArgs) > 0 {
+                               fmt.Println("Missing required arguments: ", 
strings.Join(missingArgs, ", "))
+                               return nil
+                       }
+
                        b, _ := NewAPIRequest(r, api.Name, apiArgs)
                        response, _ := json.MarshalIndent(b, "", "  ")
 
diff --git a/cmd/network.go b/cmd/network.go
index 352afd9..6ff7d7c 100644
--- a/cmd/network.go
+++ b/cmd/network.go
@@ -57,8 +57,6 @@ func encodeRequestParams(params url.Values) string {
 }
 
 func NewAPIRequest(r *Request, api string, args []string) 
(map[string]interface{}, error) {
-       fmt.Println("[debug] Running api:", api, args)
-
        params := make(url.Values)
        params.Add("command", api)
        for _, arg := range args {
@@ -85,7 +83,7 @@ func NewAPIRequest(r *Request, api string, args []string) 
(map[string]interface{
 
        apiUrl := fmt.Sprintf("%s?%s", r.Config.ActiveProfile.Url, 
encodedParams)
 
-       fmt.Println("[debug] Requesting: ", apiUrl)
+       //fmt.Println("[debug] Requesting: ", apiUrl)
        response, err := http.Get(apiUrl)
        if err != nil {
                fmt.Println("Error:", err)
diff --git a/config/cache.go b/config/cache.go
index 3814669..27b431f 100644
--- a/config/cache.go
+++ b/config/cache.go
@@ -21,31 +21,46 @@ import (
        "encoding/json"
        "fmt"
        "io/ioutil"
+       "sort"
        "strings"
        "unicode"
 )
 
 type ApiArg struct {
        Name        string
+       Type        string
+       Related     []string
        Description string
        Required    bool
        Length      int
-       Type        string
-       Related     []string
 }
 
 type Api struct {
        Name         string
-       ResponseName string
-       Description  string
-       Async        bool
-       Related      []string
-       Args         []*ApiArg
-       RequiredArgs []*ApiArg
        Verb         string
+       Noun         string
+       Args         []*ApiArg
+       RequiredArgs []string
+       Related      []string
+       Async        bool
+       Description  string
+       ResponseName string
 }
 
 var apiCache map[string]*Api
+var apiVerbMap map[string][]*Api
+
+func (c *Config) GetApiVerbMap() map[string][]*Api {
+       if apiVerbMap != nil {
+               return apiVerbMap
+       }
+       apiSplitMap := make(map[string][]*Api)
+       for api := range apiCache {
+               verb := apiCache[api].Verb
+               apiSplitMap[verb] = append(apiSplitMap[verb], apiCache[api])
+       }
+       return apiSplitMap
+}
 
 func (c *Config) GetCache() map[string]*Api {
        if apiCache == nil {
@@ -73,6 +88,7 @@ func (c *Config) SaveCache(response map[string]interface{}) {
 
 func (c *Config) UpdateCache(response map[string]interface{}) interface{} {
        apiCache = make(map[string]*Api)
+       apiVerbMap = nil
 
        count := response["count"]
        apiList := response["api"].([]interface{})
@@ -85,6 +101,7 @@ func (c *Config) UpdateCache(response 
map[string]interface{}) interface{} {
                }
                apiName := api["name"].(string)
                isAsync := api["isasync"].(bool)
+               description := api["description"].(string)
 
                idx := 0
                for _, chr := range apiName {
@@ -95,22 +112,44 @@ func (c *Config) UpdateCache(response 
map[string]interface{}) interface{} {
                        }
                }
                verb := apiName[:idx]
+               noun := strings.ToLower(apiName[idx:])
 
                var apiArgs []*ApiArg
                for _, argNode := range api["params"].([]interface{}) {
                        apiArg, _ := argNode.(map[string]interface{})
+                       related := []string{}
+                       if apiArg["related"] != nil {
+                               related = 
strings.Split(apiArg["related"].(string), ",")
+                               sort.Strings(related)
+                       }
                        apiArgs = append(apiArgs, &ApiArg{
-                               Name:     apiArg["name"].(string),
-                               Type:     apiArg["type"].(string),
-                               Required: apiArg["required"].(bool),
+                               Name:        apiArg["name"].(string),
+                               Type:        apiArg["type"].(string),
+                               Required:    apiArg["required"].(bool),
+                               Related:     related,
+                               Description: apiArg["description"].(string),
                        })
                }
 
+               sort.Slice(apiArgs, func(i, j int) bool {
+                       return apiArgs[i].Name < apiArgs[j].Name
+               })
+
+               var requiredArgs []string
+               for _, arg := range apiArgs {
+                       if arg.Required {
+                               requiredArgs = append(requiredArgs, arg.Name)
+                       }
+               }
+
                apiCache[strings.ToLower(apiName)] = &Api{
-                       Name:  apiName,
-                       Async: isAsync,
-                       Args:  apiArgs,
-                       Verb:  verb,
+                       Name:         apiName,
+                       Verb:         verb,
+                       Noun:         noun,
+                       Args:         apiArgs,
+                       RequiredArgs: requiredArgs,
+                       Async:        isAsync,
+                       Description:  description,
                }
        }
        return count
diff --git a/config/config.go b/config/config.go
index 06c5393..0f2d71e 100644
--- a/config/config.go
+++ b/config/config.go
@@ -18,10 +18,24 @@
 package config
 
 import (
+       "fmt"
+       homedir "github.com/mitchellh/go-homedir"
        "os"
        "path"
 )
 
+var name = "cloudmonkey"
+var version = "6.0.0-alpha1"
+
+func getDefaultConfigDir() string {
+       home, err := homedir.Dir()
+       if err != nil {
+               fmt.Println(err)
+               os.Exit(1)
+       }
+       return path.Join(home, ".cmk")
+}
+
 type OutputFormat string
 
 const (
@@ -98,3 +112,32 @@ func loadConfig() *Config {
 
        return cfg
 }
+
+func (c *Config) Name() string {
+       return name
+}
+
+func (c *Config) Version() string {
+       return version
+}
+
+func (c *Config) PrintHeader() {
+       fmt.Printf("Apache CloudStack 🐵 cloudmonkey %s.\n", version)
+       fmt.Printf("Type \"help\" for details, \"sync\" to update API cache or 
press tab to list commands.\n\n")
+}
+
+func (c *Config) GetPrompt() string {
+       return fmt.Sprintf("(%s) \033[34m🐵\033[0m > ", c.ActiveProfile.Name)
+}
+
+func (c *Config) UpdateGlobalConfig(key string, value string) {
+       c.UpdateConfig("", key, value)
+}
+
+func (c *Config) UpdateConfig(namespace string, key string, value string) {
+       fmt.Println("Updating for key", key, ", value=", value, ", in ns=", 
namespace)
+       if key == "profile" {
+               //FIXME
+               c.ActiveProfile.Name = value
+       }
+}
diff --git a/config/util.go b/config/util.go
deleted file mode 100644
index 7421e5b..0000000
--- a/config/util.go
+++ /dev/null
@@ -1,67 +0,0 @@
-// Licensed to the Apache Software Foundation (ASF) under one
-// or more contributor license agreements.  See the NOTICE file
-// distributed with this work for additional information
-// regarding copyright ownership.  The ASF licenses this file
-// to you under the Apache License, Version 2.0 (the
-// "License"); you may not use this file except in compliance
-// with the License.  You may obtain a copy of the License at
-//
-//   http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing,
-// software distributed under the License is distributed on an
-// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-// KIND, either express or implied.  See the License for the
-// specific language governing permissions and limitations
-// under the License.
-
-package config
-
-import (
-       "fmt"
-       "os"
-       "path"
-
-       "github.com/mitchellh/go-homedir"
-)
-
-var name = "cloudmonkey"
-var version = "6.0.0-alpha1"
-
-func getDefaultConfigDir() string {
-       home, err := homedir.Dir()
-       if err != nil {
-               fmt.Println(err)
-               os.Exit(1)
-       }
-       return path.Join(home, ".cmk")
-}
-
-func (c *Config) Name() string {
-       return name
-}
-
-func (c *Config) Version() string {
-       return version
-}
-
-func (c *Config) PrintHeader() {
-       fmt.Printf("Apache CloudStack 🐵 cloudmonkey %s.\n", version)
-       fmt.Printf("Type \"help\" for details, \"sync\" to update API cache or 
press tab to list commands.\n\n")
-}
-
-func (c *Config) GetPrompt() string {
-       return fmt.Sprintf("(%s) \033[34m🐵\033[0m > ", c.ActiveProfile.Name)
-}
-
-func (c *Config) UpdateGlobalConfig(key string, value string) {
-       c.UpdateConfig("", key, value)
-}
-
-func (c *Config) UpdateConfig(namespace string, key string, value string) {
-       fmt.Println("Updating for key", key, ", value=", value, ", in ns=", 
namespace)
-       if key == "profile" {
-               //FIXME
-               c.ActiveProfile.Name = value
-       }
-}

-- 
To stop receiving notification emails like this one, please contact
ro...@apache.org.

Reply via email to