Skip to main content

rskit_dataset/
item.rs

1use std::collections::HashMap;
2use std::path::{Path, PathBuf};
3
4use rskit_errors::{AppError, AppResult, ErrorCode};
5
6use crate::{DataPayload, DatasetLimits, Label, MediaType};
7
8/// Capability an item must provide to flow through the generic collection engine.
9///
10/// The engine stays item-agnostic: it routes and counts items by [`label`](DatasetItem::label)
11/// and resumes sources from [`source_offset`](DatasetItem::source_offset),
12/// without knowing the concrete item type. Both blob samples ([`DataItem`])
13/// and tabular records implement this trait.
14pub trait DatasetItem: Send + 'static {
15    /// Classification label used to route and count the item. Defaults to [`Label::Real`].
16    fn label(&self) -> Label {
17        Label::Real
18    }
19
20    /// Source-provided resume cursor observed after this item, if any.
21    fn source_offset(&self) -> Option<usize> {
22        None
23    }
24}
25
26/// A single data sample flowing through the dataset pipeline.
27#[derive(Debug, Clone)]
28pub struct DataItem {
29    /// Payload bytes or file reference.
30    payload: DataPayload,
31    /// Dataset label.
32    pub label: Label,
33    /// Media kind for the sample.
34    pub media_type: MediaType,
35    /// Logical source name.
36    pub source_name: String,
37    /// File extension including the leading dot.
38    pub extension: String,
39    /// String metadata attached to the sample.
40    pub metadata: HashMap<String, String>,
41    /// Source-reported resume cursor after this item, if available.
42    source_offset: Option<usize>,
43}
44
45impl DataItem {
46    /// Create an item from bounded in-memory bytes.
47    pub fn new(
48        bytes: Vec<u8>,
49        label: Label,
50        media_type: MediaType,
51        source_name: impl Into<String>,
52    ) -> AppResult<Self> {
53        Self::new_bytes(bytes, label, media_type, source_name)
54    }
55
56    /// Create an item from bounded in-memory bytes.
57    pub fn new_bytes(
58        bytes: Vec<u8>,
59        label: Label,
60        media_type: MediaType,
61        source_name: impl Into<String>,
62    ) -> AppResult<Self> {
63        Self::new_bytes_with_limits(
64            bytes,
65            label,
66            media_type,
67            source_name,
68            &DatasetLimits::default(),
69        )
70    }
71
72    /// Create an item from bounded in-memory bytes with explicit limits.
73    pub fn new_bytes_with_limits(
74        bytes: Vec<u8>,
75        label: Label,
76        media_type: MediaType,
77        source_name: impl Into<String>,
78        limits: &DatasetLimits,
79    ) -> AppResult<Self> {
80        Ok(Self {
81            payload: DataPayload::bytes(bytes, limits)?,
82            label,
83            media_type,
84            source_name: source_name.into(),
85            extension: ".jpg".to_string(),
86            metadata: HashMap::new(),
87            source_offset: None,
88        })
89    }
90
91    /// Create an item from a file path for streaming large payloads.
92    #[must_use]
93    pub fn new_file(
94        path: impl Into<PathBuf>,
95        label: Label,
96        media_type: MediaType,
97        source_name: impl Into<String>,
98    ) -> Self {
99        Self {
100            payload: DataPayload::file(path),
101            label,
102            media_type,
103            source_name: source_name.into(),
104            extension: ".bin".to_string(),
105            metadata: HashMap::new(),
106            source_offset: None,
107        }
108    }
109
110    /// Borrow the item payload.
111    #[must_use]
112    pub fn payload(&self) -> &DataPayload {
113        &self.payload
114    }
115
116    /// Replace the payload after validating it against explicit limits.
117    pub fn try_with_payload(
118        mut self,
119        payload: DataPayload,
120        limits: &DatasetLimits,
121    ) -> AppResult<Self> {
122        if payload.is_bytes() && payload.len()? > limits.max_in_memory_bytes as u64 {
123            return Err(AppError::new(
124                ErrorCode::InvalidInput,
125                format!(
126                    "in-memory dataset payload exceeds max_in_memory_bytes={}",
127                    limits.max_in_memory_bytes
128                ),
129            ));
130        }
131        self.payload = payload;
132        Ok(self)
133    }
134
135    /// Return the source-provided resume cursor after this item, if available.
136    #[must_use]
137    pub fn source_offset(&self) -> Option<usize> {
138        self.source_offset
139    }
140
141    /// Attach a source-provided resume cursor.
142    #[must_use]
143    pub fn with_source_offset(mut self, offset: usize) -> Self {
144        self.source_offset = Some(offset);
145        self
146    }
147
148    /// Set the output extension.
149    #[must_use]
150    pub fn with_extension(mut self, ext: impl Into<String>) -> Self {
151        self.extension = ext.into();
152        self
153    }
154
155    /// Attach string metadata.
156    #[must_use]
157    pub fn with_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
158        self.metadata.insert(key.into(), value.into());
159        self
160    }
161
162    /// Validate the item against configured limits and path safety rules.
163    pub fn validate(&self, limits: &DatasetLimits) -> AppResult<()> {
164        validate_extension(&self.extension)?;
165        let len = self.payload.len()?;
166        if self.payload.is_bytes() && len > limits.max_in_memory_bytes as u64 {
167            return Err(AppError::new(
168                ErrorCode::InvalidInput,
169                format!(
170                    "in-memory dataset payload is {len} bytes, exceeding max_in_memory_bytes={}",
171                    limits.max_in_memory_bytes
172                ),
173            ));
174        }
175        Ok(())
176    }
177
178    /// Write this item to `path` using the configured payload limits.
179    pub fn write_to_path(&self, path: &Path, limits: &DatasetLimits) -> AppResult<u64> {
180        self.validate(limits)?;
181        self.payload.write_to_path(path, limits)
182    }
183}
184
185fn validate_extension(extension: &str) -> AppResult<()> {
186    let extension = extension.trim_start_matches('.');
187    if extension.is_empty()
188        || extension.contains('/')
189        || extension.contains('\\')
190        || extension == "."
191        || extension == ".."
192        || extension.contains("..")
193    {
194        return Err(AppError::new(
195            ErrorCode::InvalidInput,
196            format!("invalid dataset item extension: {extension:?}"),
197        ));
198    }
199    rskit_validation::input::validate_safe_path(extension)
200}
201
202impl crate::DatasetItem for DataItem {
203    fn label(&self) -> Label {
204        self.label
205    }
206
207    fn source_offset(&self) -> Option<usize> {
208        self.source_offset
209    }
210}