github.com/gogf/gf/v2@v2.7.4/os/gcmd/gcmd_command_object.go (about)

     1  // Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
     2  //
     3  // This Source Code Form is subject to the terms of the MIT License.
     4  // If a copy of the MIT was not distributed with this file,
     5  // You can obtain one at https://github.com/gogf/gf.
     6  //
     7  
     8  package gcmd
     9  
    10  import (
    11  	"context"
    12  	"fmt"
    13  	"reflect"
    14  
    15  	"github.com/gogf/gf/v2/container/gset"
    16  	"github.com/gogf/gf/v2/encoding/gjson"
    17  	"github.com/gogf/gf/v2/errors/gcode"
    18  	"github.com/gogf/gf/v2/errors/gerror"
    19  	"github.com/gogf/gf/v2/internal/intlog"
    20  	"github.com/gogf/gf/v2/internal/reflection"
    21  	"github.com/gogf/gf/v2/internal/utils"
    22  	"github.com/gogf/gf/v2/os/gstructs"
    23  	"github.com/gogf/gf/v2/text/gstr"
    24  	"github.com/gogf/gf/v2/util/gconv"
    25  	"github.com/gogf/gf/v2/util/gmeta"
    26  	"github.com/gogf/gf/v2/util/gtag"
    27  	"github.com/gogf/gf/v2/util/gutil"
    28  	"github.com/gogf/gf/v2/util/gvalid"
    29  )
    30  
    31  var (
    32  	// defaultValueTags is the struct tag names for default value storing.
    33  	defaultValueTags = []string{"d", "default"}
    34  )
    35  
    36  // NewFromObject creates and returns a root command object using given object.
    37  func NewFromObject(object interface{}) (rootCmd *Command, err error) {
    38  	switch c := object.(type) {
    39  	case Command:
    40  		return &c, nil
    41  	case *Command:
    42  		return c, nil
    43  	}
    44  
    45  	originValueAndKind := reflection.OriginValueAndKind(object)
    46  	if originValueAndKind.OriginKind != reflect.Struct {
    47  		err = gerror.Newf(
    48  			`input object should be type of struct, but got "%s"`,
    49  			originValueAndKind.InputValue.Type().String(),
    50  		)
    51  		return
    52  	}
    53  	var reflectValue = originValueAndKind.InputValue
    54  	// If given `object` is not pointer, it then creates a temporary one,
    55  	// of which the value is `reflectValue`.
    56  	// It then can retrieve all the methods both of struct/*struct.
    57  	if reflectValue.Kind() == reflect.Struct {
    58  		newValue := reflect.New(reflectValue.Type())
    59  		newValue.Elem().Set(reflectValue)
    60  		reflectValue = newValue
    61  	}
    62  
    63  	// Root command creating.
    64  	rootCmd, err = newCommandFromObjectMeta(object, "")
    65  	if err != nil {
    66  		return
    67  	}
    68  	// Sub command creating.
    69  	var (
    70  		nameSet         = gset.NewStrSet()
    71  		rootCommandName = gmeta.Get(object, gtag.Root).String()
    72  		subCommands     []*Command
    73  	)
    74  	if rootCommandName == "" {
    75  		rootCommandName = rootCmd.Name
    76  	}
    77  	for i := 0; i < reflectValue.NumMethod(); i++ {
    78  		var (
    79  			method      = reflectValue.Type().Method(i)
    80  			methodValue = reflectValue.Method(i)
    81  			methodType  = methodValue.Type()
    82  			methodCmd   *Command
    83  		)
    84  		methodCmd, err = newCommandFromMethod(object, method, methodValue, methodType)
    85  		if err != nil {
    86  			return
    87  		}
    88  		if nameSet.Contains(methodCmd.Name) {
    89  			err = gerror.Newf(
    90  				`command name should be unique, found duplicated command name in method "%s"`,
    91  				methodType.String(),
    92  			)
    93  			return
    94  		}
    95  		if rootCommandName == methodCmd.Name {
    96  			methodToRootCmdWhenNameEqual(rootCmd, methodCmd)
    97  		} else {
    98  			subCommands = append(subCommands, methodCmd)
    99  		}
   100  	}
   101  	if len(subCommands) > 0 {
   102  		err = rootCmd.AddCommand(subCommands...)
   103  	}
   104  	return
   105  }
   106  
   107  func methodToRootCmdWhenNameEqual(rootCmd *Command, methodCmd *Command) {
   108  	if rootCmd.Usage == "" {
   109  		rootCmd.Usage = methodCmd.Usage
   110  	}
   111  	if rootCmd.Brief == "" {
   112  		rootCmd.Brief = methodCmd.Brief
   113  	}
   114  	if rootCmd.Description == "" {
   115  		rootCmd.Description = methodCmd.Description
   116  	}
   117  	if rootCmd.Examples == "" {
   118  		rootCmd.Examples = methodCmd.Examples
   119  	}
   120  	if rootCmd.Func == nil {
   121  		rootCmd.Func = methodCmd.Func
   122  	}
   123  	if rootCmd.FuncWithValue == nil {
   124  		rootCmd.FuncWithValue = methodCmd.FuncWithValue
   125  	}
   126  	if rootCmd.HelpFunc == nil {
   127  		rootCmd.HelpFunc = methodCmd.HelpFunc
   128  	}
   129  	if len(rootCmd.Arguments) == 0 {
   130  		rootCmd.Arguments = methodCmd.Arguments
   131  	}
   132  	if !rootCmd.Strict {
   133  		rootCmd.Strict = methodCmd.Strict
   134  	}
   135  	if rootCmd.Config == "" {
   136  		rootCmd.Config = methodCmd.Config
   137  	}
   138  }
   139  
   140  // The `object` is the Meta attribute from business object, and the `name` is the command name,
   141  // commonly from method name, which is used when no name tag is defined in Meta.
   142  func newCommandFromObjectMeta(object interface{}, name string) (command *Command, err error) {
   143  	var metaData = gmeta.Data(object)
   144  	if err = gconv.Scan(metaData, &command); err != nil {
   145  		return
   146  	}
   147  	// Name field is necessary.
   148  	if command.Name == "" {
   149  		if name == "" {
   150  			err = gerror.Newf(
   151  				`command name cannot be empty, "name" tag not found in meta of struct "%s"`,
   152  				reflect.TypeOf(object).String(),
   153  			)
   154  			return
   155  		}
   156  		command.Name = name
   157  	}
   158  	if command.Brief == "" {
   159  		for _, tag := range []string{gtag.Summary, gtag.SummaryShort, gtag.SummaryShort2} {
   160  			command.Brief = metaData[tag]
   161  			if command.Brief != "" {
   162  				break
   163  			}
   164  		}
   165  	}
   166  	if command.Description == "" {
   167  		command.Description = metaData[gtag.DescriptionShort]
   168  	}
   169  	if command.Brief == "" && command.Description != "" {
   170  		command.Brief = command.Description
   171  		command.Description = ""
   172  	}
   173  	if command.Examples == "" {
   174  		command.Examples = metaData[gtag.ExampleShort]
   175  	}
   176  	if command.Additional == "" {
   177  		command.Additional = metaData[gtag.AdditionalShort]
   178  	}
   179  	return
   180  }
   181  
   182  func newCommandFromMethod(
   183  	object interface{}, method reflect.Method, methodValue reflect.Value, methodType reflect.Type,
   184  ) (command *Command, err error) {
   185  	// Necessary validation for input/output parameters and naming.
   186  	if methodType.NumIn() != 2 || methodType.NumOut() != 2 {
   187  		if methodType.PkgPath() != "" {
   188  			err = gerror.NewCodef(
   189  				gcode.CodeInvalidParameter,
   190  				`invalid command: %s.%s.%s defined as "%s", but "func(context.Context, Input)(Output, error)" is required`,
   191  				methodType.PkgPath(), reflect.TypeOf(object).Name(), method.Name, methodType.String(),
   192  			)
   193  		} else {
   194  			err = gerror.NewCodef(
   195  				gcode.CodeInvalidParameter,
   196  				`invalid command: %s.%s defined as "%s", but "func(context.Context, Input)(Output, error)" is required`,
   197  				reflect.TypeOf(object).Name(), method.Name, methodType.String(),
   198  			)
   199  		}
   200  		return
   201  	}
   202  	if !methodType.In(0).Implements(reflect.TypeOf((*context.Context)(nil)).Elem()) {
   203  		err = gerror.NewCodef(
   204  			gcode.CodeInvalidParameter,
   205  			`invalid command: %s.%s defined as "%s", but the first input parameter should be type of "context.Context"`,
   206  			reflect.TypeOf(object).Name(), method.Name, methodType.String(),
   207  		)
   208  		return
   209  	}
   210  	if !methodType.Out(1).Implements(reflect.TypeOf((*error)(nil)).Elem()) {
   211  		err = gerror.NewCodef(
   212  			gcode.CodeInvalidParameter,
   213  			`invalid command: %s.%s defined as "%s", but the last output parameter should be type of "error"`,
   214  			reflect.TypeOf(object).Name(), method.Name, methodType.String(),
   215  		)
   216  		return
   217  	}
   218  	// The input struct should be named as `xxxInput`.
   219  	if !gstr.HasSuffix(methodType.In(1).String(), `Input`) {
   220  		err = gerror.NewCodef(
   221  			gcode.CodeInvalidParameter,
   222  			`invalid struct naming for input: defined as "%s", but it should be named with "Input" suffix like "xxxInput"`,
   223  			methodType.In(1).String(),
   224  		)
   225  		return
   226  	}
   227  	// The output struct should be named as `xxxOutput`.
   228  	if !gstr.HasSuffix(methodType.Out(0).String(), `Output`) {
   229  		err = gerror.NewCodef(
   230  			gcode.CodeInvalidParameter,
   231  			`invalid struct naming for output: defined as "%s", but it should be named with "Output" suffix like "xxxOutput"`,
   232  			methodType.Out(0).String(),
   233  		)
   234  		return
   235  	}
   236  
   237  	var inputObject reflect.Value
   238  	if methodType.In(1).Kind() == reflect.Ptr {
   239  		inputObject = reflect.New(methodType.In(1).Elem()).Elem()
   240  	} else {
   241  		inputObject = reflect.New(methodType.In(1)).Elem()
   242  	}
   243  
   244  	// Command creating.
   245  	if command, err = newCommandFromObjectMeta(inputObject.Interface(), method.Name); err != nil {
   246  		return
   247  	}
   248  
   249  	// Options creating.
   250  	if command.Arguments, err = newArgumentsFromInput(inputObject.Interface()); err != nil {
   251  		return
   252  	}
   253  
   254  	// For input struct converting using priority tag.
   255  	var priorityTag = gstr.Join([]string{tagNameName, tagNameShort}, ",")
   256  
   257  	// =============================================================================================
   258  	// Create function that has value return.
   259  	// =============================================================================================
   260  	command.FuncWithValue = func(ctx context.Context, parser *Parser) (out interface{}, err error) {
   261  		ctx = context.WithValue(ctx, CtxKeyParser, parser)
   262  		var (
   263  			data        = gconv.Map(parser.GetOptAll())
   264  			argIndex    = 0
   265  			arguments   = parser.GetArgAll()
   266  			inputValues = []reflect.Value{reflect.ValueOf(ctx)}
   267  		)
   268  		if value := ctx.Value(CtxKeyArgumentsIndex); value != nil {
   269  			argIndex = value.(int)
   270  			// Use the left args to assign to input struct object.
   271  			if argIndex < len(arguments) {
   272  				arguments = arguments[argIndex:]
   273  			}
   274  		}
   275  		if data == nil {
   276  			data = map[string]interface{}{}
   277  		}
   278  		// Handle orphan options.
   279  		for _, arg := range command.Arguments {
   280  			if arg.IsArg {
   281  				// Read argument from command line index.
   282  				if argIndex < len(arguments) {
   283  					data[arg.Name] = arguments[argIndex]
   284  					argIndex++
   285  				}
   286  			} else {
   287  				// Read argument from command line option name.
   288  				if arg.Orphan {
   289  					if orphanValue := parser.GetOpt(arg.Name); orphanValue != nil {
   290  						if orphanValue.String() == "" {
   291  							// Example: gf -f
   292  							data[arg.Name] = "true"
   293  							if arg.Short != "" {
   294  								data[arg.Short] = "true"
   295  							}
   296  						} else {
   297  							// Adapter with common user habits.
   298  							// Eg:
   299  							// `gf -f=0`: which parameter `f` is parsed as false
   300  							// `gf -f=1`: which parameter `f` is parsed as true
   301  							data[arg.Name] = orphanValue.Bool()
   302  						}
   303  					}
   304  				}
   305  			}
   306  		}
   307  		// Default values from struct tag.
   308  		if err = mergeDefaultStructValue(data, inputObject.Interface()); err != nil {
   309  			return nil, err
   310  		}
   311  		// Construct input parameters.
   312  		if len(data) > 0 {
   313  			intlog.PrintFunc(ctx, func() string {
   314  				return fmt.Sprintf(`input command data map: %s`, gjson.MustEncode(data))
   315  			})
   316  			if inputObject.Kind() == reflect.Ptr {
   317  				err = gconv.StructTag(data, inputObject.Interface(), priorityTag)
   318  			} else {
   319  				err = gconv.StructTag(data, inputObject.Addr().Interface(), priorityTag)
   320  			}
   321  			intlog.PrintFunc(ctx, func() string {
   322  				return fmt.Sprintf(`input object assigned data: %s`, gjson.MustEncode(inputObject.Interface()))
   323  			})
   324  			if err != nil {
   325  				return
   326  			}
   327  		}
   328  
   329  		// Parameters validation.
   330  		if err = gvalid.New().Bail().Data(inputObject.Interface()).Assoc(data).Run(ctx); err != nil {
   331  			err = gerror.Wrapf(gerror.Current(err), `arguments validation failed for command "%s"`, command.Name)
   332  			return
   333  		}
   334  		inputValues = append(inputValues, inputObject)
   335  
   336  		// Call handler with dynamic created parameter values.
   337  		results := methodValue.Call(inputValues)
   338  		out = results[0].Interface()
   339  		if !results[1].IsNil() {
   340  			if v, ok := results[1].Interface().(error); ok {
   341  				err = v
   342  			}
   343  		}
   344  		return
   345  	}
   346  	return
   347  }
   348  
   349  func newArgumentsFromInput(object interface{}) (args []Argument, err error) {
   350  	var (
   351  		fields   []gstructs.Field
   352  		nameSet  = gset.NewStrSet()
   353  		shortSet = gset.NewStrSet()
   354  	)
   355  	fields, err = gstructs.Fields(gstructs.FieldsInput{
   356  		Pointer:         object,
   357  		RecursiveOption: gstructs.RecursiveOptionEmbeddedNoTag,
   358  	})
   359  	for _, field := range fields {
   360  		var (
   361  			arg      = Argument{}
   362  			metaData = field.TagMap()
   363  		)
   364  		if err = gconv.Scan(metaData, &arg); err != nil {
   365  			return nil, err
   366  		}
   367  		if arg.Name == "" {
   368  			arg.Name = field.Name()
   369  		}
   370  		if arg.Name == helpOptionName {
   371  			return nil, gerror.Newf(
   372  				`argument name "%s" defined in "%s.%s" is already token by built-in arguments`,
   373  				arg.Name, reflect.TypeOf(object).String(), field.Name(),
   374  			)
   375  		}
   376  		if arg.Short == helpOptionNameShort {
   377  			return nil, gerror.Newf(
   378  				`short argument name "%s" defined in "%s.%s" is already token by built-in arguments`,
   379  				arg.Short, reflect.TypeOf(object).String(), field.Name(),
   380  			)
   381  		}
   382  		if arg.Brief == "" {
   383  			arg.Brief = field.TagDescription()
   384  		}
   385  		if v, ok := metaData[gtag.Arg]; ok {
   386  			arg.IsArg = gconv.Bool(v)
   387  		}
   388  		if nameSet.Contains(arg.Name) {
   389  			return nil, gerror.Newf(
   390  				`argument name "%s" defined in "%s.%s" is already token by other argument`,
   391  				arg.Name, reflect.TypeOf(object).String(), field.Name(),
   392  			)
   393  		}
   394  		nameSet.Add(arg.Name)
   395  
   396  		if arg.Short != "" {
   397  			if shortSet.Contains(arg.Short) {
   398  				return nil, gerror.Newf(
   399  					`short argument name "%s" defined in "%s.%s" is already token by other argument`,
   400  					arg.Short, reflect.TypeOf(object).String(), field.Name(),
   401  				)
   402  			}
   403  			shortSet.Add(arg.Short)
   404  		}
   405  
   406  		args = append(args, arg)
   407  	}
   408  
   409  	return
   410  }
   411  
   412  // mergeDefaultStructValue merges the request parameters with default values from struct tag definition.
   413  func mergeDefaultStructValue(data map[string]interface{}, pointer interface{}) error {
   414  	tagFields, err := gstructs.TagFields(pointer, defaultValueTags)
   415  	if err != nil {
   416  		return err
   417  	}
   418  	if len(tagFields) > 0 {
   419  		var (
   420  			foundKey   string
   421  			foundValue interface{}
   422  		)
   423  		for _, field := range tagFields {
   424  			var (
   425  				nameValue  = field.Tag(tagNameName)
   426  				shortValue = field.Tag(tagNameShort)
   427  			)
   428  			// If it already has value, it then ignores the default value.
   429  			if value, ok := data[nameValue]; ok {
   430  				data[field.Name()] = value
   431  				continue
   432  			}
   433  			if value, ok := data[shortValue]; ok {
   434  				data[field.Name()] = value
   435  				continue
   436  			}
   437  			foundKey, foundValue = gutil.MapPossibleItemByKey(data, field.Name())
   438  			if foundKey == "" {
   439  				data[field.Name()] = field.TagValue
   440  			} else {
   441  				if utils.IsEmpty(foundValue) {
   442  					data[foundKey] = field.TagValue
   443  				}
   444  			}
   445  		}
   446  	}
   447  	return nil
   448  }