github.com/wangyougui/gf/v2@v2.6.5/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/wangyougui/gf. 6 // 7 8 package gcmd 9 10 import ( 11 "context" 12 "fmt" 13 "reflect" 14 15 "github.com/wangyougui/gf/v2/container/gset" 16 "github.com/wangyougui/gf/v2/encoding/gjson" 17 "github.com/wangyougui/gf/v2/errors/gcode" 18 "github.com/wangyougui/gf/v2/errors/gerror" 19 "github.com/wangyougui/gf/v2/internal/intlog" 20 "github.com/wangyougui/gf/v2/internal/reflection" 21 "github.com/wangyougui/gf/v2/internal/utils" 22 "github.com/wangyougui/gf/v2/os/gstructs" 23 "github.com/wangyougui/gf/v2/text/gstr" 24 "github.com/wangyougui/gf/v2/util/gconv" 25 "github.com/wangyougui/gf/v2/util/gmeta" 26 "github.com/wangyougui/gf/v2/util/gtag" 27 "github.com/wangyougui/gf/v2/util/gutil" 28 "github.com/wangyougui/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 // ============================================================================================= 255 // Create function that has value return. 256 // ============================================================================================= 257 command.FuncWithValue = func(ctx context.Context, parser *Parser) (out interface{}, err error) { 258 ctx = context.WithValue(ctx, CtxKeyParser, parser) 259 var ( 260 data = gconv.Map(parser.GetOptAll()) 261 argIndex = 0 262 arguments = gconv.Strings(ctx.Value(CtxKeyArguments)) 263 inputValues = []reflect.Value{reflect.ValueOf(ctx)} 264 ) 265 if data == nil { 266 data = map[string]interface{}{} 267 } 268 // Handle orphan options. 269 for _, arg := range command.Arguments { 270 if arg.IsArg { 271 // Read argument from command line index. 272 if argIndex < len(arguments) { 273 data[arg.Name] = arguments[argIndex] 274 argIndex++ 275 } 276 } else { 277 // Read argument from command line option name. 278 if arg.Orphan { 279 if orphanValue := parser.GetOpt(arg.Name); orphanValue != nil { 280 if orphanValue.String() == "" { 281 // Eg: gf -f 282 data[arg.Name] = "true" 283 } else { 284 // Adapter with common user habits. 285 // Eg: 286 // `gf -f=0`: which parameter `f` is parsed as false 287 // `gf -f=1`: which parameter `f` is parsed as true 288 data[arg.Name] = orphanValue.Bool() 289 } 290 } 291 } 292 } 293 } 294 // Default values from struct tag. 295 if err = mergeDefaultStructValue(data, inputObject.Interface()); err != nil { 296 return nil, err 297 } 298 // Construct input parameters. 299 if len(data) > 0 { 300 intlog.PrintFunc(ctx, func() string { 301 return fmt.Sprintf(`input command data map: %s`, gjson.MustEncode(data)) 302 }) 303 if inputObject.Kind() == reflect.Ptr { 304 err = gconv.Scan(data, inputObject.Interface()) 305 } else { 306 err = gconv.Struct(data, inputObject.Addr().Interface()) 307 } 308 intlog.PrintFunc(ctx, func() string { 309 return fmt.Sprintf(`input object assigned data: %s`, gjson.MustEncode(inputObject.Interface())) 310 }) 311 if err != nil { 312 return 313 } 314 } 315 316 // Parameters validation. 317 if err = gvalid.New().Bail().Data(inputObject.Interface()).Assoc(data).Run(ctx); err != nil { 318 err = gerror.Wrapf(gerror.Current(err), `arguments validation failed for command "%s"`, command.Name) 319 return 320 } 321 inputValues = append(inputValues, inputObject) 322 323 // Call handler with dynamic created parameter values. 324 results := methodValue.Call(inputValues) 325 out = results[0].Interface() 326 if !results[1].IsNil() { 327 if v, ok := results[1].Interface().(error); ok { 328 err = v 329 } 330 } 331 return 332 } 333 return 334 } 335 336 func newArgumentsFromInput(object interface{}) (args []Argument, err error) { 337 var ( 338 fields []gstructs.Field 339 nameSet = gset.NewStrSet() 340 shortSet = gset.NewStrSet() 341 ) 342 fields, err = gstructs.Fields(gstructs.FieldsInput{ 343 Pointer: object, 344 RecursiveOption: gstructs.RecursiveOptionEmbeddedNoTag, 345 }) 346 for _, field := range fields { 347 var ( 348 arg = Argument{} 349 metaData = field.TagMap() 350 ) 351 if err = gconv.Scan(metaData, &arg); err != nil { 352 return nil, err 353 } 354 if arg.Name == "" { 355 arg.Name = field.Name() 356 } 357 if arg.Name == helpOptionName { 358 return nil, gerror.Newf( 359 `argument name "%s" defined in "%s.%s" is already token by built-in arguments`, 360 arg.Name, reflect.TypeOf(object).String(), field.Name(), 361 ) 362 } 363 if arg.Short == helpOptionNameShort { 364 return nil, gerror.Newf( 365 `short argument name "%s" defined in "%s.%s" is already token by built-in arguments`, 366 arg.Short, reflect.TypeOf(object).String(), field.Name(), 367 ) 368 } 369 if arg.Brief == "" { 370 arg.Brief = field.TagDescription() 371 } 372 if v, ok := metaData[gtag.Arg]; ok { 373 arg.IsArg = gconv.Bool(v) 374 } 375 if nameSet.Contains(arg.Name) { 376 return nil, gerror.Newf( 377 `argument name "%s" defined in "%s.%s" is already token by other argument`, 378 arg.Name, reflect.TypeOf(object).String(), field.Name(), 379 ) 380 } 381 nameSet.Add(arg.Name) 382 383 if arg.Short != "" { 384 if shortSet.Contains(arg.Short) { 385 return nil, gerror.Newf( 386 `short argument name "%s" defined in "%s.%s" is already token by other argument`, 387 arg.Short, reflect.TypeOf(object).String(), field.Name(), 388 ) 389 } 390 shortSet.Add(arg.Short) 391 } 392 393 args = append(args, arg) 394 } 395 396 return 397 } 398 399 // mergeDefaultStructValue merges the request parameters with default values from struct tag definition. 400 func mergeDefaultStructValue(data map[string]interface{}, pointer interface{}) error { 401 tagFields, err := gstructs.TagFields(pointer, defaultValueTags) 402 if err != nil { 403 return err 404 } 405 if len(tagFields) > 0 { 406 var ( 407 foundKey string 408 foundValue interface{} 409 ) 410 for _, field := range tagFields { 411 var ( 412 nameValue = field.Tag(tagNameName) 413 shortValue = field.Tag(tagNameShort) 414 ) 415 // If it already has value, it then ignores the default value. 416 if value, ok := data[nameValue]; ok { 417 data[field.Name()] = value 418 continue 419 } 420 if value, ok := data[shortValue]; ok { 421 data[field.Name()] = value 422 continue 423 } 424 foundKey, foundValue = gutil.MapPossibleItemByKey(data, field.Name()) 425 if foundKey == "" { 426 data[field.Name()] = field.TagValue 427 } else { 428 if utils.IsEmpty(foundValue) { 429 data[foundKey] = field.TagValue 430 } 431 } 432 } 433 } 434 return nil 435 }