blob: 650c84b87d5e729e3ad328271acee50f704e9588 [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"
"github.com/apache/cloudstack-cloudmonkey/cmd"
"github.com/apache/cloudstack-cloudmonkey/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 equal(a, b []rune) bool {
if len(a) != len(b) {
return false
}
for i := 0; i < len(a); i++ {
if a[i] != b[i] {
return false
}
}
return true
}
func hasPrefix(r, prefix []rune) bool {
if len(r) < len(prefix) {
return false
}
return equal(r[:len(prefix)], prefix)
}
func inArray(s string, array []string) bool {
for _, item := range array {
if s == item {
return true
}
}
return false
}
func lastString(array []string) string {
return array[len(array)-1]
}
type argOption struct {
Value string
Detail string
}
func buildArgOptions(response map[string]interface{}, hasID bool) []argOption {
argOptions := []argOption{}
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
}
var id, name, detail string
if resource["id"] != nil {
id = resource["id"].(string)
}
if resource["name"] != nil {
name = resource["name"].(string)
} else if resource["username"] != nil {
name = resource["username"].(string)
}
if resource["displaytext"] != nil {
detail = resource["displaytext"].(string)
}
if len(detail) == 0 && resource["description"] != nil {
detail = resource["description"].(string)
}
if len(detail) == 0 && resource["ipaddress"] != nil {
detail = resource["ipaddress"].(string)
}
var opt argOption
if hasID {
opt.Value = id
opt.Detail = name
if len(name) == 0 {
opt.Detail = detail
}
} else {
opt.Value = name
opt.Detail = detail
if len(name) == 0 {
opt.Value = detail
}
}
argOptions = append(argOptions, opt)
}
break
}
}
return argOptions
}
func doInternal(line []rune, pos int, lineLen int, argName []rune) (newLine [][]rune, offset int) {
offset = lineLen
if lineLen >= len(argName) {
if hasPrefix(line, argName) {
if lineLen == len(argName) {
newLine = append(newLine, []rune{' '})
} else {
newLine = append(newLine, argName)
}
offset = offset - len(argName) - 1
}
} else {
if hasPrefix(argName, line) {
newLine = append(newLine, argName[offset:])
}
}
return
}
func findAutocompleteAPI(arg *config.APIArg, apiFound *config.API, apiMap map[string][]*config.API) *config.API {
if arg.Type == "map" {
return nil
}
var autocompleteAPI *config.API
argName := strings.Replace(arg.Name, "=", "", -1)
relatedNoun := argName
if argName == "id" || argName == "ids" {
// Heuristic: user is trying to autocomplete for id/ids arg for a list API
relatedNoun = apiFound.Noun
if apiFound.Verb != "list" {
relatedNoun += "s"
}
} else if argName == "account" {
// Heuristic: user is trying to autocomplete for accounts
relatedNoun = "accounts"
} else if argName == "ipaddressid" {
// Heuristic: user is trying to autocomplete for ip addresses
relatedNoun = "publicipaddresses"
} else {
// Heuristic: autocomplete for the arg for which a list<Arg without id/ids>s API exists
// For example, for zoneid arg, listZones API exists
cutIdx := len(argName)
if strings.HasSuffix(argName, "id") {
cutIdx -= 2
} else if strings.HasSuffix(argName, "ids") {
cutIdx -= 3
} else {
}
relatedNoun = argName[:cutIdx] + "s"
}
config.Debug("Possible related noun for the arg: ", relatedNoun, " and type: ", arg.Type)
for _, listAPI := range apiMap["list"] {
if relatedNoun == listAPI.Noun {
autocompleteAPI = listAPI
break
}
}
if autocompleteAPI != nil {
config.Debug("Autocomplete: API found using heuristics: ", autocompleteAPI.Name)
}
if strings.HasSuffix(relatedNoun, "s") {
relatedNoun = relatedNoun[:len(relatedNoun)-1]
}
// Heuristic: find any list API that contains the arg name
if autocompleteAPI == nil {
config.Debug("Finding possible API that have: ", argName, " related APIs: ", arg.Related)
possibleAPIs := []*config.API{}
for _, listAPI := range apiMap["list"] {
if strings.Contains(listAPI.Noun, argName) {
config.Debug("Found possible API: ", listAPI.Name)
possibleAPIs = append(possibleAPIs, listAPI)
}
}
if len(possibleAPIs) == 1 {
autocompleteAPI = possibleAPIs[0]
}
}
return autocompleteAPI
}
type autoCompleter struct {
Config *config.Config
}
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 !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 !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 arg
splitLine := strings.Split(string(line), " ")
line = trimSpaceLeft([]rune(splitLine[len(splitLine)-1]))
for _, arg := range apiFound.Args {
search := arg.Name
if !hasPrefix(line, []rune(search)) {
sLine, sOffset := doInternal(line, pos, len(line), []rune(search))
options = append(options, sLine...)
offset = sOffset
} else {
words := strings.Split(string(line), "=")
argInput := lastString(words)
if arg.Type == "boolean" {
for _, search := range []string{"true ", "false "} {
offset = 0
if strings.HasPrefix(search, argInput) {
options = append(options, []rune(search[len(argInput):]))
offset = len(argInput)
}
}
return
}
if arg.Type == config.FAKE && arg.Name == "filter=" {
offset = 0
filterInputs := strings.Split(strings.Replace(argInput, ",", ",|", -1), "|")
lastFilterInput := lastString(filterInputs)
for _, key := range apiFound.ResponseKeys {
if inArray(key, filterInputs) {
continue
}
if strings.HasPrefix(key, lastFilterInput) {
options = append(options, []rune(key[len(lastFilterInput):]))
offset = len(lastFilterInput)
}
}
return
}
autocompleteAPI := findAutocompleteAPI(arg, apiFound, apiMap)
if autocompleteAPI == nil {
return nil, 0
}
completeArgs := t.Config.Core.AutoComplete
autocompleteAPIArgs := []string{}
argOptions := []argOption{}
if completeArgs {
autocompleteAPIArgs = []string{"listall=true"}
if autocompleteAPI.Noun == "templates" {
autocompleteAPIArgs = append(autocompleteAPIArgs, "templatefilter=executable")
}
if apiFound.Name != "provisionCertificate" && autocompleteAPI.Name == "listHosts" {
autocompleteAPIArgs = append(autocompleteAPIArgs, "type=Routing")
}
spinner := t.Config.StartSpinner("fetching options, please wait...")
request := cmd.NewRequest(nil, completer.Config, nil)
response, _ := cmd.NewAPIRequest(request, autocompleteAPI.Name, autocompleteAPIArgs, false)
t.Config.StopSpinner(spinner)
hasID := strings.HasSuffix(arg.Name, "id=") || strings.HasSuffix(arg.Name, "ids=")
argOptions = buildArgOptions(response, hasID)
}
filteredOptions := []argOption{}
if len(argOptions) > 0 {
sort.Slice(argOptions, func(i, j int) bool {
return argOptions[i].Value < argOptions[j].Value
})
for _, item := range argOptions {
if strings.HasPrefix(item.Value, argInput) {
filteredOptions = append(filteredOptions, item)
}
}
}
offset = 0
if len(filteredOptions) == 0 {
options = [][]rune{[]rune("")}
}
for _, item := range filteredOptions {
option := item.Value + " "
if len(filteredOptions) > 1 && len(item.Detail) > 0 {
option += fmt.Sprintf("(%v)", item.Detail)
}
if strings.HasPrefix(option, argInput) {
options = append(options, []rune(option[len(argInput):]))
offset = len(argInput)
}
}
return
}
}
return options, offset
}