gf/os/gcmd/gcmd_command_object.go

323 lines
8.6 KiB
Go
Raw Normal View History

2021-11-22 11:23:46 +08:00
// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
//
// This Source Code Form is subject to the terms of the MIT License.
// If a copy of the MIT was not distributed with this file,
// You can obtain one at https://github.com/gogf/gf.
//
package gcmd
import (
"context"
2021-11-22 11:23:46 +08:00
"reflect"
"github.com/gogf/gf/v2/container/gset"
2021-11-22 11:23:46 +08:00
"github.com/gogf/gf/v2/errors/gcode"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/internal/utils"
"github.com/gogf/gf/v2/os/gstructs"
2021-11-22 11:23:46 +08:00
"github.com/gogf/gf/v2/text/gstr"
2021-11-23 14:08:37 +08:00
"github.com/gogf/gf/v2/util/gconv"
2021-11-22 11:23:46 +08:00
"github.com/gogf/gf/v2/util/gmeta"
2021-11-23 14:08:37 +08:00
"github.com/gogf/gf/v2/util/gutil"
"github.com/gogf/gf/v2/util/gvalid"
2021-11-22 11:23:46 +08:00
)
const (
tagNameDc = `dc`
tagNameAd = `ad`
tagNameRoot = `root`
2021-11-22 11:23:46 +08:00
)
var (
// defaultValueTags is the struct tag names for default value storing.
defaultValueTags = []string{"d", "default"}
)
// NewFromObject creates and returns a root command object using given object.
func NewFromObject(object interface{}) (rootCmd Command, err error) {
2021-11-22 11:23:46 +08:00
originValueAndKind := utils.OriginValueAndKind(object)
if originValueAndKind.OriginKind != reflect.Struct {
err = gerror.Newf(
2021-11-22 11:23:46 +08:00
`input object should be type of struct, but got "%s"`,
originValueAndKind.InputValue.Type().String(),
)
return
2021-11-22 11:23:46 +08:00
}
// Root command creating.
rootCmd, err = newCommandFromObjectMeta(object)
if err != nil {
return
}
// Sub command creating.
var (
nameSet = gset.NewStrSet()
rootCommandName = gmeta.Get(object, tagNameRoot).String()
subCommands []Command
)
for i := 0; i < originValueAndKind.InputValue.NumMethod(); i++ {
var (
method = originValueAndKind.InputValue.Method(i)
methodCommand Command
)
methodCommand, err = newCommandFromMethod(object, method)
if err != nil {
return
}
if nameSet.Contains(methodCommand.Name) {
err = gerror.Newf(
`command name should be unique, found duplicated command name in method "%s"`,
method.Type().String(),
)
return
}
if rootCommandName == methodCommand.Name {
if rootCmd.Func == nil {
rootCmd.Func = methodCommand.Func
}
if rootCmd.FuncWithValue == nil {
rootCmd.FuncWithValue = methodCommand.FuncWithValue
}
if len(rootCmd.Options) == 0 {
rootCmd.Options = methodCommand.Options
}
} else {
subCommands = append(subCommands, methodCommand)
}
}
if len(subCommands) > 0 {
err = rootCmd.AddCommand(subCommands...)
}
2021-11-23 14:08:37 +08:00
return
2021-11-22 11:23:46 +08:00
}
func newCommandFromObjectMeta(object interface{}) (command Command, err error) {
var (
metaData = gmeta.Data(object)
)
if len(metaData) == 0 {
err = gerror.Newf(
`no meta data found in struct "%s"`,
reflect.TypeOf(object).String(),
)
return
}
if err = gconv.Scan(metaData, &command); err != nil {
return
}
// Name filed is necessary.
if command.Name == "" {
err = gerror.Newf(
`command name cannot be empty, "name" tag not found in meta of struct "%s"`,
reflect.TypeOf(object).String(),
)
return
}
if command.Description == "" {
command.Description = metaData[tagNameDc]
}
if command.Additional == "" {
command.Additional = metaData[tagNameAd]
}
return
}
func newCommandFromMethod(object interface{}, method reflect.Value) (command Command, err error) {
2021-11-22 11:23:46 +08:00
var (
reflectType = method.Type()
)
2021-11-23 14:08:37 +08:00
// Necessary validation for input/output parameters and naming.
2021-11-22 11:23:46 +08:00
if reflectType.NumIn() != 2 || reflectType.NumOut() != 2 {
if reflectType.PkgPath() != "" {
err = gerror.NewCodef(
gcode.CodeInvalidParameter,
`invalid command: %s.%s.%s defined as "%s", but "func(context.Context, Input)(Output, error)" is required`,
reflectType.PkgPath(), reflect.TypeOf(object).Name(), reflectType.Name(), reflectType.String(),
2021-11-22 11:23:46 +08:00
)
} else {
err = gerror.NewCodef(
gcode.CodeInvalidParameter,
`invalid command: defined as "%s", but "func(context.Context, Input)(Output, error)" is required`,
2021-11-22 11:23:46 +08:00
reflectType.String(),
)
}
return
2021-11-22 11:23:46 +08:00
}
if reflectType.In(0).String() != "context.Context" {
err = gerror.NewCodef(
gcode.CodeInvalidParameter,
`invalid command: defined as "%s", but the first input parameter should be type of "context.Context"`,
2021-11-22 11:23:46 +08:00
reflectType.String(),
)
return
2021-11-22 11:23:46 +08:00
}
if reflectType.Out(1).String() != "error" {
err = gerror.NewCodef(
gcode.CodeInvalidParameter,
`invalid command: defined as "%s", but the last output parameter should be type of "error"`,
2021-11-22 11:23:46 +08:00
reflectType.String(),
)
return
2021-11-22 11:23:46 +08:00
}
// The input struct should be named as `xxxInput`.
if !gstr.HasSuffix(reflectType.In(1).String(), `Input`) {
err = gerror.NewCodef(
gcode.CodeInvalidParameter,
`invalid struct naming for input: defined as "%s", but it should be named with "Input" suffix like "xxxInput"`,
reflectType.In(1).String(),
)
return
2021-11-22 11:23:46 +08:00
}
// The output struct should be named as `xxxOutput`.
if !gstr.HasSuffix(reflectType.Out(0).String(), `Output`) {
err = gerror.NewCodef(
gcode.CodeInvalidParameter,
`invalid struct naming for output: defined as "%s", but it should be named with "Output" suffix like "xxxOutput"`,
reflectType.Out(0).String(),
)
return
2021-11-23 14:08:37 +08:00
}
var (
inputObject reflect.Value
2021-11-23 14:08:37 +08:00
)
if method.Type().In(1).Kind() == reflect.Ptr {
inputObject = reflect.New(method.Type().In(1).Elem()).Elem()
} else {
inputObject = reflect.New(method.Type().In(1)).Elem()
}
// Command creating.
if command, err = newCommandFromObjectMeta(inputObject.Interface()); err != nil {
return
}
// Options creating.
if command.Options, err = newOptionsFromInput(inputObject.Interface()); err != nil {
return
2021-11-22 11:23:46 +08:00
}
// Create function that has value return.
command.FuncWithValue = func(ctx context.Context, parser *Parser) (out interface{}, err error) {
2021-11-24 21:57:41 +08:00
ctx = context.WithValue(ctx, CtxKeyParser, command)
defer func() {
if exception := recover(); exception != nil {
if v, ok := exception.(error); ok && gerror.HasStack(v) {
err = v
} else {
err = gerror.New(`exception recovered:` + gconv.String(exception))
}
}
}()
var (
data = gconv.Map(parser.GetOptAll())
inputValues = []reflect.Value{reflect.ValueOf(ctx)}
)
if data == nil {
data = map[string]interface{}{}
}
2021-11-24 21:41:36 +08:00
// Handle orphan options.
for _, option := range command.Options {
if option.Orphan && parser.ContainsOpt(option.Name) {
data[option.Name] = "true"
}
}
// Default values from struct tag.
2021-11-24 21:57:41 +08:00
if err = mergeDefaultStructValue(data, inputObject.Interface()); err != nil {
return nil, err
}
// Construct input parameters.
if len(data) > 0 {
2021-11-24 21:41:36 +08:00
if inputObject.Kind() == reflect.Ptr {
err = gconv.Scan(data, inputObject.Interface())
} else {
err = gconv.Struct(data, inputObject.Addr().Interface())
}
if err != nil {
return
}
}
// Parameters validation.
if err = gvalid.New().Bail().Data(inputObject.Interface()).Assoc(data).Run(ctx); err != nil {
2021-11-30 16:43:42 +08:00
err = gerror.Wrap(gerror.Current(err), `validation failed for command options`)
return
}
inputValues = append(inputValues, inputObject)
// Call handler with dynamic created parameter values.
results := method.Call(inputValues)
out = results[0].Interface()
if !results[1].IsNil() {
if v, ok := results[1].Interface().(error); ok {
err = v
}
}
return
2021-11-23 14:08:37 +08:00
}
return
}
2021-11-23 14:08:37 +08:00
2021-11-24 22:27:09 +08:00
func newOptionsFromInput(object interface{}) (options []Option, err error) {
var (
fields []gstructs.Field
)
fields, err = gstructs.Fields(gstructs.FieldsInput{
Pointer: object,
RecursiveOption: gstructs.RecursiveOptionEmbeddedNoTag,
})
for _, field := range fields {
var (
option = Option{}
metaData = field.TagMap()
)
if err = gconv.Scan(metaData, &option); err != nil {
return nil, err
}
if option.Name == "" {
option.Name = field.Name()
}
if option.Name == helpOptionName {
return nil, gerror.Newf(
`option name "%s" is already token by built-in options`,
option.Name,
)
}
if option.Short == helpOptionNameShort {
return nil, gerror.Newf(
`short option name "%s" is already token by built-in options`,
option.Short,
)
}
options = append(options, option)
}
return
}
// mergeDefaultStructValue merges the request parameters with default values from struct tag definition.
func mergeDefaultStructValue(data map[string]interface{}, pointer interface{}) error {
tagFields, err := gstructs.TagFields(pointer, defaultValueTags)
if err != nil {
return err
}
if len(tagFields) > 0 {
var (
foundKey string
foundValue interface{}
)
for _, field := range tagFields {
foundKey, foundValue = gutil.MapPossibleItemByKey(data, field.Name())
if foundKey == "" {
data[field.Name()] = field.TagValue
} else {
if utils.IsEmpty(foundValue) {
data[foundKey] = field.TagValue
}
}
}
2021-11-23 14:08:37 +08:00
}
return nil
2021-11-23 14:08:37 +08:00
}