Skip to main content

surrealml_core/storage/header/
mod.rs

1//! Handles the loading, saving, and utilisation of all the data in the header of the model file.
2pub mod engine;
3pub mod input_dims;
4pub mod keys;
5pub mod normalisers;
6pub mod origin;
7pub mod output;
8pub mod string_value;
9pub mod version;
10
11use engine::Engine;
12use input_dims::InputDims;
13use keys::KeyBindings;
14use normalisers::NormaliserMap;
15use normalisers::wrapper::NormaliserType;
16use origin::Origin;
17use output::Output;
18use string_value::StringValue;
19use version::Version;
20
21use crate::errors::error::{SurrealError, SurrealErrorStatus};
22use crate::safe_eject;
23
24/// The header of the model file.
25///
26/// # Fields
27/// * `keys` - The key bindings where the order of the input columns is stored.
28/// * `normalisers` - The normalisers where the normalisation functions are stored per column if
29///   there are any.
30/// * `output` - The output where the output column name and normaliser are stored if there are any.
31/// * `name` - The name of the model.
32/// * `version` - The version of the model.
33/// * `description` - The description of the model.
34/// * `engine` - The engine of the model (could be native or pytorch).
35/// * `origin` - The origin of the model which is where the model was created and who the author is.
36#[derive(Debug, PartialEq)]
37pub struct Header {
38	pub keys: KeyBindings,
39	pub normalisers: NormaliserMap,
40	pub output: Output,
41	pub name: StringValue,
42	pub version: Version,
43	pub description: StringValue,
44	pub engine: Engine,
45	pub origin: Origin,
46	pub input_dims: InputDims,
47}
48
49impl Header {
50	/// Creates a new header with no columns or normalisers.
51	///
52	/// # Returns
53	/// A new header with no columns or normalisers.
54	pub fn fresh() -> Self {
55		Header {
56			keys: KeyBindings::fresh(),
57			normalisers: NormaliserMap::fresh(),
58			output: Output::fresh(),
59			name: StringValue::fresh(),
60			version: Version::fresh(),
61			description: StringValue::fresh(),
62			engine: Engine::fresh(),
63			origin: Origin::fresh(),
64			input_dims: InputDims::fresh(),
65		}
66	}
67
68	/// Adds a model name to the `self.name` field.
69	///
70	/// # Arguments
71	/// * `model_name` - The name of the model to be added.
72	pub fn add_name(&mut self, model_name: String) {
73		self.name = StringValue::from_string(model_name);
74	}
75
76	/// Adds a version to the `self.version` field.
77	///
78	/// # Arguments
79	/// * `version` - The version to be added.
80	pub fn add_version(&mut self, version: String) -> Result<(), SurrealError> {
81		self.version = Version::from_string(version)?;
82		Ok(())
83	}
84
85	/// Adds a description to the `self.description` field.
86	///
87	/// # Arguments
88	/// * `description` - The description to be added.
89	pub fn add_description(&mut self, description: String) {
90		self.description = StringValue::from_string(description);
91	}
92
93	/// Adds a column name to the `self.keys` field. It must be noted that the order in which the
94	/// columns are added is the order in which they will be expected in the input data. We can do
95	/// this with the followng example:
96	///
97	/// # Arguments
98	/// * `column_name` - The name of the column to be added.
99	pub fn add_column(&mut self, column_name: String) {
100		self.keys.add_column(column_name);
101	}
102
103	/// Adds a normaliser to the `self.normalisers` field.
104	///
105	/// # Arguments
106	/// * `column_name` - The name of the column to which the normaliser will be applied.
107	/// * `normaliser` - The normaliser to be applied to the column.
108	pub fn add_normaliser(
109		&mut self,
110		column_name: String,
111		normaliser: NormaliserType,
112	) -> Result<(), SurrealError> {
113		self.normalisers.add_normaliser(normaliser, column_name, &self.keys)?;
114		Ok(())
115	}
116
117	/// Gets the normaliser for a given column name.
118	///
119	/// # Arguments
120	/// * `column_name` - The name of the column to which the normaliser will be applied.
121	///
122	/// # Returns
123	/// The normaliser for the given column name.
124	pub fn get_normaliser(
125		&self,
126		column_name: &String,
127	) -> Result<Option<&NormaliserType>, SurrealError> {
128		self.normalisers.get_normaliser(column_name.to_string(), &self.keys)
129	}
130
131	/// Adds an output column to the `self.output` field.
132	///
133	/// # Arguments
134	/// * `column_name` - The name of the column to be added.
135	/// * `normaliser` - The normaliser to be applied to the column.
136	pub fn add_output(&mut self, column_name: String, normaliser: Option<NormaliserType>) {
137		self.output.name = Some(column_name);
138		self.output.normaliser = normaliser;
139	}
140
141	/// Adds an engine to the `self.engine` field.
142	///
143	/// # Arguments
144	/// * `engine` - The engine to be added.
145	pub fn add_engine(&mut self, engine: String) {
146		self.engine = Engine::from_string(engine);
147	}
148
149	/// Adds an author to the `self.origin` field.
150	///
151	/// # Arguments
152	/// * `author` - The author to be added.
153	pub fn add_author(&mut self, author: String) {
154		self.origin.add_author(author);
155	}
156
157	/// Adds an origin to the `self.origin` field.
158	///
159	/// # Arguments
160	/// * `origin` - The origin to be added.
161	pub fn add_origin(&mut self, origin: String) -> Result<(), SurrealError> {
162		self.origin.add_origin(origin)
163	}
164
165	/// The standard delimiter used to seperate each field in the header.
166	fn delimiter() -> &'static str {
167		"//=>"
168	}
169
170	/// Constructs the `Header` struct from bytes.
171	///
172	/// # Arguments
173	/// * `data` - The bytes to be converted into a `Header` struct.
174	///
175	/// # Returns
176	/// The `Header` struct.
177	pub fn from_bytes(data: Vec<u8>) -> Result<Self, SurrealError> {
178		let string_data = safe_eject!(String::from_utf8(data), SurrealErrorStatus::BadRequest);
179
180		let buffer = string_data.split(Self::delimiter()).collect::<Vec<&str>>();
181
182		let keys: KeyBindings = KeyBindings::from_string(buffer.get(1).unwrap_or(&"").to_string());
183		let normalisers =
184			NormaliserMap::from_string(buffer.get(2).unwrap_or(&"").to_string(), &keys)?;
185		let output = Output::from_string(buffer.get(3).unwrap_or(&"").to_string())?;
186		let name = StringValue::from_string(buffer.get(4).unwrap_or(&"").to_string());
187		let version = Version::from_string(buffer.get(5).unwrap_or(&"").to_string())?;
188		let description = StringValue::from_string(buffer.get(6).unwrap_or(&"").to_string());
189		let engine = Engine::from_string(buffer.get(7).unwrap_or(&"").to_string());
190		let origin = Origin::from_string(buffer.get(8).unwrap_or(&"").to_string())?;
191		let input_dims = InputDims::from_string(buffer.get(9).unwrap_or(&"").to_string())?;
192		Ok(Header {
193			keys,
194			normalisers,
195			output,
196			name,
197			version,
198			description,
199			engine,
200			origin,
201			input_dims,
202		})
203	}
204
205	/// Converts the `Header` struct into bytes.
206	///
207	/// # Returns
208	/// A tuple containing the number of bytes in the header and the bytes themselves.
209	pub fn to_bytes(&self) -> (i32, Vec<u8>) {
210		let buffer = vec![
211			"".to_string(),
212			self.keys.to_string(),
213			self.normalisers.to_string(),
214			self.output.to_string(),
215			self.name.to_string(),
216			self.version.to_string(),
217			self.description.to_string(),
218			self.engine.to_string(),
219			self.origin.to_string(),
220			self.input_dims.to_string(),
221			"".to_string(),
222		];
223		let buffer = buffer.join(Self::delimiter()).into_bytes();
224		(buffer.len() as i32, buffer)
225	}
226}
227
228#[cfg(test)]
229mod tests {
230
231	use super::keys::tests::generate_string as generate_key_string;
232	use super::normalisers::clipping::Clipping;
233	use super::normalisers::linear_scaling::LinearScaling;
234	use super::normalisers::log_scale::LogScaling;
235	use super::normalisers::tests::generate_string as generate_normaliser_string;
236	use super::normalisers::z_score::ZScore;
237	use super::*;
238
239	pub fn generate_string() -> String {
240		let keys = generate_key_string();
241		let normalisers = generate_normaliser_string();
242		let output = "g=>linear_scaling(0.0,1.0)".to_string();
243		format!(
244			"{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}",
245			Header::delimiter(),
246			keys,
247			Header::delimiter(),
248			normalisers,
249			Header::delimiter(),
250			output,
251			Header::delimiter(),
252			"test model name",
253			Header::delimiter(),
254			"0.0.1",
255			Header::delimiter(),
256			"test description",
257			Header::delimiter(),
258			Engine::PyTorch,
259			Header::delimiter(),
260			Origin::from_string("author=>local".to_string()).unwrap(),
261			Header::delimiter(),
262			InputDims::from_string("1,2".to_string()).unwrap(),
263			Header::delimiter(),
264		)
265	}
266
267	pub fn generate_bytes() -> Vec<u8> {
268		generate_string().into_bytes()
269	}
270
271	#[test]
272	fn test_from_bytes() {
273		let header = Header::from_bytes(generate_bytes()).unwrap();
274
275		assert_eq!(header.keys.store.len(), 6);
276		assert_eq!(header.keys.reference.len(), 6);
277		assert_eq!(header.normalisers.store.len(), 4);
278
279		assert_eq!(header.keys.store[0], "a");
280		assert_eq!(header.keys.store[1], "b");
281		assert_eq!(header.keys.store[2], "c");
282		assert_eq!(header.keys.store[3], "d");
283		assert_eq!(header.keys.store[4], "e");
284		assert_eq!(header.keys.store[5], "f");
285	}
286
287	#[test]
288	fn test_empty_header() {
289		let string = "//=>//=>//=>//=>//=>//=>//=>//=>//=>".to_string();
290		let data = string.as_bytes();
291		let header = Header::from_bytes(data.to_vec()).unwrap();
292
293		assert_eq!(header, Header::fresh());
294
295		let string = "".to_string();
296		let data = string.as_bytes();
297		let header = Header::from_bytes(data.to_vec()).unwrap();
298
299		assert_eq!(header, Header::fresh());
300	}
301
302	// Regression test for GHSA-jwr6-6444-28xv: the malformed header from the advisory
303	// proof-of-concept (a non-numeric `bad` input-dimensions field) must produce a
304	// structured error instead of panicking the parser.
305	#[test]
306	fn test_from_bytes_malformed_header_does_not_panic() {
307		let header = "//=>//=>//=>//=>m//=>1.2.3//=>desc//=>pytorch//=>author=>local//=>bad//=>";
308		let result = Header::from_bytes(header.as_bytes().to_vec());
309		assert!(result.is_err());
310	}
311
312	#[test]
313	fn test_to_bytes() {
314		let header = Header::from_bytes(generate_bytes()).unwrap();
315		let (bytes_num, bytes) = header.to_bytes();
316		let string = String::from_utf8(bytes).unwrap();
317
318		// below the integers are correct but there is a difference with the decimal point
319		// representation in the string, we can alter this fairly easy and will investigate it
320		let expected_string = "//=>a=>b=>c=>d=>e=>f//=>a=>linear_scaling(0,1)//b=>clipping(0,1.5)//c=>log_scaling(10,0)//e=>z_score(0,1)//=>g=>linear_scaling(0,1)//=>test model name//=>0.0.1//=>test description//=>pytorch//=>author=>local//=>1,2//=>".to_string();
321
322		assert_eq!(string, expected_string);
323		assert_eq!(bytes_num, expected_string.len() as i32);
324
325		let empty_header = Header::fresh();
326		let (bytes_num, bytes) = empty_header.to_bytes();
327		let string = String::from_utf8(bytes).unwrap();
328		let expected_string = "//=>//=>//=>//=>//=>//=>//=>//=>//=>//=>".to_string();
329
330		assert_eq!(string, expected_string);
331		assert_eq!(bytes_num, expected_string.len() as i32);
332	}
333
334	#[test]
335	fn test_add_column() {
336		let mut header = Header::fresh();
337		header.add_column("a".to_string());
338		header.add_column("b".to_string());
339		header.add_column("c".to_string());
340		header.add_column("d".to_string());
341		header.add_column("e".to_string());
342		header.add_column("f".to_string());
343
344		assert_eq!(header.keys.store.len(), 6);
345		assert_eq!(header.keys.reference.len(), 6);
346
347		assert_eq!(header.keys.store[0], "a");
348		assert_eq!(header.keys.store[1], "b");
349		assert_eq!(header.keys.store[2], "c");
350		assert_eq!(header.keys.store[3], "d");
351		assert_eq!(header.keys.store[4], "e");
352		assert_eq!(header.keys.store[5], "f");
353	}
354
355	#[test]
356	fn test_add_normalizer() {
357		let mut header = Header::fresh();
358		header.add_column("a".to_string());
359		header.add_column("b".to_string());
360		header.add_column("c".to_string());
361		header.add_column("d".to_string());
362		header.add_column("e".to_string());
363		header.add_column("f".to_string());
364
365		let _ = header.add_normaliser(
366			"a".to_string(),
367			NormaliserType::LinearScaling(LinearScaling {
368				min: 0.0,
369				max: 1.0,
370			}),
371		);
372		let _ = header.add_normaliser(
373			"b".to_string(),
374			NormaliserType::Clipping(Clipping {
375				min: Some(0.0),
376				max: Some(1.5),
377			}),
378		);
379		let _ = header.add_normaliser(
380			"c".to_string(),
381			NormaliserType::LogScaling(LogScaling {
382				base: 10.0,
383				min: 0.0,
384			}),
385		);
386		let _ = header.add_normaliser(
387			"e".to_string(),
388			NormaliserType::ZScore(ZScore {
389				mean: 0.0,
390				std_dev: 1.0,
391			}),
392		);
393
394		assert_eq!(header.normalisers.store.len(), 4);
395		assert_eq!(
396			header.normalisers.store[0],
397			NormaliserType::LinearScaling(LinearScaling {
398				min: 0.0,
399				max: 1.0
400			})
401		);
402		assert_eq!(
403			header.normalisers.store[1],
404			NormaliserType::Clipping(Clipping {
405				min: Some(0.0),
406				max: Some(1.5)
407			})
408		);
409		assert_eq!(
410			header.normalisers.store[2],
411			NormaliserType::LogScaling(LogScaling {
412				base: 10.0,
413				min: 0.0
414			})
415		);
416		assert_eq!(
417			header.normalisers.store[3],
418			NormaliserType::ZScore(ZScore {
419				mean: 0.0,
420				std_dev: 1.0
421			})
422		);
423	}
424}