├── .github ├── PULL_REQUEST_TEMPLATE.md └── workflows │ └── go.yml ├── .gitignore ├── LICENSE ├── README.md ├── _config.yml ├── cache.go ├── csvutil.go ├── csvutil_go117_test.go ├── csvutil_race_test.go ├── csvutil_test.go ├── decode.go ├── decoder.go ├── decoder_test.go ├── doc.go ├── encode.go ├── encoder.go ├── encoder_test.go ├── error.go ├── example_decoder_interface_test.go ├── example_decoder_no_header_test.go ├── example_decoder_register_test.go ├── example_decoder_test.go ├── example_decoder_unmashaler_test.go ├── example_encoder_test.go ├── example_header_test.go ├── example_marshal_marshaler_test.go ├── example_marshal_slice_map_test.go ├── example_marshal_test.go ├── example_unmarshal_test.go ├── go.mod ├── interface.go └── tag.go /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Description 2 | Please explain the changes that have been made in this area. 3 | 4 | ### Checklist 5 | - [ ] Code compiles without errors 6 | - [ ] Added new tests for the provided functionality 7 | - [ ] All tests are passing 8 | - [ ] Updated the README and/or documentation, if necessary 9 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | 11 | test: 12 | strategy: 13 | matrix: 14 | go-version: ["1.23"] 15 | os: [ubuntu-latest, macos-latest, windows-latest] 16 | 17 | runs-on: ${{ matrix.os }} 18 | steps: 19 | - uses: actions/checkout@v2 20 | 21 | - name: Set up Go 22 | uses: actions/setup-go@v2 23 | with: 24 | go-version: ${{ matrix.go-version }} 25 | 26 | - name: Test 27 | run: go test -race -v ./... -coverprofile=coverage.txt -covermode=atomic 28 | 29 | - name: Codecov 30 | uses: codecov/codecov-action@v1.2.1 31 | with: 32 | files: coverage.txt 33 | 34 | backward-compatibility-test: 35 | runs-on: ubuntu-latest 36 | strategy: 37 | matrix: 38 | go-version: ["1.21", "1.22"] 39 | steps: 40 | - uses: actions/checkout@v2 41 | with: 42 | fetch-depth: 1 43 | path: go/src/github.com/jszwec/csvutil 44 | 45 | - name: Set up Go 46 | uses: actions/setup-go@v2 47 | with: 48 | go-version: ${{ matrix.go-version }} 49 | 50 | - name: Test 51 | run: go test -race -v ./... 52 | env: 53 | GOPATH: /home/runner/work/csvutil/csvutil/go 54 | working-directory: go/src/github.com/jszwec/csvutil 55 | 56 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.gitignore.io/api/osx,go,windows,linux 2 | 3 | ### OSX ### 4 | .DS_Store 5 | .AppleDouble 6 | .LSOverride 7 | 8 | # Icon must end with two \r 9 | Icon 10 | 11 | 12 | # Thumbnails 13 | ._* 14 | 15 | # Files that might appear in the root of a volume 16 | .DocumentRevisions-V100 17 | .fseventsd 18 | .Spotlight-V100 19 | .TemporaryItems 20 | .Trashes 21 | .VolumeIcon.icns 22 | 23 | # Directories potentially created on remote AFP share 24 | .AppleDB 25 | .AppleDesktop 26 | Network Trash Folder 27 | Temporary Items 28 | .apdisk 29 | 30 | 31 | ### Go ### 32 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 33 | *.o 34 | *.a 35 | *.so 36 | 37 | # Folders 38 | _obj 39 | _test 40 | 41 | # Architecture specific extensions/prefixes 42 | *.[568vq] 43 | [568vq].out 44 | 45 | *.cgo1.go 46 | *.cgo2.c 47 | _cgo_defun.c 48 | _cgo_gotypes.go 49 | _cgo_export.* 50 | 51 | _testmain.go 52 | 53 | *.exe 54 | *.test 55 | *.prof 56 | 57 | 58 | ### Windows ### 59 | # Windows image file caches 60 | Thumbs.db 61 | ehthumbs.db 62 | 63 | # Folder config file 64 | Desktop.ini 65 | 66 | # Recycle Bin used on file shares 67 | $RECYCLE.BIN/ 68 | 69 | # Windows Installer files 70 | *.cab 71 | *.msi 72 | *.msm 73 | *.msp 74 | 75 | # Windows shortcuts 76 | *.lnk 77 | 78 | 79 | ### Linux ### 80 | *~ 81 | 82 | # temporary files which can be created if a process still has a handle open of a deleted file 83 | .fuse_hidden* 84 | 85 | # KDE directory preferences 86 | .directory 87 | 88 | # Linux trash folder which might appear on any partition or disk 89 | .Trash-* 90 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Jacek Szwec 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | csvutil [![PkgGoDev](https://pkg.go.dev/badge/github.com/jszwec/csvutil@v1.4.0?tab=doc)](https://pkg.go.dev/github.com/jszwec/csvutil?tab=doc) ![Go](https://github.com/jszwec/csvutil/workflows/Go/badge.svg) [![Go Report Card](https://goreportcard.com/badge/github.com/jszwec/csvutil)](https://goreportcard.com/report/github.com/jszwec/csvutil) [![codecov](https://codecov.io/gh/jszwec/csvutil/branch/master/graph/badge.svg)](https://codecov.io/gh/jszwec/csvutil) 2 | ================= 3 | 4 |

5 | 6 |

7 | 8 | Package csvutil provides fast, idiomatic, and dependency free mapping between CSV and Go (golang) values. 9 | 10 | This package is not a CSV parser, it is based on the [Reader](https://godoc.org/github.com/jszwec/csvutil#Reader) and [Writer](https://godoc.org/github.com/jszwec/csvutil#Writer) 11 | interfaces which are implemented by eg. std Go (golang) [csv package](https://golang.org/pkg/encoding/csv). This gives a possibility 12 | of choosing any other CSV writer or reader which may be more performant. 13 | 14 | Installation 15 | ------------ 16 | 17 | go get github.com/jszwec/csvutil 18 | 19 | Requirements 20 | ------------- 21 | 22 | * Go1.18+ 23 | 24 | Index 25 | ------ 26 | 27 | 1. [Examples](#examples) 28 | 1. [Unmarshal](#examples_unmarshal) 29 | 2. [Marshal](#examples_marshal) 30 | 3. [Unmarshal and metadata](#examples_unmarshal_and_metadata) 31 | 4. [But my CSV file has no header...](#examples_but_my_csv_has_no_header) 32 | 5. [Decoder.Map - data normalization](#examples_decoder_map) 33 | 6. [Different separator/delimiter](#examples_different_separator) 34 | 7. [Custom Types](#examples_custom_types) 35 | 8. [Custom time.Time format](#examples_time_format) 36 | 9. [Custom struct tags](#examples_struct_tags) 37 | 10. [Slice and Map fields](#examples_slice_and_map_field) 38 | 11. [Nested/Embedded structs](#examples_nested_structs) 39 | 12. [Inline tag](#examples_inlined_structs) 40 | 2. [Performance](#performance) 41 | 1. [Unmarshal](#performance_unmarshal) 42 | 2. [Marshal](#performance_marshal) 43 | 44 | Example 45 | -------- 46 | 47 | ### Unmarshal 48 | 49 | Nice and easy Unmarshal is using the Go std [csv.Reader](https://golang.org/pkg/encoding/csv/#Reader) with its default options. Use [Decoder](https://godoc.org/github.com/jszwec/csvutil#Decoder) for streaming and more advanced use cases. 50 | 51 | ```go 52 | var csvInput = []byte(` 53 | name,age,CreatedAt 54 | jacek,26,2012-04-01T15:00:00Z 55 | john,,0001-01-01T00:00:00Z`, 56 | ) 57 | 58 | type User struct { 59 | Name string `csv:"name"` 60 | Age int `csv:"age,omitempty"` 61 | CreatedAt time.Time 62 | } 63 | 64 | var users []User 65 | if err := csvutil.Unmarshal(csvInput, &users); err != nil { 66 | fmt.Println("error:", err) 67 | } 68 | 69 | for _, u := range users { 70 | fmt.Printf("%+v\n", u) 71 | } 72 | 73 | // Output: 74 | // {Name:jacek Age:26 CreatedAt:2012-04-01 15:00:00 +0000 UTC} 75 | // {Name:john Age:0 CreatedAt:0001-01-01 00:00:00 +0000 UTC} 76 | ``` 77 | 78 | ### Marshal 79 | 80 | Marshal is using the Go std [csv.Writer](https://golang.org/pkg/encoding/csv/#Writer) with its default options. Use [Encoder](https://godoc.org/github.com/jszwec/csvutil#Encoder) for streaming or to use a different Writer. 81 | 82 | ```go 83 | type Address struct { 84 | City string 85 | Country string 86 | } 87 | 88 | type User struct { 89 | Name string 90 | Address 91 | Age int `csv:"age,omitempty"` 92 | CreatedAt time.Time 93 | } 94 | 95 | users := []User{ 96 | { 97 | Name: "John", 98 | Address: Address{"Boston", "USA"}, 99 | Age: 26, 100 | CreatedAt: time.Date(2010, 6, 2, 12, 0, 0, 0, time.UTC), 101 | }, 102 | { 103 | Name: "Alice", 104 | Address: Address{"SF", "USA"}, 105 | }, 106 | } 107 | 108 | b, err := csvutil.Marshal(users) 109 | if err != nil { 110 | fmt.Println("error:", err) 111 | } 112 | fmt.Println(string(b)) 113 | 114 | // Output: 115 | // Name,City,Country,age,CreatedAt 116 | // John,Boston,USA,26,2010-06-02T12:00:00Z 117 | // Alice,SF,USA,,0001-01-01T00:00:00Z 118 | ``` 119 | 120 | ### Unmarshal and metadata 121 | 122 | It may happen that your CSV input will not always have the same header. In addition 123 | to your base fields you may get extra metadata that you would still like to store. 124 | [Decoder](https://godoc.org/github.com/jszwec/csvutil#Decoder) provides 125 | [Unused](https://godoc.org/github.com/jszwec/csvutil#Decoder.Unused) method, which after each call to 126 | [Decode](https://godoc.org/github.com/jszwec/csvutil#Decoder.Decode) can report which header indexes 127 | were not used during decoding. Based on that, it is possible to handle and store all these extra values. 128 | 129 | ```go 130 | type User struct { 131 | Name string `csv:"name"` 132 | City string `csv:"city"` 133 | Age int `csv:"age"` 134 | OtherData map[string]string `csv:"-"` 135 | } 136 | 137 | csvReader := csv.NewReader(strings.NewReader(` 138 | name,age,city,zip 139 | alice,25,la,90005 140 | bob,30,ny,10005`)) 141 | 142 | dec, err := csvutil.NewDecoder(csvReader) 143 | if err != nil { 144 | log.Fatal(err) 145 | } 146 | 147 | header := dec.Header() 148 | var users []User 149 | for { 150 | u := User{OtherData: make(map[string]string)} 151 | 152 | if err := dec.Decode(&u); err == io.EOF { 153 | break 154 | } else if err != nil { 155 | log.Fatal(err) 156 | } 157 | 158 | for _, i := range dec.Unused() { 159 | u.OtherData[header[i]] = dec.Record()[i] 160 | } 161 | users = append(users, u) 162 | } 163 | 164 | fmt.Println(users) 165 | 166 | // Output: 167 | // [{alice la 25 map[zip:90005]} {bob ny 30 map[zip:10005]}] 168 | ``` 169 | 170 | ### But my CSV file has no header... 171 | 172 | Some CSV files have no header, but if you know how it should look like, it is 173 | possible to define a struct and generate it. All that is left to do, is to pass 174 | it to a decoder. 175 | 176 | ```go 177 | type User struct { 178 | ID int 179 | Name string 180 | Age int `csv:",omitempty"` 181 | City string 182 | } 183 | 184 | csvReader := csv.NewReader(strings.NewReader(` 185 | 1,John,27,la 186 | 2,Bob,,ny`)) 187 | 188 | // in real application this should be done once in init function. 189 | userHeader, err := csvutil.Header(User{}, "csv") 190 | if err != nil { 191 | log.Fatal(err) 192 | } 193 | 194 | dec, err := csvutil.NewDecoder(csvReader, userHeader...) 195 | if err != nil { 196 | log.Fatal(err) 197 | } 198 | 199 | var users []User 200 | for { 201 | var u User 202 | if err := dec.Decode(&u); err == io.EOF { 203 | break 204 | } else if err != nil { 205 | log.Fatal(err) 206 | } 207 | users = append(users, u) 208 | } 209 | 210 | fmt.Printf("%+v", users) 211 | 212 | // Output: 213 | // [{ID:1 Name:John Age:27 City:la} {ID:2 Name:Bob Age:0 City:ny}] 214 | ``` 215 | 216 | ### Decoder.Map - data normalization 217 | 218 | The Decoder's [Map](https://godoc.org/github.com/jszwec/csvutil#Decoder.Map) function is a powerful tool that can help clean up or normalize 219 | the incoming data before the actual decoding takes place. 220 | 221 | Lets say we want to decode some floats and the csv input contains some NaN values, but these values are represented by the 'n/a' string. An attempt to decode 'n/a' into float will end up with error, because strconv.ParseFloat expects 'NaN'. Knowing that, we can implement a Map function that will normalize our 'n/a' string and turn it to 'NaN' only for float types. 222 | 223 | ```go 224 | dec, err := csvutil.NewDecoder(r) 225 | if err != nil { 226 | log.Fatal(err) 227 | } 228 | 229 | dec.Map = func(field, column string, v any) string { 230 | if _, ok := v.(float64); ok && field == "n/a" { 231 | return "NaN" 232 | } 233 | return field 234 | } 235 | ``` 236 | 237 | Now our float64 fields will be decoded properly into NaN. What about float32, float type aliases and other NaN formats? Look at the full example [here](https://gist.github.com/jszwec/2bb94f8f3612e0162eb16003701f727e). 238 | 239 | ### Different separator/delimiter 240 | 241 | Some files may use different value separators, for example TSV files would use `\t`. The following examples show how to set up a Decoder and Encoder for such use case. 242 | 243 | #### Decoder: 244 | ```go 245 | csvReader := csv.NewReader(r) 246 | csvReader.Comma = '\t' 247 | 248 | dec, err := csvutil.NewDecoder(csvReader) 249 | if err != nil { 250 | log.Fatal(err) 251 | } 252 | 253 | var users []User 254 | for { 255 | var u User 256 | if err := dec.Decode(&u); err == io.EOF { 257 | break 258 | } else if err != nil { 259 | log.Fatal(err) 260 | } 261 | users = append(users, u) 262 | } 263 | 264 | ``` 265 | 266 | #### Encoder: 267 | ```go 268 | var buf bytes.Buffer 269 | 270 | w := csv.NewWriter(&buf) 271 | w.Comma = '\t' 272 | enc := csvutil.NewEncoder(w) 273 | 274 | for _, u := range users { 275 | if err := enc.Encode(u); err != nil { 276 | log.Fatal(err) 277 | } 278 | } 279 | 280 | w.Flush() 281 | if err := w.Error(); err != nil { 282 | log.Fatal(err) 283 | } 284 | ``` 285 | 286 | ### Custom Types and Overrides 287 | 288 | There are multiple ways to customize or override your type's behavior. 289 | 290 | 1. a type implements [csvutil.Marshaler](https://pkg.go.dev/github.com/jszwec/csvutil#Marshaler) and/or [csvutil.Unmarshaler](https://pkg.go.dev/github.com/jszwec/csvutil#Unmarshaler) 291 | ```go 292 | type Foo int64 293 | 294 | func (f Foo) MarshalCSV() ([]byte, error) { 295 | return strconv.AppendInt(nil, int64(f), 16), nil 296 | } 297 | 298 | func (f *Foo) UnmarshalCSV(data []byte) error { 299 | i, err := strconv.ParseInt(string(data), 16, 64) 300 | if err != nil { 301 | return err 302 | } 303 | *f = Foo(i) 304 | return nil 305 | } 306 | ``` 307 | 2. a type implements [encoding.TextUnmarshaler](https://golang.org/pkg/encoding/#TextUnmarshaler) and/or [encoding.TextMarshaler](https://golang.org/pkg/encoding/#TextMarshaler) 308 | ```go 309 | type Foo int64 310 | 311 | func (f Foo) MarshalText() ([]byte, error) { 312 | return strconv.AppendInt(nil, int64(f), 16), nil 313 | } 314 | 315 | func (f *Foo) UnmarshalText(data []byte) error { 316 | i, err := strconv.ParseInt(string(data), 16, 64) 317 | if err != nil { 318 | return err 319 | } 320 | *f = Foo(i) 321 | return nil 322 | } 323 | ``` 324 | 3. a type is registered using [Encoder.WithMarshalers](https://pkg.go.dev/github.com/jszwec/csvutil#Encoder.WithMarshalers) and/or [Decoder.WithUnmarshalers](https://pkg.go.dev/github.com/jszwec/csvutil#Decoder.WithUnmarshalers) 325 | ```go 326 | type Foo int64 327 | 328 | enc.WithMarshalers( 329 | csvutil.MarshalFunc(func(f Foo) ([]byte, error) { 330 | return strconv.AppendInt(nil, int64(f), 16), nil 331 | }), 332 | ) 333 | 334 | dec.WithUnmarshalers( 335 | csvutil.UnmarshalFunc(func(data []byte, f *Foo) error { 336 | v, err := strconv.ParseInt(string(data), 16, 64) 337 | if err != nil { 338 | return err 339 | } 340 | *f = Foo(v) 341 | return nil 342 | }), 343 | ) 344 | ``` 345 | 4. a type implements an interface that was registered using [Encoder.WithMarshalers](https://pkg.go.dev/github.com/jszwec/csvutil#Encoder.WithMarshalers) and/or [Decoder.WithUnmarshalers](https://pkg.go.dev/github.com/jszwec/csvutil#Decoder.WithUnmarshalers) 346 | ```go 347 | type Foo int64 348 | 349 | func (f Foo) String() string { 350 | return strconv.FormatInt(int64(f), 16) 351 | } 352 | 353 | func (f *Foo) Scan(state fmt.ScanState, verb rune) error { 354 | // too long; look here: https://github.com/jszwec/csvutil/blob/master/example_decoder_register_test.go#L19 355 | } 356 | 357 | enc.WithMarshalers( 358 | csvutil.MarshalFunc(func(s fmt.Stringer) ([]byte, error) { 359 | return []byte(s.String()), nil 360 | }), 361 | ) 362 | 363 | dec.WithUnmarshalers( 364 | csvutil.UnmarshalFunc(func(data []byte, s fmt.Scanner) error { 365 | _, err := fmt.Sscan(string(data), s) 366 | return err 367 | }), 368 | ) 369 | ``` 370 | 371 | The order of precedence for both Encoder and Decoder is: 372 | 1. type is registered 373 | 2. type implements an interface that was registered 374 | 3. csvutil.{Un,M}arshaler 375 | 4. encoding.Text{Un,M}arshaler 376 | 377 | For more examples look [here](https://pkg.go.dev/github.com/jszwec/csvutil?readme=expanded#pkg-examples) 378 | 379 | ### Custom time.Time format 380 | 381 | Type [time.Time](https://golang.org/pkg/time/#Time) can be used as is in the struct fields by both Decoder and Encoder 382 | due to the fact that both have builtin support for [encoding.TextUnmarshaler](https://golang.org/pkg/encoding/#TextUnmarshaler) and [encoding.TextMarshaler](https://golang.org/pkg/encoding/#TextMarshaler). This means that by default 383 | Time has a specific format; look at [MarshalText](https://golang.org/pkg/time/#Time.MarshalText) and [UnmarshalText](https://golang.org/pkg/time/#Time.UnmarshalText). There are two ways to override it, which one you choose depends on your use case: 384 | 385 | 1. Via Register func (based on encoding/json) 386 | ```go 387 | const format = "2006/01/02 15:04:05" 388 | 389 | marshalTime := func(t time.Time) ([]byte, error) { 390 | return t.AppendFormat(nil, format), nil 391 | } 392 | 393 | unmarshalTime := func(data []byte, t *time.Time) error { 394 | tt, err := time.Parse(format, string(data)) 395 | if err != nil { 396 | return err 397 | } 398 | *t = tt 399 | return nil 400 | } 401 | 402 | enc := csvutil.NewEncoder(w) 403 | enc.Register(marshalTime) 404 | 405 | dec, err := csvutil.NewDecoder(r) 406 | if err != nil { 407 | return err 408 | } 409 | dec.Register(unmarshalTime) 410 | ``` 411 | 412 | 2. With custom type: 413 | ```go 414 | type Time struct { 415 | time.Time 416 | } 417 | 418 | const format = "2006/01/02 15:04:05" 419 | 420 | func (t Time) MarshalCSV() ([]byte, error) { 421 | var b [len(format)]byte 422 | return t.AppendFormat(b[:0], format), nil 423 | } 424 | 425 | func (t *Time) UnmarshalCSV(data []byte) error { 426 | tt, err := time.Parse(format, string(data)) 427 | if err != nil { 428 | return err 429 | } 430 | *t = Time{Time: tt} 431 | return nil 432 | } 433 | ``` 434 | 435 | ### Custom struct tags 436 | 437 | Like in other Go encoding packages struct field tags can be used to set 438 | custom names or options. By default encoders and decoders are looking at `csv` tag. 439 | However, this can be overriden by manually setting the Tag field. 440 | 441 | ```go 442 | type Foo struct { 443 | Bar int `custom:"bar"` 444 | } 445 | ``` 446 | 447 | ```go 448 | dec, err := csvutil.NewDecoder(r) 449 | if err != nil { 450 | log.Fatal(err) 451 | } 452 | dec.Tag = "custom" 453 | ``` 454 | 455 | ```go 456 | enc := csvutil.NewEncoder(w) 457 | enc.Tag = "custom" 458 | ``` 459 | 460 | ### Slice and Map fields 461 | 462 | There is no default encoding/decoding support for slice and map fields because there is no CSV spec for such values. 463 | In such case, it is recommended to create a custom type alias and implement Marshaler and Unmarshaler interfaces. 464 | Please note that slice and map aliases behave differently than aliases of other types - there is no need for type casting. 465 | 466 | ```go 467 | type Strings []string 468 | 469 | func (s Strings) MarshalCSV() ([]byte, error) { 470 | return []byte(strings.Join(s, ",")), nil // strings.Join takes []string but it will also accept Strings 471 | } 472 | 473 | type StringMap map[string]string 474 | 475 | func (sm StringMap) MarshalCSV() ([]byte, error) { 476 | return []byte(fmt.Sprint(sm)), nil 477 | } 478 | 479 | func main() { 480 | b, err := csvutil.Marshal([]struct { 481 | Strings Strings `csv:"strings"` 482 | Map StringMap `csv:"map"` 483 | }{ 484 | {[]string{"a", "b"}, map[string]string{"a": "1"}}, // no type casting is required for slice and map aliases 485 | {Strings{"c", "d"}, StringMap{"b": "1"}}, 486 | }) 487 | 488 | if err != nil { 489 | log.Fatal(err) 490 | } 491 | 492 | fmt.Printf("%s\n", b) 493 | 494 | // Output: 495 | // strings,map 496 | // "a,b",map[a:1] 497 | // "c,d",map[b:1] 498 | } 499 | ``` 500 | 501 | ### Nested/Embedded structs 502 | 503 | Both Encoder and Decoder support nested or embedded structs. 504 | 505 | Playground: https://play.golang.org/p/ZySjdVkovbf 506 | 507 | ```go 508 | package main 509 | 510 | import ( 511 | "fmt" 512 | 513 | "github.com/jszwec/csvutil" 514 | ) 515 | 516 | type Address struct { 517 | Street string `csv:"street"` 518 | City string `csv:"city"` 519 | } 520 | 521 | type User struct { 522 | Name string `csv:"name"` 523 | Address 524 | } 525 | 526 | func main() { 527 | users := []User{ 528 | { 529 | Name: "John", 530 | Address: Address{ 531 | Street: "Boylston", 532 | City: "Boston", 533 | }, 534 | }, 535 | } 536 | 537 | b, err := csvutil.Marshal(users) 538 | if err != nil { 539 | panic(err) 540 | } 541 | 542 | fmt.Printf("%s\n", b) 543 | 544 | var out []User 545 | if err := csvutil.Unmarshal(b, &out); err != nil { 546 | panic(err) 547 | } 548 | 549 | fmt.Printf("%+v\n", out) 550 | 551 | // Output: 552 | // 553 | // name,street,city 554 | // John,Boylston,Boston 555 | // 556 | // [{Name:John Address:{Street:Boylston City:Boston}}] 557 | } 558 | ``` 559 | 560 | ### Inline tag 561 | 562 | Fields with inline tag behave similarly to embedded struct fields. However, 563 | it gives a possibility to specify the prefix for all underlying fields. This 564 | can be useful when one structure can define multiple CSV columns because they 565 | are different from each other only by a certain prefix. Look at the example below. 566 | 567 | Playground: https://play.golang.org/p/jyEzeskSnj7 568 | 569 | ```go 570 | package main 571 | 572 | import ( 573 | "fmt" 574 | 575 | "github.com/jszwec/csvutil" 576 | ) 577 | 578 | func main() { 579 | type Address struct { 580 | Street string `csv:"street"` 581 | City string `csv:"city"` 582 | } 583 | 584 | type User struct { 585 | Name string `csv:"name"` 586 | Address Address `csv:",inline"` 587 | HomeAddress Address `csv:"home_address_,inline"` 588 | WorkAddress Address `csv:"work_address_,inline"` 589 | Age int `csv:"age,omitempty"` 590 | } 591 | 592 | users := []User{ 593 | { 594 | Name: "John", 595 | Address: Address{"Washington", "Boston"}, 596 | HomeAddress: Address{"Boylston", "Boston"}, 597 | WorkAddress: Address{"River St", "Cambridge"}, 598 | Age: 26, 599 | }, 600 | } 601 | 602 | b, err := csvutil.Marshal(users) 603 | if err != nil { 604 | fmt.Println("error:", err) 605 | } 606 | 607 | fmt.Printf("%s\n", b) 608 | 609 | // Output: 610 | // name,street,city,home_address_street,home_address_city,work_address_street,work_address_city,age 611 | // John,Washington,Boston,Boylston,Boston,River St,Cambridge,26 612 | } 613 | ``` 614 | 615 | Performance 616 | ------------ 617 | 618 | csvutil provides the best encoding and decoding performance with small memory usage. 619 | 620 | ### Unmarshal 621 | 622 | [benchmark code](https://gist.github.com/jszwec/e8515e741190454fa3494bcd3e1f100f) 623 | 624 | #### csvutil: 625 | ``` 626 | BenchmarkUnmarshal/csvutil.Unmarshal/1_record-12 280696 4516 ns/op 7332 B/op 26 allocs/op 627 | BenchmarkUnmarshal/csvutil.Unmarshal/10_records-12 95750 11517 ns/op 8356 B/op 35 allocs/op 628 | BenchmarkUnmarshal/csvutil.Unmarshal/100_records-12 14997 83146 ns/op 18532 B/op 125 allocs/op 629 | BenchmarkUnmarshal/csvutil.Unmarshal/1000_records-12 1485 750143 ns/op 121094 B/op 1025 allocs/op 630 | BenchmarkUnmarshal/csvutil.Unmarshal/10000_records-12 154 7587205 ns/op 1136662 B/op 10025 allocs/op 631 | BenchmarkUnmarshal/csvutil.Unmarshal/100000_records-12 14 76126616 ns/op 11808744 B/op 100025 allocs/op 632 | ``` 633 | 634 | #### gocsv: 635 | ``` 636 | BenchmarkUnmarshal/gocsv.Unmarshal/1_record-12 141330 7499 ns/op 7795 B/op 97 allocs/op 637 | BenchmarkUnmarshal/gocsv.Unmarshal/10_records-12 54252 21664 ns/op 13891 B/op 307 allocs/op 638 | BenchmarkUnmarshal/gocsv.Unmarshal/100_records-12 6920 159662 ns/op 72644 B/op 2380 allocs/op 639 | BenchmarkUnmarshal/gocsv.Unmarshal/1000_records-12 752 1556083 ns/op 650248 B/op 23083 allocs/op 640 | BenchmarkUnmarshal/gocsv.Unmarshal/10000_records-12 72 17086623 ns/op 7017469 B/op 230092 allocs/op 641 | BenchmarkUnmarshal/gocsv.Unmarshal/100000_records-12 7 163610749 ns/op 75004923 B/op 2300105 allocs/op 642 | ``` 643 | 644 | #### easycsv: 645 | ``` 646 | BenchmarkUnmarshal/easycsv.ReadAll/1_record-12 101527 10662 ns/op 8855 B/op 81 allocs/op 647 | BenchmarkUnmarshal/easycsv.ReadAll/10_records-12 23325 51437 ns/op 24072 B/op 391 allocs/op 648 | BenchmarkUnmarshal/easycsv.ReadAll/100_records-12 2402 447296 ns/op 170538 B/op 3454 allocs/op 649 | BenchmarkUnmarshal/easycsv.ReadAll/1000_records-12 272 4370854 ns/op 1595683 B/op 34057 allocs/op 650 | BenchmarkUnmarshal/easycsv.ReadAll/10000_records-12 24 47502457 ns/op 18861808 B/op 340068 allocs/op 651 | BenchmarkUnmarshal/easycsv.ReadAll/100000_records-12 3 468974170 ns/op 189427066 B/op 3400082 allocs/op 652 | ``` 653 | 654 | ### Marshal 655 | 656 | [benchmark code](https://gist.github.com/jszwec/31980321e1852ebb5615a44ccf374f17) 657 | 658 | #### csvutil: 659 | ``` 660 | BenchmarkMarshal/csvutil.Marshal/1_record-12 279558 4390 ns/op 9952 B/op 12 allocs/op 661 | BenchmarkMarshal/csvutil.Marshal/10_records-12 82478 15608 ns/op 10800 B/op 21 allocs/op 662 | BenchmarkMarshal/csvutil.Marshal/100_records-12 10275 117288 ns/op 28208 B/op 112 allocs/op 663 | BenchmarkMarshal/csvutil.Marshal/1000_records-12 1075 1147473 ns/op 168508 B/op 1014 allocs/op 664 | BenchmarkMarshal/csvutil.Marshal/10000_records-12 100 11985382 ns/op 1525973 B/op 10017 allocs/op 665 | BenchmarkMarshal/csvutil.Marshal/100000_records-12 9 113640813 ns/op 22455873 B/op 100021 allocs/op 666 | ``` 667 | 668 | #### gocsv: 669 | ``` 670 | BenchmarkMarshal/gocsv.Marshal/1_record-12 203052 6077 ns/op 5914 B/op 81 allocs/op 671 | BenchmarkMarshal/gocsv.Marshal/10_records-12 50132 24585 ns/op 9284 B/op 360 allocs/op 672 | BenchmarkMarshal/gocsv.Marshal/100_records-12 5480 212008 ns/op 51916 B/op 3151 allocs/op 673 | BenchmarkMarshal/gocsv.Marshal/1000_records-12 514 2053919 ns/op 444506 B/op 31053 allocs/op 674 | BenchmarkMarshal/gocsv.Marshal/10000_records-12 52 21066666 ns/op 4332377 B/op 310064 allocs/op 675 | BenchmarkMarshal/gocsv.Marshal/100000_records-12 5 207408929 ns/op 51169419 B/op 3100077 allocs/op 676 | ``` 677 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman 2 | markdown: GFM 3 | -------------------------------------------------------------------------------- /cache.go: -------------------------------------------------------------------------------- 1 | package csvutil 2 | 3 | import ( 4 | "reflect" 5 | "sort" 6 | "sync" 7 | ) 8 | 9 | var fieldCache sync.Map // map[typeKey][]field 10 | 11 | func cachedFields(k typeKey) fields { 12 | if v, ok := fieldCache.Load(k); ok { 13 | return v.(fields) 14 | } 15 | 16 | v, _ := fieldCache.LoadOrStore(k, buildFields(k)) 17 | return v.(fields) 18 | } 19 | 20 | type field struct { 21 | name string 22 | baseType reflect.Type 23 | typ reflect.Type 24 | tag tag 25 | index []int 26 | } 27 | 28 | type fields []field 29 | 30 | func (fs fields) Len() int { return len(fs) } 31 | 32 | func (fs fields) Swap(i, j int) { fs[i], fs[j] = fs[j], fs[i] } 33 | 34 | func (fs fields) Less(i, j int) bool { 35 | for k, n := range fs[i].index { 36 | if n != fs[j].index[k] { 37 | return n < fs[j].index[k] 38 | } 39 | } 40 | return len(fs[i].index) < len(fs[j].index) 41 | } 42 | 43 | type typeKey struct { 44 | tag string 45 | typ reflect.Type 46 | } 47 | 48 | type fieldMap map[string]fields 49 | 50 | func (m fieldMap) insert(f field) { 51 | fs, ok := m[f.name] 52 | if !ok { 53 | m[f.name] = append(fs, f) 54 | return 55 | } 56 | 57 | // insert only fields with the shortest path. 58 | if len(fs[0].index) != len(f.index) { 59 | return 60 | } 61 | 62 | // fields that are tagged have priority. 63 | if !f.tag.empty { 64 | m[f.name] = append([]field{f}, fs...) 65 | return 66 | } 67 | 68 | m[f.name] = append(fs, f) 69 | } 70 | 71 | func (m fieldMap) fields() fields { 72 | out := make(fields, 0, len(m)) 73 | for _, v := range m { 74 | for i, f := range v { 75 | if f.tag.empty != v[0].tag.empty { 76 | v = v[:i] 77 | break 78 | } 79 | } 80 | if len(v) > 1 { 81 | continue 82 | } 83 | out = append(out, v[0]) 84 | } 85 | sort.Sort(out) 86 | return out 87 | } 88 | 89 | func buildFields(k typeKey) fields { 90 | type key struct { 91 | reflect.Type 92 | tag 93 | } 94 | 95 | q := fields{{typ: k.typ}} 96 | visited := make(map[key]struct{}) 97 | fm := make(fieldMap) 98 | 99 | for len(q) > 0 { 100 | f := q[0] 101 | q = q[1:] 102 | 103 | key := key{f.typ, f.tag} 104 | if _, ok := visited[key]; ok { 105 | continue 106 | } 107 | visited[key] = struct{}{} 108 | 109 | depth := len(f.index) 110 | 111 | numField := f.typ.NumField() 112 | for i := 0; i < numField; i++ { 113 | sf := f.typ.Field(i) 114 | 115 | if sf.PkgPath != "" && !sf.Anonymous { 116 | // unexported field 117 | continue 118 | } 119 | 120 | if sf.Anonymous { 121 | t := sf.Type 122 | if t.Kind() == reflect.Ptr { 123 | t = t.Elem() 124 | } 125 | if sf.PkgPath != "" && t.Kind() != reflect.Struct { 126 | // ignore embedded unexported non-struct fields. 127 | continue 128 | } 129 | } 130 | 131 | tag := parseTag(k.tag, sf) 132 | if tag.ignore { 133 | continue 134 | } 135 | if f.tag.prefix != "" { 136 | tag.prefix = f.tag.prefix + tag.prefix 137 | } 138 | 139 | ft := sf.Type 140 | if ft.Kind() == reflect.Ptr { 141 | ft = ft.Elem() 142 | } 143 | 144 | newf := field{ 145 | name: tag.prefix + tag.name, 146 | baseType: sf.Type, 147 | typ: ft, 148 | tag: tag, 149 | index: makeIndex(f.index, i), 150 | } 151 | 152 | if sf.Anonymous && ft.Kind() == reflect.Struct && tag.empty { 153 | q = append(q, newf) 154 | continue 155 | } 156 | 157 | if tag.inline && ft.Kind() == reflect.Struct { 158 | q = append(q, newf) 159 | continue 160 | } 161 | 162 | fm.insert(newf) 163 | 164 | // look for duplicate nodes on the same level. Nodes won't be 165 | // revisited, so write all fields for the current type now. 166 | for _, v := range q { 167 | if len(v.index) != depth { 168 | break 169 | } 170 | if v.typ == f.typ && v.tag.prefix == tag.prefix { 171 | // other nodes can have different path. 172 | fm.insert(field{ 173 | name: tag.prefix + tag.name, 174 | baseType: sf.Type, 175 | typ: ft, 176 | tag: tag, 177 | index: makeIndex(v.index, i), 178 | }) 179 | } 180 | } 181 | } 182 | } 183 | return fm.fields() 184 | } 185 | 186 | func makeIndex(index []int, v int) []int { 187 | out := make([]int, len(index), len(index)+1) 188 | copy(out, index) 189 | return append(out, v) 190 | } 191 | -------------------------------------------------------------------------------- /csvutil.go: -------------------------------------------------------------------------------- 1 | package csvutil 2 | 3 | import ( 4 | "bytes" 5 | "encoding/csv" 6 | "io" 7 | "reflect" 8 | ) 9 | 10 | const defaultTag = "csv" 11 | 12 | var ( 13 | _bytes = reflect.TypeOf(([]byte)(nil)) 14 | _error = reflect.TypeOf((*error)(nil)).Elem() 15 | ) 16 | 17 | // Unmarshal parses the CSV-encoded data and stores the result in the slice or 18 | // the array pointed to by v. If v is nil or not a pointer to a struct slice or 19 | // struct array, Unmarshal returns an InvalidUnmarshalError. 20 | // 21 | // Unmarshal uses the std encoding/csv.Reader for parsing and csvutil.Decoder 22 | // for populating the struct elements in the provided slice. For exact decoding 23 | // rules look at the Decoder's documentation. 24 | // 25 | // The first line in data is treated as a header. Decoder will use it to map 26 | // csv columns to struct's fields. 27 | // 28 | // In case of success the provided slice will be reinitialized and its content 29 | // fully replaced with decoded data. 30 | func Unmarshal(data []byte, v any) error { 31 | val := reflect.ValueOf(v) 32 | 33 | if val.Kind() != reflect.Ptr || val.IsNil() { 34 | return &InvalidUnmarshalError{Type: reflect.TypeOf(v)} 35 | } 36 | 37 | switch val.Type().Elem().Kind() { 38 | case reflect.Slice, reflect.Array: 39 | default: 40 | return &InvalidUnmarshalError{Type: val.Type()} 41 | } 42 | 43 | typ := val.Type().Elem() 44 | 45 | if walkType(typ.Elem()).Kind() != reflect.Struct { 46 | return &InvalidUnmarshalError{Type: val.Type()} 47 | } 48 | 49 | dec, err := NewDecoder(newCSVReader(bytes.NewReader(data))) 50 | if err == io.EOF { 51 | return nil 52 | } else if err != nil { 53 | return err 54 | } 55 | 56 | // for the array just call decodeArray directly; for slice values call the 57 | // optimized code for better performance. 58 | 59 | if typ.Kind() == reflect.Array { 60 | return dec.decodeArray(val.Elem()) 61 | } 62 | 63 | c := countRecords(data) 64 | slice := reflect.MakeSlice(typ, c, c) 65 | 66 | var i int 67 | for ; ; i++ { 68 | // just in case countRecords counts it wrong. 69 | if i >= c && i >= slice.Len() { 70 | slice = reflect.Append(slice, reflect.New(typ.Elem()).Elem()) 71 | } 72 | 73 | if err := dec.Decode(slice.Index(i).Addr().Interface()); err == io.EOF { 74 | break 75 | } else if err != nil { 76 | return err 77 | } 78 | } 79 | 80 | val.Elem().Set(slice.Slice3(0, i, i)) 81 | return nil 82 | } 83 | 84 | // Marshal returns the CSV encoding of slice or array v. If v is not a slice or 85 | // elements are not structs then Marshal returns InvalidMarshalError. 86 | // 87 | // Marshal uses the std encoding/csv.Writer with its default settings for csv 88 | // encoding. 89 | // 90 | // Marshal will always encode the CSV header even for the empty slice. 91 | // 92 | // For the exact encoding rules look at Encoder.Encode method. 93 | func Marshal(v any) ([]byte, error) { 94 | val := walkValue(reflect.ValueOf(v)) 95 | 96 | if !val.IsValid() { 97 | return nil, &InvalidMarshalError{} 98 | } 99 | 100 | switch val.Kind() { 101 | case reflect.Array, reflect.Slice: 102 | default: 103 | return nil, &InvalidMarshalError{Type: reflect.ValueOf(v).Type()} 104 | } 105 | 106 | typ := walkType(val.Type().Elem()) 107 | if typ.Kind() != reflect.Struct { 108 | return nil, &InvalidMarshalError{Type: reflect.ValueOf(v).Type()} 109 | } 110 | 111 | var buf bytes.Buffer 112 | w := csv.NewWriter(&buf) 113 | enc := NewEncoder(w) 114 | 115 | if err := enc.encodeHeader(typ); err != nil { 116 | return nil, err 117 | } 118 | 119 | if err := enc.encodeArray(val); err != nil { 120 | return nil, err 121 | } 122 | 123 | w.Flush() 124 | if err := w.Error(); err != nil { 125 | return nil, err 126 | } 127 | return buf.Bytes(), nil 128 | } 129 | 130 | func countRecords(s []byte) (n int) { 131 | var prev byte 132 | inQuote := false 133 | for { 134 | if len(s) == 0 && prev != '"' { 135 | return n 136 | } 137 | 138 | i := bytes.IndexAny(s, "\n\"") 139 | if i == -1 { 140 | return n + 1 141 | } 142 | 143 | switch s[i] { 144 | case '\n': 145 | if !inQuote && (i > 0 || prev == '"') { 146 | n++ 147 | } 148 | case '"': 149 | inQuote = !inQuote 150 | } 151 | 152 | prev = s[i] 153 | s = s[i+1:] 154 | } 155 | } 156 | 157 | // Header scans the provided struct type, struct slice or struct array and generates a CSV header for it. 158 | // 159 | // Field names are written in the same order as struct fields are defined. 160 | // Embedded struct's fields are treated as if they were part of the outer struct. 161 | // Fields that are embedded types and that are tagged are treated like any 162 | // other field. 163 | // 164 | // Unexported fields and fields with tag "-" are ignored. 165 | // 166 | // Tagged fields have the priority over non tagged fields with the same name. 167 | // 168 | // Following the Go visibility rules if there are multiple fields with the same 169 | // name (tagged or not tagged) on the same level and choice between them is 170 | // ambiguous, then all these fields will be ignored. 171 | // 172 | // It is a good practice to call Header once for each type. The suitable place 173 | // for calling it is init function. Look at Decoder.DecodingDataWithNoHeader 174 | // example. 175 | // 176 | // If tag is left empty the default "csv" will be used. 177 | // 178 | // Header will return UnsupportedTypeError if the provided value is nil, is 179 | // not a struct, a struct slice or a struct array. 180 | func Header(v any, tag string) ([]string, error) { 181 | typ, err := valueType(v) 182 | if err != nil { 183 | return nil, err 184 | } 185 | 186 | if tag == "" { 187 | tag = defaultTag 188 | } 189 | 190 | fields := cachedFields(typeKey{tag, typ}) 191 | h := make([]string, len(fields)) 192 | for i, f := range fields { 193 | h[i] = f.name 194 | } 195 | return h, nil 196 | } 197 | 198 | func valueType(v any) (reflect.Type, error) { 199 | val := reflect.ValueOf(v) 200 | if !val.IsValid() { 201 | return nil, &UnsupportedTypeError{} 202 | } 203 | 204 | loop: 205 | for { 206 | switch val.Kind() { 207 | case reflect.Ptr, reflect.Interface: 208 | el := val.Elem() 209 | if !el.IsValid() { 210 | break loop 211 | } 212 | val = el 213 | default: 214 | break loop 215 | } 216 | } 217 | 218 | typ := walkType(val.Type()) 219 | switch typ.Kind() { 220 | case reflect.Struct: 221 | return typ, nil 222 | case reflect.Slice, reflect.Array: 223 | if eTyp := walkType(typ.Elem()); eTyp.Kind() == reflect.Struct { 224 | return eTyp, nil 225 | } 226 | } 227 | return nil, &UnsupportedTypeError{Type: typ} 228 | } 229 | 230 | func newCSVReader(r io.Reader) *csv.Reader { 231 | rr := csv.NewReader(r) 232 | rr.ReuseRecord = true 233 | return rr 234 | } 235 | -------------------------------------------------------------------------------- /csvutil_go117_test.go: -------------------------------------------------------------------------------- 1 | //go:build go1.17 2 | // +build go1.17 3 | 4 | package csvutil 5 | 6 | import ( 7 | "encoding/csv" 8 | "reflect" 9 | "testing" 10 | ) 11 | 12 | // In Go1.17 csv.ParseError.Column became 1-indexed instead of 0-indexed. 13 | // so we need this file for Go 1.17+. 14 | 15 | var testUnmarshalInvalidFirstLineErr = &csv.ParseError{ 16 | StartLine: 1, 17 | Line: 1, 18 | Column: 2, 19 | Err: csv.ErrQuote, 20 | } 21 | 22 | var testUnmarshalInvalidSecondLineErr = &csv.ParseError{ 23 | StartLine: 2, 24 | Line: 2, 25 | Column: 2, 26 | Err: csv.ErrQuote, 27 | } 28 | 29 | var ptrUnexportedEmbeddedDecodeErr = errPtrUnexportedStruct(reflect.TypeOf(new(embedded))) 30 | 31 | func TestUnmarshalGo117(t *testing.T) { 32 | t.Run("unmarshal type error message", func(t *testing.T) { 33 | expected := `csvutil: cannot unmarshal "field" into Go value of type int: field "X" line 3 column 3` 34 | err := Unmarshal([]byte("Y,X\n1,1\n2,field"), &[]A{}) 35 | if err == nil { 36 | t.Fatal("want err not to be nil") 37 | } 38 | if err.Error() != expected { 39 | t.Errorf("want=%s; got %s", expected, err.Error()) 40 | } 41 | }) 42 | } 43 | -------------------------------------------------------------------------------- /csvutil_race_test.go: -------------------------------------------------------------------------------- 1 | //go:build race 2 | // +build race 3 | 4 | package csvutil 5 | 6 | import ( 7 | "bytes" 8 | "encoding/csv" 9 | "io" 10 | "sync" 11 | "testing" 12 | ) 13 | 14 | func TestCacheDataRaces(t *testing.T) { 15 | const routines = 50 16 | const rows = 1000 17 | 18 | v := TypeF{ 19 | Int: 1, 20 | Pint: ptr(2), 21 | Int8: 3, 22 | Pint8: ptr[int8](4), 23 | Int16: 5, 24 | Pint16: ptr[int16](6), 25 | Int32: 7, 26 | Pint32: ptr[int32](8), 27 | Int64: 9, 28 | Pint64: ptr[int64](10), 29 | UInt: 11, 30 | Puint: ptr[uint](12), 31 | Uint8: 13, 32 | Puint8: ptr[uint8](14), 33 | Uint16: 15, 34 | Puint16: ptr[uint16](16), 35 | Uint32: 17, 36 | Puint32: ptr[uint32](18), 37 | Uint64: 19, 38 | Puint64: ptr[uint64](20), 39 | Float32: 21, 40 | Pfloat32: ptr[float32](22), 41 | Float64: 23, 42 | Pfloat64: ptr[float64](24), 43 | String: "25", 44 | PString: ptr("26"), 45 | Bool: true, 46 | Pbool: ptr(true), 47 | V: pptr(100), 48 | Pv: ptr[any](pptr(200)), 49 | Binary: Binary, 50 | PBinary: &Binary, 51 | } 52 | 53 | t.Run("encoding", func(t *testing.T) { 54 | var wg sync.WaitGroup 55 | for i := 0; i < routines; i++ { 56 | tag := "csv" 57 | if i%2 == 0 { 58 | tag = "custom" 59 | } 60 | 61 | wg.Add(1) 62 | go func() { 63 | defer wg.Done() 64 | 65 | var buf bytes.Buffer 66 | w := csv.NewWriter(&buf) 67 | enc := NewEncoder(w) 68 | enc.Tag = tag 69 | for i := 0; i < rows; i++ { 70 | if err := enc.Encode(v); err != nil { 71 | panic(err) 72 | } 73 | } 74 | w.Flush() 75 | }() 76 | } 77 | wg.Wait() 78 | }) 79 | 80 | t.Run("decoding", func(t *testing.T) { 81 | vs := make([]*TypeF, 0, rows) 82 | for i := 0; i < rows; i++ { 83 | vs = append(vs, &v) 84 | } 85 | 86 | data, err := Marshal(vs) 87 | if err != nil { 88 | t.Fatal(err) 89 | } 90 | 91 | var wg sync.WaitGroup 92 | for i := 0; i < routines; i++ { 93 | tag := "csv" 94 | if i%2 == 0 { 95 | tag = "custom" 96 | } 97 | 98 | wg.Add(1) 99 | go func() { 100 | defer wg.Done() 101 | 102 | dec, err := NewDecoder(csv.NewReader(bytes.NewReader(data))) 103 | if err != nil { 104 | t.Fatal(err) 105 | } 106 | dec.Tag = tag 107 | 108 | for { 109 | var val TypeF 110 | if err := dec.Decode(&val); err == io.EOF { 111 | break 112 | } else if err != nil { 113 | panic(err) 114 | } 115 | } 116 | }() 117 | } 118 | wg.Wait() 119 | }) 120 | } 121 | -------------------------------------------------------------------------------- /csvutil_test.go: -------------------------------------------------------------------------------- 1 | package csvutil 2 | 3 | import ( 4 | "errors" 5 | "reflect" 6 | "testing" 7 | ) 8 | 9 | func TestUnmarshal(t *testing.T) { 10 | fixture := []struct { 11 | desc string 12 | src []byte 13 | in any 14 | out any 15 | }{ 16 | { 17 | desc: "type with two records", 18 | src: []byte("String,int\nstring1,1\nstring2,2"), 19 | in: new([]TypeI), 20 | out: &[]TypeI{ 21 | {"string1", 1}, 22 | {"string2", 2}, 23 | }, 24 | }, 25 | { 26 | desc: "pointer types with two records", 27 | src: []byte("String,int\nstring1,1\nstring2,2"), 28 | in: &[]*TypeI{}, 29 | out: &[]*TypeI{ 30 | {"string1", 1}, 31 | {"string2", 2}, 32 | }, 33 | }, 34 | { 35 | desc: "array - two records", 36 | src: []byte("String,int\nstring1,1\nstring2,2"), 37 | in: new([2]TypeI), 38 | out: &[2]TypeI{ 39 | {"string1", 1}, 40 | {"string2", 2}, 41 | }, 42 | }, 43 | { 44 | desc: "array - pointer type with two records", 45 | src: []byte("String,int\nstring1,1\nstring2,2"), 46 | in: &[2]*TypeI{}, 47 | out: &[2]*TypeI{ 48 | {"string1", 1}, 49 | {"string2", 2}, 50 | }, 51 | }, 52 | { 53 | desc: "array - pointer type with two records size three", 54 | src: []byte("String,int\nstring1,1\nstring2,2"), 55 | in: &[3]*TypeI{}, 56 | out: &[3]*TypeI{ 57 | {"string1", 1}, 58 | {"string2", 2}, 59 | nil, 60 | }, 61 | }, 62 | { 63 | desc: "array - pointer type with two records size three - initialized", 64 | src: []byte("String,int\nstring1,1\nstring2,2"), 65 | in: &[3]*TypeI{{}, {}, {}}, 66 | out: &[3]*TypeI{ 67 | {"string1", 1}, 68 | {"string2", 2}, 69 | nil, 70 | }, 71 | }, 72 | { 73 | desc: "array - two records size three", 74 | src: []byte("String,int\nstring1,1\nstring2,2"), 75 | in: new([3]TypeI), 76 | out: &[3]TypeI{ 77 | {"string1", 1}, 78 | {"string2", 2}, 79 | {}, 80 | }, 81 | }, 82 | { 83 | desc: "array - two records size one", 84 | src: []byte("String,int\nstring1,1\nstring2,2"), 85 | in: new([1]TypeI), 86 | out: &[1]TypeI{ 87 | {"string1", 1}, 88 | }, 89 | }, 90 | { 91 | desc: "array - two records size zero", 92 | src: []byte("String,int\nstring1,1\nstring2,2"), 93 | in: new([0]TypeI), 94 | out: &[0]TypeI{}, 95 | }, 96 | { 97 | desc: "quoted input", 98 | src: []byte("\n\n\n\"String\",\"int\"\n\"string1,\n\",\"1\"\n\n\n\n\"string2\",\"2\""), 99 | in: &[]TypeI{}, 100 | out: &[]TypeI{ 101 | {"string1,\n", 1}, 102 | {"string2", 2}, 103 | }, 104 | }, 105 | { 106 | desc: "quoted input - with endline", 107 | src: []byte("\n\n\n\"String\",\"int\"\n\"string1,\n\",\"1\"\n\"string2\",\"2\"\n\n\n"), 108 | in: &[]TypeI{}, 109 | out: &[]TypeI{ 110 | {"string1,\n", 1}, 111 | {"string2", 2}, 112 | }, 113 | }, 114 | { 115 | desc: "header only", 116 | src: []byte("String,int\n"), 117 | in: &[]TypeI{}, 118 | out: &[]TypeI{}, 119 | }, 120 | { 121 | desc: "no data", 122 | src: []byte(""), 123 | in: &[]TypeI{}, 124 | out: &[]TypeI{}, 125 | }, 126 | } 127 | 128 | for _, f := range fixture { 129 | t.Run(f.desc, func(t *testing.T) { 130 | if err := Unmarshal(f.src, f.in); err != nil { 131 | t.Fatalf("want err=nil; got %v", err) 132 | } 133 | 134 | if !reflect.DeepEqual(f.in, f.out) { 135 | t.Errorf("want out=%v; got %v", f.out, f.in) 136 | } 137 | 138 | out := reflect.ValueOf(f.out).Elem() 139 | in := reflect.ValueOf(f.in).Elem() 140 | if cout, cin := out.Cap(), in.Cap(); cout != cin { 141 | t.Errorf("want cap=%d; got %d", cout, cin) 142 | } 143 | }) 144 | } 145 | 146 | t.Run("invalid data", func(t *testing.T) { 147 | type A struct{} 148 | 149 | fixtures := []struct { 150 | desc string 151 | data []byte 152 | err error 153 | }{ 154 | { 155 | desc: "invalid first line", 156 | data: []byte(`"`), 157 | err: testUnmarshalInvalidFirstLineErr, 158 | }, 159 | { 160 | desc: "invalid second line", 161 | data: []byte("line\n\""), 162 | err: testUnmarshalInvalidSecondLineErr, 163 | }, 164 | } 165 | 166 | for _, f := range fixtures { 167 | t.Run(f.desc, func(t *testing.T) { 168 | var a []A 169 | if err := Unmarshal(f.data, &a); !checkErr(f.err, err) { 170 | t.Errorf("want err=%v; got %v", f.err, err) 171 | } 172 | }) 173 | } 174 | }) 175 | 176 | t.Run("test invalid arguments", func(t *testing.T) { 177 | n := 1 178 | 179 | var fixtures = []struct { 180 | desc string 181 | v any 182 | expected string 183 | }{ 184 | {"nil interface", any(nil), "csvutil: Unmarshal(nil)"}, 185 | {"nil", nil, "csvutil: Unmarshal(nil)"}, 186 | {"non pointer struct", struct{}{}, "csvutil: Unmarshal(non-pointer struct {})"}, 187 | {"invalid type double pointer int", (**int)(nil), "csvutil: Unmarshal(invalid type **int)"}, 188 | {"invalid type int", (*int)(nil), "csvutil: Unmarshal(invalid type *int)"}, 189 | {"invalid initialized type int", &n, "csvutil: Unmarshal(invalid type *int)"}, 190 | {"invalid type array of slice", (*[2][]TypeI)(nil), "csvutil: Unmarshal(invalid type *[2][]csvutil.TypeI)"}, 191 | {"double array", &[2][1]TypeI{}, "csvutil: Unmarshal(invalid type *[2][1]csvutil.TypeI)"}, 192 | {"double slice", &[][]TypeI{}, "csvutil: Unmarshal(invalid type *[][]csvutil.TypeI)"}, 193 | {"triple slice", &[][][]TypeI{}, "csvutil: Unmarshal(invalid type *[][][]csvutil.TypeI)"}, 194 | {"double ptr slice", &[]*[]TypeI{}, "csvutil: Unmarshal(invalid type *[]*[]csvutil.TypeI)"}, 195 | {"int slice", &[]int{}, "csvutil: Unmarshal(invalid type *[]int)"}, 196 | } 197 | 198 | for _, f := range fixtures { 199 | t.Run(f.desc, func(t *testing.T) { 200 | err := Unmarshal([]byte(""), f.v) 201 | if err == nil { 202 | t.Fatalf("want err != nil") 203 | } 204 | if got := err.Error(); got != f.expected { 205 | t.Errorf("want err=%s; got %s", f.expected, got) 206 | } 207 | }) 208 | } 209 | }) 210 | } 211 | 212 | func TestCountLines(t *testing.T) { 213 | fixtures := []struct { 214 | desc string 215 | data []byte 216 | out int 217 | }{ 218 | { 219 | desc: "three lines no endline", 220 | data: []byte(`line1,line1 221 | line2,line2, 222 | line3,line3`), 223 | out: 3, 224 | }, 225 | { 226 | desc: "three lines", 227 | data: []byte(`line1,line1 228 | line2,line2 229 | line3,line3 230 | `), 231 | out: 3, 232 | }, 233 | { 234 | desc: "no data", 235 | data: []byte(``), 236 | out: 0, 237 | }, 238 | { 239 | desc: "endline in a quoted string", 240 | data: []byte(`"line 241 | ""1""",line1 242 | line2,"line 243 | 2""" 244 | `), 245 | out: 2, 246 | }, 247 | { 248 | desc: "empty lines", 249 | data: []byte("\n\nline1,line1\n\n\n\nline2,line2\n\n"), 250 | out: 2, 251 | }, 252 | { 253 | desc: "1 line ending with quote", 254 | data: []byte(`"line1","line2"`), 255 | out: 1, 256 | }, 257 | { 258 | desc: "1 line ending with quote - with endline", 259 | data: []byte(`"line1","line2" 260 | `), 261 | out: 1, 262 | }, 263 | { 264 | desc: "2 lines ending with quote", 265 | data: []byte(`"line1","line2" 266 | line2,"line2"`), 267 | out: 2, 268 | }, 269 | } 270 | 271 | for _, f := range fixtures { 272 | t.Run(f.desc, func(t *testing.T) { 273 | if out := countRecords(f.data); out != f.out { 274 | t.Errorf("want=%d; got %d", f.out, out) 275 | } 276 | }) 277 | } 278 | } 279 | 280 | func TestMarshal(t *testing.T) { 281 | fixtures := []struct { 282 | desc string 283 | v any 284 | out [][]string 285 | err error 286 | }{ 287 | { 288 | desc: "slice with basic type", 289 | v: []TypeI{ 290 | {String: "string", Int: 10}, 291 | {String: "", Int: 0}, 292 | }, 293 | out: [][]string{ 294 | {"String", "int"}, 295 | {"string", "10"}, 296 | {"", ""}, 297 | }, 298 | }, 299 | { 300 | desc: "array with basic type", 301 | v: [2]TypeI{ 302 | {String: "string", Int: 10}, 303 | {String: "", Int: 0}, 304 | }, 305 | out: [][]string{ 306 | {"String", "int"}, 307 | {"string", "10"}, 308 | {"", ""}, 309 | }, 310 | }, 311 | { 312 | desc: "slice with pointer type", 313 | v: []*TypeI{ 314 | {String: "string", Int: 10}, 315 | {String: "", Int: 0}, 316 | }, 317 | out: [][]string{ 318 | {"String", "int"}, 319 | {"string", "10"}, 320 | {"", ""}, 321 | }, 322 | }, 323 | { 324 | desc: "array with pointer type", 325 | v: [2]*TypeI{ 326 | {String: "string", Int: 10}, 327 | {String: "", Int: 0}, 328 | }, 329 | out: [][]string{ 330 | {"String", "int"}, 331 | {"string", "10"}, 332 | {"", ""}, 333 | }, 334 | }, 335 | { 336 | desc: "slice pointer", 337 | v: &[]*TypeI{ 338 | {String: "string", Int: 10}, 339 | }, 340 | out: [][]string{ 341 | {"String", "int"}, 342 | {"string", "10"}, 343 | }, 344 | }, 345 | { 346 | desc: "array pointer", 347 | v: &[1]*TypeI{ 348 | {String: "string", Int: 10}, 349 | }, 350 | out: [][]string{ 351 | {"String", "int"}, 352 | {"string", "10"}, 353 | }, 354 | }, 355 | { 356 | desc: "slice pointer wrapped in interface", 357 | v: func() (v any) { 358 | v = &[]*TypeI{ 359 | {String: "string", Int: 10}, 360 | } 361 | return v 362 | }(), 363 | out: [][]string{ 364 | {"String", "int"}, 365 | {"string", "10"}, 366 | }, 367 | }, 368 | { 369 | desc: "array pointer wrapped in interface", 370 | v: func() (v any) { 371 | v = &[1]*TypeI{ 372 | {String: "string", Int: 10}, 373 | } 374 | return v 375 | }(), 376 | out: [][]string{ 377 | {"String", "int"}, 378 | {"string", "10"}, 379 | }, 380 | }, 381 | { 382 | desc: "not a slice or array", 383 | v: int64(1), 384 | err: &InvalidMarshalError{Type: reflect.TypeOf(int64(1))}, 385 | }, 386 | { 387 | desc: "slice of non structs", 388 | v: []int64{1}, 389 | err: &InvalidMarshalError{Type: reflect.TypeOf([]int64{})}, 390 | }, 391 | { 392 | desc: "array of non pointers", 393 | v: [1]int64{1}, 394 | err: &InvalidMarshalError{Type: reflect.TypeOf([1]int64{})}, 395 | }, 396 | { 397 | desc: "nil value", 398 | v: nilIface, 399 | err: &InvalidMarshalError{Type: reflect.TypeOf(nilIface)}, 400 | }, 401 | { 402 | desc: "nil ptr value", 403 | v: nilPtr, 404 | err: &InvalidMarshalError{}, 405 | }, 406 | { 407 | desc: "nil interface ptr value", 408 | v: nilIfacePtr, 409 | err: &InvalidMarshalError{}, 410 | }, 411 | { 412 | desc: "marshal empty slice", 413 | v: []TypeI{}, 414 | out: [][]string{ 415 | {"String", "int"}, 416 | }, 417 | }, 418 | { 419 | desc: "marshal nil slice", 420 | v: []TypeI(nil), 421 | out: [][]string{ 422 | {"String", "int"}, 423 | }, 424 | }, 425 | { 426 | desc: "marshal invalid struct type", 427 | v: []InvalidType(nil), 428 | err: &UnsupportedTypeError{Type: reflect.TypeOf(struct{}{})}, 429 | }, 430 | } 431 | 432 | for _, f := range fixtures { 433 | t.Run(f.desc, func(t *testing.T) { 434 | b, err := Marshal(f.v) 435 | if f.err != nil { 436 | if !checkErr(f.err, err) { 437 | t.Errorf("want err=%v; got %v", f.err, err) 438 | } 439 | return 440 | } else if err != nil { 441 | t.Errorf("want err=nil; got %v", err) 442 | } 443 | 444 | if expected := encodeCSV(t, f.out); string(b) != expected { 445 | t.Errorf("want %s; got %s", expected, string(b)) 446 | } 447 | }) 448 | } 449 | 450 | t.Run("invalid marshal error message", func(t *testing.T) { 451 | fixtures := []struct { 452 | desc string 453 | expected string 454 | v any 455 | }{ 456 | { 457 | desc: "int64", 458 | expected: "csvutil: Marshal(invalid type int64)", 459 | v: int64(1), 460 | }, 461 | { 462 | desc: "*int64", 463 | expected: "csvutil: Marshal(invalid type *int64)", 464 | v: ptr[int64](1), 465 | }, 466 | { 467 | desc: "[]int64", 468 | expected: "csvutil: Marshal(non struct slice []int64)", 469 | v: []int64{}, 470 | }, 471 | { 472 | desc: "[]int64", 473 | expected: "csvutil: Marshal(non struct slice *[]int64)", 474 | v: &[]int64{}, 475 | }, 476 | { 477 | desc: "[2]int64", 478 | expected: "csvutil: Marshal(non struct array *[2]int64)", 479 | v: &[2]int64{}, 480 | }, 481 | { 482 | desc: "[2]*int64", 483 | expected: "csvutil: Marshal(non struct array *[2]*int64)", 484 | v: &[2]*int64{}, 485 | }, 486 | { 487 | desc: "*[][]*TypeI", 488 | expected: "csvutil: Marshal(non struct slice *[][]*csvutil.TypeI)", 489 | v: &[][]*TypeI{{}}, 490 | }, 491 | { 492 | desc: "*[]*[]*TypeI", 493 | expected: "csvutil: Marshal(non struct slice *[]*[]*csvutil.TypeI)", 494 | v: &[]*[]*TypeI{{}}, 495 | }, 496 | { 497 | desc: "*[1][2]*TypeI", 498 | expected: "csvutil: Marshal(non struct array *[1][2]*csvutil.TypeI)", 499 | v: &[1][2]*TypeI{{}}, 500 | }, 501 | { 502 | desc: "*[1]*[2]*TypeI", 503 | expected: "csvutil: Marshal(non struct array *[1]*[2]*csvutil.TypeI)", 504 | v: &[1]*[2]*TypeI{{}}, 505 | }, 506 | { 507 | desc: "[1][2]TypeI", 508 | expected: "csvutil: Marshal(non struct array [1][2]csvutil.TypeI)", 509 | v: [1][2]TypeI{{}}, 510 | }, 511 | { 512 | desc: "nil interface", 513 | expected: "csvutil: Marshal(nil)", 514 | v: nilIface, 515 | }, 516 | { 517 | desc: "nil ptr value", 518 | expected: "csvutil: Marshal(nil)", 519 | v: nilPtr, 520 | }, 521 | { 522 | desc: "nil interface ptr value", 523 | expected: "csvutil: Marshal(nil)", 524 | v: nilIfacePtr, 525 | }, 526 | } 527 | 528 | for _, f := range fixtures { 529 | t.Run(f.desc, func(t *testing.T) { 530 | _, err := Marshal(f.v) 531 | if err == nil { 532 | t.Fatal("want err not to be nil") 533 | } 534 | if err.Error() != f.expected { 535 | t.Errorf("want=%s; got %s", f.expected, err.Error()) 536 | } 537 | }) 538 | } 539 | }) 540 | } 541 | 542 | type TypeJ struct { 543 | String string `csv:"STR" json:"string"` 544 | Int string `csv:"int" json:"-"` 545 | Embedded16 546 | Float string `csv:"float"` 547 | } 548 | 549 | type Embedded16 struct { 550 | Bool bool `json:"bool"` 551 | Uint uint `csv:"-"` 552 | Uint8 uint8 `json:"-"` 553 | } 554 | 555 | func TestHeader(t *testing.T) { 556 | fixture := []struct { 557 | desc string 558 | v any 559 | tag string 560 | header []string 561 | err error 562 | }{ 563 | { 564 | desc: "simple type with default tag", 565 | v: TypeG{}, 566 | tag: "", 567 | header: []string{"String", "Int"}, 568 | }, 569 | { 570 | desc: "simple type", 571 | v: TypeG{}, 572 | tag: "csv", 573 | header: []string{"String", "Int"}, 574 | }, 575 | { 576 | desc: "simple type with ptr value", 577 | v: &TypeG{}, 578 | tag: "csv", 579 | header: []string{"String", "Int"}, 580 | }, 581 | { 582 | desc: "embedded types with conflict", 583 | v: &TypeA{}, 584 | tag: "csv", 585 | header: []string{"string", "bool", "int"}, 586 | }, 587 | { 588 | desc: "embedded type with tag", 589 | v: &TypeB{}, 590 | tag: "csv", 591 | header: []string{"json", "string"}, 592 | }, 593 | { 594 | desc: "embedded ptr type with tag", 595 | v: &TypeD{}, 596 | tag: "csv", 597 | header: []string{"json", "string"}, 598 | }, 599 | { 600 | desc: "embedded ptr type no tag", 601 | v: &TypeC{}, 602 | tag: "csv", 603 | header: []string{"float", "string"}, 604 | }, 605 | { 606 | desc: "type with omitempty tags", 607 | v: TypeI{}, 608 | tag: "csv", 609 | header: []string{"String", "int"}, 610 | }, 611 | { 612 | desc: "embedded with different json tag", 613 | v: TypeJ{}, 614 | tag: "json", 615 | header: []string{"string", "bool", "Uint", "Float"}, 616 | }, 617 | { 618 | desc: "embedded with default csv tag", 619 | v: TypeJ{}, 620 | tag: "csv", 621 | header: []string{"STR", "int", "Bool", "Uint8", "float"}, 622 | }, 623 | { 624 | desc: "not a struct", 625 | v: int(10), 626 | tag: "csv", 627 | err: &UnsupportedTypeError{Type: reflect.TypeOf(int(0))}, 628 | }, 629 | { 630 | desc: "slice", 631 | v: []TypeJ{{}}, 632 | tag: "csv", 633 | header: []string{"STR", "int", "Bool", "Uint8", "float"}, 634 | }, 635 | { 636 | desc: "ptr slice", 637 | v: &[]TypeJ{{}}, 638 | tag: "csv", 639 | header: []string{"STR", "int", "Bool", "Uint8", "float"}, 640 | }, 641 | { 642 | desc: "slice with ptr value", 643 | v: []*TypeJ{{}}, 644 | tag: "csv", 645 | header: []string{"STR", "int", "Bool", "Uint8", "float"}, 646 | }, 647 | { 648 | desc: "slice with non-struct", 649 | v: []int{0}, 650 | tag: "csv", 651 | err: &UnsupportedTypeError{Type: reflect.TypeOf([]int{0})}, 652 | }, 653 | { 654 | desc: "two-dimensional slice", 655 | v: [][]TypeJ{{{}}}, 656 | tag: "csv", 657 | err: &UnsupportedTypeError{Type: reflect.TypeOf([][]TypeJ{{}})}, 658 | }, 659 | { 660 | desc: "array", 661 | v: [1]TypeJ{{}}, 662 | tag: "csv", 663 | header: []string{"STR", "int", "Bool", "Uint8", "float"}, 664 | }, 665 | { 666 | desc: "ptr array", 667 | v: &[1]TypeJ{{}}, 668 | tag: "csv", 669 | header: []string{"STR", "int", "Bool", "Uint8", "float"}, 670 | }, 671 | { 672 | desc: "array with ptr value", 673 | v: [1]*TypeJ{{}}, 674 | tag: "csv", 675 | header: []string{"STR", "int", "Bool", "Uint8", "float"}, 676 | }, 677 | { 678 | desc: "array with non-struct", 679 | v: [1]int{0}, 680 | tag: "csv", 681 | err: &UnsupportedTypeError{Type: reflect.TypeOf([1]int{0})}, 682 | }, 683 | { 684 | desc: "two-dimensional array", 685 | v: [1][1]TypeJ{{{}}}, 686 | tag: "csv", 687 | err: &UnsupportedTypeError{Type: reflect.TypeOf([1][1]TypeJ{{}})}, 688 | }, 689 | { 690 | desc: "nil interface", 691 | v: nilIface, 692 | tag: "csv", 693 | err: &UnsupportedTypeError{}, 694 | }, 695 | { 696 | desc: "circular reference type", 697 | v: &A{}, 698 | tag: "csv", 699 | header: []string{"Y", "X"}, 700 | }, 701 | { 702 | desc: "conflicting fields", 703 | v: &Embedded10{}, 704 | tag: "csv", 705 | header: []string{"Y"}, 706 | }, 707 | { 708 | desc: "inline - simple", 709 | v: &Inline{}, 710 | tag: "csv", 711 | header: []string{ 712 | "int", 713 | "Bool", 714 | "Uint8", 715 | "float", 716 | "prefix-STR", 717 | "prefix-int", 718 | "prefix-Bool", 719 | "prefix-Uint8", 720 | "prefix-float", 721 | "top-string", 722 | "STR", 723 | }, 724 | }, 725 | { 726 | desc: "inline - chain", 727 | v: &Inline5{}, 728 | tag: "csv", 729 | header: []string{"AS", "AAA", "S", "A"}, 730 | }, 731 | { 732 | desc: "inline - top level", 733 | v: &Inline8{}, 734 | tag: "csv", 735 | header: []string{"AA"}, 736 | }, 737 | { 738 | desc: "nil ptr of TypeF", 739 | v: nilPtr, 740 | tag: "csv", 741 | header: []string{ 742 | "int", 743 | "pint", 744 | "int8", 745 | "pint8", 746 | "int16", 747 | "pint16", 748 | "int32", 749 | "pint32", 750 | "int64", 751 | "pint64", 752 | "uint", 753 | "puint", 754 | "uint8", 755 | "puint8", 756 | "uint16", 757 | "puint16", 758 | "uint32", 759 | "puint32", 760 | "uint64", 761 | "puint64", 762 | "float32", 763 | "pfloat32", 764 | "float64", 765 | "pfloat64", 766 | "string", 767 | "pstring", 768 | "bool", 769 | "pbool", 770 | "interface", 771 | "pinterface", 772 | "binary", 773 | "pbinary", 774 | }, 775 | }, 776 | { 777 | desc: "ptr to nil interface ptr of TypeF", 778 | v: &nilIfacePtr, 779 | tag: "csv", 780 | header: []string{ 781 | "int", 782 | "pint", 783 | "int8", 784 | "pint8", 785 | "int16", 786 | "pint16", 787 | "int32", 788 | "pint32", 789 | "int64", 790 | "pint64", 791 | "uint", 792 | "puint", 793 | "uint8", 794 | "puint8", 795 | "uint16", 796 | "puint16", 797 | "uint32", 798 | "puint32", 799 | "uint64", 800 | "puint64", 801 | "float32", 802 | "pfloat32", 803 | "float64", 804 | "pfloat64", 805 | "string", 806 | "pstring", 807 | "bool", 808 | "pbool", 809 | "interface", 810 | "pinterface", 811 | "binary", 812 | "pbinary", 813 | }, 814 | }, 815 | { 816 | desc: "nil interface ptr of TypeF", 817 | v: nilIfacePtr, 818 | tag: "csv", 819 | header: []string{ 820 | "int", 821 | "pint", 822 | "int8", 823 | "pint8", 824 | "int16", 825 | "pint16", 826 | "int32", 827 | "pint32", 828 | "int64", 829 | "pint64", 830 | "uint", 831 | "puint", 832 | "uint8", 833 | "puint8", 834 | "uint16", 835 | "puint16", 836 | "uint32", 837 | "puint32", 838 | "uint64", 839 | "puint64", 840 | "float32", 841 | "pfloat32", 842 | "float64", 843 | "pfloat64", 844 | "string", 845 | "pstring", 846 | "bool", 847 | "pbool", 848 | "interface", 849 | "pinterface", 850 | "binary", 851 | "pbinary", 852 | }, 853 | }, 854 | { 855 | desc: "ptr to nil interface", 856 | v: &nilIface, 857 | err: &UnsupportedTypeError{Type: reflect.ValueOf(&nilIface).Type().Elem()}, 858 | }, 859 | } 860 | 861 | for _, f := range fixture { 862 | t.Run(f.desc, func(t *testing.T) { 863 | h, err := Header(f.v, f.tag) 864 | 865 | if !checkErr(f.err, err) { 866 | t.Errorf("want err=%v; got %v", f.err, err) 867 | } 868 | 869 | if !reflect.DeepEqual(h, f.header) { 870 | t.Errorf("want header=%v; got %v", f.header, h) 871 | } 872 | }) 873 | } 874 | 875 | t.Run("test nil value error message", func(t *testing.T) { 876 | const expected = "csvutil: unsupported type: nil" 877 | h, err := Header(nilIface, "") 878 | if h != nil { 879 | t.Errorf("want h=nil; got %v", h) 880 | } 881 | if err.Error() != expected { 882 | t.Errorf("want err=%s; got %s", expected, err.Error()) 883 | } 884 | }) 885 | } 886 | 887 | func TestParity(t *testing.T) { 888 | type A struct { 889 | Int int 890 | Pint *int 891 | OmitInt int `csv:",omitempty"` 892 | OmitPint *int `csv:",omitempty"` 893 | } 894 | 895 | in := []A{ 896 | { 897 | Int: 0, 898 | Pint: ptr(0), 899 | OmitInt: 0, 900 | OmitPint: ptr(0), 901 | }, 902 | { 903 | Int: 1, 904 | Pint: ptr(1), 905 | OmitInt: 1, 906 | OmitPint: ptr(1), 907 | }, 908 | { 909 | Int: 0, 910 | Pint: nil, 911 | OmitInt: 0, 912 | OmitPint: nil, 913 | }, 914 | } 915 | 916 | b, err := Marshal(in) 917 | if err != nil { 918 | t.Fatalf("want err=nil; got %v", err) 919 | } 920 | 921 | var out []A 922 | if err := Unmarshal(b, &out); err != nil { 923 | t.Fatalf("want err=nil; got %v", err) 924 | } 925 | 926 | if !reflect.DeepEqual(in, out) { 927 | t.Errorf("want out=%v; got %v", in, out) 928 | } 929 | } 930 | 931 | func checkErr(expected, err error) bool { 932 | if expected == err { 933 | return true 934 | } 935 | 936 | eVal := reflect.New(reflect.TypeOf(expected)) 937 | if !errors.As(err, eVal.Interface()) { 938 | return false 939 | } 940 | return reflect.DeepEqual(eVal.Elem().Interface(), expected) 941 | } 942 | -------------------------------------------------------------------------------- /decode.go: -------------------------------------------------------------------------------- 1 | package csvutil 2 | 3 | import ( 4 | "encoding" 5 | "encoding/base64" 6 | "reflect" 7 | "strconv" 8 | ) 9 | 10 | var ( 11 | textUnmarshaler = reflect.TypeOf((*encoding.TextUnmarshaler)(nil)).Elem() 12 | csvUnmarshaler = reflect.TypeOf((*Unmarshaler)(nil)).Elem() 13 | ) 14 | 15 | var intDecoders = map[int]decodeFunc{ 16 | 8: decodeIntN(8), 17 | 16: decodeIntN(16), 18 | 32: decodeIntN(32), 19 | 64: decodeIntN(64), 20 | } 21 | 22 | var uintDecoders = map[int]decodeFunc{ 23 | 8: decodeUintN(8), 24 | 16: decodeUintN(16), 25 | 32: decodeUintN(32), 26 | 64: decodeUintN(64), 27 | } 28 | 29 | var ( 30 | decodeFloat32 = decodeFloatN(32) 31 | decodeFloat64 = decodeFloatN(64) 32 | ) 33 | 34 | type decodeFunc func(s string, v reflect.Value) error 35 | 36 | func decodeFuncValue(f func([]byte, any) error) decodeFunc { 37 | return func(s string, v reflect.Value) error { 38 | return f([]byte(s), v.Interface()) 39 | } 40 | } 41 | 42 | func decodeFuncValuePtr(f func([]byte, any) error) decodeFunc { 43 | return func(s string, v reflect.Value) error { 44 | v = v.Addr() 45 | return f([]byte(s), v.Interface()) 46 | } 47 | } 48 | 49 | func decodeString(s string, v reflect.Value) error { 50 | v.SetString(s) 51 | return nil 52 | } 53 | 54 | func decodeIntN(bits int) decodeFunc { 55 | return func(s string, v reflect.Value) error { 56 | n, err := strconv.ParseInt(s, 10, bits) 57 | if err != nil { 58 | return &UnmarshalTypeError{Value: s, Type: v.Type()} 59 | } 60 | v.SetInt(n) 61 | return nil 62 | } 63 | } 64 | 65 | func decodeUintN(bits int) decodeFunc { 66 | return func(s string, v reflect.Value) error { 67 | n, err := strconv.ParseUint(s, 10, bits) 68 | if err != nil { 69 | return &UnmarshalTypeError{Value: s, Type: v.Type()} 70 | } 71 | v.SetUint(n) 72 | return nil 73 | } 74 | } 75 | 76 | func decodeFloatN(bits int) decodeFunc { 77 | return func(s string, v reflect.Value) error { 78 | n, err := strconv.ParseFloat(s, bits) 79 | if err != nil { 80 | return &UnmarshalTypeError{Value: s, Type: v.Type()} 81 | } 82 | v.SetFloat(n) 83 | return nil 84 | } 85 | } 86 | 87 | func decodeBool(s string, v reflect.Value) error { 88 | b, err := strconv.ParseBool(s) 89 | if err != nil { 90 | return &UnmarshalTypeError{Value: s, Type: v.Type()} 91 | } 92 | v.SetBool(b) 93 | return nil 94 | } 95 | 96 | func decodePtrTextUnmarshaler(s string, v reflect.Value) error { 97 | return decodeTextUnmarshaler(s, v.Addr()) 98 | } 99 | 100 | func decodeTextUnmarshaler(s string, v reflect.Value) error { 101 | return v.Interface().(encoding.TextUnmarshaler).UnmarshalText([]byte(s)) 102 | } 103 | 104 | func decodePtrFieldUnmarshaler(s string, v reflect.Value) error { 105 | return decodeFieldUnmarshaler(s, v.Addr()) 106 | } 107 | 108 | func decodeFieldUnmarshaler(s string, v reflect.Value) error { 109 | return v.Interface().(Unmarshaler).UnmarshalCSV([]byte(s)) 110 | } 111 | 112 | func decodePtr(typ reflect.Type, funcMap map[reflect.Type]func([]byte, any) error, ifaceFuncs []ifaceDecodeFunc) (decodeFunc, error) { 113 | next, err := decodeFn(typ.Elem(), funcMap, ifaceFuncs) 114 | if err != nil { 115 | return nil, err 116 | } 117 | 118 | return func(s string, v reflect.Value) error { 119 | if v.IsNil() { 120 | v.Set(reflect.New(v.Type().Elem())) 121 | } 122 | return next(s, v.Elem()) 123 | }, nil 124 | } 125 | 126 | func decodeInterface(funcMap map[reflect.Type]func([]byte, any) error, ifaceFuncs []ifaceDecodeFunc) decodeFunc { 127 | return func(s string, v reflect.Value) error { 128 | if v.NumMethod() != 0 { 129 | return &UnmarshalTypeError{ 130 | Value: s, 131 | Type: v.Type(), 132 | } 133 | } 134 | 135 | if v.IsNil() { 136 | v.Set(reflect.ValueOf(s)) 137 | return nil 138 | } 139 | 140 | el := walkValue(v) 141 | if !el.CanSet() { 142 | if el.IsValid() { 143 | // we may get a value receiver unmarshalers or registered funcs 144 | // underneath the interface in which case we should call 145 | // Unmarshal/Registered func. 146 | typ := el.Type() 147 | if f, ok := funcMap[typ]; ok { 148 | return decodeFuncValue(f)(s, el) 149 | } 150 | for _, f := range ifaceFuncs { 151 | if typ.AssignableTo(f.argType) { 152 | return decodeFuncValue(f.f)(s, el) 153 | } 154 | } 155 | if typ.Implements(csvUnmarshaler) { 156 | return decodeFieldUnmarshaler(s, el) 157 | } 158 | if typ.Implements(textUnmarshaler) { 159 | return decodeTextUnmarshaler(s, el) 160 | } 161 | } 162 | v.Set(reflect.ValueOf(s)) 163 | return nil 164 | } 165 | 166 | fn, err := decodeFn(el.Type(), funcMap, ifaceFuncs) 167 | if err != nil { 168 | return err 169 | } 170 | return fn(s, el) 171 | } 172 | } 173 | 174 | func decodeBytes(s string, v reflect.Value) error { 175 | b, err := base64.StdEncoding.DecodeString(s) 176 | if err != nil { 177 | return err 178 | } 179 | v.SetBytes(b) 180 | return nil 181 | } 182 | 183 | func decodeFn(typ reflect.Type, funcMap map[reflect.Type]func([]byte, any) error, ifaceFuncs []ifaceDecodeFunc) (decodeFunc, error) { 184 | if f, ok := funcMap[typ]; ok { 185 | return decodeFuncValue(f), nil 186 | } 187 | if f, ok := funcMap[reflect.PtrTo(typ)]; ok { 188 | return decodeFuncValuePtr(f), nil 189 | } 190 | 191 | for _, f := range ifaceFuncs { 192 | if typ.AssignableTo(f.argType) { 193 | return decodeFuncValue(f.f), nil 194 | } 195 | if reflect.PtrTo(typ).AssignableTo(f.argType) { 196 | return decodeFuncValuePtr(f.f), nil 197 | } 198 | } 199 | 200 | if reflect.PtrTo(typ).Implements(csvUnmarshaler) { 201 | return decodePtrFieldUnmarshaler, nil 202 | } 203 | if reflect.PtrTo(typ).Implements(textUnmarshaler) { 204 | return decodePtrTextUnmarshaler, nil 205 | } 206 | 207 | switch typ.Kind() { 208 | case reflect.Ptr: 209 | return decodePtr(typ, funcMap, ifaceFuncs) 210 | case reflect.Interface: 211 | return decodeInterface(funcMap, ifaceFuncs), nil 212 | case reflect.String: 213 | return decodeString, nil 214 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 215 | return intDecoders[typ.Bits()], nil 216 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: 217 | return uintDecoders[typ.Bits()], nil 218 | case reflect.Float32: 219 | return decodeFloat32, nil 220 | case reflect.Float64: 221 | return decodeFloat64, nil 222 | case reflect.Bool: 223 | return decodeBool, nil 224 | case reflect.Slice: 225 | if typ.Elem().Kind() == reflect.Uint8 { 226 | return decodeBytes, nil 227 | } 228 | } 229 | 230 | return nil, &UnsupportedTypeError{Type: typ} 231 | } 232 | -------------------------------------------------------------------------------- /decoder.go: -------------------------------------------------------------------------------- 1 | package csvutil 2 | 3 | import ( 4 | "errors" 5 | "io" 6 | "reflect" 7 | ) 8 | 9 | type decField struct { 10 | columnIndex int 11 | field 12 | decodeFunc 13 | zero any 14 | } 15 | 16 | // A Decoder reads and decodes string records into structs. 17 | type Decoder struct { 18 | // Tag defines which key in the struct field's tag to scan for names and 19 | // options (Default: 'csv'). 20 | Tag string 21 | 22 | // If true, Decoder will return a MissingColumnsError if it discovers 23 | // that any of the columns are missing. This means that a CSV input 24 | // will be required to contain all columns that were defined in the 25 | // provided struct. 26 | DisallowMissingColumns bool 27 | 28 | // AlignRecord will cause Decoder to align returned record slice to the 29 | // header in case Reader returns records of different lengths. 30 | // 31 | // This flag is supposed to work with csv.Reader.FieldsPerRecord set to -1 32 | // which may cause this behavior. 33 | // 34 | // When header is longer than the record, it will populate the missing 35 | // records with an empty string. 36 | // 37 | // When header is shorter than the record, it will slice the record to match 38 | // header's length. 39 | // 40 | // When this flag is used, Decoder will not ever return ErrFieldCount. 41 | AlignRecord bool 42 | 43 | // If not nil, Map is a function that is called for each field in the csv 44 | // record before decoding the data. It allows mapping certain string values 45 | // for specific columns or types to a known format. Decoder calls Map with 46 | // the current column name (taken from header) and a zero non-pointer value 47 | // of a type to which it is going to decode data into. Implementations 48 | // should use type assertions to recognize the type. 49 | // 50 | // The good example of use case for Map is if NaN values are represented by 51 | // eg 'n/a' string, implementing a specific Map function for all floats 52 | // could map 'n/a' back into 'NaN' to allow successful decoding. 53 | // 54 | // Use Map with caution. If the requirements of column or type are not met 55 | // Map should return 'field', since it is the original value that was 56 | // read from the csv input, this would indicate no change. 57 | // 58 | // If struct field is an interface v will be of type string, unless the 59 | // struct field contains a settable pointer value - then v will be a zero 60 | // value of that type. 61 | // 62 | // Map must be set before the first call to Decode and not changed after it. 63 | Map func(field, col string, v any) string 64 | 65 | r Reader 66 | typeKey typeKey 67 | hmap map[string]int 68 | header []string 69 | record []string 70 | cache []decField 71 | unused []int 72 | funcMap map[reflect.Type]func([]byte, any) error 73 | ifaceFuncs []ifaceDecodeFunc 74 | } 75 | 76 | type ifaceDecodeFunc struct { 77 | f func([]byte, any) error 78 | argType reflect.Type 79 | } 80 | 81 | // NewDecoder returns a new decoder that reads from r. 82 | // 83 | // Decoder will match struct fields according to the given header. 84 | // 85 | // If header is empty NewDecoder will read one line and treat it as a header. 86 | // 87 | // Records coming from r must be of the same length as the header. 88 | // 89 | // NewDecoder may return io.EOF if there is no data in r and no header was 90 | // provided by the caller. 91 | func NewDecoder(r Reader, header ...string) (dec *Decoder, err error) { 92 | if len(header) == 0 { 93 | header, err = r.Read() 94 | if err != nil { 95 | return nil, err 96 | } 97 | } 98 | 99 | h := make([]string, len(header)) 100 | copy(h, header) 101 | header = h 102 | 103 | m := make(map[string]int, len(header)) 104 | for i, h := range header { 105 | m[h] = i 106 | } 107 | 108 | return &Decoder{ 109 | r: r, 110 | header: header, 111 | hmap: m, 112 | unused: make([]int, 0, len(header)), 113 | }, nil 114 | } 115 | 116 | // Decode reads the next string record or records from its input and stores it 117 | // in the value pointed to by v which must be a pointer to a struct, struct slice 118 | // or struct array. 119 | // 120 | // Decode matches all exported struct fields based on the header. Struct fields 121 | // can be adjusted by using tags. 122 | // 123 | // The "omitempty" option specifies that the field should be omitted from 124 | // the decoding if record's field is an empty string. 125 | // 126 | // Examples of struct field tags and their meanings: 127 | // 128 | // // Decode matches this field with "myName" header column. 129 | // Field int `csv:"myName"` 130 | // 131 | // // Decode matches this field with "Field" header column. 132 | // Field int 133 | // 134 | // // Decode matches this field with "myName" header column and decoding is not 135 | // // called if record's field is an empty string. 136 | // Field int `csv:"myName,omitempty"` 137 | // 138 | // // Decode matches this field with "Field" header column and decoding is not 139 | // // called if record's field is an empty string. 140 | // Field int `csv:",omitempty"` 141 | // 142 | // // Decode ignores this field. 143 | // Field int `csv:"-"` 144 | // 145 | // // Decode treats this field exactly as if it was an embedded field and 146 | // // matches header columns that start with "my_prefix_" to all fields of this 147 | // // type. 148 | // Field Struct `csv:"my_prefix_,inline"` 149 | // 150 | // // Decode treats this field exactly as if it was an embedded field. 151 | // Field Struct `csv:",inline"` 152 | // 153 | // By default decode looks for "csv" tag, but this can be changed by setting 154 | // Decoder.Tag field. 155 | // 156 | // To Decode into a custom type v must implement csvutil.Unmarshaler or 157 | // encoding.TextUnmarshaler. 158 | // 159 | // Anonymous struct fields with tags are treated like normal fields and they 160 | // must implement csvutil.Unmarshaler or encoding.TextUnmarshaler unless inline 161 | // tag is specified. 162 | // 163 | // Anonymous struct fields without tags are populated just as if they were 164 | // part of the main struct. However, fields in the main struct have bigger 165 | // priority and they are populated first. If main struct and anonymous struct 166 | // field have the same fields, the main struct's fields will be populated. 167 | // 168 | // Fields of type []byte expect the data to be base64 encoded strings. 169 | // 170 | // Float fields are decoded to NaN if a string value is 'NaN'. This check 171 | // is case insensitive. 172 | // 173 | // Interface fields are decoded to strings unless they contain settable pointer 174 | // value. 175 | // 176 | // Pointer fields are decoded to nil if a string value is empty. 177 | // 178 | // If v is a slice, Decode resets it and reads the input until EOF, storing all 179 | // decoded values in the given slice. Decode returns nil on EOF. 180 | // 181 | // If v is an array, Decode reads the input until EOF or until it decodes all 182 | // corresponding array elements. If the input contains less elements than the 183 | // array, the additional Go array elements are set to zero values. Decode 184 | // returns nil on EOF unless there were no records decoded. 185 | // 186 | // Fields with inline tags that have a non-empty prefix must not be cyclic 187 | // structures. Passing such values to Decode will result in an infinite loop. 188 | func (d *Decoder) Decode(v any) (err error) { 189 | val := reflect.ValueOf(v) 190 | if val.Kind() != reflect.Ptr || val.IsNil() { 191 | return &InvalidDecodeError{Type: reflect.TypeOf(v)} 192 | } 193 | 194 | elem := indirect(val.Elem()) 195 | switch elem.Kind() { 196 | case reflect.Struct: 197 | return d.decodeStruct(elem) 198 | case reflect.Slice: 199 | return d.decodeSlice(elem) 200 | case reflect.Array: 201 | return d.decodeArray(elem) 202 | case reflect.Interface, reflect.Invalid: 203 | elem = walkValue(elem) 204 | if elem.Kind() != reflect.Invalid { 205 | return &InvalidDecodeError{Type: elem.Type()} 206 | } 207 | return &InvalidDecodeError{Type: val.Type()} 208 | default: 209 | return &InvalidDecodeError{Type: reflect.PtrTo(elem.Type())} 210 | } 211 | } 212 | 213 | // Record returns the most recently read record. The slice is valid until the 214 | // next call to Decode. 215 | func (d *Decoder) Record() []string { 216 | return d.record 217 | } 218 | 219 | // Header returns the first line that came from the reader, or returns the 220 | // defined header by the caller. 221 | func (d *Decoder) Header() []string { 222 | header := make([]string, len(d.header)) 223 | copy(header, d.header) 224 | return header 225 | } 226 | 227 | // NormalizeHeader applies f to every column in the header. It returns error 228 | // if calling f results in conflicting header columns. 229 | // 230 | // NormalizeHeader must be called before Decode. 231 | func (d *Decoder) NormalizeHeader(f func(string) string) error { 232 | set := make(map[string]int, len(d.header)) 233 | for i, s := range d.header { 234 | set[f(s)] = i 235 | } 236 | 237 | if len(set) != len(d.header) { 238 | return errors.New("csvutil: normalize header results in conflicting columns") 239 | } 240 | 241 | for s, i := range set { 242 | d.header[i] = s 243 | } 244 | d.hmap = set 245 | return nil 246 | } 247 | 248 | // Unused returns a list of column indexes that were not used during decoding 249 | // due to lack of matching struct field. 250 | func (d *Decoder) Unused() []int { 251 | if len(d.unused) == 0 { 252 | return nil 253 | } 254 | 255 | indices := make([]int, len(d.unused)) 256 | copy(indices, d.unused) 257 | return indices 258 | } 259 | 260 | // Register registers a custom decoding function for a concrete type or interface. 261 | // The argument f must be of type: 262 | // 263 | // func([]byte, T) error 264 | // 265 | // T must be a concrete type such as *time.Time, or interface that has at least one 266 | // method. 267 | // 268 | // During decoding, fields are matched by the concrete type first. If match is not 269 | // found then Decoder looks if field implements any of the registered interfaces 270 | // in order they were registered. 271 | // 272 | // Register panics if: 273 | // - f does not match the right signature 274 | // - f is an empty interface 275 | // - f was already registered 276 | // 277 | // Register is based on the encoding/json proposal: 278 | // https://github.com/golang/go/issues/5901. 279 | // 280 | // Deprecated: use UnmarshalFunc function with type parameter instead. The benefits 281 | // are type safety and much better performance. 282 | func (d *Decoder) Register(f any) { 283 | v := reflect.ValueOf(f) 284 | typ := v.Type() 285 | 286 | if typ.Kind() != reflect.Func || 287 | typ.NumIn() != 2 || typ.NumOut() != 1 || 288 | typ.In(0) != _bytes || typ.Out(0) != _error { 289 | panic("csvutil: func must be of type func([]byte, T) error") 290 | } 291 | 292 | argType := typ.In(1) 293 | 294 | if argType.Kind() == reflect.Interface && argType.NumMethod() == 0 { 295 | panic("csvutil: func argument type must not be an empty interface") 296 | } 297 | 298 | if d.funcMap == nil { 299 | d.funcMap = make(map[reflect.Type]func([]byte, any) error) 300 | } 301 | 302 | if _, ok := d.funcMap[argType]; ok { 303 | panic("csvutil: func " + typ.String() + " already registered") 304 | } 305 | 306 | isIface := argType.Kind() == reflect.Interface 307 | isArgPtr := v.Type().In(1).Kind() == reflect.Ptr 308 | 309 | fn := func(data []byte, in any) error { 310 | dst := reflect.ValueOf(in) 311 | 312 | if isIface && !dst.IsValid() { 313 | return &UnmarshalTypeError{Value: string(data), Type: argType} 314 | } 315 | 316 | if !isIface && isArgPtr && dst.Kind() != reflect.Pointer { 317 | dst = dst.Addr() 318 | } 319 | 320 | out := v.Call([]reflect.Value{ 321 | reflect.ValueOf(data), 322 | dst, 323 | }) 324 | err, _ := out[0].Interface().(error) 325 | return err 326 | } 327 | 328 | d.funcMap[argType] = fn 329 | 330 | if argType.Kind() == reflect.Interface { 331 | d.ifaceFuncs = append(d.ifaceFuncs, ifaceDecodeFunc{ 332 | f: fn, 333 | argType: argType, 334 | }) 335 | } 336 | } 337 | 338 | // WithUnmarshalers sets the provided Unmarshalers for the decoder. 339 | // 340 | // WithUnmarshalers is based on the encoding/json proposal: 341 | // https://github.com/golang/go/issues/5901. 342 | func (d *Decoder) WithUnmarshalers(u *Unmarshalers) { 343 | d.funcMap = u.funcMap 344 | d.ifaceFuncs = u.ifaceFuncs 345 | } 346 | 347 | func (d *Decoder) decodeSlice(slice reflect.Value) error { 348 | typ := slice.Type().Elem() 349 | if walkType(typ).Kind() != reflect.Struct { 350 | return &InvalidDecodeError{Type: reflect.PtrTo(slice.Type())} 351 | } 352 | 353 | slice.SetLen(0) 354 | 355 | var c int 356 | for ; ; c++ { 357 | v := reflect.New(typ) 358 | 359 | err := d.decodeStruct(indirect(v)) 360 | if err == io.EOF { 361 | if c == 0 { 362 | return io.EOF 363 | } 364 | break 365 | } 366 | 367 | // we want to ensure that we append this element to the slice even if it 368 | // was partially decoded due to error. This is how JSON pkg does it. 369 | slice.Set(reflect.Append(slice, v.Elem())) 370 | if err != nil { 371 | return err 372 | } 373 | } 374 | 375 | slice.Set(slice.Slice3(0, c, c)) 376 | return nil 377 | } 378 | 379 | func (d *Decoder) decodeArray(v reflect.Value) error { 380 | if walkType(v.Type().Elem()).Kind() != reflect.Struct { 381 | return &InvalidDecodeError{Type: reflect.PtrTo(v.Type())} 382 | } 383 | 384 | l := v.Len() 385 | 386 | var i int 387 | for ; i < l; i++ { 388 | if err := d.decodeStruct(indirect(v.Index(i))); err == io.EOF { 389 | if i == 0 { 390 | return io.EOF 391 | } 392 | break 393 | } else if err != nil { 394 | return err 395 | } 396 | } 397 | 398 | zero := reflect.Zero(v.Type().Elem()) 399 | for i := i; i < l; i++ { 400 | v.Index(i).Set(zero) 401 | } 402 | return nil 403 | } 404 | 405 | func (d *Decoder) decodeStruct(v reflect.Value) (err error) { 406 | d.record, err = d.r.Read() 407 | if err != nil { 408 | return err 409 | } 410 | 411 | if len(d.record) != len(d.header) { 412 | if !d.AlignRecord { 413 | return ErrFieldCount 414 | } 415 | 416 | if len(d.record) > len(d.header) { 417 | d.record = d.record[:len(d.header)] 418 | } else { 419 | d.record = append(d.record, make([]string, len(d.header)-len(d.record))...) 420 | } 421 | } 422 | 423 | return d.unmarshal(d.record, v) 424 | } 425 | 426 | func (d *Decoder) unmarshal(record []string, v reflect.Value) error { 427 | fields, err := d.fields(typeKey{d.tag(), v.Type()}) 428 | if err != nil { 429 | return err 430 | } 431 | 432 | fieldLoop: 433 | for _, f := range fields { 434 | isBlank := record[f.columnIndex] == "" 435 | if f.tag.omitEmpty && isBlank { 436 | continue 437 | } 438 | 439 | fv := v 440 | for n, i := range f.index { 441 | fv = fv.Field(i) 442 | if fv.Kind() == reflect.Ptr { 443 | if fv.IsNil() { 444 | if isBlank && n == len(f.index)-1 { // ensure we are on the leaf. 445 | continue fieldLoop 446 | } 447 | // this can happen if a field is an unexported embedded 448 | // pointer type. In Go prior to 1.10 it was possible to 449 | // set such value because of a bug in the reflect package 450 | // https://github.com/golang/go/issues/21353 451 | if !fv.CanSet() { 452 | return errPtrUnexportedStruct(fv.Type()) 453 | } 454 | fv.Set(reflect.New(fv.Type().Elem())) 455 | } 456 | 457 | if isBlank && n == len(f.index)-1 { // ensure we are on the leaf. 458 | fv.Set(reflect.Zero(fv.Type())) 459 | continue fieldLoop 460 | } 461 | 462 | if n != len(f.index)-1 { 463 | fv = fv.Elem() // walk pointer until we are on the the leaf. 464 | } 465 | } 466 | } 467 | 468 | s := record[f.columnIndex] 469 | if d.Map != nil && f.zero != nil { 470 | zero := f.zero 471 | if fv := walkPtr(fv); fv.Kind() == reflect.Interface && !fv.IsNil() { 472 | if v := walkValue(fv); v.CanSet() { 473 | zero = reflect.Zero(v.Type()).Interface() 474 | } 475 | } 476 | s = d.Map(s, d.header[f.columnIndex], zero) 477 | } 478 | 479 | if err := f.decodeFunc(s, fv); err != nil { 480 | return wrapDecodeError(d.r, d.header[f.columnIndex], f.columnIndex, err) 481 | } 482 | } 483 | return nil 484 | } 485 | 486 | // wrapDecodeError provides the given error with more context such as: 487 | // - column name (field) 488 | // - line number 489 | // - column within record 490 | // 491 | // Line and Column info is available only if the used Reader supports 'FieldPos' 492 | // that is available e.g. in csv.Reader (since Go1.17). 493 | // 494 | // The caller should use errors.As in order to fetch the original error. 495 | func wrapDecodeError(r Reader, field string, fieldIndex int, err error) error { 496 | fp, ok := r.(interface { 497 | FieldPos(fieldIndex int) (line, column int) 498 | }) 499 | if !ok { 500 | return &DecodeError{ 501 | Field: field, 502 | Err: err, 503 | } 504 | } 505 | 506 | l, c := fp.FieldPos(fieldIndex) 507 | 508 | return &DecodeError{ 509 | Field: field, 510 | Line: l, 511 | Column: c, 512 | Err: err, 513 | } 514 | } 515 | 516 | func (d *Decoder) fields(k typeKey) ([]decField, error) { 517 | if k == d.typeKey { 518 | return d.cache, nil 519 | } 520 | 521 | var ( 522 | fields = cachedFields(k) 523 | decFields = make([]decField, 0, len(fields)) 524 | used = make([]bool, len(d.header)) 525 | missingCols []string 526 | ) 527 | for _, f := range fields { 528 | i, ok := d.hmap[f.name] 529 | if !ok { 530 | if d.DisallowMissingColumns { 531 | missingCols = append(missingCols, f.name) 532 | } 533 | continue 534 | } 535 | 536 | fn, err := decodeFn(f.baseType, d.funcMap, d.ifaceFuncs) 537 | if err != nil { 538 | return nil, err 539 | } 540 | 541 | df := decField{ 542 | columnIndex: i, 543 | field: f, 544 | decodeFunc: fn, 545 | } 546 | 547 | if d.Map != nil { 548 | switch f.typ.Kind() { 549 | case reflect.Interface: 550 | df.zero = "" // interface values are decoded to strings 551 | default: 552 | df.zero = reflect.Zero(walkType(f.typ)).Interface() 553 | } 554 | } 555 | 556 | decFields = append(decFields, df) 557 | used[i] = true 558 | } 559 | 560 | if len(missingCols) > 0 { 561 | return nil, &MissingColumnsError{ 562 | Columns: missingCols, 563 | } 564 | } 565 | 566 | d.unused = d.unused[:0] 567 | for i, b := range used { 568 | if !b { 569 | d.unused = append(d.unused, i) 570 | } 571 | } 572 | 573 | d.cache, d.typeKey = decFields, k 574 | return d.cache, nil 575 | } 576 | 577 | func (d *Decoder) tag() string { 578 | if d.Tag == "" { 579 | return defaultTag 580 | } 581 | return d.Tag 582 | } 583 | 584 | func indirect(v reflect.Value) reflect.Value { 585 | for { 586 | switch v.Kind() { 587 | case reflect.Interface: 588 | if v.IsNil() { 589 | return v 590 | } 591 | e := v.Elem() 592 | if e.Kind() == reflect.Ptr && !e.IsNil() { 593 | v = e 594 | continue 595 | } 596 | return v 597 | case reflect.Ptr: 598 | if v.IsNil() { 599 | v.Set(reflect.New(v.Type().Elem())) 600 | } 601 | v = v.Elem() 602 | default: 603 | return v 604 | } 605 | } 606 | } 607 | 608 | // Unmarshalers stores custom unmarshal functions. Unmarshalers is immutable. 609 | // 610 | // Unmarshalers are based on the encoding/json proposal: 611 | // https://github.com/golang/go/issues/5901. 612 | type Unmarshalers struct { 613 | funcMap map[reflect.Type]func([]byte, any) error 614 | ifaceFuncs []ifaceDecodeFunc 615 | } 616 | 617 | // NewUnmarshalers merges the provided Unmarshalers into one and returns it. 618 | // If Unmarshalers contain duplicate function signatures, the one that was 619 | // provided first wins. 620 | func NewUnmarshalers(us ...*Unmarshalers) *Unmarshalers { 621 | out := &Unmarshalers{ 622 | funcMap: make(map[reflect.Type]func([]byte, any) error), 623 | } 624 | 625 | for _, u := range us { 626 | for k, v := range u.funcMap { 627 | if _, ok := out.funcMap[k]; ok { 628 | continue 629 | } 630 | out.funcMap[k] = v 631 | } 632 | out.ifaceFuncs = append(out.ifaceFuncs, u.ifaceFuncs...) 633 | } 634 | 635 | return out 636 | } 637 | 638 | // UnmarshalFunc stores the provided function in Unmarshaler and returns it. 639 | // 640 | // Type Parameter T must be a concrete type such as *time.Time, or interface 641 | // that has at least one method. 642 | // 643 | // During decoding, fields are matched by the concrete type first. If match is not 644 | // found then Decoder looks if field implements any of the registered interfaces 645 | // in order they were registered. 646 | // 647 | // UnmarshalFunc panics if T is an empty interface. 648 | func UnmarshalFunc[T any](f func([]byte, T) error) *Unmarshalers { 649 | var ( 650 | funcMap = make(map[reflect.Type]func([]byte, any) error) 651 | ifaceFuncs []ifaceDecodeFunc 652 | argType = reflect.TypeOf(f).In(1) 653 | isIface = argType.Kind() == reflect.Interface 654 | ) 655 | 656 | fn := func(data []byte, v any) error { 657 | if !isIface { 658 | return f(data, v.(T)) 659 | } 660 | if _, ok := v.(T); !ok { 661 | return &UnmarshalTypeError{Value: string(data), Type: argType} 662 | } 663 | return f(data, v.(T)) 664 | } 665 | 666 | funcMap[argType] = fn 667 | 668 | if argType.Kind() == reflect.Interface { 669 | if argType.NumMethod() == 0 { 670 | panic("csvutil: func argument type must not be an empty interface") 671 | } 672 | 673 | ifaceFuncs = append(ifaceFuncs, ifaceDecodeFunc{ 674 | f: fn, 675 | argType: argType, 676 | }) 677 | } 678 | 679 | return &Unmarshalers{ 680 | funcMap: funcMap, 681 | ifaceFuncs: ifaceFuncs, 682 | } 683 | } 684 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // Package csvutil provides fast and idiomatic mapping between CSV and Go values. 2 | // 3 | // This package does not provide a CSV parser itself, it is based on the Reader and Writer 4 | // interfaces which are implemented by eg. std csv package. This gives a possibility 5 | // of choosing any other CSV writer or reader which may be more performant. 6 | package csvutil 7 | -------------------------------------------------------------------------------- /encode.go: -------------------------------------------------------------------------------- 1 | package csvutil 2 | 3 | import ( 4 | "encoding" 5 | "encoding/base64" 6 | "reflect" 7 | "strconv" 8 | ) 9 | 10 | var ( 11 | textMarshaler = reflect.TypeOf((*encoding.TextMarshaler)(nil)).Elem() 12 | csvMarshaler = reflect.TypeOf((*Marshaler)(nil)).Elem() 13 | ) 14 | 15 | var ( 16 | encodeFloat32 = encodeFloatN(32) 17 | encodeFloat64 = encodeFloatN(64) 18 | ) 19 | 20 | type encodeFunc func(buf []byte, v reflect.Value, omitempty bool) ([]byte, error) 21 | 22 | func nopEncode(buf []byte, _ reflect.Value, _ bool) ([]byte, error) { 23 | return buf, nil 24 | } 25 | 26 | func encodeFuncValue(fn marshalFunc) encodeFunc { 27 | return func(buf []byte, v reflect.Value, omitempty bool) ([]byte, error) { 28 | b, err := fn.f(v.Interface()) 29 | if err != nil { 30 | return nil, err 31 | } 32 | return append(buf, b...), nil 33 | } 34 | } 35 | 36 | func encodeFuncValuePtr(fn marshalFunc) encodeFunc { 37 | return func(buf []byte, v reflect.Value, omitempty bool) ([]byte, error) { 38 | if !v.CanAddr() { 39 | fallback, err := encodeFn(v.Type(), false, nil, nil) 40 | if err != nil { 41 | return nil, err 42 | } 43 | return fallback(buf, v, omitempty) 44 | } 45 | 46 | b, err := fn.f(v.Addr().Interface()) 47 | if err != nil { 48 | return nil, err 49 | } 50 | return append(buf, b...), nil 51 | } 52 | } 53 | 54 | func encodeString(buf []byte, v reflect.Value, omitempty bool) ([]byte, error) { 55 | return append(buf, v.String()...), nil 56 | } 57 | 58 | func encodeInt(buf []byte, v reflect.Value, omitempty bool) ([]byte, error) { 59 | n := v.Int() 60 | if n == 0 && omitempty { 61 | return buf, nil 62 | } 63 | return strconv.AppendInt(buf, n, 10), nil 64 | } 65 | 66 | func encodeUint(buf []byte, v reflect.Value, omitempty bool) ([]byte, error) { 67 | n := v.Uint() 68 | if n == 0 && omitempty { 69 | return buf, nil 70 | } 71 | return strconv.AppendUint(buf, n, 10), nil 72 | } 73 | 74 | func encodeFloatN(bits int) encodeFunc { 75 | return func(buf []byte, v reflect.Value, omitempty bool) ([]byte, error) { 76 | f := v.Float() 77 | if f == 0 && omitempty { 78 | return buf, nil 79 | } 80 | return strconv.AppendFloat(buf, f, 'G', -1, bits), nil 81 | } 82 | } 83 | 84 | func encodeBool(buf []byte, v reflect.Value, omitempty bool) ([]byte, error) { 85 | t := v.Bool() 86 | if !t && omitempty { 87 | return buf, nil 88 | } 89 | return strconv.AppendBool(buf, t), nil 90 | } 91 | 92 | func encodeInterface(funcMap map[reflect.Type]marshalFunc, funcs []marshalFunc) encodeFunc { 93 | return func(buf []byte, v reflect.Value, omitempty bool) ([]byte, error) { 94 | if !v.IsValid() || v.IsNil() || !v.Elem().IsValid() { 95 | return buf, nil 96 | } 97 | 98 | v = v.Elem() 99 | canAddr := v.Kind() == reflect.Ptr 100 | 101 | switch v.Kind() { 102 | case reflect.Ptr, reflect.Interface: 103 | if v.IsNil() { 104 | return buf, nil 105 | } 106 | default: 107 | } 108 | 109 | enc, err := encodeFn(v.Type(), canAddr, funcMap, funcs) 110 | if err != nil { 111 | return nil, err 112 | } 113 | return enc(buf, v, omitempty) 114 | } 115 | } 116 | 117 | func encodePtrMarshaler(buf []byte, v reflect.Value, omitempty bool) ([]byte, error) { 118 | if v.CanAddr() { 119 | return encodeMarshaler(buf, v.Addr(), omitempty) 120 | } 121 | 122 | fallback, err := encodeFn(v.Type(), false, nil, nil) 123 | if err != nil { 124 | return nil, err 125 | } 126 | return fallback(buf, v, omitempty) 127 | } 128 | 129 | func encodeTextMarshaler(buf []byte, v reflect.Value, _ bool) ([]byte, error) { 130 | if v.Kind() == reflect.Ptr && v.IsNil() { 131 | return buf, nil 132 | } 133 | 134 | b, err := v.Interface().(encoding.TextMarshaler).MarshalText() 135 | if err != nil { 136 | return nil, &MarshalerError{Type: v.Type(), MarshalerType: "MarshalText", Err: err} 137 | } 138 | return append(buf, b...), nil 139 | } 140 | 141 | func encodePtrTextMarshaler(buf []byte, v reflect.Value, omitempty bool) ([]byte, error) { 142 | if v.CanAddr() { 143 | return encodeTextMarshaler(buf, v.Addr(), omitempty) 144 | } 145 | 146 | fallback, err := encodeFn(v.Type(), false, nil, nil) 147 | if err != nil { 148 | return nil, err 149 | } 150 | return fallback(buf, v, omitempty) 151 | } 152 | 153 | func encodeMarshaler(buf []byte, v reflect.Value, _ bool) ([]byte, error) { 154 | if v.Kind() == reflect.Ptr && v.IsNil() { 155 | return buf, nil 156 | } 157 | 158 | b, err := v.Interface().(Marshaler).MarshalCSV() 159 | if err != nil { 160 | return nil, &MarshalerError{Type: v.Type(), MarshalerType: "MarshalCSV", Err: err} 161 | } 162 | return append(buf, b...), nil 163 | } 164 | 165 | func encodePtr(typ reflect.Type, canAddr bool, funcMap map[reflect.Type]marshalFunc, funcs []marshalFunc) (encodeFunc, error) { 166 | next, err := encodeFn(typ.Elem(), canAddr, funcMap, funcs) 167 | if err != nil { 168 | return nil, err 169 | } 170 | return func(buf []byte, v reflect.Value, omitempty bool) ([]byte, error) { 171 | if v.IsNil() { 172 | return buf, nil 173 | } 174 | return next(buf, v.Elem(), omitempty) 175 | }, nil 176 | } 177 | 178 | func encodeBytes(buf []byte, v reflect.Value, _ bool) ([]byte, error) { 179 | data := v.Bytes() 180 | 181 | l := len(buf) 182 | buf = append(buf, make([]byte, base64.StdEncoding.EncodedLen(len(data)))...) 183 | base64.StdEncoding.Encode(buf[l:], data) 184 | return buf, nil 185 | } 186 | 187 | func encodeFn(typ reflect.Type, canAddr bool, funcMap map[reflect.Type]marshalFunc, funcs []marshalFunc) (encodeFunc, error) { 188 | if v, ok := funcMap[typ]; ok { 189 | return encodeFuncValue(v), nil 190 | } 191 | 192 | if v, ok := funcMap[reflect.PtrTo(typ)]; ok && canAddr { 193 | return encodeFuncValuePtr(v), nil 194 | } 195 | 196 | for _, v := range funcs { 197 | argType := v.argType 198 | if typ.AssignableTo(argType) { 199 | return encodeFuncValue(v), nil 200 | } 201 | 202 | if canAddr && reflect.PtrTo(typ).AssignableTo(argType) { 203 | return encodeFuncValuePtr(v), nil 204 | } 205 | } 206 | 207 | if typ.Implements(csvMarshaler) { 208 | return encodeMarshaler, nil 209 | } 210 | 211 | if canAddr && reflect.PtrTo(typ).Implements(csvMarshaler) { 212 | return encodePtrMarshaler, nil 213 | } 214 | 215 | if typ.Implements(textMarshaler) { 216 | return encodeTextMarshaler, nil 217 | } 218 | 219 | if canAddr && reflect.PtrTo(typ).Implements(textMarshaler) { 220 | return encodePtrTextMarshaler, nil 221 | } 222 | 223 | switch typ.Kind() { 224 | case reflect.String: 225 | return encodeString, nil 226 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 227 | return encodeInt, nil 228 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: 229 | return encodeUint, nil 230 | case reflect.Float32: 231 | return encodeFloat32, nil 232 | case reflect.Float64: 233 | return encodeFloat64, nil 234 | case reflect.Bool: 235 | return encodeBool, nil 236 | case reflect.Interface: 237 | return encodeInterface(funcMap, funcs), nil 238 | case reflect.Ptr: 239 | return encodePtr(typ, canAddr, funcMap, funcs) 240 | case reflect.Slice: 241 | if typ.Elem().Kind() == reflect.Uint8 { 242 | return encodeBytes, nil 243 | } 244 | } 245 | 246 | return nil, &UnsupportedTypeError{Type: typ} 247 | } 248 | -------------------------------------------------------------------------------- /encoder.go: -------------------------------------------------------------------------------- 1 | package csvutil 2 | 3 | import ( 4 | "reflect" 5 | "sort" 6 | ) 7 | 8 | const defaultBufSize = 4096 9 | 10 | type encField struct { 11 | field 12 | encodeFunc 13 | } 14 | 15 | type encCache struct { 16 | fields []encField 17 | buf []byte 18 | index []int 19 | record []string 20 | } 21 | 22 | func newEncCache(k typeKey, funcMap map[reflect.Type]marshalFunc, funcs []marshalFunc, header []string) (_ *encCache, err error) { 23 | fields := cachedFields(k) 24 | encFields := make([]encField, 0, len(fields)) 25 | 26 | // if header is not empty, we are going to track columns in a set and we will 27 | // track which columns are covered by type fields. 28 | set := make(map[string]bool, len(header)) 29 | for _, s := range header { 30 | set[s] = false 31 | } 32 | 33 | for _, f := range fields { 34 | if _, ok := set[f.name]; len(header) > 0 && !ok { 35 | continue 36 | } 37 | set[f.name] = true 38 | 39 | fn, err := encodeFn(f.baseType, true, funcMap, funcs) 40 | if err != nil { 41 | return nil, err 42 | } 43 | 44 | encFields = append(encFields, encField{ 45 | field: f, 46 | encodeFunc: fn, 47 | }) 48 | } 49 | 50 | if len(header) > 0 { 51 | // look for columns that were defined in a header but are not present 52 | // in the provided data type. In case we find any, we will set it to 53 | // a no-op encoder that always produces an empty column. 54 | for k, b := range set { 55 | if b { 56 | continue 57 | } 58 | encFields = append(encFields, encField{ 59 | field: field{ 60 | name: k, 61 | }, 62 | encodeFunc: nopEncode, 63 | }) 64 | } 65 | 66 | sortEncFields(header, encFields) 67 | } 68 | 69 | return &encCache{ 70 | fields: encFields, 71 | buf: make([]byte, 0, defaultBufSize), 72 | index: make([]int, len(encFields)), 73 | record: make([]string, len(encFields)), 74 | }, nil 75 | } 76 | 77 | // sortEncFields sorts the provided fields according to the given header. 78 | // at this stage header expects to contain matching fields, so both slices 79 | // are expected to be of the same length. 80 | func sortEncFields(header []string, fields []encField) { 81 | set := make(map[string]int, len(header)) 82 | for i, s := range header { 83 | set[s] = i 84 | } 85 | 86 | sort.Slice(fields, func(i, j int) bool { 87 | return set[fields[i].name] < set[fields[j].name] 88 | }) 89 | } 90 | 91 | // Encoder writes structs CSV representations to the output stream. 92 | type Encoder struct { 93 | // Tag defines which key in the struct field's tag to scan for names and 94 | // options (Default: 'csv'). 95 | Tag string 96 | 97 | // If AutoHeader is true, a struct header is encoded during the first call 98 | // to Encode automatically (Default: true). 99 | AutoHeader bool 100 | 101 | w Writer 102 | c *encCache 103 | header []string 104 | noHeader bool 105 | typeKey typeKey 106 | funcMap map[reflect.Type]marshalFunc 107 | ifaceFuncs []marshalFunc 108 | } 109 | 110 | // NewEncoder returns a new encoder that writes to w. 111 | func NewEncoder(w Writer) *Encoder { 112 | return &Encoder{ 113 | w: w, 114 | noHeader: true, 115 | AutoHeader: true, 116 | } 117 | } 118 | 119 | // Register registers a custom encoding function for a concrete type or interface. 120 | // The argument f must be of type: 121 | // 122 | // func(T) ([]byte, error) 123 | // 124 | // T must be a concrete type such as Foo or *Foo, or interface that has at 125 | // least one method. 126 | // 127 | // During encoding, fields are matched by the concrete type first. If match is not 128 | // found then Encoder looks if field implements any of the registered interfaces 129 | // in order they were registered. 130 | // 131 | // Register panics if: 132 | // - f does not match the right signature 133 | // - f is an empty interface 134 | // - f was already registered 135 | // 136 | // Register is based on the encoding/json proposal: 137 | // https://github.com/golang/go/issues/5901. 138 | // 139 | // Deprecated: use MarshalFunc function with type parameter instead. The benefits 140 | // are type safety and much better performance. 141 | func (e *Encoder) Register(f any) { 142 | var ( 143 | fn = reflect.ValueOf(f) 144 | typ = fn.Type() 145 | ) 146 | 147 | if typ.Kind() != reflect.Func || 148 | typ.NumIn() != 1 || typ.NumOut() != 2 || 149 | typ.Out(0) != _bytes || typ.Out(1) != _error { 150 | panic("csvutil: func must be of type func(T) ([]byte, error)") 151 | } 152 | 153 | var ( 154 | argType = typ.In(0) 155 | 156 | isIface = argType.Kind() == reflect.Interface 157 | isPtr = argType.Kind() == reflect.Pointer 158 | ) 159 | 160 | if isIface && argType.NumMethod() == 0 { 161 | panic("csvutil: func argument type must not be an empty interface") 162 | } 163 | 164 | wrappedFn := marshalFunc{ 165 | f: func(val any) ([]byte, error) { 166 | v := reflect.ValueOf(val) 167 | if !v.IsValid() && (isIface || isPtr) { 168 | v = reflect.Zero(argType) 169 | } 170 | 171 | out := fn.Call([]reflect.Value{v}) 172 | err, _ := out[1].Interface().(error) 173 | return out[0].Bytes(), err 174 | }, 175 | argType: typ.In(0), 176 | } 177 | 178 | if e.funcMap == nil { 179 | e.funcMap = make(map[reflect.Type]marshalFunc) 180 | } 181 | 182 | if _, ok := e.funcMap[argType]; ok { 183 | panic("csvutil: func " + typ.String() + " already registered") 184 | } 185 | 186 | e.funcMap[argType] = wrappedFn 187 | 188 | if isIface { 189 | e.ifaceFuncs = append(e.ifaceFuncs, wrappedFn) 190 | } 191 | } 192 | 193 | // SetHeader overrides the provided data type's default header. Fields are 194 | // encoded in the order of the provided header. If a column specified in the 195 | // header doesn't exist in the provided type, it will be encoded as an empty 196 | // column. Fields that are not part of the provided header are ignored. 197 | // Encoder can't guarantee the right order if the provided header contains 198 | // duplicate column names. 199 | // 200 | // SetHeader must be called before EncodeHeader and/or Encode in order to take 201 | // effect. 202 | func (enc *Encoder) SetHeader(header []string) { 203 | cp := make([]string, len(header)) 204 | copy(cp, header) 205 | enc.header = cp 206 | } 207 | 208 | // WithMarshalers sets the provided Marshalers for the encoder. 209 | // 210 | // WithMarshalers are based on the encoding/json proposal: 211 | // https://github.com/golang/go/issues/5901. 212 | func (enc *Encoder) WithMarshalers(m *Marshalers) { 213 | enc.funcMap = m.funcMap 214 | enc.ifaceFuncs = m.ifaceFuncs 215 | } 216 | 217 | // Encode writes the CSV encoding of v to the output stream. The provided 218 | // argument v must be a struct, struct slice or struct array. 219 | // 220 | // Only the exported fields will be encoded. 221 | // 222 | // First call to Encode will write a header unless EncodeHeader was called first 223 | // or AutoHeader is false. Header names can be customized by using tags 224 | // ('csv' by default), otherwise original Field names are used. 225 | // 226 | // If header was provided through SetHeader then it overrides the provided data 227 | // type's default header. Fields are encoded in the order of the provided header. 228 | // If a column specified in the header doesn't exist in the provided type, it will 229 | // be encoded as an empty column. Fields that are not part of the provided header 230 | // are ignored. Encoder can't guarantee the right order if the provided header 231 | // contains duplicate column names. 232 | // 233 | // Header and fields are written in the same order as struct fields are defined. 234 | // Embedded struct's fields are treated as if they were part of the outer struct. 235 | // Fields that are embedded types and that are tagged are treated like any 236 | // other field, but they have to implement Marshaler or encoding.TextMarshaler 237 | // interfaces. 238 | // 239 | // Marshaler interface has the priority over encoding.TextMarshaler. 240 | // 241 | // Tagged fields have the priority over non tagged fields with the same name. 242 | // 243 | // Following the Go visibility rules if there are multiple fields with the same 244 | // name (tagged or not tagged) on the same level and choice between them is 245 | // ambiguous, then all these fields will be ignored. 246 | // 247 | // Nil values will be encoded as empty strings. Same will happen if 'omitempty' 248 | // tag is set, and the value is a default value like 0, false or nil interface. 249 | // 250 | // Bool types are encoded as 'true' or 'false'. 251 | // 252 | // Float types are encoded using strconv.FormatFloat with precision -1 and 'G' 253 | // format. NaN values are encoded as 'NaN' string. 254 | // 255 | // Fields of type []byte are being encoded as base64-encoded strings. 256 | // 257 | // Fields can be excluded from encoding by using '-' tag option. 258 | // 259 | // Examples of struct tags: 260 | // 261 | // // Field appears as 'myName' header in CSV encoding. 262 | // Field int `csv:"myName"` 263 | // 264 | // // Field appears as 'Field' header in CSV encoding. 265 | // Field int 266 | // 267 | // // Field appears as 'myName' header in CSV encoding and is an empty string 268 | // // if Field is 0. 269 | // Field int `csv:"myName,omitempty"` 270 | // 271 | // // Field appears as 'Field' header in CSV encoding and is an empty string 272 | // // if Field is 0. 273 | // Field int `csv:",omitempty"` 274 | // 275 | // // Encode ignores this field. 276 | // Field int `csv:"-"` 277 | // 278 | // // Encode treats this field exactly as if it was an embedded field and adds 279 | // // "my_prefix_" to each field's name. 280 | // Field Struct `csv:"my_prefix_,inline"` 281 | // 282 | // // Encode treats this field exactly as if it was an embedded field. 283 | // Field Struct `csv:",inline"` 284 | // 285 | // Fields with inline tags that have a non-empty prefix must not be cyclic 286 | // structures. Passing such values to Encode will result in an infinite loop. 287 | // 288 | // Encode doesn't flush data. The caller is responsible for calling Flush() if 289 | // the used Writer supports it. 290 | func (e *Encoder) Encode(v any) error { 291 | return e.encode(reflect.ValueOf(v)) 292 | } 293 | 294 | // EncodeHeader writes the CSV header of the provided struct value to the output 295 | // stream. The provided argument v must be a struct value. 296 | // 297 | // The first Encode method call will not write header if EncodeHeader was called 298 | // before it. This method can be called in cases when a data set could be 299 | // empty, but header is desired. 300 | // 301 | // EncodeHeader is like Header function, but it works with the Encoder and writes 302 | // directly to the output stream. Look at Header documentation for the exact 303 | // header encoding rules. 304 | func (e *Encoder) EncodeHeader(v any) error { 305 | typ, err := valueType(v) 306 | if err != nil { 307 | return err 308 | } 309 | return e.encodeHeader(typ) 310 | } 311 | 312 | func (e *Encoder) encode(v reflect.Value) error { 313 | val := walkValue(v) 314 | 315 | if !val.IsValid() { 316 | return &InvalidEncodeError{} 317 | } 318 | 319 | switch val.Kind() { 320 | case reflect.Struct: 321 | return e.encodeStruct(val) 322 | case reflect.Array, reflect.Slice: 323 | if walkType(val.Type().Elem()).Kind() != reflect.Struct { 324 | return &InvalidEncodeError{v.Type()} 325 | } 326 | return e.encodeArray(val) 327 | default: 328 | return &InvalidEncodeError{v.Type()} 329 | } 330 | } 331 | 332 | func (e *Encoder) encodeStruct(v reflect.Value) error { 333 | if e.AutoHeader && e.noHeader { 334 | if err := e.encodeHeader(v.Type()); err != nil { 335 | return err 336 | } 337 | } 338 | return e.marshal(v) 339 | } 340 | 341 | func (e *Encoder) encodeArray(v reflect.Value) error { 342 | l := v.Len() 343 | for i := 0; i < l; i++ { 344 | if err := e.encodeStruct(walkValue(v.Index(i))); err != nil { 345 | return err 346 | } 347 | } 348 | return nil 349 | } 350 | 351 | func (e *Encoder) encodeHeader(typ reflect.Type) error { 352 | fields, _, _, record, err := e.cache(typ) 353 | if err != nil { 354 | return err 355 | } 356 | 357 | for i, f := range fields { 358 | record[i] = f.name 359 | } 360 | 361 | if err := e.w.Write(record); err != nil { 362 | return err 363 | } 364 | 365 | e.noHeader = false 366 | return nil 367 | } 368 | 369 | func (e *Encoder) marshal(v reflect.Value) error { 370 | fields, buf, index, record, err := e.cache(v.Type()) 371 | if err != nil { 372 | return err 373 | } 374 | 375 | for i, f := range fields { 376 | v := walkIndex(v, f.index) 377 | 378 | omitempty := f.tag.omitEmpty 379 | if v.Kind() == reflect.Ptr || v.Kind() == reflect.Interface { 380 | // We should disable omitempty for pointer and interface values, 381 | // because if it's nil we will automatically encode it as an empty 382 | // string. However, the initialized pointer should not be affected, 383 | // even if it's a default value. 384 | omitempty = false 385 | } 386 | 387 | if !v.IsValid() { 388 | index[i] = 0 389 | continue 390 | } 391 | 392 | b, err := f.encodeFunc(buf, v, omitempty) 393 | if err != nil { 394 | return err 395 | } 396 | index[i], buf = len(b)-len(buf), b 397 | } 398 | 399 | out := string(buf) 400 | for i, n := range index { 401 | record[i], out = out[:n], out[n:] 402 | } 403 | e.c.buf = buf[:0] 404 | 405 | return e.w.Write(record) 406 | } 407 | 408 | func (e *Encoder) tag() string { 409 | if e.Tag == "" { 410 | return defaultTag 411 | } 412 | return e.Tag 413 | } 414 | 415 | func (e *Encoder) cache(typ reflect.Type) ([]encField, []byte, []int, []string, error) { 416 | if k := (typeKey{e.tag(), typ}); k != e.typeKey { 417 | c, err := newEncCache(k, e.funcMap, e.ifaceFuncs, e.header) 418 | if err != nil { 419 | return nil, nil, nil, nil, err 420 | } 421 | e.c, e.typeKey = c, k 422 | } 423 | return e.c.fields, e.c.buf[:0], e.c.index, e.c.record, nil 424 | } 425 | 426 | // Marshalers stores custom unmarshal functions. Marshalers are immutable. 427 | // 428 | // Marshalers are based on the encoding/json proposal: 429 | // https://github.com/golang/go/issues/5901. 430 | type Marshalers struct { 431 | funcMap map[reflect.Type]marshalFunc 432 | ifaceFuncs []marshalFunc 433 | } 434 | 435 | // NewMarshalers merges the provided Marshalers into one and returns it. 436 | // If Marshalers contain duplicate function signatures, the one that was 437 | // provided first wins. 438 | func NewMarshalers(ms ...*Marshalers) *Marshalers { 439 | out := &Marshalers{ 440 | funcMap: make(map[reflect.Type]marshalFunc), 441 | } 442 | 443 | for _, u := range ms { 444 | for k, v := range u.funcMap { 445 | if _, ok := out.funcMap[k]; ok { 446 | continue 447 | } 448 | out.funcMap[k] = v 449 | } 450 | out.ifaceFuncs = append(out.ifaceFuncs, u.ifaceFuncs...) 451 | } 452 | 453 | return out 454 | } 455 | 456 | // MarshalFunc stores the provided function in Marshalers and returns it. 457 | // 458 | // T must be a concrete type such as Foo or *Foo, or interface that has at 459 | // least one method. 460 | // 461 | // During encoding, fields are matched by the concrete type first. If match is not 462 | // found then Encoder looks if field implements any of the registered interfaces 463 | // in order they were registered. 464 | // 465 | // UnmarshalFunc panics if T is an empty interface. 466 | func MarshalFunc[T any](f func(T) ([]byte, error)) *Marshalers { 467 | var ( 468 | v = reflect.ValueOf(f) 469 | typ = v.Type() 470 | argType = typ.In(0) 471 | isIface = argType.Kind() == reflect.Interface 472 | ) 473 | 474 | if isIface && argType.NumMethod() == 0 { 475 | panic("csvutil: func argument type must not be an empty interface") 476 | } 477 | 478 | var zero T 479 | wrappedFn := marshalFunc{ 480 | f: func(v any) ([]byte, error) { 481 | if !isIface { 482 | return f(v.(T)) 483 | } 484 | 485 | if v == nil { 486 | return f(zero) 487 | } 488 | return f(v.(T)) 489 | }, 490 | argType: typ.In(0), 491 | } 492 | 493 | funcMap := map[reflect.Type]marshalFunc{ 494 | argType: wrappedFn, 495 | } 496 | 497 | var ifaceFuncs []marshalFunc 498 | if isIface { 499 | ifaceFuncs = []marshalFunc{ 500 | wrappedFn, 501 | } 502 | } 503 | 504 | return &Marshalers{ 505 | funcMap: funcMap, 506 | ifaceFuncs: ifaceFuncs, 507 | } 508 | } 509 | 510 | func walkIndex(v reflect.Value, index []int) reflect.Value { 511 | for _, i := range index { 512 | v = walkPtr(v) 513 | if !v.IsValid() { 514 | return reflect.Value{} 515 | } 516 | v = v.Field(i) 517 | } 518 | return v 519 | } 520 | 521 | func walkPtr(v reflect.Value) reflect.Value { 522 | for v.Kind() == reflect.Ptr { 523 | v = v.Elem() 524 | } 525 | return v 526 | } 527 | 528 | func walkValue(v reflect.Value) reflect.Value { 529 | for { 530 | switch v.Kind() { 531 | case reflect.Ptr, reflect.Interface: 532 | v = v.Elem() 533 | default: 534 | return v 535 | } 536 | } 537 | } 538 | 539 | func walkType(typ reflect.Type) reflect.Type { 540 | for typ.Kind() == reflect.Ptr { 541 | typ = typ.Elem() 542 | } 543 | return typ 544 | } 545 | 546 | type marshalFunc struct { 547 | f func(any) ([]byte, error) 548 | argType reflect.Type 549 | } 550 | -------------------------------------------------------------------------------- /encoder_test.go: -------------------------------------------------------------------------------- 1 | package csvutil 2 | 3 | import ( 4 | "bytes" 5 | "encoding" 6 | "encoding/csv" 7 | "encoding/json" 8 | "errors" 9 | "math" 10 | "reflect" 11 | "strconv" 12 | "testing" 13 | ) 14 | 15 | var Error = errors.New("error") 16 | 17 | var nilIface any 18 | 19 | var nilPtr *TypeF 20 | 21 | var nilIfacePtr any = nilPtr 22 | 23 | type embeddedMap map[string]string 24 | 25 | type Embedded14 Embedded3 26 | 27 | func (e *Embedded14) MarshalCSV() ([]byte, error) { 28 | return json.Marshal(e) 29 | } 30 | 31 | type Embedded15 Embedded3 32 | 33 | func (e *Embedded15) MarshalText() ([]byte, error) { 34 | return json.Marshal(Embedded3(*e)) 35 | } 36 | 37 | type CSVMarshaler struct { 38 | Err error 39 | } 40 | 41 | func (m CSVMarshaler) MarshalCSV() ([]byte, error) { 42 | if m.Err != nil { 43 | return nil, m.Err 44 | } 45 | return []byte("csvmarshaler"), nil 46 | } 47 | 48 | type PtrRecCSVMarshaler int 49 | 50 | func (m *PtrRecCSVMarshaler) MarshalCSV() ([]byte, error) { 51 | return []byte("ptrreccsvmarshaler"), nil 52 | } 53 | 54 | func (m *PtrRecCSVMarshaler) CSV() ([]byte, error) { 55 | return []byte("ptrreccsvmarshaler.CSV"), nil 56 | } 57 | 58 | type PtrRecTextMarshaler int 59 | 60 | func (m *PtrRecTextMarshaler) MarshalText() ([]byte, error) { 61 | return []byte("ptrrectextmarshaler"), nil 62 | } 63 | 64 | type TextMarshaler struct { 65 | Err error 66 | } 67 | 68 | func (m TextMarshaler) MarshalText() ([]byte, error) { 69 | if m.Err != nil { 70 | return nil, m.Err 71 | } 72 | return []byte("textmarshaler"), nil 73 | } 74 | 75 | type CSVTextMarshaler struct { 76 | CSVMarshaler 77 | TextMarshaler 78 | } 79 | 80 | type Inline struct { 81 | J1 TypeJ `csv:",inline"` 82 | J2 TypeJ `csv:"prefix-,inline"` 83 | String string `csv:"top-string"` 84 | String2 string `csv:"STR"` 85 | } 86 | 87 | type Inline2 struct { 88 | S string 89 | A Inline3 `csv:"A,inline"` 90 | B Inline3 `csv:",inline"` 91 | } 92 | 93 | type Inline3 struct { 94 | Inline4 `csv:",inline"` 95 | } 96 | 97 | type Inline4 struct { 98 | A string 99 | } 100 | 101 | type Inline5 struct { 102 | A Inline2 `csv:"A,inline"` 103 | B Inline2 `csv:",inline"` 104 | } 105 | 106 | type Inline6 struct { 107 | A Inline7 `csv:",inline"` 108 | } 109 | 110 | type Inline7 struct { 111 | A *Inline6 `csv:",inline"` 112 | X int 113 | } 114 | 115 | type Inline8 struct { 116 | F *Inline4 `csv:"A,inline"` 117 | AA int 118 | } 119 | 120 | type TypeH struct { 121 | Int int `csv:"int,omitempty"` 122 | Int8 int8 `csv:"int8,omitempty"` 123 | Int16 int16 `csv:"int16,omitempty"` 124 | Int32 int32 `csv:"int32,omitempty"` 125 | Int64 int64 `csv:"int64,omitempty"` 126 | UInt uint `csv:"uint,omitempty"` 127 | Uint8 uint8 `csv:"uint8,omitempty"` 128 | Uint16 uint16 `csv:"uint16,omitempty"` 129 | Uint32 uint32 `csv:"uint32,omitempty"` 130 | Uint64 uint64 `csv:"uint64,omitempty"` 131 | Float32 float32 `csv:"float32,omitempty"` 132 | Float64 float64 `csv:"float64,omitempty"` 133 | String string `csv:"string,omitempty"` 134 | Bool bool `csv:"bool,omitempty"` 135 | V any `csv:"interface,omitempty"` 136 | } 137 | 138 | type TypeM struct { 139 | *TextMarshaler `csv:"text"` 140 | } 141 | 142 | func TestEncoder(t *testing.T) { 143 | fixtures := []struct { 144 | desc string 145 | in []any 146 | regFunc marshalersSlice 147 | out [][]string 148 | err error 149 | }{ 150 | { 151 | desc: "test all types", 152 | in: []any{ 153 | TypeF{ 154 | Int: 1, 155 | Pint: ptr(2), 156 | Int8: 3, 157 | Pint8: ptr[int8](4), 158 | Int16: 5, 159 | Pint16: ptr[int16](6), 160 | Int32: 7, 161 | Pint32: ptr[int32](8), 162 | Int64: 9, 163 | Pint64: ptr[int64](10), 164 | UInt: 11, 165 | Puint: ptr[uint](12), 166 | Uint8: 13, 167 | Puint8: ptr[uint8](14), 168 | Uint16: 15, 169 | Puint16: ptr[uint16](16), 170 | Uint32: 17, 171 | Puint32: ptr[uint32](18), 172 | Uint64: 19, 173 | Puint64: ptr[uint64](20), 174 | Float32: 21, 175 | Pfloat32: ptr[float32](22), 176 | Float64: 23, 177 | Pfloat64: ptr[float64](24), 178 | String: "25", 179 | PString: ptr("26"), 180 | Bool: true, 181 | Pbool: ptr(true), 182 | V: "true", 183 | Pv: ptr[any]("1"), 184 | Binary: Binary, 185 | PBinary: &BinaryLarge, 186 | }, 187 | TypeF{}, 188 | }, 189 | out: [][]string{ 190 | { 191 | "int", "pint", "int8", "pint8", "int16", "pint16", "int32", 192 | "pint32", "int64", "pint64", "uint", "puint", "uint8", "puint8", 193 | "uint16", "puint16", "uint32", "puint32", "uint64", "puint64", 194 | "float32", "pfloat32", "float64", "pfloat64", "string", "pstring", 195 | "bool", "pbool", "interface", "pinterface", "binary", "pbinary", 196 | }, 197 | {"1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", 198 | "12", "13", "14", "15", "16", "17", "18", "19", "20", "21", 199 | "22", "23", "24", "25", "26", "true", "true", "true", "1", 200 | EncodedBinary, EncodedBinaryLarge, 201 | }, 202 | {"0", "", "0", "", "0", "", "0", "", "0", "", "0", "", 203 | "0", "", "0", "", "0", "", "0", "", "0", "", "0", "", "", "", 204 | "false", "", "", "", "", "", 205 | }, 206 | }, 207 | }, 208 | { 209 | desc: "tags and unexported fields", 210 | in: []any{ 211 | TypeG{ 212 | String: "string", 213 | Int: 1, 214 | Float: 3.14, 215 | unexported1: 100, 216 | unexported2: 200, 217 | }, 218 | }, 219 | out: [][]string{ 220 | {"String", "Int"}, 221 | {"string", "1"}, 222 | }, 223 | }, 224 | { 225 | desc: "omitempty tags", 226 | in: []any{ 227 | TypeH{}, 228 | }, 229 | out: [][]string{ 230 | {"int", "int8", "int16", "int32", "int64", "uint", "uint8", "uint16", 231 | "uint32", "uint64", "float32", "float64", "string", "bool", "interface", 232 | }, 233 | {"", "", "", "", "", "", "", "", "", "", "", "", "", "", ""}, 234 | }, 235 | }, 236 | { 237 | desc: "omitempty tags on pointers - non nil default values", 238 | in: []any{ 239 | struct { 240 | Pint *int `csv:",omitempty"` 241 | PPint **int `csv:",omitempty"` 242 | PPint2 **int `csv:",omitempty"` 243 | PString *string `csv:",omitempty"` 244 | PBool *bool `csv:",omitempty"` 245 | Iint *any `csv:",omitempty"` 246 | }{ 247 | ptr(0), 248 | pptr(0), 249 | new(*int), 250 | ptr(""), 251 | ptr(false), 252 | ptr[any](0), 253 | }, 254 | }, 255 | out: [][]string{ 256 | {"Pint", "PPint", "PPint2", "PString", "PBool", "Iint"}, 257 | {"0", "0", "", "", "false", "0"}, 258 | }, 259 | }, 260 | { 261 | desc: "omitempty tags on pointers - nil ptrs", 262 | in: []any{ 263 | struct { 264 | Pint *int `csv:",omitempty"` 265 | PPint **int `csv:",omitempty"` 266 | PString *string `csv:",omitempty"` 267 | PBool *bool `csv:",omitempty"` 268 | Iint *any `csv:",omitempty"` 269 | }{}, 270 | }, 271 | out: [][]string{ 272 | {"Pint", "PPint", "PString", "PBool", "Iint"}, 273 | {"", "", "", "", ""}, 274 | }, 275 | }, 276 | { 277 | desc: "omitempty tags on interfaces - non nil default values", 278 | in: []any{ 279 | struct { 280 | Iint any `csv:",omitempty"` 281 | IPint any `csv:",omitempty"` 282 | }{ 283 | 0, 284 | ptr(0), 285 | }, 286 | struct { 287 | Iint any `csv:",omitempty"` 288 | IPint any `csv:",omitempty"` 289 | }{ 290 | 1, 291 | ptr(1), 292 | }, 293 | }, 294 | out: [][]string{ 295 | {"Iint", "IPint"}, 296 | {"0", "0"}, 297 | {"1", "1"}, 298 | }, 299 | }, 300 | { 301 | desc: "omitempty tags on interfaces - nil", 302 | in: []any{ 303 | struct { 304 | Iint any `csv:",omitempty"` 305 | IPint any `csv:",omitempty"` 306 | }{ 307 | nil, 308 | nil, 309 | }, 310 | struct { 311 | Iint any `csv:",omitempty"` 312 | IPint any `csv:",omitempty"` 313 | }{ 314 | (*int)(nil), 315 | ptr[any]((*int)(nil)), 316 | }, 317 | }, 318 | out: [][]string{ 319 | {"Iint", "IPint"}, 320 | {"", ""}, 321 | {"", ""}, 322 | }, 323 | }, 324 | { 325 | desc: "embedded types #1", 326 | in: []any{ 327 | TypeA{ 328 | Embedded1: Embedded1{ 329 | String: "string1", 330 | Float: 1, 331 | }, 332 | String: "string", 333 | Embedded2: Embedded2{ 334 | Float: 2, 335 | Bool: true, 336 | }, 337 | Int: 10, 338 | }, 339 | }, 340 | out: [][]string{ 341 | {"string", "bool", "int"}, 342 | {"string", "true", "10"}, 343 | }, 344 | }, 345 | { 346 | desc: "embedded non struct tagged types", 347 | in: []any{ 348 | TypeB{ 349 | Embedded3: Embedded3{"key": "val"}, 350 | String: "string1", 351 | }, 352 | }, 353 | out: [][]string{ 354 | {"json", "string"}, 355 | {`{"key":"val"}`, "string1"}, 356 | }, 357 | }, 358 | { 359 | desc: "embedded non struct tagged types with pointer receiver MarshalCSV", 360 | in: []any{ 361 | &struct { 362 | Embedded14 `csv:"json"` 363 | A Embedded14 `csv:"json2"` 364 | }{ 365 | Embedded14: Embedded14{"key": "val"}, 366 | A: Embedded14{"key1": "val1"}, 367 | }, 368 | struct { 369 | *Embedded14 `csv:"json"` 370 | A *Embedded14 `csv:"json2"` 371 | }{ 372 | Embedded14: &Embedded14{"key": "val"}, 373 | A: &Embedded14{"key1": "val1"}, 374 | }, 375 | }, 376 | out: [][]string{ 377 | {"json", "json2"}, 378 | {`{"key":"val"}`, `{"key1":"val1"}`}, 379 | {`{"key":"val"}`, `{"key1":"val1"}`}, 380 | }, 381 | }, 382 | { 383 | desc: "embedded non struct tagged types with pointer receiver MarshalText", 384 | in: []any{ 385 | &struct { 386 | Embedded15 `csv:"json"` 387 | A Embedded15 `csv:"json2"` 388 | }{ 389 | Embedded15: Embedded15{"key": "val"}, 390 | A: Embedded15{"key1": "val1"}, 391 | }, 392 | struct { 393 | *Embedded15 `csv:"json"` 394 | A *Embedded15 `csv:"json2"` 395 | }{ 396 | Embedded15: &Embedded15{"key": "val"}, 397 | A: &Embedded15{"key1": "val1"}, 398 | }, 399 | }, 400 | out: [][]string{ 401 | {"json", "json2"}, 402 | {`{"key":"val"}`, `{"key1":"val1"}`}, 403 | {`{"key":"val"}`, `{"key1":"val1"}`}, 404 | }, 405 | }, 406 | { 407 | desc: "embedded pointer types", 408 | in: []any{ 409 | TypeC{ 410 | Embedded1: &Embedded1{ 411 | String: "string2", 412 | Float: 1, 413 | }, 414 | String: "string1", 415 | }, 416 | }, 417 | out: [][]string{ 418 | {"float", "string"}, 419 | {`1`, "string1"}, 420 | }, 421 | }, 422 | { 423 | desc: "embedded pointer types with nil values", 424 | in: []any{ 425 | TypeC{ 426 | Embedded1: nil, 427 | String: "string1", 428 | }, 429 | }, 430 | out: [][]string{ 431 | {"float", "string"}, 432 | {``, "string1"}, 433 | }, 434 | }, 435 | { 436 | desc: "embedded non struct tagged pointer types", 437 | in: []any{ 438 | TypeD{ 439 | Embedded3: &Embedded3{"key": "val"}, 440 | String: "string1", 441 | }, 442 | }, 443 | out: [][]string{ 444 | {"json", "string"}, 445 | {`{"key":"val"}`, "string1"}, 446 | }, 447 | }, 448 | { 449 | desc: "embedded non struct tagged pointer types with nil value - textmarshaler", 450 | in: []any{ 451 | TypeM{ 452 | TextMarshaler: nil, 453 | }, 454 | }, 455 | out: [][]string{ 456 | {"text"}, 457 | {""}, 458 | }, 459 | }, 460 | { 461 | desc: "embedded non struct tagged pointer types with nil value - csvmarshaler", 462 | in: []any{ 463 | TypeD{ 464 | Embedded3: nil, 465 | String: "string1", 466 | }, 467 | }, 468 | out: [][]string{ 469 | {"json", "string"}, 470 | {"", "string1"}, 471 | }, 472 | }, 473 | { 474 | desc: "tagged fields priority", 475 | in: []any{ 476 | TagPriority{Foo: 1, Bar: 2}, 477 | }, 478 | out: [][]string{ 479 | {"Foo"}, 480 | {"2"}, 481 | }, 482 | }, 483 | { 484 | desc: "conflicting embedded fields #1", 485 | in: []any{ 486 | Embedded5{ 487 | Embedded6: Embedded6{X: 60}, 488 | Embedded7: Embedded7{X: 70}, 489 | Embedded8: Embedded8{ 490 | Embedded9: Embedded9{ 491 | X: 90, 492 | Y: 91, 493 | }, 494 | }, 495 | }, 496 | }, 497 | out: [][]string{ 498 | {"Y"}, 499 | {"91"}, 500 | }, 501 | }, 502 | { 503 | desc: "conflicting embedded fields #2", 504 | in: []any{ 505 | Embedded10{ 506 | Embedded11: Embedded11{ 507 | Embedded6: Embedded6{X: 60}, 508 | }, 509 | Embedded12: Embedded12{ 510 | Embedded6: Embedded6{X: 60}, 511 | }, 512 | Embedded13: Embedded13{ 513 | Embedded8: Embedded8{ 514 | Embedded9: Embedded9{ 515 | X: 90, 516 | Y: 91, 517 | }, 518 | }, 519 | }, 520 | }, 521 | }, 522 | out: [][]string{ 523 | {"Y"}, 524 | {"91"}, 525 | }, 526 | }, 527 | { 528 | desc: "double pointer", 529 | in: []any{ 530 | TypeE{ 531 | String: &PString, 532 | Int: &Int, 533 | }, 534 | }, 535 | out: [][]string{ 536 | {"string", "int"}, 537 | {"string", "10"}, 538 | }, 539 | }, 540 | { 541 | desc: "nil double pointer", 542 | in: []any{ 543 | TypeE{}, 544 | }, 545 | out: [][]string{ 546 | {"string", "int"}, 547 | {"", ""}, 548 | }, 549 | }, 550 | { 551 | desc: "unexported non-struct embedded", 552 | in: []any{ 553 | struct { 554 | A int 555 | embeddedMap 556 | }{1, make(embeddedMap)}, 557 | }, 558 | out: [][]string{ 559 | {"A"}, 560 | {"1"}, 561 | }, 562 | }, 563 | { 564 | desc: "cyclic reference", 565 | in: []any{ 566 | A{ 567 | B: B{Y: 2, A: &A{}}, 568 | X: 1, 569 | }, 570 | }, 571 | out: [][]string{ 572 | {"Y", "X"}, 573 | {"2", "1"}, 574 | }, 575 | }, 576 | { 577 | desc: "ptr receiver csv marshaler", 578 | in: []any{ 579 | &struct { 580 | A PtrRecCSVMarshaler 581 | }{}, 582 | struct { 583 | A PtrRecCSVMarshaler 584 | }{}, 585 | struct { 586 | A *PtrRecCSVMarshaler 587 | }{new(PtrRecCSVMarshaler)}, 588 | &struct { 589 | A *PtrRecCSVMarshaler 590 | }{new(PtrRecCSVMarshaler)}, 591 | &struct { 592 | A *PtrRecCSVMarshaler 593 | }{}, 594 | }, 595 | out: [][]string{ 596 | {"A"}, 597 | {"ptrreccsvmarshaler"}, 598 | {"0"}, 599 | {"ptrreccsvmarshaler"}, 600 | {"ptrreccsvmarshaler"}, 601 | {""}, 602 | }, 603 | }, 604 | { 605 | desc: "ptr receiver text marshaler", 606 | in: []any{ 607 | &struct { 608 | A PtrRecTextMarshaler 609 | }{}, 610 | struct { 611 | A PtrRecTextMarshaler 612 | }{}, 613 | struct { 614 | A *PtrRecTextMarshaler 615 | }{new(PtrRecTextMarshaler)}, 616 | &struct { 617 | A *PtrRecTextMarshaler 618 | }{new(PtrRecTextMarshaler)}, 619 | &struct { 620 | A *PtrRecTextMarshaler 621 | }{}, 622 | }, 623 | out: [][]string{ 624 | {"A"}, 625 | {"ptrrectextmarshaler"}, 626 | {"0"}, 627 | {"ptrrectextmarshaler"}, 628 | {"ptrrectextmarshaler"}, 629 | {""}, 630 | }, 631 | }, 632 | { 633 | desc: "text marshaler", 634 | in: []any{ 635 | struct { 636 | A CSVMarshaler 637 | }{}, 638 | struct { 639 | A TextMarshaler 640 | }{}, 641 | struct { 642 | A struct { 643 | TextMarshaler 644 | CSVMarshaler 645 | } 646 | }{}, 647 | }, 648 | out: [][]string{ 649 | {"A"}, 650 | {"csvmarshaler"}, 651 | {"textmarshaler"}, 652 | {"csvmarshaler"}, 653 | }, 654 | }, 655 | { 656 | desc: "primitive type alias implementing Marshaler", 657 | in: []any{ 658 | EnumType{Enum: EnumFirst}, 659 | EnumType{Enum: EnumSecond}, 660 | }, 661 | out: [][]string{ 662 | {"enum"}, 663 | {"first"}, 664 | {"second"}, 665 | }, 666 | }, 667 | { 668 | desc: "aliased type", 669 | in: []any{ 670 | struct{ Float float64 }{3.14}, 671 | }, 672 | out: [][]string{ 673 | {"Float"}, 674 | {"3.14"}, 675 | }, 676 | }, 677 | { 678 | desc: "embedded tagged marshalers", 679 | in: []any{ 680 | struct { 681 | CSVMarshaler `csv:"csv"` 682 | TextMarshaler `csv:"text"` 683 | }{}, 684 | }, 685 | out: [][]string{ 686 | {"csv", "text"}, 687 | {"csvmarshaler", "textmarshaler"}, 688 | }, 689 | }, 690 | { 691 | desc: "embedded pointer tagged marshalers", 692 | in: []any{ 693 | struct { 694 | *CSVMarshaler `csv:"csv"` 695 | *TextMarshaler `csv:"text"` 696 | }{&CSVMarshaler{}, &TextMarshaler{}}, 697 | }, 698 | out: [][]string{ 699 | {"csv", "text"}, 700 | {"csvmarshaler", "textmarshaler"}, 701 | }, 702 | }, 703 | { 704 | desc: "inline fields", 705 | in: []any{ 706 | Inline{ 707 | J1: TypeJ{ 708 | String: "j1", 709 | Int: "1", 710 | Float: "1", 711 | Embedded16: Embedded16{Bool: true, Uint8: 1}, 712 | }, 713 | J2: TypeJ{ 714 | String: "j2", 715 | Int: "2", 716 | Float: "2", 717 | Embedded16: Embedded16{Bool: true, Uint8: 2}, 718 | }, 719 | String: "top-level-str", 720 | String2: "STR", 721 | }, 722 | }, 723 | out: [][]string{ 724 | {"int", "Bool", "Uint8", "float", "prefix-STR", "prefix-int", "prefix-Bool", "prefix-Uint8", "prefix-float", "top-string", "STR"}, 725 | {"1", "true", "1", "1", "j2", "2", "true", "2", "2", "top-level-str", "STR"}, 726 | }, 727 | }, 728 | { 729 | desc: "inline chain", 730 | in: []any{ 731 | Inline5{ 732 | A: Inline2{ 733 | S: "1", 734 | A: Inline3{ 735 | Inline4: Inline4{A: "11"}, 736 | }, 737 | B: Inline3{ 738 | Inline4: Inline4{A: "12"}, 739 | }, 740 | }, 741 | B: Inline2{ 742 | S: "2", 743 | A: Inline3{ 744 | Inline4: Inline4{A: "21"}, 745 | }, 746 | B: Inline3{ 747 | Inline4: Inline4{A: "22"}, 748 | }, 749 | }, 750 | }, 751 | }, 752 | out: [][]string{ 753 | {"AS", "AAA", "S", "A"}, 754 | {"1", "11", "2", "22"}, 755 | }, 756 | }, 757 | { 758 | desc: "cyclic inline - no prefix", 759 | in: []any{ 760 | Inline6{ 761 | A: Inline7{ 762 | A: &Inline6{A: Inline7{ 763 | A: &Inline6{}, 764 | X: 10, 765 | }}, 766 | X: 1, 767 | }, 768 | }, 769 | }, 770 | out: [][]string{ 771 | {"X"}, 772 | {"1"}, 773 | }, 774 | }, 775 | { 776 | desc: "embedded with inline tag", 777 | in: []any{ 778 | struct { 779 | Inline7 `csv:"A,inline"` 780 | }{ 781 | Inline7: Inline7{ 782 | A: &Inline6{A: Inline7{ 783 | A: &Inline6{}, 784 | X: 10, 785 | }}, 786 | X: 1, 787 | }, 788 | }, 789 | }, 790 | out: [][]string{ 791 | {"AX"}, 792 | {"1"}, 793 | }, 794 | }, 795 | { 796 | desc: "embedded with empty inline tag", 797 | in: []any{ 798 | struct { 799 | Inline7 `csv:",inline"` 800 | }{ 801 | Inline7: Inline7{ 802 | A: &Inline6{A: Inline7{ 803 | A: &Inline6{}, 804 | X: 10, 805 | }}, 806 | X: 1, 807 | }, 808 | }, 809 | }, 810 | out: [][]string{ 811 | {"X"}, 812 | {"1"}, 813 | }, 814 | }, 815 | { 816 | desc: "embedded with ptr inline tag", 817 | in: []any{ 818 | struct { 819 | *Inline7 `csv:"A,inline"` 820 | }{ 821 | Inline7: &Inline7{ 822 | A: &Inline6{A: Inline7{ 823 | A: &Inline6{}, 824 | X: 10, 825 | }}, 826 | X: 1, 827 | }, 828 | }, 829 | }, 830 | out: [][]string{ 831 | {"AX"}, 832 | {"1"}, 833 | }, 834 | }, 835 | { 836 | desc: "inline visibility rules - top field first", 837 | in: []any{ 838 | struct { 839 | AA string 840 | F Inline4 `csv:"A,inline"` 841 | }{ 842 | AA: "1", 843 | F: Inline4{A: "10"}, 844 | }, 845 | }, 846 | out: [][]string{ 847 | {"AA"}, 848 | {"1"}, 849 | }, 850 | }, 851 | { 852 | desc: "inline visibility rules - top field last", 853 | in: []any{ 854 | Inline8{ 855 | F: &Inline4{A: "10"}, 856 | AA: 1, 857 | }, 858 | }, 859 | out: [][]string{ 860 | {"AA"}, 861 | {"1"}, 862 | }, 863 | }, 864 | { 865 | desc: "ignore inline tag on non struct", 866 | in: []any{ 867 | struct { 868 | X int `csv:",inline"` 869 | Y int `csv:"y,inline"` 870 | }{ 871 | X: 1, 872 | Y: 2, 873 | }, 874 | }, 875 | out: [][]string{ 876 | {"X", "y"}, 877 | {"1", "2"}, 878 | }, 879 | }, 880 | { 881 | desc: "registered func - non ptr elem", 882 | in: []any{ 883 | struct { 884 | Int int 885 | Pint *int 886 | Iface any 887 | Piface *any 888 | }{ 889 | Pint: ptr(0), 890 | Iface: 34, 891 | Piface: ptr[any](34), 892 | }, 893 | }, 894 | regFunc: marshalersSlice{ 895 | marshalerFunc(func(int) ([]byte, error) { return []byte("int"), nil }), 896 | }, 897 | out: [][]string{ 898 | {"Int", "Pint", "Iface", "Piface"}, 899 | {"int", "int", "int", "int"}, 900 | }, 901 | }, 902 | { 903 | desc: "registered func - ptr elem", 904 | in: []any{ 905 | &struct { 906 | Int int 907 | Pint *int 908 | Iface any 909 | Piface *any 910 | }{ 911 | Pint: ptr(0), 912 | Iface: 34, 913 | Piface: ptr[any](34), 914 | }, 915 | }, 916 | regFunc: marshalersSlice{ 917 | marshalerFunc(func(int) ([]byte, error) { return []byte("int"), nil }), 918 | }, 919 | out: [][]string{ 920 | {"Int", "Pint", "Iface", "Piface"}, 921 | {"int", "int", "int", "int"}, 922 | }, 923 | }, 924 | { 925 | desc: "registered func - ptr type - non ptr elem", 926 | in: []any{ 927 | struct { 928 | Int int 929 | Pint *int 930 | Iface any 931 | Piface *any 932 | }{ 933 | Pint: ptr(0), 934 | Iface: 34, 935 | Piface: ptr[any](ptr(34)), 936 | }, 937 | }, 938 | regFunc: marshalersSlice{ 939 | marshalerFunc(func(*int) ([]byte, error) { return []byte("int"), nil }), 940 | }, 941 | out: [][]string{ 942 | {"Int", "Pint", "Iface", "Piface"}, 943 | {"0", "int", "34", "int"}, 944 | }, 945 | }, 946 | { 947 | desc: "registered func - ptr type - ptr elem", 948 | in: []any{ 949 | &struct { 950 | Int int 951 | Pint *int 952 | Iface any 953 | Piface *any 954 | }{ 955 | Pint: ptr(0), 956 | Iface: 34, 957 | Piface: ptr[any](ptr(34)), 958 | }, 959 | }, 960 | regFunc: marshalersSlice{ 961 | marshalerFunc(func(*int) ([]byte, error) { return []byte("int"), nil }), 962 | }, 963 | out: [][]string{ 964 | {"Int", "Pint", "Iface", "Piface"}, 965 | {"int", "int", "34", "int"}, 966 | }, 967 | }, 968 | { 969 | desc: "registered func - mixed types - non ptr elem", 970 | in: []any{ 971 | struct { 972 | Int int 973 | Pint *int 974 | Iface any 975 | Piface *any 976 | }{ 977 | Pint: ptr(0), 978 | Iface: 34, 979 | Piface: ptr[any](ptr(34)), 980 | }, 981 | }, 982 | regFunc: marshalersSlice{ 983 | marshalerFunc(func(int) ([]byte, error) { return []byte("int"), nil }), 984 | marshalerFunc(func(*int) ([]byte, error) { return []byte("*int"), nil }), 985 | }, 986 | out: [][]string{ 987 | {"Int", "Pint", "Iface", "Piface"}, 988 | {"int", "*int", "int", "*int"}, 989 | }, 990 | }, 991 | { 992 | desc: "registered func - mixed types - ptr elem", 993 | in: []any{ 994 | &struct { 995 | Int int 996 | Pint *int 997 | Iface any 998 | Piface *any 999 | }{ 1000 | Pint: ptr(0), 1001 | Iface: 34, 1002 | Piface: ptr[any](ptr(34)), 1003 | }, 1004 | }, 1005 | regFunc: marshalersSlice{ 1006 | marshalerFunc(func(int) ([]byte, error) { return []byte("int"), nil }), 1007 | marshalerFunc(func(*int) ([]byte, error) { return []byte("*int"), nil }), 1008 | }, 1009 | out: [][]string{ 1010 | {"Int", "Pint", "Iface", "Piface"}, 1011 | {"int", "*int", "int", "*int"}, 1012 | }, 1013 | }, 1014 | { 1015 | desc: "registered func - interfaces", 1016 | in: []any{ 1017 | &struct { 1018 | CSVMarshaler Marshaler 1019 | Marshaler CSVMarshaler 1020 | PMarshaler *CSVMarshaler 1021 | CSVTextMarshaler CSVTextMarshaler 1022 | PCSVTextMarshaler *CSVTextMarshaler 1023 | PtrRecCSVMarshaler PtrRecCSVMarshaler 1024 | PtrRecTextMarshaler PtrRecTextMarshaler 1025 | }{ 1026 | PMarshaler: &CSVMarshaler{}, 1027 | PCSVTextMarshaler: &CSVTextMarshaler{}, 1028 | }, 1029 | }, 1030 | regFunc: marshalersSlice{ 1031 | marshalerFunc(func(Marshaler) ([]byte, error) { return []byte("registered.marshaler"), nil }), 1032 | marshalerFunc(func(encoding.TextMarshaler) ([]byte, error) { return []byte("registered.textmarshaler"), nil }), 1033 | }, 1034 | out: [][]string{ 1035 | {"CSVMarshaler", "Marshaler", "PMarshaler", "CSVTextMarshaler", "PCSVTextMarshaler", "PtrRecCSVMarshaler", "PtrRecTextMarshaler"}, 1036 | {"registered.marshaler", "registered.marshaler", "registered.marshaler", "registered.marshaler", "registered.marshaler", "registered.marshaler", "registered.textmarshaler"}, 1037 | }, 1038 | }, 1039 | { 1040 | desc: "registered func - interface order", 1041 | in: []any{ 1042 | &struct { 1043 | CSVTextMarshaler CSVTextMarshaler 1044 | PCSVTextMarshaler *CSVTextMarshaler 1045 | }{ 1046 | PCSVTextMarshaler: &CSVTextMarshaler{}, 1047 | }, 1048 | }, 1049 | regFunc: marshalersSlice{ 1050 | marshalerFunc(func(encoding.TextMarshaler) ([]byte, error) { return []byte("registered.textmarshaler"), nil }), 1051 | marshalerFunc(func(Marshaler) ([]byte, error) { return []byte("registered.marshaler"), nil }), 1052 | }, 1053 | out: [][]string{ 1054 | {"CSVTextMarshaler", "PCSVTextMarshaler"}, 1055 | {"registered.textmarshaler", "registered.textmarshaler"}, 1056 | }, 1057 | }, 1058 | { 1059 | desc: "registered func - method", 1060 | in: []any{ 1061 | &struct { 1062 | PtrRecCSVMarshaler PtrRecCSVMarshaler 1063 | }{}, 1064 | struct { 1065 | PtrRecCSVMarshaler PtrRecCSVMarshaler 1066 | }{}, 1067 | }, 1068 | regFunc: marshalersSlice{ 1069 | marshalerFunc((*PtrRecCSVMarshaler).CSV), 1070 | }, 1071 | out: [][]string{ 1072 | {"PtrRecCSVMarshaler"}, 1073 | {"ptrreccsvmarshaler.CSV"}, 1074 | {"0"}, 1075 | }, 1076 | }, 1077 | { 1078 | desc: "registered func - fallback error", 1079 | in: []any{ 1080 | struct { 1081 | Embedded14 1082 | }{}, 1083 | }, 1084 | regFunc: marshalersSlice{ 1085 | marshalerFunc((*Embedded14).MarshalCSV), 1086 | }, 1087 | err: &UnsupportedTypeError{ 1088 | Type: reflect.TypeOf(Embedded14{}), 1089 | }, 1090 | }, 1091 | { 1092 | desc: "registered interface func - returning error", 1093 | in: []any{ 1094 | &struct { 1095 | Embedded14 Embedded14 1096 | }{}, 1097 | }, 1098 | regFunc: marshalersSlice{ 1099 | marshalerFunc(func(Marshaler) ([]byte, error) { return nil, Error }), 1100 | }, 1101 | err: Error, 1102 | }, 1103 | { 1104 | desc: "registered func - returning error", 1105 | in: []any{ 1106 | &struct { 1107 | A InvalidType 1108 | }{}, 1109 | }, 1110 | regFunc: marshalersSlice{ 1111 | marshalerFunc(func(*InvalidType) ([]byte, error) { return nil, Error }), 1112 | }, 1113 | err: Error, 1114 | }, 1115 | { 1116 | desc: "registered func - fallback error on interface", 1117 | in: []any{ 1118 | struct { 1119 | Embedded14 1120 | }{}, 1121 | }, 1122 | regFunc: marshalersSlice{ 1123 | marshalerFunc(func(m Marshaler) ([]byte, error) { return nil, nil }), 1124 | }, 1125 | err: &UnsupportedTypeError{ 1126 | Type: reflect.TypeOf(Embedded14{}), 1127 | }, 1128 | }, 1129 | { 1130 | desc: "marshaler fallback error", 1131 | in: []any{ 1132 | struct { 1133 | Embedded14 1134 | }{}, 1135 | }, 1136 | err: &UnsupportedTypeError{ 1137 | Type: reflect.TypeOf(Embedded14{}), 1138 | }, 1139 | }, 1140 | { 1141 | desc: "encode different types", 1142 | // This doesnt mean the output csv is valid. Generally this is an invalid 1143 | // use. However, we need to make sure that the encoder is doing what it is 1144 | // asked to... correctly. 1145 | in: []any{ 1146 | struct { 1147 | A int 1148 | }{}, 1149 | struct { 1150 | A int 1151 | B string 1152 | }{}, 1153 | struct { 1154 | A int 1155 | }{}, 1156 | struct{}{}, 1157 | }, 1158 | out: [][]string{ 1159 | {"A"}, 1160 | {"0"}, 1161 | {"0", ""}, 1162 | {"0"}, 1163 | {}, 1164 | }, 1165 | }, 1166 | { 1167 | desc: "encode interface values", 1168 | in: []any{ 1169 | struct { 1170 | V any 1171 | }{1}, 1172 | struct { 1173 | V any 1174 | }{ptr(10)}, 1175 | struct { 1176 | V any 1177 | }{pptr(100)}, 1178 | struct { 1179 | V any 1180 | }{ppptr(1000)}, 1181 | struct { 1182 | V *any 1183 | }{ptr[any](pptr(10000))}, 1184 | struct { 1185 | V *any 1186 | }{func() *any { 1187 | var v any = ppptr(100000) 1188 | var vv any = v 1189 | return &vv 1190 | }()}, 1191 | struct { 1192 | V any 1193 | }{func() any { 1194 | var v any = &CSVMarshaler{} 1195 | var vv any = v 1196 | return &vv 1197 | }()}, 1198 | struct { 1199 | V any 1200 | }{func() any { 1201 | var v any = CSVMarshaler{} 1202 | var vv any = v 1203 | return &vv 1204 | }()}, 1205 | struct { 1206 | V any 1207 | }{func() any { 1208 | var v any = &CSVMarshaler{} 1209 | var vv any = v 1210 | return vv 1211 | }()}, 1212 | struct { 1213 | V any 1214 | }{ 1215 | V: func() any { 1216 | return PtrRecCSVMarshaler(5) 1217 | }(), 1218 | }, 1219 | struct { 1220 | V any 1221 | }{ 1222 | V: func() any { 1223 | m := PtrRecCSVMarshaler(5) 1224 | return &m 1225 | }(), 1226 | }, 1227 | struct { 1228 | V any 1229 | }{func() any { 1230 | var v any 1231 | var vv any = v 1232 | return &vv 1233 | }()}, 1234 | }, 1235 | out: [][]string{ 1236 | {"V"}, 1237 | {"1"}, 1238 | {"10"}, 1239 | {"100"}, 1240 | {"1000"}, 1241 | {"10000"}, 1242 | {"100000"}, 1243 | {"csvmarshaler"}, 1244 | {"csvmarshaler"}, 1245 | {"csvmarshaler"}, 1246 | {"5"}, 1247 | {"ptrreccsvmarshaler"}, 1248 | {""}, 1249 | }, 1250 | }, 1251 | { 1252 | desc: "encode NaN", 1253 | in: []any{ 1254 | struct { 1255 | Float float64 1256 | }{math.NaN()}, 1257 | }, 1258 | out: [][]string{ 1259 | {"Float"}, 1260 | {"NaN"}, 1261 | }, 1262 | }, 1263 | { 1264 | desc: "encode NaN with aliased type", 1265 | in: []any{ 1266 | struct { 1267 | Float Float 1268 | }{Float(math.NaN())}, 1269 | }, 1270 | out: [][]string{ 1271 | {"Float"}, 1272 | {"NaN"}, 1273 | }, 1274 | }, 1275 | { 1276 | desc: "empty struct", 1277 | in: []any{ 1278 | struct{}{}, 1279 | }, 1280 | out: [][]string{{}, {}}, 1281 | }, 1282 | { 1283 | desc: "value wrapped in interfaces and pointers", 1284 | in: []any{ 1285 | func() (v any) { v = &struct{ A int }{5}; return v }(), 1286 | }, 1287 | out: [][]string{{"A"}, {"5"}}, 1288 | }, 1289 | { 1290 | desc: "csv marshaler error", 1291 | in: []any{ 1292 | struct { 1293 | A CSVMarshaler 1294 | }{ 1295 | A: CSVMarshaler{Err: Error}, 1296 | }, 1297 | }, 1298 | err: &MarshalerError{Type: reflect.TypeOf(CSVMarshaler{}), MarshalerType: "MarshalCSV", Err: Error}, 1299 | }, 1300 | { 1301 | desc: "csv marshaler error as registered error", 1302 | in: []any{ 1303 | struct { 1304 | A CSVMarshaler 1305 | }{ 1306 | A: CSVMarshaler{Err: Error}, 1307 | }, 1308 | }, 1309 | regFunc: marshalersSlice{ 1310 | marshalerFunc(CSVMarshaler.MarshalCSV), 1311 | }, 1312 | err: Error, 1313 | }, 1314 | { 1315 | desc: "text marshaler error", 1316 | in: []any{ 1317 | struct { 1318 | A TextMarshaler 1319 | }{ 1320 | A: TextMarshaler{Err: Error}, 1321 | }, 1322 | }, 1323 | err: &MarshalerError{Type: reflect.TypeOf(TextMarshaler{}), MarshalerType: "MarshalText", Err: Error}, 1324 | }, 1325 | { 1326 | desc: "text marshaler fallback error - ptr reciever", 1327 | in: []any{ 1328 | struct { 1329 | A Embedded15 1330 | }{}, 1331 | }, 1332 | err: &UnsupportedTypeError{Type: reflect.TypeOf(Embedded15{})}, 1333 | }, 1334 | { 1335 | desc: "text marshaler error as registered func", 1336 | in: []any{ 1337 | struct { 1338 | A TextMarshaler 1339 | }{ 1340 | A: TextMarshaler{Err: Error}, 1341 | }, 1342 | }, 1343 | regFunc: marshalersSlice{ 1344 | marshalerFunc(TextMarshaler.MarshalText), 1345 | }, 1346 | err: Error, 1347 | }, 1348 | { 1349 | desc: "unsupported type", 1350 | in: []any{ 1351 | InvalidType{}, 1352 | }, 1353 | err: &UnsupportedTypeError{ 1354 | Type: reflect.TypeOf(struct{}{}), 1355 | }, 1356 | }, 1357 | { 1358 | desc: "unsupported double pointer type", 1359 | in: []any{ 1360 | struct { 1361 | A **struct{} 1362 | }{}, 1363 | }, 1364 | err: &UnsupportedTypeError{ 1365 | Type: reflect.TypeOf(struct{}{}), 1366 | }, 1367 | }, 1368 | { 1369 | desc: "unsupported interface type", 1370 | in: []any{ 1371 | TypeF{V: TypeA{}}, 1372 | }, 1373 | err: &UnsupportedTypeError{ 1374 | Type: reflect.TypeOf(TypeA{}), 1375 | }, 1376 | }, 1377 | { 1378 | desc: "encode not a struct", 1379 | in: []any{int(1)}, 1380 | err: &InvalidEncodeError{ 1381 | Type: reflect.TypeOf(int(1)), 1382 | }, 1383 | }, 1384 | { 1385 | desc: "encode nil interface", 1386 | in: []any{nilIface}, 1387 | err: &InvalidEncodeError{ 1388 | Type: reflect.TypeOf(nilIface), 1389 | }, 1390 | }, 1391 | { 1392 | desc: "encode nil ptr", 1393 | in: []any{nilPtr}, 1394 | err: &InvalidEncodeError{}, 1395 | }, 1396 | { 1397 | desc: "encode nil interface pointer", 1398 | in: []any{nilIfacePtr}, 1399 | err: &InvalidEncodeError{}, 1400 | }, 1401 | } 1402 | 1403 | for _, f := range fixtures { 1404 | f := f 1405 | 1406 | do := func(t *testing.T, fn func(*Encoder)) { 1407 | t.Helper() 1408 | 1409 | var buf bytes.Buffer 1410 | w := csv.NewWriter(&buf) 1411 | enc := NewEncoder(w) 1412 | fn(enc) 1413 | 1414 | for _, v := range f.in { 1415 | err := enc.Encode(v) 1416 | if f.err != nil { 1417 | if !checkErr(f.err, err) { 1418 | t.Errorf("want err=%v; got %v", f.err, err) 1419 | } 1420 | return 1421 | } else if err != nil { 1422 | t.Errorf("want err=nil; got %v", err) 1423 | } 1424 | } 1425 | w.Flush() 1426 | if err := w.Error(); err != nil { 1427 | t.Errorf("want err=nil; got %v", err) 1428 | } 1429 | 1430 | var out bytes.Buffer 1431 | if err := csv.NewWriter(&out).WriteAll(f.out); err != nil { 1432 | t.Errorf("want err=nil; got %v", err) 1433 | } 1434 | 1435 | if buf.String() != out.String() { 1436 | t.Errorf("want=%s; got %s", out.String(), buf.String()) 1437 | } 1438 | } 1439 | 1440 | if len(f.regFunc) == 0 { 1441 | t.Run(f.desc, func(t *testing.T) { 1442 | do(t, func(e *Encoder) {}) 1443 | }) 1444 | continue 1445 | } 1446 | 1447 | t.Run("old register "+f.desc, func(t *testing.T) { 1448 | do(t, func(enc *Encoder) { 1449 | for _, f := range f.regFunc { 1450 | enc.Register(f.RawFunc.Interface()) 1451 | } 1452 | }) 1453 | }) 1454 | 1455 | t.Run("new register "+f.desc, func(t *testing.T) { 1456 | do(t, func(enc *Encoder) { 1457 | enc.WithMarshalers(NewMarshalers(f.regFunc.Marshalers()...)) 1458 | }) 1459 | }) 1460 | } 1461 | 1462 | t.Run("test decoder tags", func(t *testing.T) { 1463 | type Test struct { 1464 | A int `custom:"1"` 1465 | B string `custom:"2"` 1466 | C float64 `custom:"-"` 1467 | } 1468 | 1469 | test := &Test{ 1470 | A: 1, 1471 | B: "b", 1472 | C: 2.5, 1473 | } 1474 | 1475 | var bufs [4]bytes.Buffer 1476 | for i := 0; i < 4; i += 2 { 1477 | encode(t, &bufs[i], test, "") 1478 | encode(t, &bufs[i+1], test, "custom") 1479 | } 1480 | 1481 | if b1, b2 := bufs[0].String(), bufs[2].String(); b1 != b2 { 1482 | t.Errorf("buffers are not equal: %s vs %s", b1, b2) 1483 | } 1484 | if b1, b2 := bufs[1].String(), bufs[3].String(); b1 != b2 { 1485 | t.Errorf("buffers are not equal: %s vs %s", b1, b2) 1486 | } 1487 | 1488 | expected1 := [][]string{ 1489 | {"A", "B", "C"}, 1490 | {"1", "b", "2.5"}, 1491 | } 1492 | expected2 := [][]string{ 1493 | {"1", "2"}, 1494 | {"1", "b"}, 1495 | } 1496 | 1497 | if b1, b2 := bufs[0].String(), encodeCSV(t, expected1); b1 != b2 { 1498 | t.Errorf("want buf=%s; got %s", b2, b1) 1499 | } 1500 | if b1, b2 := bufs[1].String(), encodeCSV(t, expected2); b1 != b2 { 1501 | t.Errorf("want buf=%s; got %s", b2, b1) 1502 | } 1503 | }) 1504 | 1505 | t.Run("error messages", func(t *testing.T) { 1506 | fixtures := []struct { 1507 | desc string 1508 | expected string 1509 | v any 1510 | }{ 1511 | { 1512 | desc: "invalid encode error message", 1513 | expected: "csvutil: Encode(int64)", 1514 | v: int64(1), 1515 | }, 1516 | { 1517 | desc: "invalid encode error message with nil interface", 1518 | expected: "csvutil: Encode(nil)", 1519 | v: nilIface, 1520 | }, 1521 | { 1522 | desc: "invalid encode error message with nil value", 1523 | expected: "csvutil: Encode(nil)", 1524 | v: nilPtr, 1525 | }, 1526 | { 1527 | desc: "unsupported type error message", 1528 | expected: "csvutil: unsupported type: struct {}", 1529 | v: struct{ InvalidType }{}, 1530 | }, 1531 | { 1532 | desc: "marshaler error message", 1533 | expected: "csvutil: error calling MarshalText for type csvutil.TextMarshaler: " + Error.Error(), 1534 | v: struct{ M TextMarshaler }{TextMarshaler{Error}}, 1535 | }, 1536 | } 1537 | 1538 | for _, f := range fixtures { 1539 | t.Run(f.desc, func(t *testing.T) { 1540 | err := NewEncoder(csv.NewWriter(bytes.NewBuffer(nil))).Encode(f.v) 1541 | if err == nil { 1542 | t.Fatal("want err not to be nil") 1543 | } 1544 | if err.Error() != f.expected { 1545 | t.Errorf("want=%s; got %s", f.expected, err.Error()) 1546 | } 1547 | }) 1548 | } 1549 | }) 1550 | 1551 | t.Run("EncodeHeader", func(t *testing.T) { 1552 | t.Run("no double header with encode", func(t *testing.T) { 1553 | var buf bytes.Buffer 1554 | w := csv.NewWriter(&buf) 1555 | enc := NewEncoder(w) 1556 | if err := enc.EncodeHeader(TypeI{}); err != nil { 1557 | t.Errorf("want err=nil; got %v", err) 1558 | } 1559 | if err := enc.Encode(TypeI{}); err != nil { 1560 | t.Errorf("want err=nil; got %v", err) 1561 | } 1562 | w.Flush() 1563 | 1564 | expected := encodeCSV(t, [][]string{ 1565 | {"String", "int"}, 1566 | {"", ""}, 1567 | }) 1568 | 1569 | if buf.String() != expected { 1570 | t.Errorf("want out=%s; got %s", expected, buf.String()) 1571 | } 1572 | }) 1573 | 1574 | t.Run("encode writes header if EncodeHeader fails", func(t *testing.T) { 1575 | var buf bytes.Buffer 1576 | w := csv.NewWriter(&buf) 1577 | enc := NewEncoder(w) 1578 | 1579 | if err := enc.EncodeHeader(InvalidType{}); err == nil { 1580 | t.Errorf("expected not nil error") 1581 | } 1582 | 1583 | if err := enc.Encode(TypeI{}); err != nil { 1584 | t.Errorf("want err=nil; got %v", err) 1585 | } 1586 | 1587 | w.Flush() 1588 | 1589 | expected := encodeCSV(t, [][]string{ 1590 | {"String", "int"}, 1591 | {"", ""}, 1592 | }) 1593 | 1594 | if buf.String() != expected { 1595 | t.Errorf("want out=%s; got %s", expected, buf.String()) 1596 | } 1597 | }) 1598 | 1599 | fixtures := []struct { 1600 | desc string 1601 | in any 1602 | tag string 1603 | out [][]string 1604 | err error 1605 | }{ 1606 | { 1607 | desc: "conflicting fields", 1608 | in: &Embedded10{}, 1609 | out: [][]string{ 1610 | {"Y"}, 1611 | }, 1612 | }, 1613 | { 1614 | desc: "custom tag", 1615 | in: TypeJ{}, 1616 | tag: "json", 1617 | out: [][]string{ 1618 | {"string", "bool", "Uint", "Float"}, 1619 | }, 1620 | }, 1621 | { 1622 | desc: "nil interface ptr value", 1623 | in: nilIfacePtr, 1624 | out: [][]string{ 1625 | { 1626 | "int", 1627 | "pint", 1628 | "int8", 1629 | "pint8", 1630 | "int16", 1631 | "pint16", 1632 | "int32", 1633 | "pint32", 1634 | "int64", 1635 | "pint64", 1636 | "uint", 1637 | "puint", 1638 | "uint8", 1639 | "puint8", 1640 | "uint16", 1641 | "puint16", 1642 | "uint32", 1643 | "puint32", 1644 | "uint64", 1645 | "puint64", 1646 | "float32", 1647 | "pfloat32", 1648 | "float64", 1649 | "pfloat64", 1650 | "string", 1651 | "pstring", 1652 | "bool", 1653 | "pbool", 1654 | "interface", 1655 | "pinterface", 1656 | "binary", 1657 | "pbinary", 1658 | }, 1659 | }, 1660 | }, 1661 | { 1662 | desc: "ptr to nil interface ptr value", 1663 | in: &nilIfacePtr, 1664 | out: [][]string{ 1665 | { 1666 | "int", 1667 | "pint", 1668 | "int8", 1669 | "pint8", 1670 | "int16", 1671 | "pint16", 1672 | "int32", 1673 | "pint32", 1674 | "int64", 1675 | "pint64", 1676 | "uint", 1677 | "puint", 1678 | "uint8", 1679 | "puint8", 1680 | "uint16", 1681 | "puint16", 1682 | "uint32", 1683 | "puint32", 1684 | "uint64", 1685 | "puint64", 1686 | "float32", 1687 | "pfloat32", 1688 | "float64", 1689 | "pfloat64", 1690 | "string", 1691 | "pstring", 1692 | "bool", 1693 | "pbool", 1694 | "interface", 1695 | "pinterface", 1696 | "binary", 1697 | "pbinary", 1698 | }, 1699 | }, 1700 | }, 1701 | { 1702 | desc: "nil ptr value", 1703 | in: nilPtr, 1704 | out: [][]string{ 1705 | { 1706 | "int", 1707 | "pint", 1708 | "int8", 1709 | "pint8", 1710 | "int16", 1711 | "pint16", 1712 | "int32", 1713 | "pint32", 1714 | "int64", 1715 | "pint64", 1716 | "uint", 1717 | "puint", 1718 | "uint8", 1719 | "puint8", 1720 | "uint16", 1721 | "puint16", 1722 | "uint32", 1723 | "puint32", 1724 | "uint64", 1725 | "puint64", 1726 | "float32", 1727 | "pfloat32", 1728 | "float64", 1729 | "pfloat64", 1730 | "string", 1731 | "pstring", 1732 | "bool", 1733 | "pbool", 1734 | "interface", 1735 | "pinterface", 1736 | "binary", 1737 | "pbinary", 1738 | }, 1739 | }, 1740 | }, 1741 | { 1742 | desc: "ptr to nil ptr value", 1743 | in: &nilPtr, 1744 | out: [][]string{ 1745 | { 1746 | "int", 1747 | "pint", 1748 | "int8", 1749 | "pint8", 1750 | "int16", 1751 | "pint16", 1752 | "int32", 1753 | "pint32", 1754 | "int64", 1755 | "pint64", 1756 | "uint", 1757 | "puint", 1758 | "uint8", 1759 | "puint8", 1760 | "uint16", 1761 | "puint16", 1762 | "uint32", 1763 | "puint32", 1764 | "uint64", 1765 | "puint64", 1766 | "float32", 1767 | "pfloat32", 1768 | "float64", 1769 | "pfloat64", 1770 | "string", 1771 | "pstring", 1772 | "bool", 1773 | "pbool", 1774 | "interface", 1775 | "pinterface", 1776 | "binary", 1777 | "pbinary", 1778 | }, 1779 | }, 1780 | }, 1781 | { 1782 | desc: "struct slice", 1783 | in: []TypeF{}, 1784 | out: [][]string{ 1785 | { 1786 | "int", 1787 | "pint", 1788 | "int8", 1789 | "pint8", 1790 | "int16", 1791 | "pint16", 1792 | "int32", 1793 | "pint32", 1794 | "int64", 1795 | "pint64", 1796 | "uint", 1797 | "puint", 1798 | "uint8", 1799 | "puint8", 1800 | "uint16", 1801 | "puint16", 1802 | "uint32", 1803 | "puint32", 1804 | "uint64", 1805 | "puint64", 1806 | "float32", 1807 | "pfloat32", 1808 | "float64", 1809 | "pfloat64", 1810 | "string", 1811 | "pstring", 1812 | "bool", 1813 | "pbool", 1814 | "interface", 1815 | "pinterface", 1816 | "binary", 1817 | "pbinary", 1818 | }, 1819 | }, 1820 | }, 1821 | { 1822 | desc: "ptr to nil interface", 1823 | in: &nilIface, 1824 | err: &UnsupportedTypeError{Type: reflect.ValueOf(&nilIface).Type().Elem()}, 1825 | }, 1826 | { 1827 | desc: "nil value", 1828 | err: &UnsupportedTypeError{}, 1829 | }, 1830 | { 1831 | desc: "ptr - not a struct", 1832 | in: &[]int{}, 1833 | err: &UnsupportedTypeError{Type: reflect.TypeOf([]int{})}, 1834 | }, 1835 | { 1836 | desc: "not a struct", 1837 | in: int(1), 1838 | err: &UnsupportedTypeError{Type: reflect.TypeOf(int(0))}, 1839 | }, 1840 | } 1841 | 1842 | for _, f := range fixtures { 1843 | t.Run(f.desc, func(t *testing.T) { 1844 | var buf bytes.Buffer 1845 | w := csv.NewWriter(&buf) 1846 | 1847 | enc := NewEncoder(w) 1848 | enc.Tag = f.tag 1849 | 1850 | err := enc.EncodeHeader(f.in) 1851 | w.Flush() 1852 | 1853 | if !checkErr(f.err, err) { 1854 | t.Errorf("want err=%v; got %v", f.err, err) 1855 | } 1856 | 1857 | if f.err != nil { 1858 | return 1859 | } 1860 | 1861 | if expected := encodeCSV(t, f.out); buf.String() != expected { 1862 | t.Errorf("want out=%s; got %s", expected, buf.String()) 1863 | } 1864 | }) 1865 | } 1866 | }) 1867 | 1868 | t.Run("AutoHeader false", func(t *testing.T) { 1869 | var buf bytes.Buffer 1870 | w := csv.NewWriter(&buf) 1871 | enc := NewEncoder(w) 1872 | enc.AutoHeader = false 1873 | 1874 | if err := enc.Encode(TypeG{ 1875 | String: "s", 1876 | Int: 10, 1877 | }); err != nil { 1878 | t.Fatalf("want err=nil; got %v", err) 1879 | } 1880 | w.Flush() 1881 | 1882 | expected := encodeCSV(t, [][]string{{"s", "10"}}) 1883 | if expected != buf.String() { 1884 | t.Errorf("want %s; got %s", expected, buf.String()) 1885 | } 1886 | }) 1887 | 1888 | t.Run("fail on type encoding without header", func(t *testing.T) { 1889 | var buf bytes.Buffer 1890 | w := csv.NewWriter(&buf) 1891 | enc := NewEncoder(w) 1892 | enc.AutoHeader = false 1893 | 1894 | err := enc.Encode(struct { 1895 | Invalid InvalidType 1896 | }{}) 1897 | 1898 | expected := &UnsupportedTypeError{Type: reflect.TypeOf(InvalidType{})} 1899 | if !reflect.DeepEqual(err, expected) { 1900 | t.Errorf("want %v; got %v", expected, err) 1901 | } 1902 | }) 1903 | 1904 | t.Run("fail while writing header", func(t *testing.T) { 1905 | Error := errors.New("error") 1906 | enc := NewEncoder(failingWriter{Err: Error}) 1907 | if err := enc.EncodeHeader(TypeA{}); err != Error { 1908 | t.Errorf("want %v; got %v", Error, err) 1909 | } 1910 | }) 1911 | 1912 | t.Run("slice and array", func(t *testing.T) { 1913 | fixtures := []struct { 1914 | desc string 1915 | in any 1916 | out [][]string 1917 | err error 1918 | }{ 1919 | { 1920 | desc: "slice", 1921 | in: []TypeI{ 1922 | {"1", 1}, 1923 | {"2", 2}, 1924 | }, 1925 | out: [][]string{ 1926 | {"String", "int"}, 1927 | {"1", "1"}, 1928 | {"2", "2"}, 1929 | }, 1930 | }, 1931 | { 1932 | desc: "ptr slice", 1933 | in: &[]TypeI{ 1934 | {"1", 1}, 1935 | {"2", 2}, 1936 | }, 1937 | out: [][]string{ 1938 | {"String", "int"}, 1939 | {"1", "1"}, 1940 | {"2", "2"}, 1941 | }, 1942 | }, 1943 | { 1944 | desc: "ptr slice with ptr elements", 1945 | in: &[]*TypeI{ 1946 | {"1", 1}, 1947 | {"2", 2}, 1948 | }, 1949 | out: [][]string{ 1950 | {"String", "int"}, 1951 | {"1", "1"}, 1952 | {"2", "2"}, 1953 | }, 1954 | }, 1955 | { 1956 | desc: "array", 1957 | in: [2]TypeI{ 1958 | {"1", 1}, 1959 | {"2", 2}, 1960 | }, 1961 | out: [][]string{ 1962 | {"String", "int"}, 1963 | {"1", "1"}, 1964 | {"2", "2"}, 1965 | }, 1966 | }, 1967 | { 1968 | desc: "ptr array", 1969 | in: &[2]TypeI{ 1970 | {"1", 1}, 1971 | {"2", 2}, 1972 | }, 1973 | out: [][]string{ 1974 | {"String", "int"}, 1975 | {"1", "1"}, 1976 | {"2", "2"}, 1977 | }, 1978 | }, 1979 | { 1980 | desc: "ptr array with ptr elements", 1981 | in: &[2]*TypeI{ 1982 | {"1", 1}, 1983 | {"2", 2}, 1984 | }, 1985 | out: [][]string{ 1986 | {"String", "int"}, 1987 | {"1", "1"}, 1988 | {"2", "2"}, 1989 | }, 1990 | }, 1991 | { 1992 | desc: "array with default val", 1993 | in: [2]TypeI{ 1994 | {"1", 1}, 1995 | }, 1996 | out: [][]string{ 1997 | {"String", "int"}, 1998 | {"1", "1"}, 1999 | {"", ""}, 2000 | }, 2001 | }, 2002 | { 2003 | desc: "no auto header on empty slice", 2004 | in: []TypeI{}, 2005 | out: [][]string{}, 2006 | }, 2007 | { 2008 | desc: "no auto header on empty array", 2009 | in: [0]TypeI{}, 2010 | out: [][]string{}, 2011 | }, 2012 | { 2013 | desc: "disallow double slice", 2014 | in: [][]TypeI{ 2015 | { 2016 | {"1", 1}, 2017 | }, 2018 | }, 2019 | err: &InvalidEncodeError{Type: reflect.TypeOf([][]TypeI{})}, 2020 | }, 2021 | { 2022 | desc: "disallow double ptr slice", 2023 | in: &[][]TypeI{ 2024 | { 2025 | {"1", 1}, 2026 | }, 2027 | }, 2028 | err: &InvalidEncodeError{Type: reflect.TypeOf(&[][]TypeI{})}, 2029 | }, 2030 | { 2031 | desc: "disallow double ptr slice with ptr slice", 2032 | in: &[]*[]TypeI{ 2033 | { 2034 | {"1", 1}, 2035 | }, 2036 | }, 2037 | err: &InvalidEncodeError{Type: reflect.TypeOf(&[]*[]TypeI{})}, 2038 | }, 2039 | { 2040 | desc: "disallow double array", 2041 | in: [2][2]TypeI{ 2042 | { 2043 | {"1", 1}, 2044 | }, 2045 | }, 2046 | err: &InvalidEncodeError{Type: reflect.TypeOf([2][2]TypeI{})}, 2047 | }, 2048 | { 2049 | desc: "disallow double ptr array", 2050 | in: &[2][2]TypeI{ 2051 | { 2052 | {"1", 1}, 2053 | }, 2054 | }, 2055 | err: &InvalidEncodeError{Type: reflect.TypeOf(&[2][2]TypeI{})}, 2056 | }, 2057 | { 2058 | desc: "disallow interface slice", 2059 | in: []any{ 2060 | TypeI{"1", 1}, 2061 | }, 2062 | err: &InvalidEncodeError{Type: reflect.TypeOf([]any{})}, 2063 | }, 2064 | { 2065 | desc: "disallow interface array", 2066 | in: [1]any{ 2067 | TypeI{"1", 1}, 2068 | }, 2069 | err: &InvalidEncodeError{Type: reflect.TypeOf([1]any{})}, 2070 | }, 2071 | } 2072 | 2073 | for _, f := range fixtures { 2074 | t.Run(f.desc, func(t *testing.T) { 2075 | var buf bytes.Buffer 2076 | w := csv.NewWriter(&buf) 2077 | err := NewEncoder(w).Encode(f.in) 2078 | 2079 | if f.err != nil { 2080 | if !checkErr(f.err, err) { 2081 | t.Errorf("want err=%v; got %v", f.err, err) 2082 | } 2083 | return 2084 | } 2085 | 2086 | if err != nil { 2087 | t.Fatalf("want err=nil; got %v", err) 2088 | } 2089 | 2090 | w.Flush() 2091 | if err := w.Error(); err != nil { 2092 | t.Errorf("want err=nil; got %v", err) 2093 | } 2094 | 2095 | var out bytes.Buffer 2096 | if err := csv.NewWriter(&out).WriteAll(f.out); err != nil { 2097 | t.Errorf("want err=nil; got %v", err) 2098 | } 2099 | 2100 | if buf.String() != out.String() { 2101 | t.Errorf("want=%s; got %s", out.String(), buf.String()) 2102 | } 2103 | }) 2104 | } 2105 | }) 2106 | 2107 | t.Run("with header", func(t *testing.T) { 2108 | t.Run("all present and sorted", func(t *testing.T) { 2109 | fixtures := []struct { 2110 | desc string 2111 | autoHeader bool 2112 | out [][]string 2113 | }{ 2114 | { 2115 | desc: "with autoheader", 2116 | autoHeader: true, 2117 | out: [][]string{ 2118 | {"C", "B", "D"}, 2119 | {"c", "b", "d"}, 2120 | }, 2121 | }, 2122 | { 2123 | desc: "without autoheader", 2124 | autoHeader: false, 2125 | out: [][]string{ 2126 | {"c", "b", "d"}, 2127 | }, 2128 | }, 2129 | } 2130 | 2131 | for _, f := range fixtures { 2132 | t.Run(f.desc, func(t *testing.T) { 2133 | type Embedded struct { 2134 | D string 2135 | } 2136 | type Foo struct { 2137 | A string 2138 | Embedded 2139 | B string 2140 | C string 2141 | } 2142 | 2143 | var buf bytes.Buffer 2144 | w := csv.NewWriter(&buf) 2145 | enc := NewEncoder(w) 2146 | enc.SetHeader([]string{"C", "B", "D"}) 2147 | enc.AutoHeader = f.autoHeader 2148 | enc.Encode(Foo{ 2149 | A: "a", 2150 | Embedded: Embedded{ 2151 | D: "d", 2152 | }, 2153 | B: "b", 2154 | C: "c", 2155 | }) 2156 | 2157 | w.Flush() 2158 | 2159 | expected := encodeCSV(t, f.out) 2160 | if expected != buf.String() { 2161 | t.Errorf("want=%s; got %s", expected, buf.String()) 2162 | } 2163 | }) 2164 | } 2165 | }) 2166 | 2167 | t.Run("missing fields", func(t *testing.T) { 2168 | fixtures := []struct { 2169 | desc string 2170 | autoHeader bool 2171 | out [][]string 2172 | }{ 2173 | { 2174 | desc: "with autoheader", 2175 | autoHeader: true, 2176 | out: [][]string{ 2177 | {"C", "X", "A", "Z"}, 2178 | {"c", "", "a", ""}, 2179 | }, 2180 | }, 2181 | { 2182 | desc: "without autoheader", 2183 | autoHeader: false, 2184 | out: [][]string{ 2185 | {"c", "", "a", ""}, 2186 | }, 2187 | }, 2188 | } 2189 | 2190 | for _, f := range fixtures { 2191 | t.Run(f.desc, func(t *testing.T) { 2192 | type Foo struct { 2193 | A string 2194 | B string 2195 | C string 2196 | } 2197 | 2198 | var buf bytes.Buffer 2199 | w := csv.NewWriter(&buf) 2200 | enc := NewEncoder(w) 2201 | enc.SetHeader([]string{"C", "X", "A", "Z"}) 2202 | enc.AutoHeader = f.autoHeader 2203 | enc.Encode(Foo{ 2204 | A: "a", 2205 | B: "b", 2206 | C: "c", 2207 | }) 2208 | 2209 | w.Flush() 2210 | 2211 | expected := encodeCSV(t, f.out) 2212 | if expected != buf.String() { 2213 | t.Errorf("want=%q; got %q", expected, buf.String()) 2214 | } 2215 | }) 2216 | } 2217 | }) 2218 | 2219 | t.Run("duplicates", func(t *testing.T) { 2220 | type Foo struct { 2221 | A string 2222 | B string 2223 | C string 2224 | } 2225 | 2226 | var buf bytes.Buffer 2227 | w := csv.NewWriter(&buf) 2228 | enc := NewEncoder(w) 2229 | enc.SetHeader([]string{"C", "X", "C", "A", "X", "Z", "A"}) 2230 | enc.Encode(Foo{ 2231 | A: "a", 2232 | B: "b", 2233 | C: "c", 2234 | }) 2235 | 2236 | w.Flush() 2237 | 2238 | expected := encodeCSV(t, [][]string{ 2239 | {"C", "X", "Z", "A"}, 2240 | {"c", "", "", "a"}, 2241 | }) 2242 | if expected != buf.String() { 2243 | t.Errorf("want=%q; got %q", expected, buf.String()) 2244 | } 2245 | }) 2246 | }) 2247 | 2248 | t.Run("register panics", func(t *testing.T) { 2249 | var buf bytes.Buffer 2250 | r := csv.NewWriter(&buf) 2251 | enc := NewEncoder(r) 2252 | 2253 | fixtures := []struct { 2254 | desc string 2255 | arg any 2256 | }{ 2257 | { 2258 | desc: "not a func", 2259 | arg: 1, 2260 | }, 2261 | { 2262 | desc: "nil", 2263 | arg: nil, 2264 | }, 2265 | { 2266 | desc: "T == empty interface", 2267 | arg: func(any) ([]byte, error) { return nil, nil }, 2268 | }, 2269 | { 2270 | desc: "first out not bytes", 2271 | arg: func(int) (int, error) { return 0, nil }, 2272 | }, 2273 | { 2274 | desc: "second out not error", 2275 | arg: func(int) (int, int) { return 0, 0 }, 2276 | }, 2277 | { 2278 | desc: "func with one out value", 2279 | arg: func(int) error { return nil }, 2280 | }, 2281 | { 2282 | desc: "func with no returns", 2283 | arg: func(int) {}, 2284 | }, 2285 | } 2286 | 2287 | for _, f := range fixtures { 2288 | t.Run(f.desc, func(t *testing.T) { 2289 | var e any 2290 | func() { 2291 | defer func() { 2292 | e = recover() 2293 | }() 2294 | enc.Register(f.arg) 2295 | }() 2296 | 2297 | if e == nil { 2298 | t.Error("Register was supposed to panic but it didnt") 2299 | } 2300 | t.Log(e) 2301 | }) 2302 | } 2303 | 2304 | t.Run("already registered", func(t *testing.T) { 2305 | f := func(int) ([]byte, error) { return nil, nil } 2306 | enc.Register(f) 2307 | 2308 | var e any 2309 | func() { 2310 | defer func() { 2311 | e = recover() 2312 | }() 2313 | enc.Register(f) 2314 | }() 2315 | 2316 | if e == nil { 2317 | t.Error("Register was supposed to panic but it didnt") 2318 | } 2319 | t.Log(e) 2320 | }) 2321 | }) 2322 | } 2323 | 2324 | func BenchmarkEncode(b *testing.B) { 2325 | b.Run("registered type", func(b *testing.B) { 2326 | type Foo struct { 2327 | A int `csv:"a"` 2328 | } 2329 | 2330 | b.Run("old register", func(b *testing.B) { 2331 | var buf bytes.Buffer 2332 | w := csv.NewWriter(&buf) 2333 | enc := NewEncoder(w) 2334 | enc.AutoHeader = false 2335 | 2336 | enc.Register(func(v int) ([]byte, error) { 2337 | return []byte(strconv.Itoa(v)), nil 2338 | }) 2339 | 2340 | var a Foo 2341 | for i := 0; i < b.N; i++ { 2342 | if err := enc.Encode(a); err != nil { 2343 | b.Fatal(err) 2344 | } 2345 | } 2346 | }) 2347 | 2348 | b.Run("new register", func(b *testing.B) { 2349 | var buf bytes.Buffer 2350 | w := csv.NewWriter(&buf) 2351 | enc := NewEncoder(w) 2352 | enc.AutoHeader = false 2353 | 2354 | enc.WithMarshalers(NewMarshalers(MarshalFunc( 2355 | func(v int) ([]byte, error) { 2356 | return []byte(strconv.Itoa(v)), nil 2357 | }, 2358 | ))) 2359 | 2360 | var a Foo 2361 | for i := 0; i < b.N; i++ { 2362 | if err := enc.Encode(a); err != nil { 2363 | b.Fatal(err) 2364 | } 2365 | } 2366 | }) 2367 | }) 2368 | } 2369 | 2370 | func encode(t *testing.T, buf *bytes.Buffer, v any, tag string) { 2371 | t.Helper() 2372 | 2373 | w := csv.NewWriter(buf) 2374 | enc := NewEncoder(w) 2375 | enc.Tag = tag 2376 | if err := enc.Encode(v); err != nil { 2377 | t.Fatalf("want err=nil; got %v", err) 2378 | } 2379 | w.Flush() 2380 | if err := w.Error(); err != nil { 2381 | t.Fatalf("want err=nil; got %v", err) 2382 | } 2383 | } 2384 | 2385 | func encodeCSV(t *testing.T, recs [][]string) string { 2386 | t.Helper() 2387 | 2388 | var buf bytes.Buffer 2389 | if err := csv.NewWriter(&buf).WriteAll(recs); err != nil { 2390 | t.Fatalf("want err=nil; got %v", err) 2391 | } 2392 | return buf.String() 2393 | } 2394 | 2395 | type failingWriter struct { 2396 | Err error 2397 | } 2398 | 2399 | func (w failingWriter) Write([]string) error { 2400 | return w.Err 2401 | } 2402 | 2403 | type marshalers struct { 2404 | *Marshalers 2405 | RawFunc reflect.Value 2406 | } 2407 | 2408 | func marshalerFunc[T any](f func(T) ([]byte, error)) marshalers { 2409 | return marshalers{ 2410 | Marshalers: MarshalFunc(f), 2411 | RawFunc: reflect.ValueOf(f), 2412 | } 2413 | } 2414 | 2415 | type marshalersSlice []marshalers 2416 | 2417 | func (ms marshalersSlice) Marshalers() (out []*Marshalers) { 2418 | for i := range ms { 2419 | out = append(out, ms[i].Marshalers) 2420 | } 2421 | return out 2422 | } 2423 | -------------------------------------------------------------------------------- /error.go: -------------------------------------------------------------------------------- 1 | package csvutil 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "fmt" 7 | "reflect" 8 | "strconv" 9 | ) 10 | 11 | // ErrFieldCount is returned when header's length doesn't match the length of 12 | // the read record. 13 | // 14 | // This Error can be disabled with Decoder.AlignRecord = true. 15 | var ErrFieldCount = errors.New("wrong number of fields in record") 16 | 17 | // An UnmarshalTypeError describes a string value that was not appropriate for 18 | // a value of a specific Go type. 19 | type UnmarshalTypeError struct { 20 | Value string // string value 21 | Type reflect.Type // type of Go value it could not be assigned to 22 | } 23 | 24 | func (e *UnmarshalTypeError) Error() string { 25 | return "csvutil: cannot unmarshal " + strconv.Quote(e.Value) + " into Go value of type " + e.Type.String() 26 | } 27 | 28 | // An UnsupportedTypeError is returned when attempting to encode or decode 29 | // a value of an unsupported type. 30 | type UnsupportedTypeError struct { 31 | Type reflect.Type 32 | } 33 | 34 | func (e *UnsupportedTypeError) Error() string { 35 | if e.Type == nil { 36 | return "csvutil: unsupported type: nil" 37 | } 38 | return "csvutil: unsupported type: " + e.Type.String() 39 | } 40 | 41 | // An InvalidDecodeError describes an invalid argument passed to Decode. 42 | // (The argument to Decode must be a non-nil struct pointer) 43 | type InvalidDecodeError struct { 44 | Type reflect.Type 45 | } 46 | 47 | func (e *InvalidDecodeError) Error() string { 48 | if e.Type == nil { 49 | return "csvutil: Decode(nil)" 50 | } 51 | 52 | if e.Type.Kind() != reflect.Ptr { 53 | return "csvutil: Decode(non-pointer " + e.Type.String() + ")" 54 | } 55 | 56 | typ := walkType(e.Type) 57 | switch typ.Kind() { 58 | case reflect.Struct: 59 | case reflect.Slice, reflect.Array: 60 | if typ.Elem().Kind() != reflect.Struct { 61 | return "csvutil: Decode(invalid type " + e.Type.String() + ")" 62 | } 63 | default: 64 | return "csvutil: Decode(invalid type " + e.Type.String() + ")" 65 | } 66 | 67 | return "csvutil: Decode(nil " + e.Type.String() + ")" 68 | } 69 | 70 | // An InvalidUnmarshalError describes an invalid argument passed to Unmarshal. 71 | // (The argument to Unmarshal must be a non-nil slice of structs pointer) 72 | type InvalidUnmarshalError struct { 73 | Type reflect.Type 74 | } 75 | 76 | func (e *InvalidUnmarshalError) Error() string { 77 | if e.Type == nil { 78 | return "csvutil: Unmarshal(nil)" 79 | } 80 | 81 | if e.Type.Kind() != reflect.Ptr { 82 | return "csvutil: Unmarshal(non-pointer " + e.Type.String() + ")" 83 | } 84 | 85 | return "csvutil: Unmarshal(invalid type " + e.Type.String() + ")" 86 | } 87 | 88 | // InvalidEncodeError is returned by Encode when the provided value was invalid. 89 | type InvalidEncodeError struct { 90 | Type reflect.Type 91 | } 92 | 93 | func (e *InvalidEncodeError) Error() string { 94 | if e.Type == nil { 95 | return "csvutil: Encode(nil)" 96 | } 97 | return "csvutil: Encode(" + e.Type.String() + ")" 98 | } 99 | 100 | // InvalidMarshalError is returned by Marshal when the provided value was invalid. 101 | type InvalidMarshalError struct { 102 | Type reflect.Type 103 | } 104 | 105 | func (e *InvalidMarshalError) Error() string { 106 | if e.Type == nil { 107 | return "csvutil: Marshal(nil)" 108 | } 109 | 110 | if walkType(e.Type).Kind() == reflect.Slice { 111 | return "csvutil: Marshal(non struct slice " + e.Type.String() + ")" 112 | } 113 | 114 | if walkType(e.Type).Kind() == reflect.Array { 115 | return "csvutil: Marshal(non struct array " + e.Type.String() + ")" 116 | } 117 | 118 | return "csvutil: Marshal(invalid type " + e.Type.String() + ")" 119 | } 120 | 121 | // MarshalerError is returned by Encoder when MarshalCSV or MarshalText returned 122 | // an error. 123 | type MarshalerError struct { 124 | Type reflect.Type 125 | MarshalerType string 126 | Err error 127 | } 128 | 129 | func (e *MarshalerError) Error() string { 130 | return "csvutil: error calling " + e.MarshalerType + " for type " + e.Type.String() + ": " + e.Err.Error() 131 | } 132 | 133 | // Unwrap implements Unwrap interface for errors package in Go1.13+. 134 | func (e *MarshalerError) Unwrap() error { 135 | return e.Err 136 | } 137 | 138 | func errPtrUnexportedStruct(typ reflect.Type) error { 139 | return fmt.Errorf("csvutil: cannot decode into a pointer to unexported struct: %s", typ) 140 | } 141 | 142 | // MissingColumnsError is returned by Decoder only when DisallowMissingColumns 143 | // option was set to true. It contains a list of all missing columns. 144 | type MissingColumnsError struct { 145 | Columns []string 146 | } 147 | 148 | func (e *MissingColumnsError) Error() string { 149 | var b bytes.Buffer 150 | b.WriteString("csvutil: missing columns: ") 151 | for i, c := range e.Columns { 152 | if i > 0 { 153 | b.WriteString(", ") 154 | } 155 | fmt.Fprintf(&b, "%q", c) 156 | } 157 | return b.String() 158 | } 159 | 160 | // DecodeError provides context to decoding errors if available. 161 | // 162 | // The caller should use errors.As in order to fetch the underlying error if 163 | // needed. 164 | // 165 | // Some of the DecodeError's fields are only populated if the Reader supports 166 | // PosField method. Specifically Line and Column. FieldPos is available in 167 | // csv.Reader since Go1.17. 168 | type DecodeError struct { 169 | // Field describes the struct's tag or field name on which the error happened. 170 | Field string 171 | 172 | // Line is 1-indexed line number taken from FieldPost method. It is only 173 | // available if the used Reader supports FieldPos method. 174 | Line int 175 | 176 | // Column is 1-indexed column index taken from FieldPost method. It is only 177 | // available if the used Reader supports FieldPos method. 178 | Column int 179 | 180 | // Error is the actual error that was returned while attempting to decode 181 | // a field. 182 | Err error 183 | } 184 | 185 | func (e *DecodeError) Error() string { 186 | if e.Line > 0 && e.Column > 0 { 187 | // Lines and Columns are 1-indexed so this check is fine. 188 | return fmt.Sprintf("%s: field %q line %d column %d", e.Err, e.Field, e.Line, e.Column) 189 | } 190 | return fmt.Sprintf("%s: field %q", e.Err, e.Field) 191 | } 192 | 193 | func (e *DecodeError) Unwrap() error { 194 | return e.Err 195 | } 196 | -------------------------------------------------------------------------------- /example_decoder_interface_test.go: -------------------------------------------------------------------------------- 1 | package csvutil_test 2 | 3 | import ( 4 | "bytes" 5 | "encoding/csv" 6 | "fmt" 7 | "io" 8 | "log" 9 | 10 | "github.com/jszwec/csvutil" 11 | ) 12 | 13 | // Value defines one record in the csv input. In this example it is important 14 | // that Type field is defined before Value. Decoder reads headers and values 15 | // in the same order as struct fields are defined. 16 | type Value struct { 17 | Type string `csv:"type"` 18 | Value any `csv:"value"` 19 | } 20 | 21 | func ExampleDecoder_interfaceValues() { 22 | // lets say our csv input defines variables with their types and values. 23 | data := []byte(` 24 | type,value 25 | string,string_value 26 | int,10 27 | `) 28 | 29 | dec, err := csvutil.NewDecoder(csv.NewReader(bytes.NewReader(data))) 30 | if err != nil { 31 | log.Fatal(err) 32 | } 33 | 34 | // we would like to read every variable and store their already parsed values 35 | // in the interface field. We can use Decoder.Map function to initialize 36 | // interface with proper values depending on the input. 37 | var value Value 38 | dec.Map = func(field, column string, v any) string { 39 | if column == "type" { 40 | switch field { 41 | case "int": // csv input tells us that this variable contains an int. 42 | var n int 43 | value.Value = &n // lets initialize interface with an initialized int pointer. 44 | default: 45 | return field 46 | } 47 | } 48 | return field 49 | } 50 | 51 | for { 52 | value = Value{} 53 | if err := dec.Decode(&value); err == io.EOF { 54 | break 55 | } else if err != nil { 56 | log.Fatal(err) 57 | } 58 | 59 | if value.Type == "int" { 60 | // our variable type is int, Map func already initialized our interface 61 | // as int pointer, so we can safely cast it and use it. 62 | n, ok := value.Value.(*int) 63 | if !ok { 64 | log.Fatal("expected value to be *int") 65 | } 66 | fmt.Printf("value_type: %s; value: (%T) %d\n", value.Type, value.Value, *n) 67 | } else { 68 | fmt.Printf("value_type: %s; value: (%T) %v\n", value.Type, value.Value, value.Value) 69 | } 70 | } 71 | 72 | // Output: 73 | // value_type: string; value: (string) string_value 74 | // value_type: int; value: (*int) 10 75 | } 76 | -------------------------------------------------------------------------------- /example_decoder_no_header_test.go: -------------------------------------------------------------------------------- 1 | package csvutil_test 2 | 3 | import ( 4 | "bytes" 5 | "encoding/csv" 6 | "fmt" 7 | "io" 8 | "log" 9 | 10 | "github.com/jszwec/csvutil" 11 | ) 12 | 13 | type User struct { 14 | ID int 15 | Name string 16 | Age int `csv:",omitempty"` 17 | State int `csv:"-"` 18 | City string 19 | ZIP string `csv:"zip_code"` 20 | } 21 | 22 | var userHeader []string 23 | 24 | func init() { 25 | h, err := csvutil.Header(User{}, "csv") 26 | if err != nil { 27 | log.Fatal(err) 28 | } 29 | userHeader = h 30 | } 31 | 32 | func ExampleDecoder_decodingDataWithNoHeader() { 33 | data := []byte(` 34 | 1,John,27,la,90005 35 | 2,Bob,,ny,10005`) 36 | 37 | r := csv.NewReader(bytes.NewReader(data)) 38 | 39 | dec, err := csvutil.NewDecoder(r, userHeader...) 40 | if err != nil { 41 | log.Fatal(err) 42 | } 43 | 44 | var users []User 45 | for { 46 | var u User 47 | 48 | if err := dec.Decode(&u); err == io.EOF { 49 | break 50 | } else if err != nil { 51 | log.Fatal(err) 52 | } 53 | users = append(users, u) 54 | } 55 | 56 | fmt.Printf("%+v", users) 57 | 58 | // Output: 59 | // [{ID:1 Name:John Age:27 State:0 City:la ZIP:90005} {ID:2 Name:Bob Age:0 State:0 City:ny ZIP:10005}] 60 | } 61 | -------------------------------------------------------------------------------- /example_decoder_register_test.go: -------------------------------------------------------------------------------- 1 | package csvutil_test 2 | 3 | import ( 4 | "encoding/csv" 5 | "errors" 6 | "fmt" 7 | "strconv" 8 | "strings" 9 | "time" 10 | "unicode" 11 | 12 | "github.com/jszwec/csvutil" 13 | ) 14 | 15 | type IntStruct struct { 16 | Value int 17 | } 18 | 19 | func (i *IntStruct) Scan(state fmt.ScanState, verb rune) error { 20 | switch verb { 21 | case 'd', 'v': 22 | default: 23 | return errors.New("unsupported verb") 24 | } 25 | 26 | t, err := state.Token(false, unicode.IsDigit) 27 | if err != nil { 28 | return err 29 | } 30 | 31 | n, err := strconv.Atoi(string(t)) 32 | if err != nil { 33 | return err 34 | } 35 | *i = IntStruct{Value: n} 36 | return nil 37 | } 38 | 39 | func ExampleDecoder_Register() { 40 | type Foo struct { 41 | Time time.Time `csv:"time"` 42 | Hex int `csv:"hex"` 43 | PtrHex *int `csv:"ptr_hex"` 44 | IntStruct IntStruct `csv:"int_struct"` 45 | } 46 | 47 | unmarshalInt := csvutil.UnmarshalFunc(func(data []byte, n *int) error { 48 | v, err := strconv.ParseInt(string(data), 16, 64) 49 | if err != nil { 50 | return err 51 | } 52 | *n = int(v) 53 | return nil 54 | }) 55 | 56 | unmarshalTime := csvutil.UnmarshalFunc(func(data []byte, t *time.Time) error { 57 | tt, err := time.Parse(time.Kitchen, string(data)) 58 | if err != nil { 59 | return err 60 | } 61 | *t = tt 62 | return nil 63 | }) 64 | 65 | unmarshalScanner := csvutil.UnmarshalFunc(func(data []byte, s fmt.Scanner) error { 66 | _, err := fmt.Sscan(string(data), s) 67 | return err 68 | }) 69 | 70 | unmarshalers := csvutil.NewUnmarshalers( 71 | unmarshalInt, 72 | unmarshalTime, 73 | unmarshalScanner, 74 | ) 75 | 76 | const data = `time,hex,ptr_hex,int_struct 77 | 12:00PM,f,a,34` 78 | 79 | r := csv.NewReader(strings.NewReader(data)) 80 | dec, err := csvutil.NewDecoder(r) 81 | if err != nil { 82 | panic(err) 83 | } 84 | 85 | dec.WithUnmarshalers(unmarshalers) 86 | 87 | var foos []Foo 88 | if err := dec.Decode(&foos); err != nil { 89 | fmt.Println("error:", err) 90 | } 91 | 92 | fmt.Printf("%s,%d,%d,%+v", 93 | foos[0].Time.Format(time.Kitchen), 94 | foos[0].Hex, 95 | *foos[0].PtrHex, 96 | foos[0].IntStruct, 97 | ) 98 | 99 | // Output: 100 | // 12:00PM,15,10,{Value:34} 101 | } 102 | -------------------------------------------------------------------------------- /example_decoder_test.go: -------------------------------------------------------------------------------- 1 | package csvutil_test 2 | 3 | import ( 4 | "encoding/csv" 5 | "fmt" 6 | "io" 7 | "log" 8 | "strings" 9 | 10 | "github.com/jszwec/csvutil" 11 | ) 12 | 13 | func ExampleDecoder_Decode() { 14 | type User struct { 15 | ID *int `csv:"id,omitempty"` 16 | Name string `csv:"name"` 17 | City string `csv:"city"` 18 | Age int `csv:"age"` 19 | } 20 | 21 | csvReader := csv.NewReader(strings.NewReader(` 22 | id,name,age,city 23 | ,alice,25,la 24 | ,bob,30,ny`)) 25 | 26 | dec, err := csvutil.NewDecoder(csvReader) 27 | if err != nil { 28 | log.Fatal(err) 29 | } 30 | 31 | var users []User 32 | for { 33 | var u User 34 | if err := dec.Decode(&u); err == io.EOF { 35 | break 36 | } else if err != nil { 37 | log.Fatal(err) 38 | } 39 | users = append(users, u) 40 | } 41 | 42 | fmt.Println(users) 43 | 44 | // Output: 45 | // [{ alice la 25} { bob ny 30}] 46 | } 47 | 48 | func ExampleDecoder_Decode_slice() { 49 | type User struct { 50 | ID *int `csv:"id,omitempty"` 51 | Name string `csv:"name"` 52 | City string `csv:"city"` 53 | Age int `csv:"age"` 54 | } 55 | 56 | csvReader := csv.NewReader(strings.NewReader(` 57 | id,name,age,city 58 | ,alice,25,la 59 | ,bob,30,ny`)) 60 | 61 | dec, err := csvutil.NewDecoder(csvReader) 62 | if err != nil { 63 | log.Fatal(err) 64 | } 65 | 66 | var users []User 67 | if err := dec.Decode(&users); err != nil { 68 | log.Fatal(err) 69 | } 70 | 71 | fmt.Println(users) 72 | 73 | // Output: 74 | // [{ alice la 25} { bob ny 30}] 75 | } 76 | 77 | func ExampleDecoder_Decode_array() { 78 | type User struct { 79 | ID *int `csv:"id,omitempty"` 80 | Name string `csv:"name"` 81 | City string `csv:"city"` 82 | Age int `csv:"age"` 83 | } 84 | 85 | csvReader := csv.NewReader(strings.NewReader(` 86 | id,name,age,city 87 | ,alice,25,la 88 | ,bob,30,ny 89 | ,john,29,ny`)) 90 | 91 | dec, err := csvutil.NewDecoder(csvReader) 92 | if err != nil { 93 | log.Fatal(err) 94 | } 95 | 96 | var users [2]User 97 | if err := dec.Decode(&users); err != nil { 98 | log.Fatal(err) 99 | } 100 | 101 | fmt.Println(users) 102 | 103 | // Output: 104 | // [{ alice la 25} { bob ny 30}] 105 | } 106 | 107 | func ExampleDecoder_Unused() { 108 | type User struct { 109 | Name string `csv:"name"` 110 | City string `csv:"city"` 111 | Age int `csv:"age"` 112 | OtherData map[string]string `csv:"-"` 113 | } 114 | 115 | csvReader := csv.NewReader(strings.NewReader(` 116 | name,age,city,zip 117 | alice,25,la,90005 118 | bob,30,ny,10005`)) 119 | 120 | dec, err := csvutil.NewDecoder(csvReader) 121 | if err != nil { 122 | log.Fatal(err) 123 | } 124 | 125 | header := dec.Header() 126 | var users []User 127 | for { 128 | u := User{OtherData: make(map[string]string)} 129 | 130 | if err := dec.Decode(&u); err == io.EOF { 131 | break 132 | } else if err != nil { 133 | log.Fatal(err) 134 | } 135 | 136 | for _, i := range dec.Unused() { 137 | u.OtherData[header[i]] = dec.Record()[i] 138 | } 139 | users = append(users, u) 140 | } 141 | 142 | fmt.Println(users) 143 | 144 | // Output: 145 | // [{alice la 25 map[zip:90005]} {bob ny 30 map[zip:10005]}] 146 | } 147 | 148 | func ExampleDecoder_decodeEmbedded() { 149 | type Address struct { 150 | ID int `csv:"id"` // same field as in User - this one will be empty 151 | City string `csv:"city"` 152 | State string `csv:"state"` 153 | } 154 | 155 | type User struct { 156 | Address 157 | ID int `csv:"id"` // same field as in Address - this one wins 158 | Name string `csv:"name"` 159 | Age int `csv:"age"` 160 | } 161 | 162 | csvReader := csv.NewReader(strings.NewReader( 163 | "id,name,age,city,state\n" + 164 | "1,alice,25,la,ca\n" + 165 | "2,bob,30,ny,ny")) 166 | 167 | dec, err := csvutil.NewDecoder(csvReader) 168 | if err != nil { 169 | log.Fatal(err) 170 | } 171 | 172 | var users []User 173 | for { 174 | var u User 175 | 176 | if err := dec.Decode(&u); err == io.EOF { 177 | break 178 | } else if err != nil { 179 | log.Fatal(err) 180 | } 181 | 182 | users = append(users, u) 183 | } 184 | 185 | fmt.Println(users) 186 | 187 | // Output: 188 | // [{{0 la ca} 1 alice 25} {{0 ny ny} 2 bob 30}] 189 | } 190 | 191 | func ExampleDecoder_Decode_inline() { 192 | type Address struct { 193 | Street string `csv:"street"` 194 | City string `csv:"city"` 195 | } 196 | 197 | type User struct { 198 | Name string `csv:"name"` 199 | Address Address `csv:",inline"` 200 | HomeAddress Address `csv:"home_address_,inline"` 201 | WorkAddress Address `csv:"work_address_,inline"` 202 | Age int `csv:"age,omitempty"` 203 | } 204 | 205 | data := []byte( 206 | "name,street,city,home_address_street,home_address_city,work_address_street,work_address_city,age\n" + 207 | "John,Washington,Boston,Boylston,Boston,River St,Cambridge,26", 208 | ) 209 | 210 | var users []User 211 | if err := csvutil.Unmarshal(data, &users); err != nil { 212 | fmt.Println("error:", err) 213 | } 214 | 215 | fmt.Println(users) 216 | 217 | // Output: 218 | // [{John {Washington Boston} {Boylston Boston} {River St Cambridge} 26}] 219 | } 220 | -------------------------------------------------------------------------------- /example_decoder_unmashaler_test.go: -------------------------------------------------------------------------------- 1 | package csvutil_test 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | 7 | "github.com/jszwec/csvutil" 8 | ) 9 | 10 | type Bar int 11 | 12 | func (b *Bar) UnmarshalCSV(data []byte) error { 13 | n, err := strconv.Atoi(string(data)) 14 | *b = Bar(n) 15 | return err 16 | } 17 | 18 | type Foo struct { 19 | Int int `csv:"int"` 20 | Bar Bar `csv:"bar"` 21 | } 22 | 23 | func ExampleDecoder_customUnmarshalCSV() { 24 | var csvInput = []byte(` 25 | int,bar 26 | 5,10 27 | 6,11`) 28 | 29 | var foos []Foo 30 | if err := csvutil.Unmarshal(csvInput, &foos); err != nil { 31 | fmt.Println("error:", err) 32 | } 33 | 34 | fmt.Printf("%+v", foos) 35 | 36 | // Output: 37 | // [{Int:5 Bar:10} {Int:6 Bar:11}] 38 | } 39 | -------------------------------------------------------------------------------- /example_encoder_test.go: -------------------------------------------------------------------------------- 1 | package csvutil_test 2 | 3 | import ( 4 | "bytes" 5 | "encoding/csv" 6 | "fmt" 7 | "strconv" 8 | "time" 9 | 10 | "github.com/jszwec/csvutil" 11 | ) 12 | 13 | func ExampleEncoder_Encode_streaming() { 14 | type Address struct { 15 | City string 16 | Country string 17 | } 18 | 19 | type User struct { 20 | Name string 21 | Address 22 | Age int `csv:"age,omitempty"` 23 | } 24 | 25 | users := []User{ 26 | {Name: "John", Address: Address{"Boston", "USA"}, Age: 26}, 27 | {Name: "Bob", Address: Address{"LA", "USA"}, Age: 27}, 28 | {Name: "Alice", Address: Address{"SF", "USA"}}, 29 | } 30 | 31 | var buf bytes.Buffer 32 | w := csv.NewWriter(&buf) 33 | enc := csvutil.NewEncoder(w) 34 | 35 | for _, u := range users { 36 | if err := enc.Encode(u); err != nil { 37 | fmt.Println("error:", err) 38 | } 39 | } 40 | 41 | w.Flush() 42 | if err := w.Error(); err != nil { 43 | fmt.Println("error:", err) 44 | } 45 | 46 | fmt.Println(buf.String()) 47 | 48 | // Output: 49 | // Name,City,Country,age 50 | // John,Boston,USA,26 51 | // Bob,LA,USA,27 52 | // Alice,SF,USA, 53 | } 54 | 55 | func ExampleEncoder_Encode_all() { 56 | type Address struct { 57 | City string 58 | Country string 59 | } 60 | 61 | type User struct { 62 | Name string 63 | Address 64 | Age int `csv:"age,omitempty"` 65 | } 66 | 67 | users := []User{ 68 | {Name: "John", Address: Address{"Boston", "USA"}, Age: 26}, 69 | {Name: "Bob", Address: Address{"LA", "USA"}, Age: 27}, 70 | {Name: "Alice", Address: Address{"SF", "USA"}}, 71 | } 72 | 73 | var buf bytes.Buffer 74 | w := csv.NewWriter(&buf) 75 | if err := csvutil.NewEncoder(w).Encode(users); err != nil { 76 | fmt.Println("error:", err) 77 | } 78 | 79 | w.Flush() 80 | if err := w.Error(); err != nil { 81 | fmt.Println("error:", err) 82 | } 83 | 84 | fmt.Println(buf.String()) 85 | 86 | // Output: 87 | // Name,City,Country,age 88 | // John,Boston,USA,26 89 | // Bob,LA,USA,27 90 | // Alice,SF,USA, 91 | } 92 | 93 | func ExampleEncoder_EncodeHeader() { 94 | type User struct { 95 | Name string 96 | Age int `csv:"age,omitempty"` 97 | } 98 | 99 | var buf bytes.Buffer 100 | w := csv.NewWriter(&buf) 101 | enc := csvutil.NewEncoder(w) 102 | 103 | if err := enc.EncodeHeader(User{}); err != nil { 104 | fmt.Println("error:", err) 105 | } 106 | 107 | w.Flush() 108 | if err := w.Error(); err != nil { 109 | fmt.Println("error:", err) 110 | } 111 | 112 | fmt.Println(buf.String()) 113 | 114 | // Output: 115 | // Name,age 116 | } 117 | 118 | func ExampleEncoder_Encode_inline() { 119 | type Owner struct { 120 | Name string `csv:"name"` 121 | } 122 | 123 | type Address struct { 124 | Street string `csv:"street"` 125 | City string `csv:"city"` 126 | Owner Owner `csv:"owner_,inline"` 127 | } 128 | 129 | type User struct { 130 | Name string `csv:"name"` 131 | Address Address `csv:",inline"` 132 | HomeAddress Address `csv:"home_address_,inline"` 133 | WorkAddress Address `csv:"work_address_,inline"` 134 | Age int `csv:"age,omitempty"` 135 | } 136 | 137 | users := []User{ 138 | { 139 | Name: "John", 140 | Address: Address{"Washington", "Boston", Owner{"Steve"}}, 141 | HomeAddress: Address{"Boylston", "Boston", Owner{"Steve"}}, 142 | WorkAddress: Address{"River St", "Cambridge", Owner{"Steve"}}, 143 | Age: 26, 144 | }, 145 | } 146 | 147 | b, err := csvutil.Marshal(users) 148 | if err != nil { 149 | fmt.Println("error:", err) 150 | } 151 | 152 | fmt.Printf("%s\n", b) 153 | 154 | // Output: 155 | // name,street,city,owner_name,home_address_street,home_address_city,home_address_owner_name,work_address_street,work_address_city,work_address_owner_name,age 156 | // John,Washington,Boston,Steve,Boylston,Boston,Steve,River St,Cambridge,Steve,26 157 | } 158 | 159 | func ExampleEncoder_Register() { 160 | type Foo struct { 161 | Time time.Time `csv:"time"` 162 | Hex int `csv:"hex"` 163 | PtrHex *int `csv:"ptr_hex"` 164 | Buffer *bytes.Buffer `csv:"buffer"` 165 | } 166 | 167 | foos := []Foo{ 168 | { 169 | Time: time.Date(2020, 6, 20, 12, 0, 0, 0, time.UTC), 170 | Hex: 15, 171 | Buffer: bytes.NewBufferString("hello"), 172 | }, 173 | } 174 | 175 | marshalInt := csvutil.MarshalFunc(func(n *int) ([]byte, error) { 176 | if n == nil { 177 | return []byte("NULL"), nil 178 | } 179 | return strconv.AppendInt(nil, int64(*n), 16), nil 180 | }) 181 | 182 | marshalTime := csvutil.MarshalFunc(func(t time.Time) ([]byte, error) { 183 | return t.AppendFormat(nil, time.Kitchen), nil 184 | }) 185 | 186 | // all fields which implement String method will use this, unless their 187 | // concrete type was already overriden. 188 | marshalStringer := csvutil.MarshalFunc(func(s fmt.Stringer) ([]byte, error) { 189 | return []byte(s.String()), nil 190 | }) 191 | 192 | marshalers := csvutil.NewMarshalers( 193 | marshalInt, 194 | marshalTime, 195 | marshalStringer, 196 | ) 197 | 198 | var buf bytes.Buffer 199 | w := csv.NewWriter(&buf) 200 | enc := csvutil.NewEncoder(w) 201 | enc.WithMarshalers(marshalers) 202 | 203 | if err := enc.Encode(foos); err != nil { 204 | fmt.Println("error:", err) 205 | } 206 | 207 | w.Flush() 208 | if err := w.Error(); err != nil { 209 | fmt.Println("error:", err) 210 | } 211 | 212 | fmt.Println(buf.String()) 213 | 214 | // Output: 215 | // time,hex,ptr_hex,buffer 216 | // 12:00PM,f,NULL,hello 217 | } 218 | -------------------------------------------------------------------------------- /example_header_test.go: -------------------------------------------------------------------------------- 1 | package csvutil_test 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | 7 | "github.com/jszwec/csvutil" 8 | ) 9 | 10 | func ExampleHeader() { 11 | type User struct { 12 | ID int 13 | Name string 14 | Age int `csv:",omitempty"` 15 | State int `csv:"-"` 16 | City string 17 | ZIP string `csv:"zip_code"` 18 | } 19 | 20 | header, err := csvutil.Header(User{}, "csv") 21 | if err != nil { 22 | log.Fatal(err) 23 | } 24 | 25 | fmt.Println(header) 26 | // Output: 27 | // [ID Name Age City zip_code] 28 | } 29 | -------------------------------------------------------------------------------- /example_marshal_marshaler_test.go: -------------------------------------------------------------------------------- 1 | package csvutil_test 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/jszwec/csvutil" 7 | ) 8 | 9 | type Status uint8 10 | 11 | const ( 12 | Unknown = iota 13 | Success 14 | Failure 15 | ) 16 | 17 | func (s Status) MarshalCSV() ([]byte, error) { 18 | switch s { 19 | case Success: 20 | return []byte("success"), nil 21 | case Failure: 22 | return []byte("failure"), nil 23 | default: 24 | return []byte("unknown"), nil 25 | } 26 | } 27 | 28 | type Job struct { 29 | ID int 30 | Status Status 31 | } 32 | 33 | func ExampleMarshal_customMarshalCSV() { 34 | jobs := []Job{ 35 | {1, Success}, 36 | {2, Failure}, 37 | } 38 | 39 | b, err := csvutil.Marshal(jobs) 40 | if err != nil { 41 | fmt.Println("error:", err) 42 | } 43 | fmt.Println(string(b)) 44 | 45 | // Output: 46 | // ID,Status 47 | // 1,success 48 | // 2,failure 49 | } 50 | -------------------------------------------------------------------------------- /example_marshal_slice_map_test.go: -------------------------------------------------------------------------------- 1 | package csvutil_test 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "strings" 7 | 8 | "github.com/jszwec/csvutil" 9 | ) 10 | 11 | type Strings []string 12 | 13 | func (s Strings) MarshalCSV() ([]byte, error) { 14 | return []byte(strings.Join(s, ",")), nil // strings.Join takes []string but it will also accept Strings 15 | } 16 | 17 | type StringMap map[string]string 18 | 19 | func (sm StringMap) MarshalCSV() ([]byte, error) { 20 | return []byte(fmt.Sprint(sm)), nil 21 | } 22 | 23 | func ExampleMarshal_sliceMap() { 24 | b, err := csvutil.Marshal([]struct { 25 | Strings Strings `csv:"strings"` 26 | Map StringMap `csv:"map"` 27 | }{ 28 | {[]string{"a", "b"}, map[string]string{"a": "1"}}, // no type casting is required for slice and map aliases 29 | {Strings{"c", "d"}, StringMap{"b": "1"}}, 30 | }) 31 | 32 | if err != nil { 33 | log.Fatal(err) 34 | } 35 | 36 | fmt.Printf("%s\n", b) 37 | 38 | // Output: 39 | // strings,map 40 | // "a,b",map[a:1] 41 | // "c,d",map[b:1] 42 | } 43 | -------------------------------------------------------------------------------- /example_marshal_test.go: -------------------------------------------------------------------------------- 1 | package csvutil_test 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/jszwec/csvutil" 8 | ) 9 | 10 | func ExampleMarshal() { 11 | type Address struct { 12 | City string 13 | Country string 14 | } 15 | 16 | type User struct { 17 | Name string 18 | Address 19 | Age int `csv:"age,omitempty"` 20 | CreatedAt time.Time 21 | } 22 | 23 | users := []User{ 24 | { 25 | Name: "John", 26 | Address: Address{"Boston", "USA"}, 27 | Age: 26, 28 | CreatedAt: time.Date(2010, 6, 2, 12, 0, 0, 0, time.UTC), 29 | }, 30 | { 31 | Name: "Alice", 32 | Address: Address{"SF", "USA"}, 33 | }, 34 | } 35 | 36 | b, err := csvutil.Marshal(users) 37 | if err != nil { 38 | fmt.Println("error:", err) 39 | } 40 | fmt.Println(string(b)) 41 | 42 | // Output: 43 | // Name,City,Country,age,CreatedAt 44 | // John,Boston,USA,26,2010-06-02T12:00:00Z 45 | // Alice,SF,USA,,0001-01-01T00:00:00Z 46 | } 47 | -------------------------------------------------------------------------------- /example_unmarshal_test.go: -------------------------------------------------------------------------------- 1 | package csvutil_test 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/jszwec/csvutil" 8 | ) 9 | 10 | func ExampleUnmarshal() { 11 | var csvInput = []byte(` 12 | name,age,CreatedAt 13 | jacek,26,2012-04-01T15:00:00Z 14 | john,,0001-01-01T00:00:00Z`, 15 | ) 16 | 17 | type User struct { 18 | Name string `csv:"name"` 19 | Age int `csv:"age,omitempty"` 20 | CreatedAt time.Time 21 | } 22 | 23 | var users []User 24 | if err := csvutil.Unmarshal(csvInput, &users); err != nil { 25 | fmt.Println("error:", err) 26 | } 27 | 28 | for _, u := range users { 29 | fmt.Printf("%+v\n", u) 30 | } 31 | 32 | // Output: 33 | // {Name:jacek Age:26 CreatedAt:2012-04-01 15:00:00 +0000 UTC} 34 | // {Name:john Age:0 CreatedAt:0001-01-01 00:00:00 +0000 UTC} 35 | } 36 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/jszwec/csvutil 2 | 3 | go 1.18 4 | -------------------------------------------------------------------------------- /interface.go: -------------------------------------------------------------------------------- 1 | package csvutil 2 | 3 | // Reader provides the interface for reading a single CSV record. 4 | // 5 | // If there is no data left to be read, Read returns (nil, io.EOF). 6 | // 7 | // It is implemented by csv.Reader. 8 | type Reader interface { 9 | Read() ([]string, error) 10 | } 11 | 12 | // Writer provides the interface for writing a single CSV record. 13 | // 14 | // It is implemented by csv.Writer. 15 | type Writer interface { 16 | Write([]string) error 17 | } 18 | 19 | // Unmarshaler is the interface implemented by types that can unmarshal 20 | // a single record's field description of themselves. 21 | type Unmarshaler interface { 22 | UnmarshalCSV([]byte) error 23 | } 24 | 25 | // Marshaler is the interface implemented by types that can marshal themselves 26 | // into valid string. 27 | type Marshaler interface { 28 | MarshalCSV() ([]byte, error) 29 | } 30 | -------------------------------------------------------------------------------- /tag.go: -------------------------------------------------------------------------------- 1 | package csvutil 2 | 3 | import ( 4 | "reflect" 5 | "strings" 6 | ) 7 | 8 | type tag struct { 9 | name string 10 | prefix string 11 | empty bool 12 | omitEmpty bool 13 | ignore bool 14 | inline bool 15 | } 16 | 17 | func parseTag(tagname string, field reflect.StructField) (t tag) { 18 | tags := strings.Split(field.Tag.Get(tagname), ",") 19 | if len(tags) == 1 && tags[0] == "" { 20 | t.name = field.Name 21 | t.empty = true 22 | return 23 | } 24 | 25 | switch tags[0] { 26 | case "-": 27 | t.ignore = true 28 | return 29 | case "": 30 | t.name = field.Name 31 | default: 32 | t.name = tags[0] 33 | } 34 | 35 | for _, tagOpt := range tags[1:] { 36 | switch tagOpt { 37 | case "omitempty": 38 | t.omitEmpty = true 39 | case "inline": 40 | if walkType(field.Type).Kind() == reflect.Struct { 41 | t.inline = true 42 | t.prefix = tags[0] 43 | } 44 | } 45 | } 46 | return 47 | } 48 | --------------------------------------------------------------------------------