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