dataset_core/lib.rs
1//! A generic, thread-safe dataset container with lazy loading and caching.
2//!
3//! `dataset-core` provides [`Dataset<T, E>`], a lightweight wrapper that pairs a storage
4//! directory with a lazily-initialized value of any type `T`. The caller supplies the
5//! download and parse logic through a loader closure stored at construction time. This
6//! makes `Dataset<T, E>` suitable for any data source: local files, remote URLs,
7//! databases, or in-memory generation.
8//!
9//! On top of this core type, the crate offers an **optional** feature-gated module:
10//!
11//! - **`utils`**: helper functions to download files, extract archives, verify
12//! SHA-256 hashes, and manage temporary directories.
13//!
14//! Ready-to-use loaders for 26 classic ML datasets live in the companion crate
15//! [`dataset-ml`](https://crates.io/crates/dataset-ml). Examples include Iris, Breast
16//! Cancer, Titanic, Forest CoverType, KDD Cup '99, and 20 Newsgroups. The crate depends
17//! on `dataset-core` with the `utils` feature enabled and serves as the reference
18//! implementation that wraps `Dataset<T, E>`.
19//!
20//! # Feature Flags
21//!
22//! | Feature | What it enables |
23//! |---------|--------------------------------------------------------------------------------------------|
24//! | `utils` | `download_to`, `download_to_with_retries`, `unzip`, `gunzip`, `untar`, `untar_gz`, `sha256_file`, `verify_sha256`, `read_latin1`, `acquire_dataset`, and the `error` module |
25//!
26//! With no features enabled, only `Dataset<T, E>` is available. It depends only on
27//! `std::sync::OnceLock`.
28//!
29//! # Quick Start: `Dataset<T, E>`
30//!
31//! ```rust
32//! use dataset_core::Dataset;
33//!
34//! fn my_loader(dir: &str) -> Result<Vec<String>, std::io::Error> {
35//! // In a real use case you would read/download files from `dir`.
36//! Ok(vec!["hello".to_string(), "world".to_string()])
37//! }
38//!
39//! // The loader is supplied once, at construction time.
40//! let mut ds: Dataset<Vec<String>, std::io::Error> = Dataset::new("./my_data", my_loader);
41//!
42//! // The first call runs the loader. Later calls return the cached reference.
43//! let data = ds.load().unwrap();
44//! assert_eq!(data.len(), 2);
45//!
46//! let data_again = ds.load().unwrap();
47//! assert!(std::ptr::eq(data, data_again)); // same reference, no reload
48//!
49//! // `get` borrows the cached value. It does not run the loader.
50//! // `get_mut` edits the value in place, with no clone or reload. The change stays cached.
51//! assert!(ds.get().is_some());
52//! if let Some(v) = ds.get_mut() {
53//! v[0] = "HELLO".to_string();
54//! }
55//! assert_eq!(ds.get().unwrap()[0], "HELLO");
56//!
57//! // Move the cached value out without cloning. `take` leaves `ds` reusable.
58//! // A later `load` call re-runs the loader. `into_inner` consumes `ds`.
59//! let owned = ds.take().unwrap();
60//! assert_eq!(owned.len(), 2);
61//! assert!(!ds.is_loaded());
62//!
63//! ds.load().unwrap(); // `take` reset the cache, so this reloads
64//! let owned = ds.into_inner().unwrap();
65//! assert_eq!(owned.len(), 2);
66//! ```
67//!
68//! # Swapping the loader
69//!
70//! Because the loader lives inside the `Dataset`, [`Dataset::set_loader`] lets you
71//! change *how* the loader parses the data. It also invalidates the cache, so the
72//! next access re-parses with the new loader. To re-run the **same** loader, for
73//! example when the file on disk changes, use [`Dataset::invalidate`].
74//!
75//! ```rust
76//! use dataset_core::Dataset;
77//!
78//! let mut ds: Dataset<i32, std::convert::Infallible> = Dataset::new("./data", |_| Ok(1));
79//! assert_eq!(*ds.load().unwrap(), 1);
80//!
81//! ds.set_loader(|_| Ok(2)); // swap the loader and drop the old cache
82//! assert!(!ds.is_loaded());
83//! assert_eq!(*ds.load().unwrap(), 2); // next load uses the new loader
84//! ```
85//!
86//! # Utility Functions (feature `utils`)
87//!
88//! - `download_to` - download a remote file into a directory
89//! - `download_to_with_retries` - same as `download_to`, but retries transient failures
90//! with backoff
91//! - `unzip` - extract a ZIP archive
92//! - `gunzip` - decompress a gzip (`.gz`) file into a single output file
93//! - `untar` - extract a tar (`.tar`) archive into a directory
94//! - `untar_gz` - extract a gzip-compressed tar (`.tar.gz` / `.tgz`) archive as a stream
95//! - `sha256_file` - compute a file's SHA-256 digest, to pin as an expected hash
96//! - `verify_sha256` - check a file against a hash you already have
97//! - `read_latin1` - read a file as Latin-1 text, with no data loss and no failure on
98//! non-UTF-8 bytes
99//! - `acquire_dataset` - cache-aware dataset acquisition workflow
100//! (temp dir → prepare → optional hash check → move to final location)
101//!
102//! `acquire_dataset` is the single entry point for caching a dataset file. It performs
103//! temp-dir creation and SHA-256 verification as internal steps. Use `sha256_file` and
104//! `verify_sha256` only outside that workflow: to pin a new dataset's hash, or to check
105//! which file is on disk after a test runs.
106
107#[cfg(feature = "utils")]
108pub use error::{DataFormatErrorKind, DatasetError};
109use std::sync::{Mutex, OnceLock};
110#[cfg(feature = "utils")]
111pub use utils::{
112 acquire_dataset, download_to, download_to_with_retries, gunzip, read_latin1, sha256_file,
113 untar, untar_gz, unzip, verify_sha256,
114};
115
116/// The boxed loader stored inside a [`Dataset`].
117///
118/// A loader takes the storage directory path and returns the parsed dataset, or an
119/// error. It is stored behind a `Box<dyn Fn ...>`, so the concrete closure type does
120/// not leak into `Dataset`'s type parameters. The `Send + Sync` bound keeps
121/// `Dataset<T, E>` shareable across threads. The implied `'static` bound means the
122/// loader must not borrow from its environment: it must capture by value or clone.
123type Loader<T, E> = Box<dyn Fn(&str) -> Result<T, E> + Send + Sync>;
124
125/// A generic, thread-safe dataset container with lazy loading and in-memory caching.
126///
127/// `Dataset<T, E>` is a thin caching wrapper that holds a `storage_dir` (the directory
128/// where dataset files are stored on disk), a loader closure, and a lazily-initialized
129/// value of type `T`. The caller supplies the download and parse logic through the
130/// loader passed to [`Dataset::new`]. [`Dataset::load`] runs this loader on first
131/// access.
132///
133/// This struct serves as the building block for the loaders in the companion crate
134/// [`dataset-ml`](https://crates.io/crates/dataset-ml) and for custom datasets that
135/// external users define.
136///
137/// # Type Parameters
138///
139/// - `T` - The type of the parsed dataset. It can be any type, such as
140/// `(Array2<f64>, Array1<f64>)`, a custom struct, or another data shape.
141/// `T` must implement `Send + Sync` so that `Dataset<T, E>` can be shared across
142/// threads.
143/// - `E` - The error type returned by the loader. Callers choose it freely, for
144/// example `std::io::Error`, a crate-specific `DatasetError`, or
145/// `std::convert::Infallible` for loaders that cannot fail.
146///
147/// # Thread Safety
148///
149/// `Dataset<T, E>` is `Send + Sync` when `T` is `Send + Sync` (the stored loader is
150/// always `Send + Sync`). The loader runs at most once even when multiple threads
151/// call [`Dataset::load`] concurrently. An internal mutex serializes the first
152/// load, so late arrivals wait for it and then share its result, rather than each
153/// thread starting its own download.
154///
155/// # Example
156///
157/// ```rust
158/// use dataset_core::Dataset;
159///
160/// // Define a simple loader that reads a value from the storage directory path.
161/// // The loader can return any error type you choose.
162/// fn my_loader(dir: &str) -> Result<Vec<String>, std::io::Error> {
163/// // A real use case downloads or reads files from `dir`.
164/// // This shows the caching behavior.
165/// Ok(vec!["hello".to_string(), "world".to_string()])
166/// }
167///
168/// // The loader is bound to the dataset at construction time.
169/// let mut dataset: Dataset<Vec<String>, std::io::Error> = Dataset::new("./my_data", my_loader);
170///
171/// // The first call to `load` triggers the loader
172/// let data = dataset.load().unwrap();
173/// assert_eq!(data.len(), 2);
174///
175/// // Subsequent calls return the cached reference instantly
176/// let data_again = dataset.load().unwrap();
177/// assert!(std::ptr::eq(data, data_again)); // same reference, no re-load
178///
179/// // Check whether data has been loaded
180/// assert!(dataset.is_loaded());
181///
182/// // Borrow the cached value without reloading, or edit it in place with `get_mut`.
183/// if let Some(v) = dataset.get_mut() {
184/// v[0] = "HELLO".to_string();
185/// }
186/// assert_eq!(dataset.get().unwrap()[0], "HELLO");
187///
188/// // Move the cached value out without cloning.
189/// // `take` leaves `dataset` reusable. `into_inner` consumes it.
190/// let owned = dataset.take().unwrap();
191/// assert_eq!(owned.len(), 2);
192/// assert!(!dataset.is_loaded()); // `take` resets it to unloaded
193///
194/// dataset.load().unwrap(); // this reloads, because `take` cleared the cache
195/// let owned = dataset.into_inner().unwrap();
196/// assert_eq!(owned.len(), 2);
197/// ```
198pub struct Dataset<T, E> {
199 storage_dir: String,
200 loader: Loader<T, E>,
201 data: OnceLock<T>,
202 /// Serializes the loader so that concurrent [`Dataset::load`] calls run it
203 /// **once** rather than racing to produce a value only one of them keeps.
204 ///
205 /// The mutex guards no data of its own. It exists only to make the check-run-store
206 /// sequence in `load` atomic. So `load` recovers from a poisoned lock (caused by a
207 /// loader that panicked on another thread) rather than propagating it.
208 init_lock: Mutex<()>,
209}
210
211impl<T, E> Dataset<T, E> {
212 /// Create a new `Dataset` instance without loading any data.
213 ///
214 /// This is a lightweight operation that only stores the storage directory path
215 /// and the loader. It performs no I/O or network requests until [`Dataset::load`]
216 /// runs.
217 ///
218 /// # Parameters
219 ///
220 /// - `storage_dir` - The directory where the loader stores dataset files. If the
221 /// directory does not yet exist, the loader creates it automatically when it
222 /// runs.
223 /// - `loader` - A closure or function that takes the storage directory path (`&str`)
224 /// and returns `Result<T, E>`. This is where you download data, handle file I/O,
225 /// and parse it. It runs at most once (see [`Dataset::load`]). `Dataset::new`
226 /// stores it behind `Box<dyn Fn ...>`, so it must be `Send + Sync + 'static`.
227 /// Capture owned values or clones instead of borrowing from the environment.
228 ///
229 /// # Returns
230 ///
231 /// A new `Dataset<T, E>` instance ready for lazy loading.
232 pub fn new(
233 storage_dir: &str,
234 loader: impl Fn(&str) -> Result<T, E> + Send + Sync + 'static,
235 ) -> Self {
236 Dataset {
237 storage_dir: storage_dir.to_string(),
238 loader: Box::new(loader),
239 data: OnceLock::new(),
240 init_lock: Mutex::new(()),
241 }
242 }
243
244 /// Load the dataset. The first call runs the stored loader and caches the result.
245 ///
246 /// On the first call, `load` runs the loader supplied to [`Dataset::new`] (or last
247 /// set via [`Dataset::set_loader`]) with the storage directory path. It caches the
248 /// returned value. Later calls, from any thread, return a reference to the cached
249 /// value without running the loader again.
250 ///
251 /// # Concurrency
252 ///
253 /// The loader runs **at most once**, even when several threads call `load`
254 /// simultaneously. Threads that arrive while a load is in flight block until it
255 /// finishes, then share its result. This matters for the typical loader, which
256 /// downloads into `storage_dir`: concurrent callers would otherwise each start
257 /// their own download of the same file.
258 ///
259 /// A loader that returns `Err` leaves the `Dataset` unloaded, so a later `load`
260 /// retries it. `load` does not cache the error.
261 ///
262 /// # Returns
263 ///
264 /// - `Ok(&T)` - A reference to the cached dataset.
265 ///
266 /// # Errors
267 ///
268 /// Returns any error the loader produces on the first call. After the first
269 /// successful load, this method never returns an error.
270 pub fn load(&self) -> Result<&T, E> {
271 // Fast path: already loaded, no locking needed.
272 if let Some(data) = self.data.get() {
273 return Ok(data);
274 }
275
276 // A poisoned lock only means that some other thread's loader panicked. The
277 // guard protects no invariant of its own, so recover from the poison and
278 // continue.
279 let _guard = self.init_lock.lock().unwrap_or_else(|e| e.into_inner());
280
281 // Check again: another thread may have loaded while this one waited for the lock.
282 if let Some(data) = self.data.get() {
283 return Ok(data);
284 }
285
286 let value = (self.loader)(&self.storage_dir)?;
287 let _ = self.data.set(value);
288
289 Ok(self
290 .data
291 .get()
292 .expect("data should be set after successful load"))
293 }
294
295 /// Load the dataset if needed, then return a **mutable** reference to it.
296 ///
297 /// This is the loading counterpart of [`Dataset::get_mut`]. Unlike `get_mut`,
298 /// which returns `None` when nothing is cached yet, `load_mut` runs the loader
299 /// first. It always returns a mutable reference on success. Use it to load the
300 /// data and adjust it in one step. For example, normalize features right after
301 /// parsing, rather than calling [`Dataset::load`] and [`Dataset::get_mut`]
302 /// separately.
303 ///
304 /// As with `get_mut`, you edit the value in place, and the change persists in
305 /// the cache.
306 ///
307 /// # Returns
308 ///
309 /// - `Ok(&mut T)` - A mutable reference to the cached dataset.
310 ///
311 /// # Errors
312 ///
313 /// Returns any error the loader produces on the first call.
314 ///
315 /// # Example
316 ///
317 /// ```rust
318 /// use dataset_core::Dataset;
319 ///
320 /// let mut ds: Dataset<Vec<i32>, std::convert::Infallible> =
321 /// Dataset::new("./data", |_| Ok(vec![1, 2, 3]));
322 ///
323 /// // Loads on first call, then returns a mutable reference.
324 /// ds.load_mut().unwrap().push(4);
325 /// assert_eq!(ds.get(), Some(&vec![1, 2, 3, 4])); // the change persisted
326 /// ```
327 pub fn load_mut(&mut self) -> Result<&mut T, E> {
328 // Make sure the value is present. The shared borrow ends with this statement.
329 self.load()?;
330
331 Ok(self
332 .data
333 .get_mut()
334 .expect("data should be set after successful load"))
335 }
336
337 /// Replace the loader and invalidate any cached data.
338 ///
339 /// Use this when the parsing logic itself needs to change. `set_loader` does not
340 /// run the new loader right away. It only swaps the loader and drops the cached
341 /// value, which resets the `Dataset` to its unloaded state. The next
342 /// [`Dataset::load`] call then re-parses the data with the new loader. This keeps
343 /// the "no I/O until access" contract intact.
344 ///
345 /// To re-run the *same* loader instead, use [`Dataset::invalidate`].
346 ///
347 /// # Parameters
348 ///
349 /// - `loader` - The replacement loader. Like the one given to [`Dataset::new`],
350 /// it must be `Send + Sync + 'static`.
351 ///
352 /// # Example
353 ///
354 /// ```rust
355 /// use dataset_core::Dataset;
356 ///
357 /// let mut ds: Dataset<i32, std::convert::Infallible> = Dataset::new("./data", |_| Ok(1));
358 /// assert_eq!(*ds.load().unwrap(), 1);
359 ///
360 /// ds.set_loader(|_| Ok(2)); // swaps the loader and drops the old cache
361 /// assert!(!ds.is_loaded());
362 /// assert_eq!(*ds.load().unwrap(), 2); // next load uses the new loader
363 /// ```
364 pub fn set_loader(&mut self, loader: impl Fn(&str) -> Result<T, E> + Send + Sync + 'static) {
365 self.loader = Box::new(loader);
366 self.invalidate();
367 }
368
369 /// Drop the cached value, keeping the current loader.
370 ///
371 /// This resets the `Dataset` to its unloaded state, so the next [`Dataset::load`]
372 /// call re-runs the **current** loader from scratch. If the underlying files
373 /// change on disk and you want to re-parse them, call this method. To swap in a
374 /// *different* loader, use [`Dataset::set_loader`].
375 ///
376 /// Unlike [`Dataset::take`], this does not return the cached value. It simply
377 /// discards it.
378 ///
379 /// # Example
380 ///
381 /// ```rust
382 /// use dataset_core::Dataset;
383 ///
384 /// let mut ds: Dataset<i32, std::convert::Infallible> = Dataset::new("./data", |_| Ok(1));
385 /// ds.load().unwrap();
386 /// assert!(ds.is_loaded());
387 ///
388 /// ds.invalidate(); // drop the cache, keep the loader
389 /// assert!(!ds.is_loaded());
390 /// assert_eq!(*ds.load().unwrap(), 1); // reloads with the same loader
391 /// ```
392 pub fn invalidate(&mut self) {
393 let _ = self.data.take();
394 }
395
396 /// Check whether the dataset has been loaded into memory.
397 ///
398 /// # Returns
399 ///
400 /// `true` if [`Dataset::load`] has been called successfully at least once,
401 /// `false` otherwise.
402 pub fn is_loaded(&self) -> bool {
403 self.data.get().is_some()
404 }
405
406 /// Get the storage directory path.
407 ///
408 /// # Returns
409 ///
410 /// The storage directory path as a string slice.
411 pub fn storage_dir(&self) -> &str {
412 &self.storage_dir
413 }
414
415 /// Get a reference to the cached value **without** triggering loading.
416 ///
417 /// Unlike [`Dataset::load`], this never runs the loader. If the dataset is not
418 /// loaded yet, it returns `None` instead of downloading or parsing anything. When
419 /// you want data only if it is already in memory, use `get`. This avoids the
420 /// loader's I/O cost when the data is not cached. For example, a fast path can
421 /// fall back to other work when the dataset is not yet cached.
422 ///
423 /// This is the reference-returning companion of [`Dataset::is_loaded`]:
424 /// `is_loaded()` answers *whether* the value is cached, and `get()` returns the
425 /// cached reference when it is.
426 ///
427 /// # Returns
428 ///
429 /// - `Some(&T)` - a reference to the cached value, if the dataset is loaded.
430 /// - `None` - if the dataset is not loaded.
431 ///
432 /// # Example
433 ///
434 /// ```rust
435 /// use dataset_core::Dataset;
436 ///
437 /// let ds: Dataset<Vec<i32>, std::convert::Infallible> =
438 /// Dataset::new("./data", |_| Ok(vec![1, 2, 3]));
439 /// assert!(ds.get().is_none()); // not loaded yet, no loader runs
440 ///
441 /// ds.load().unwrap();
442 /// assert_eq!(ds.get(), Some(&vec![1, 2, 3]));
443 /// ```
444 pub fn get(&self) -> Option<&T> {
445 self.data.get()
446 }
447
448 /// Get a mutable reference to the cached value for **in-place** editing.
449 ///
450 /// This is the only way to mutate the cached value without moving it out. You can
451 /// tweak the loaded data, for example to normalize features, add missing values,
452 /// or augment samples. The changes persist in the cache, so later [`Dataset::load`]
453 /// and [`Dataset::get`] calls observe them.
454 ///
455 /// Because it needs unique access (`&mut self`), there is no risk of aliasing or a
456 /// race. Unlike both [`take`](Dataset::take) and [`into_inner`](Dataset::into_inner),
457 /// it neither clones nor removes the value. The `Dataset` stays loaded.
458 ///
459 /// Like [`Dataset::get`], this does **not** trigger loading. It returns `None` if
460 /// the dataset is not loaded. If you need the value to be present, call
461 /// [`Dataset::load`] first.
462 ///
463 /// # Returns
464 ///
465 /// - `Some(&mut T)` - a mutable reference to the cached value, if the dataset is
466 /// loaded.
467 /// - `None` - if the dataset is not loaded.
468 ///
469 /// # Example
470 ///
471 /// ```rust
472 /// use dataset_core::Dataset;
473 ///
474 /// let mut ds: Dataset<Vec<i32>, std::convert::Infallible> =
475 /// Dataset::new("./data", |_| Ok(vec![1, 2, 3]));
476 /// assert!(ds.get_mut().is_none()); // not loaded yet, no loader runs
477 ///
478 /// ds.load().unwrap();
479 /// if let Some(data) = ds.get_mut() {
480 /// data.push(4); // edit the cached value in place, no clone, no reload
481 /// }
482 /// assert_eq!(ds.get(), Some(&vec![1, 2, 3, 4])); // the change persisted
483 /// ```
484 pub fn get_mut(&mut self) -> Option<&mut T> {
485 self.data.get_mut()
486 }
487
488 /// Consume the `Dataset` and return the cached value, if any.
489 ///
490 /// This **moves** the cached `T` out of the container. There is no clone.
491 /// Because it takes `self` by value, this consumes the `Dataset`. You cannot use
492 /// it afterward.
493 ///
494 /// This method does **not** trigger loading. It returns `None` if the dataset was
495 /// never loaded. If you need the value to be present, call [`Dataset::load`]
496 /// first.
497 ///
498 /// # `into_inner` vs [`take`](Dataset::take)
499 ///
500 /// Both move the cached value out without cloning. The difference is what
501 /// happens to the container:
502 ///
503 /// - [`into_inner`](Dataset::into_inner) takes `self` and **consumes** the
504 /// `Dataset`. Use it when you are done with the container.
505 /// - [`take`](Dataset::take) takes `&mut self`, leaving the `Dataset`
506 /// **reusable** in its unloaded state (a later [`load`](Dataset::load)
507 /// re-runs the loader).
508 ///
509 /// # Returns
510 ///
511 /// - `Some(T)` - the cached value, if the dataset is loaded.
512 /// - `None` - if the dataset was never loaded.
513 ///
514 /// # Example
515 ///
516 /// ```rust
517 /// use dataset_core::Dataset;
518 ///
519 /// let ds: Dataset<Vec<i32>, std::convert::Infallible> =
520 /// Dataset::new("./data", |_| Ok(vec![1, 2, 3]));
521 /// ds.load().unwrap();
522 ///
523 /// let owned: Vec<i32> = ds.into_inner().unwrap();
524 /// assert_eq!(owned, vec![1, 2, 3]);
525 /// // `into_inner` consumed `ds`. You can no longer use it.
526 ///
527 /// // A dataset that was never loaded yields `None`.
528 /// let empty: Dataset<Vec<i32>, std::convert::Infallible> =
529 /// Dataset::new("./data", |_| Ok(vec![1, 2, 3]));
530 /// assert!(empty.into_inner().is_none());
531 /// ```
532 #[must_use = "this consumes the Dataset; discarding the returned value drops the loaded data"]
533 pub fn into_inner(self) -> Option<T> {
534 self.data.into_inner()
535 }
536
537 /// Take the cached value out of the `Dataset`, leaving it reusable.
538 ///
539 /// This **moves** the cached `T` out. There is no clone. It also resets the
540 /// `Dataset` to its unloaded state. Unlike [`into_inner`](Dataset::into_inner),
541 /// `take` leaves the container intact. You can use it again, and a later
542 /// [`Dataset::load`] call runs the loader from scratch.
543 ///
544 /// This method does **not** trigger loading. It returns `None` if the dataset is
545 /// not loaded.
546 ///
547 /// # `take` vs [`into_inner`](Dataset::into_inner)
548 ///
549 /// Both move the cached value out without cloning. The difference is what
550 /// happens to the container:
551 ///
552 /// - [`take`](Dataset::take) takes `&mut self` and keeps the `Dataset`
553 /// **reusable** (reset to unloaded) after extracting the value.
554 /// - [`into_inner`](Dataset::into_inner) takes `self` and **consumes** the
555 /// container entirely.
556 ///
557 /// # Returns
558 ///
559 /// - `Some(T)` - the cached value, if the dataset is loaded.
560 /// - `None` - if the dataset is not loaded.
561 ///
562 /// # Example
563 ///
564 /// ```rust
565 /// use dataset_core::Dataset;
566 ///
567 /// let mut ds: Dataset<i32, std::convert::Infallible> = Dataset::new("./data", |_| Ok(1));
568 /// ds.load().unwrap();
569 /// assert!(ds.is_loaded());
570 ///
571 /// let taken = ds.take().unwrap();
572 /// assert_eq!(taken, 1);
573 /// assert!(!ds.is_loaded()); // reset to unloaded, but `ds` is still usable
574 ///
575 /// // Because it was reset, `load` runs the loader again:
576 /// let reloaded = ds.load().unwrap();
577 /// assert_eq!(*reloaded, 1);
578 /// ```
579 #[must_use = "discarding the returned value drops the data taken out of the Dataset"]
580 pub fn take(&mut self) -> Option<T> {
581 self.data.take()
582 }
583}
584
585impl<T, E> std::fmt::Debug for Dataset<T, E> {
586 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
587 f.debug_struct("Dataset")
588 .field("storage_dir", &self.storage_dir)
589 .field("data_loaded", &self.is_loaded())
590 .finish()
591 }
592}
593
594/// Error handling module.
595///
596/// This module provides structured error types for dataset loading operations, such
597/// as download failures, validation errors, and I/O errors. It also provides detailed
598/// data format errors with line numbers and context for debugging.
599#[cfg(feature = "utils")]
600pub mod error;
601
602/// Utility functions for dataset authors.
603///
604/// Provides helpers to download files, extract archives, verify SHA-256 hashes, and
605/// manage the dataset acquisition workflow.
606#[cfg(feature = "utils")]
607pub mod utils;