feat: add permissive text and list values

This commit is contained in:
AJ ONeal 2026-09-16 16:13:24 -06:00
parent ca6e271f66
commit bd03bfb769
No known key found for this signature in database
5 changed files with 142 additions and 2 deletions

View File

@ -4,6 +4,7 @@
// can be read from a sheet, written to a file, and scanned from a database. // can be read from a sheet, written to a file, and scanned from a database.
// //
// It also provides generic Parse/Serialize functions for CSV, TSV, JSON, // It also provides generic Parse/Serialize functions for CSV, TSV, JSON,
// and JSONL, delimiter-specific string and integer slices, plus text // and JSONL, delimiter-specific string and integer slices, and permissive
// normalization helpers (NormalizeEmail, NormalizePhone). // text/list value types, plus normalization helpers (NormalizeEmail,
// NormalizePhone).
package csvx package csvx

26
database/csvx/listish.go Normal file
View File

@ -0,0 +1,26 @@
package csvx
import (
"strings"
"unicode"
)
// Listish is a list encoded as comma- or whitespace-delimited text.
type Listish []string
// UnmarshalCSV parses comma- or whitespace-delimited values.
func (s *Listish) UnmarshalCSV(data []byte) error {
if len(data) == 0 {
*s = Listish{}
return nil
}
parts := strings.FieldsFunc(string(data), func(r rune) bool {
return r == ',' || unicode.IsSpace(r)
})
if len(parts) == 0 {
*s = Listish{}
return nil
}
*s = Listish(parts)
return nil
}

View File

@ -0,0 +1,31 @@
package csvx
import (
"reflect"
"testing"
)
func TestListishUnmarshalCSV(t *testing.T) {
tests := []struct {
name string
in string
want Listish
}{
{name: "comma", in: "admin,hr,pm", want: Listish{"admin", "hr", "pm"}},
{name: "whitespace", in: "admin hr\tpm", want: Listish{"admin", "hr", "pm"}},
{name: "mixed", in: " admin, hr\npm ", want: Listish{"admin", "hr", "pm"}},
{name: "empty", in: " , \t", want: Listish{}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var got Listish
if err := got.UnmarshalCSV([]byte(tt.in)); err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(got, tt.want) {
t.Fatalf("got %#v, want %#v", got, tt.want)
}
})
}
}

60
database/csvx/textish.go Normal file
View File

@ -0,0 +1,60 @@
package csvx
import (
"database/sql/driver"
"encoding/json"
"fmt"
"strings"
)
// Textish is text with common explicit-empty words normalized to an empty value.
type Textish string
func (t Textish) Empty() bool { return string(t) == "" }
func (t Textish) String() string { return string(t) }
func normalizeTextish(value string) string {
switch strings.ToLower(strings.TrimSpace(value)) {
case "", "none", "disable", "disabled":
return ""
default:
return strings.TrimSpace(value)
}
}
func (t Textish) Value() (driver.Value, error) { return string(t), nil }
func (t *Textish) Scan(value any) error {
if value == nil {
*t = ""
return nil
}
switch v := value.(type) {
case string:
*t = Textish(normalizeTextish(v))
case []byte:
*t = Textish(normalizeTextish(string(v)))
default:
return fmt.Errorf("csvx.Textish: cannot scan %T", value)
}
return nil
}
func (t Textish) MarshalCSV() ([]byte, error) { return []byte(t), nil }
func (t *Textish) UnmarshalCSV(data []byte) error {
*t = Textish(normalizeTextish(string(data)))
return nil
}
func (t Textish) MarshalJSON() ([]byte, error) { return json.Marshal(string(t)) }
func (t *Textish) UnmarshalJSON(data []byte) error {
var value string
if err := json.Unmarshal(data, &value); err != nil {
return err
}
*t = Textish(normalizeTextish(value))
return nil
}

View File

@ -0,0 +1,22 @@
package csvx
import "testing"
func TestTextishNormalizesExplicitEmptyValues(t *testing.T) {
for _, value := range []string{"", "none", "disable", "disabled", " NONE "} {
var got Textish
if err := got.UnmarshalCSV([]byte(value)); err != nil {
t.Fatal(err)
}
if !got.Empty() {
t.Fatalf("Textish(%q) = %q, want empty", value, got)
}
}
var got Textish
if err := got.UnmarshalCSV([]byte("https://hooks.example.test/enrich")); err != nil {
t.Fatal(err)
}
if got.String() != "https://hooks.example.test/enrich" {
t.Fatalf("Textish URL = %q", got)
}
}