1package commands 2 3import ( 4 "fmt" 5 "sort" 6 "strings" 7 "unicode" 8 9 "github.com/chzyer/readline" 10 "github.com/wallix/awless/aws/services" 11 "github.com/wallix/awless/cloud" 12 "github.com/wallix/awless/graph" 13 "github.com/wallix/awless/template" 14) 15 16func enumCompletionFunc(enum []string) readline.AutoCompleter { 17 if len(enum) == 1 && enum[0] == "" { 18 return readline.NewPrefixCompleter() 19 } 20 var items []readline.PrefixCompleterInterface 21 for _, e := range enum { 22 items = append(items, readline.PcItem(e)) 23 } 24 return readline.NewPrefixCompleter(items...) 25} 26 27func typedParamCompletionFunc(g cloud.GraphAPI, resourceType, propName string) readline.AutoCompleter { 28 var items []readline.PrefixCompleterInterface 29 resources, _ := g.Find(cloud.NewQuery(resourceType)) 30 for _, res := range resources { 31 if val, ok := res.Properties()[propName]; ok { 32 switch vv := val.(type) { 33 case []string: 34 for _, s := range vv { 35 items = append(items, readline.PcItem(s)) 36 } 37 default: 38 items = append(items, readline.PcItem(fmt.Sprint(val))) 39 } 40 } 41 } 42 43 return readline.NewPrefixCompleter(items...) 44} 45func holeAutoCompletion(g cloud.GraphAPI, paramPaths []string) readline.AutoCompleter { 46 type typesProp struct { 47 types []string 48 prop string 49 } 50 51 var entities []typesProp 52 53 for _, paramPath := range paramPaths { 54 splits := strings.Split(paramPath, ".") 55 if len(splits) != 3 { 56 continue 57 } 58 59 if entityTypes, entityProp := guessEntityTypeFromHoleQuestion(splits[1] + "." + splits[2]); len(entityTypes) > 0 { 60 entities = append(entities, typesProp{types: entityTypes, prop: entityProp}) 61 } 62 } 63 64 var possibleSuggests []string 65 for _, entityProp := range entities { 66 resources, err := g.Find(cloud.NewQuery(entityProp.types...)) 67 exitOn(err) 68 if len(resources) == 0 { 69 continue 70 } 71 72 var validPropName string 73 if entityProp.prop != "" { 74 for _, r := range resources { 75 for propName := range r.Properties() { 76 if keyCorrespondsToProperty(entityProp.prop, propName) { 77 validPropName = propName 78 } 79 } 80 } 81 } 82 if validPropName == "" { 83 for _, r := range resources { 84 possibleSuggests = append(possibleSuggests, r.Id()) 85 possibleSuggests = appendWithNameAliases(possibleSuggests, r) 86 } 87 continue 88 } 89 90 for _, r := range resources { 91 if v, ok := r.Property(validPropName); ok { 92 switch prop := v.(type) { 93 case string, float64, int, bool: 94 possibleSuggests = append(possibleSuggests, fmt.Sprint(prop)) 95 if validPropName == "ID" { 96 possibleSuggests = appendWithNameAliases(possibleSuggests, r) 97 } 98 case []string: 99 for _, str := range prop { 100 possibleSuggests = append(possibleSuggests, str) 101 } 102 case []*graph.KeyValue: 103 for _, kv := range prop { 104 possibleSuggests = append(possibleSuggests, fmt.Sprintf("%s:%s", kv.KeyName, kv.Value)) 105 } 106 } 107 } 108 } 109 } 110 111 completeFunc := func(s string) (suggest []string) { 112 s = splitKeepLast(s, ",") 113 s = strings.TrimLeft(s, "'@\"") 114 for _, possible := range possibleSuggests { 115 suggest = appendIfContains(suggest, possible, s) 116 } 117 suggest = quotedSortedSet(suggest) 118 return 119 } 120 121 return &prefixCompleter{callback: completeFunc, splitChar: ","} 122} 123 124type prefixCompleter struct { 125 callback readline.DynamicCompleteFunc 126 splitChar string 127} 128 129func (p *prefixCompleter) Do(line []rune, pos int) (newLine [][]rune, offset int) { 130 var lines []string 131 lines, offset = doInternal(p, string(line), pos, line) 132 for _, l := range lines { 133 newLine = append(newLine, []rune(l)) 134 } 135 return 136} 137 138func doInternal(p *prefixCompleter, line string, pos int, origLine []rune) (newLine []string, offset int) { 139 strings.TrimLeftFunc(line[:pos], func(r rune) bool { 140 return unicode.IsSpace(r) 141 }) 142 if p.splitChar != "" { 143 line = splitKeepLast(line, p.splitChar) 144 } 145 for _, suggest := range p.callback(line) { 146 line = strings.TrimLeft(line, "[") 147 if len(line) >= len(suggest) { 148 if strings.HasPrefix(line, suggest) { 149 if len(line) != len(suggest) { 150 newLine = append(newLine, suggest) 151 } 152 offset = len(suggest) 153 } 154 } else { 155 if strings.HasPrefix(suggest, line) { 156 newLine = append(newLine, suggest[len(line):]) 157 offset = len(line) 158 } 159 } 160 } 161 return 162} 163 164func splitKeepLast(s, sep string) (last string) { 165 if !strings.Contains(s, sep) { 166 last = s 167 return 168 } 169 offset := strings.LastIndex(s, sep) 170 if offset+1 < len(s) { 171 last = s[offset+1:] 172 } 173 return 174} 175 176func quotedSortedSet(list []string) (out []string) { 177 unique := make(map[string]bool) 178 for _, l := range list { 179 unique[l] = true 180 } 181 182 for k := range unique { 183 if !template.MatchStringParamValue(k) { 184 k = "'" + k + "'" 185 } 186 187 out = append(out, k) 188 } 189 190 sort.Strings(out) 191 return 192} 193 194// Return potential resource types and a prop 195// according to given holes questions. 196// See corresponding unit test for logic 197func guessEntityTypeFromHoleQuestion(hole string) (resolved []string, prop string) { 198 tokens := strings.Split(strings.TrimSpace(hole), ".") 199 if len(tokens) == 0 { 200 return 201 } 202 203 var types []string 204 for _, t := range tokens { 205 for _, r := range resourcesTypesWithPlural { 206 if t == r { 207 types = append(types, cloud.SingularizeResource(r)) 208 break 209 } 210 } 211 } 212 213 if l := len(types); l > 0 { 214 if len(tokens) == 2 { 215 prop = tokens[1] 216 } 217 if l > 1 { 218 prop = "" 219 } 220 resolved = []string{types[l-1]} 221 } else if len(tokens) > 1 { 222 for i := len(tokens) - 1; i >= 0; i-- { 223 if len(tokens[i]) < 4 { 224 continue 225 } 226 for _, r := range awsservices.ResourceTypes { 227 if strings.Contains(r, tokens[i]) { 228 resolved = append(resolved, r) 229 } 230 } 231 if len(resolved) > 0 { 232 return 233 } 234 } 235 } 236 return 237} 238 239func keyCorrespondsToProperty(holekey, prop string) bool { 240 holekey = strings.ToLower(holekey) 241 prop = strings.ToLower(prop) 242 if holekey == prop { 243 return true 244 } 245 if strings.Replace(holekey, "-", "", -1) == prop { 246 return true 247 } 248 return false 249} 250 251func appendIfContains(slice []string, value, subst string) []string { 252 subst = strings.TrimLeft(subst, "[") 253 254 if strings.Contains(value, subst) && value != "" { 255 return append(slice, value) 256 } 257 return slice 258} 259 260func appendWithNameAliases(slice []string, res cloud.Resource) []string { 261 if val, ok := res.Properties()["Name"]; ok { 262 switch val.(type) { 263 case string: 264 name := val.(string) 265 if name != "" { 266 slice = append(slice, fmt.Sprintf("@%s", name)) 267 } 268 } 269 } 270 return slice 271} 272 273var resourcesTypesWithPlural []string 274 275func init() { 276 for _, r := range awsservices.ResourceTypes { 277 resourcesTypesWithPlural = append(resourcesTypesWithPlural, r, cloud.PluralizeResource(r)) 278 } 279} 280