tabnas-csv (Rust)
The CSV grammar plugin for the
tabnas parsing engine, crate
tabnas_csv.
CSV text becomes an array of objects, one per row and keyed by the header
row, or an array of arrays. Quoting follows RFC 4180 ("" escapes an
embedded quote, a quoted field may hold the separator, a line break, or
a control character such as a tab),
the field and record separators are configurable, records can be streamed
as they are parsed, and a non-strict mode lets a field body hold embedded
jsonic ([1,2], {x:1}). The plugin is not standalone: it layers on the
relaxed-JSON grammar of
tabnas-jsonic, and reuses the
engine's lexer, comment handling, and lifecycle hooks.
This is the Rust port of the canonical TypeScript implementation in
../ts; the TypeScript version is authoritative and this crate
tracks it. The Go port is in ../go. All three embed the one
grammar, ../csv-grammar.jsonic.
Use
parse builds one parser on first use and reuses it. Building the parser
(installing the jsonic base, then the CSV grammar over it) costs far more
than a small parse, so for anything but a one-off call build an instance
once and keep it:
Options are a struct, with the defaults the TypeScript plugin documents.
object: false gives arrays, header: false keeps the first row as data,
field.separation and record.separators change the separators, and
strict: false reads a field body as jsonic:
use ;
The same options travel as a JSON bag through the engine's plugin mechanism, which is how the shared fixtures configure a parser per row and how a derived instance rebuilds the grammar:
use Value;
To stream records instead of collecting them, give the options a
Stream; the result is then an empty array:
use ;
use ;
Parse errors are the engine's TabnasError, re-exported as CsvError,
with code, row, col and a report that shows the offending source.
The codes this plugin adds are csv_extra_field and csv_missing_field,
raised under field.exact; unexpected and unterminated_string come
from the engine.
Install
The crate is published on crates.io as tabnas-csv, and so are the
engine, tabnas-parser, and the jsonic base, tabnas-jsonic:
All four entries are needed. A crate's dependencies are not passed on to
its dependents, so tabnas-csv alone does not put tabnas,
tabnas-jsonic or serde_json in your extern prelude, and the examples
above that name tabnas::Value, tabnas_jsonic::make or
serde_json::json! would not resolve. The engine's library is named
tabnas in code. Only CsvError is re-exported.
serde_json is declared because the option bag is a tabnas::Value and
Value::from_json takes a serde_json::Value; a program that uses only
the CsvOptions struct can leave it out.
This repository's own rs/Cargo.toml names the engine, jsonic and, for
the test suite's shared fixture runner, tabnas-support by path, and
jsonic takes tabnas-json the same way. A build here therefore needs
parser, jsonic, json and support from https://github.com/tabnas/
cloned beside it. The release workflow swaps those paths for crates.io
versions when it publishes the crate.
Differences from the canonical TypeScript
Every parse result is the TypeScript one: the shared fixtures in
../test/spec, the manifest corpus in
../test/fixtures and the two third-party
conformance corpora hold all three runtimes to it. What differs is the
shape of the API and a few points where the host language has no way to
say what JavaScript says:
- Options are a struct, or a JSON bag.
make_withtakes aCsvOptions;plugin()reads the baguse_pluginmerges over the defaults. The three-statetrim,comment,numberandvalueareOption<bool>, withNonestanding for the TypeScriptnull. - Streaming is a typed callback. A closure cannot travel in the
option bag, so
Streamsits onCsvOptionsoutside serialization and reaches the plugin throughplugin_withandmake_with. There is no'error'event: a failed parse returnsErr, as it does in Go. parseis a convenience the other runtimes lack. It keeps one default instance behind aOnceLock, which is the reuse the TypeScript and Go suites tell a caller to arrange by hand.- Records keep document order. An object record is a
Value::Objectover anIndexMap, so the columns come back in header order without the JavaScript object's own ordering rules. - No token descriptions. The TypeScript plugin registers human descriptions of the CSV tokens for the railroad diagram tool. The Rust engine has no such table and no diagram tool reads it, so that registration has no counterpart.
- Lone surrogates fold to U+FFFD, and the regular expression dialect
is the
regexcrate's. Both come from the engine, and both are recorded there.
Build and test
The engine, the jsonic base, the JSON core it needs, and the fixture runner are path dependencies on sibling checkouts, so there is nothing to fetch for the build:
&&
Or, from the repository root, make test-rs. For what CI would say,
including formatting, Clippy and the lockfile check, run
ci/rust/run.sh.
The suite runs every shared ../test/spec/*.tsv fixture through the
shared runner, building a fresh parser from each row's opts column, and
the whole ../test/fixtures manifest corpus. The conformance tests judge
the two third-party corpora (csv-spectrum and Go's encoding/csv
readTests) with the same divergence table the TypeScript and Go suites
carry; they read the corpora vendored under ../test/suites, with no
network, and FAIL, never skip, when a corpus is missing. Beside them are the
in-language tests: the API surface, streaming, typed option values, the
field.exact messages, the exported string matcher on a plain jsonic
instance, the shared default parser under threads, instance reuse, the
version sites, and the embedded grammar against the file on disk.
License
MIT.