zlight_csv 0.4.0-aarch64-linux → 0.6.0-aarch64-linux
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/ARCHITECTURE.md +132 -0
- data/CHANGELOG.md +149 -12
- data/LICENSE +1 -1
- data/README.md +108 -6
- data/VERSION +1 -1
- data/ext/zlight_csv/Cargo.toml +30 -0
- data/ext/zlight_csv/extconf.rb +16 -0
- data/ext/zlight_csv/src/convert.rs +203 -0
- data/ext/zlight_csv/src/error.rs +128 -0
- data/ext/zlight_csv/src/lib.rs +57 -0
- data/ext/zlight_csv/src/options.rs +143 -0
- data/ext/zlight_csv/src/reader.rs +26 -0
- data/ext/zlight_csv/src/row.rs +85 -0
- data/lib/zlight_csv/3.1/zlight_csv.so +0 -0
- data/lib/zlight_csv/3.2/zlight_csv.so +0 -0
- data/lib/zlight_csv/3.3/zlight_csv.so +0 -0
- data/lib/zlight_csv/3.4/zlight_csv.so +0 -0
- data/lib/zlight_csv/4.0/zlight_csv.so +0 -0
- data/lib/zlight_csv/errors.rb +20 -0
- data/lib/zlight_csv/native.rb +20 -0
- data/lib/zlight_csv.rb +135 -256
- metadata +39 -10
- data/lib/zlight_csv/3.0/zlight_csv.so +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 909be5fc73a40e80c24d2de7033a47f2b39e524f841debc2d04e189a47d7b566
|
|
4
|
+
data.tar.gz: 25c98525efbad42ceb05a1094d01d5dcd97b2b0d2b5babae432786a018840af5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4a05435ca0b75311665460f8e52637e3bfb170c266f29e9a37265208f27232ea923e4f4eedf8609d04da19972baf422fdd5e67dc016051786d9a69772c8ec1af
|
|
7
|
+
data.tar.gz: 8217badf7d1e3e146d8e9e519d0868ae043a5a1b8bb8189084336090966a580f3ca78ce4f069148fb725c20639a6b29ec25068fa2f8d88f606fac38a6b2a9af1
|
data/ARCHITECTURE.md
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# Architecture
|
|
2
|
+
|
|
3
|
+
This is a guide for changing the code, written on the assumption that you know
|
|
4
|
+
Ruby or Python and may not know Rust. You do not need to read Rust fluently to
|
|
5
|
+
find your way around — the layout is designed so that a given change lives in
|
|
6
|
+
one predictable place.
|
|
7
|
+
|
|
8
|
+
## The short version
|
|
9
|
+
|
|
10
|
+
ZLight is a **Ruby gem with its hot loop written in Rust**. Ruby handles the
|
|
11
|
+
public API and file conveniences; Rust does the parsing and writing.
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
your code
|
|
15
|
+
│
|
|
16
|
+
▼
|
|
17
|
+
lib/zlight_csv.rb Ruby: the public API
|
|
18
|
+
│ read / write / foreach / open are plain Ruby
|
|
19
|
+
│ parse / generate / stream / stream_file are not —
|
|
20
|
+
▼ they are implemented in Rust
|
|
21
|
+
ext/zlight_csv/src/ Rust: the parser, the writer, the streaming reader
|
|
22
|
+
│
|
|
23
|
+
▼
|
|
24
|
+
the `csv` crate the actual CSV state machine
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
The `magnus` crate is the bridge: it is what lets Rust define a module Ruby can
|
|
28
|
+
call and build Ruby objects (`Hash`, `Array`, `String`) from Rust data.
|
|
29
|
+
|
|
30
|
+
## Which file does what
|
|
31
|
+
|
|
32
|
+
### Ruby side — `lib/`
|
|
33
|
+
|
|
34
|
+
| File | Owns |
|
|
35
|
+
|---|---|
|
|
36
|
+
| `zlight_csv.rb` | the public API and its documentation |
|
|
37
|
+
| `zlight_csv/native.rb` | loading the compiled library |
|
|
38
|
+
| `zlight_csv/errors.rb` | the exception hierarchy |
|
|
39
|
+
| `zlight_csv/version.rb` | the version, read from the `VERSION` file |
|
|
40
|
+
|
|
41
|
+
`native.rb` is worth understanding once. A compiled extension is built against
|
|
42
|
+
one specific Ruby version's internals, so a precompiled gem ships **one library
|
|
43
|
+
per Ruby minor version**:
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
lib/zlight_csv/3.1/zlight_csv.so
|
|
47
|
+
lib/zlight_csv/3.4/zlight_csv.so
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
`native.rb` picks the right one at load time. A gem built from source has a
|
|
51
|
+
single library instead, which is the fallback path.
|
|
52
|
+
|
|
53
|
+
### Rust side — `ext/zlight_csv/src/`
|
|
54
|
+
|
|
55
|
+
| File | Owns | Analogy |
|
|
56
|
+
|---|---|---|
|
|
57
|
+
| `lib.rs` | the list of what Ruby can call | a routes file |
|
|
58
|
+
| `options.rs` | keyword arguments → typed settings | parsing params |
|
|
59
|
+
| `convert.rs` | one field → one Ruby value | a type coercer |
|
|
60
|
+
| `row.rs` | one record → one Ruby row | a serializer |
|
|
61
|
+
| `reader.rs` | building the CSV reader | a factory |
|
|
62
|
+
| `read/eager.rs` | `ZLight.parse` | |
|
|
63
|
+
| `read/stream.rs` | `ZLight::StreamReader` | |
|
|
64
|
+
| `write/mod.rs` | `ZLight.generate` | |
|
|
65
|
+
| `error.rs` | Rust errors → Ruby exceptions | an error handler |
|
|
66
|
+
|
|
67
|
+
The important structural point: **both read paths sit on the same three
|
|
68
|
+
helpers.** `read/eager.rs` and `read/stream.rs` differ only in *when* they read
|
|
69
|
+
— all at once, or a row at a time — and both build their reader with
|
|
70
|
+
`reader.rs`, convert fields with `convert.rs`, and shape rows with `row.rs`.
|
|
71
|
+
That is what stops the two from drifting apart.
|
|
72
|
+
|
|
73
|
+
## Where to put a change
|
|
74
|
+
|
|
75
|
+
| You want to… | Change | Why there |
|
|
76
|
+
|---|---|---|
|
|
77
|
+
| add a field type (dates, booleans, a Ruby proc) | `convert.rs` + one arm in `options.rs` | `Converter` is the only thing that turns bytes into values |
|
|
78
|
+
| change what a row looks like (a Row object, a struct) | `row.rs` | `RowShape` is the only thing that builds rows |
|
|
79
|
+
| add a parse or write option | `options.rs` | both readers and the writer read their settings from here |
|
|
80
|
+
| add a whole new Ruby method | a file under `read/` or `write/`, plus one line in `lib.rs` | `lib.rs` is the complete list of the public surface |
|
|
81
|
+
| change an error's Ruby class | `error.rs` | one table maps every Rust error to a Ruby class |
|
|
82
|
+
|
|
83
|
+
If a change seems to require editing two files that do the same kind of work,
|
|
84
|
+
that is a sign the seam is in the wrong place — say so rather than copying the
|
|
85
|
+
code.
|
|
86
|
+
|
|
87
|
+
## Two ideas that are easy to miss
|
|
88
|
+
|
|
89
|
+
**Conversion is a value, not a flag.** `Converter` is an enum — think of it as
|
|
90
|
+
a small sealed class hierarchy. It used to be a boolean (`convert_numeric`)
|
|
91
|
+
threaded through every function, which meant a second converter could not be
|
|
92
|
+
added without changing every signature it passed through. Now adding one is a
|
|
93
|
+
new variant plus an arm in `options.rs`.
|
|
94
|
+
|
|
95
|
+
**Row shape is a value too.** `RowShape` is either `Hashes(headers)` or
|
|
96
|
+
`Arrays`. Deciding once, up front, means the per-row code does not re-ask "do
|
|
97
|
+
we have headers?" for every row, and there is exactly one place that knows how
|
|
98
|
+
a row is built.
|
|
99
|
+
|
|
100
|
+
## Working on it
|
|
101
|
+
|
|
102
|
+
You do not need Ruby or Rust installed on your machine — everything runs in
|
|
103
|
+
containers:
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
./docker/test-matrix.sh # every supported Ruby, 3.1 through 4.0
|
|
107
|
+
./docker/test-matrix.sh 3.4 # just one
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
With a local Ruby (3.1–4.0) and a Rust toolchain:
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
bundle install
|
|
114
|
+
bundle exec rake compile # build the extension
|
|
115
|
+
bundle exec rake spec # run the suite
|
|
116
|
+
bundle exec rake clobber # remove every build artifact, cargo cache included
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
`rake compile` must be re-run after any change under `ext/`. Ruby-only changes
|
|
120
|
+
take effect immediately.
|
|
121
|
+
|
|
122
|
+
## The rule the tests enforce
|
|
123
|
+
|
|
124
|
+
The RSpec suite is the contract. A refactor that changes behaviour will fail
|
|
125
|
+
it; a refactor that does not, will not. If you find yourself editing a spec to
|
|
126
|
+
make a refactor pass, you are changing behaviour — stop and decide whether you
|
|
127
|
+
meant to.
|
|
128
|
+
|
|
129
|
+
Several examples deliberately pin places where ZLight **differs** from Ruby's
|
|
130
|
+
stdlib CSV (duplicate headers, empty fields, numeric edge cases). They assert
|
|
131
|
+
both sides on purpose, so the documented differences and the code cannot drift
|
|
132
|
+
apart. See the "Differences from stdlib CSV" section of the README.
|
data/CHANGELOG.md
CHANGED
|
@@ -7,22 +7,159 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
-
## [0.
|
|
10
|
+
## [0.6.0] - 2026-09-12
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- Custom converters. `converters:` now accepts any object answering `call`, a
|
|
15
|
+
built-in name, or an Array mixing the two, applied left to right:
|
|
16
|
+
|
|
17
|
+
```ruby
|
|
18
|
+
ZLight.parse(csv, converters: ->(field) { field.strip })
|
|
19
|
+
ZLight.parse(csv, converters: [->(f) { f.strip }, :numeric])
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Semantics match Ruby's CSV, including that the chain stops as soon as a
|
|
23
|
+
converter returns something other than a String, and that converters are
|
|
24
|
+
never applied to the header row. An unusable converter raises ArgumentError
|
|
25
|
+
naming the problem, where Ruby's CSV raises NoMethodError from inside itself.
|
|
26
|
+
|
|
27
|
+
A converter must not read from or close the reader it is converting for, and
|
|
28
|
+
a reader running a converter must not be used from another thread. Either
|
|
29
|
+
raises `ZLight::Error` rather than corrupting a row. `ZLight.parse` is safe
|
|
30
|
+
from any thread, with or without a converter; Ractors are refused by Ruby.
|
|
31
|
+
|
|
32
|
+
### Changed
|
|
33
|
+
|
|
34
|
+
- The extension is organised around three seams — field conversion, row shape
|
|
35
|
+
and reader construction — each owned by one file and shared by both read
|
|
36
|
+
paths, so a feature is added in one place rather than two. See
|
|
37
|
+
ARCHITECTURE.md
|
|
38
|
+
- Removed comments that restated the code they sat above, keeping those that
|
|
39
|
+
explain a decision, an invariant, or where new code belongs
|
|
40
|
+
|
|
41
|
+
### Fixed
|
|
42
|
+
|
|
43
|
+
- The documentation for `parse`, `generate` and `StreamReader` was attached to
|
|
44
|
+
no method, so YARD rendered none of it
|
|
45
|
+
|
|
46
|
+
## [0.5.1] - 2026-09-12
|
|
47
|
+
|
|
48
|
+
### Fixed
|
|
49
|
+
|
|
50
|
+
- The extension now builds against a Ruby that rb-sys has no prebuilt bindings
|
|
51
|
+
for, via rb-sys `stable-api-compiled-fallback`, rather than failing outright.
|
|
52
|
+
This is the mechanism that breaks the build on every new Ruby release until
|
|
53
|
+
rb-sys catches up
|
|
54
|
+
- Windows CI could not build the extension at all: the runner paired a MinGW
|
|
55
|
+
Ruby with an MSVC Rust toolchain. Ruby and Rust are now set up together so
|
|
56
|
+
their ABIs match, and Windows is verified across Ruby 3.1-4.0
|
|
57
|
+
- Specs that asserted Unix-only error messages and paths now pass on Windows
|
|
58
|
+
- Dropped an unreachable credentials check from the release workflow: the
|
|
59
|
+
credentials action fails first, so the check never ran
|
|
60
|
+
|
|
61
|
+
### Note
|
|
62
|
+
|
|
63
|
+
- 0.5.0 was tagged but never reached RubyGems: its release run built every gem
|
|
64
|
+
and then failed to publish, because trusted publishing still named the
|
|
65
|
+
project's former GitHub account. 0.5.1 is the first release carrying the
|
|
66
|
+
0.5.0 changes below.
|
|
67
|
+
|
|
68
|
+
## [0.5.0] - 2026-09-12
|
|
69
|
+
|
|
70
|
+
### Added
|
|
71
|
+
|
|
72
|
+
- Ruby 4.0 support
|
|
73
|
+
- `docker/test-matrix.sh`, which builds and tests the extension against every
|
|
74
|
+
supported Ruby in containers
|
|
75
|
+
- Windows, Ruby 4.0 and a `ruby-head` early-warning job in CI, plus weekly
|
|
76
|
+
scheduled runs and Dependabot
|
|
77
|
+
|
|
78
|
+
### Changed
|
|
79
|
+
|
|
80
|
+
- Upgraded magnus from 0.6 to 0.8. magnus 0.6 read a field of Ruby's
|
|
81
|
+
RTypedData struct that Ruby 4.0 removed, so the extension could not compile
|
|
82
|
+
against it at all
|
|
83
|
+
- Minimum Ruby is now 3.1. Ruby 3.0 is end-of-life and no longer supported by
|
|
84
|
+
magnus
|
|
85
|
+
- Repository URLs now point at `codebyisaad/zlight`, which is where the code
|
|
86
|
+
is hosted
|
|
87
|
+
|
|
88
|
+
### Fixed
|
|
89
|
+
|
|
90
|
+
- The source gem shipped no extension and could not be compiled, so any Ruby
|
|
91
|
+
or platform without a prebuilt binary installed a gem that failed to load
|
|
92
|
+
- `ZLight::ParseError`, `ZLight::EncodingError` and `ZLight::StreamClosedError`
|
|
93
|
+
were documented but never raised
|
|
94
|
+
- Integers outside the i64 range were silently converted to lossy Floats
|
|
95
|
+
- `col_sep` and `quote_char` longer than one byte were silently truncated when
|
|
96
|
+
reading, while writing rejected them
|
|
97
|
+
- Releases no longer yank every previously published version
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
## [0.4.0] - 2026-06-06
|
|
101
|
+
|
|
102
|
+
### Added
|
|
103
|
+
|
|
104
|
+
- `ZLight.generate` — build a CSV string from an array of hashes or an array
|
|
105
|
+
of arrays, with `headers`, `col_sep`, `quote_char` and `force_quotes` options
|
|
106
|
+
- `ZLight.write` — generate a CSV and write it to a file in one call
|
|
107
|
+
|
|
108
|
+
### Changed
|
|
109
|
+
|
|
110
|
+
- Parsed fields are tagged with the encoding of the input string, matching
|
|
111
|
+
Ruby's stdlib CSV, so the parser and writer round-trip without
|
|
112
|
+
`force_encoding`
|
|
113
|
+
|
|
114
|
+
## [0.3.0] - 2026-05-21
|
|
115
|
+
|
|
116
|
+
### Changed
|
|
117
|
+
|
|
118
|
+
- Release pipeline updates
|
|
119
|
+
|
|
120
|
+
## [0.2.5] - 2026-05-21
|
|
121
|
+
|
|
122
|
+
### Added
|
|
123
|
+
|
|
124
|
+
- `ZLight.stream` and `ZLight.stream_file` — read rows one at a time without
|
|
125
|
+
loading the whole input into memory
|
|
126
|
+
- `ZLight.open` — streaming with a block, closing the reader automatically
|
|
127
|
+
- `ZLight::StreamReader`, including `Enumerable`, so `lazy`, `select`, `find`
|
|
128
|
+
and friends work over a stream
|
|
129
|
+
|
|
130
|
+
### Fixed
|
|
131
|
+
|
|
132
|
+
- Packaging and gemspec metadata corrections across 0.2.1–0.2.5
|
|
133
|
+
|
|
134
|
+
## [0.2.0] - 2026-05-21
|
|
135
|
+
|
|
136
|
+
### Added
|
|
137
|
+
|
|
138
|
+
- Benchmark suite comparing ZLight against Ruby's stdlib CSV
|
|
139
|
+
- Edge case coverage for quoting, encodings and flexible records
|
|
140
|
+
|
|
141
|
+
## [0.1.2] - 2026-05-19
|
|
11
142
|
|
|
12
143
|
### Added
|
|
13
144
|
|
|
14
145
|
- Initial release
|
|
15
|
-
- `
|
|
16
|
-
|
|
17
|
-
- `
|
|
18
|
-
- `
|
|
19
|
-
-
|
|
20
|
-
- Support for custom quote characters (`quote_char`)
|
|
21
|
-
- Flexible record length support
|
|
22
|
-
- Enum-based error handling with descriptive messages
|
|
23
|
-
- Cross-platform native gem builds
|
|
146
|
+
- `ZLight.parse` — parse CSV strings, with `headers`, `converters`, `col_sep`,
|
|
147
|
+
`quote_char` and `flexible` options
|
|
148
|
+
- `ZLight.read` — read and parse a CSV file
|
|
149
|
+
- `ZLight.foreach` — iterate over parsed rows
|
|
150
|
+
- Cross-platform precompiled native gems
|
|
24
151
|
|
|
25
152
|
### Performance
|
|
26
153
|
|
|
27
|
-
-
|
|
28
|
-
|
|
154
|
+
- Substantially faster than Ruby's stdlib CSV; see the README for measured
|
|
155
|
+
figures
|
|
156
|
+
|
|
157
|
+
[Unreleased]: https://github.com/codebyisaad/zlight/compare/v0.6.0...HEAD
|
|
158
|
+
[0.6.0]: https://github.com/codebyisaad/zlight/compare/v0.5.1...v0.6.0
|
|
159
|
+
[0.5.1]: https://github.com/codebyisaad/zlight/compare/v0.5.0...v0.5.1
|
|
160
|
+
[0.5.0]: https://github.com/codebyisaad/zlight/compare/v0.4.0...v0.5.0
|
|
161
|
+
[0.4.0]: https://github.com/codebyisaad/zlight/compare/v0.3.0...v0.4.0
|
|
162
|
+
[0.3.0]: https://github.com/codebyisaad/zlight/compare/v0.2.5...v0.3.0
|
|
163
|
+
[0.2.5]: https://github.com/codebyisaad/zlight/compare/v0.2.0...v0.2.5
|
|
164
|
+
[0.2.0]: https://github.com/codebyisaad/zlight/compare/v0.1.2...v0.2.0
|
|
165
|
+
[0.1.2]: https://github.com/codebyisaad/zlight/releases/tag/v0.1.2
|
data/LICENSE
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
MIT License
|
|
2
2
|
|
|
3
|
-
Copyright (c) 2024 Zaidan Chaudhary
|
|
3
|
+
Copyright (c) 2024 Zaidan Chaudhary (also known as Saad Chaudhary)
|
|
4
4
|
|
|
5
5
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
6
|
of this software and associated documentation files (the "Software"), to deal
|
data/README.md
CHANGED
|
@@ -7,7 +7,8 @@ A fast CSV parser for Ruby, powered by Rust.
|
|
|
7
7
|
|
|
8
8
|
## Why ZLight?
|
|
9
9
|
|
|
10
|
-
Ruby's built-in CSV library is slow. ZLight parses CSV files **up to
|
|
10
|
+
Ruby's built-in CSV library is slow. ZLight parses CSV files **up to 40x faster** by using Rust under the hood.
|
|
11
|
+
The gap is widest on small inputs and narrows as they grow; the measurements below show the range.
|
|
11
12
|
|
|
12
13
|
### Benchmark Results
|
|
13
14
|
|
|
@@ -102,12 +103,64 @@ ZLight.parse (full): 73ms
|
|
|
102
103
|
ZLight.stream (lazy): 0.3ms ← 5,600x faster!
|
|
103
104
|
```
|
|
104
105
|
|
|
106
|
+
### Custom Converters
|
|
107
|
+
|
|
108
|
+
`converters:` takes a built-in name, any object answering `call`, or an Array
|
|
109
|
+
of them applied left to right:
|
|
110
|
+
|
|
111
|
+
```ruby
|
|
112
|
+
# A single callable, given each field as a String
|
|
113
|
+
ZLight.parse(csv, converters: ->(field) { field.strip })
|
|
114
|
+
|
|
115
|
+
# Chained: trim, then recognise numbers
|
|
116
|
+
ZLight.parse(csv, converters: [->(f) { f.strip }, :numeric])
|
|
117
|
+
|
|
118
|
+
# Anything answering #call works
|
|
119
|
+
ZLight.parse(csv, converters: MyConverter.new)
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Two rules, both matching Ruby's CSV:
|
|
123
|
+
|
|
124
|
+
- **The chain stops** as soon as a converter returns something other than a
|
|
125
|
+
String, so `[:numeric, other]` never calls `other` for a field that was
|
|
126
|
+
converted to a number.
|
|
127
|
+
- **Converters never apply to the header row.** Headers are always symbols.
|
|
128
|
+
|
|
129
|
+
An exception raised inside a converter propagates to the caller. A converter
|
|
130
|
+
that is neither a known name nor callable raises `ArgumentError` naming the
|
|
131
|
+
problem.
|
|
132
|
+
|
|
133
|
+
A converter must not use the reader it is converting for — calling `next_row`,
|
|
134
|
+
`headers` or `close` on it raises `ZLight::Error`. Any other reader, and
|
|
135
|
+
`ZLight.parse` itself, are fine:
|
|
136
|
+
|
|
137
|
+
```ruby
|
|
138
|
+
# Raises ZLight::Error
|
|
139
|
+
reader = ZLight.stream(csv, converters: ->(f) { reader.next_row })
|
|
140
|
+
|
|
141
|
+
# Fine
|
|
142
|
+
ZLight.parse(csv, converters: ->(f) { ZLight.parse(lookup_csv).first[:name] })
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### Threads and Ractors
|
|
146
|
+
|
|
147
|
+
| | |
|
|
148
|
+
|---|---|
|
|
149
|
+
| `ZLight.parse`, `.generate`, `.read`, `.write` | safe from any thread |
|
|
150
|
+
| A `StreamReader` shared between threads | safe while no converter is running |
|
|
151
|
+
| A `StreamReader` shared between threads, with a converter | raises `ZLight::Error` |
|
|
152
|
+
| Ractors | not supported; Ruby refuses to cross the boundary |
|
|
153
|
+
|
|
154
|
+
A converter runs Ruby code, which lets another thread take the GVL mid-row. The
|
|
155
|
+
reader refuses that rather than returning a corrupted row. **Give each thread
|
|
156
|
+
its own reader** and the question does not arise.
|
|
157
|
+
|
|
105
158
|
### Options (Reading)
|
|
106
159
|
|
|
107
160
|
| Option | Default | Description |
|
|
108
161
|
|--------|---------|-------------|
|
|
109
162
|
| `headers` | `true` | Use first row as headers (returns hashes). Set `false` for arrays. |
|
|
110
|
-
| `converters` | `nil` |
|
|
163
|
+
| `converters` | `nil` | `:numeric`, any object answering `call`, or an Array of them |
|
|
111
164
|
| `col_sep` | `","` | Column separator (`"\t"` for TSV, `";"` for European CSV) |
|
|
112
165
|
| `quote_char` | `"` | Quote character |
|
|
113
166
|
| `flexible` | `true` | Allow rows with varying column counts |
|
|
@@ -156,21 +209,70 @@ CSV.parse(data, headers: true, header_converters: :symbol, converters: :numeric)
|
|
|
156
209
|
ZLight.parse(data, converters: :numeric)
|
|
157
210
|
```
|
|
158
211
|
|
|
212
|
+
### Differences from stdlib CSV
|
|
213
|
+
|
|
214
|
+
ZLight is not a complete reimplementation of `CSV`. The differences below are
|
|
215
|
+
deliberate, and are the ones most likely to matter when migrating.
|
|
216
|
+
|
|
217
|
+
**API shape**
|
|
218
|
+
|
|
219
|
+
- `ZLight.foreach` takes a **CSV string**, while `CSV.foreach` takes a **file
|
|
220
|
+
path**. Use `ZLight.open` to iterate a file.
|
|
221
|
+
- `ZLight.foreach` parses the whole input before yielding. For genuinely lazy
|
|
222
|
+
iteration use `ZLight.stream` or `ZLight.open`.
|
|
223
|
+
- `ZLight::StreamReader` is single-pass. It includes `Enumerable`, but each
|
|
224
|
+
row is consumed as it is read, so a second pass yields nothing and there is
|
|
225
|
+
no rewind. Call `ZLight.stream` again to re-read.
|
|
226
|
+
|
|
227
|
+
**Parsing**
|
|
228
|
+
|
|
229
|
+
- Duplicate headers collapse. Rows are `Hash`es, so `"a,a"` keeps only the
|
|
230
|
+
last `:a` column; `CSV` keeps both.
|
|
231
|
+
- Fields beyond the header count are dropped rather than collected.
|
|
232
|
+
- Headers become symbols always, equivalent to `header_converters: :symbol`.
|
|
233
|
+
|
|
234
|
+
**`converters: :numeric`**
|
|
235
|
+
|
|
236
|
+
| Input | ZLight | Ruby CSV |
|
|
237
|
+
|--------------|-------------------|------------|
|
|
238
|
+
| `""` | `""` | `nil` |
|
|
239
|
+
| `"0x10"` | `"0x10"` | `16` |
|
|
240
|
+
| `"1_000"` | `"1_000"` | `1000` |
|
|
241
|
+
| `"Infinity"` | `Float::INFINITY` | `"Infinity"` |
|
|
242
|
+
| `"NaN"` | `Float::NAN` | `"NaN"` |
|
|
243
|
+
|
|
244
|
+
Integers of any size are exact, as in `CSV`.
|
|
245
|
+
|
|
246
|
+
**Writing**
|
|
247
|
+
|
|
248
|
+
- `ZLight.generate` takes its column order from the keys of the **first**
|
|
249
|
+
hash. Keys that appear only in later rows are not written; keys missing
|
|
250
|
+
from a later row are written as empty fields.
|
|
251
|
+
|
|
159
252
|
## Requirements
|
|
160
253
|
|
|
161
|
-
- Ruby 3.0
|
|
254
|
+
- Ruby 3.1 or newer, including Ruby 4.0
|
|
162
255
|
- Linux (x86_64, aarch64), macOS (Intel, Apple Silicon), or Windows (x64)
|
|
163
256
|
|
|
257
|
+
Every supported Ruby is built and tested on each change; see
|
|
258
|
+
`docker/test-matrix.sh` to run that matrix yourself. Rubies without a
|
|
259
|
+
precompiled binary install from the source gem and need a Rust toolchain.
|
|
260
|
+
|
|
164
261
|
## Roadmap
|
|
165
262
|
|
|
166
263
|
- [x] Streaming/lazy parsing for large files
|
|
167
264
|
- [x] CSV writing support
|
|
168
|
-
- [
|
|
265
|
+
- [x] Custom converter procs
|
|
266
|
+
- [ ] Streaming writer for large output
|
|
169
267
|
|
|
170
268
|
## Contributing
|
|
171
269
|
|
|
172
|
-
Bug reports and pull requests are welcome on [GitHub](https://github.com/
|
|
270
|
+
Bug reports and pull requests are welcome on [GitHub](https://github.com/codebyisaad/zlight).
|
|
271
|
+
|
|
272
|
+
## Author
|
|
273
|
+
|
|
274
|
+
Zaidan Chaudhary, who also publishes as Saad Chaudhary.
|
|
173
275
|
|
|
174
276
|
## License
|
|
175
277
|
|
|
176
|
-
MIT
|
|
278
|
+
MIT. See [LICENSE](LICENSE).
|
data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.
|
|
1
|
+
0.6.0
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
[package]
|
|
2
|
+
name = "zlight_csv"
|
|
3
|
+
# Synced from ../../VERSION by extconf.rb — do not edit manually.
|
|
4
|
+
version = "0.6.0"
|
|
5
|
+
edition = "2021"
|
|
6
|
+
authors = ["Zaidan Chaudhary", "Saad Chaudhary"]
|
|
7
|
+
description = "High-performance CSV parser for Ruby, powered by Rust"
|
|
8
|
+
license = "MIT"
|
|
9
|
+
repository = "https://github.com/codebyisaad/zlight"
|
|
10
|
+
|
|
11
|
+
[lib]
|
|
12
|
+
crate-type = ["cdylib"]
|
|
13
|
+
path = "src/lib.rs"
|
|
14
|
+
|
|
15
|
+
[dependencies]
|
|
16
|
+
csv = "1"
|
|
17
|
+
magnus = "0.8"
|
|
18
|
+
# Depended on directly, only to turn on stable-api-compiled-fallback.
|
|
19
|
+
# rb-sys ships prebuilt bindings for Ruby versions it knows about and fails
|
|
20
|
+
# outright on ones it does not, which is what breaks the build on every new
|
|
21
|
+
# Ruby before rb-sys catches up. The fallback compiles the shim from the
|
|
22
|
+
# headers of whatever Ruby is present instead, so an unreleased Ruby builds.
|
|
23
|
+
rb-sys = { version = "0.9", features = ["stable-api-compiled-fallback"] }
|
|
24
|
+
thiserror = "1"
|
|
25
|
+
|
|
26
|
+
[profile.release]
|
|
27
|
+
lto = true
|
|
28
|
+
codegen-units = 1
|
|
29
|
+
opt-level = 3
|
|
30
|
+
strip = true
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "mkmf"
|
|
4
|
+
require "rb_sys/mkmf"
|
|
5
|
+
|
|
6
|
+
def sync_cargo_version!
|
|
7
|
+
version = File.read(File.expand_path("../../VERSION", __dir__), encoding: "UTF-8").strip
|
|
8
|
+
cargo_toml = File.join(__dir__, "Cargo.toml")
|
|
9
|
+
contents = File.read(cargo_toml, encoding: "UTF-8")
|
|
10
|
+
updated = contents.sub(/^version = ".*"$/, %(version = "#{version}"))
|
|
11
|
+
File.write(cargo_toml, updated) if contents != updated
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
sync_cargo_version!
|
|
15
|
+
|
|
16
|
+
create_rust_makefile("zlight_csv/zlight_csv")
|