package util import ( "bytes" "context" "encoding/json" "fmt" "math" "os" "sort" "strconv" "strings" "unicode" "github.com/stretchr/objx" "github.com/urfave/cli/v2" "go-micro.dev/v6/client" "go-micro.dev/v6/metadata" "go-micro.dev/v6/registry" ) // AddMetadataToContext parses metadata strings in the format "Key:Value" and adds them to the context func AddMetadataToContext(ctx context.Context, metadataStrings []string) context.Context { if len(metadataStrings) == 0 { return ctx } md := make(metadata.Metadata) for _, m := range metadataStrings { parts := strings.SplitN(m, ":", 2) if len(parts) != 2 { continue } key := strings.TrimSpace(parts[0]) value := strings.TrimSpace(parts[1]) md[key] = value } return metadata.MergeContext(ctx, md, true) } // LookupService queries the service for a service with the given alias. If // no services are found for a given alias, the registry will return nil and // the error will also be nil. An error is only returned if there was an issue // listing from the registry. func LookupService(name string) (*registry.Service, error) { // return a lookup in the default domain as a catch all return serviceWithName(name) } // FormatServiceUsage returns a string containing the service usage. func FormatServiceUsage(srv *registry.Service, c *cli.Context) string { alias := c.Args().First() subcommand := c.Args().Get(1) commands := make([]string, len(srv.Endpoints)) endpoints := make([]*registry.Endpoint, len(srv.Endpoints)) for i, e := range srv.Endpoints { // map "Helloworld.Call" to "helloworld.call" parts := strings.Split(e.Name, ".") for i, part := range parts { parts[i] = lowercaseInitial(part) } name := strings.Join(parts, ".") // remove the prefix if it is the service name, e.g. rather than // "micro run helloworld helloworld call", it would be // "micro run helloworld call". name = strings.TrimPrefix(name, alias+".") // instead of "micro run helloworld foo.bar", the command should // be "micro run helloworld foo bar". commands[i] = strings.Replace(name, ".", " ", 1) endpoints[i] = e } result := "" if len(subcommand) > 0 && subcommand != "--help" { result += fmt.Sprintf("NAME:\n\tmicro %v %v\n\n", alias, subcommand) result += fmt.Sprintf("USAGE:\n\tmicro %v %v [flags]\n\n", alias, subcommand) result += "FLAGS:\n" for i, command := range commands { if command == subcommand { result += renderFlags(endpoints[i]) } } } else { // sort the command names alphabetically sort.Strings(commands) result += fmt.Sprintf("NAME:\n\tmicro %v\n\n", alias) result += fmt.Sprintf("VERSION:\n\t%v\n\n", srv.Version) result += fmt.Sprintf("USAGE:\n\tmicro %v [command]\n\n", alias) result += fmt.Sprintf("COMMANDS:\n\t%v\n", strings.Join(commands, "\n\t")) } return result } func lowercaseInitial(str string) string { for i, v := range str { return string(unicode.ToLower(v)) + str[i+1:] } return "" } func renderFlags(endpoint *registry.Endpoint) string { ret := "" for _, value := range endpoint.Request.Values { ret += renderValue([]string{}, value) + "\n" } return ret } func renderValue(path []string, value *registry.Value) string { if len(value.Values) > 0 { renders := []string{} for _, v := range value.Values { renders = append(renders, renderValue(append(path, value.Name), v)) } return strings.Join(renders, "\n") } return fmt.Sprintf("\t--%v %v", strings.Join(append(path, value.Name), "_"), value.Type) } // CallService will call a service using the arguments and flags provided // in the context. It will print the result or error to stdout. If there // was an error performing the call, it will be returned. func CallService(srv *registry.Service, args []string) error { // parse the flags and args args, flags, err := splitCmdArgs(args) if err != nil { return err } // construct the endpoint endpoint, err := constructEndpoint(args) if err != nil { return err } // ensure the endpoint exists on the service var ep *registry.Endpoint for _, e := range srv.Endpoints { if e.Name == endpoint { ep = e break } } if ep == nil { return fmt.Errorf("endpoint %v not found for service %v", endpoint, srv.Name) } // create a context for the call callCtx := context.TODO() // parse out --header or --metadata flags before parsing request body // Note: This is for dynamic service calls (e.g., 'micro helloworld call --header X:Y'). // Direct 'micro call' commands are handled in cli.go. if headerFlags, ok := flags["header"]; ok { callCtx = AddMetadataToContext(callCtx, headerFlags) delete(flags, "header") } if metadataFlags, ok := flags["metadata"]; ok { callCtx = AddMetadataToContext(callCtx, metadataFlags) delete(flags, "metadata") } // parse the flags into request body body, err := FlagsToRequest(flags, ep.Request) if err != nil { return err } // construct and execute the request using the json content type req := client.DefaultClient.NewRequest(srv.Name, endpoint, body, client.WithContentType("application/json")) var rsp json.RawMessage if err := client.DefaultClient.Call(callCtx, req, &rsp); err != nil { return err } // format the response var out bytes.Buffer defer out.Reset() if err := json.Indent(&out, rsp, "", "\t"); err != nil { return err } out.Write([]byte("\n")) _, _ = out.WriteTo(os.Stdout) return nil } // splitCmdArgs takes a cli context and parses out the args and flags, for // example "micro helloworld --name=foo call apple" would result in "call", // "apple" as args and {"name":"foo"} as the flags. func splitCmdArgs(arguments []string) ([]string, map[string][]string, error) { args := []string{} flags := map[string][]string{} prev := "" for _, a := range arguments { if !strings.HasPrefix(a, "--") { if len(prev) == 0 { args = append(args, a) continue } _, exists := flags[prev] if !exists { flags[prev] = []string{} } flags[prev] = append(flags[prev], a) prev = "" continue } // comps would be "foo", "bar" for "--foo=bar" comps := strings.Split(strings.TrimPrefix(a, "--"), "=") _, exists := flags[comps[0]] if !exists { flags[comps[0]] = []string{} } switch len(comps) { case 1: prev = comps[0] case 2: flags[comps[0]] = append(flags[comps[0]], comps[1]) default: return nil, nil, fmt.Errorf("invalid flag: %v. Expected format: --foo=bar", a) } } return args, flags, nil } // constructEndpoint takes a slice of args and converts it into a valid endpoint // such as Helloworld.Call or Foo.Bar, it will return an error if an invalid number // of arguments were provided func constructEndpoint(args []string) (string, error) { var epComps []string switch len(args) { case 1: epComps = append(args, "call") case 2: epComps = args case 3: epComps = args[1:3] default: return "", fmt.Errorf("incorrect number of arguments") } // transform the endpoint components, e.g ["helloworld", "call"] to the // endpoint name: "Helloworld.Call". return fmt.Sprintf("%v.%v", strings.Title(epComps[0]), strings.Title(epComps[1])), nil } // ShouldRenderHelp returns true if the help flag was passed func ShouldRenderHelp(args []string) bool { args, flags, _ := splitCmdArgs(args) // only 1 arg e.g micro helloworld if len(args) == 1 { return true } for key := range flags { if key == "help" { return true } } return false } // FlagsToRequest parses a set of flags, e.g {name:"Foo", "options_surname","Bar"} and // converts it into a request body. If the key is not a valid object in the request, an // error will be returned. // // This function constructs []interface{} slices // as opposed to typed ([]string etc) slices for easier testing func FlagsToRequest(flags map[string][]string, req *registry.Value) (map[string]interface{}, error) { coerceValue := func(valueType string, value []string) (interface{}, error) { switch valueType { case "bool": if len(value) == 0 || len(strings.TrimSpace(value[0])) == 0 { return true, nil } return strconv.ParseBool(value[0]) case "int32": i, err := strconv.Atoi(value[0]) if err != nil { return nil, err } if i < math.MinInt32 || i > math.MaxInt32 { return nil, fmt.Errorf("value out of range for int32: %d", i) } return int32(i), nil case "int64": return strconv.ParseInt(value[0], 0, 64) case "float64": return strconv.ParseFloat(value[0], 64) case "[]bool": // length is one if it's a `,` separated int slice if len(value) == 1 { value = strings.Split(value[0], ",") } ret := []interface{}{} for _, v := range value { i, err := strconv.ParseBool(v) if err != nil { return nil, err } ret = append(ret, i) } return ret, nil case "[]int32": // length is one if it's a `,` separated int slice if len(value) == 1 { value = strings.Split(value[0], ",") } ret := []interface{}{} for _, v := range value { i, err := strconv.Atoi(v) if err != nil { return nil, err } if i < math.MinInt32 || i > math.MaxInt32 { return nil, fmt.Errorf("value out of range for int32: %d", i) } ret = append(ret, int32(i)) } return ret, nil case "[]int64": // length is one if it's a `,` separated int slice if len(value) == 1 { value = strings.Split(value[0], ",") } ret := []interface{}{} for _, v := range value { i, err := strconv.ParseInt(v, 0, 64) if err != nil { return nil, err } ret = append(ret, i) } return ret, nil case "[]float64": // length is one if it's a `,` separated float slice if len(value) == 1 { value = strings.Split(value[0], ",") } ret := []interface{}{} for _, v := range value { i, err := strconv.ParseFloat(v, 64) if err != nil { return nil, err } ret = append(ret, i) } return ret, nil case "[]string": // length is one it's a `,` separated string slice if len(value) == 1 { value = strings.Split(value[0], ",") } ret := []interface{}{} for _, v := range value { ret = append(ret, v) } return ret, nil case "string": return value[0], nil case "map[string]string": var val map[string]string if err := json.Unmarshal([]byte(value[0]), &val); err != nil { return value[0], nil } return val, nil default: return value, nil } } result := objx.MustFromJSON("{}") var flagType func(key string, values []*registry.Value, path ...string) (string, bool) flagType = func(key string, values []*registry.Value, path ...string) (string, bool) { for _, attr := range values { if strings.Join(append(path, attr.Name), "-") == key { return attr.Type, true } if attr.Values != nil { typ, found := flagType(key, attr.Values, append(path, attr.Name)...) if found { return typ, found } } } return "", false } for key, value := range flags { ty, found := flagType(key, req.Values) if !found { return nil, fmt.Errorf("unknown flag: %v", key) } parsed, err := coerceValue(ty, value) if err != nil { return nil, err } // objx.Set does not create the path, // so we do that here if strings.Contains(key, "-") { parts := strings.Split(key, "-") for i := range parts { pToCreate := strings.Join(parts[0:i], ".") if i > 0 && i < len(parts) && !result.Has(pToCreate) { result.Set(pToCreate, map[string]interface{}{}) } } } path := strings.ReplaceAll(key, "-", ".") result.Set(path, parsed) } return result, nil } // find a service in a domain matching the name func serviceWithName(name string) (*registry.Service, error) { srvs, err := registry.GetService(name) if err == registry.ErrNotFound { return nil, nil } else if err != nil { return nil, err } if len(srvs) == 0 { return nil, nil } return srvs[0], nil }