Skip to main content

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