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 }