Files
aego/data/csv.go
T

195 lines
4.9 KiB
Go

package data
import (
"bytes"
"encoding/csv"
"slices"
"strconv"
"strings"
"aego/core/errs"
"aego/core/ids"
"aego/core/log"
"aego/format/aetext"
"aego/schema"
)
func csvField(fd *schema.FieldDescriptor) bool {
if !fd.Flags.Has(schema.FlagSerialize) {
return false
}
switch fd.Kind {
case schema.KindBool, schema.KindInt32, schema.KindInt64, schema.KindFloat32, schema.KindFloat64,
schema.KindString, schema.KindEnum, schema.KindAssetRef:
return true
}
return false
}
func tableType(reg *schema.Registry, name string) (*schema.TypeDescriptor, error) {
td, ok := reg.ByName(name)
if !ok || td.Kind != schema.TypeAsset {
return nil, errs.New(errs.SchemaUnknownType, "unknown data type "+name)
}
return td, nil
}
func cellText(v aetext.Value) string {
switch v.Kind {
case aetext.VInt:
return strconv.FormatInt(v.Int, 10)
case aetext.VFloat:
bits := 64
if v.Single {
bits = 32
}
return strconv.FormatFloat(v.Float, 'g', -1, bits)
case aetext.VBool:
return strconv.FormatBool(v.Bool)
case aetext.VString, aetext.VAsset:
return v.Str
case aetext.VIdent:
return v.Ident
}
return ""
}
func ExportCSV(reg *schema.Registry, t *TableDoc) ([]byte, error) {
td, err := tableType(reg, t.Type)
if err != nil {
return nil, err
}
header := []string{"id"}
for i := range td.Fields {
if fd := &td.Fields[i]; csvField(fd) {
header = append(header, fd.Name)
}
}
var b bytes.Buffer
w := csv.NewWriter(&b)
if err := w.Write(header); err != nil {
return nil, err
}
rec := make([]string, len(header))
for _, r := range t.Rows {
clear(rec)
rec[0] = r.ID
for _, f := range r.Fields {
if j := slices.Index(header, f.Name); j > 0 {
rec[j] = cellText(f.Value)
}
}
if err := w.Write(rec); err != nil {
return nil, err
}
}
w.Flush()
return b.Bytes(), w.Error()
}
func parseCell(fd *schema.FieldDescriptor, s string) (aetext.Value, error) {
switch fd.Kind {
case schema.KindBool:
v, err := strconv.ParseBool(s)
return aetext.Bool(v), err
case schema.KindInt32, schema.KindInt64:
v, err := strconv.ParseInt(s, 10, 64)
return aetext.Int(v), err
case schema.KindFloat32:
v, err := strconv.ParseFloat(s, 32)
return aetext.Float32(float32(v)), err
case schema.KindFloat64:
v, err := strconv.ParseFloat(s, 64)
return aetext.Float64(v), err
case schema.KindString:
return aetext.String(s), nil
case schema.KindEnum:
if _, ok := fd.EnumValueOf(s); ok {
return aetext.Ident(s), nil
}
v, err := strconv.ParseInt(s, 10, 64)
return aetext.Int(v), err
case schema.KindAssetRef:
id, err := ids.ParseAssetID(s)
return aetext.AssetRef(id.String()), err
}
return aetext.Value{}, errs.New(errs.SchemaBadField, "field "+fd.Name+" cannot be a CSV column")
}
func order(td *schema.TypeDescriptor, fs []aetext.Field) {
pos := func(name string) int {
for i := range td.Fields {
if td.Fields[i].Name == name {
return i
}
}
return len(td.Fields)
}
slices.SortStableFunc(fs, func(a, b aetext.Field) int { return pos(a.Name) - pos(b.Name) })
}
func ImportCSV(reg *schema.Registry, into *TableDoc, src []byte) (*TableDoc, error) {
td, err := tableType(reg, into.Type)
if err != nil {
return nil, err
}
records, err := csv.NewReader(bytes.NewReader(src)).ReadAll()
if err != nil {
return nil, errs.Wrap(err, errs.AssetImportFailed, "malformed CSV")
}
if len(records) == 0 || strings.TrimSpace(records[0][0]) != "id" {
return nil, errs.New(errs.AssetImportFailed, "the first CSV column must be id")
}
header := records[0]
cols := make([]*schema.FieldDescriptor, len(header))
for j := 1; j < len(header); j++ {
fd, ok := td.Field(strings.TrimSpace(header[j]))
if !ok || !csvField(fd) {
return nil, errs.New(errs.AssetImportFailed, "column "+header[j]+" is not a CSV field of "+td.FullName)
}
cols[j] = fd
}
inCols := func(name string) bool {
for _, fd := range cols[1:] {
if fd.Name == name {
return true
}
}
return false
}
old := map[string]*TableRow{}
for i := range into.Rows {
old[into.Rows[i].ID] = &into.Rows[i]
}
out := &TableDoc{Type: into.Type}
seen := map[string]bool{}
for line, rec := range records[1:] {
id := strings.TrimSpace(rec[0])
if id == "" || seen[id] {
return nil, errs.New(errs.AssetImportFailed, "CSV line "+strconv.Itoa(line+2)+" has an empty or repeated id", log.F("id", id))
}
seen[id] = true
row := TableRow{ID: id}
if prev := old[id]; prev != nil {
for _, f := range prev.Fields {
if !inCols(f.Name) {
row.Fields = append(row.Fields, f)
}
}
}
for j := 1; j < len(rec); j++ {
if rec[j] == "" {
continue
}
v, err := parseCell(cols[j], rec[j])
if err != nil {
return nil, errs.Wrap(err, errs.AssetImportFailed, "row "+id+", column "+cols[j].Name)
}
row.Fields = append(row.Fields, aetext.Field{Name: cols[j].Name, Value: v})
}
order(td, row.Fields)
out.Rows = append(out.Rows, row)
}
return out, nil
}