blob: 9c02effe59fa9fe8baf96f1ea676fb1dd41edfe5 [file] [log] [blame]
// 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 cli
import (
"fmt"
"sort"
"strings"
"unicode"
"cloudmonkey/cmd"
"cloudmonkey/config"
"github.com/chzyer/readline/runes"
)
type autoCompleter struct {
Config *config.Config
}
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 command, opts := range cmd.SubCommands {
var args []*config.APIArg
options := opts
if command == "profile" {
options = config.GetProfiles()
}
for _, opt := range options {
args = append(args, &config.APIArg{
Name: opt,
})
}
apiMap[verb] = append(apiMap[verb], &config.API{
Name: command,
Verb: verb,
Noun: command,
Args: args,
})
}
} 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 {
firstIndex = i
break
}
}
return in[firstIndex:]
}
func doInternal(line []rune, pos int, lineLen int, argName []rune) (newLine [][]rune, offset int) {
offset = lineLen
if lineLen >= len(argName) {
if runes.HasPrefix(line, argName) {
if lineLen == len(argName) {
newLine = append(newLine, []rune{' '})
} else {
newLine = append(newLine, argName)
}
offset = offset - len(argName) - 1
}
} else {
if runes.HasPrefix(argName, line) {
newLine = append(newLine, argName[offset:])
}
}
return
}
func (t *autoCompleter) Do(line []rune, pos int) (options [][]rune, offset int) {
apiMap := buildAPICacheMap(t.Config.GetAPIVerbMap())
var verbs []string
for verb := range apiMap {
verbs = append(verbs, verb)
sort.Slice(apiMap[verb], func(i, j int) bool {
return apiMap[verb][i].Name < apiMap[verb][j].Name
})
}
sort.Strings(verbs)
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, len(line), []rune(search))
options = append(options, sLine...)
offset = sOffset
} else {
verbFound = verb
break
}
}
if len(verbFound) == 0 {
return
}
// 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
}
// Find API
var apiFound *config.API
for _, api := range apiMap[verbFound] {
if api.Noun == nounFound {
apiFound = api
break
}
}
if apiFound == nil {
return
}
// 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
}
if arg.Type == config.FAKE && arg.Name == "filter=" {
options = [][]rune{}
offset = 0
for _, key := range apiFound.ResponseKeys {
options = append(options, []rune(key))
}
return
}
argName := strings.Replace(arg.Name, "=", "", -1)
var autocompleteAPI *config.API
var relatedNoun string
if argName == "id" || argName == "ids" {
relatedNoun = apiFound.Noun
if apiFound.Verb != "list" {
relatedNoun += "s"
}
} else if argName == "account" {
relatedNoun = "accounts"
} else {
relatedNoun = strings.Replace(strings.Replace(argName, "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, completer.Config, nil)
autocompleteAPIArgs := []string{"listall=true"}
if autocompleteAPI.Noun == "templates" {
autocompleteAPIArgs = append(autocompleteAPIArgs, "templatefilter=executable")
}
fmt.Println("")
spinner := t.Config.StartSpinner("fetching options, please wait...")
response, _ := cmd.NewAPIRequest(r, autocompleteAPI.Name, autocompleteAPIArgs, false)
t.Config.StopSpinner(spinner)
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
}
}
var selected string
if len(autocompleteOptions) > 1 {
sort.Slice(autocompleteOptions, func(i, j int) bool {
return autocompleteOptions[i].Name < autocompleteOptions[j].Name
})
selectedOption := showSelector(autocompleteOptions)
if strings.HasSuffix(arg.Name, "id=") || strings.HasSuffix(arg.Name, "ids=") {
selected = selectedOption.ID
} else {
selected = selectedOption.Name
}
} else {
if len(autocompleteOptions) == 1 {
selected = autocompleteOptions[0].ID
}
}
options = [][]rune{[]rune(selected + " ")}
offset = 0
}
}
return options, offset
}