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  }