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  }