chore: import upstream snapshot with attribution
govulncheck / govulncheck (push) Has been cancelled
Lint / golangci-lint (push) Has been cancelled
Run Tests / Unit Tests (push) Has been cancelled
Run Tests / Etcd Integration Tests (push) Has been cancelled
Harness (E2E) / Harnesses (mock LLM) (push) Has been cancelled
Harness (E2E) / Provider harnesses (live LLM conformance) (push) Has been cancelled
govulncheck / govulncheck (push) Has been cancelled
Lint / golangci-lint (push) Has been cancelled
Run Tests / Unit Tests (push) Has been cancelled
Run Tests / Etcd Integration Tests (push) Has been cancelled
Harness (E2E) / Harnesses (mock LLM) (push) Has been cancelled
Harness (E2E) / Provider harnesses (live LLM conformance) (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,447 @@
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,453 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"context"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
"go-micro.dev/v6/metadata"
|
||||
goregistry "go-micro.dev/v6/registry"
|
||||
)
|
||||
|
||||
type parseCase struct {
|
||||
args []string
|
||||
values *goregistry.Value
|
||||
expected map[string]interface{}
|
||||
}
|
||||
|
||||
func TestDynamicFlagParsing(t *testing.T) {
|
||||
cases := []parseCase{
|
||||
{
|
||||
args: []string{"--ss=a,b"},
|
||||
values: &goregistry.Value{
|
||||
Values: []*goregistry.Value{
|
||||
{
|
||||
Name: "ss",
|
||||
Type: "[]string",
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: map[string]interface{}{
|
||||
"ss": []interface{}{"a", "b"},
|
||||
},
|
||||
},
|
||||
{
|
||||
args: []string{"--ss", "a,b"},
|
||||
values: &goregistry.Value{
|
||||
Values: []*goregistry.Value{
|
||||
{
|
||||
Name: "ss",
|
||||
Type: "[]string",
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: map[string]interface{}{
|
||||
"ss": []interface{}{"a", "b"},
|
||||
},
|
||||
},
|
||||
{
|
||||
args: []string{"--ss=a", "--ss=b"},
|
||||
values: &goregistry.Value{
|
||||
Values: []*goregistry.Value{
|
||||
{
|
||||
Name: "ss",
|
||||
Type: "[]string",
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: map[string]interface{}{
|
||||
"ss": []interface{}{"a", "b"},
|
||||
},
|
||||
},
|
||||
{
|
||||
args: []string{"--ss", "a", "--ss", "b"},
|
||||
values: &goregistry.Value{
|
||||
Values: []*goregistry.Value{
|
||||
{
|
||||
Name: "ss",
|
||||
Type: "[]string",
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: map[string]interface{}{
|
||||
"ss": []interface{}{"a", "b"},
|
||||
},
|
||||
},
|
||||
{
|
||||
args: []string{"--bs=true,false"},
|
||||
values: &goregistry.Value{
|
||||
Values: []*goregistry.Value{
|
||||
{
|
||||
Name: "bs",
|
||||
Type: "[]bool",
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: map[string]interface{}{
|
||||
"bs": []interface{}{true, false},
|
||||
},
|
||||
},
|
||||
{
|
||||
args: []string{"--bs", "true,false"},
|
||||
values: &goregistry.Value{
|
||||
Values: []*goregistry.Value{
|
||||
{
|
||||
Name: "bs",
|
||||
Type: "[]bool",
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: map[string]interface{}{
|
||||
"bs": []interface{}{true, false},
|
||||
},
|
||||
},
|
||||
{
|
||||
args: []string{"--bs=true", "--bs=false"},
|
||||
values: &goregistry.Value{
|
||||
Values: []*goregistry.Value{
|
||||
{
|
||||
Name: "bs",
|
||||
Type: "[]bool",
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: map[string]interface{}{
|
||||
"bs": []interface{}{true, false},
|
||||
},
|
||||
},
|
||||
{
|
||||
args: []string{"--bs", "true", "--bs", "false"},
|
||||
values: &goregistry.Value{
|
||||
Values: []*goregistry.Value{
|
||||
{
|
||||
Name: "bs",
|
||||
Type: "[]bool",
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: map[string]interface{}{
|
||||
"bs": []interface{}{true, false},
|
||||
},
|
||||
},
|
||||
{
|
||||
args: []string{"--is=10,20"},
|
||||
values: &goregistry.Value{
|
||||
Values: []*goregistry.Value{
|
||||
{
|
||||
Name: "is",
|
||||
Type: "[]int32",
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: map[string]interface{}{
|
||||
"is": []interface{}{int32(10), int32(20)},
|
||||
},
|
||||
},
|
||||
{
|
||||
args: []string{"--is", "10,20"},
|
||||
values: &goregistry.Value{
|
||||
Values: []*goregistry.Value{
|
||||
{
|
||||
Name: "is",
|
||||
Type: "[]int32",
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: map[string]interface{}{
|
||||
"is": []interface{}{int32(10), int32(20)},
|
||||
},
|
||||
},
|
||||
{
|
||||
args: []string{"--is=10", "--is=20"},
|
||||
values: &goregistry.Value{
|
||||
Values: []*goregistry.Value{
|
||||
{
|
||||
Name: "is",
|
||||
Type: "[]int32",
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: map[string]interface{}{
|
||||
"is": []interface{}{int32(10), int32(20)},
|
||||
},
|
||||
},
|
||||
{
|
||||
args: []string{"--is", "10", "--is", "20"},
|
||||
values: &goregistry.Value{
|
||||
Values: []*goregistry.Value{
|
||||
{
|
||||
Name: "is",
|
||||
Type: "[]int32",
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: map[string]interface{}{
|
||||
"is": []interface{}{int32(10), int32(20)},
|
||||
},
|
||||
},
|
||||
{
|
||||
args: []string{"--is=10,20"},
|
||||
values: &goregistry.Value{
|
||||
Values: []*goregistry.Value{
|
||||
{
|
||||
Name: "is",
|
||||
Type: "[]int64",
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: map[string]interface{}{
|
||||
"is": []interface{}{int64(10), int64(20)},
|
||||
},
|
||||
},
|
||||
{
|
||||
args: []string{"--is", "10,20"},
|
||||
values: &goregistry.Value{
|
||||
Values: []*goregistry.Value{
|
||||
{
|
||||
Name: "is",
|
||||
Type: "[]int64",
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: map[string]interface{}{
|
||||
"is": []interface{}{int64(10), int64(20)},
|
||||
},
|
||||
},
|
||||
{
|
||||
args: []string{"--is=10", "--is=20"},
|
||||
values: &goregistry.Value{
|
||||
Values: []*goregistry.Value{
|
||||
{
|
||||
Name: "is",
|
||||
Type: "[]int64",
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: map[string]interface{}{
|
||||
"is": []interface{}{int64(10), int64(20)},
|
||||
},
|
||||
},
|
||||
{
|
||||
args: []string{"--is", "10", "--is", "20"},
|
||||
values: &goregistry.Value{
|
||||
Values: []*goregistry.Value{
|
||||
{
|
||||
Name: "is",
|
||||
Type: "[]int64",
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: map[string]interface{}{
|
||||
"is": []interface{}{int64(10), int64(20)},
|
||||
},
|
||||
},
|
||||
{
|
||||
args: []string{"--fs=10.1,20.2"},
|
||||
values: &goregistry.Value{
|
||||
Values: []*goregistry.Value{
|
||||
{
|
||||
Name: "fs",
|
||||
Type: "[]float64",
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: map[string]interface{}{
|
||||
"fs": []interface{}{float64(10.1), float64(20.2)},
|
||||
},
|
||||
},
|
||||
{
|
||||
args: []string{"--fs", "10.1,20.2"},
|
||||
values: &goregistry.Value{
|
||||
Values: []*goregistry.Value{
|
||||
{
|
||||
Name: "fs",
|
||||
Type: "[]float64",
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: map[string]interface{}{
|
||||
"fs": []interface{}{float64(10.1), float64(20.2)},
|
||||
},
|
||||
},
|
||||
{
|
||||
args: []string{"--fs=10.1", "--fs=20.2"},
|
||||
values: &goregistry.Value{
|
||||
Values: []*goregistry.Value{
|
||||
{
|
||||
Name: "fs",
|
||||
Type: "[]float64",
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: map[string]interface{}{
|
||||
"fs": []interface{}{float64(10.1), float64(20.2)},
|
||||
},
|
||||
},
|
||||
{
|
||||
args: []string{"--fs", "10.1", "--fs", "20.2"},
|
||||
values: &goregistry.Value{
|
||||
Values: []*goregistry.Value{
|
||||
{
|
||||
Name: "fs",
|
||||
Type: "[]float64",
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: map[string]interface{}{
|
||||
"fs": []interface{}{float64(10.1), float64(20.2)},
|
||||
},
|
||||
},
|
||||
{
|
||||
args: []string{"--user_email=someemail"},
|
||||
values: &goregistry.Value{
|
||||
Values: []*goregistry.Value{
|
||||
{
|
||||
Name: "user_email",
|
||||
Type: "string",
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: map[string]interface{}{
|
||||
"user_email": "someemail",
|
||||
},
|
||||
},
|
||||
{
|
||||
args: []string{"--user_email=someemail", "--user_name=somename"},
|
||||
values: &goregistry.Value{
|
||||
Values: []*goregistry.Value{
|
||||
{
|
||||
Name: "user_email",
|
||||
Type: "string",
|
||||
},
|
||||
{
|
||||
Name: "user_name",
|
||||
Type: "string",
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: map[string]interface{}{
|
||||
"user_email": "someemail",
|
||||
"user_name": "somename",
|
||||
},
|
||||
},
|
||||
{
|
||||
args: []string{"--b"},
|
||||
values: &goregistry.Value{
|
||||
Values: []*goregistry.Value{
|
||||
{
|
||||
Name: "b",
|
||||
Type: "bool",
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: map[string]interface{}{
|
||||
"b": true,
|
||||
},
|
||||
},
|
||||
{
|
||||
args: []string{"--user_friend_email=hi"},
|
||||
values: &goregistry.Value{
|
||||
Values: []*goregistry.Value{
|
||||
{
|
||||
Name: "user_friend_email",
|
||||
Type: "string",
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: map[string]interface{}{
|
||||
"user_friend_email": "hi",
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(strings.Join(c.args, " "), func(t *testing.T) {
|
||||
_, flags, err := splitCmdArgs(c.args)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
req, err := FlagsToRequest(flags, c.values)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !reflect.DeepEqual(c.expected, req) {
|
||||
spew.Dump("Expected:", c.expected, "got: ", req)
|
||||
t.Fatalf("Expected %v, got %v", c.expected, req)
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddMetadataToContext(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
metadataStrs []string
|
||||
expectedKeys []string
|
||||
expectedValues []string
|
||||
}{
|
||||
{
|
||||
name: "Single metadata",
|
||||
metadataStrs: []string{"Key1:Value1"},
|
||||
expectedKeys: []string{"Key1"},
|
||||
expectedValues: []string{"Value1"},
|
||||
},
|
||||
{
|
||||
name: "Multiple metadata",
|
||||
metadataStrs: []string{"Key1:Value1", "Key2:Value2"},
|
||||
expectedKeys: []string{"Key1", "Key2"},
|
||||
expectedValues: []string{"Value1", "Value2"},
|
||||
},
|
||||
{
|
||||
name: "Metadata with spaces",
|
||||
metadataStrs: []string{"Key1: Value1 ", " Key2 : Value2"},
|
||||
expectedKeys: []string{"Key1", "Key2"},
|
||||
expectedValues: []string{"Value1", "Value2"},
|
||||
},
|
||||
{
|
||||
name: "Metadata with colon in value",
|
||||
metadataStrs: []string{"Authorization:Bearer token:123"},
|
||||
expectedKeys: []string{"Authorization"},
|
||||
expectedValues: []string{"Bearer token:123"},
|
||||
},
|
||||
{
|
||||
name: "Empty metadata",
|
||||
metadataStrs: []string{},
|
||||
expectedKeys: []string{},
|
||||
expectedValues: []string{},
|
||||
},
|
||||
{
|
||||
name: "Invalid metadata format",
|
||||
metadataStrs: []string{"InvalidFormat"},
|
||||
expectedKeys: []string{},
|
||||
expectedValues: []string{},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
ctx = AddMetadataToContext(ctx, tt.metadataStrs)
|
||||
|
||||
md, ok := metadata.FromContext(ctx)
|
||||
if len(tt.expectedKeys) == 0 && !ok {
|
||||
return // Expected no metadata
|
||||
}
|
||||
|
||||
if !ok && len(tt.expectedKeys) > 0 {
|
||||
t.Fatal("Expected metadata in context but got none")
|
||||
}
|
||||
|
||||
for i, key := range tt.expectedKeys {
|
||||
value, found := md.Get(key)
|
||||
if !found {
|
||||
t.Fatalf("Expected key %s not found in metadata", key)
|
||||
}
|
||||
if value != tt.expectedValues[i] {
|
||||
t.Fatalf("Expected value %s for key %s, got %s", tt.expectedValues[i], key, value)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
// Package cliutil contains methods used across all cli commands
|
||||
// @todo: get rid of os.Exits and use errors instread
|
||||
package util
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/urfave/cli/v2"
|
||||
merrors "go-micro.dev/v6/errors"
|
||||
)
|
||||
|
||||
type Exec func(*cli.Context, []string) ([]byte, error)
|
||||
|
||||
func Print(e Exec) func(*cli.Context) error {
|
||||
return func(c *cli.Context) error {
|
||||
rsp, err := e(c, c.Args().Slice())
|
||||
if err != nil {
|
||||
return CliError(err)
|
||||
}
|
||||
if len(rsp) > 0 {
|
||||
fmt.Printf("%s\n", string(rsp))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// CliError returns a user friendly message from error. If we can't determine a good one returns an error with code 128
|
||||
func CliError(err error) cli.ExitCoder {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
// if it's already a cli.ExitCoder we use this
|
||||
cerr, ok := err.(cli.ExitCoder)
|
||||
if ok {
|
||||
return cerr
|
||||
}
|
||||
|
||||
// grpc errors
|
||||
if mname := regexp.MustCompile(`malformed method name: \\?"(\w+)\\?"`).FindStringSubmatch(err.Error()); len(mname) > 0 {
|
||||
return cli.Exit(fmt.Sprintf(`Method name "%s" invalid format. Expecting service.endpoint`, mname[1]), 3)
|
||||
}
|
||||
if service := regexp.MustCompile(`service ([\w\.]+): route not found`).FindStringSubmatch(err.Error()); len(service) > 0 {
|
||||
return cli.Exit(fmt.Sprintf(`Service "%s" not found`, service[1]), 4)
|
||||
}
|
||||
if service := regexp.MustCompile(`unknown service ([\w\.]+)`).FindStringSubmatch(err.Error()); len(service) > 0 {
|
||||
if strings.Contains(service[0], ".") {
|
||||
return cli.Exit(fmt.Sprintf(`Service method "%s" not found`, service[1]), 5)
|
||||
}
|
||||
return cli.Exit(fmt.Sprintf(`Service "%s" not found`, service[1]), 5)
|
||||
}
|
||||
if address := regexp.MustCompile(`Error while dialing dial tcp.*?([\w]+\.[\w:\.]+): `).FindStringSubmatch(err.Error()); len(address) > 0 {
|
||||
return cli.Exit(fmt.Sprintf(`Failed to connect to micro server at %s`, address[1]), 4)
|
||||
}
|
||||
|
||||
merr, ok := err.(*merrors.Error)
|
||||
if !ok {
|
||||
return cli.Exit(err, 128)
|
||||
}
|
||||
|
||||
switch merr.Code {
|
||||
case 408:
|
||||
return cli.Exit("Request timed out", 1)
|
||||
case 401:
|
||||
// TODO check if not signed in, prompt to sign in
|
||||
return cli.Exit("Not authorized to perform this request", 2)
|
||||
}
|
||||
|
||||
// fallback to using the detail from the merr
|
||||
return cli.Exit(merr.Detail, 127)
|
||||
}
|
||||
Reference in New Issue
Block a user