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;