package input import ( "encoding" "fmt" "reflect" "strconv" "time" ) // TODO: Locale dependent formatting // ValueGetter can be implemented by types that need to be bound to UI components. type ValueGetter interface { StringValue() string // StringValue returns the value as a string formatted in the users/current locale. } // ValueSetter can be implemented by types that need to be bound to UI components. type ValueSetter interface { SetStringValue(string) error // SetStringValue parses the string value in the users/current locale. } // ValueBinder can be implemented by types that need to be bound to UI components. type ValueBinder interface { ValueGetter ValueSetter } // HTMLInputTyper can be implemented by types that need to be bound to UI components. type HTMLInputTyper interface { HTMLInputType() string // HTMLInputType returns the to be used type parameter for the HTML input element. } // ValueBindAny implements the ValueBinder interface for all basic types and the encoding.TextMarshaler, encoding.TextUnmarshaler interfaces. type ValueBindAny struct{ Value any } func (f ValueBindAny) StringValue() string { switch v := f.Value.(type) { case ValueGetter: // Support nested value getters. return v.StringValue() case *time.Time: // Put time.Time before TextMarshaler, so we can define a custom format that is compatible with input fields. return v.Local().Format("2006-01-02T15:04") case time.Time: return v.Local().Format("2006-01-02T15:04") case encoding.TextMarshaler: data, _ := v.MarshalText() // Ignore any errors, as we can't handle them here. return string(data) } // Determine the reflection type so we can support any type that wraps basic types. str, err := valueBindAnyMarshalBasicValue(reflect.ValueOf(f.Value)) if err != nil { return err.Error() } return str } func (f ValueBindAny) SetStringValue(value string) error { switch v := f.Value.(type) { case ValueSetter: // Support nested value setters. return v.SetStringValue(value) case *time.Time: // Put time.Time before TextMarshaler, so we can define a custom format that is compatible with input fields. val, err := time.ParseInLocation("2006-01-02T15:04", value, time.Local) if err != nil { return err } *v = val case encoding.TextUnmarshaler: return v.UnmarshalText([]byte(value)) } return valueBindAnyUnmarshalBasicValue(value, reflect.ValueOf(f.Value)) } func (f ValueBindAny) HTMLInputType() string { switch f.Value.(type) { case *time.Time: return "datetime-local" case *bool: return "text" // We will render the boolean as text, as this is an input field, not a checkbox. case *string: return "text" case *int, *int8, *int16, *int32, *int64, *uint, *uint8, *uint16, *uint32, *uint64: return "number" case *float32, *float64: return "number" } return "text" } func valueBindAnyMarshalBasicValue(v reflect.Value) (string, error) { if !v.IsValid() { return "", fmt.Errorf("bound value is invalid") } switch v.Kind() { case reflect.Pointer, reflect.Interface: if v.IsNil() { return "", fmt.Errorf("bound value pointer is nil") } return valueBindAnyMarshalBasicValue(v.Elem()) case reflect.Bool: return strconv.FormatBool(v.Bool()), nil case reflect.String: return v.String(), nil case reflect.Int: return strconv.FormatInt(v.Int(), 10), nil case reflect.Int8: return strconv.FormatInt(v.Int(), 10), nil case reflect.Int16: return strconv.FormatInt(v.Int(), 10), nil case reflect.Int32: return strconv.FormatInt(v.Int(), 10), nil case reflect.Int64: return strconv.FormatInt(v.Int(), 10), nil case reflect.Uint: return strconv.FormatUint(v.Uint(), 10), nil case reflect.Uint8: return strconv.FormatUint(v.Uint(), 10), nil case reflect.Uint16: return strconv.FormatUint(v.Uint(), 10), nil case reflect.Uint32: return strconv.FormatUint(v.Uint(), 10), nil case reflect.Uint64: return strconv.FormatUint(v.Uint(), 10), nil case reflect.Float32: return strconv.FormatFloat(v.Float(), 'f', -1, 32), nil case reflect.Float64: return strconv.FormatFloat(v.Float(), 'f', -1, 64), nil } return "", fmt.Errorf("bound type %T is not supported", v.Interface()) } func valueBindAnyUnmarshalBasicValue(str string, v reflect.Value) error { if !v.IsValid() { return fmt.Errorf("bound value is invalid") } // Get value that is stored in pointer or interface. kind := v.Kind() if kind == reflect.Pointer || kind == reflect.Interface { if v.IsNil() { return fmt.Errorf("bound value pointer is nil") } return valueBindAnyUnmarshalBasicValue(str, v.Elem()) } // Check if we are allowed to modify the value. if !v.CanSet() { return fmt.Errorf("bound value can't be modified") } switch kind { case reflect.Bool: val, err := strconv.ParseBool(str) if err != nil { return err } v.SetBool(val) case reflect.String: v.SetString(str) case reflect.Int: val, err := strconv.ParseInt(str, 10, 0) if err != nil { return err } v.SetInt(val) case reflect.Int8: val, err := strconv.ParseInt(str, 10, 8) if err != nil { return err } v.SetInt(val) case reflect.Int16: val, err := strconv.ParseInt(str, 10, 16) if err != nil { return err } v.SetInt(val) case reflect.Int32: val, err := strconv.ParseInt(str, 10, 32) if err != nil { return err } v.SetInt(val) case reflect.Int64: val, err := strconv.ParseInt(str, 10, 64) if err != nil { return err } v.SetInt(val) case reflect.Uint: val, err := strconv.ParseUint(str, 10, 0) if err != nil { return err } v.SetUint(val) case reflect.Uint8: val, err := strconv.ParseUint(str, 10, 8) if err != nil { return err } v.SetUint(val) case reflect.Uint16: val, err := strconv.ParseUint(str, 10, 16) if err != nil { return err } v.SetUint(val) case reflect.Uint32: val, err := strconv.ParseUint(str, 10, 32) if err != nil { return err } v.SetUint(val) case reflect.Uint64: val, err := strconv.ParseUint(str, 10, 64) if err != nil { return err } v.SetUint(val) case reflect.Float32: val, err := strconv.ParseFloat(str, 32) if err != nil { return err } v.SetFloat(val) case reflect.Float64: val, err := strconv.ParseFloat(str, 64) if err != nil { return err } v.SetFloat(val) default: return fmt.Errorf("bound type %T is not supported", v.Interface()) } return nil }