diff options
Diffstat (limited to 'systems/sheet')
| -rw-r--r-- | systems/sheet/macros/src/lib.rs | 167 | ||||
| -rw-r--r-- | systems/sheet/src/index_source.rs | 72 | ||||
| -rw-r--r-- | systems/sheet/src/mapping.rs | 10 | ||||
| -rw-r--r-- | systems/sheet/src/mapping/parse.rs | 189 | ||||
| -rw-r--r-- | systems/sheet/src/mapping/parse_test.rs | 252 | ||||
| -rw-r--r-- | systems/sheet/src/sheet.rs | 38 | ||||
| -rw-r--r-- | systems/sheet/src/sheet/error.rs | 14 | ||||
| -rw-r--r-- | systems/sheet/src/sheet/v1/constants.rs | 6 | ||||
| -rw-r--r-- | systems/sheet/src/sheet/v1/reader.rs | 20 | ||||
| -rw-r--r-- | systems/sheet/src/sheet/v1/test.rs | 950 | ||||
| -rw-r--r-- | systems/sheet/src/sheet/v1/writer.rs | 18 |
11 files changed, 1286 insertions, 450 deletions
diff --git a/systems/sheet/macros/src/lib.rs b/systems/sheet/macros/src/lib.rs index c06f25e..2990f7e 100644 --- a/systems/sheet/macros/src/lib.rs +++ b/systems/sheet/macros/src/lib.rs @@ -35,12 +35,22 @@ fn parse_sheet_path(input: &str) -> Result<(String, Vec<String>), String> { Ok((sheet, path_parts)) } -/// Parse strings in the format "id/ver" -fn parse_id_version(input: &str) -> Result<(u32, u16), String> { - let parts: Vec<&str> = input.split('/').collect(); +/// Parse strings in the format "id/ver" or "~id/ver" +/// Returns (remote, id, ver) +fn parse_id_version(input: &str) -> Result<(bool, u32, u16), String> { + let trimmed = input.trim(); + + // Check if it starts with ~ for local + let (remote, id_part) = if trimmed.starts_with('~') { + (false, &trimmed[1..]) + } else { + (true, trimmed) + }; + + let parts: Vec<&str> = id_part.split('/').collect(); if parts.len() != 2 { return Err(format!( - "Invalid id/version syntax. Expected: id/ver, got: {}", + "Invalid id/version syntax. Expected: id/ver or ~id/ver, got: {}", input )); } @@ -62,7 +72,7 @@ fn parse_id_version(input: &str) -> Result<(u32, u16), String> { .parse::<u16>() .map_err(|e| format!("Failed to parse version as u16: {}", e))?; - Ok((id, ver)) + Ok((remote, id, ver)) } /// Parse a path string into a vector of strings @@ -113,7 +123,7 @@ pub fn mapping_buf(input: TokenStream) -> TokenStream { } }; - let (id, ver) = match parse_id_version(right) { + let (remote, id, ver) = match parse_id_version(right) { Ok(result) => result, Err(err) => { return syn::Error::new(Span::call_site(), err) @@ -133,7 +143,7 @@ pub fn mapping_buf(input: TokenStream) -> TokenStream { #mapping_buf_path::new( #sheet.to_string(), #path_vec_tokens, - #index_source_path::new(#id, #ver) + #index_source_path::new(#remote, #id, #ver) ) }; @@ -176,7 +186,7 @@ pub fn mapping(input: TokenStream) -> TokenStream { } }; - let (id, ver) = match parse_id_version(right) { + let (remote, id, ver) = match parse_id_version(right) { Ok(result) => result, Err(err) => { return syn::Error::new(Span::call_site(), err) @@ -195,7 +205,7 @@ pub fn mapping(input: TokenStream) -> TokenStream { #mapping_path::new( #sheet, #path, - #index_source_path::new(#id, #ver) + #index_source_path::new(#remote, #id, #ver) ) }; @@ -203,9 +213,10 @@ pub fn mapping(input: TokenStream) -> TokenStream { } enum LocalMappingParts { - Latest(String, u32, u16), - Version(String, u32, u16), - WithRef(String, u32, u16, String), + Latest(String, bool, u32, u16), + Version(String, bool, u32, u16), + WithRef(String, bool, u32, u16, String), + VersionForward(String, bool, u32, u16, u16), } impl LocalMappingParts { @@ -221,13 +232,54 @@ impl LocalMappingParts { )); } - // When both "==" and "=>" appear + // Check for "=>" followed by "==" syntax: "path" => "id/ver" == "ver2" + if input_str.contains("=>") && input_str.contains("==") { + // Count occurrences to determine the pattern + let arrow_count = input_str.matches("=>").count(); + let equal_count = input_str.matches("==").count(); + + if arrow_count == 1 && equal_count == 1 { + // Try to parse as "path" => "id/ver" == "ver2" + let parts: Vec<&str> = input_str.split("=>").collect(); + if parts.len() == 2 { + let left = parts[0].trim().trim_matches('"').trim(); + let right_part = parts[1].trim(); + + // Split the right part by "==" + let right_parts: Vec<&str> = right_part.split("==").collect(); + if right_parts.len() == 2 { + let middle = right_parts[0].trim().trim_matches('"').trim(); + let version_str = right_parts[1].trim().trim_matches('"').trim(); + + let (remote, id, ver) = parse_id_version(middle) + .map_err(|err| syn::Error::new(Span::call_site(), err))?; + + let target_ver = version_str.parse::<u16>().map_err(|err| { + syn::Error::new( + Span::call_site(), + format!("Failed to parse target version as u16: {}", err), + ) + })?; + + return Ok(LocalMappingParts::VersionForward( + left.to_string(), + remote, + id, + ver, + target_ver, + )); + } + } + } + } + + // When both "==" and "=>" appear but not in the expected pattern // It's impossible to determine whether to match the current version or point to a Ref // Should report an error if input_str.contains("==") && input_str.contains("=>") { return Err(syn::Error::new( Span::call_site(), - "Ambiguous forward direction. Use either '==' for version or '=>' for ref, not both.", + "Ambiguous forward direction. Use either '==' for version or '=>' for ref, or use 'path' => 'id/ver' == 'ver2' syntax.", )); } @@ -243,10 +295,15 @@ impl LocalMappingParts { let left = parts[0].trim().trim_matches('"').trim(); let right = parts[1].trim().trim_matches('"').trim(); - let (id, ver) = + let (remote, id, ver) = parse_id_version(right).map_err(|err| syn::Error::new(Span::call_site(), err))?; - return Ok(LocalMappingParts::Version(left.to_string(), id, ver)); + return Ok(LocalMappingParts::Version( + left.to_string(), + remote, + id, + ver, + )); } let parts: Vec<&str> = input_str.split("=>").collect(); @@ -257,30 +314,44 @@ impl LocalMappingParts { let left = parts[0].trim().trim_matches('"').trim(); let right = parts[1].trim().trim_matches('"').trim(); - let (id, ver) = parse_id_version(right) + let (remote, id, ver) = parse_id_version(right) .map_err(|err| syn::Error::new(Span::call_site(), err))?; - Ok(LocalMappingParts::Latest(left.to_string(), id, ver)) + Ok(LocalMappingParts::Latest(left.to_string(), remote, id, ver)) } 3 => { - // local_mapping!("path" => "id/ver" => "ref") - Ref + // Check if the third part is a ref (string) or a version number (u16) let left = parts[0].trim().trim_matches('"').trim(); let middle = parts[1].trim().trim_matches('"').trim(); let right = parts[2].trim().trim_matches('"').trim(); - let (id, ver) = parse_id_version(middle) + let (remote, id, ver) = parse_id_version(middle) .map_err(|err| syn::Error::new(Span::call_site(), err))?; - Ok(LocalMappingParts::WithRef( - left.to_string(), - id, - ver, - right.to_string(), - )) + // Try to parse right as u16 (version number) + if let Ok(target_ver) = right.parse::<u16>() { + // This is "path" => "id/ver" => "ver2" syntax + Ok(LocalMappingParts::VersionForward( + left.to_string(), + remote, + id, + ver, + target_ver, + )) + } else { + // This is "path" => "id/ver" => "ref" syntax + Ok(LocalMappingParts::WithRef( + left.to_string(), + remote, + id, + ver, + right.to_string(), + )) + } } _ => Err(syn::Error::new( Span::call_site(), - "Invalid local_mapping syntax. Expected: local_mapping!(\"path\" => \"id/ver\") or local_mapping!(\"path\" == \"id/ver\") or local_mapping!(\"path\" => \"id/ver\" => \"ref\")", + "Invalid local_mapping syntax. Expected: local_mapping!(\"path\" => \"id/ver\") or local_mapping!(\"path\" == \"id/ver\") or local_mapping!(\"path\" => \"id/ver\" => \"ref\") or local_mapping!(\"path\" => \"id/ver\" => \"ver2\") or local_mapping!(\"path\" => \"id/ver\" == \"ver2\")", )), } } @@ -310,6 +381,18 @@ impl LocalMappingParts { /// // and expects to match the version declared in `ref` /// "your_dir/your_file.suffix" => "index_id/version" => "ref" /// ); +/// +/// let lcoal_mapping_version_forward = local_mapping!( +/// // Map the `version` of index `index_id` +/// // to `your_dir/your_file.suffix` +/// // but expects to point to a specific version `ver2` +/// "your_dir/your_file.suffix" => "index_id/version" => "ver2" +/// ); +/// +/// let lcoal_mapping_version_forward_alt = local_mapping!( +/// // Alternative syntax for the same behavior +/// "your_dir/your_file.suffix" => "index_id/version" == "ver2" +/// ); /// ``` #[proc_macro] pub fn local_mapping(input: TokenStream) -> TokenStream { @@ -326,44 +409,44 @@ pub fn local_mapping(input: TokenStream) -> TokenStream { parse_str(INDEX_SOURCE).expect("Failed to parse INDEX_SOURCE"); match parts { - LocalMappingParts::Latest(path_str, id, ver) => { + LocalMappingParts::Latest(path_str, remote, id, ver) => { let path_vec = parse_path_string(&path_str); let path_vec_tokens = path_vec_to_tokens(&path_vec); let expanded = quote! { #local_mapping_path::new( #path_vec_tokens, - #index_source_path::new(#id, #ver), + #index_source_path::new(#remote, #id, #ver), #local_mapping_forward_path::Latest ) }; expanded.into() } - LocalMappingParts::Version(path_str, id, ver) => { + LocalMappingParts::Version(path_str, remote, id, ver) => { let path_vec = parse_path_string(&path_str); let path_vec_tokens = path_vec_to_tokens(&path_vec); let expanded = quote! { #local_mapping_path::new( #path_vec_tokens, - #index_source_path::new(#id, #ver), + #index_source_path::new(#remote, #id, #ver), #local_mapping_forward_path::Version { - version_name: #ver.to_string() + version: #ver } ) }; expanded.into() } - LocalMappingParts::WithRef(path_str, id, ver, ref_name) => { + LocalMappingParts::WithRef(path_str, remote, id, ver, ref_name) => { let path_vec = parse_path_string(&path_str); let path_vec_tokens = path_vec_to_tokens(&path_vec); let expanded = quote! { #local_mapping_path::new( #path_vec_tokens, - #index_source_path::new(#id, #ver), + #index_source_path::new(#remote, #id, #ver), #local_mapping_forward_path::Ref { sheet_name: #ref_name.to_string() } @@ -372,5 +455,21 @@ pub fn local_mapping(input: TokenStream) -> TokenStream { expanded.into() } + LocalMappingParts::VersionForward(path_str, remote, id, ver, target_ver) => { + let path_vec = parse_path_string(&path_str); + let path_vec_tokens = path_vec_to_tokens(&path_vec); + + let expanded = quote! { + #local_mapping_path::new( + #path_vec_tokens, + #index_source_path::new(#remote, #id, #ver), + #local_mapping_forward_path::Version { + version: #target_ver + } + ) + }; + + expanded.into() + } } } diff --git a/systems/sheet/src/index_source.rs b/systems/sheet/src/index_source.rs index b22f5a6..e322670 100644 --- a/systems/sheet/src/index_source.rs +++ b/systems/sheet/src/index_source.rs @@ -4,6 +4,8 @@ use serde::{Deserialize, Serialize}; /// Points to a unique resource address in Vault #[derive(Debug, Clone, Copy, Serialize, Deserialize)] pub struct IndexSource { + remote: bool, + /// The index ID of the resource id: u32, @@ -15,8 +17,31 @@ pub struct IndexSource { impl IndexSource { /// Create IndexSource - pub fn new(id: u32, ver: u16) -> Self { - IndexSource { id, ver } + pub fn new(remote: bool, id: u32, ver: u16) -> Self { + IndexSource { remote, id, ver } + } + + /// Create IndexSource To Local Namespace + pub fn new_local(id: u32, ver: u16) -> Self { + IndexSource { + remote: false, + id, + ver, + } + } + + /// Create IndexSource To Remote Namespace + pub fn new_remote(id: u32, ver: u16) -> Self { + IndexSource { + remote: true, + id, + ver, + } + } + + /// Check if the IndexSource points to a remote namespace + pub fn is_remote(&self) -> bool { + self.remote } /// Get index ID from IndexSource @@ -34,7 +59,7 @@ impl IndexSource { impl PartialEq for IndexSource { fn eq(&self, other: &Self) -> bool { - &self.id == &other.id && &self.ver == &other.ver + &self.remote == &other.remote && &self.id == &other.id && &self.ver == &other.ver } } @@ -44,6 +69,7 @@ impl Eq for IndexSource {} impl std::hash::Hash for IndexSource { fn hash<H: std::hash::Hasher>(&self, state: &mut H) { + self.remote.hash(state); self.id.hash(state); self.ver.hash(state); } @@ -55,9 +81,13 @@ impl<'a> TryFrom<&'a str> for IndexSource { type Error = &'static str; fn try_from(value: &'a str) -> Result<Self, Self::Error> { - let parts: Vec<&str> = value.split('/').collect(); + let trimmed = value.trim(); + let remote = !trimmed.starts_with('~'); + let value_without_tilde = if remote { trimmed } else { &trimmed[1..] }; + + let parts: Vec<&str> = value_without_tilde.split('/').collect(); if parts.len() != 2 { - return Err("Invalid format: expected 'id/version'"); + return Err("Invalid format: expected '[~]id/version'"); } let id_str = parts[0].trim(); @@ -74,9 +104,7 @@ impl<'a> TryFrom<&'a str> for IndexSource { .parse::<u16>() .map_err(|_| "Version must be a valid u16")?; - // Check for overflow (though parsing already validates range) - // Additional bounds checks can be added here if needed - Ok(Self { id, ver }) + Ok(Self { remote, id, ver }) } } @@ -88,9 +116,9 @@ impl TryFrom<String> for IndexSource { } } -impl From<IndexSource> for (u32, u16) { +impl From<IndexSource> for (bool, u32, u16) { fn from(src: IndexSource) -> Self { - (src.id, src.ver) + (src.remote, src.id, src.ver) } } @@ -105,12 +133,34 @@ impl IndexSource { pub fn set_version(&mut self, version: u16) { self.ver = version; } + + /// Set the remote flag of IndexSource + pub fn set_is_remote(&mut self, remote: bool) { + self.remote = remote; + } + + /// Convert IndexSource to local namespace + pub fn to_local(&mut self) { + self.remote = false; + } + + /// Convert IndexSource to remote namespace + pub fn to_remote(&mut self) { + self.remote = true; + } } // Implement Display for IndexSourceBuf and IndexSource impl std::fmt::Display for IndexSource { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}/{}", self.id.to_string(), self.ver.to_string()) + let local_symbol = if self.remote { "" } else { "~" }; + write!( + f, + "{}{}/{}", + local_symbol, + self.id.to_string(), + self.ver.to_string() + ) } } diff --git a/systems/sheet/src/mapping.rs b/systems/sheet/src/mapping.rs index f509c0b..3dfb67e 100644 --- a/systems/sheet/src/mapping.rs +++ b/systems/sheet/src/mapping.rs @@ -6,6 +6,8 @@ use serde::{Deserialize, Serialize}; use crate::{index_source::IndexSource, mapping::error::ParseMappingError}; pub mod error; +pub mod parse; +pub mod parse_test; // Validation rules for LocalMapping // LocalMapping is a key component for writing and reading SheetData @@ -438,6 +440,14 @@ impl std::fmt::Display for LocalMapping { } } +impl TryFrom<String> for LocalMapping { + type Error = ParseMappingError; + + fn try_from(s: String) -> Result<Self, Self::Error> { + s.as_str().try_into() + } +} + // Implement editing functionality for MappingBuf and LocalMapping impl MappingBuf { diff --git a/systems/sheet/src/mapping/parse.rs b/systems/sheet/src/mapping/parse.rs new file mode 100644 index 0000000..e203c96 --- /dev/null +++ b/systems/sheet/src/mapping/parse.rs @@ -0,0 +1,189 @@ +use just_fmt::fmt_path::fmt_path_str; + +use crate::{ + index_source::IndexSource, + mapping::{LocalMapping, LocalMappingForward, error::ParseMappingError}, +}; + +impl TryFrom<&str> for LocalMapping { + type Error = ParseMappingError; + + fn try_from(s: &str) -> Result<Self, Self::Error> { + // Remove surrounding quotes if present + let s = s.trim_matches('"'); + + // Helper function to remove quotes from a string + fn remove_quotes(s: &str) -> String { + // Simply remove all quotes from the string + s.replace('"', "").trim().to_string() + } + + // Helper function to split by operator, handling both spaced and non-spaced versions + fn split_by_operator<'a>(s: &'a str, operator: &'a str) -> Vec<&'a str> { + let mut result = Vec::new(); + let mut start = 0; + + // Find all occurrences of the operator + let mut search_from = 0; + while let Some(pos) = s[search_from..].find(operator) { + let actual_pos = search_from + pos; + result.push(&s[start..actual_pos]); + start = actual_pos + operator.len(); + search_from = start; + } + + if start < s.len() { + result.push(&s[start..]); + } + + result + } + + // Helper function to find operator position + fn find_operator<'a>(s: &'a str, operator: &'a str) -> Option<usize> { + s.find(operator) + } + + // Try to parse "path" => "source" == "version" pattern + if let Some(arrow_pos) = find_operator(s, "=>") { + let after_arrow = &s[arrow_pos + 2..]; + if let Some(equal_pos) = find_operator(after_arrow, "==") { + // Format: "path" => "source" == "version" + let path = remove_quotes(s[..arrow_pos].trim()); + let middle_part = after_arrow[..equal_pos].trim(); + let version_part = after_arrow[equal_pos + 2..].trim(); + + let middle = remove_quotes(middle_part); + let version_part_str = remove_quotes(version_part); + + let val = fmt_path_str(path) + .map_err(|_| ParseMappingError::InvalidMapping)? + .split('/') + .map(|s| s.to_string()) + .collect(); + + let source = IndexSource::try_from(middle.as_str()) + .map_err(|_| ParseMappingError::InvalidMapping)?; + + let version = version_part_str + .parse::<u16>() + .map_err(|_| ParseMappingError::InvalidMapping)?; + + return Ok(LocalMapping { + val, + source, + forward: LocalMappingForward::Version { version }, + }); + } + } + + // Split by "=>" to parse the format + let parts = split_by_operator(s, "=>"); + + match parts.len() { + 1 => { + // Check for "==" operator + if let Some(equal_pos) = find_operator(s, "==") { + // Format: "path" == "source" (when mapped_version equals version) + let path_raw = s[..equal_pos].trim(); + let source_part_raw = s[equal_pos + 2..].trim(); + let path = remove_quotes(path_raw); + let source_str = remove_quotes(source_part_raw); + + let val = fmt_path_str(path) + .map_err(|_| ParseMappingError::InvalidMapping)? + .split('/') + .map(|s| s.to_string()) + .collect(); + + let source = IndexSource::try_from(source_str.as_str()) + .map_err(|_| ParseMappingError::InvalidMapping)?; + + let version = source.version(); + + Ok(LocalMapping { + val, + source, + forward: LocalMappingForward::Version { version }, + }) + } else { + Err(ParseMappingError::InvalidMapping) + } + } + 2 => { + // Check if the second part contains "==" + if let Some(equal_pos) = find_operator(parts[1], "==") { + // Format: "path" => "source" == "version" + let path = remove_quotes(parts[0].trim()); + let middle_part = parts[1][..equal_pos].trim(); + let version_part = parts[1][equal_pos + 2..].trim(); + + let middle = remove_quotes(middle_part); + let version_part_str = remove_quotes(version_part); + + let val = fmt_path_str(path) + .map_err(|_| ParseMappingError::InvalidMapping)? + .split('/') + .map(|s| s.to_string()) + .collect(); + + let source = IndexSource::try_from(middle.as_str()) + .map_err(|_| ParseMappingError::InvalidMapping)?; + + let version = version_part_str + .parse::<u16>() + .map_err(|_| ParseMappingError::InvalidMapping)?; + + return Ok(LocalMapping { + val, + source, + forward: LocalMappingForward::Version { version }, + }); + } + + // Format: "path" => "source" + let path = remove_quotes(parts[0].trim()); + let source_str = remove_quotes(parts[1].trim()); + + let val = fmt_path_str(path) + .map_err(|_| ParseMappingError::InvalidMapping)? + .split('/') + .map(|s| s.to_string()) + .collect(); + + let source = IndexSource::try_from(source_str.as_str()) + .map_err(|_| ParseMappingError::InvalidMapping)?; + + Ok(LocalMapping { + val, + source, + forward: LocalMappingForward::Latest, + }) + } + 3 => { + // Format: "path" => "source" => "sheet_name" + let path = remove_quotes(parts[0].trim()); + let source_str = remove_quotes(parts[1].trim()); + let sheet_name = remove_quotes(parts[2].trim()); + + let val = fmt_path_str(path) + .map_err(|_| ParseMappingError::InvalidMapping)? + .split('/') + .map(|s| s.to_string()) + .collect(); + + let source = IndexSource::try_from(source_str.as_str()) + .map_err(|_| ParseMappingError::InvalidMapping)?; + + Ok(LocalMapping { + val, + source, + forward: LocalMappingForward::Ref { + sheet_name: sheet_name, + }, + }) + } + _ => Err(ParseMappingError::InvalidMapping), + } + } +} diff --git a/systems/sheet/src/mapping/parse_test.rs b/systems/sheet/src/mapping/parse_test.rs new file mode 100644 index 0000000..a411360 --- /dev/null +++ b/systems/sheet/src/mapping/parse_test.rs @@ -0,0 +1,252 @@ +#[cfg(test)] +mod tests { + use crate::{ + index_source::IndexSource, + mapping::{LocalMapping, LocalMappingForward}, + }; + + /// Helper macro for comparing two LocalMapping instances + /// Checks equality of the mappings themselves, their forward fields, and their index sources + macro_rules! mapping_eq { + ($a:expr, $b:expr) => { + assert_eq!($a, $b); + assert_eq!($a.forward, $b.forward); + assert_eq!($a.index_source(), $b.index_source()); + }; + } + + #[test] + fn test_local_mapping_parse() { + let local_mapping = LocalMapping::new( + vec!["A.png".to_string()], + IndexSource::new(true, 1u32, 2u16), + LocalMappingForward::Latest, + ) + .unwrap(); + let local_mapping_gen = LocalMapping::try_from("\"A.png\" => \"1/2\"").unwrap(); + mapping_eq!(local_mapping, local_mapping_gen); + + let local_mapping = LocalMapping::new( + vec!["A.png".to_string()], + IndexSource::new(true, 1u32, 2u16), + LocalMappingForward::Version { version: 2u16 }, + ) + .unwrap(); + let local_mapping_gen = LocalMapping::try_from("\"A.png\" == \"1/2\"").unwrap(); + mapping_eq!(local_mapping, local_mapping_gen); + + let local_mapping = LocalMapping::new( + vec!["A.png".to_string()], + IndexSource::new(true, 1u32, 2u16), + LocalMappingForward::Ref { + sheet_name: "ref".to_string(), + }, + ) + .unwrap(); + let local_mapping_gen = LocalMapping::try_from("\"A.png\" => \"1/2\" => \"ref\"").unwrap(); + mapping_eq!(local_mapping, local_mapping_gen); + + let local_mapping = LocalMapping::new( + vec!["A.png".to_string()], + IndexSource::new(true, 1u32, 2u16), + LocalMappingForward::Version { version: 3u16 }, + ) + .unwrap(); + let local_mapping_gen = LocalMapping::try_from("\"A.png\" => \"1/2\" == \"3\"").unwrap(); + mapping_eq!(local_mapping, local_mapping_gen); + + let local_mapping = LocalMapping::new( + vec!["A.png".to_string()], + IndexSource::new(false, 1u32, 2u16), + LocalMappingForward::Latest, + ) + .unwrap(); + let local_mapping_gen = LocalMapping::try_from("\"A.png\" => \"~1/2\"").unwrap(); + mapping_eq!(local_mapping, local_mapping_gen); + + let local_mapping = LocalMapping::new( + vec!["A.png".to_string()], + IndexSource::new(false, 1u32, 2u16), + LocalMappingForward::Version { version: 2u16 }, + ) + .unwrap(); + let local_mapping_gen = LocalMapping::try_from("\"A.png\" == \"~1/2\"").unwrap(); + mapping_eq!(local_mapping, local_mapping_gen); + + let local_mapping = LocalMapping::new( + vec!["A.png".to_string()], + IndexSource::new(false, 1u32, 2u16), + LocalMappingForward::Ref { + sheet_name: "ref".to_string(), + }, + ) + .unwrap(); + let local_mapping_gen = LocalMapping::try_from("\"A.png\" => \"~1/2\" => \"ref\"").unwrap(); + mapping_eq!(local_mapping, local_mapping_gen); + + let local_mapping = LocalMapping::new( + vec!["A.png".to_string()], + IndexSource::new(false, 1u32, 2u16), + LocalMappingForward::Version { version: 3u16 }, + ) + .unwrap(); + let local_mapping_gen = LocalMapping::try_from("\"A.png\" => \"~1/2\" == \"3\"").unwrap(); + mapping_eq!(local_mapping, local_mapping_gen); + + let local_mapping = LocalMapping::new( + vec!["A.png".to_string()], + IndexSource::new(true, 1u32, 2u16), + LocalMappingForward::Latest, + ) + .unwrap(); + let local_mapping_gen = LocalMapping::try_from("\"A.png\"=>\"1/2\"").unwrap(); + mapping_eq!(local_mapping, local_mapping_gen); + + let local_mapping = LocalMapping::new( + vec!["A.png".to_string()], + IndexSource::new(true, 1u32, 2u16), + LocalMappingForward::Version { version: 2u16 }, + ) + .unwrap(); + let local_mapping_gen = LocalMapping::try_from("\"A.png\"==\"1/2\"").unwrap(); + mapping_eq!(local_mapping, local_mapping_gen); + + let local_mapping = LocalMapping::new( + vec!["A.png".to_string()], + IndexSource::new(true, 1u32, 2u16), + LocalMappingForward::Ref { + sheet_name: "ref".to_string(), + }, + ) + .unwrap(); + let local_mapping_gen = LocalMapping::try_from("\"A.png\"=>\"1/2\"=>\"ref\"").unwrap(); + mapping_eq!(local_mapping, local_mapping_gen); + + let local_mapping = LocalMapping::new( + vec!["A.png".to_string()], + IndexSource::new(true, 1u32, 2u16), + LocalMappingForward::Version { version: 3u16 }, + ) + .unwrap(); + let local_mapping_gen = LocalMapping::try_from("\"A.png\"=>\"1/2\"==\"3\"").unwrap(); + mapping_eq!(local_mapping, local_mapping_gen); + + let local_mapping = LocalMapping::new( + vec!["A.png".to_string()], + IndexSource::new(false, 1u32, 2u16), + LocalMappingForward::Latest, + ) + .unwrap(); + let local_mapping_gen = LocalMapping::try_from("\"A.png\"=>\"~1/2\"").unwrap(); + mapping_eq!(local_mapping, local_mapping_gen); + + let local_mapping = LocalMapping::new( + vec!["A.png".to_string()], + IndexSource::new(false, 1u32, 2u16), + LocalMappingForward::Version { version: 2u16 }, + ) + .unwrap(); + let local_mapping_gen = LocalMapping::try_from("\"A.png\"==\"~1/2\"").unwrap(); + mapping_eq!(local_mapping, local_mapping_gen); + + let local_mapping = LocalMapping::new( + vec!["A.png".to_string()], + IndexSource::new(false, 1u32, 2u16), + LocalMappingForward::Ref { + sheet_name: "ref".to_string(), + }, + ) + .unwrap(); + let local_mapping_gen = LocalMapping::try_from("\"A.png\"=>\"~1/2\"=>\"ref\"").unwrap(); + mapping_eq!(local_mapping, local_mapping_gen); + + let local_mapping = LocalMapping::new( + vec!["A.png".to_string()], + IndexSource::new(false, 1u32, 2u16), + LocalMappingForward::Version { version: 3u16 }, + ) + .unwrap(); + let local_mapping_gen = LocalMapping::try_from("\"A.png\"=>\"~1/2\"==\"3\"").unwrap(); + mapping_eq!(local_mapping, local_mapping_gen); + + // + + let local_mapping = LocalMapping::new( + vec!["A.png".to_string()], + IndexSource::new(true, 1u32, 2u16), + LocalMappingForward::Latest, + ) + .unwrap(); + let local_mapping_gen = LocalMapping::try_from("\"A.png\" =>\"1/2\"").unwrap(); + mapping_eq!(local_mapping, local_mapping_gen); + + let local_mapping = LocalMapping::new( + vec!["A.png".to_string()], + IndexSource::new(true, 1u32, 2u16), + LocalMappingForward::Version { version: 2u16 }, + ) + .unwrap(); + let local_mapping_gen = LocalMapping::try_from("\"A.png\"== \"1/2\"").unwrap(); + mapping_eq!(local_mapping, local_mapping_gen); + + let local_mapping = LocalMapping::new( + vec!["A.png".to_string()], + IndexSource::new(true, 1u32, 2u16), + LocalMappingForward::Ref { + sheet_name: "ref".to_string(), + }, + ) + .unwrap(); + let local_mapping_gen = + LocalMapping::try_from("\"A.png\" => \"1/2\" =>\"ref\"").unwrap(); + mapping_eq!(local_mapping, local_mapping_gen); + + let local_mapping = LocalMapping::new( + vec!["A.png".to_string()], + IndexSource::new(true, 1u32, 2u16), + LocalMappingForward::Version { version: 3u16 }, + ) + .unwrap(); + let local_mapping_gen = LocalMapping::try_from("\"A.png\"=> \"1/2\" ==\"3\"").unwrap(); + mapping_eq!(local_mapping, local_mapping_gen); + + let local_mapping = LocalMapping::new( + vec!["A.png".to_string()], + IndexSource::new(false, 1u32, 2u16), + LocalMappingForward::Latest, + ) + .unwrap(); + let local_mapping_gen = LocalMapping::try_from(" \"A.png\"=>\"~1/2\"").unwrap(); + mapping_eq!(local_mapping, local_mapping_gen); + + let local_mapping = LocalMapping::new( + vec!["A.png".to_string()], + IndexSource::new(false, 1u32, 2u16), + LocalMappingForward::Version { version: 2u16 }, + ) + .unwrap(); + let local_mapping_gen = LocalMapping::try_from("\"A.png\" ==\"~1/2\" ").unwrap(); + mapping_eq!(local_mapping, local_mapping_gen); + + let local_mapping = LocalMapping::new( + vec!["A.png".to_string()], + IndexSource::new(false, 1u32, 2u16), + LocalMappingForward::Ref { + sheet_name: "ref".to_string(), + }, + ) + .unwrap(); + let local_mapping_gen = + LocalMapping::try_from("\"A.png\"=> \"~1/2\"=> \"ref\"").unwrap(); + mapping_eq!(local_mapping, local_mapping_gen); + + let local_mapping = LocalMapping::new( + vec!["A.png".to_string()], + IndexSource::new(false, 1u32, 2u16), + LocalMappingForward::Version { version: 3u16 }, + ) + .unwrap(); + let local_mapping_gen = LocalMapping::try_from("\"A.png\"=> \"~1/2\" ==\"3\"").unwrap(); + mapping_eq!(local_mapping, local_mapping_gen); + } +} diff --git a/systems/sheet/src/sheet.rs b/systems/sheet/src/sheet.rs index 07d284b..e7a130d 100644 --- a/systems/sheet/src/sheet.rs +++ b/systems/sheet/src/sheet.rs @@ -13,7 +13,7 @@ use crate::{ index_source::IndexSource, mapping::{LocalMapping, LocalMappingForward, Mapping, MappingBuf}, sheet::{ - error::{ReadSheetDataError, SheetApplyError, SheetEditError}, + error::{ParseSheetError, ReadSheetDataError, SheetApplyError, SheetEditError}, reader::{read_mapping, read_sheet_data}, writer::convert_sheet_data_to_bytes, }, @@ -486,6 +486,42 @@ impl SheetData { } } +impl std::fmt::Display for SheetData { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let mut vec = self + .mappings() + .iter() + .cloned() + .collect::<Vec<LocalMapping>>(); + vec.sort(); + write!( + f, + "{}", + vec.iter() + .map(|m| m.to_string()) + .collect::<Vec<_>>() + .join("\n") + ) + } +} + +impl TryFrom<&str> for SheetData { + type Error = ParseSheetError; + + fn try_from(value: &str) -> Result<Self, Self::Error> { + let mut sheet = SheetData::empty().pack("temp"); + for line in value.split("\n") { + if line.trim().is_empty() { + continue; + } + let mapping = LocalMapping::try_from(line)?; + let _ = sheet.insert_mapping(mapping)?; + } + let _ = sheet.apply()?; + Ok(sheet.unpack()) + } +} + impl From<SheetData> for Vec<u8> { fn from(value: SheetData) -> Self { value.as_bytes() diff --git a/systems/sheet/src/sheet/error.rs b/systems/sheet/src/sheet/error.rs index 3e5661a..3142a6d 100644 --- a/systems/sheet/src/sheet/error.rs +++ b/systems/sheet/src/sheet/error.rs @@ -1,3 +1,5 @@ +use crate::mapping::error::ParseMappingError; + #[derive(Debug, thiserror::Error)] pub enum SheetEditError { #[error("Edit Failed: Node already exists: `{0}`")] @@ -17,6 +19,18 @@ pub enum SheetApplyError { } #[derive(Debug, thiserror::Error)] +pub enum ParseSheetError { + #[error("Parse mapping error: {0}")] + ParseMappingError(#[from] ParseMappingError), + + #[error("Sheet edit error: {0}")] + SheetEditError(#[from] SheetEditError), + + #[error("Sheet apply error: {0}")] + SheetApplyError(#[from] SheetApplyError), +} + +#[derive(Debug, thiserror::Error)] pub enum ReadSheetDataError { #[error("IO error: {0}")] IOErr(#[from] std::io::Error), diff --git a/systems/sheet/src/sheet/v1/constants.rs b/systems/sheet/src/sheet/v1/constants.rs index 69714bb..7073278 100644 --- a/systems/sheet/src/sheet/v1/constants.rs +++ b/systems/sheet/src/sheet/v1/constants.rs @@ -44,12 +44,16 @@ pub const MAPPING_BUCKET_MIN_SIZE: usize = 0 + 2 // INDEX_OFFSET ; -// Index Table (6: 4 + 2) +// Index Table (10: 4 + 2 + 1 + 3) // // [INDEX_ID: u32] // [INDEX_VERSION: u16] +// [REMOTE_FLAG: u8] +// [RESERVED: u8; 3] pub const INDEX_ENTRY_SIZE: usize = 0 + 4 // INDEX_ID + 2 // INDEX_VERSION + + 1 // REMOTE_FLAG + + 3 // RESERVED ; diff --git a/systems/sheet/src/sheet/v1/reader.rs b/systems/sheet/src/sheet/v1/reader.rs index e23e91b..66d8914 100644 --- a/systems/sheet/src/sheet/v1/reader.rs +++ b/systems/sheet/src/sheet/v1/reader.rs @@ -257,8 +257,9 @@ fn read_index_table( let id = u32::from_le_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]); let ver = u16::from_le_bytes([data[pos + 4], data[pos + 5]]); + let remote = data[pos + 6] != 0; // 0 = local, non-zero = remote - sources.push(IndexSource::new(id, ver)); + sources.push(IndexSource::new(remote, id, ver)); pos += INDEX_ENTRY_SIZE; } @@ -510,17 +511,26 @@ mod tests { #[test] fn test_read_index_table() { let mut data = Vec::new(); + // First entry: local source data.extend_from_slice(&123u32.to_le_bytes()); data.extend_from_slice(&456u16.to_le_bytes()); + data.push(0); // remote flag (0 = local) + data.extend_from_slice(&[0u8; 3]); // reserved bytes + + // Second entry: remote source data.extend_from_slice(&789u32.to_le_bytes()); data.extend_from_slice(&1011u16.to_le_bytes()); + data.push(1); // remote flag (1 = remote) + data.extend_from_slice(&[0u8; 3]); // reserved bytes let sources = read_index_table(&data, 0, 2).unwrap(); assert_eq!(sources.len(), 2); assert_eq!(sources[0].id(), 123); assert_eq!(sources[0].version(), 456); + assert_eq!(sources[0].is_remote(), false); assert_eq!(sources[1].id(), 789); assert_eq!(sources[1].version(), 1011); + assert_eq!(sources[1].is_remote(), true); } #[test] @@ -544,7 +554,7 @@ mod tests { bucket_data.extend_from_slice(path2); // KEY_BYTES bucket_data.extend_from_slice(&1u32.to_le_bytes()); // INDEX_OFFSET - let index_sources = vec![IndexSource::new(1, 1), IndexSource::new(2, 1)]; + let index_sources = vec![IndexSource::new_local(1, 1), IndexSource::new_local(2, 1)]; let mappings = read_bucket_data(&bucket_data, &index_sources).unwrap(); assert_eq!(mappings.len(), 2); @@ -574,21 +584,21 @@ mod tests { // Add mappings that will go to different buckets let mapping1 = crate::mapping::LocalMapping::new( vec!["aaa".to_string(), "file1.txt".to_string()], - crate::index_source::IndexSource::new(1, 1), + crate::index_source::IndexSource::new_local(1, 1), crate::mapping::LocalMappingForward::Latest, ) .unwrap(); let mapping2 = crate::mapping::LocalMapping::new( vec!["mmm".to_string(), "file2.txt".to_string()], - crate::index_source::IndexSource::new(2, 2), + crate::index_source::IndexSource::new_local(2, 2), crate::mapping::LocalMappingForward::Latest, ) .unwrap(); let mapping3 = crate::mapping::LocalMapping::new( vec!["zzz".to_string(), "file3.txt".to_string()], - crate::index_source::IndexSource::new(3, 3), + crate::index_source::IndexSource::new_local(3, 3), crate::mapping::LocalMappingForward::Latest, ) .unwrap(); diff --git a/systems/sheet/src/sheet/v1/test.rs b/systems/sheet/src/sheet/v1/test.rs index dfba3c8..995bcf9 100644 --- a/systems/sheet/src/sheet/v1/test.rs +++ b/systems/sheet/src/sheet/v1/test.rs @@ -11,450 +11,616 @@ use crate::{ use std::collections::HashSet; use std::fs; -/// Test writing and re-reading sheet data -#[test] -fn test_sheet_data_roundtrip() { - // Create test data - let _sheet_data = SheetData::empty(); - - // Create some test mappings - let mapping1 = LocalMapping::new( - vec!["src".to_string(), "main.rs".to_string()], - IndexSource::new(1001, 1), - LocalMappingForward::Latest, - ) - .unwrap(); - - let mapping2 = LocalMapping::new( - vec!["docs".to_string(), "README.md".to_string()], - IndexSource::new(1002, 2), - LocalMappingForward::Ref { - sheet_name: "reference".to_string(), - }, - ) - .unwrap(); - - let mapping3 = LocalMapping::new( - vec![ - "assets".to_string(), - "images".to_string(), - "logo.png".to_string(), - ], - IndexSource::new(1003, 3), - LocalMappingForward::Version { version: 12345 }, - ) - .unwrap(); - - // Add mappings to SheetData - // Note: Since the mappings field of SheetData is private, we need to create SheetData in another way - // Here we directly create a new HashSet - let mut mappings = HashSet::new(); - mappings.insert(mapping1.clone()); - mappings.insert(mapping2.clone()); - mappings.insert(mapping3.clone()); - - let sheet_data = SheetData { mappings }; - - // Convert SheetData to bytes - let bytes = convert_sheet_data_to_bytes(sheet_data.clone()); - - // Verify byte data is not empty - assert!(!bytes.is_empty(), "Converted bytes should not be empty"); - - // Verify file header - assert_eq!(bytes[0], 1, "Sheet version should be 1"); - - // Re-read SheetData from bytes - let restored_sheet_data = - read_sheet_data(&bytes).expect("Failed to read sheet data from bytes"); - - // Verify mapping count - assert_eq!( - restored_sheet_data.mappings.len(), - sheet_data.mappings.len(), - "Restored sheet should have same number of mappings" - ); - - // Verify each mapping exists - for mapping in &sheet_data.mappings { - assert!( - restored_sheet_data.mappings.contains(mapping), - "Restored sheet should contain mapping: {:?}", - mapping +#[cfg(test)] +mod tests { + use super::*; + + /// Test writing and re-reading sheet data + #[test] + fn test_sheet_data_roundtrip() { + // Create test data + let _sheet_data = SheetData::empty(); + + // Create some test mappings + let mapping1 = LocalMapping::new( + vec!["src".to_string(), "main.rs".to_string()], + IndexSource::new_local(1001, 1), + LocalMappingForward::Latest, + ) + .unwrap(); + + let mapping2 = LocalMapping::new( + vec!["docs".to_string(), "README.md".to_string()], + IndexSource::new_local(1002, 2), + LocalMappingForward::Ref { + sheet_name: "reference".to_string(), + }, + ) + .unwrap(); + + let mapping3 = LocalMapping::new( + vec![ + "assets".to_string(), + "images".to_string(), + "logo.png".to_string(), + ], + IndexSource::new_local(1003, 3), + LocalMappingForward::Version { version: 12345 }, + ) + .unwrap(); + + // Add mappings to SheetData + // Note: Since the mappings field of SheetData is private, we need to create SheetData in another way + // Here we directly create a new HashSet + let mut mappings = HashSet::new(); + mappings.insert(mapping1.clone()); + mappings.insert(mapping2.clone()); + mappings.insert(mapping3.clone()); + + let sheet_data = SheetData { mappings }; + + // Convert SheetData to bytes + let bytes = convert_sheet_data_to_bytes(sheet_data.clone()); + + // Verify byte data is not empty + assert!(!bytes.is_empty(), "Converted bytes should not be empty"); + + // Verify file header + assert_eq!(bytes[0], 1, "Sheet version should be 1"); + + // Re-read SheetData from bytes + let restored_sheet_data = + read_sheet_data(&bytes).expect("Failed to read sheet data from bytes"); + + // Verify mapping count + assert_eq!( + restored_sheet_data.mappings.len(), + sheet_data.mappings.len(), + "Restored sheet should have same number of mappings" ); + + // Verify each mapping exists + for mapping in &sheet_data.mappings { + assert!( + restored_sheet_data.mappings.contains(mapping), + "Restored sheet should contain mapping: {:?}", + mapping + ); + } + + // Verify specific mapping content + for mapping in &restored_sheet_data.mappings { + // Find original mapping + let original_mapping = sheet_data.mappings.get(mapping.value()).unwrap(); + + // Verify path + assert_eq!( + mapping.value(), + original_mapping.value(), + "Path should match" + ); + + // Verify index source + assert_eq!( + mapping.index_source().id(), + original_mapping.index_source().id(), + "Index source ID should match" + ); + + assert_eq!( + mapping.index_source().version(), + original_mapping.index_source().version(), + "Index source version should match" + ); + + // Verify forward information + let (original_type, _, _) = original_mapping.forward().unpack(); + let (restored_type, _, _) = mapping.forward().unpack(); + assert_eq!(restored_type, original_type, "Forward type should match"); + } } - // Verify specific mapping content - for mapping in &restored_sheet_data.mappings { - // Find original mapping - let original_mapping = sheet_data.mappings.get(mapping.value()).unwrap(); + /// Test reading and writing empty sheet data + #[test] + fn test_empty_sheet_roundtrip() { + // Create empty SheetData + let sheet_data = SheetData::empty(); + + // Convert to bytes + let bytes = convert_sheet_data_to_bytes(sheet_data.clone()); + + // Verify file header + assert_eq!(bytes.len(), 15, "Empty sheet should have header size only"); + assert_eq!(bytes[0], 1, "Sheet version should be 1"); - // Verify path + // Verify offsets - For empty sheet, mapping data offset and index table offset should be the same + let mapping_data_offset = + u32::from_le_bytes([bytes[7], bytes[8], bytes[9], bytes[10]]) as usize; + let index_table_offset = + u32::from_le_bytes([bytes[11], bytes[12], bytes[13], bytes[14]]) as usize; assert_eq!( - mapping.value(), - original_mapping.value(), - "Path should match" + mapping_data_offset, index_table_offset, + "For empty sheet, both offsets should be the same" ); - - // Verify index source assert_eq!( - mapping.index_source().id(), - original_mapping.index_source().id(), - "Index source ID should match" + mapping_data_offset, HEADER_SIZE, + "Offsets should point to end of header" ); - assert_eq!( - mapping.index_source().version(), - original_mapping.index_source().version(), - "Index source version should match" + // Mapping count should be 0 + let mapping_count = u32::from_le_bytes([bytes[1], bytes[2], bytes[3], bytes[4]]); + assert_eq!(mapping_count, 0, "Mapping count should be 0"); + + // Index source count should be 0 + let index_count = u16::from_le_bytes([bytes[5], bytes[6]]); + assert_eq!(index_count, 0, "Index count should be 0"); + + // Re-read + let restored_sheet_data = read_sheet_data(&bytes).expect("Failed to read empty sheet data"); + + // Verify it's empty + assert!( + restored_sheet_data.mappings.is_empty(), + "Restored empty sheet should have no mappings" ); + } + + /// Test reading and writing a single mapping + #[test] + fn test_single_mapping_roundtrip() { + // Create a single mapping + let mapping = LocalMapping::new( + vec!["test.txt".to_string()], + IndexSource::new_local(999, 42), + LocalMappingForward::Latest, + ) + .unwrap(); + + let mut mappings = HashSet::new(); + mappings.insert(mapping.clone()); - // Verify forward information - let (original_type, _, _) = original_mapping.forward().unpack(); - let (restored_type, _, _) = mapping.forward().unpack(); - assert_eq!(restored_type, original_type, "Forward type should match"); + let sheet_data = SheetData { mappings }; + + // Convert to bytes + let bytes = convert_sheet_data_to_bytes(sheet_data.clone()); + + // Re-read + let restored_sheet_data = read_sheet_data(&bytes).expect("Failed to read sheet data"); + + // Verify + assert_eq!(restored_sheet_data.mappings.len(), 1); + let restored_mapping = restored_sheet_data.mappings.iter().next().unwrap(); + + assert_eq!(restored_mapping.value(), &["test.txt".to_string()]); + assert_eq!(restored_mapping.index_source().id(), 999); + assert_eq!(restored_mapping.index_source().version(), 42); + + let (forward_type, _, _) = restored_mapping.forward().unpack(); + assert_eq!(forward_type, 0); // Latest type id is 0 } -} -/// Test reading and writing empty sheet data -#[test] -fn test_empty_sheet_roundtrip() { - // Create empty SheetData - let sheet_data = SheetData::empty(); - - // Convert to bytes - let bytes = convert_sheet_data_to_bytes(sheet_data.clone()); - - // Verify file header - assert_eq!(bytes.len(), 15, "Empty sheet should have header size only"); - assert_eq!(bytes[0], 1, "Sheet version should be 1"); - - // Verify offsets - For empty sheet, mapping data offset and index table offset should be the same - let mapping_data_offset = - u32::from_le_bytes([bytes[7], bytes[8], bytes[9], bytes[10]]) as usize; - let index_table_offset = - u32::from_le_bytes([bytes[11], bytes[12], bytes[13], bytes[14]]) as usize; - assert_eq!( - mapping_data_offset, index_table_offset, - "For empty sheet, both offsets should be the same" - ); - assert_eq!( - mapping_data_offset, HEADER_SIZE, - "Offsets should point to end of header" - ); - - // Mapping count should be 0 - let mapping_count = u32::from_le_bytes([bytes[1], bytes[2], bytes[3], bytes[4]]); - assert_eq!(mapping_count, 0, "Mapping count should be 0"); - - // Index source count should be 0 - let index_count = u16::from_le_bytes([bytes[5], bytes[6]]); - assert_eq!(index_count, 0, "Index count should be 0"); - - // Re-read - let restored_sheet_data = read_sheet_data(&bytes).expect("Failed to read empty sheet data"); - - // Verify it's empty - assert!( - restored_sheet_data.mappings.is_empty(), - "Restored empty sheet should have no mappings" - ); -} + /// Test file system read/write + #[test] + fn test_file_system_roundtrip() { + // Create test data + let mapping1 = LocalMapping::new( + vec!["file0.txt".to_string()], + IndexSource::new_local(1, 1), + LocalMappingForward::Latest, + ) + .unwrap(); + + let mapping2 = LocalMapping::new( + vec!["dir1".to_string(), "file1.txt".to_string()], + IndexSource::new_local(2, 2), + LocalMappingForward::Ref { + sheet_name: "other".to_string(), + }, + ) + .unwrap(); + + let mapping3 = LocalMapping::new( + vec!["dir2".to_string(), "file2.txt".to_string()], + IndexSource::new_local(3, 3), + LocalMappingForward::Version { version: 35 }, + ) + .unwrap(); -/// Test reading and writing a single mapping -#[test] -fn test_single_mapping_roundtrip() { - // Create a single mapping - let mapping = LocalMapping::new( - vec!["test.txt".to_string()], - IndexSource::new(999, 42), - LocalMappingForward::Latest, - ) - .unwrap(); + let mut mappings = HashSet::new(); + mappings.insert(mapping1.clone()); + mappings.insert(mapping2.clone()); + mappings.insert(mapping3.clone()); - let mut mappings = HashSet::new(); - mappings.insert(mapping.clone()); + let sheet_data = SheetData { mappings }; - let sheet_data = SheetData { mappings }; + // Convert to bytes + let bytes = convert_sheet_data_to_bytes(sheet_data.clone()); - // Convert to bytes - let bytes = convert_sheet_data_to_bytes(sheet_data.clone()); + // Write to file + let test_file_path = ".temp/test.sheet"; + let test_file_path_hex = ".temp/test_hex.txt"; - // Re-read - let restored_sheet_data = read_sheet_data(&bytes).expect("Failed to read sheet data"); + // Ensure directory exists + if let Some(parent) = std::path::Path::new(test_file_path).parent() { + fs::create_dir_all(parent).expect("Failed to create test directory"); + } - // Verify - assert_eq!(restored_sheet_data.mappings.len(), 1); - let restored_mapping = restored_sheet_data.mappings.iter().next().unwrap(); + fs::write(test_file_path, &bytes).expect("Failed to write test file"); + fs::write(test_file_path_hex, hex_display_slice(&bytes)) + .expect("Failed to write test file"); - assert_eq!(restored_mapping.value(), &["test.txt".to_string()]); - assert_eq!(restored_mapping.index_source().id(), 999); - assert_eq!(restored_mapping.index_source().version(), 42); + // Read file + let file_bytes = fs::read(test_file_path).expect("Failed to read test file"); - let (forward_type, _, _) = restored_mapping.forward().unpack(); - assert_eq!(forward_type, 0); // Latest type id is 0 -} + // Verify file content matches original bytes + assert_eq!( + file_bytes, bytes, + "File content should match original bytes" + ); + + // Re-read SheetData from file bytes + let restored_from_file = + read_sheet_data(&file_bytes).expect("Failed to read from file bytes"); -/// Test file system read/write -#[test] -fn test_file_system_roundtrip() { - // Create test data - let mapping1 = LocalMapping::new( - vec!["file0.txt".to_string()], - IndexSource::new(1, 1), - LocalMappingForward::Latest, - ) - .unwrap(); - - let mapping2 = LocalMapping::new( - vec!["dir1".to_string(), "file1.txt".to_string()], - IndexSource::new(2, 2), - LocalMappingForward::Ref { - sheet_name: "other".to_string(), - }, - ) - .unwrap(); - - let mapping3 = LocalMapping::new( - vec!["dir2".to_string(), "file2.txt".to_string()], - IndexSource::new(3, 3), - LocalMappingForward::Version { version: 35 }, - ) - .unwrap(); - - let mut mappings = HashSet::new(); - mappings.insert(mapping1.clone()); - mappings.insert(mapping2.clone()); - mappings.insert(mapping3.clone()); - - let sheet_data = SheetData { mappings }; - - // Convert to bytes - let bytes = convert_sheet_data_to_bytes(sheet_data.clone()); - - // Write to file - let test_file_path = ".temp/test.sheet"; - let test_file_path_hex = ".temp/test_hex.txt"; - - // Ensure directory exists - if let Some(parent) = std::path::Path::new(test_file_path).parent() { - fs::create_dir_all(parent).expect("Failed to create test directory"); + // Use SheetData's Eq trait for direct comparison + assert_eq!( + restored_from_file, sheet_data, + "Restored sheet data should be equal to original" + ); + + // Verify mappings in SheetData read from file + // Check if each original mapping can be found in restored data + for original_mapping in &sheet_data.mappings { + let found = restored_from_file + .mappings + .iter() + .any(|m| m == original_mapping); + assert!( + found, + "Original mapping {:?} should be present in restored sheet data", + original_mapping + ); + } + + // Also check if each mapping in restored data can be found in original data + for restored_mapping in &restored_from_file.mappings { + let found = sheet_data.mappings.iter().any(|m| m == restored_mapping); + assert!( + found, + "Restored mapping {:?} should be present in original sheet data", + restored_mapping + ); + } + + // Test file remains in .temp/test.sheet for subsequent inspection + // Note: Need to manually clean up .temp directory before next test run } - fs::write(test_file_path, &bytes).expect("Failed to write test file"); - fs::write(test_file_path_hex, hex_display_slice(&bytes)).expect("Failed to write test file"); + /// Test reading and writing different forward types + #[test] + fn test_different_forward_types() { + // Test Latest type + let mapping_latest = LocalMapping::new( + vec!["latest.txt".to_string()], + IndexSource::new_local(1, 1), + LocalMappingForward::Latest, + ) + .unwrap(); + + // Test Ref type + let mapping_ref = LocalMapping::new( + vec!["ref.txt".to_string()], + IndexSource::new_local(2, 2), + LocalMappingForward::Ref { + sheet_name: "reference_sheet".to_string(), + }, + ) + .unwrap(); + + // Test Version type + let mapping_version = LocalMapping::new( + vec!["version.txt".to_string()], + IndexSource::new_local(3, 3), + LocalMappingForward::Version { version: 54321 }, + ) + .unwrap(); - // Read file - let file_bytes = fs::read(test_file_path).expect("Failed to read test file"); + let mut mappings = HashSet::new(); + mappings.insert(mapping_latest.clone()); + mappings.insert(mapping_ref.clone()); + mappings.insert(mapping_version.clone()); - // Verify file content matches original bytes - assert_eq!( - file_bytes, bytes, - "File content should match original bytes" - ); + let sheet_data = SheetData { mappings }; - // Re-read SheetData from file bytes - let restored_from_file = read_sheet_data(&file_bytes).expect("Failed to read from file bytes"); + // Convert to bytes and re-read + let bytes = convert_sheet_data_to_bytes(sheet_data.clone()); + let restored_sheet_data = read_sheet_data(&bytes).expect("Failed to read sheet data"); - // Use SheetData's Eq trait for direct comparison - assert_eq!( - restored_from_file, sheet_data, - "Restored sheet data should be equal to original" - ); + // Verify all mappings exist + assert_eq!(restored_sheet_data.mappings.len(), 3); - // Verify mappings in SheetData read from file - // Check if each original mapping can be found in restored data - for original_mapping in &sheet_data.mappings { - let found = restored_from_file + // Verify Latest type + let restored_latest = restored_sheet_data .mappings - .iter() - .any(|m| m == original_mapping); - assert!( - found, - "Original mapping {:?} should be present in restored sheet data", - original_mapping - ); + .get(&vec!["latest.txt".to_string()]) + .unwrap(); + let (latest_type, latest_len, _) = restored_latest.forward().unpack(); + assert_eq!(latest_type, 0); + assert_eq!(latest_len, 0); + + // Verify Ref type + let restored_ref = restored_sheet_data + .mappings + .get(&vec!["ref.txt".to_string()]) + .unwrap(); + let (ref_type, ref_len, ref_bytes) = restored_ref.forward().unpack(); + assert_eq!(ref_type, 1); + assert_eq!(ref_len as usize, "reference_sheet".len()); + assert_eq!(String::from_utf8(ref_bytes).unwrap(), "reference_sheet"); + + // Verify Version type + let restored_version = restored_sheet_data + .mappings + .get(&vec!["version.txt".to_string()]) + .unwrap(); + let (version_type, version_len, version_bytes) = restored_version.forward().unpack(); + assert_eq!(version_type, 2); + assert_eq!(version_len, 2); // u16 is 2 bytes + assert_eq!(u16::from_be_bytes(version_bytes.try_into().unwrap()), 54321); } - // Also check if each mapping in restored data can be found in original data - for restored_mapping in &restored_from_file.mappings { - let found = sheet_data.mappings.iter().any(|m| m == restored_mapping); - assert!( - found, - "Restored mapping {:?} should be present in original sheet data", - restored_mapping - ); + /// Test duplicate index source optimization + #[test] + fn test_duplicate_index_source_optimization() { + // Create multiple mappings sharing the same index source + let shared_source = IndexSource::new_local(777, 88); + + let mapping1 = LocalMapping::new( + vec!["file1.txt".to_string()], + shared_source, + LocalMappingForward::Latest, + ) + .unwrap(); + + let mapping2 = LocalMapping::new( + vec!["file2.txt".to_string()], + shared_source, + LocalMappingForward::Latest, + ) + .unwrap(); + + let mapping3 = LocalMapping::new( + vec!["file3.txt".to_string()], + shared_source, + LocalMappingForward::Latest, + ) + .unwrap(); + + let mut mappings = HashSet::new(); + mappings.insert(mapping1); + mappings.insert(mapping2); + mappings.insert(mapping3); + + let sheet_data = SheetData { mappings }; + + // Convert to bytes + let bytes = convert_sheet_data_to_bytes(sheet_data.clone()); + + // Verify index table should have only one entry + let index_count = u32::from_le_bytes([bytes[3], bytes[4], bytes[5], bytes[6]]); + assert_eq!(index_count, 1, "Should have only one unique index source"); + + // Re-read and verify + let restored_sheet_data = read_sheet_data(&bytes).expect("Failed to read sheet data"); + assert_eq!(restored_sheet_data.mappings.len(), 3); + + // Verify all mappings use the same index source + for mapping in &restored_sheet_data.mappings { + assert_eq!(mapping.index_source().id(), 777); + assert_eq!(mapping.index_source().version(), 88); + } } - // Test file remains in .temp/test.sheet for subsequent inspection - // Note: Need to manually clean up .temp directory before next test run -} + /// Test path serialization and deserialization + #[test] + fn test_path_serialization_deserialization() { + // Test various paths + let test_cases = vec![ + vec!["single".to_string()], + vec!["dir".to_string(), "file.txt".to_string()], + vec![ + "a".to_string(), + "b".to_string(), + "c".to_string(), + "d.txt".to_string(), + ], + vec!["with spaces".to_string(), "file name.txt".to_string()], + vec!["unicode".to_string(), "文件.txt".to_string()], + ]; + + for path in test_cases { + let mapping = LocalMapping::new( + path.clone(), + IndexSource::new_local(1, 1), + LocalMappingForward::Latest, + ) + .unwrap(); + + let mut mappings = HashSet::new(); + mappings.insert(mapping); + + let sheet_data = SheetData { mappings }; + + // Convert to bytes and re-read + let bytes = convert_sheet_data_to_bytes(sheet_data.clone()); + let restored_sheet_data = read_sheet_data(&bytes).expect("Failed to read sheet data"); + + // Verify path + let restored_mapping = restored_sheet_data.mappings.iter().next().unwrap(); + assert_eq!( + restored_mapping.value(), + &path, + "Path should be preserved after roundtrip" + ); + } + } -/// Test reading and writing different forward types -#[test] -fn test_different_forward_types() { - // Test Latest type - let mapping_latest = LocalMapping::new( - vec!["latest.txt".to_string()], - IndexSource::new(1, 1), - LocalMappingForward::Latest, - ) - .unwrap(); - - // Test Ref type - let mapping_ref = LocalMapping::new( - vec!["ref.txt".to_string()], - IndexSource::new(2, 2), - LocalMappingForward::Ref { - sheet_name: "reference_sheet".to_string(), - }, - ) - .unwrap(); - - // Test Version type - let mapping_version = LocalMapping::new( - vec!["version.txt".to_string()], - IndexSource::new(3, 3), - LocalMappingForward::Version { version: 54321 }, - ) - .unwrap(); - - let mut mappings = HashSet::new(); - mappings.insert(mapping_latest.clone()); - mappings.insert(mapping_ref.clone()); - mappings.insert(mapping_version.clone()); - - let sheet_data = SheetData { mappings }; - - // Convert to bytes and re-read - let bytes = convert_sheet_data_to_bytes(sheet_data.clone()); - let restored_sheet_data = read_sheet_data(&bytes).expect("Failed to read sheet data"); - - // Verify all mappings exist - assert_eq!(restored_sheet_data.mappings.len(), 3); - - // Verify Latest type - let restored_latest = restored_sheet_data - .mappings - .get(&vec!["latest.txt".to_string()]) + /// Test mixed local and remote index sources + #[test] + fn test_mixed_local_remote_index_sources() { + // Create mappings with mixed local and remote index sources + let mapping_local1 = LocalMapping::new( + vec!["local1.txt".to_string()], + IndexSource::new_local(100, 1), + LocalMappingForward::Latest, + ) .unwrap(); - let (latest_type, latest_len, _) = restored_latest.forward().unpack(); - assert_eq!(latest_type, 0); - assert_eq!(latest_len, 0); - - // Verify Ref type - let restored_ref = restored_sheet_data - .mappings - .get(&vec!["ref.txt".to_string()]) + + let mapping_local2 = LocalMapping::new( + vec!["local2.txt".to_string()], + IndexSource::new_local(200, 2), + LocalMappingForward::Ref { + sheet_name: "ref_sheet".to_string(), + }, + ) .unwrap(); - let (ref_type, ref_len, ref_bytes) = restored_ref.forward().unpack(); - assert_eq!(ref_type, 1); - assert_eq!(ref_len as usize, "reference_sheet".len()); - assert_eq!(String::from_utf8(ref_bytes).unwrap(), "reference_sheet"); - - // Verify Version type - let restored_version = restored_sheet_data - .mappings - .get(&vec!["version.txt".to_string()]) + + let mapping_remote1 = LocalMapping::new( + vec!["remote1.txt".to_string()], + IndexSource::new_remote(300, 3), + LocalMappingForward::Latest, + ) .unwrap(); - let (version_type, version_len, version_bytes) = restored_version.forward().unpack(); - assert_eq!(version_type, 2); - assert_eq!(version_len, 2); // u16 is 2 bytes - assert_eq!(u16::from_be_bytes(version_bytes.try_into().unwrap()), 54321); -} -/// Test duplicate index source optimization -#[test] -fn test_duplicate_index_source_optimization() { - // Create multiple mappings sharing the same index source - let shared_source = IndexSource::new(777, 88); - - let mapping1 = LocalMapping::new( - vec!["file1.txt".to_string()], - shared_source, - LocalMappingForward::Latest, - ) - .unwrap(); - - let mapping2 = LocalMapping::new( - vec!["file2.txt".to_string()], - shared_source, - LocalMappingForward::Latest, - ) - .unwrap(); - - let mapping3 = LocalMapping::new( - vec!["file3.txt".to_string()], - shared_source, - LocalMappingForward::Latest, - ) - .unwrap(); - - let mut mappings = HashSet::new(); - mappings.insert(mapping1); - mappings.insert(mapping2); - mappings.insert(mapping3); - - let sheet_data = SheetData { mappings }; - - // Convert to bytes - let bytes = convert_sheet_data_to_bytes(sheet_data.clone()); - - // Verify index table should have only one entry - let index_count = u32::from_le_bytes([bytes[3], bytes[4], bytes[5], bytes[6]]); - assert_eq!(index_count, 1, "Should have only one unique index source"); - - // Re-read and verify - let restored_sheet_data = read_sheet_data(&bytes).expect("Failed to read sheet data"); - assert_eq!(restored_sheet_data.mappings.len(), 3); - - // Verify all mappings use the same index source - for mapping in &restored_sheet_data.mappings { - assert_eq!(mapping.index_source().id(), 777); - assert_eq!(mapping.index_source().version(), 88); - } -} + let mapping_remote2 = LocalMapping::new( + vec!["remote2.txt".to_string()], + IndexSource::new_remote(400, 4), + LocalMappingForward::Version { version: 12345 }, + ) + .unwrap(); -/// Test path serialization and deserialization -#[test] -fn test_path_serialization_deserialization() { - // Test various paths - let test_cases = vec![ - vec!["single".to_string()], - vec!["dir".to_string(), "file.txt".to_string()], - vec![ - "a".to_string(), - "b".to_string(), - "c".to_string(), - "d.txt".to_string(), - ], - vec!["with spaces".to_string(), "file name.txt".to_string()], - vec!["unicode".to_string(), "文件.txt".to_string()], - ]; - - for path in test_cases { - let mapping = LocalMapping::new( - path.clone(), - IndexSource::new(1, 1), + // Test same ID but different remote status + let mapping_same_id_local = LocalMapping::new( + vec!["same_id_local.txt".to_string()], + IndexSource::new_local(500, 5), + LocalMappingForward::Latest, + ) + .unwrap(); + + let mapping_same_id_remote = LocalMapping::new( + vec!["same_id_remote.txt".to_string()], + IndexSource::new_remote(500, 5), LocalMappingForward::Latest, ) .unwrap(); let mut mappings = HashSet::new(); - mappings.insert(mapping); + mappings.insert(mapping_local1.clone()); + mappings.insert(mapping_local2.clone()); + mappings.insert(mapping_remote1.clone()); + mappings.insert(mapping_remote2.clone()); + mappings.insert(mapping_same_id_local.clone()); + mappings.insert(mapping_same_id_remote.clone()); let sheet_data = SheetData { mappings }; - // Convert to bytes and re-read + // Convert to bytes let bytes = convert_sheet_data_to_bytes(sheet_data.clone()); + + // Re-read from bytes let restored_sheet_data = read_sheet_data(&bytes).expect("Failed to read sheet data"); - // Verify path - let restored_mapping = restored_sheet_data.mappings.iter().next().unwrap(); + // Verify all mappings exist + assert_eq!( + restored_sheet_data.mappings.len(), + 6, + "Should have all 6 mappings" + ); + + // Verify local mappings + let restored_local1 = restored_sheet_data + .mappings + .get(&vec!["local1.txt".to_string()]) + .unwrap(); + assert_eq!(restored_local1.index_source().id(), 100); + assert_eq!(restored_local1.index_source().version(), 1); + assert_eq!(restored_local1.index_source().is_remote(), false); + + let restored_local2 = restored_sheet_data + .mappings + .get(&vec!["local2.txt".to_string()]) + .unwrap(); + assert_eq!(restored_local2.index_source().id(), 200); + assert_eq!(restored_local2.index_source().version(), 2); + assert_eq!(restored_local2.index_source().is_remote(), false); + + // Verify remote mappings + let restored_remote1 = restored_sheet_data + .mappings + .get(&vec!["remote1.txt".to_string()]) + .unwrap(); + assert_eq!(restored_remote1.index_source().id(), 300); + assert_eq!(restored_remote1.index_source().version(), 3); + assert_eq!(restored_remote1.index_source().is_remote(), true); + + let restored_remote2 = restored_sheet_data + .mappings + .get(&vec!["remote2.txt".to_string()]) + .unwrap(); + assert_eq!(restored_remote2.index_source().id(), 400); + assert_eq!(restored_remote2.index_source().version(), 4); + assert_eq!(restored_remote2.index_source().is_remote(), true); + + // Verify same ID but different remote status are treated as different sources + let restored_same_id_local = restored_sheet_data + .mappings + .get(&vec!["same_id_local.txt".to_string()]) + .unwrap(); + assert_eq!(restored_same_id_local.index_source().id(), 500); + assert_eq!(restored_same_id_local.index_source().version(), 5); + assert_eq!(restored_same_id_local.index_source().is_remote(), false); + + let restored_same_id_remote = restored_sheet_data + .mappings + .get(&vec!["same_id_remote.txt".to_string()]) + .unwrap(); + assert_eq!(restored_same_id_remote.index_source().id(), 500); + assert_eq!(restored_same_id_remote.index_source().version(), 5); + assert_eq!(restored_same_id_remote.index_source().is_remote(), true); + + // Verify that local and remote with same ID are different + assert_ne!( + restored_same_id_local.index_source(), + restored_same_id_remote.index_source() + ); + + // Verify forward types are preserved + let (forward_type_local2, forward_len_local2, forward_bytes_local2) = + restored_local2.forward().unpack(); + assert_eq!(forward_type_local2, 1); // Ref type + assert_eq!(forward_len_local2 as usize, "ref_sheet".len()); + assert_eq!( + String::from_utf8(forward_bytes_local2).unwrap(), + "ref_sheet" + ); + + let (forward_type_remote2, forward_len_remote2, forward_bytes_remote2) = + restored_remote2.forward().unpack(); + assert_eq!(forward_type_remote2, 2); // Version type + assert_eq!(forward_len_remote2, 2); // u16 is 2 bytes assert_eq!( - restored_mapping.value(), - &path, - "Path should be preserved after roundtrip" + u16::from_be_bytes(forward_bytes_remote2.try_into().unwrap()), + 12345 ); + + // Test duplicate index source optimization with remote flag + // Should have 6 unique index sources (local1, local2, remote1, remote2, local500, remote500) + let index_count = u32::from_le_bytes([bytes[3], bytes[4], bytes[5], bytes[6]]); + assert_eq!( + index_count, 6, + "Should have 6 unique index sources (including remote flag)" + ); + + println!("Mixed local/remote test passed successfully!"); } } diff --git a/systems/sheet/src/sheet/v1/writer.rs b/systems/sheet/src/sheet/v1/writer.rs index 00f0987..e310029 100644 --- a/systems/sheet/src/sheet/v1/writer.rs +++ b/systems/sheet/src/sheet/v1/writer.rs @@ -17,11 +17,15 @@ pub fn convert_sheet_data_to_bytes(sheet_data: SheetData) -> Vec<u8> { for mapping in &mappings { let source = mapping.index_source(); - let key = (source.id(), source.version()); + let key = (source.is_remote(), source.id(), source.version()); if !source_to_offset.contains_key(&key) { let offset = index_sources.len() as u32; source_to_offset.insert(key, offset); - index_sources.push(IndexSource::new(source.id(), source.version())); + index_sources.push(IndexSource::new( + source.is_remote(), + source.id(), + source.version(), + )); } } @@ -86,6 +90,8 @@ pub fn convert_sheet_data_to_bytes(sheet_data: SheetData) -> Vec<u8> { for source in &index_sources { result.extend_from_slice(&source.id().to_le_bytes()); // Index ID (4 bytes) result.extend_from_slice(&source.version().to_le_bytes()); // Index version (2 bytes) + result.push(if source.is_remote() { 1 } else { 0 }); // Remote flag (1 byte) + result.extend_from_slice(&[0u8; 3]); // Reserved bytes (3 bytes) } // 8. Bucket data @@ -110,7 +116,7 @@ pub fn calculate_path_hash(path: &[String]) -> u32 { fn write_mapping_bucket( result: &mut Vec<u8>, mapping: &LocalMapping, - source_to_offset: &HashMap<(u32, u16), u32>, + source_to_offset: &HashMap<(bool, u32, u16), u32>, ) { // Serialize path let path_bytes = serialize_path(mapping.value()); @@ -121,7 +127,7 @@ fn write_mapping_bucket( // Get index offset let source = mapping.index_source(); - let key = (source.id(), source.version()); + let key = (source.is_remote(), source.id(), source.version()); let index_offset = source_to_offset.get(&key).unwrap(); // Write mapping bucket entry @@ -196,7 +202,7 @@ mod tests { fn test_calculate_mapping_bucket_size() { let mapping = LocalMapping::new( vec!["test".to_string(), "file.txt".to_string()], - IndexSource::new(1, 1), + IndexSource::new_local(1, 1), LocalMappingForward::Latest, ) .unwrap(); @@ -228,7 +234,7 @@ mod tests { let mut sheet_data = SheetData::empty(); let mapping = LocalMapping::new( vec!["dir".to_string(), "file.txt".to_string()], - IndexSource::new(1, 1), + IndexSource::new_local(1, 1), LocalMappingForward::Latest, ) .unwrap(); |
