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;