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 actual downloading
5//! and parsing logic is supplied by the caller through a loader closure stored at
6//! construction time, making `Dataset<T, E>` suitable for any data source — local
7//! files, remote URLs, 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 for downloading files, extracting archives,
12//!   verifying SHA-256 hashes, and managing temporary directories.
13//!
14//! Ready-to-use loaders for classic ML datasets (Iris, Boston Housing, Diabetes,
15//! Titanic, Wine Quality) live in the companion crate
16//! [`dataset-ml`](https://crates.io/crates/dataset-ml), which depends on
17//! `dataset-core` with the `utils` feature enabled and serves as the reference
18//! implementation for wrapping `Dataset<T, E>`.
19//!
20//! # Feature Flags
21//!
22//! | Feature | What it enables                                                  |
23//! |---------|------------------------------------------------------------------|
24//! | `utils` | `download_to`, `unzip`, `acquire_dataset`, and the `error` module |
25//!
26//! With no features enabled, only `Dataset<T, E>` is available — depending 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//! // First call runs the loader; subsequent 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 without ever running the loader;
50//! // `get_mut` edits it in place (no clone, no 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` 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`, you change *how* the data is
71//! parsed with [`Dataset::set_loader`], which also invalidates the cache so the
72//! next access re-parses with the new loader. To re-run the **same** loader
73//! (e.g. the file on disk changed), 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; old cache is dropped
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//! - `unzip` — extract a ZIP archive
90//! - `acquire_dataset` — cache-aware dataset acquisition workflow
91//!   (temp dir → prepare → optional hash check → move to final location)
92//!
93//! `acquire_dataset` is the single entry point for caching a dataset file; temp-dir
94//! creation and SHA-256 verification are internal steps it performs for you.
95
96#[cfg(feature = "utils")]
97pub use error::{DataFormatErrorKind, DatasetError};
98use std::sync::OnceLock;
99#[cfg(feature = "utils")]
100pub use utils::{acquire_dataset, download_to, unzip};
101
102/// The boxed loader stored inside a [`Dataset`].
103///
104/// A loader takes the storage directory path and returns the parsed dataset (or
105/// an error). It is stored behind a `Box<dyn Fn ...>` so the concrete closure
106/// type does not leak into `Dataset`'s type parameters. The `Send + Sync` bound
107/// keeps `Dataset<T, E>` shareable across threads (matching the guarantee given
108/// by the internal `OnceLock`); the implied `'static` bound means the loader may
109/// not borrow from its environment — capture by value or clone instead.
110type Loader<T, E> = Box<dyn Fn(&str) -> Result<T, E> + Send + Sync>;
111
112/// A generic, thread-safe dataset container with lazy loading and in-memory caching.
113///
114/// `Dataset<T, E>` is a thin caching wrapper that holds a `storage_dir` (the directory
115/// where dataset files are stored on disk), a loader closure, and a lazily-initialized
116/// value of type `T`. The downloading and parsing logic is provided by the caller
117/// through the loader passed to [`Dataset::new`] and run on first access by
118/// [`Dataset::load`].
119///
120/// This struct is designed to be the building block for both the built-in datasets
121/// shipped with this crate and any custom datasets defined by external users.
122///
123/// # Type Parameters
124///
125/// - `T` - The type of the parsed dataset. Can be any type, such as
126///   `(Array2<f64>, Array1<f64>)`, a custom struct, or any other data representation.
127///   `T` must implement `Send + Sync` for `Dataset<T, E>` to be shared across threads.
128/// - `E` - The error type returned by the loader. Callers choose it freely (e.g.
129///   `std::io::Error`, a crate-specific `DatasetError`, or `std::convert::Infallible`
130///   for loaders that cannot fail).
131///
132/// # Thread Safety
133///
134/// `Dataset<T, E>` is `Send + Sync` when `T` is `Send + Sync` (the stored loader is
135/// always `Send + Sync`). The internal `OnceLock` ensures that the loader runs at
136/// most once, even when multiple threads call [`Dataset::load`] concurrently.
137///
138/// # Example
139///
140/// ```rust
141/// use dataset_core::Dataset;
142///
143/// // Define a simple loader that reads a value from the storage directory path.
144/// // The loader can return any error type you choose.
145/// fn my_loader(dir: &str) -> Result<Vec<String>, std::io::Error> {
146///     // In a real use case, you would download/read files from `dir`.
147///     // Here we just demonstrate the caching behavior.
148///     Ok(vec!["hello".to_string(), "world".to_string()])
149/// }
150///
151/// // The loader is bound to the dataset at construction time.
152/// let mut dataset: Dataset<Vec<String>, std::io::Error> = Dataset::new("./my_data", my_loader);
153///
154/// // The first call to `load` triggers the loader
155/// let data = dataset.load().unwrap();
156/// assert_eq!(data.len(), 2);
157///
158/// // Subsequent calls return the cached reference instantly
159/// let data_again = dataset.load().unwrap();
160/// assert!(std::ptr::eq(data, data_again)); // same reference, no re-load
161///
162/// // Check whether data has been loaded
163/// assert!(dataset.is_loaded());
164///
165/// // Borrow the cached value without reloading, or edit it in place via `get_mut`.
166/// if let Some(v) = dataset.get_mut() {
167///     v[0] = "HELLO".to_string();
168/// }
169/// assert_eq!(dataset.get().unwrap()[0], "HELLO");
170///
171/// // Move the cached value out without cloning.
172/// // `take` leaves `dataset` reusable; `into_inner` consumes it.
173/// let owned = dataset.take().unwrap();
174/// assert_eq!(owned.len(), 2);
175/// assert!(!dataset.is_loaded()); // `take` reset it to unloaded
176///
177/// dataset.load().unwrap(); // reloads, since `take` cleared the cache
178/// let owned = dataset.into_inner().unwrap();
179/// assert_eq!(owned.len(), 2);
180/// ```
181pub struct Dataset<T, E> {
182    storage_dir: String,
183    loader: Loader<T, E>,
184    data: OnceLock<T>,
185}
186
187impl<T, E> Dataset<T, E> {
188    /// Create a new `Dataset` instance without loading any data.
189    ///
190    /// This is a lightweight operation that only stores the storage directory path
191    /// and the loader. No I/O or network requests are performed until
192    /// [`Dataset::load`] is called.
193    ///
194    /// # Parameters
195    ///
196    /// - `storage_dir` - Directory where dataset files will be stored. The directory
197    ///   will be created automatically when the loader runs if it does not exist.
198    /// - `loader` - A closure or function that takes the storage directory path (`&str`)
199    ///   and returns `Result<T, E>`. This is where you perform downloading, file I/O,
200    ///   and parsing. It runs at most once (see [`Dataset::load`]). Because it is
201    ///   stored behind `Box<dyn Fn ...>`, it must be `Send + Sync + 'static` —
202    ///   capture owned values or clones rather than borrowing from the environment.
203    ///
204    /// # Returns
205    ///
206    /// A new `Dataset<T, E>` instance ready for lazy loading.
207    pub fn new(
208        storage_dir: &str,
209        loader: impl Fn(&str) -> Result<T, E> + Send + Sync + 'static,
210    ) -> Self {
211        Dataset {
212            storage_dir: storage_dir.to_string(),
213            loader: Box::new(loader),
214            data: OnceLock::new(),
215        }
216    }
217
218    /// Load the dataset, executing the stored loader on first call and caching the result.
219    ///
220    /// On the first call, the loader supplied to [`Dataset::new`] (or last set via
221    /// [`Dataset::set_loader`]) is invoked with the storage directory path. The
222    /// returned value is cached internally. All subsequent calls — from any thread —
223    /// return a reference to the cached value without running the loader again.
224    ///
225    /// # Returns
226    ///
227    /// - `Ok(&T)` - A reference to the cached dataset.
228    ///
229    /// # Errors
230    ///
231    /// Returns any error produced by the loader on first invocation. Once data is
232    /// successfully loaded and cached, this method never returns an error.
233    pub fn load(&self) -> Result<&T, E> {
234        if let Some(data) = self.data.get() {
235            return Ok(data);
236        }
237
238        let value = (self.loader)(&self.storage_dir)?;
239        let _ = self.data.set(value);
240
241        Ok(self
242            .data
243            .get()
244            .expect("data should be set after successful load"))
245    }
246
247    /// Replace the loader and invalidate any cached data.
248    ///
249    /// Use this when the parsing logic itself needs to change. The new loader is
250    /// **not** run immediately: this method only swaps the loader and drops the
251    /// cached value (resetting the `Dataset` to its unloaded state), so the next
252    /// [`Dataset::load`] lazily re-parses with the new loader. This keeps the
253    /// "no I/O until access" contract intact.
254    ///
255    /// To re-run the *same* loader instead, use [`Dataset::invalidate`].
256    ///
257    /// # Parameters
258    ///
259    /// - `loader` - The replacement loader. Like the one given to [`Dataset::new`],
260    ///   it must be `Send + Sync + 'static`.
261    ///
262    /// # Example
263    ///
264    /// ```rust
265    /// use dataset_core::Dataset;
266    ///
267    /// let mut ds: Dataset<i32, std::convert::Infallible> = Dataset::new("./data", |_| Ok(1));
268    /// assert_eq!(*ds.load().unwrap(), 1);
269    ///
270    /// ds.set_loader(|_| Ok(2)); // swap the loader; the old cache is dropped
271    /// assert!(!ds.is_loaded());
272    /// assert_eq!(*ds.load().unwrap(), 2); // next load uses the new loader
273    /// ```
274    pub fn set_loader(&mut self, loader: impl Fn(&str) -> Result<T, E> + Send + Sync + 'static) {
275        self.loader = Box::new(loader);
276        self.invalidate();
277    }
278
279    /// Drop the cached value, keeping the current loader.
280    ///
281    /// Resets the `Dataset` to its unloaded state so the next [`Dataset::load`]
282    /// re-runs the **current** loader from scratch — useful when the underlying
283    /// files have changed on disk and you want to re-parse them. To swap in a
284    /// *different* loader, use [`Dataset::set_loader`].
285    ///
286    /// Unlike [`Dataset::take`], this does not hand the cached value back; it simply
287    /// discards it.
288    ///
289    /// # Example
290    ///
291    /// ```rust
292    /// use dataset_core::Dataset;
293    ///
294    /// let mut ds: Dataset<i32, std::convert::Infallible> = Dataset::new("./data", |_| Ok(1));
295    /// ds.load().unwrap();
296    /// assert!(ds.is_loaded());
297    ///
298    /// ds.invalidate(); // drop the cache, keep the loader
299    /// assert!(!ds.is_loaded());
300    /// assert_eq!(*ds.load().unwrap(), 1); // reloads with the same loader
301    /// ```
302    pub fn invalidate(&mut self) {
303        let _ = self.data.take();
304    }
305
306    /// Check whether the dataset has been loaded into memory.
307    ///
308    /// # Returns
309    ///
310    /// `true` if [`Dataset::load`] has been called successfully at least once,
311    /// `false` otherwise.
312    pub fn is_loaded(&self) -> bool {
313        self.data.get().is_some()
314    }
315
316    /// Get the storage directory path.
317    ///
318    /// # Returns
319    ///
320    /// The storage directory path as a string slice.
321    pub fn storage_dir(&self) -> &str {
322        &self.storage_dir
323    }
324
325    /// Get a reference to the cached value **without** triggering loading.
326    ///
327    /// Unlike [`Dataset::load`], this never runs the loader: if the dataset has
328    /// not been loaded yet, it returns `None` rather than downloading/parsing.
329    /// Use it when you only want the data if it is already in memory and want to
330    /// avoid paying the loader's I/O cost otherwise — for example a fast path
331    /// that falls back to other work when the dataset is not yet cached.
332    ///
333    /// This is the reference-returning companion of [`Dataset::is_loaded`]:
334    /// `is_loaded()` answers *whether* the value is cached, `get()` hands you the
335    /// cached reference when it is.
336    ///
337    /// # Returns
338    ///
339    /// - `Some(&T)` - a reference to the cached value, if the dataset had been loaded.
340    /// - `None` - if the dataset has not been loaded.
341    ///
342    /// # Example
343    ///
344    /// ```rust
345    /// use dataset_core::Dataset;
346    ///
347    /// let ds: Dataset<Vec<i32>, std::convert::Infallible> =
348    ///     Dataset::new("./data", |_| Ok(vec![1, 2, 3]));
349    /// assert!(ds.get().is_none()); // not loaded yet — no loader is run
350    ///
351    /// ds.load().unwrap();
352    /// assert_eq!(ds.get(), Some(&vec![1, 2, 3]));
353    /// ```
354    pub fn get(&self) -> Option<&T> {
355        self.data.get()
356    }
357
358    /// Get a mutable reference to the cached value for **in-place** editing.
359    ///
360    /// This is the only way to mutate the cached value without moving it out:
361    /// you can tweak the loaded data (e.g. normalize features, fill in missing
362    /// entries, augment samples) and the changes persist in the cache, so later
363    /// [`Dataset::load`] / [`Dataset::get`] calls observe them.
364    ///
365    /// Because it requires unique access (`&mut self`), there is no aliasing or
366    /// race concern. And unlike [`take`](Dataset::take) /
367    /// [`into_inner`](Dataset::into_inner), it neither clones nor removes the
368    /// value — the `Dataset` stays loaded.
369    ///
370    /// Like [`Dataset::get`], this does **not** trigger loading: it returns
371    /// `None` if the dataset has not been loaded. Call [`Dataset::load`] first if
372    /// you need to ensure the value is present.
373    ///
374    /// # Returns
375    ///
376    /// - `Some(&mut T)` - a mutable reference to the cached value, if the dataset
377    ///   had been loaded.
378    /// - `None` - if the dataset has not been loaded.
379    ///
380    /// # Example
381    ///
382    /// ```rust
383    /// use dataset_core::Dataset;
384    ///
385    /// let mut ds: Dataset<Vec<i32>, std::convert::Infallible> =
386    ///     Dataset::new("./data", |_| Ok(vec![1, 2, 3]));
387    /// assert!(ds.get_mut().is_none()); // not loaded yet — no loader is run
388    ///
389    /// ds.load().unwrap();
390    /// if let Some(data) = ds.get_mut() {
391    ///     data.push(4); // edit the cached value in place, no clone, no reload
392    /// }
393    /// assert_eq!(ds.get(), Some(&vec![1, 2, 3, 4])); // the change persisted
394    /// ```
395    pub fn get_mut(&mut self) -> Option<&mut T> {
396        self.data.get_mut()
397    }
398
399    /// Consume the `Dataset` and return the cached value, if any.
400    ///
401    /// This **moves** the cached `T` out of the container — there is no clone.
402    /// Because it takes `self` by value, the `Dataset` is consumed and cannot be
403    /// used afterwards.
404    ///
405    /// This method does **not** trigger loading: it returns `None` if the dataset
406    /// was never loaded. Call [`Dataset::load`] first if you need to ensure the
407    /// value is present.
408    ///
409    /// # `into_inner` vs [`take`](Dataset::take)
410    ///
411    /// Both move the cached value out without cloning; the difference is what
412    /// happens to the container:
413    ///
414    /// - [`into_inner`](Dataset::into_inner) takes `self` and **consumes** the
415    ///   `Dataset`. Use it when you are done with the container.
416    /// - [`take`](Dataset::take) takes `&mut self`, leaving the `Dataset`
417    ///   **reusable** in its unloaded state (a later [`load`](Dataset::load)
418    ///   re-runs the loader).
419    ///
420    /// # Returns
421    ///
422    /// - `Some(T)` - the cached value, if the dataset had been loaded.
423    /// - `None` - if the dataset was never loaded.
424    ///
425    /// # Example
426    ///
427    /// ```rust
428    /// use dataset_core::Dataset;
429    ///
430    /// let ds: Dataset<Vec<i32>, std::convert::Infallible> =
431    ///     Dataset::new("./data", |_| Ok(vec![1, 2, 3]));
432    /// ds.load().unwrap();
433    ///
434    /// let owned: Vec<i32> = ds.into_inner().unwrap();
435    /// assert_eq!(owned, vec![1, 2, 3]);
436    /// // `ds` has been consumed and can no longer be used.
437    ///
438    /// // A dataset that was never loaded yields `None`.
439    /// let empty: Dataset<Vec<i32>, std::convert::Infallible> =
440    ///     Dataset::new("./data", |_| Ok(vec![1, 2, 3]));
441    /// assert!(empty.into_inner().is_none());
442    /// ```
443    #[must_use = "this consumes the Dataset; discarding the returned value drops the loaded data"]
444    pub fn into_inner(self) -> Option<T> {
445        self.data.into_inner()
446    }
447
448    /// Take the cached value out of the `Dataset`, leaving it reusable.
449    ///
450    /// This **moves** the cached `T` out — there is no clone — and resets the
451    /// `Dataset` to its unloaded state. Unlike [`into_inner`](Dataset::into_inner),
452    /// the container is left intact: it can be used again, and a later
453    /// [`Dataset::load`] will run the loader from scratch.
454    ///
455    /// This method does **not** trigger loading: it returns `None` if the dataset
456    /// was not loaded.
457    ///
458    /// # `take` vs [`into_inner`](Dataset::into_inner)
459    ///
460    /// Both move the cached value out without cloning; the difference is what
461    /// happens to the container:
462    ///
463    /// - [`take`](Dataset::take) takes `&mut self` and keeps the `Dataset`
464    ///   **reusable** (reset to unloaded) after extracting the value.
465    /// - [`into_inner`](Dataset::into_inner) takes `self` and **consumes** the
466    ///   container entirely.
467    ///
468    /// # Returns
469    ///
470    /// - `Some(T)` - the cached value, if the dataset had been loaded.
471    /// - `None` - if the dataset was not loaded.
472    ///
473    /// # Example
474    ///
475    /// ```rust
476    /// use dataset_core::Dataset;
477    ///
478    /// let mut ds: Dataset<i32, std::convert::Infallible> = Dataset::new("./data", |_| Ok(1));
479    /// ds.load().unwrap();
480    /// assert!(ds.is_loaded());
481    ///
482    /// let taken = ds.take().unwrap();
483    /// assert_eq!(taken, 1);
484    /// assert!(!ds.is_loaded()); // reset to unloaded, but `ds` is still usable
485    ///
486    /// // Because it was reset, `load` runs the loader again:
487    /// let reloaded = ds.load().unwrap();
488    /// assert_eq!(*reloaded, 1);
489    /// ```
490    #[must_use = "discarding the returned value drops the data taken out of the Dataset"]
491    pub fn take(&mut self) -> Option<T> {
492        self.data.take()
493    }
494}
495
496impl<T, E> std::fmt::Debug for Dataset<T, E> {
497    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
498        f.debug_struct("Dataset")
499            .field("storage_dir", &self.storage_dir)
500            .field("data_loaded", &self.is_loaded())
501            .finish()
502    }
503}
504
505/// Error handling module.
506///
507/// Provides structured error types for dataset loading operations including
508/// download failures, validation errors, I/O errors, and detailed data format
509/// errors with line numbers and contextual information for debugging.
510#[cfg(feature = "utils")]
511pub mod error;
512
513/// Utility functions for dataset authors.
514///
515/// Provides helpers for downloading files, extracting archives, verifying
516/// SHA256 hashes, and managing the dataset acquisition workflow.
517#[cfg(feature = "utils")]
518pub mod utils;