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 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;