117 lines
2.9 KiB
Go
117 lines
2.9 KiB
Go
package models
|
|
|
|
import (
|
|
"database/sql/driver"
|
|
"encoding/json"
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
// FlexibleInt is an integer column that accepts what clients actually send.
|
|
//
|
|
// The counterpart to FlexibleString, and it exists for the same reason: a strict
|
|
// type here does not reject one field, it rejects the WHOLE request. Fiber's
|
|
// BodyParser fails on the first field it cannot convert, so `"status": "Active"`
|
|
// or `"primaryaddress": true` returns 400 "Invalid request body" and nothing is
|
|
// saved. On the address form that surfaced as a shopper adding a delivery
|
|
// address, seeing no error worth acting on, and then finding no address to pick
|
|
// at checkout — measured 2026-09-02: three plausible payloads, three 400s.
|
|
//
|
|
// Accepted, in the shapes a form or a phone actually produces:
|
|
//
|
|
// 1 a number
|
|
// "1" a number as a string, which is what a text input gives
|
|
// true a checkbox — "set as my default address"
|
|
// "" an untouched field, read as 0 rather than an error
|
|
// "Active" a word, mapped by meaning: active/yes/true/default are 1
|
|
//
|
|
// A word it does not recognise is an error, not a silent 0. Guessing there
|
|
// would store something the shopper did not choose.
|
|
type FlexibleInt int
|
|
|
|
func (fi *FlexibleInt) UnmarshalJSON(b []byte) error {
|
|
if len(b) == 0 || string(b) == "null" {
|
|
return nil
|
|
}
|
|
|
|
if b[0] == '"' {
|
|
var s string
|
|
if err := json.Unmarshal(b, &s); err != nil {
|
|
return err
|
|
}
|
|
s = strings.TrimSpace(s)
|
|
if s == "" {
|
|
*fi = 0
|
|
return nil
|
|
}
|
|
if n, err := strconv.Atoi(s); err == nil {
|
|
*fi = FlexibleInt(n)
|
|
return nil
|
|
}
|
|
switch strings.ToLower(s) {
|
|
case "active", "yes", "true", "default", "primary":
|
|
*fi = 1
|
|
case "inactive", "no", "false":
|
|
*fi = 0
|
|
default:
|
|
return fmt.Errorf("cannot read %q as a number", s)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
if b[0] == 't' || b[0] == 'f' {
|
|
var v bool
|
|
if err := json.Unmarshal(b, &v); err != nil {
|
|
return err
|
|
}
|
|
if v {
|
|
*fi = 1
|
|
} else {
|
|
*fi = 0
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// A number, possibly written with a decimal point by a client that has no
|
|
// integer type of its own.
|
|
var f float64
|
|
if err := json.Unmarshal(b, &f); err != nil {
|
|
return err
|
|
}
|
|
*fi = FlexibleInt(int(f))
|
|
return nil
|
|
}
|
|
|
|
func (fi FlexibleInt) MarshalJSON() ([]byte, error) {
|
|
return json.Marshal(int(fi))
|
|
}
|
|
|
|
// Scan reads the column back. bigint arrives as int64; the string cases cover a
|
|
// text column that holds a number, which this schema has in places.
|
|
func (fi *FlexibleInt) Scan(value interface{}) error {
|
|
switch v := value.(type) {
|
|
case nil:
|
|
*fi = 0
|
|
case int64:
|
|
*fi = FlexibleInt(v)
|
|
case int:
|
|
*fi = FlexibleInt(v)
|
|
case float64:
|
|
*fi = FlexibleInt(int(v))
|
|
case []byte:
|
|
n, _ := strconv.Atoi(strings.TrimSpace(string(v)))
|
|
*fi = FlexibleInt(n)
|
|
case string:
|
|
n, _ := strconv.Atoi(strings.TrimSpace(v))
|
|
*fi = FlexibleInt(n)
|
|
default:
|
|
return fmt.Errorf("cannot read %T as a number", value)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (fi FlexibleInt) Value() (driver.Value, error) {
|
|
return int64(fi), nil
|
|
}
|