go-micro.dev/v5@v5.12.0/cmd/micro/cli/util/dynamic.go (about) 1 package util 2 3 import ( 4 "bytes" 5 "context" 6 "encoding/json" 7 "fmt" 8 "math" 9 "os" 10 "sort" 11 "strconv" 12 "strings" 13 "unicode" 14 15 "github.com/stretchr/objx" 16 "github.com/urfave/cli/v2" 17 "go-micro.dev/v5/client" 18 "go-micro.dev/v5/registry" 19 ) 20 21 // LookupService queries the service for a service with the given alias. If 22 // no services are found for a given alias, the registry will return nil and 23 // the error will also be nil. An error is only returned if there was an issue 24 // listing from the registry. 25 func LookupService(name string) (*registry.Service, error) { 26 // return a lookup in the default domain as a catch all 27 return serviceWithName(name) 28 } 29 30 // FormatServiceUsage returns a string containing the service usage. 31 func FormatServiceUsage(srv *registry.Service, c *cli.Context) string { 32 alias := c.Args().First() 33 subcommand := c.Args().Get(1) 34 35 commands := make([]string, len(srv.Endpoints)) 36 endpoints := make([]*registry.Endpoint, len(srv.Endpoints)) 37 for i, e := range srv.Endpoints { 38 // map "Helloworld.Call" to "helloworld.call" 39 parts := strings.Split(e.Name, ".") 40 for i, part := range parts { 41 parts[i] = lowercaseInitial(part) 42 } 43 name := strings.Join(parts, ".") 44 45 // remove the prefix if it is the service name, e.g. rather than 46 // "micro run helloworld helloworld call", it would be 47 // "micro run helloworld call". 48 name = strings.TrimPrefix(name, alias+".") 49 50 // instead of "micro run helloworld foo.bar", the command should 51 // be "micro run helloworld foo bar". 52 commands[i] = strings.Replace(name, ".", " ", 1) 53 endpoints[i] = e 54 } 55 56 result := "" 57 if len(subcommand) > 0 && subcommand != "--help" { 58 result += fmt.Sprintf("NAME:\n\tmicro %v %v\n\n", alias, subcommand) 59 result += fmt.Sprintf("USAGE:\n\tmicro %v %v [flags]\n\n", alias, subcommand) 60 result += fmt.Sprintf("FLAGS:\n") 61 62 for i, command := range commands { 63 if command == subcommand { 64 result += renderFlags(endpoints[i]) 65 } 66 } 67 } else { 68 // sort the command names alphabetically 69 sort.Strings(commands) 70 71 result += fmt.Sprintf("NAME:\n\tmicro %v\n\n", alias) 72 result += fmt.Sprintf("VERSION:\n\t%v\n\n", srv.Version) 73 result += fmt.Sprintf("USAGE:\n\tmicro %v [command]\n\n", alias) 74 result += fmt.Sprintf("COMMANDS:\n\t%v\n", strings.Join(commands, "\n\t")) 75 76 } 77 78 return result 79 } 80 81 func lowercaseInitial(str string) string { 82 for i, v := range str { 83 return string(unicode.ToLower(v)) + str[i+1:] 84 } 85 return "" 86 } 87 88 func renderFlags(endpoint *registry.Endpoint) string { 89 ret := "" 90 for _, value := range endpoint.Request.Values { 91 ret += renderValue([]string{}, value) + "\n" 92 } 93 return ret 94 } 95 96 func renderValue(path []string, value *registry.Value) string { 97 if len(value.Values) > 0 { 98 renders := []string{} 99 for _, v := range value.Values { 100 renders = append(renders, renderValue(append(path, value.Name), v)) 101 } 102 return strings.Join(renders, "\n") 103 } 104 return fmt.Sprintf("\t--%v %v", strings.Join(append(path, value.Name), "_"), value.Type) 105 } 106 107 // CallService will call a service using the arguments and flags provided 108 // in the context. It will print the result or error to stdout. If there 109 // was an error performing the call, it will be returned. 110 func CallService(srv *registry.Service, args []string) error { 111 // parse the flags and args 112 args, flags, err := splitCmdArgs(args) 113 if err != nil { 114 return err 115 } 116 117 // construct the endpoint 118 endpoint, err := constructEndpoint(args) 119 if err != nil { 120 return err 121 } 122 123 // ensure the endpoint exists on the service 124 var ep *registry.Endpoint 125 for _, e := range srv.Endpoints { 126 if e.Name == endpoint { 127 ep = e 128 break 129 } 130 } 131 if ep == nil { 132 return fmt.Errorf("Endpoint %v not found for service %v", endpoint, srv.Name) 133 } 134 135 // parse the flags 136 body, err := FlagsToRequest(flags, ep.Request) 137 if err != nil { 138 return err 139 } 140 141 // create a context for the call based on the cli context 142 callCtx := context.TODO() 143 144 // TODO: parse out --header or --metadata 145 146 // construct and execute the request using the json content type 147 req := client.DefaultClient.NewRequest(srv.Name, endpoint, body, client.WithContentType("application/json")) 148 var rsp json.RawMessage 149 150 if err := client.DefaultClient.Call(callCtx, req, &rsp); err != nil { 151 return err 152 } 153 154 // format the response 155 var out bytes.Buffer 156 defer out.Reset() 157 if err := json.Indent(&out, rsp, "", "\t"); err != nil { 158 return err 159 } 160 out.Write([]byte("\n")) 161 out.WriteTo(os.Stdout) 162 163 return nil 164 } 165 166 // splitCmdArgs takes a cli context and parses out the args and flags, for 167 // example "micro helloworld --name=foo call apple" would result in "call", 168 // "apple" as args and {"name":"foo"} as the flags. 169 func splitCmdArgs(arguments []string) ([]string, map[string][]string, error) { 170 args := []string{} 171 flags := map[string][]string{} 172 173 prev := "" 174 for _, a := range arguments { 175 if !strings.HasPrefix(a, "--") { 176 if len(prev) == 0 { 177 args = append(args, a) 178 continue 179 } 180 _, exists := flags[prev] 181 if !exists { 182 flags[prev] = []string{} 183 } 184 185 flags[prev] = append(flags[prev], a) 186 prev = "" 187 continue 188 } 189 190 // comps would be "foo", "bar" for "--foo=bar" 191 comps := strings.Split(strings.TrimPrefix(a, "--"), "=") 192 _, exists := flags[comps[0]] 193 if !exists { 194 flags[comps[0]] = []string{} 195 } 196 switch len(comps) { 197 case 1: 198 prev = comps[0] 199 case 2: 200 flags[comps[0]] = append(flags[comps[0]], comps[1]) 201 default: 202 return nil, nil, fmt.Errorf("Invalid flag: %v. Expected format: --foo=bar", a) 203 } 204 } 205 206 return args, flags, nil 207 } 208 209 // constructEndpoint takes a slice of args and converts it into a valid endpoint 210 // such as Helloworld.Call or Foo.Bar, it will return an error if an invalid number 211 // of arguments were provided 212 func constructEndpoint(args []string) (string, error) { 213 var epComps []string 214 switch len(args) { 215 case 1: 216 epComps = append(args, "call") 217 case 2: 218 epComps = args 219 case 3: 220 epComps = args[1:3] 221 default: 222 return "", fmt.Errorf("Incorrect number of arguments") 223 } 224 225 // transform the endpoint components, e.g ["helloworld", "call"] to the 226 // endpoint name: "Helloworld.Call". 227 return fmt.Sprintf("%v.%v", strings.Title(epComps[0]), strings.Title(epComps[1])), nil 228 } 229 230 // ShouldRenderHelp returns true if the help flag was passed 231 func ShouldRenderHelp(args []string) bool { 232 args, flags, _ := splitCmdArgs(args) 233 234 // only 1 arg e.g micro helloworld 235 if len(args) == 1 { 236 return true 237 } 238 239 for key := range flags { 240 if key == "help" { 241 return true 242 } 243 } 244 245 return false 246 } 247 248 // FlagsToRequest parses a set of flags, e.g {name:"Foo", "options_surname","Bar"} and 249 // converts it into a request body. If the key is not a valid object in the request, an 250 // error will be returned. 251 // 252 // This function constructs []interface{} slices 253 // as opposed to typed ([]string etc) slices for easier testing 254 func FlagsToRequest(flags map[string][]string, req *registry.Value) (map[string]interface{}, error) { 255 coerceValue := func(valueType string, value []string) (interface{}, error) { 256 switch valueType { 257 case "bool": 258 if len(value) == 0 || len(strings.TrimSpace(value[0])) == 0 { 259 return true, nil 260 } 261 return strconv.ParseBool(value[0]) 262 case "int32": 263 i, err := strconv.Atoi(value[0]) 264 if err != nil { 265 return nil, err 266 } 267 if i < math.MinInt32 || i > math.MaxInt32 { 268 return nil, fmt.Errorf("value out of range for int32: %d", i) 269 } 270 return int32(i), nil 271 case "int64": 272 return strconv.ParseInt(value[0], 0, 64) 273 case "float64": 274 return strconv.ParseFloat(value[0], 64) 275 case "[]bool": 276 // length is one if it's a `,` separated int slice 277 if len(value) == 1 { 278 value = strings.Split(value[0], ",") 279 } 280 ret := []interface{}{} 281 for _, v := range value { 282 i, err := strconv.ParseBool(v) 283 if err != nil { 284 return nil, err 285 } 286 ret = append(ret, i) 287 } 288 return ret, nil 289 case "[]int32": 290 // length is one if it's a `,` separated int slice 291 if len(value) == 1 { 292 value = strings.Split(value[0], ",") 293 } 294 ret := []interface{}{} 295 for _, v := range value { 296 i, err := strconv.Atoi(v) 297 if err != nil { 298 return nil, err 299 } 300 if i < math.MinInt32 || i > math.MaxInt32 { 301 return nil, fmt.Errorf("value out of range for int32: %d", i) 302 } 303 ret = append(ret, int32(i)) 304 } 305 return ret, nil 306 case "[]int64": 307 // length is one if it's a `,` separated int slice 308 if len(value) == 1 { 309 value = strings.Split(value[0], ",") 310 } 311 ret := []interface{}{} 312 for _, v := range value { 313 i, err := strconv.ParseInt(v, 0, 64) 314 if err != nil { 315 return nil, err 316 } 317 ret = append(ret, i) 318 } 319 return ret, nil 320 case "[]float64": 321 // length is one if it's a `,` separated float slice 322 if len(value) == 1 { 323 value = strings.Split(value[0], ",") 324 } 325 ret := []interface{}{} 326 for _, v := range value { 327 i, err := strconv.ParseFloat(v, 64) 328 if err != nil { 329 return nil, err 330 } 331 ret = append(ret, i) 332 } 333 return ret, nil 334 case "[]string": 335 // length is one it's a `,` separated string slice 336 if len(value) == 1 { 337 value = strings.Split(value[0], ",") 338 } 339 ret := []interface{}{} 340 for _, v := range value { 341 ret = append(ret, v) 342 } 343 return ret, nil 344 case "string": 345 return value[0], nil 346 case "map[string]string": 347 var val map[string]string 348 if err := json.Unmarshal([]byte(value[0]), &val); err != nil { 349 return value[0], nil 350 } 351 return val, nil 352 default: 353 return value, nil 354 } 355 return nil, nil 356 } 357 358 result := objx.MustFromJSON("{}") 359 360 var flagType func(key string, values []*registry.Value, path ...string) (string, bool) 361 362 flagType = func(key string, values []*registry.Value, path ...string) (string, bool) { 363 for _, attr := range values { 364 if strings.Join(append(path, attr.Name), "-") == key { 365 return attr.Type, true 366 } 367 if attr.Values != nil { 368 typ, found := flagType(key, attr.Values, append(path, attr.Name)...) 369 if found { 370 return typ, found 371 } 372 } 373 } 374 return "", false 375 } 376 377 for key, value := range flags { 378 ty, found := flagType(key, req.Values) 379 if !found { 380 return nil, fmt.Errorf("Unknown flag: %v", key) 381 } 382 parsed, err := coerceValue(ty, value) 383 if err != nil { 384 return nil, err 385 } 386 // objx.Set does not create the path, 387 // so we do that here 388 if strings.Contains(key, "-") { 389 parts := strings.Split(key, "-") 390 for i, _ := range parts { 391 pToCreate := strings.Join(parts[0:i], ".") 392 if i > 0 && i < len(parts) && !result.Has(pToCreate) { 393 result.Set(pToCreate, map[string]interface{}{}) 394 } 395 } 396 } 397 path := strings.Replace(key, "-", ".", -1) 398 result.Set(path, parsed) 399 } 400 401 return result, nil 402 } 403 404 // find a service in a domain matching the name 405 func serviceWithName(name string) (*registry.Service, error) { 406 srvs, err := registry.GetService(name) 407 if err == registry.ErrNotFound { 408 return nil, nil 409 } else if err != nil { 410 return nil, err 411 } 412 if len(srvs) == 0 { 413 return nil, nil 414 } 415 return srvs[0], nil 416 }