package slag import ( "bytes" "errors" "fmt" "reflect" "strconv" "github.com/danielgtaylor/casing" "github.com/olekukonko/tablewriter" "github.com/olekukonko/tablewriter/renderer" "github.com/olekukonko/tablewriter/tw" ) var ( ErrNoImplemented = fmt.Errorf("to implement") ErrNoPointer = fmt.Errorf("structure provided is not a pointer") ErrNoStructUnderneath = fmt.Errorf("input provided is not a pointer to a struct") ErrNoFlagsProvided = fmt.Errorf("expected at least one flag name but got none") ErrNoFlagsValuesProvided = fmt.Errorf("expected value for this flag") ErrInvalidFlagName = fmt.Errorf("invalid flag name") ErrInvalidPropName = fmt.Errorf("invalid prop name") ErrTypeNotImplemented = fmt.Errorf("this type has not been implemented for parsing") separator = " " helpMessageTag = "slag_help" ) func use(v ...any) { } func SetHelpMessageTag(tag string) { helpMessageTag = tag } func GetHelpMessageTag() string { return helpMessageTag } func SetArgsSeparator(s string) { separator = s } func errFlagValueIsMissing(flagName string) error { return fmt.Errorf("%v value is missing", flagName) } func errTypeNotImplemented(t reflect.Kind) error { return fmt.Errorf("(%v) type has not been implemented for parsing", t) } func isFlag(str string) bool { return str[0] == '-' } func isProp(str string) bool { return str[0] == '.' } // @Todo this two functions do the same thing, i should remove them and just handle the error // in place func parseFlagName(str string) (string, error) { if len(str) == 1 { return "", ErrInvalidFlagName } return str[1:], nil } func parsePropName(str string) (string, error) { if len(str) == 1 { return "", ErrInvalidPropName } return str[1:], nil } // returns array []string // returns obj // returns array []obj func consumeFlagSimpleValues(args []string, pivot int) ([]string, int) { var i int var values []string for i = pivot; i < len(args); i += 1 { a := args[i] if isFlag(a) { break } values = append(values, a) } return values, i } const _END_OBJ = "." // @Todo: make this less error prone? it works well i guess func consumeFlagObjectValues(args []string, pivot int) (map[string]any, int) { var i int // @Todo: handle the i+=2 better // var step int // i+= step, the default value for step would be 2 when is a raw value, but N when an object/array was consumed values := make(map[string]any) for i = pivot; i < len(args); { p := args[i] if isFlag(p) { break } // @Note we use a SLAG_END_OBJ to denotate an object has ended, can cause a bunch of bugs sadly if p == _END_OBJ { i += 1 break } p, err := parsePropName(args[i]) if err != nil { // @Todo: handle this error // kinda spaguetti i += 1 continue } v := args[i+1] if isProp(v) { values[p], i = consumeFlagObjectValues(args, i+1) } else { values[p] = v i += 2 } } if len(values) == 0 { return nil, i } return values, i } type ArgKind = byte const ( FlagKind ArgKind = '-' PropertyKind ArgKind = '.' ) func isNextToken(args []string, pivot int, kind ArgKind) bool { if pivot >= len(args) { return false } return args[pivot][0] == kind } type ArgValue any func parseArgs(args []string) (map[string]any, error) { flagsAndValues := make(map[string]any) for i := 0; i < len(args); { var values any var name = args[i] pivot := i + 1 isFlag := isFlag(name) if !isFlag { i = pivot continue } flagName, err := parseFlagName(name) if err != nil { return flagsAndValues, err } // @Todo: handle object arrays // "-person .name mar .age 23 -hobbies reading" // "-people [.name marg .age 23, .name]" if isNextToken(args, pivot, PropertyKind) { var tmp map[string]any tmp, i = consumeFlagObjectValues(args, pivot) if tmp != nil { values = tmp } } else { var tmp []string tmp, i = consumeFlagSimpleValues(args, pivot) if tmp != nil { values = tmp } } flagsAndValues[flagName] = values } if len(flagsAndValues) == 0 { return flagsAndValues, ErrNoFlagsProvided } return flagsAndValues, nil } func unmarshalString(field reflect.Value, rawValues ...string) (err error) { kind := field.Kind() t := field.Type() if kind == reflect.Bool && len(rawValues) == 0 { field.SetBool(true) return nil } if len(rawValues) == 0 { return ErrNoFlagsValuesProvided } first := rawValues[0] switch kind { case reflect.Bool: b, err := strconv.ParseBool(first) if err != nil { return err } field.SetBool(b) case reflect.String: field.SetString(first) case reflect.Int: var number int64 number, err = strconv.ParseInt(first, 10, 64) if err != nil { return err } field.SetInt(number) case reflect.Float32: var decimal float64 decimal, err = strconv.ParseFloat(first, 32) if err != nil { return err } field.SetFloat(decimal) case reflect.Float64: var decimal float64 decimal, err = strconv.ParseFloat(first, 64) if err != nil { return err } field.SetFloat(decimal) case reflect.Slice: cap := field.Cap() if cap > 0 { field.SetLen(cap) } var i int var rv any // We set the len to be the capacity // and when we stop iterating over the array // we just use i to set the real length // the reason on why i guess they do this (json/v2/arshal_default.go) would be to // not be calling SetLen in each of the iterations // as i was doing before, that's clever, it just makes me wonder how does the // appending work under the hood in golang cause when growing the array // we do not allocate just the memory we asked for, even if we are growing the capacity // i know this because it literally is what happens when calling grow it allocates and padds // so it gets powers of 2 aligned example 27 real elements makes the allocation be for -> 32 // also this doesn't cause any problems with the assigning because we are using accesing the fields and // assigning them values with the index, and we are iterating with the rawValues, which have the real quantity // so even if the array has an incorrect length for a little bit it doesn't introduce a bug cause again, this array/slice // will just be used when we finish up the filling of it for i, rv = range rawValues { if i == cap { field.Grow(1) cap = field.Cap() field.SetLen(cap) } idxField := field.Index(i) err := unmarshal(idxField, rv) if err != nil { // We need to set the len to be i because the current element // could not be appended to the array so that's why we need to // remove it from the length count (reason on why there is no + 1 here) field.SetLen(i) return err } } if i == 0 { emptySlice := reflect.MakeSlice(t, 0, 0) field.Set(emptySlice) } else { // Golang range loops leave the index variable always at the last element of the ranging array field.SetLen(i + 1) } default: err = errTypeNotImplemented(kind) } return } // @Todo: add support for multiple objects in rawValues func unmarshalObject(field reflect.Value, rawValues map[string]any) (err error) { // kind := field.Kind() // t := field.Type() // @Todo: check if this ever is true if len(rawValues) == 0 { return ErrNoFlagsValuesProvided } for name, value := range rawValues { fieldName := casing.Camel(name) f := field.FieldByName(fieldName) err = unmarshal(f, value) } return err } func unmarshal(field reflect.Value, value any) (err error) { switch v := value.(type) { case string: err = unmarshalString(field, v) case []string: err = unmarshalString(field, v...) case map[string]any: err = unmarshalObject(field, v) } return } func Unmarshal(structure any, args map[string]any) error { tmp := reflect.ValueOf(structure) kind := tmp.Kind() if kind != reflect.Pointer { return ErrNoPointer } raw := tmp.Elem() for name, value := range args { // @Update: casing module and check if they corrected the warning // this uses a deprecated thing // strings.Title is deprecated: The rule Title uses for word boundaries does not handle Unicode // punctuation properly. Use golang.org/x/text/cases instead. [default] fieldName := casing.Camel(name) field := raw.FieldByName(fieldName) if !field.IsValid() { continue } err := unmarshal(field, value) if err != nil { return err } } return nil } type HelpMessage struct { FlagName string Message string DefaultValue any Inner []HelpMessage } func unwrap(v reflect.Value) reflect.Value { if v.Kind() == reflect.Pointer { v = v.Elem() } return v } // We accept the type of the object we are typing because if one of the fields // is a pointer we can just use the type for accessing all the fields and struct info // which before that was being done with the value (that lead to problems because) // if the values are empty there is no such info func getStructFieldHelpMessage(v reflect.Type, d reflect.Value) []HelpMessage { var msgs []HelpMessage if v.Kind() == reflect.Pointer { v = v.Elem() } if d.Kind() == reflect.Pointer { d = d.Elem() } for field := range v.Fields() { kind := field.Type.Kind() if kind == reflect.Pointer { kind = field.Type.Elem().Kind() } flagName := casing.Kebab(field.Name) message := field.Tag.Get("help") var helpMessage HelpMessage switch kind { case reflect.Struct: innerStructDefaults := d.FieldByName(field.Name) innerHelpMessages := getStructFieldHelpMessage(field.Type, innerStructDefaults) helpMessage = HelpMessage{ FlagName: flagName, Inner: innerHelpMessages, } default: dflt := unwrap(d.FieldByName(field.Name)).Interface() helpMessage = HelpMessage{ Message: message, FlagName: flagName, DefaultValue: dflt, } } msgs = append(msgs, helpMessage) } return msgs } func GetHelpMessage(structure any, defaults any, helpTagName string) (string, error) { rv := reflect.ValueOf(structure) dv := reflect.ValueOf(defaults) if dv.Kind() != reflect.Struct || rv.Kind() != reflect.Struct { return "", ErrNoStructUnderneath } var data [][]any for f := range rv.Fields() { // @Todo: find another way to check if the user provided them cause // i kind wanted the user to be able to use whatever casing they want for the flags // but is not a priority tbh helpMsg, ok := f.Tag.Lookup(helpTagName) d := dv.FieldByName(f.Name) fieldName := casing.Kebab(f.Name) if ok { flag := fmt.Sprintf("-%v", fieldName) data = append(data, []any{flag, helpMsg, d.Interface()}) } } symbols := tw.NewSymbolCustom("Help"). WithRow(" "). WithColumn(" ").WithCenter(" ") b := []byte{} buf := bytes.NewBuffer(b) table := tablewriter.NewTable(buf, tablewriter.WithRenderer(renderer.NewBlueprint(tw.Rendition{Symbols: symbols}))) table.Header("Flag", "Usage", "Default value") table.Bulk(data) table.Render() completeHelpMsg := buf.String() return completeHelpMsg, nil } type Config struct { ErrorOnUnkownFlags bool } var DefaultConfig = Config{ErrorOnUnkownFlags: true} // @Todo: refactor the unmarshal part using the previous func func Parse(structure, defaults any, rawArgs []string, config Config) error { args, err := parseArgs(rawArgs) if err != nil && !errors.Is(err, ErrNoFlagsProvided) { return err } tmp := reflect.ValueOf(structure) kind := tmp.Kind() if kind != reflect.Pointer { return ErrNoPointer } out := tmp.Elem() if out.Kind() != reflect.Struct { return ErrNoStructUnderneath } rDefaults := reflect.ValueOf(defaults) // rDefaults := reflect.ValueOf(defaults) for name, value := range args { // @Update: casing module and check if they corrected the warning // this uses a deprecated thing // strings.Title is deprecated: The rule Title uses for word boundaries does not handle Unicode // punctuation properly. Use golang.org/x/text/cases instead. [default] fieldName := casing.Camel(name) field := out.FieldByName(fieldName) if !field.IsValid() { if config.ErrorOnUnkownFlags { // @Todo: // fuzzy find other flags with similar names to tell the user // did you mean "__name_of_similar_flag__"? return fmt.Errorf("%v is not a valid flag", name) } else { continue } } // @Todo: remove this type assertion err := unmarshal(field, value) if err != nil { return err } } for f, v := range out.Fields() { // @Todo: find another way to check if the user provided them cause // i kind wanted the user to be able to use whatever casing they want for the flags // but is not a priority tbh fieldName := casing.Kebab(f.Name) _, isProvided := args[fieldName] if isProvided { continue } dv := rDefaults.FieldByName(f.Name) v.Set(dv) } return nil }