diff --git a/README.md b/README.md index 7e30de0..44d3eda 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,8 @@ CSV++ extends traditional CSV to support **arrays** and **structured fields** wi ## Requirements -- Go 1.24 or later +- Go 1.25 or later +- `GOEXPERIMENT=jsonv2` environment variable (required by `csvpputil` package, which uses `encoding/json/jsontext`) ## Installation @@ -29,6 +30,13 @@ CSV++ extends traditional CSV to support **arrays** and **structured fields** wi go get github.com/osamingo/go-csvpp ``` +When building or testing packages that depend on `csvpputil`, set the experiment flag: + +```bash +GOEXPERIMENT=jsonv2 go build ./... +GOEXPERIMENT=jsonv2 go test ./... +``` + ## Quick Start ### Reading CSV++ Data @@ -234,29 +242,17 @@ writer.Flush() // Flush buffer ### Marshal/Unmarshal ```go -// Unmarshal CSV++ data into structs +// Unmarshal CSV++ data into structs (r is io.Reader) var people []Person -err := csvpp.Unmarshal(reader, &people) +err := csvpp.Unmarshal(r, &people) -// Marshal structs to CSV++ data -err := csvpp.Marshal(writer, people) +// Marshal structs to CSV++ data (w is io.Writer) +err := csvpp.Marshal(w, people) ``` ### Struct Tags -Use `csvpp` struct tags to map fields: - -```go -type Record struct { - Name string `csvpp:"name"` // Simple field - Tags []string `csvpp:"tags[]"` // Array field - Location struct { // Structured field - Lat string - Lon string - } `csvpp:"geo(lat^lon)"` - Addresses []Address `csvpp:"addr[](street^city)"` // Array structured -} -``` +See [Struct Mapping](#struct-mapping) above for tag syntax and usage. ## JSON/YAML Conversion (csvpputil) diff --git a/cmd/csvpp/README.md b/cmd/csvpp/README.md index eeb7aac..146af2a 100644 --- a/cmd/csvpp/README.md +++ b/cmd/csvpp/README.md @@ -5,15 +5,17 @@ A command-line tool for working with CSV++ files. ## Installation ```bash -go install github.com/osamingo/go-csvpp/cmd/csvpp@latest +GOEXPERIMENT=jsonv2 go install github.com/osamingo/go-csvpp/cmd/csvpp@latest ``` Or build from source: ```bash -go build -o csvpp ./cmd/csvpp +GOEXPERIMENT=jsonv2 go build -o csvpp ./cmd/csvpp ``` +> **Note:** `GOEXPERIMENT=jsonv2` is required because this tool depends on `encoding/json/jsontext` (Go 1.25+). + ## Commands ### validate @@ -82,9 +84,15 @@ cat input.csvpp | csvpp view | `↑` / `↓` | Navigate rows | | `Space` | Toggle row selection | | `y` / `c` | Copy header + selected rows to clipboard (CSV++ format) | -| `Esc` | Clear selection | +| `/` | Open filter input | +| `Enter` | Apply filter (in filter mode) | +| `Esc` | Cancel filter / Clear active filter / Clear selection | | `q` / `Ctrl+C` | Quit | +**Filter syntax:** +- Type text to search all columns (e.g., `Alice`) +- Use `column:value` to search a specific column (e.g., `name:Alice`) + **Note:** When stdin is not a TTY (e.g., in a pipe), a plain text table is displayed instead of the interactive TUI. ## Examples diff --git a/csvpputil/README.md b/csvpputil/README.md index 3026ff4..5b91c8d 100644 --- a/csvpputil/README.md +++ b/csvpputil/README.md @@ -42,7 +42,9 @@ if err := w.Close(); err != nil { #### YAMLArrayWriter ```go -w := csvpputil.NewYAMLArrayWriter(os.Stdout, headers) +w := csvpputil.NewYAMLArrayWriter(os.Stdout, headers, + csvpputil.WithYAMLCapacity(1000), // optional: pre-allocate buffer +) for _, record := range records { if err := w.Write(record); err != nil { @@ -55,6 +57,9 @@ if err := w.Close(); err != nil { } ``` +**Options:** +- `WithYAMLCapacity(n)` - Pre-allocates the internal buffer for `n` records, reducing memory allocations when the approximate record count is known. + **Note:** YAML output is buffered until `Close()` due to go-yaml library constraints. ### Convenience Functions diff --git a/export_test.go b/export_test.go index cdcdbbc..4368764 100644 --- a/export_test.go +++ b/export_test.go @@ -15,3 +15,8 @@ var ( ExtractTagName = extractTagName CachedTypeInfo = cachedTypeInfo ) + +// ReaderLine returns the current line number of the reader for testing. +func ReaderLine(r *Reader) int { + return r.line +} diff --git a/reader.go b/reader.go index 3f524cc..c6de9ce 100644 --- a/reader.go +++ b/reader.go @@ -85,6 +85,7 @@ func (r *Reader) ReadAll() ([][]*Field, error) { result := make([][]*Field, 0, len(records)) for _, record := range records { + r.line++ fields, err := r.parseRecord(record) if err != nil { return nil, err diff --git a/reader_test.go b/reader_test.go index 4a41017..3ab2dad 100644 --- a/reader_test.go +++ b/reader_test.go @@ -576,6 +576,23 @@ func TestReader_ComponentsMoreThanHeaders(t *testing.T) { } } +func TestReader_ReadAll_LineTracking(t *testing.T) { + t.Parallel() + + input := "name,age\nAlice,30\nBob,25\nCharlie,35\n" + r := csvpp.NewReader(strings.NewReader(input)) + + _, err := r.ReadAll() + if err != nil { + t.Fatalf("Reader.ReadAll() error = %v", err) + } + + // After reading header (line 1) + 3 data records, line should be 4. + if got := csvpp.ReaderLine(r); got != 4 { + t.Errorf("Reader.ReadAll() line = %d, want 4", got) + } +} + func TestReader_ArrayStructuredInComponents(t *testing.T) { t.Parallel() diff --git a/writer.go b/writer.go index bae7eba..31028f5 100644 --- a/writer.go +++ b/writer.go @@ -162,6 +162,9 @@ func formatComponentList(components []*ColumnHeader, delim rune) string { // formatField converts a Field to a string. func (w *Writer) formatField(header *ColumnHeader, field *Field) string { + if field == nil { + return "" + } if header == nil { return field.Value } diff --git a/writer_test.go b/writer_test.go index 50d9ae0..6e60b74 100644 --- a/writer_test.go +++ b/writer_test.go @@ -506,6 +506,35 @@ func TestWriter_NoHeaderForRecord(t *testing.T) { } } +func TestWriter_Write_NilField(t *testing.T) { + t.Parallel() + + headers := []*csvpp.ColumnHeader{ + {Name: "name", Kind: csvpp.SimpleField}, + {Name: "age", Kind: csvpp.SimpleField}, + } + record := []*csvpp.Field{ + {Value: "Alice"}, + nil, // nil field should not panic + } + + var buf bytes.Buffer + w := csvpp.NewWriter(&buf) + w.SetHeaders(headers) + + err := w.Write(record) + if err != nil { + t.Fatalf("Writer.Write() error = %v", err) + } + + w.Flush() + want := "Alice,\n" + got := buf.String() + if diff := cmp.Diff(want, got); diff != "" { + t.Errorf("Writer.Write() mismatch (-want +got):\n%s", diff) + } +} + func TestFormatComponentList(t *testing.T) { t.Parallel()