├── .gitignore ├── LICENSE ├── csvparser.go ├── error.go ├── go.mod ├── parse_test.go └── readme.MD /.gitignore: -------------------------------------------------------------------------------- 1 | .idea -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 João Duarte 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /csvparser.go: -------------------------------------------------------------------------------- 1 | package csvparser 2 | 3 | import ( 4 | "bytes" 5 | "encoding/csv" 6 | "fmt" 7 | "io" 8 | "strings" 9 | ) 10 | 11 | // ParserFunc is the callback that will be called at each column parsing/reading 12 | // The value parameter is the column value, and the destination is the struct to add values from the parsing 13 | type ParserFunc[ReadTo any] func(value string, destination *ReadTo) error 14 | 15 | // AfterParsingRowFunc is a callback/hook that will run after each row is parsed. 16 | type AfterParsingRowFunc[ReadTo any] func(parsedObject ReadTo) 17 | 18 | // OnErrorFunc is a callback that will run after every parsing error. 19 | type OnErrorFunc func(row []string, err error) 20 | 21 | // CsvParser is the internal object that will keep all the references needed to parse the file 22 | type CsvParser[ReadTo any] struct { 23 | fileReader *csv.Reader 24 | columnParsers map[string]ParserFunc[ReadTo] 25 | onError OnErrorFunc 26 | afterParsingHook AfterParsingRowFunc[ReadTo] 27 | headers []string 28 | onFinish func() 29 | onStart func() 30 | terminateOnParsingError bool 31 | } 32 | 33 | // NewCsvParserFromBytes instantiates a new CsvParser from a []byte input 34 | // The *headers parameter are necessary if your .csv file doesn't contain headers 35 | // by default. Adding headers to the constructor will make the parser know what to handle. 36 | func NewCsvParserFromBytes[ReadTo any](input []byte, headers ...string) *CsvParser[ReadTo] { 37 | reader := bytes.NewReader(input) 38 | return NewCsvParserFromReader[ReadTo](reader, headers...) 39 | } 40 | 41 | // NewCsvParserFromReader instantiates a new CsvParser from an io.Reader directly. 42 | // Useful when reading from multipart files. 43 | // The *headers parameter are necessary if your .csv file doesn't contain headers 44 | // by default. Adding headers to the constructor will make the parser know what to handle. 45 | func NewCsvParserFromReader[ReadTo any](input io.Reader, headers ...string) *CsvParser[ReadTo] { 46 | return &CsvParser[ReadTo]{ 47 | fileReader: csv.NewReader(input), 48 | headers: headers, 49 | columnParsers: map[string]ParserFunc[ReadTo]{}, 50 | } 51 | } 52 | 53 | // TerminateOnParsingError sets a flag to finish the parsing if a single row throws an error. 54 | // if flag is set to false, it will continue to parse and skip the record with the error. 55 | func (c *CsvParser[ReadTo]) TerminateOnParsingError() *CsvParser[ReadTo] { 56 | c.terminateOnParsingError = true 57 | return c 58 | } 59 | 60 | // OnParseError sets a callback that is supposed to be run after a row has a parsing error 61 | func (c *CsvParser[ReadTo]) OnParseError(callback OnErrorFunc) *CsvParser[ReadTo] { 62 | c.onError = callback 63 | return c 64 | } 65 | 66 | // AfterEachParsingHook adds a handler that will run after every single parsing 67 | func (c *CsvParser[ReadTo]) AfterEachParsingHook(handler AfterParsingRowFunc[ReadTo]) *CsvParser[ReadTo] { 68 | c.afterParsingHook = handler 69 | return c 70 | } 71 | 72 | // OnFinish adds a handler that will run at the end of the file parsing. 73 | func (c *CsvParser[ReadTo]) OnFinish(handler func()) *CsvParser[ReadTo] { 74 | c.onFinish = handler 75 | return c 76 | } 77 | 78 | // OnStart adds a handler that will run at the start of the file parsing. 79 | func (c *CsvParser[ReadTo]) OnStart(handler func()) *CsvParser[ReadTo] { 80 | c.onStart = handler 81 | return c 82 | } 83 | 84 | // AddColumnParser adds a parser for each column to the internal parser list 85 | func (c *CsvParser[ReadTo]) AddColumnParser(headerName string, parser ParserFunc[ReadTo]) *CsvParser[ReadTo] { 86 | c.columnParsers[headerName] = parser 87 | return c 88 | } 89 | 90 | // Parse returns an array of the object to return ([]ReadTo) from the input data and parsers provided. 91 | func (c *CsvParser[ReadTo]) Parse() ([]ReadTo, error) { 92 | c.runOnStart() 93 | err := c.prepareHeaders() 94 | if err != nil { 95 | return []ReadTo{}, err 96 | } 97 | res, err := c.parseResults() 98 | c.runOnFinish() 99 | return res, err 100 | } 101 | 102 | // prepareHeaders verifies if the headers and parsers are matched. If the headers are not passed in the constructor, 103 | // it will load the headers from the file data. 104 | func (c *CsvParser[ReadTo]) prepareHeaders() error { 105 | if c.areHeadersEmpty() { 106 | return c.loadHeadersFromFile() 107 | } 108 | header, existsUnparsableHeader := c.isThereAnUnparsableHeader() 109 | if existsUnparsableHeader { 110 | return newUnparsableHeaderErr(header) 111 | } 112 | return nil 113 | } 114 | 115 | // areHeadersEmpty checks if the headers are empty 116 | func (c *CsvParser[ReadTo]) areHeadersEmpty() bool { 117 | return len(c.headers) == 0 118 | } 119 | 120 | // areHeadersAndParsersMatched makes sure that each header has an equivalent parser. 121 | func (c *CsvParser[ReadTo]) isThereAnUnparsableHeader() (string, bool) { 122 | for _, header := range c.headers { 123 | if !c.existsParserForHeader(header) { 124 | return header, true 125 | } 126 | } 127 | return "", false 128 | } 129 | 130 | // existsParserForHeader verifies if there is a parser defined for a specific header 131 | func (c *CsvParser[ReadTo]) existsParserForHeader(header string) bool { 132 | _, ok := c.getParserFor(header) 133 | return ok 134 | } 135 | 136 | // loadHeadersFromFile reads the first row in the file and loads it into the headers 137 | func (c *CsvParser[ReadTo]) loadHeadersFromFile() error { 138 | headers, err := c.fileReader.Read() 139 | if err != nil { 140 | return parseError{Msg: fmt.Sprintf("couldn't read headers from file: %s", err.Error())} 141 | } 142 | return c.loadHeaders(headers) 143 | } 144 | 145 | // loadHeaders loads a set of headers into the struct. 146 | func (c *CsvParser[ReadTo]) loadHeaders(headers []string) error { 147 | for _, header := range headers { 148 | if err := c.loadHeader(header); err != nil { 149 | return err 150 | } 151 | } 152 | return nil 153 | } 154 | 155 | // loadHeader loads one header into the struct. If it is not able to be parsed 156 | // (doesn't have an associated parser), it will return an error. 157 | func (c *CsvParser[ReadTo]) loadHeader(header string) error { 158 | header = strings.Trim(header, " ") 159 | if !c.isHeaderAbleToBeParsed(header) { 160 | return newUnparsableHeaderErr(header) 161 | } 162 | c.headers = append(c.headers, header) 163 | return nil 164 | } 165 | 166 | // isHeaderAbleToBeParsed verifies if there is a correspondent parser for said header. 167 | func (c *CsvParser[ReadTo]) isHeaderAbleToBeParsed(header string) bool { 168 | _, ok := c.getParserFor(header) 169 | return ok 170 | } 171 | 172 | // getParserFor gets a parser for a specific header. 173 | func (c *CsvParser[ReadTo]) getParserFor(header string) (ParserFunc[ReadTo], bool) { 174 | res, ok := c.columnParsers[header] 175 | return res, ok 176 | } 177 | 178 | // parseResults returns the slice of objects to be parsed from the .csv file. 179 | func (c *CsvParser[ReadTo]) parseResults() ([]ReadTo, error) { 180 | result := make([]ReadTo, 0) 181 | for { 182 | object, err := c.readRowAndParseObject() 183 | if err == io.EOF { 184 | break 185 | } 186 | if err != nil { 187 | if !c.terminateOnParsingError { 188 | continue 189 | } 190 | return []ReadTo{}, newparseError(err) 191 | } 192 | result = append(result, *object) 193 | } 194 | return result, nil 195 | } 196 | 197 | // readRowAndParseObject reads a file row and parses it into an object. 198 | func (c *CsvParser[ReadTo]) readRowAndParseObject() (*ReadTo, error) { 199 | row, err := c.fileReader.Read() 200 | if err != nil { 201 | return nil, err 202 | } 203 | return c.parseRow(row) 204 | } 205 | 206 | // parseRow parses a single row into the target object. Runs the hook for the object if success. 207 | func (c *CsvParser[ReadTo]) parseRow(row []string) (*ReadTo, error) { 208 | object := new(ReadTo) 209 | err := c.parseColumns(row, object) 210 | if err != nil { 211 | c.runOnError(row, err) 212 | return nil, err 213 | } 214 | c.runAfterParsingHook(object) 215 | return object, nil 216 | } 217 | 218 | // runOnError runs the onError callback. 219 | func (c *CsvParser[ReadTo]) runOnError(row []string, err error) { 220 | if c.onErrorExists() { 221 | c.onError(row, err) 222 | } 223 | } 224 | 225 | func (c *CsvParser[ReadTo]) onErrorExists() bool { 226 | return c.onError != nil 227 | } 228 | 229 | // runHook runs the hook that is set up in the struct 230 | func (c *CsvParser[ReadTo]) runAfterParsingHook(object *ReadTo) { 231 | if c.afterParsingHookExists() { 232 | c.afterParsingHook(*object) 233 | } 234 | } 235 | 236 | func (c *CsvParser[ReadTo]) afterParsingHookExists() bool { 237 | return c.afterParsingHook != nil 238 | } 239 | 240 | // parseColumns parses all the columns into a destination object. 241 | func (c *CsvParser[ReadTo]) parseColumns(row []string, destination *ReadTo) error { 242 | for i, columnValue := range row { 243 | err := c.parseColumn(columnValue, c.headers[i], destination) 244 | if err != nil { 245 | return err 246 | } 247 | } 248 | return nil 249 | } 250 | 251 | // parseColumn parses a single column. Uses columnParsers from the columnHeader to do it. 252 | func (c *CsvParser[ReadTo]) parseColumn(columnValue, columnHeader string, destination *ReadTo) error { 253 | parser, ok := c.getParserFor(columnHeader) 254 | if !ok { 255 | return newUnparsableHeaderErr(columnHeader) 256 | } 257 | if err := parser(columnValue, destination); err != nil { 258 | return err 259 | } 260 | return nil 261 | } 262 | 263 | func (c *CsvParser[ReadTo]) runOnStart() { 264 | if c.onStart != nil { 265 | c.onStart() 266 | } 267 | } 268 | 269 | func (c *CsvParser[ReadTo]) runOnFinish() { 270 | if c.onFinish != nil { 271 | c.onFinish() 272 | } 273 | } 274 | -------------------------------------------------------------------------------- /error.go: -------------------------------------------------------------------------------- 1 | package csvparser 2 | 3 | import "fmt" 4 | 5 | type parseError struct { 6 | Msg string 7 | } 8 | 9 | func (e parseError) Error() string { 10 | return fmt.Sprintf("csvparser: %s", e.Msg) 11 | } 12 | 13 | func newUnparsableHeaderErr(header string) parseError { 14 | return parseError{Msg: fmt.Sprintf("header \"%s\" doesn't have an associated parser", header)} 15 | } 16 | 17 | func newparseError(err error) parseError { 18 | return parseError{Msg: fmt.Sprintf("file couldn't be parsed: %s", err.Error())} 19 | } 20 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/plagioriginal/csvparser 2 | 3 | go 1.18 4 | -------------------------------------------------------------------------------- /parse_test.go: -------------------------------------------------------------------------------- 1 | package csvparser 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io" 7 | "reflect" 8 | "strconv" 9 | "strings" 10 | "testing" 11 | ) 12 | 13 | type person struct { 14 | Name string 15 | Age int 16 | School string 17 | } 18 | 19 | var impossibleAgeError = errors.New("impossible age") 20 | 21 | func nameParser(value string, into *person) error { 22 | into.Name = strings.Trim(value, " ") 23 | return nil 24 | } 25 | 26 | func ageParser(value string, into *person) error { 27 | value = strings.Trim(value, " ") 28 | age, err := strconv.Atoi(value) 29 | if err != nil { 30 | return err 31 | } 32 | if age > 150 { 33 | return impossibleAgeError 34 | } 35 | into.Age = age 36 | into.School = "new school" 37 | if age > 20 && age < 65 { 38 | into.School = "middle school" 39 | } 40 | if age > 65 { 41 | into.School = "old school" 42 | } 43 | return nil 44 | } 45 | 46 | func TestCsvParserWithoutHookAndFinishingIfParsingErrorIsFound(t *testing.T) { 47 | tests := []struct { 48 | name string 49 | input []byte 50 | headersToAdd []string 51 | parserAdder func(parser *CsvParser[person]) 52 | expectedResult []person 53 | expectedErr error 54 | }{ 55 | { 56 | name: "empty input results in eof error", 57 | input: []byte(""), 58 | headersToAdd: []string{}, 59 | parserAdder: nil, 60 | expectedResult: []person{}, 61 | expectedErr: parseError{Msg: fmt.Sprintf("couldn't read headers from file: %s", io.EOF.Error())}, 62 | }, 63 | { 64 | name: "empty input but with headers", 65 | input: []byte(""), 66 | headersToAdd: []string{"header one"}, 67 | parserAdder: nil, 68 | expectedResult: []person{}, 69 | expectedErr: parseError{Msg: fmt.Sprintf("header \"%s\" doesn't have an associated parser", "header one")}, 70 | }, 71 | { 72 | name: "header age without parser should return error", 73 | input: []byte(` 74 | name,age 75 | frank,13 76 | anabelle,65`), 77 | headersToAdd: []string{}, 78 | parserAdder: func(parser *CsvParser[person]) { 79 | parser.AddColumnParser("name", nameParser) 80 | }, 81 | expectedResult: []person{}, 82 | expectedErr: parseError{Msg: fmt.Sprintf("header \"%s\" doesn't have an associated parser", "age")}, 83 | }, 84 | { 85 | name: "success with no headers added", 86 | input: []byte(` 87 | name,age 88 | frank,13 89 | anabelle,70`), 90 | headersToAdd: []string{}, 91 | parserAdder: func(parser *CsvParser[person]) { 92 | parser.AddColumnParser("name", nameParser). 93 | AddColumnParser("age", ageParser) 94 | }, 95 | expectedResult: []person{ 96 | { 97 | Name: "frank", 98 | Age: 13, 99 | School: "new school", 100 | }, 101 | { 102 | Name: "anabelle", 103 | Age: 70, 104 | School: "old school", 105 | }, 106 | }, 107 | expectedErr: nil, 108 | }, 109 | { 110 | name: "success with headers", 111 | input: []byte(` 112 | frank,13 113 | anabelle,70`), 114 | headersToAdd: []string{"name", "age"}, 115 | parserAdder: func(parser *CsvParser[person]) { 116 | parser.AddColumnParser("name", nameParser). 117 | AddColumnParser("age", ageParser) 118 | }, 119 | expectedResult: []person{ 120 | { 121 | Name: "frank", 122 | Age: 13, 123 | School: "new school", 124 | }, 125 | { 126 | Name: "anabelle", 127 | Age: 70, 128 | School: "old school", 129 | }, 130 | }, 131 | expectedErr: nil, 132 | }, 133 | { 134 | name: "make sure error from custom-parser is triggered", 135 | input: []byte(` 136 | name,age 137 | frank,13 138 | anabelle,70 139 | rita,170`), 140 | headersToAdd: []string{}, 141 | parserAdder: func(parser *CsvParser[person]) { 142 | parser.AddColumnParser("name", nameParser). 143 | AddColumnParser("age", ageParser) 144 | }, 145 | expectedResult: []person{}, 146 | expectedErr: newparseError(impossibleAgeError), 147 | }, 148 | } 149 | 150 | for _, tt := range tests { 151 | t.Run(tt.name, func(t *testing.T) { 152 | parser := NewCsvParserFromBytes[person](tt.input, tt.headersToAdd...) 153 | parser.TerminateOnParsingError() 154 | if tt.parserAdder != nil { 155 | tt.parserAdder(parser) 156 | } 157 | res, err := parser.Parse() 158 | if tt.expectedErr == nil && err != nil { 159 | t.Errorf("wanted error \"%v\", got error \"%v\"", tt.expectedErr, err) 160 | } 161 | if tt.expectedErr != nil && !errors.Is(err, tt.expectedErr) { 162 | t.Errorf("wanted error \"%v\", got error \"%v\"", tt.expectedErr, err) 163 | } 164 | if !reflect.DeepEqual(tt.expectedResult, res) { 165 | t.Errorf("result %v, but got %v", tt.expectedResult, res) 166 | } 167 | }) 168 | } 169 | } 170 | 171 | func TestCsvParserHook(t *testing.T) { 172 | middleAgedPeople := make([]person, 0) 173 | input := []byte(` 174 | name,age 175 | frank,13 176 | rita, 40 177 | robert, 25 178 | anabelle,70`) 179 | parser := NewCsvParserFromBytes[person](input). 180 | AddColumnParser("name", nameParser). 181 | AddColumnParser("age", ageParser). 182 | AfterEachParsingHook(func(parsedObject person) { 183 | 184 | if parsedObject.School == "middle school" { 185 | middleAgedPeople = append(middleAgedPeople, parsedObject) 186 | } 187 | }) 188 | res, err := parser.Parse() 189 | if err != nil { 190 | t.Errorf("expected nil error, got %v", err) 191 | } 192 | expectedEndResult := []person{ 193 | { 194 | Name: "frank", 195 | Age: 13, 196 | School: "new school", 197 | }, 198 | { 199 | Name: "rita", 200 | Age: 40, 201 | School: "middle school", 202 | }, 203 | { 204 | Name: "robert", 205 | Age: 25, 206 | School: "middle school", 207 | }, 208 | { 209 | Name: "anabelle", 210 | Age: 70, 211 | School: "old school", 212 | }, 213 | } 214 | 215 | expectedMiddleAgedPeople := []person{ 216 | { 217 | Name: "rita", 218 | Age: 40, 219 | School: "middle school", 220 | }, 221 | { 222 | Name: "robert", 223 | Age: 25, 224 | School: "middle school", 225 | }, 226 | } 227 | 228 | if !reflect.DeepEqual(res, expectedEndResult) { 229 | t.Errorf("expected result %v, got result %v", expectedEndResult, res) 230 | } 231 | if !reflect.DeepEqual(middleAgedPeople, expectedMiddleAgedPeople) { 232 | t.Errorf("expected middle-aged people result %v, got result %v", expectedMiddleAgedPeople, middleAgedPeople) 233 | } 234 | } 235 | 236 | func TestOnParseErrorHook(t *testing.T) { 237 | hasOnErrorRan := false 238 | input := []byte(` 239 | name,age 240 | frank,13 241 | rita, 40 242 | robert, 25 243 | anabelle,170`) 244 | NewCsvParserFromBytes[person](input). 245 | AddColumnParser("name", nameParser). 246 | AddColumnParser("age", ageParser). 247 | TerminateOnParsingError(). 248 | OnParseError(func(row []string, err error) { 249 | hasOnErrorRan = true 250 | expectedRow := []string{"anabelle", "170"} 251 | expectedErr := impossibleAgeError 252 | if !reflect.DeepEqual(row, expectedRow) { 253 | t.Errorf("wanted row %v, got row %v", expectedRow, row) 254 | } 255 | if err != expectedErr { 256 | t.Errorf("wanted error %v, got error %v", expectedErr, err) 257 | } 258 | }). 259 | Parse() 260 | if !hasOnErrorRan { 261 | t.Errorf("error hook didn't start.") 262 | } 263 | } 264 | 265 | func TestOnStartAndOnFinishHooks(t *testing.T) { 266 | hasOnStartRan := false 267 | hasOnEndRan := false 268 | input := []byte(` 269 | name,age 270 | frank,13 271 | rita, 40 272 | robert, 25 273 | anabelle,17`) 274 | NewCsvParserFromBytes[person](input). 275 | AddColumnParser("name", nameParser). 276 | AddColumnParser("age", ageParser). 277 | TerminateOnParsingError(). 278 | OnStart(func() { 279 | hasOnStartRan = true 280 | }). 281 | OnFinish(func() { 282 | hasOnEndRan = true 283 | }). 284 | Parse() 285 | if !hasOnStartRan { 286 | t.Errorf("start hook didn't start.") 287 | } 288 | if !hasOnEndRan { 289 | t.Errorf("end hook didn't start.") 290 | } 291 | } 292 | 293 | func TestCsvParserDontFinishOnError(t *testing.T) { 294 | input := []byte(` 295 | name,age 296 | frank,13 297 | rita, 40 298 | robert, 25 299 | anabelle,170`) 300 | results, err := NewCsvParserFromBytes[person](input). 301 | AddColumnParser("name", nameParser). 302 | AddColumnParser("age", ageParser). 303 | Parse() 304 | 305 | expectedResults := []person{ 306 | {Name: "frank", Age: 13, School: "new school"}, 307 | {Name: "rita", Age: 40, School: "middle school"}, 308 | {Name: "robert", Age: 25, School: "middle school"}, 309 | } 310 | if !reflect.DeepEqual(results, expectedResults) { 311 | t.Errorf("wanted %v, got %v", expectedResults, results) 312 | } 313 | if err != nil { 314 | t.Errorf("wanted nil error, got error %v", err) 315 | } 316 | } 317 | -------------------------------------------------------------------------------- /readme.MD: -------------------------------------------------------------------------------- 1 | # csvparser 2 | [![Go Report Card](https://goreportcard.com/badge/github.com/plagioriginal/csvparser)](https://goreportcard.com/report/github.com/plagioriginal/csvparser) 3 | 4 | This package provides a fast and easy-of-use custom mapping from .csv data into Golang structs. 5 | 6 | # Index 7 | - [Pre-requisites](#pre-requisites) 8 | - [Installation](#installation) 9 | - [Examples](#examples) 10 | - [CSV Parsing from bytes](#csv-parsing-from-bytes) 11 | - [Csv Parsing from multipart file / anything that applies the io.Reader](#csv-parsing-from-multipart-file--anything-that-applies-the-ioreader) 12 | - [Adding hooks](#adding-hooks) 13 | - [After each successful parsing](#after-each-successful-parsing) 14 | - [On Error Hook](#on-error-hook) 15 | - [Additional Settings](#additional-settings) 16 | - [Terminate on row parsing error](#terminate-on-row-parsing-error) 17 | 18 | ## Pre-requisites 19 | Since the library uses generics, it is necessary to have `go1.18` 20 | 21 | ## Installation 22 | ``` 23 | go get github.com/plagioriginal/csvparser 24 | ``` 25 | 26 | ## Examples 27 | 28 | ### Csv parsing from bytes 29 | This will read the .csv data being sent, and will return an array of whatever you would like. 30 | 31 | ```go 32 | type Person struct { 33 | Name string 34 | Age int 35 | isInSchool bool 36 | } 37 | 38 | var input = []byte(` 39 | name,age 40 | frank,13 41 | anabelle,70`) 42 | 43 | parser := csvparser.NewCsvParserFromBytes[Person](input) 44 | parser.AddColumnParser("name", func (value string, into *Person) error { 45 | into.Name = strings.Trim(value, " ") 46 | return nil 47 | }) 48 | parser.AddColumnParser("age", func (value string, into *Person) error { 49 | value = strings.Trim(value, " ") 50 | age, err := strconv.Atoi(value) 51 | if err != nil { 52 | return err 53 | } 54 | into.Age = age 55 | if age < 18 { 56 | into.IsInSchool = true 57 | } 58 | return nil 59 | }) 60 | 61 | // res is []Person type 62 | res, err := parser.Parse() 63 | ``` 64 | 65 | Note: as long as there is a parser for the header that you want, the order of the .csv columns will not matter 66 | 67 | #### What if the file doesn't have headers 68 | When instantiating the parser, you can specify the headers of the file, in order, and the parser will handle everything 69 | for you. Just remember that the ParserHandlers need to be added. 70 | 71 | ```go 72 | var input = []byte(` 73 | frank,13 74 | anabelle,70`) 75 | 76 | parser := csvparser.NewCsvParserFromBytes[Person](input, "name", "age"). 77 | AddColumnParser("name", nameHandler). 78 | AddColumnParser("age", ageHandler) 79 | ... 80 | ``` 81 | ### Csv Parsing from multipart file / anything that applies the io.Reader 82 | If you need to directly use something like a multipart file directly, you can do something like this: 83 | ```go 84 | func (h *OrderHandler) handlerFunc(w http.ResponseWriter, r *http.Request) { 85 | file, _, err := r.FormFile("file-key-in-request") 86 | if err != nil { 87 | ... 88 | } 89 | defer file.Close() 90 | parser := csvparser.NewCsvParserFromReader[WhateverStruct](file) 91 | ... 92 | } 93 | ``` 94 | 95 | ### Adding hooks 96 | 97 | #### After each successful parsing 98 | You can add a hook that will run everytime something is parsed from the .csv file, 99 | so that you don't have to do another loop in the results in case you want to add more logic into it. 100 | To do this, use the function `AfterEachParsingHook()` 101 | 102 | ```go 103 | parser := csvparser.NewCsvParserFromBytes[Person](input) 104 | children := make([]Person, 0) 105 | parser.AfterEachParsingHook(func(person Person) { 106 | if parsedPerson.IsInSchool { 107 | children = append(children, person) 108 | } 109 | }) 110 | ``` 111 | 112 | #### On Error Hook 113 | Use the `OnError()` function to handle the error of an invalid row yourself. 114 | ```go 115 | parser := csvparser.NewCsvParserFromBytes[Person](input). 116 | OnError(func(row []string, err error) { 117 | log.Printf("row %v has thrown the error: %v", row, err) 118 | }) 119 | ``` 120 | 121 | ### Additional Settings 122 | #### Terminate on row parsing error 123 | You can choose if you want to throw an error on the parsing results if the input has an invalid row, or 124 | just continue and skip that record. 125 | By default, the behaviour is to skip the error. 126 | 127 | However, you can make it stop and throw an error with the function 128 | `TerminateOnParsingError()`: 129 | 130 | ```go 131 | res, err := csvparser.NewCsvParserFromBytes[Person](input). 132 | AddColumnParser("name", nameHandler). 133 | AddColumnParser("age", ageHandler). 134 | TerminateOnParsingError(). 135 | Parse() 136 | ``` 137 | --------------------------------------------------------------------------------