zlight_csv 0.4.0-aarch64-linux → 0.5.1-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.
@@ -0,0 +1,268 @@
1
+ //! CSV writer: converts Ruby data structures into CSV strings.
2
+ //!
3
+ //! Mirrors the reader's option/error patterns. The heavy lifting is done by
4
+ //! `csv::Writer`, with field values converted from Ruby objects following the
5
+ //! same semantics as Ruby's stdlib `CSV` (i.e. `nil` becomes an empty field and
6
+ //! every other value is stringified with `#to_s`).
7
+
8
+ use csv::{QuoteStyle, WriterBuilder};
9
+ use magnus::{
10
+ prelude::*,
11
+ scan_args::{get_kwargs, scan_args},
12
+ Error as MagnusError, RArray, RHash, RString, Ruby, Value,
13
+ };
14
+
15
+ use crate::error::ZlightError;
16
+ use crate::options::single_byte;
17
+
18
+ /// A CSV writer backed by an in-memory byte buffer.
19
+ type ByteWriter = csv::Writer<Vec<u8>>;
20
+
21
+ /// Configuration options for CSV writing.
22
+ #[derive(Debug, Clone, Copy)]
23
+ pub struct WriteOptions {
24
+ /// Whether to write a header row (only applicable for hash input).
25
+ pub write_headers: bool,
26
+ /// Field delimiter byte (defaults to comma).
27
+ pub delimiter: u8,
28
+ /// Quote byte (defaults to double quote).
29
+ pub quote: u8,
30
+ /// Whether to force quoting of every field.
31
+ pub force_quotes: bool,
32
+ }
33
+
34
+ impl Default for WriteOptions {
35
+ #[inline]
36
+ fn default() -> Self {
37
+ Self {
38
+ write_headers: true,
39
+ delimiter: b',',
40
+ quote: b'"',
41
+ force_quotes: false,
42
+ }
43
+ }
44
+ }
45
+
46
+ impl WriteOptions {
47
+ /// Extracts the `rows` argument and write options from Ruby arguments.
48
+ fn from_ruby_args(args: &[Value]) -> Result<(RArray, Self), ZlightError> {
49
+ let parsed = scan_args::<(RArray,), (), (), (), RHash, ()>(args)
50
+ .map_err(|_| ZlightError::MissingArgument("rows (expected an Array)"))?;
51
+
52
+ let (rows,) = parsed.required;
53
+
54
+ let kw = get_kwargs::<
55
+ _,
56
+ (),
57
+ (Option<bool>, Option<RString>, Option<RString>, Option<bool>),
58
+ (),
59
+ >(
60
+ parsed.keywords,
61
+ &[],
62
+ &["headers", "col_sep", "quote_char", "force_quotes"],
63
+ )
64
+ .map_err(|e| ZlightError::InvalidOption {
65
+ key: "keywords",
66
+ expected: "valid keyword arguments",
67
+ actual: e.to_string(),
68
+ })?;
69
+
70
+ let (headers_opt, col_sep_opt, quote_opt, force_quotes_opt) = kw.optional;
71
+
72
+ let delimiter = single_byte(col_sep_opt, "col_sep")?.unwrap_or(b',');
73
+ let quote = single_byte(quote_opt, "quote_char")?.unwrap_or(b'"');
74
+
75
+ Ok((
76
+ rows,
77
+ WriteOptions {
78
+ write_headers: headers_opt.unwrap_or(true),
79
+ delimiter,
80
+ quote,
81
+ force_quotes: force_quotes_opt.unwrap_or(false),
82
+ },
83
+ ))
84
+ }
85
+ }
86
+
87
+ /// Builds a CSV writer with the given options.
88
+ #[inline]
89
+ fn build_writer(options: &WriteOptions) -> ByteWriter {
90
+ let mut builder = WriterBuilder::new();
91
+ builder
92
+ .delimiter(options.delimiter)
93
+ .quote(options.quote)
94
+ // Rows may legitimately have differing field counts (e.g. hashes with
95
+ // missing keys, or jagged arrays), so never enforce a uniform width.
96
+ .flexible(true);
97
+
98
+ if options.force_quotes {
99
+ builder.quote_style(QuoteStyle::Always);
100
+ }
101
+
102
+ builder.from_writer(Vec::new())
103
+ }
104
+
105
+ /// Converts a csv error into a Ruby exception.
106
+ #[inline]
107
+ fn csv_err(e: csv::Error) -> MagnusError {
108
+ ZlightError::from(e).into()
109
+ }
110
+
111
+ /// Returns a UTF-8 encoded Ruby string for an arbitrary field value.
112
+ ///
113
+ /// Semantics mirror Ruby's stdlib CSV writer:
114
+ /// * `nil` becomes an empty field.
115
+ /// * Strings are used as-is (transcoded to UTF-8 when needed).
116
+ /// * Everything else is stringified via `#to_s`.
117
+ #[inline]
118
+ fn field_string(ruby: &Ruby, value: Value) -> Result<RString, MagnusError> {
119
+ if value.is_nil() {
120
+ return Ok(ruby.str_new(""));
121
+ }
122
+
123
+ let string = match RString::from_value(value) {
124
+ Some(s) => s,
125
+ None => value.funcall("to_s", ())?,
126
+ };
127
+
128
+ if string.is_utf8_compatible_encoding() {
129
+ Ok(string)
130
+ } else {
131
+ // Transcode foreign encodings (e.g. ISO-8859-1, Shift_JIS) so the
132
+ // resulting buffer is always valid UTF-8.
133
+ string.conv_enc(ruby.utf8_encoding())
134
+ }
135
+ }
136
+
137
+ /// Writes a single field value to the writer.
138
+ #[inline]
139
+ fn write_field(ruby: &Ruby, writer: &mut ByteWriter, value: Value) -> Result<(), MagnusError> {
140
+ let string = field_string(ruby, value)?;
141
+ // SAFETY: `string` is held on the stack and no Ruby allocation happens between
142
+ // taking the slice and handing it to the writer, so it cannot be collected.
143
+ let bytes = unsafe { string.as_slice() };
144
+ writer.write_field(bytes).map_err(csv_err)
145
+ }
146
+
147
+ /// Terminates the current record (row).
148
+ #[inline]
149
+ fn end_record(writer: &mut ByteWriter) -> Result<(), MagnusError> {
150
+ writer.write_record(None::<&[u8]>).map_err(csv_err)
151
+ }
152
+
153
+ /// Finalizes the writer and returns the buffer as a UTF-8 Ruby string.
154
+ #[inline]
155
+ fn finish(ruby: &Ruby, writer: ByteWriter) -> Result<RString, MagnusError> {
156
+ let data = writer
157
+ .into_inner()
158
+ .map_err(|e| MagnusError::from(ZlightError::Io(e.into_error())))?;
159
+ Ok(ruby.enc_str_new(&data, ruby.utf8_encoding()))
160
+ }
161
+
162
+ /// Coerces a row value into an `RArray`, raising a clear error otherwise.
163
+ #[inline]
164
+ fn expect_array(value: Value) -> Result<RArray, MagnusError> {
165
+ RArray::from_value(value).ok_or_else(|| {
166
+ MagnusError::from(ZlightError::InvalidOption {
167
+ key: "rows",
168
+ expected: "an Array of Arrays",
169
+ actual: format!("element of type {}", value.class()),
170
+ })
171
+ })
172
+ }
173
+
174
+ /// Coerces a row value into an `RHash`, raising a clear error otherwise.
175
+ #[inline]
176
+ fn expect_hash(value: Value) -> Result<RHash, MagnusError> {
177
+ RHash::from_value(value).ok_or_else(|| {
178
+ MagnusError::from(ZlightError::InvalidOption {
179
+ key: "rows",
180
+ expected: "an Array of Hashes",
181
+ actual: format!("element of type {}", value.class()),
182
+ })
183
+ })
184
+ }
185
+
186
+ /// Generates CSV from an array of arrays (positional fields, no header row).
187
+ fn generate_from_arrays(
188
+ ruby: &Ruby,
189
+ rows: RArray,
190
+ options: &WriteOptions,
191
+ ) -> Result<RString, MagnusError> {
192
+ let mut writer = build_writer(options);
193
+
194
+ for i in 0..rows.len() {
195
+ let row = expect_array(rows.entry(i as isize)?)?;
196
+ for j in 0..row.len() {
197
+ write_field(ruby, &mut writer, row.entry(j as isize)?)?;
198
+ }
199
+ end_record(&mut writer)?;
200
+ }
201
+
202
+ finish(ruby, writer)
203
+ }
204
+
205
+ /// Generates CSV from an array of hashes, using the first hash's keys as the
206
+ /// column order. Missing keys in later rows are written as empty fields.
207
+ fn generate_from_hashes(
208
+ ruby: &Ruby,
209
+ rows: RArray,
210
+ options: &WriteOptions,
211
+ ) -> Result<RString, MagnusError> {
212
+ let mut writer = build_writer(options);
213
+
214
+ // Column order is defined by the keys of the first hash.
215
+ let first_hash = expect_hash(rows.entry(0)?)?;
216
+ let keys: RArray = first_hash.funcall("keys", ())?;
217
+ let key_count = keys.len();
218
+ let header_keys: Vec<Value> = (0..key_count)
219
+ .map(|i| keys.entry(i as isize))
220
+ .collect::<Result<_, _>>()?;
221
+
222
+ // Emit the header row from the keys (skipped for keyless/empty hashes).
223
+ if options.write_headers && !header_keys.is_empty() {
224
+ for &key in &header_keys {
225
+ write_field(ruby, &mut writer, key)?;
226
+ }
227
+ end_record(&mut writer)?;
228
+ }
229
+
230
+ for i in 0..rows.len() {
231
+ let hash = expect_hash(rows.entry(i as isize)?)?;
232
+ for &key in &header_keys {
233
+ let value = hash.get(key).unwrap_or_else(|| ruby.qnil().as_value());
234
+ write_field(ruby, &mut writer, value)?;
235
+ }
236
+ end_record(&mut writer)?;
237
+ }
238
+
239
+ finish(ruby, writer)
240
+ }
241
+
242
+ /// Ruby: `ZLight.generate(rows, **options) -> String`.
243
+ ///
244
+ /// Dispatches on the type of the first row: an array of hashes produces a CSV
245
+ /// with a header row (unless `headers: false`); an array of arrays produces a
246
+ /// headerless CSV.
247
+ pub fn generate(ruby: &Ruby, args: &[Value]) -> Result<RString, MagnusError> {
248
+ let (rows, options) = WriteOptions::from_ruby_args(args)?;
249
+
250
+ if rows.is_empty() {
251
+ return Ok(ruby.str_new(""));
252
+ }
253
+
254
+ let first = rows.entry(0)?;
255
+
256
+ if RHash::from_value(first).is_some() {
257
+ generate_from_hashes(ruby, rows, &options)
258
+ } else if RArray::from_value(first).is_some() {
259
+ generate_from_arrays(ruby, rows, &options)
260
+ } else {
261
+ Err(ZlightError::InvalidOption {
262
+ key: "rows",
263
+ expected: "an Array of Arrays or an Array of Hashes",
264
+ actual: format!("an Array whose first element is {}", first.class()),
265
+ }
266
+ .into())
267
+ }
268
+ }
Binary file
Binary file
Binary file
Binary file
Binary file
data/lib/zlight_csv.rb CHANGED
@@ -13,7 +13,7 @@ end
13
13
  # ZLight is a high-performance CSV parser for Ruby, powered by Rust.
14
14
  #
15
15
  # It provides a simple, Ruby CSV-compatible API for parsing CSV files
16
- # up to 30x faster than the standard library.
16
+ # up to 40x faster than the standard library.
17
17
  #
18
18
  # @example Basic parsing with headers
19
19
  # result = ZLight.parse("name,age\nAlice,30\nBob,25")
@@ -40,7 +40,7 @@ end
40
40
  # reader.lazy.select { |row| row[:age] > 30 }.first(10)
41
41
  # reader.close
42
42
  #
43
- # @see https://github.com/zaidanch/zlight
43
+ # @see https://github.com/codebyisaad/zlight
44
44
  module ZLight
45
45
  # Base error class for all ZLight errors
46
46
  class Error < StandardError; end
@@ -234,10 +234,10 @@ module ZLight
234
234
  # .map { |row| row[:name] }
235
235
  #
236
236
  # @see .parse
237
- def foreach(csv_string, **options, &block)
237
+ def foreach(csv_string, **options, &)
238
238
  return to_enum(:foreach, csv_string, **options) unless block_given?
239
239
 
240
- parse(csv_string, **options).each(&block)
240
+ parse(csv_string, **options).each(&)
241
241
  end
242
242
 
243
243
  # Creates a streaming reader for a CSV file with automatic resource management.
@@ -261,8 +261,9 @@ module ZLight
261
261
  # @return [ZLight::StreamReader] The streaming reader (if no block)
262
262
  # @return [Object] The block's return value (if block given)
263
263
  #
264
- # @raise [Errno::ENOENT] If the file does not exist
265
- # @raise [Errno::EACCES] If the file is not readable
264
+ # @raise [IOError] If the file does not exist or cannot be read
265
+ # @raise [ZLight::StreamClosedError] If a row is read after the reader
266
+ # has been closed
266
267
  #
267
268
  # @example Process a large file row by row
268
269
  # ZLight.open("users.csv") do |reader|
metadata CHANGED
@@ -1,18 +1,33 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: zlight_csv
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.5.1
5
5
  platform: aarch64-linux
6
6
  authors:
7
7
  - Zaidan Chaudhary
8
+ - Saad Chaudhary
8
9
  autorequire:
9
10
  bindir: bin
10
11
  cert_chain: []
11
- date: 2026-06-06 00:00:00.000000000 Z
12
- dependencies: []
12
+ date: 2026-09-12 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: rb_sys
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - "~>"
19
+ - !ruby/object:Gem::Version
20
+ version: '0.9'
21
+ type: :runtime
22
+ prerelease: false
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - "~>"
26
+ - !ruby/object:Gem::Version
27
+ version: '0.9'
13
28
  description: |
14
29
  ZLight brings the speed of Rust to Ruby's CSV parsing. Built as a native
15
- extension, it delivers up to 30x faster performance than Ruby's standard
30
+ extension, it delivers up to 40x faster performance than Ruby's standard
16
31
  CSV library while maintaining full API compatibility. Whether you're parsing
17
32
  small configuration files or processing millions of rows, ZLight handles it
18
33
  efficiently. The streaming API enables memory-efficient processing of large
@@ -28,18 +43,30 @@ files:
28
43
  - LICENSE
29
44
  - README.md
30
45
  - VERSION
46
+ - ext/zlight_csv/Cargo.toml
47
+ - ext/zlight_csv/extconf.rb
48
+ - ext/zlight_csv/src/converter.rs
49
+ - ext/zlight_csv/src/error.rs
50
+ - ext/zlight_csv/src/lib.rs
51
+ - ext/zlight_csv/src/options.rs
52
+ - ext/zlight_csv/src/parser.rs
53
+ - ext/zlight_csv/src/stream.rs
54
+ - ext/zlight_csv/src/writer.rs
31
55
  - lib/zlight_csv.rb
32
- - lib/zlight_csv/3.0/zlight_csv.so
33
56
  - lib/zlight_csv/3.1/zlight_csv.so
34
57
  - lib/zlight_csv/3.2/zlight_csv.so
35
58
  - lib/zlight_csv/3.3/zlight_csv.so
59
+ - lib/zlight_csv/3.4/zlight_csv.so
60
+ - lib/zlight_csv/4.0/zlight_csv.so
36
61
  - lib/zlight_csv/version.rb
37
- homepage: https://rubygems.org/gems/zlight_csv
62
+ homepage: https://github.com/codebyisaad/zlight
38
63
  licenses:
39
64
  - MIT
40
65
  metadata:
41
- homepage_uri: https://rubygems.org/gems/zlight_csv
42
- documentation_uri: https://zaidanch.github.io/zlight-docs/
66
+ source_code_uri: https://github.com/codebyisaad/zlight
67
+ changelog_uri: https://github.com/codebyisaad/zlight/blob/main/CHANGELOG.md
68
+ bug_tracker_uri: https://github.com/codebyisaad/zlight/issues
69
+ documentation_uri: https://codebyisaad.github.io/zlight-docs/
43
70
  rubygems_mfa_required: 'true'
44
71
  post_install_message:
45
72
  rdoc_options: []
@@ -49,10 +76,10 @@ required_ruby_version: !ruby/object:Gem::Requirement
49
76
  requirements:
50
77
  - - ">="
51
78
  - !ruby/object:Gem::Version
52
- version: '3.0'
79
+ version: '3.1'
53
80
  - - "<"
54
81
  - !ruby/object:Gem::Version
55
- version: 3.4.dev
82
+ version: 4.1.dev
56
83
  required_rubygems_version: !ruby/object:Gem::Requirement
57
84
  requirements:
58
85
  - - ">="
Binary file