package csvmagic import ( "bytes" "encoding/csv" "os" "reflect" "strconv" "strings" ) type ( LoadParams struct { Separator rune FirstRowIsHeader bool } FieldModificationInfo struct { SerializedName string FieldIdx int TypeInfo reflect.Kind } TypeConverter func(string, reflect.Value) ) var ( //you can use this to override the string->type conversion. types are indexed by their [reflect.Kind] typeConverters = make([]*TypeConverter, 27, 27) typesInitialized = false ) func CsvLoadParams(firstRowIsHeader bool) *LoadParams { return NewLoadParams(',', firstRowIsHeader) } func NewLoadParams(separator byte, firstRowIsHeader bool) *LoadParams { return &LoadParams{Separator: bytes.Runes([]byte{separator})[0], FirstRowIsHeader: firstRowIsHeader} } //LoadCsvAsObjects The crown jewel of this (currently) 200 line library. You can pass in a nil-value for params to //use the default parameter of comma-delimited. If the CSV file does not contain headers, you will have to annotate //your structs with the `csv:"MyHeader"` style of tag func LoadCsvAsObjects[K any](pathlike string, params *LoadParams) *[]K { //"Why do we need to pass a parameter of type K? lol" you might ask. Honestly? I couldn't figure out how to get //reflect info out of the generic type. I feel like one allocation and one copy isn't going to be a high cost. rows := *loadFile(pathlike, params) typ := reflect.TypeOf((*K)(nil)).Elem() fieldInfo := mapTypeInfo(typ) objSlice := reflect.MakeSlice(reflect.SliceOf(typ), len(rows)-1, len(rows)-1) container := objSlice.Interface().([]K) headers := rows[0] startingIdx := 1 indexOffset := 1 rowCount := len(rows) if !params.FirstRowIsHeader { startingIdx = 0 indexOffset = 0 rowCount = len(rows) - 1 headerLen := len(rows[0]) headers = make([]string, headerLen, headerLen) for i := 0; i < headerLen; i++ { headers[i] = strconv.Itoa(i) } } for i := startingIdx; i < rowCount; i++ { row := rows[i] err := inflate[K](&container[i-indexOffset], &fieldInfo, headers, row) if err != nil { panic(err) } } return &container } func loadFile(pathlike string, params *LoadParams) *[][]string { f, err := os.OpenFile(pathlike, os.O_RDONLY, 0755) if err != nil { panic(err) } defer f.Close() reader := csv.NewReader(f) if params == nil { params = CsvLoadParams(true) } reader.Comma = params.Separator records, err := reader.ReadAll() if err != nil { panic(err) } return &records } func mapTypeInfo(typeOf reflect.Type) map[string]FieldModificationInfo { fields := reflect.VisibleFields(typeOf) m := make(map[string]FieldModificationInfo) for _, v := range fields { jsonParams := v.Tag.Get("csv") info := FieldModificationInfo{} info.FieldIdx = v.Index[0] info.SerializedName = strings.Split(jsonParams, ",")[0] info.TypeInfo = v.Type.Kind() m[info.SerializedName] = info } return m } func inflate[K any](objPtr *K, fieldInfo *map[string]FieldModificationInfo, headers []string, data []string) error { lMap := *fieldInfo obj := reflect.Indirect(reflect.ValueOf(objPtr)) for i := 0; i < len(headers); i++ { header := headers[i] value := data[i] info, ok := lMap[header] if !ok { continue } field := obj.Field(info.FieldIdx) typeIdx := int(info.TypeInfo) converters := getConverters() converter := converters[typeIdx] if converter == nil { converter = &youAreOnYourOwn } (*converter)(value, field) } return nil } // /*=================================================================================================== typeConverters ===================================================================================================*/ func getConverters() []*TypeConverter { // Before you start casting stones at me, look, we need to memoize this thing because of how we //structured this whole thing. Yes, I know it's global state. Can it be turned into something much more // memory/branching friendly? It sure can. Did I spend the time doing it for this 'lil tiny project? No, // I did not. if !typesInitialized { populateConverters() typesInitialized = true } return typeConverters } //OverrideConverter // idx int func OverrideConverter(idx int, converter *TypeConverter) { if len(typeConverters) < idx-1 { tmp := make([]*TypeConverter, idx+1, idx+1) copy(tmp[:len(typeConverters)], typeConverters[:]) typeConverters = tmp } getConverters() typeConverters[idx] = converter } //populateConverters this is what makes all the magic happen. It's populates a bit of a fat array for function pointers func populateConverters() { typeConverters[int(reflect.Invalid)] = nil typeConverters[int(reflect.Bool)] = &convertBool typeConverters[int(reflect.Int)] = &convertInt typeConverters[int(reflect.Int8)] = &convertInt8 typeConverters[int(reflect.Int16)] = &convertInt16 typeConverters[int(reflect.Int32)] = &convertInt32 typeConverters[int(reflect.Int64)] = &convertInt64 typeConverters[int(reflect.Uint)] = &convertUint typeConverters[int(reflect.Uint8)] = &convertUint8 typeConverters[int(reflect.Uint16)] = &convertUint16 typeConverters[int(reflect.Uint32)] = &convertUint32 typeConverters[int(reflect.Uint64)] = &convertUint64 typeConverters[int(reflect.Uintptr)] = nil typeConverters[int(reflect.Float32)] = &convertFloat32 typeConverters[int(reflect.Float64)] = &convertFloat64 typeConverters[int(reflect.Complex64)] = nil typeConverters[int(reflect.Complex128)] = nil //exercise left for the reader lol typeConverters[int(reflect.Array)] = nil typeConverters[int(reflect.Chan)] = nil typeConverters[int(reflect.Func)] = nil typeConverters[int(reflect.Interface)] = nil typeConverters[int(reflect.Map)] = nil typeConverters[int(reflect.Pointer)] = nil typeConverters[int(reflect.Slice)] = nil typeConverters[int(reflect.String)] = &convertString typeConverters[int(reflect.Struct)] = nil typeConverters[int(reflect.UnsafePointer)] = nil } var convertBool TypeConverter = func(s string, field reflect.Value) { if s == "NULL" || len(s) == 0 { field.Set(reflect.ValueOf(false)) } } var convertInt TypeConverter = func(s string, field reflect.Value) { if s == "NULL" || len(s) == 0 { s = "0" } i, err := strconv.Atoi(s) if err != nil { panic(err) } settable := reflect.ValueOf(i) field.Set(settable) } var convertInt8 TypeConverter = func(s string, field reflect.Value) { if s == "NULL" || len(s) == 0 { s = "0" } i, err := strconv.ParseInt(s, 0, 8) if err != nil { panic(err) } settable := reflect.ValueOf(int8(i)) field.Set(settable) } var convertInt16 TypeConverter = func(s string, field reflect.Value) { if s == "NULL" || len(s) == 0 { s = "0" } i, err := strconv.ParseInt(s, 0, 16) if err != nil { panic(err) } settable := reflect.ValueOf(int16(i)) field.Set(settable) } var convertInt32 TypeConverter = func(s string, field reflect.Value) { if s == "NULL" || len(s) == 0 { s = "0" } i, err := strconv.ParseInt(s, 0, 32) if err != nil { panic(err) } settable := reflect.ValueOf(int32(i)) field.Set(settable) } var convertInt64 TypeConverter = func(s string, field reflect.Value) { if s == "NULL" || len(s) == 0 { s = "0" } i, err := strconv.ParseInt(s, 0, 64) if err != nil { panic(err) } settable := reflect.ValueOf(i) field.Set(settable) } var convertUint TypeConverter = func(s string, field reflect.Value) { if s == "NULL" || len(s) == 0 { s = "0" } i, err := strconv.ParseUint(s, 10, 0) if err != nil { panic(err) } settable := reflect.ValueOf(uint(i)) field.Set(settable) } var convertUint8 TypeConverter = func(s string, field reflect.Value) { if s == "NULL" || len(s) == 0 { s = "0" } i, err := strconv.ParseUint(s, 0, 8) if err != nil { panic(err) } settable := reflect.ValueOf(uint8(i)) field.Set(settable) } var convertUint16 TypeConverter = func(s string, field reflect.Value) { if s == "NULL" || len(s) == 0 { s = "0" } i, err := strconv.ParseUint(s, 0, 16) if err != nil { panic(err) } settable := reflect.ValueOf(uint16(i)) field.Set(settable) } var convertUint32 TypeConverter = func(s string, field reflect.Value) { if s == "NULL" || len(s) == 0 { s = "0" } i, err := strconv.ParseUint(s, 0, 32) if err != nil { panic(err) } settable := reflect.ValueOf(uint32(i)) field.Set(settable) } var convertUint64 TypeConverter = func(s string, field reflect.Value) { if s == "NULL" || len(s) == 0 { s = "0" } i, err := strconv.ParseUint(s, 10, 64) if err != nil { panic(err) } settable := reflect.ValueOf(i) field.Set(settable) } var convertFloat32 TypeConverter = func(s string, field reflect.Value) { if s == "NULL" || len(s) == 0 { s = "0" } f, err := strconv.ParseFloat(s, 32) if err != nil { panic(err) } field.SetFloat(f) } var convertFloat64 TypeConverter = func(s string, field reflect.Value) { if s == "NULL" || len(s) == 0 { s = "0" } f, err := strconv.ParseFloat(s, 64) if err != nil { panic(err) } field.SetFloat(f) } var convertString TypeConverter = func(s string, field reflect.Value) { if s == "NULL" { s = "" } settable := reflect.ValueOf(s) field.Set(settable) } var youAreOnYourOwn TypeConverter = func(s string, field reflect.Value) { panic(`type[` + field.Type().Name() + "\t" + field.Kind().String() + ` (` + strconv.Itoa(int(field.Kind())) + `)] has not been implemented. See OverrideConverter for more info.`) } //