package input import ( "encoding" "fmt" "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) case *bool: return strconv.FormatBool(*v) case bool: return strconv.FormatBool(v) case *string: return *v case string: return v case *int: return strconv.FormatInt(int64(*v), 10) case int: return strconv.FormatInt(int64(v), 10) case *int8: return strconv.FormatInt(int64(*v), 10) case int8: return strconv.FormatInt(int64(v), 10) case *int16: return strconv.FormatInt(int64(*v), 10) case int16: return strconv.FormatInt(int64(v), 10) case *int32: return strconv.FormatInt(int64(*v), 10) case int32: return strconv.FormatInt(int64(v), 10) case *int64: return strconv.FormatInt(int64(*v), 10) case int64: return strconv.FormatInt(int64(v), 10) case *uint: return strconv.FormatUint(uint64(*v), 10) case uint: return strconv.FormatUint(uint64(v), 10) case *uint8: return strconv.FormatUint(uint64(*v), 10) case uint8: return strconv.FormatUint(uint64(v), 10) case *uint16: return strconv.FormatUint(uint64(*v), 10) case uint16: return strconv.FormatUint(uint64(v), 10) case *uint32: return strconv.FormatUint(uint64(*v), 10) case uint32: return strconv.FormatUint(uint64(v), 10) case *uint64: return strconv.FormatUint(uint64(*v), 10) case uint64: return strconv.FormatUint(uint64(v), 10) case *float32: return strconv.FormatFloat(float64(*v), 'f', -1, 32) // TODO: Format number in current user's locale case float32: return strconv.FormatFloat(float64(v), 'f', -1, 32) // TODO: Format number in current user's locale case *float64: return strconv.FormatFloat(float64(*v), 'f', -1, 64) case float64: return strconv.FormatFloat(float64(v), 'f', -1, 64) } return "" } 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)) case *bool: val, err := strconv.ParseBool(value) if err != nil { return err } *v = val case *string: *v = value case *int: val, err := strconv.ParseInt(value, 10, 0) if err != nil { return err } *v = int(val) case *int8: val, err := strconv.ParseInt(value, 10, 8) if err != nil { return err } *v = int8(val) case *int16: val, err := strconv.ParseInt(value, 10, 16) if err != nil { return err } *v = int16(val) case *int32: val, err := strconv.ParseInt(value, 10, 32) if err != nil { return err } *v = int32(val) case *int64: val, err := strconv.ParseInt(value, 10, 64) if err != nil { return err } *v = int64(val) case *uint: val, err := strconv.ParseUint(value, 10, 0) if err != nil { return err } *v = uint(val) case *uint8: val, err := strconv.ParseUint(value, 10, 8) if err != nil { return err } *v = uint8(val) case *uint16: val, err := strconv.ParseUint(value, 10, 16) if err != nil { return err } *v = uint16(val) case *uint32: val, err := strconv.ParseUint(value, 10, 32) if err != nil { return err } *v = uint32(val) case *uint64: val, err := strconv.ParseUint(value, 10, 64) if err != nil { return err } *v = uint64(val) case *float32: val, err := strconv.ParseFloat(value, 32) if err != nil { return err } *v = float32(val) case *float64: val, err := strconv.ParseFloat(value, 64) if err != nil { return err } *v = float64(val) default: return fmt.Errorf("bound type %T is not supported", f.Value) } return nil } 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" }