├── .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 [](https://pkg.go.dev/github.com/jszwec/csvutil?tab=doc)  [](https://goreportcard.com/report/github.com/jszwec/csvutil) [](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 |
--------------------------------------------------------------------------------