osv 0.3.3 → 0.3.5

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 33c644fac6e61f8bf3b9f11e646d6706017ece2a386df03a568b5ed06fd91e2a
4
- data.tar.gz: 0d977fb3a7eaf867663feb76161eb2d3fbe42e758523ef8cafbff51a9acfef0d
3
+ metadata.gz: 9a28169673f9693d3985c700f79490ae9057b43d86161a4354e9cb7196b92425
4
+ data.tar.gz: 3871878a45c4e564eeacbc4af911fc1e91c95f7ca0ca93fcaf8f06426ce0b3a5
5
5
  SHA512:
6
- metadata.gz: b6fe382c005837fbfc705bd02b1859fdfa9fa9f955c15f7b13ffacb97f1d5dc0714288c23a7e2431a21aada6aacb1952b33ae8d14acd019c96ed719f5580c02d
7
- data.tar.gz: 4e7c3d783f23af709505a4c182d1ee6744c9f28226aecdf68c532ada9ffa653858bc730bfd879cfa94d8e8f7a1280c49380bb5b6f74ed16b7ac776318aa53d1b
6
+ metadata.gz: 2038c7f8a0f56460e2bb6cb11b1bf6342841306b19b0847d7f57e3be6fc96687ba5743f1f5326251110bc6b41f049d24e5b1045543a71b9aa9b7364053f9a3a7
7
+ data.tar.gz: a99622513a55f4ec6e3c19ac73d29ab0f7e85191bacc68de324e6e12c8cfff06355b881d46380735fa1625420744d6dcd652ab0cdca08c92b7ecdd3f82f43170
data/Cargo.lock CHANGED
@@ -273,6 +273,7 @@ dependencies = [
273
273
  "rb-sys",
274
274
  "serde",
275
275
  "serde_magnus",
276
+ "thiserror",
276
277
  ]
277
278
 
278
279
  [[package]]
@@ -421,9 +422,9 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
421
422
 
422
423
  [[package]]
423
424
  name = "syn"
424
- version = "2.0.90"
425
+ version = "2.0.91"
425
426
  source = "registry+https://github.com/rust-lang/crates.io-index"
426
- checksum = "919d3b74a5dd0ccd15aeb8f93e7006bd9e14c295087c9896a110f490752bcf31"
427
+ checksum = "d53cbcb5a243bd33b7858b1d7f4aca2153490815872d86d955d6ea29f743c035"
427
428
  dependencies = [
428
429
  "proc-macro2",
429
430
  "quote",
@@ -436,6 +437,26 @@ version = "1.0.1"
436
437
  source = "registry+https://github.com/rust-lang/crates.io-index"
437
438
  checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
438
439
 
440
+ [[package]]
441
+ name = "thiserror"
442
+ version = "2.0.9"
443
+ source = "registry+https://github.com/rust-lang/crates.io-index"
444
+ checksum = "f072643fd0190df67a8bab670c20ef5d8737177d6ac6b2e9a236cb096206b2cc"
445
+ dependencies = [
446
+ "thiserror-impl",
447
+ ]
448
+
449
+ [[package]]
450
+ name = "thiserror-impl"
451
+ version = "2.0.9"
452
+ source = "registry+https://github.com/rust-lang/crates.io-index"
453
+ checksum = "7b50fa271071aae2e6ee85f842e2e28ba8cd2c5fb67f11fcb1fd70b276f9e7d4"
454
+ dependencies = [
455
+ "proc-macro2",
456
+ "quote",
457
+ "syn",
458
+ ]
459
+
439
460
  [[package]]
440
461
  name = "unicode-ident"
441
462
  version = "1.0.14"
data/Gemfile CHANGED
@@ -2,6 +2,13 @@ source "https://rubygems.org"
2
2
 
3
3
  gem "rb_sys", "~> 0.9.56"
4
4
  gem "rake"
5
- gem "rake-compiler", "1.2.0"
5
+ gem "csv"
6
6
 
7
- gem "minitest", "~> 5.0", group: :test
7
+ # Use local version of osv
8
+ gemspec
9
+
10
+ group :development, :test do
11
+ gem "minitest", "~> 5.0"
12
+ gem "benchmark-ips", "~> 2.12"
13
+ gem "fastcsv", "~> 0.0.7"
14
+ end
data/README.md CHANGED
@@ -65,19 +65,12 @@ Both methods support the following options:
65
65
 
66
66
  - `has_headers`: Boolean indicating if the first row contains headers (default: true)
67
67
  - `col_sep`: String specifying the field separator (default: ",")
68
-
69
- ```ruby
70
- # Reading TSV files
71
- OSV.for_each("path/to/file.tsv", col_sep: "\t") do |row|
72
- puts row["name"]
73
- end
74
-
75
- # Reading without headers
76
- OSV.for_each("path/to/file.csv", has_headers: false) do |row|
77
- # Headers will be automatically generated as "c0", "c1", etc.
78
- puts row["c0"]
79
- end
80
- ```
68
+ - `quote_char`: String specifying the quote character (default: "\"")
69
+ - `nil_string`: String that should be interpreted as nil
70
+ - by default, empty strings are interpreted as empty strings
71
+ - if you want to interpret empty strings as nil, set this to an empty string
72
+ - `buffer_size`: Integer specifying the read buffer size
73
+ - `result_type`: String specifying the output format ("hash" or "array")
81
74
 
82
75
  ### Input Sources
83
76
 
@@ -111,3 +104,93 @@ OSV.for_each(data) { |row| puts row["name"] }
111
104
  ## Performance
112
105
 
113
106
  This library is faster than the standard Ruby CSV library, and is comparable to the fastest CSV parser gems I've used.
107
+
108
+ Here's some unscientific benchmarks. You can find the code in the [benchmark/comparison_benchmark.rb](benchmark/comparison_benchmark.rb) file.
109
+
110
+ ### 10,000 lines
111
+
112
+ ```
113
+ Benchmarking with 10001 lines of data
114
+
115
+ ruby 3.3.3 (2024-06-12 revision f1c7b6f435) [arm64-darwin23]
116
+ Warming up --------------------------------------
117
+ OSV - Hash output 6.000 i/100ms
118
+ CSV - Hash output 1.000 i/100ms
119
+ OSV - Array output 18.000 i/100ms
120
+ CSV - Array output 2.000 i/100ms
121
+ FastCSV - Array output
122
+ 9.000 i/100ms
123
+ OSV - StringIO 7.000 i/100ms
124
+ CSV - StringIO 1.000 i/100ms
125
+ FastCSV - StringIO 20.000 i/100ms
126
+ OSV - Gzipped 6.000 i/100ms
127
+ CSV - Gzipped 1.000 i/100ms
128
+ Calculating -------------------------------------
129
+ OSV - Hash output 73.360 (± 4.1%) i/s (13.63 ms/i) - 366.000 in 5.000390s
130
+ CSV - Hash output 11.937 (±25.1%) i/s (83.78 ms/i) - 52.000 in 5.036297s
131
+ OSV - Array output 189.738 (± 8.4%) i/s (5.27 ms/i) - 954.000 in 5.071018s
132
+ CSV - Array output 25.471 (±11.8%) i/s (39.26 ms/i) - 120.000 in 5.015289s
133
+ FastCSV - Array output
134
+ 97.867 (± 2.0%) i/s (10.22 ms/i) - 495.000 in 5.060957s
135
+ OSV - StringIO 80.784 (± 6.2%) i/s (12.38 ms/i) - 406.000 in 5.046696s
136
+ CSV - StringIO 15.872 (± 0.0%) i/s (63.01 ms/i) - 80.000 in 5.043361s
137
+ FastCSV - StringIO 200.511 (± 2.0%) i/s (4.99 ms/i) - 1.020k in 5.088592s
138
+ OSV - Gzipped 55.220 (±12.7%) i/s (18.11 ms/i) - 258.000 in 5.030928s
139
+ CSV - Gzipped 12.591 (±15.9%) i/s (79.42 ms/i) - 59.000 in 5.039709s
140
+
141
+ Comparison:
142
+ FastCSV - StringIO: 200.5 i/s
143
+ OSV - Array output: 189.7 i/s - same-ish: difference falls within error
144
+ FastCSV - Array output: 97.9 i/s - 2.05x slower
145
+ OSV - StringIO: 80.8 i/s - 2.48x slower
146
+ OSV - Hash output: 73.4 i/s - 2.73x slower
147
+ OSV - Gzipped: 55.2 i/s - 3.63x slower
148
+ CSV - Array output: 25.5 i/s - 7.87x slower
149
+ CSV - StringIO: 15.9 i/s - 12.63x slower
150
+ CSV - Gzipped: 12.6 i/s - 15.92x slower
151
+ CSV - Hash output: 11.9 i/s - 16.80x slower
152
+ ```
153
+
154
+ ### 1,000,000 lines
155
+
156
+ ```
157
+ Benchmarking with 1000001 lines of data
158
+
159
+ ruby 3.3.3 (2024-06-12 revision f1c7b6f435) [arm64-darwin23]
160
+ Warming up --------------------------------------
161
+ OSV - Hash output 1.000 i/100ms
162
+ CSV - Hash output 1.000 i/100ms
163
+ OSV - Array output 1.000 i/100ms
164
+ CSV - Array output 1.000 i/100ms
165
+ FastCSV - Array output
166
+ 1.000 i/100ms
167
+ OSV - StringIO 1.000 i/100ms
168
+ CSV - StringIO 1.000 i/100ms
169
+ FastCSV - StringIO 1.000 i/100ms
170
+ OSV - Gzipped 1.000 i/100ms
171
+ CSV - Gzipped 1.000 i/100ms
172
+ Calculating -------------------------------------
173
+ OSV - Hash output 0.578 (± 0.0%) i/s (1.73 s/i) - 3.000 in 5.287845s
174
+ CSV - Hash output 0.117 (± 0.0%) i/s (8.57 s/i) - 1.000 in 8.571770s
175
+ OSV - Array output 1.142 (± 0.0%) i/s (875.97 ms/i) - 5.000 in 5.234694s
176
+ CSV - Array output 0.235 (± 0.0%) i/s (4.25 s/i) - 2.000 in 8.561144s
177
+ FastCSV - Array output
178
+ 0.768 (± 0.0%) i/s (1.30 s/i) - 4.000 in 6.924574s
179
+ OSV - StringIO 0.522 (± 0.0%) i/s (1.91 s/i) - 3.000 in 5.803969s
180
+ CSV - StringIO 0.132 (± 0.0%) i/s (7.59 s/i) - 1.000 in 7.593243s
181
+ FastCSV - StringIO 1.039 (± 0.0%) i/s (962.53 ms/i) - 6.000 in 5.806644s
182
+ OSV - Gzipped 0.437 (± 0.0%) i/s (2.29 s/i) - 3.000 in 6.885125s
183
+ CSV - Gzipped 0.115 (± 0.0%) i/s (8.68 s/i) - 1.000 in 8.684069s
184
+
185
+ Comparison:
186
+ OSV - Array output: 1.1 i/s
187
+ FastCSV - StringIO: 1.0 i/s - 1.10x slower
188
+ FastCSV - Array output: 0.8 i/s - 1.49x slower
189
+ OSV - Hash output: 0.6 i/s - 1.98x slower
190
+ OSV - StringIO: 0.5 i/s - 2.19x slower
191
+ OSV - Gzipped: 0.4 i/s - 2.61x slower
192
+ CSV - Array output: 0.2 i/s - 4.86x slower
193
+ CSV - StringIO: 0.1 i/s - 8.67x slower
194
+ CSV - Hash output: 0.1 i/s - 9.79x slower
195
+ CSV - Gzipped: 0.1 i/s - 9.91x slower
196
+ ```
data/ext/osv/Cargo.toml CHANGED
@@ -14,3 +14,4 @@ magnus = { version = "0.7", features = ["rb-sys"] }
14
14
  rb-sys = "^0.9"
15
15
  serde = { version = "1.0", features = ["derive"] }
16
16
  serde_magnus = "0.8.1"
17
+ thiserror = "2.0"
@@ -1,11 +1,50 @@
1
1
  use super::{
2
- header_cache::StringCache,
2
+ header_cache::{CacheError, StringCache},
3
3
  parser::RecordParser,
4
- reader::{ReadImpl, RecordReader},
4
+ read_impl::ReadImpl,
5
+ reader::RecordReader,
5
6
  };
6
7
  use flate2::read::GzDecoder;
7
- use magnus::{rb_sys::AsRawValue, value::ReprValue, Error, RString, Ruby, Value};
8
- use std::{fs::File, io::Read, marker::PhantomData, os::fd::FromRawFd, thread};
8
+ use magnus::{rb_sys::AsRawValue, value::ReprValue, Error as MagnusError, RString, Ruby, Value};
9
+ use std::{
10
+ fs::File,
11
+ io::{self, Read},
12
+ marker::PhantomData,
13
+ os::fd::FromRawFd,
14
+ thread,
15
+ };
16
+ use thiserror::Error;
17
+
18
+ #[derive(Error, Debug)]
19
+ pub enum ReaderError {
20
+ #[error("Failed to get file descriptor: {0}")]
21
+ FileDescriptor(String),
22
+ #[error("Invalid file descriptor")]
23
+ InvalidFileDescriptor,
24
+ #[error("Failed to open file: {0}")]
25
+ FileOpen(#[from] io::Error),
26
+ #[error("Failed to intern headers: {0}")]
27
+ HeaderIntern(#[from] CacheError),
28
+ #[error("Unsupported GzipReader")]
29
+ UnsupportedGzipReader,
30
+ #[error("Ruby error: {0}")]
31
+ Ruby(String),
32
+ }
33
+
34
+ impl From<MagnusError> for ReaderError {
35
+ fn from(err: MagnusError) -> Self {
36
+ Self::Ruby(err.to_string())
37
+ }
38
+ }
39
+
40
+ impl From<ReaderError> for MagnusError {
41
+ fn from(err: ReaderError) -> Self {
42
+ MagnusError::new(
43
+ Ruby::get().unwrap().exception_runtime_error(),
44
+ err.to_string(),
45
+ )
46
+ }
47
+ }
9
48
 
10
49
  pub struct RecordReaderBuilder<'a, T: RecordParser + Send + 'static> {
11
50
  ruby: &'a Ruby,
@@ -13,7 +52,7 @@ pub struct RecordReaderBuilder<'a, T: RecordParser + Send + 'static> {
13
52
  has_headers: bool,
14
53
  delimiter: u8,
15
54
  quote_char: u8,
16
- null_string: String,
55
+ null_string: Option<String>,
17
56
  buffer: usize,
18
57
  _phantom: PhantomData<T>,
19
58
  }
@@ -26,7 +65,7 @@ impl<'a, T: RecordParser + Send + 'static> RecordReaderBuilder<'a, T> {
26
65
  has_headers: true,
27
66
  delimiter: b',',
28
67
  quote_char: b'"',
29
- null_string: String::new(),
68
+ null_string: None,
30
69
  buffer: 1000,
31
70
  _phantom: PhantomData,
32
71
  }
@@ -47,7 +86,7 @@ impl<'a, T: RecordParser + Send + 'static> RecordReaderBuilder<'a, T> {
47
86
  self
48
87
  }
49
88
 
50
- pub fn null_string(mut self, null_string: String) -> Self {
89
+ pub fn null_string(mut self, null_string: Option<String>) -> Self {
51
90
  self.null_string = null_string;
52
91
  self
53
92
  }
@@ -57,36 +96,83 @@ impl<'a, T: RecordParser + Send + 'static> RecordReaderBuilder<'a, T> {
57
96
  self
58
97
  }
59
98
 
60
- fn get_reader(&self) -> Result<Box<dyn Read + Send + 'static>, Error> {
99
+ fn handle_string_io(&self) -> Result<Box<dyn Read + Send + 'static>, ReaderError> {
100
+ let string: RString = self.to_read.funcall("string", ())?;
101
+ let content = string.to_string()?;
102
+ Ok(Box::new(std::io::Cursor::new(content)))
103
+ }
104
+
105
+ fn handle_file_descriptor(&self) -> Result<Box<dyn Read + Send + 'static>, ReaderError> {
106
+ let raw_value = self.to_read.as_raw();
107
+ let fd = std::panic::catch_unwind(|| unsafe { rb_sys::rb_io_descriptor(raw_value) })
108
+ .map_err(|_| {
109
+ ReaderError::FileDescriptor("Failed to get file descriptor".to_string())
110
+ })?;
111
+
112
+ if fd < 0 {
113
+ return Err(ReaderError::InvalidFileDescriptor);
114
+ }
115
+
116
+ let file = unsafe { File::from_raw_fd(fd) };
117
+ Ok(Box::new(file))
118
+ }
119
+
120
+ fn handle_file_path(&self) -> Result<Box<dyn Read + Send + 'static>, ReaderError> {
121
+ let path = self.to_read.to_r_string()?.to_string()?;
122
+ let file = File::open(&path)?;
123
+
124
+ Ok(if path.ends_with(".gz") {
125
+ Box::new(GzDecoder::new(file))
126
+ } else {
127
+ Box::new(file)
128
+ })
129
+ }
130
+
131
+ fn get_reader(&self) -> Result<Box<dyn Read + Send + 'static>, ReaderError> {
61
132
  let string_io: magnus::RClass = self.ruby.eval("StringIO")?;
133
+ let gzip_reader_class: magnus::RClass = self.ruby.eval("Zlib::GzipReader")?;
62
134
 
63
135
  if self.to_read.is_kind_of(string_io) {
64
- let string: RString = self.to_read.funcall("string", ())?;
65
- let content = string.to_string()?;
66
- Ok(Box::new(std::io::Cursor::new(content)))
136
+ self.handle_string_io()
137
+ } else if self.to_read.is_kind_of(gzip_reader_class) {
138
+ Err(ReaderError::UnsupportedGzipReader)
67
139
  } else if self.to_read.is_kind_of(self.ruby.class_io()) {
68
- let fd = unsafe { rb_sys::rb_io_descriptor(self.to_read.as_raw()) };
69
- let file = unsafe { File::from_raw_fd(fd) };
70
- Ok(Box::new(file))
140
+ self.handle_file_descriptor()
71
141
  } else {
72
- let path = self.to_read.to_r_string()?.to_string()?;
73
- let file = std::fs::File::open(&path).map_err(|e| {
74
- Error::new(
75
- self.ruby.exception_runtime_error(),
76
- format!("Failed to open file: {e}"),
77
- )
78
- })?;
79
- if path.ends_with(".gz") {
80
- let file = GzDecoder::new(file);
81
- Ok(Box::new(file))
82
- } else {
83
- Ok(Box::new(file))
142
+ self.handle_file_path()
143
+ }
144
+ }
145
+
146
+ fn get_single_threaded_reader(&self) -> Result<Box<dyn Read>, ReaderError> {
147
+ let string_io: magnus::RClass = self.ruby.eval("StringIO")?;
148
+ let gzip_reader_class: magnus::RClass = self.ruby.eval("Zlib::GzipReader")?;
149
+
150
+ if self.to_read.is_kind_of(string_io) {
151
+ self.handle_string_io().map(|r| -> Box<dyn Read> { r })
152
+ } else if self.to_read.is_kind_of(gzip_reader_class) {
153
+ Ok(Box::new(RubyReader::new(self.to_read)))
154
+ } else if self.to_read.is_kind_of(self.ruby.class_io()) {
155
+ self.handle_file_descriptor()
156
+ .map(|r| -> Box<dyn Read> { r })
157
+ } else {
158
+ self.handle_file_path().map(|r| -> Box<dyn Read> { r })
159
+ }
160
+ }
161
+
162
+ pub fn build(self) -> Result<RecordReader<T>, ReaderError> {
163
+ match self.get_reader() {
164
+ Ok(readable) => self.build_multi_threaded(readable),
165
+ Err(_) => {
166
+ let readable = self.get_single_threaded_reader()?;
167
+ self.build_single_threaded(readable)
84
168
  }
85
169
  }
86
170
  }
87
171
 
88
- pub fn build(self) -> Result<RecordReader<T>, Error> {
89
- let readable = self.get_reader()?;
172
+ fn build_multi_threaded(
173
+ self,
174
+ readable: Box<dyn Read + Send + 'static>,
175
+ ) -> Result<RecordReader<T>, ReaderError> {
90
176
  let mut reader = csv::ReaderBuilder::new()
91
177
  .has_headers(self.has_headers)
92
178
  .delimiter(self.delimiter)
@@ -94,21 +180,16 @@ impl<'a, T: RecordParser + Send + 'static> RecordReaderBuilder<'a, T> {
94
180
  .from_reader(readable);
95
181
 
96
182
  let headers = RecordReader::<T>::get_headers(self.ruby, &mut reader, self.has_headers)?;
97
- let null_string = self.null_string;
98
-
99
- let static_headers = StringCache::intern_many(&headers).map_err(|e| {
100
- Error::new(
101
- self.ruby.exception_runtime_error(),
102
- format!("Failed to intern headers: {e}"),
103
- )
104
- })?;
183
+ let static_headers = StringCache::intern_many(&headers)?;
105
184
  let headers_for_cleanup = static_headers.clone();
106
185
 
107
186
  let (sender, receiver) = kanal::bounded(self.buffer);
187
+ let null_string = self.null_string.clone();
188
+
108
189
  let handle = thread::spawn(move || {
109
190
  let mut record = csv::StringRecord::new();
110
191
  while let Ok(true) = reader.read_record(&mut record) {
111
- let row = T::parse(&static_headers, &record, &null_string);
192
+ let row = T::parse(&static_headers, &record, null_string.as_deref());
112
193
  if sender.send(row).is_err() {
113
194
  break;
114
195
  }
@@ -125,4 +206,58 @@ impl<'a, T: RecordParser + Send + 'static> RecordReaderBuilder<'a, T> {
125
206
  },
126
207
  })
127
208
  }
209
+
210
+ fn build_single_threaded(
211
+ self,
212
+ readable: Box<dyn Read>,
213
+ ) -> Result<RecordReader<T>, ReaderError> {
214
+ let mut reader = csv::ReaderBuilder::new()
215
+ .has_headers(self.has_headers)
216
+ .delimiter(self.delimiter)
217
+ .quote(self.quote_char)
218
+ .from_reader(readable);
219
+
220
+ let headers = RecordReader::<T>::get_headers(self.ruby, &mut reader, self.has_headers)?;
221
+ let static_headers = StringCache::intern_many(&headers)?;
222
+
223
+ Ok(RecordReader {
224
+ reader: ReadImpl::SingleThreaded {
225
+ reader,
226
+ headers: static_headers,
227
+ null_string: self.null_string,
228
+ },
229
+ })
230
+ }
231
+ }
232
+
233
+ struct RubyReader {
234
+ inner: Value,
235
+ }
236
+
237
+ impl RubyReader {
238
+ fn new(inner: Value) -> Self {
239
+ Self { inner }
240
+ }
241
+ }
242
+
243
+ impl Read for RubyReader {
244
+ fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
245
+ let result = self.inner.funcall::<_, _, Value>("read", (buf.len(),));
246
+ match result {
247
+ Ok(data) => {
248
+ if data.is_nil() {
249
+ return Ok(0);
250
+ }
251
+
252
+ let string = RString::from_value(data).ok_or_else(|| {
253
+ io::Error::new(io::ErrorKind::Other, "Failed to convert to RString")
254
+ })?;
255
+ let bytes = unsafe { string.as_slice() };
256
+ let len = bytes.len().min(buf.len());
257
+ buf[..len].copy_from_slice(&bytes[..len]);
258
+ Ok(len)
259
+ }
260
+ Err(e) => Err(io::Error::new(io::ErrorKind::Other, e.to_string())),
261
+ }
262
+ }
128
263
  }
@@ -4,22 +4,29 @@
4
4
  ///
5
5
  /// Note: Performance testing on macOS showed minimal speed improvements,
6
6
  /// so this optimization could be removed if any issues arise.
7
-
8
-
9
7
  use std::{
10
8
  collections::HashMap,
11
9
  sync::{atomic::AtomicU32, LazyLock, Mutex},
12
10
  };
11
+ use thiserror::Error;
12
+
13
+ #[derive(Debug, Error)]
14
+ pub enum CacheError {
15
+ #[error("Failed to acquire lock: {0}")]
16
+ LockError(String),
17
+ }
13
18
 
14
19
  static STRING_CACHE: LazyLock<Mutex<HashMap<&'static str, AtomicU32>>> =
15
20
  LazyLock::new(|| Mutex::new(HashMap::with_capacity(100)));
16
21
 
17
- pub struct StringCache {}
22
+ pub struct StringCache;
18
23
 
19
24
  impl StringCache {
20
25
  #[allow(dead_code)]
21
- pub fn intern(string: String) -> Result<&'static str, String> {
22
- let mut cache = STRING_CACHE.lock().map_err(|e| e.to_string())?;
26
+ pub fn intern(string: String) -> Result<&'static str, CacheError> {
27
+ let mut cache = STRING_CACHE
28
+ .lock()
29
+ .map_err(|e| CacheError::LockError(e.to_string()))?;
23
30
 
24
31
  if let Some((&existing, count)) = cache.get_key_value(string.as_str()) {
25
32
  count.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
@@ -31,33 +38,36 @@ impl StringCache {
31
38
  }
32
39
  }
33
40
 
34
- pub fn intern_many(strings: &[String]) -> Result<Vec<&'static str>, String> {
35
- let mut cache = STRING_CACHE.lock().map_err(|e| e.to_string())?;
36
- let mut result = Vec::with_capacity(strings.len());
41
+ pub fn intern_many(strings: &[String]) -> Result<Vec<&'static str>, CacheError> {
42
+ let mut cache = STRING_CACHE
43
+ .lock()
44
+ .map_err(|e| CacheError::LockError(e.to_string()))?;
37
45
 
46
+ let mut result = Vec::with_capacity(strings.len());
38
47
  for string in strings {
39
- let static_str: &'static str =
40
- if let Some((&existing, count)) = cache.get_key_value(string.as_str()) {
41
- count.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
42
- existing
43
- } else {
44
- let leaked = Box::leak(string.clone().into_boxed_str());
45
- cache.insert(leaked, AtomicU32::new(1));
46
- leaked
47
- };
48
- result.push(static_str);
48
+ if let Some((&existing, count)) = cache.get_key_value(string.as_str()) {
49
+ count.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
50
+ result.push(existing);
51
+ } else {
52
+ let leaked = Box::leak(string.clone().into_boxed_str());
53
+ cache.insert(leaked, AtomicU32::new(1));
54
+ result.push(leaked);
55
+ }
49
56
  }
50
-
51
57
  Ok(result)
52
58
  }
53
59
 
54
- pub fn clear(headers: &[&'static str]) -> Result<(), String> {
55
- let cache = STRING_CACHE.lock().map_err(|e| e.to_string())?;
60
+ pub fn clear(headers: &[&'static str]) -> Result<(), CacheError> {
61
+ let mut cache = STRING_CACHE
62
+ .lock()
63
+ .map_err(|e| CacheError::LockError(e.to_string()))?;
56
64
 
57
65
  for header in headers {
58
66
  if let Some(count) = cache.get(header) {
59
- let remaining = count.fetch_sub(1, std::sync::atomic::Ordering::Relaxed);
60
- if remaining == 0 {
67
+ // Returns the previous value of the counter
68
+ let was = count.fetch_sub(1, std::sync::atomic::Ordering::Relaxed);
69
+ if was == 1 {
70
+ cache.remove(header);
61
71
  let ptr = *header as *const str as *mut str;
62
72
  unsafe {
63
73
  let _ = Box::from_raw(ptr);
@@ -1,6 +1,7 @@
1
1
  mod builder;
2
2
  mod header_cache;
3
3
  mod parser;
4
+ pub mod read_impl;
4
5
  mod reader;
5
6
  mod record;
6
7
 
@@ -2,50 +2,68 @@ use std::collections::HashMap;
2
2
 
3
3
  pub trait RecordParser {
4
4
  type Output;
5
+
5
6
  fn parse(
6
7
  headers: &[&'static str],
7
8
  record: &csv::StringRecord,
8
- null_string: &str,
9
+ null_string: Option<&str>,
9
10
  ) -> Self::Output;
10
11
  }
11
12
 
12
13
  impl RecordParser for HashMap<&'static str, Option<String>> {
13
14
  type Output = Self;
15
+
16
+ #[inline]
14
17
  fn parse(
15
18
  headers: &[&'static str],
16
19
  record: &csv::StringRecord,
17
- null_string: &str,
20
+ null_string: Option<&str>,
18
21
  ) -> Self::Output {
19
22
  let mut map = HashMap::with_capacity(headers.len());
20
- for (header, field) in headers.iter().zip(record.iter()) {
21
- map.insert(
22
- *header,
23
- if field == null_string {
24
- None
25
- } else {
26
- Some(field.to_string())
27
- },
28
- );
29
- }
23
+ headers
24
+ .iter()
25
+ .zip(record.iter())
26
+ .for_each(|(header, field)| {
27
+ map.insert(
28
+ *header,
29
+ if null_string == Some(field) {
30
+ None
31
+ } else {
32
+ // Avoid allocating for empty strings
33
+ if field.is_empty() {
34
+ Some(String::new())
35
+ } else {
36
+ Some(field.to_string())
37
+ }
38
+ },
39
+ );
40
+ });
30
41
  map
31
42
  }
32
43
  }
33
44
 
34
45
  impl RecordParser for Vec<Option<String>> {
35
46
  type Output = Self;
47
+
48
+ #[inline]
36
49
  fn parse(
37
50
  _headers: &[&'static str],
38
51
  record: &csv::StringRecord,
39
- null_string: &str,
52
+ null_string: Option<&str>,
40
53
  ) -> Self::Output {
41
54
  let mut vec = Vec::with_capacity(record.len());
42
- for field in record.iter() {
43
- vec.push(if field == null_string {
55
+ vec.extend(record.iter().map(|field| {
56
+ if null_string == Some(field) {
44
57
  None
45
58
  } else {
46
- Some(field.to_string())
47
- });
48
- }
59
+ // Avoid allocating for empty strings
60
+ if field.is_empty() {
61
+ Some(String::new())
62
+ } else {
63
+ Some(field.to_string())
64
+ }
65
+ }
66
+ }));
49
67
  vec
50
68
  }
51
69
  }
@@ -0,0 +1,65 @@
1
+ use super::{header_cache::StringCache, parser::RecordParser};
2
+ use std::{io::Read, thread};
3
+
4
+ pub enum ReadImpl<T: RecordParser> {
5
+ SingleThreaded {
6
+ reader: csv::Reader<Box<dyn Read>>,
7
+ headers: Vec<&'static str>,
8
+ null_string: Option<String>,
9
+ },
10
+ MultiThreaded {
11
+ headers: Vec<&'static str>,
12
+ receiver: kanal::Receiver<T::Output>,
13
+ handle: Option<thread::JoinHandle<()>>,
14
+ },
15
+ }
16
+
17
+ impl<T: RecordParser> ReadImpl<T> {
18
+ #[inline]
19
+ pub fn next(&mut self) -> Option<T::Output> {
20
+ match self {
21
+ Self::MultiThreaded {
22
+ receiver, handle, ..
23
+ } => match receiver.recv() {
24
+ Ok(record) => Some(record),
25
+ Err(_) => {
26
+ if let Some(handle) = handle.take() {
27
+ let _ = handle.join();
28
+ }
29
+ None
30
+ }
31
+ },
32
+ Self::SingleThreaded {
33
+ reader,
34
+ headers,
35
+ null_string,
36
+ } => {
37
+ let mut record = csv::StringRecord::new();
38
+ match reader.read_record(&mut record) {
39
+ Ok(true) => Some(T::parse(headers, &record, null_string.as_deref())),
40
+ _ => None,
41
+ }
42
+ }
43
+ }
44
+ }
45
+
46
+ #[inline]
47
+ pub fn cleanup(&mut self) {
48
+ match self {
49
+ Self::MultiThreaded {
50
+ receiver,
51
+ handle,
52
+ headers,
53
+ } => {
54
+ receiver.close();
55
+ if let Some(handle) = handle.take() {
56
+ let _ = handle.join();
57
+ }
58
+ let _ = StringCache::clear(headers);
59
+ }
60
+ Self::SingleThreaded { headers, .. } => {
61
+ let _ = StringCache::clear(headers);
62
+ }
63
+ }
64
+ }
65
+ }
@@ -1,66 +1,35 @@
1
- use super::{header_cache::StringCache, parser::RecordParser};
1
+ use super::{parser::RecordParser, read_impl::ReadImpl};
2
2
  use magnus::{Error, Ruby};
3
- use std::{io::Read, thread};
3
+ use std::{borrow::Cow, io::Read};
4
4
 
5
5
  pub struct RecordReader<T: RecordParser> {
6
6
  pub(crate) reader: ReadImpl<T>,
7
7
  }
8
8
 
9
- impl<T: RecordParser> Drop for RecordReader<T> {
10
- fn drop(&mut self) {
11
- match &mut self.reader {
12
- ReadImpl::MultiThreaded {
13
- receiver,
14
- handle,
15
- headers,
16
- } => {
17
- receiver.close();
18
- if let Some(handle) = handle.take() {
19
- let _ = handle.join();
20
- }
21
- StringCache::clear(headers).unwrap();
22
- }
23
- ReadImpl::SingleThreaded { headers, .. } => {
24
- StringCache::clear(headers).unwrap();
25
- }
26
- }
27
- }
28
- }
29
-
30
- #[allow(dead_code)]
31
- pub enum ReadImpl<T: RecordParser> {
32
- SingleThreaded {
33
- reader: csv::Reader<Box<dyn Read + Send + 'static>>,
34
- headers: Vec<&'static str>,
35
- null_string: String,
36
- },
37
- MultiThreaded {
38
- headers: Vec<&'static str>,
39
- receiver: kanal::Receiver<T::Output>,
40
- handle: Option<thread::JoinHandle<()>>,
41
- },
42
- }
43
-
44
9
  impl<T: RecordParser> RecordReader<T> {
10
+ #[inline]
45
11
  pub(crate) fn get_headers(
46
12
  ruby: &Ruby,
47
13
  reader: &mut csv::Reader<impl Read>,
48
14
  has_headers: bool,
49
15
  ) -> Result<Vec<String>, Error> {
50
- let first_row = reader
51
- .headers()
52
- .map_err(|e| {
53
- Error::new(
54
- ruby.exception_runtime_error(),
55
- format!("Failed to read headers: {e}"),
56
- )
57
- })?
58
- .clone();
16
+ let first_row = reader.headers().map_err(|e| {
17
+ Error::new(
18
+ ruby.exception_runtime_error(),
19
+ Cow::Owned(format!("Failed to read headers: {e}")),
20
+ )
21
+ })?;
59
22
 
60
23
  Ok(if has_headers {
61
- first_row.iter().map(String::from).collect()
24
+ // Pre-allocate the vector with exact capacity
25
+ let mut headers = Vec::with_capacity(first_row.len());
26
+ headers.extend(first_row.iter().map(String::from));
27
+ headers
62
28
  } else {
63
- (0..first_row.len()).map(|i| format!("c{i}")).collect()
29
+ // Pre-allocate the vector with exact capacity
30
+ let mut headers = Vec::with_capacity(first_row.len());
31
+ headers.extend((0..first_row.len()).map(|i| format!("c{i}")));
32
+ headers
64
33
  })
65
34
  }
66
35
  }
@@ -68,30 +37,21 @@ impl<T: RecordParser> RecordReader<T> {
68
37
  impl<T: RecordParser> Iterator for RecordReader<T> {
69
38
  type Item = T::Output;
70
39
 
40
+ #[inline]
71
41
  fn next(&mut self) -> Option<Self::Item> {
72
- match &mut self.reader {
73
- ReadImpl::MultiThreaded {
74
- receiver, handle, ..
75
- } => match receiver.recv() {
76
- Ok(record) => Some(record),
77
- Err(_) => {
78
- if let Some(handle) = handle.take() {
79
- let _ = handle.join();
80
- }
81
- None
82
- }
83
- },
84
- ReadImpl::SingleThreaded {
85
- reader,
86
- headers,
87
- null_string,
88
- } => {
89
- let mut record = csv::StringRecord::new();
90
- match reader.read_record(&mut record) {
91
- Ok(true) => Some(T::parse(headers, &record, null_string)),
92
- _ => None,
93
- }
94
- }
95
- }
42
+ self.reader.next()
43
+ }
44
+
45
+ #[inline]
46
+ fn size_hint(&self) -> (usize, Option<usize>) {
47
+ // We can't know the exact size without reading the whole file
48
+ (0, None)
49
+ }
50
+ }
51
+
52
+ impl<T: RecordParser> Drop for RecordReader<T> {
53
+ #[inline]
54
+ fn drop(&mut self) {
55
+ self.reader.cleanup();
96
56
  }
97
57
  }
@@ -1,4 +1,4 @@
1
- use magnus::{IntoValue, RHash, Ruby, Value};
1
+ use magnus::{IntoValue, Ruby, Value};
2
2
  use std::collections::HashMap;
3
3
 
4
4
  #[derive(Debug)]
@@ -8,14 +8,16 @@ pub enum CsvRecord {
8
8
  }
9
9
 
10
10
  impl IntoValue for CsvRecord {
11
+ #[inline]
11
12
  fn into_value_with(self, handle: &Ruby) -> Value {
12
13
  match self {
13
14
  CsvRecord::Vec(vec) => vec.into_value_with(handle),
14
15
  CsvRecord::Map(map) => {
15
- let hash = RHash::new();
16
- for (k, v) in map {
17
- hash.aset(k, v).unwrap();
18
- }
16
+ // Pre-allocate the hash with the known size
17
+ let hash = handle.hash_new_capa(map.len());
18
+ map.into_iter()
19
+ .try_for_each(|(k, v)| hash.aset(k, v))
20
+ .unwrap();
19
21
  hash.into_value_with(handle)
20
22
  }
21
23
  }
@@ -71,7 +71,7 @@ struct EnumeratorArgs {
71
71
  has_headers: bool,
72
72
  delimiter: u8,
73
73
  quote_char: u8,
74
- null_string: String,
74
+ null_string: Option<String>,
75
75
  buffer_size: usize,
76
76
  result_type: String,
77
77
  }
data/ext/osv/src/utils.rs CHANGED
@@ -10,7 +10,7 @@ pub struct CsvArgs {
10
10
  pub has_headers: bool,
11
11
  pub delimiter: u8,
12
12
  pub quote_char: u8,
13
- pub null_string: String,
13
+ pub null_string: Option<String>,
14
14
  pub buffer_size: usize,
15
15
  pub result_type: String,
16
16
  }
@@ -73,7 +73,7 @@ pub fn parse_csv_args(ruby: &Ruby, args: &[Value]) -> Result<CsvArgs, Error> {
73
73
  )
74
74
  })?;
75
75
 
76
- let null_string = kwargs.optional.3.unwrap_or_else(|| "".to_string());
76
+ let null_string = kwargs.optional.3;
77
77
 
78
78
  let buffer_size = kwargs.optional.4.unwrap_or(1000);
79
79
 
data/lib/osv/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module OSV
2
- VERSION = "0.3.3"
2
+ VERSION = "0.3.5"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: osv
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.3
4
+ version: 0.3.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nathan Jaremko
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-12-23 00:00:00.000000000 Z
11
+ date: 2024-12-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rb_sys
@@ -52,13 +52,13 @@ files:
52
52
  - LICENSE
53
53
  - README.md
54
54
  - Rakefile
55
- - ext/osv/Cargo.lock
56
55
  - ext/osv/Cargo.toml
57
56
  - ext/osv/extconf.rb
58
57
  - ext/osv/src/csv/builder.rs
59
58
  - ext/osv/src/csv/header_cache.rs
60
59
  - ext/osv/src/csv/mod.rs
61
60
  - ext/osv/src/csv/parser.rs
61
+ - ext/osv/src/csv/read_impl.rs
62
62
  - ext/osv/src/csv/reader.rs
63
63
  - ext/osv/src/csv/record.rs
64
64
  - ext/osv/src/lib.rs
data/ext/osv/Cargo.lock DELETED
@@ -1,402 +0,0 @@
1
- # This file is automatically @generated by Cargo.
2
- # It is not intended for manual editing.
3
- version = 3
4
-
5
- [[package]]
6
- name = "aho-corasick"
7
- version = "1.1.3"
8
- source = "registry+https://github.com/rust-lang/crates.io-index"
9
- checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
10
- dependencies = [
11
- "memchr",
12
- ]
13
-
14
- [[package]]
15
- name = "bindgen"
16
- version = "0.69.5"
17
- source = "registry+https://github.com/rust-lang/crates.io-index"
18
- checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088"
19
- dependencies = [
20
- "bitflags",
21
- "cexpr",
22
- "clang-sys",
23
- "itertools",
24
- "lazy_static",
25
- "lazycell",
26
- "proc-macro2",
27
- "quote",
28
- "regex",
29
- "rustc-hash",
30
- "shlex",
31
- "syn",
32
- ]
33
-
34
- [[package]]
35
- name = "bitflags"
36
- version = "2.6.0"
37
- source = "registry+https://github.com/rust-lang/crates.io-index"
38
- checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
39
-
40
- [[package]]
41
- name = "cexpr"
42
- version = "0.6.0"
43
- source = "registry+https://github.com/rust-lang/crates.io-index"
44
- checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766"
45
- dependencies = [
46
- "nom",
47
- ]
48
-
49
- [[package]]
50
- name = "cfg-if"
51
- version = "1.0.0"
52
- source = "registry+https://github.com/rust-lang/crates.io-index"
53
- checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
54
-
55
- [[package]]
56
- name = "clang-sys"
57
- version = "1.8.1"
58
- source = "registry+https://github.com/rust-lang/crates.io-index"
59
- checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4"
60
- dependencies = [
61
- "glob",
62
- "libc",
63
- "libloading",
64
- ]
65
-
66
- [[package]]
67
- name = "csv"
68
- version = "1.3.1"
69
- source = "registry+https://github.com/rust-lang/crates.io-index"
70
- checksum = "acdc4883a9c96732e4733212c01447ebd805833b7275a73ca3ee080fd77afdaf"
71
- dependencies = [
72
- "csv-core",
73
- "itoa",
74
- "ryu",
75
- "serde",
76
- ]
77
-
78
- [[package]]
79
- name = "csv-core"
80
- version = "0.1.11"
81
- source = "registry+https://github.com/rust-lang/crates.io-index"
82
- checksum = "5efa2b3d7902f4b634a20cae3c9c4e6209dc4779feb6863329607560143efa70"
83
- dependencies = [
84
- "memchr",
85
- ]
86
-
87
- [[package]]
88
- name = "either"
89
- version = "1.13.0"
90
- source = "registry+https://github.com/rust-lang/crates.io-index"
91
- checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0"
92
-
93
- [[package]]
94
- name = "glob"
95
- version = "0.3.1"
96
- source = "registry+https://github.com/rust-lang/crates.io-index"
97
- checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
98
-
99
- [[package]]
100
- name = "itertools"
101
- version = "0.12.1"
102
- source = "registry+https://github.com/rust-lang/crates.io-index"
103
- checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569"
104
- dependencies = [
105
- "either",
106
- ]
107
-
108
- [[package]]
109
- name = "itoa"
110
- version = "1.0.14"
111
- source = "registry+https://github.com/rust-lang/crates.io-index"
112
- checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674"
113
-
114
- [[package]]
115
- name = "lazy_static"
116
- version = "1.5.0"
117
- source = "registry+https://github.com/rust-lang/crates.io-index"
118
- checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
119
-
120
- [[package]]
121
- name = "lazycell"
122
- version = "1.3.0"
123
- source = "registry+https://github.com/rust-lang/crates.io-index"
124
- checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
125
-
126
- [[package]]
127
- name = "libc"
128
- version = "0.2.169"
129
- source = "registry+https://github.com/rust-lang/crates.io-index"
130
- checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a"
131
-
132
- [[package]]
133
- name = "libloading"
134
- version = "0.8.6"
135
- source = "registry+https://github.com/rust-lang/crates.io-index"
136
- checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34"
137
- dependencies = [
138
- "cfg-if",
139
- "windows-targets",
140
- ]
141
-
142
- [[package]]
143
- name = "magnus"
144
- version = "0.7.1"
145
- source = "registry+https://github.com/rust-lang/crates.io-index"
146
- checksum = "3d87ae53030f3a22e83879e666cb94e58a7bdf31706878a0ba48752994146dab"
147
- dependencies = [
148
- "magnus-macros",
149
- "rb-sys",
150
- "rb-sys-env",
151
- "seq-macro",
152
- ]
153
-
154
- [[package]]
155
- name = "magnus-macros"
156
- version = "0.6.0"
157
- source = "registry+https://github.com/rust-lang/crates.io-index"
158
- checksum = "5968c820e2960565f647819f5928a42d6e874551cab9d88d75e3e0660d7f71e3"
159
- dependencies = [
160
- "proc-macro2",
161
- "quote",
162
- "syn",
163
- ]
164
-
165
- [[package]]
166
- name = "memchr"
167
- version = "2.7.4"
168
- source = "registry+https://github.com/rust-lang/crates.io-index"
169
- checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
170
-
171
- [[package]]
172
- name = "minimal-lexical"
173
- version = "0.2.1"
174
- source = "registry+https://github.com/rust-lang/crates.io-index"
175
- checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
176
-
177
- [[package]]
178
- name = "nom"
179
- version = "7.1.3"
180
- source = "registry+https://github.com/rust-lang/crates.io-index"
181
- checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
182
- dependencies = [
183
- "memchr",
184
- "minimal-lexical",
185
- ]
186
-
187
- [[package]]
188
- name = "osv"
189
- version = "0.1.0"
190
- dependencies = [
191
- "csv",
192
- "magnus",
193
- "rb-sys",
194
- ]
195
-
196
- [[package]]
197
- name = "proc-macro2"
198
- version = "1.0.92"
199
- source = "registry+https://github.com/rust-lang/crates.io-index"
200
- checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0"
201
- dependencies = [
202
- "unicode-ident",
203
- ]
204
-
205
- [[package]]
206
- name = "quote"
207
- version = "1.0.37"
208
- source = "registry+https://github.com/rust-lang/crates.io-index"
209
- checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af"
210
- dependencies = [
211
- "proc-macro2",
212
- ]
213
-
214
- [[package]]
215
- name = "rb-sys"
216
- version = "0.9.103"
217
- source = "registry+https://github.com/rust-lang/crates.io-index"
218
- checksum = "91dbe37ab6ac2fba187480fb6544b92445e41e5c6f553bf0c33743f3c450a1df"
219
- dependencies = [
220
- "rb-sys-build",
221
- ]
222
-
223
- [[package]]
224
- name = "rb-sys-build"
225
- version = "0.9.103"
226
- source = "registry+https://github.com/rust-lang/crates.io-index"
227
- checksum = "c4d56a49dcb646b70b758789c0d16c055a386a4f2a3346333abb69850fa860ce"
228
- dependencies = [
229
- "bindgen",
230
- "lazy_static",
231
- "proc-macro2",
232
- "quote",
233
- "regex",
234
- "shell-words",
235
- "syn",
236
- ]
237
-
238
- [[package]]
239
- name = "rb-sys-env"
240
- version = "0.1.2"
241
- source = "registry+https://github.com/rust-lang/crates.io-index"
242
- checksum = "a35802679f07360454b418a5d1735c89716bde01d35b1560fc953c1415a0b3bb"
243
-
244
- [[package]]
245
- name = "regex"
246
- version = "1.11.1"
247
- source = "registry+https://github.com/rust-lang/crates.io-index"
248
- checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
249
- dependencies = [
250
- "aho-corasick",
251
- "memchr",
252
- "regex-automata",
253
- "regex-syntax",
254
- ]
255
-
256
- [[package]]
257
- name = "regex-automata"
258
- version = "0.4.9"
259
- source = "registry+https://github.com/rust-lang/crates.io-index"
260
- checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
261
- dependencies = [
262
- "aho-corasick",
263
- "memchr",
264
- "regex-syntax",
265
- ]
266
-
267
- [[package]]
268
- name = "regex-syntax"
269
- version = "0.8.5"
270
- source = "registry+https://github.com/rust-lang/crates.io-index"
271
- checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
272
-
273
- [[package]]
274
- name = "rustc-hash"
275
- version = "1.1.0"
276
- source = "registry+https://github.com/rust-lang/crates.io-index"
277
- checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
278
-
279
- [[package]]
280
- name = "ryu"
281
- version = "1.0.18"
282
- source = "registry+https://github.com/rust-lang/crates.io-index"
283
- checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
284
-
285
- [[package]]
286
- name = "seq-macro"
287
- version = "0.3.5"
288
- source = "registry+https://github.com/rust-lang/crates.io-index"
289
- checksum = "a3f0bf26fd526d2a95683cd0f87bf103b8539e2ca1ef48ce002d67aad59aa0b4"
290
-
291
- [[package]]
292
- name = "serde"
293
- version = "1.0.216"
294
- source = "registry+https://github.com/rust-lang/crates.io-index"
295
- checksum = "0b9781016e935a97e8beecf0c933758c97a5520d32930e460142b4cd80c6338e"
296
- dependencies = [
297
- "serde_derive",
298
- ]
299
-
300
- [[package]]
301
- name = "serde_derive"
302
- version = "1.0.216"
303
- source = "registry+https://github.com/rust-lang/crates.io-index"
304
- checksum = "46f859dbbf73865c6627ed570e78961cd3ac92407a2d117204c49232485da55e"
305
- dependencies = [
306
- "proc-macro2",
307
- "quote",
308
- "syn",
309
- ]
310
-
311
- [[package]]
312
- name = "shell-words"
313
- version = "1.1.0"
314
- source = "registry+https://github.com/rust-lang/crates.io-index"
315
- checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde"
316
-
317
- [[package]]
318
- name = "shlex"
319
- version = "1.3.0"
320
- source = "registry+https://github.com/rust-lang/crates.io-index"
321
- checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
322
-
323
- [[package]]
324
- name = "syn"
325
- version = "2.0.90"
326
- source = "registry+https://github.com/rust-lang/crates.io-index"
327
- checksum = "919d3b74a5dd0ccd15aeb8f93e7006bd9e14c295087c9896a110f490752bcf31"
328
- dependencies = [
329
- "proc-macro2",
330
- "quote",
331
- "unicode-ident",
332
- ]
333
-
334
- [[package]]
335
- name = "unicode-ident"
336
- version = "1.0.14"
337
- source = "registry+https://github.com/rust-lang/crates.io-index"
338
- checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83"
339
-
340
- [[package]]
341
- name = "windows-targets"
342
- version = "0.52.6"
343
- source = "registry+https://github.com/rust-lang/crates.io-index"
344
- checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
345
- dependencies = [
346
- "windows_aarch64_gnullvm",
347
- "windows_aarch64_msvc",
348
- "windows_i686_gnu",
349
- "windows_i686_gnullvm",
350
- "windows_i686_msvc",
351
- "windows_x86_64_gnu",
352
- "windows_x86_64_gnullvm",
353
- "windows_x86_64_msvc",
354
- ]
355
-
356
- [[package]]
357
- name = "windows_aarch64_gnullvm"
358
- version = "0.52.6"
359
- source = "registry+https://github.com/rust-lang/crates.io-index"
360
- checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
361
-
362
- [[package]]
363
- name = "windows_aarch64_msvc"
364
- version = "0.52.6"
365
- source = "registry+https://github.com/rust-lang/crates.io-index"
366
- checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
367
-
368
- [[package]]
369
- name = "windows_i686_gnu"
370
- version = "0.52.6"
371
- source = "registry+https://github.com/rust-lang/crates.io-index"
372
- checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
373
-
374
- [[package]]
375
- name = "windows_i686_gnullvm"
376
- version = "0.52.6"
377
- source = "registry+https://github.com/rust-lang/crates.io-index"
378
- checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
379
-
380
- [[package]]
381
- name = "windows_i686_msvc"
382
- version = "0.52.6"
383
- source = "registry+https://github.com/rust-lang/crates.io-index"
384
- checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
385
-
386
- [[package]]
387
- name = "windows_x86_64_gnu"
388
- version = "0.52.6"
389
- source = "registry+https://github.com/rust-lang/crates.io-index"
390
- checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
391
-
392
- [[package]]
393
- name = "windows_x86_64_gnullvm"
394
- version = "0.52.6"
395
- source = "registry+https://github.com/rust-lang/crates.io-index"
396
- checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
397
-
398
- [[package]]
399
- name = "windows_x86_64_msvc"
400
- version = "0.52.6"
401
- source = "registry+https://github.com/rust-lang/crates.io-index"
402
- checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"