9 Commits

Author SHA1 Message Date
f02f5f8410 chore: merge to fork 2023-06-30 22:38:16 +10:00
3be5acadaf Merge branch 'dev/unmarshal' into dev/suyono.dev/unmarshal 2023-06-30 22:36:50 +10:00
Suyono
8a84f5164d wip: unmarshal basic functionality works in debug 2023-06-25 22:14:31 +10:00
Suyono
5b9b896f34 wip: unmarshal 2023-06-25 13:41:36 +10:00
Suyono
bba267490a chore: update module path on forked repo 2023-06-23 18:47:12 +10:00
Suyono
518d723a8a chore: go mod edit go version 2023-06-23 18:44:43 +10:00
Suyono
b062eea7b6 wip: unmarshaler 2023-06-23 11:46:38 +10:00
Suyono
07fabf1962 wip: unmarshaler 2023-06-19 11:29:21 +10:00
Suyono
33a3a90d81 wip: unmarshal 2023-06-18 16:28:27 +10:00
11 changed files with 775 additions and 0 deletions

1
.tool-versions Normal file
View File

@@ -0,0 +1 @@
golang 1.18

194
default_record_handler.go Normal file
View File

@@ -0,0 +1,194 @@
package gocsvparser
import (
"errors"
"fmt"
"reflect"
"strconv"
"strings"
)
type defaultRecordHandler struct {
fieldByTag map[string]columnFieldBinding
outType reflect.Type
columnNameMapped bool
}
type columnFieldBinding struct {
field reflect.StructField
index int
}
func newDefaultHandler() *defaultRecordHandler {
newHandler := new(defaultRecordHandler)
newHandler.fieldByTag = make(map[string]columnFieldBinding)
newHandler.columnNameMapped = false
return newHandler
}
func (d *defaultRecordHandler) HandleRecord(v any, record []string) error {
var (
err error
strField string
index int
ok bool
binding columnFieldBinding
val reflect.Value
)
if d.outType == nil {
err = d.detType(v)
if err != nil {
return fmt.Errorf("error HandleRecord: detType: %+v", err)
}
}
if !d.columnNameMapped {
for index, strField = range record {
if binding, ok = d.fieldByTag[strField]; ok {
binding.index = index
d.fieldByTag[strField] = binding
}
}
d.columnNameMapped = true
return HeaderRead
}
val = reflect.ValueOf(v)
if val.Kind() != reflect.Pointer {
return errors.New("error HandleRecord: v Kind() is not Pointer")
}
val = val.Elem()
switch val.Kind() {
case reflect.Struct:
for _, binding = range d.fieldByTag {
if binding.index < len(record) {
val, err = d.setValue(val, record[binding.index], binding.field)
if err != nil {
return fmt.Errorf("error HandleRecord: StructField SetValue: %+v", err)
}
}
}
case reflect.Map:
return errors.New("error HandleRecord: Map is not supported yet") //TODO: fix me
case reflect.Slice:
return errors.New("error HandleRecord: Slice is not supported yet") //TODO: fix me
}
return nil
}
func (d *defaultRecordHandler) setValue(val reflect.Value, strValue string, structFiled reflect.StructField) (reflect.Value, error) {
var (
fieldVal reflect.Value
f64 float64
i64 int64
i int
err error
b bool
)
fieldVal = val.FieldByIndex(structFiled.Index)
switch fieldVal.Type().Kind() {
case reflect.String:
fieldVal.SetString(strValue)
case reflect.Bool:
b, err = strconv.ParseBool(strValue)
if err != nil {
return reflect.Value{}, fmt.Errorf("ParseBool: %+v", err)
}
fieldVal.SetBool(b)
case reflect.Int64:
i64, err = strconv.ParseInt(strValue, 0, 64)
if err != nil {
return reflect.Value{}, fmt.Errorf("ParseInt 64: %+v", err)
}
fieldVal.SetInt(i64)
case reflect.Int32:
i64, err = strconv.ParseInt(strValue, 0, 32)
if err != nil {
return reflect.Value{}, fmt.Errorf("ParseInt 32: %+v", err)
}
fieldVal.SetInt(i64)
case reflect.Int16:
i64, err = strconv.ParseInt(strValue, 0, 16)
if err != nil {
return reflect.Value{}, fmt.Errorf("ParseInt 16: %+v", err)
}
fieldVal.SetInt(i64)
case reflect.Int8:
i64, err = strconv.ParseInt(strValue, 0, 8)
if err != nil {
return reflect.Value{}, fmt.Errorf("ParseInt 8: %+v", err)
}
fieldVal.SetInt(i64)
case reflect.Int:
i, err = strconv.Atoi(strValue)
if err != nil {
return reflect.Value{}, fmt.Errorf("strconv.Atoi: %+v", err)
}
fieldVal.SetInt(int64(i))
case reflect.Uint64, reflect.Uint32, reflect.Uint16, reflect.Uint8, reflect.Uint:
return reflect.Value{}, errors.New("unimplemented") //TODO: fix me
case reflect.Float32:
f64, err = strconv.ParseFloat(strValue, 32)
if err != nil {
return reflect.Value{}, fmt.Errorf("ParseFloat 32: %+v", err)
}
fieldVal.SetFloat(f64)
case reflect.Float64:
f64, err = strconv.ParseFloat(strValue, 64)
if err != nil {
return reflect.Value{}, fmt.Errorf("ParseFloat 64: %+v", err)
}
fieldVal.SetFloat(f64)
default:
return reflect.Value{}, errors.New("missing implementation") //TODO: fix me
}
return val, nil
}
func (d *defaultRecordHandler) SetFieldConfigs(configs []FieldsConfig) {
}
func (d *defaultRecordHandler) detType(v any) error {
typ := reflect.TypeOf(v)
if typ.Kind() == reflect.Pointer {
typ = typ.Elem()
if typ.Kind() == reflect.Struct {
d.outType = typ
return d.mapStructTag()
} else if typ.Kind() == reflect.Map {
//TODO: implementation
} else if typ.Kind() == reflect.Slice {
//TODO: implementation
}
}
return fmt.Errorf("v should be pointer of Struct, Map, or Slice: %+v", typ)
}
func (d *defaultRecordHandler) mapStructTag() error {
//TODO: implementation
for _, field := range reflect.VisibleFields(d.outType) {
if csv, ok := field.Tag.Lookup(csvTag); ok {
s := strings.Split(csv, ",")
if len(s) == 0 {
return fmt.Errorf("invalid tag %+v", field.Tag)
}
if _, ok = d.fieldByTag[s[0]]; ok {
return fmt.Errorf("problem with the receiving struct, multiple field with tag %s", s[0])
}
d.fieldByTag[s[0]] = columnFieldBinding{
field: field,
}
} else if csvIndex, ok := field.Tag.Lookup(csvIndexTag); ok {
_ = csvIndex //TODO: process tag
}
}
return nil
}

38
example/example_test.go Normal file
View File

@@ -0,0 +1,38 @@
package example
import (
"testing"
"gitea.suyono.dev/suyono/gocsvparser"
)
type mtcarsFlat struct {
Model string `csv:"model"`
MilesPerGalon float64 `csv:"mpg"`
Cylinder int `csv:"cyl"`
Displacement float64 `csv:"disp"`
Horsepower int `csv:"hp"`
DriveShaftRatio float64 `csv:"drat"`
Weight float64 `csv:"wt"`
QuerterMileTime float64 `csv:"qsec"`
VEngine bool `csv:"vs"`
AutoTransmission bool `csv:"am"`
Gear int `csv:"gear"`
Carburetors int `csv:"carb"`
}
func TestMtcars(t *testing.T) {
var (
cars []mtcarsFlat
err error
)
err = gocsvparser.Unmarshal(MtcarsCsv, &cars)
if err != nil {
t.Fatalf("unexpected error %+v", err)
}
for _, c := range cars {
t.Logf("%+v", c)
}
}

37
example/mtcars.go Normal file
View File

@@ -0,0 +1,37 @@
package example
var (
MtcarsCsv = []byte(`model,mpg,cyl,disp,hp,drat,wt,qsec,vs,am,gear,carb
Mazda RX4,21,6,160,110,3.9,2.62,16.46,0,1,4,4
Mazda RX4 Wag,21,6,160,110,3.9,2.875,17.02,0,1,4,4
Datsun 710,22.8,4,108,93,3.85,2.32,18.61,1,1,4,1
Hornet 4 Drive,21.4,6,258,110,3.08,3.215,19.44,1,0,3,1
Hornet Sportabout,18.7,8,360,175,3.15,3.44,17.02,0,0,3,2
Valiant,18.1,6,225,105,2.76,3.46,20.22,1,0,3,1
Duster 360,14.3,8,360,245,3.21,3.57,15.84,0,0,3,4
Merc 240D,24.4,4,146.7,62,3.69,3.19,20,1,0,4,2
Merc 230,22.8,4,140.8,95,3.92,3.15,22.9,1,0,4,2
Merc 280,19.2,6,167.6,123,3.92,3.44,18.3,1,0,4,4
Merc 280C,17.8,6,167.6,123,3.92,3.44,18.9,1,0,4,4
Merc 450SE,16.4,8,275.8,180,3.07,4.07,17.4,0,0,3,3
Merc 450SL,17.3,8,275.8,180,3.07,3.73,17.6,0,0,3,3
Merc 450SLC,15.2,8,275.8,180,3.07,3.78,18,0,0,3,3
Cadillac Fleetwood,10.4,8,472,205,2.93,5.25,17.98,0,0,3,4
Lincoln Continental,10.4,8,460,215,3,5.424,17.82,0,0,3,4
Chrysler Imperial,14.7,8,440,230,3.23,5.345,17.42,0,0,3,4
Fiat 128,32.4,4,78.7,66,4.08,2.2,19.47,1,1,4,1
Honda Civic,30.4,4,75.7,52,4.93,1.615,18.52,1,1,4,2
Toyota Corolla,33.9,4,71.1,65,4.22,1.835,19.9,1,1,4,1
Toyota Corona,21.5,4,120.1,97,3.7,2.465,20.01,1,0,3,1
Dodge Challenger,15.5,8,318,150,2.76,3.52,16.87,0,0,3,2
AMC Javelin,15.2,8,304,150,3.15,3.435,17.3,0,0,3,2
Camaro Z28,13.3,8,350,245,3.73,3.84,15.41,0,0,3,4
Pontiac Firebird,19.2,8,400,175,3.08,3.845,17.05,0,0,3,2
Fiat X1-9,27.3,4,79,66,4.08,1.935,18.9,1,1,4,1
Porsche 914-2,26,4,120.3,91,4.43,2.14,16.7,0,1,5,2
Lotus Europa,30.4,4,95.1,113,3.77,1.513,16.9,1,1,5,2
Ford Pantera L,15.8,8,351,264,4.22,3.17,14.5,0,1,5,4
Ferrari Dino,19.7,6,145,175,3.62,2.77,15.5,0,1,5,6
Maserati Bora,15,8,301,335,3.54,3.57,14.6,0,1,5,8
Volvo 142E,21.4,4,121,109,4.11,2.78,18.6,1,1,4,2`)
)

25
ext_test.go Normal file
View File

@@ -0,0 +1,25 @@
package gocsvparser_test
import (
"testing"
"gitea.suyono.dev/suyono/gocsvparser"
)
type testDirect struct {
}
func (t *testDirect) SetFieldConfigs(configs []gocsvparser.FieldsConfig) {
}
func (t *testDirect) HandleRecord(v any, records []string) error {
return nil
}
func TestExt(t *testing.T) {
// _, err := gocsvparser.NewUnmarshaler().WithRecordHandler(new(testDirect))
// if err != nil {
// t.Fatalf("unexpected error %v", err)
// }
}

3
go.mod Normal file
View File

@@ -0,0 +1,3 @@
module gitea.suyono.dev/suyono/gocsvparser
go 1.18

6
gocsvparser.go Normal file
View File

@@ -0,0 +1,6 @@
package gocsvparser
const (
csvTag string = "csv"
csvIndexTag string = "csv.index"
)

1
marshal.go Normal file
View File

@@ -0,0 +1 @@
package gocsvparser

178
options.go Normal file
View File

@@ -0,0 +1,178 @@
package gocsvparser
import (
"encoding/csv"
"errors"
"fmt"
)
type csvOptionsType int
type CsvOption interface {
getType() csvOptionsType
}
const (
comma csvOptionsType = iota
comment
fieldsPerRecord
lazyQuotes
trimLeadingSpace
reuseRecord
useCrlf
columnHeader
recordHandler
csvReader
)
type csvReaderOption struct {
reader *csv.Reader
}
func CsvReader(reader *csv.Reader) csvReaderOption {
return csvReaderOption{
reader: reader,
}
}
func (csvReaderOption) getType() csvOptionsType {
return csvReader
}
type recordHandlerOption struct {
handlerType recordHandlerType
recordHandler RecordHandler
recordFieldsHandler RecordFieldsHandler
}
func RecordHandlerOption(handler any) (*recordHandlerOption, error) {
var rho *recordHandlerOption
if d, ok := handler.(RecordHandler); ok {
if d == nil {
return nil, errors.New("handler value is nil")
}
rho = &recordHandlerOption{
handlerType: direct,
recordHandler: d,
}
} else if s, ok := handler.(RecordFieldsHandler); ok {
if s == nil {
return nil, errors.New("handler value is nil")
}
rho = &recordHandlerOption{
handlerType: fieldsSpecific,
recordFieldsHandler: s,
}
} else {
return nil, fmt.Errorf("invalid handler type %T", handler)
}
return rho, nil
}
func (r *recordHandlerOption) getType() csvOptionsType {
return recordHandler
}
type columnHeaderOption struct {
header []string
}
func ColumnHeader(header ...string) columnHeaderOption {
columnHeader := columnHeaderOption{}
if len(header) > 0 {
columnHeader.header = make([]string, len(header))
copy(columnHeader.header, header)
}
return columnHeader
}
func (c columnHeaderOption) getType() csvOptionsType {
return columnHeader
}
type commaOption struct {
comma rune
}
func CommaOption(comma rune) commaOption {
return commaOption{
comma: comma,
}
}
func (c commaOption) getType() csvOptionsType {
return comma
}
type commentOption struct {
comment rune
}
func CommentOption(comment rune) commentOption {
return commentOption{
comment: comment,
}
}
func (c commentOption) getType() csvOptionsType {
return comment
}
type fieldsPerRecordOption struct {
fieldsPerRecord int
}
func FieldPerRecordOption(i int) fieldsPerRecordOption {
return fieldsPerRecordOption{
fieldsPerRecord: i,
}
}
func (f fieldsPerRecordOption) getType() csvOptionsType {
return fieldsPerRecord
}
type lazyQuotesOption struct {
lazyQuotes bool
}
func LazyQuotesOption(b bool) lazyQuotesOption {
return lazyQuotesOption{
lazyQuotes: b,
}
}
func (l lazyQuotesOption) getType() csvOptionsType {
return lazyQuotes
}
type trimLeadingSpaceOption struct {
trimLeadingSpace bool
}
func TrimLeadingSpaceOption(b bool) trimLeadingSpaceOption {
return trimLeadingSpaceOption{
trimLeadingSpace: b,
}
}
func (t trimLeadingSpaceOption) getType() csvOptionsType {
return trimLeadingSpace
}
type reuseRecordOption struct {
reuseRecord bool
}
func ReuseRecordOption(b bool) reuseRecordOption {
return reuseRecordOption{
reuseRecord: b,
}
}
func (r reuseRecordOption) getType() csvOptionsType {
return reuseRecord
}

219
unmarshal.go Normal file
View File

@@ -0,0 +1,219 @@
package gocsvparser
import (
"bytes"
"encoding/csv"
"errors"
"fmt"
"io"
"reflect"
)
var (
HeaderRead = errors.New("column headers successfully read")
)
type FieldsConfig struct {
Name string
Num int
}
type FieldsHandlerByName interface {
FieldName() string
FieldByName(name, field string) error
FieldsHandler
}
type FieldsHandlerByIndices interface {
FieldsIndices() []int
FieldByIndex(index int, field string) error
FieldsHandler
}
type FieldsHandler interface {
Fields(fields ...string) error
NumFields() int
}
type RecordFieldsHandler interface {
FieldsHandlers() []FieldsHandler
Out(v any) error
}
type RecordHandler interface {
// SetFieldConfigs is only effective if a Map is passed to HandleRecord
SetFieldConfigs(configs []FieldsConfig)
HandleRecord(v any, record []string) error
}
type recordHandlerType int
const (
direct recordHandlerType = 1
fieldsSpecific recordHandlerType = 2
)
type Unmarshaler struct {
options []CsvOption
header []string
recordHandler RecordHandler
recordFieldHandler RecordFieldsHandler
handlerType recordHandlerType
csvReader *csv.Reader
}
func NewUnmarshaler(options ...CsvOption) *Unmarshaler {
unmarshaler := &Unmarshaler{
handlerType: direct,
options: []CsvOption{nil},
}
if len(options) > 0 {
unmarshaler.options = append(unmarshaler.options, options...)
}
return unmarshaler
}
func (u *Unmarshaler) processOptions(options ...CsvOption) {
for _, option := range options {
if option == nil {
continue
}
switch option.getType() {
case csvReader:
if u.csvReader != nil {
continue
}
o := option.(csvReaderOption)
u.csvReader = o.reader
case comma:
o := option.(commaOption)
u.csvReader.Comma = o.comma
case comment:
o := option.(commentOption)
u.csvReader.Comment = o.comment
case fieldsPerRecord:
o := option.(fieldsPerRecordOption)
u.csvReader.FieldsPerRecord = o.fieldsPerRecord
case lazyQuotes:
o := option.(lazyQuotesOption)
u.csvReader.LazyQuotes = o.lazyQuotes
case trimLeadingSpace:
o := option.(trimLeadingSpaceOption)
u.csvReader.TrimLeadingSpace = o.trimLeadingSpace
case reuseRecord:
o := option.(reuseRecordOption)
u.csvReader.ReuseRecord = o.reuseRecord
case columnHeader:
headerOption := option.(columnHeaderOption)
u.header = headerOption.header
case recordHandler:
rho := option.(*recordHandlerOption)
u.handlerType = rho.handlerType
switch rho.handlerType {
case direct:
u.recordHandler = rho.recordHandler
case fieldsSpecific:
u.recordFieldHandler = rho.recordFieldsHandler
}
}
}
}
func (u *Unmarshaler) Unmarshal(data []byte, v any, options ...CsvOption) error {
var (
typ reflect.Type
val, slice reflect.Value
record []string
err error
)
if len(options) > 0 {
u.options = append(u.options, options...)
}
u.options[0] = CsvReader(csv.NewReader(bytes.NewReader(data)))
u.processOptions(u.options...)
if u.handlerType == direct {
if u.recordHandler == nil {
u.recordHandler = newDefaultHandler()
}
slice, typ, err = u.detOutputType(v)
if err != nil {
return fmt.Errorf("error Unmarshal: detOutputType: %+v", err)
}
for {
record, err = u.csvReader.Read()
if err != nil {
if err == io.EOF {
break
}
return fmt.Errorf("error Unmarshal: csv.Reader.Read: %+v", err)
}
val, err = u.newElem(typ)
if err != nil {
return fmt.Errorf("error Unmarshal: newElem: %+v", err)
}
err = u.recordHandler.HandleRecord(val.Interface(), record)
if err != nil {
if err == HeaderRead {
continue
}
return fmt.Errorf("error Unmarshal: RecordHandler.HandleRecord: %+v", err)
}
slice.Set(reflect.Append(slice, val.Elem()))
}
} else {
}
//TODO: implementation
return nil
}
func (u *Unmarshaler) detOutputType(v any) (reflect.Value, reflect.Type, error) {
var typ reflect.Type
if v == nil {
return reflect.Value{}, nil, errors.New("the output parameter is nil")
}
val := reflect.ValueOf(v)
if val.Kind() != reflect.Pointer {
return reflect.Value{}, nil, errors.New("invalid output parameter type")
}
val = val.Elem()
if val.Kind() != reflect.Slice {
return reflect.Value{}, nil, errors.New("invalid output parameter type")
}
typ = val.Type().Elem()
return val, typ, nil
}
func Unmarshal(data []byte, v any, options ...CsvOption) error {
return NewUnmarshaler().Unmarshal(data, v, options...)
}
func (u *Unmarshaler) newElem(typ reflect.Type) (reflect.Value, error) {
switch typ.Kind() {
case reflect.Struct:
return reflect.New(typ), nil
case reflect.Map:
//TODO: implementation
case reflect.Slice:
//TODO: implementation
}
return reflect.Zero(typ), errors.New("invalid impelementation") //TODO: placeholder; update me
}

73
unmarshal_test.go Normal file
View File

@@ -0,0 +1,73 @@
package gocsvparser
import (
"reflect"
"testing"
)
type Coba struct {
Name string `csv:"name"`
Address string `csv:"address,omitempty"`
Mile int64 `csv:"mile"`
anon
}
type anon struct {
FieldX int64 `csv:"x"`
OutputY int64 `csv:"y"`
}
func TestParse(t *testing.T) {
newDefaultHandler().detType(&Coba{})
}
func TestRead(t *testing.T) {
var coba []Coba
Read(&coba, t)
t.Logf("outside %+v", coba)
// type args struct {
// i interface{}
// }
// tests := []struct {
// name string
// args args
// }{
// // TODO: Add test cases.
// }
// for _, tt := range tests {
// t.Run(tt.name, func(t *testing.T) {
// Read(tt.args.i)
// })
// }
}
func Read(i interface{}, t *testing.T) {
var val reflect.Value
val = reflect.ValueOf(i)
if val.Kind() == reflect.Pointer {
val = val.Elem()
if val.Kind() == reflect.Slice {
vslice := val
typ := val.Type().Elem()
if typ.Kind() == reflect.Struct {
for _, x := range reflect.VisibleFields(typ) {
t.Logf("test: %+v", x)
}
nv := reflect.New(typ).Elem()
nv.FieldByName("Name").SetString("hello")
nv.FieldByName("Mile").SetInt(72)
// vslice = reflect.Append(vslice, nv)
vslice.Set(reflect.Append(vslice, nv))
t.Logf("inside: %+v", vslice)
}
}
}
}