From 81c9f47f5d9517ab273a34aeea4b6e40f45aac36 Mon Sep 17 00:00:00 2001 From: 魏曹先生 <1992414357@qq.com> Date: Fri, 26 Sep 2025 14:18:53 +0800 Subject: refactor: Update sheet input handling and fix tests - Modify Sheet::add_input to accept InputPackage instead of separate parameters - Use output_mappings method to generate InputPackage in tests - Update test assertions to match new path transformation logic - Fix mapping count assertions after adding multiple mappings - Clean up string_proc module structure --- crates/utils/string_proc/src/format_processer.rs | 132 +++++++++++++++++++++ crates/utils/string_proc/src/lib.rs | 9 +- crates/utils/string_proc/src/macros.rs | 32 ++--- crates/utils/string_proc/src/simple_processer.rs | 15 +++ crates/utils/string_proc/src/string_processer.rs | 130 -------------------- crates/vcs/src/data/sheet.rs | 116 ++++++++++++++++-- crates/vcs/src/data/vault.rs | 4 +- ...st_sheet_creation_management_and_persistence.rs | 53 ++++++--- 8 files changed, 309 insertions(+), 182 deletions(-) create mode 100644 crates/utils/string_proc/src/format_processer.rs create mode 100644 crates/utils/string_proc/src/simple_processer.rs delete mode 100644 crates/utils/string_proc/src/string_processer.rs diff --git a/crates/utils/string_proc/src/format_processer.rs b/crates/utils/string_proc/src/format_processer.rs new file mode 100644 index 0000000..8d0a770 --- /dev/null +++ b/crates/utils/string_proc/src/format_processer.rs @@ -0,0 +1,132 @@ +pub struct FormatProcesser { + content: Vec, +} + +impl From for FormatProcesser { + fn from(value: String) -> Self { + Self { + content: Self::process_string(value), + } + } +} + +impl From<&str> for FormatProcesser { + fn from(value: &str) -> Self { + Self { + content: Self::process_string(value.to_string()), + } + } +} + +impl FormatProcesser { + /// Process the string into an intermediate format + fn process_string(input: String) -> Vec { + let mut result = String::new(); + let mut prev_space = false; + + for c in input.chars() { + match c { + 'a'..='z' | 'A'..='Z' | '0'..='9' => { + result.push(c); + prev_space = false; + } + '_' | ',' | '.' | '-' | ' ' => { + if !prev_space { + result.push(' '); + prev_space = true; + } + } + _ => {} + } + } + + let mut processed = String::new(); + let mut chars = result.chars().peekable(); + + while let Some(c) = chars.next() { + processed.push(c); + if let Some(&next) = chars.peek() + && c.is_lowercase() + && next.is_uppercase() + { + processed.push(' '); + } + } + + processed + .to_lowercase() + .split_whitespace() + .map(|s| s.to_string()) + .collect() + } + + /// Convert to camelCase format (brewCoffee) + pub fn to_camel_case(&self) -> String { + let mut result = String::new(); + for (i, word) in self.content.iter().enumerate() { + if i == 0 { + result.push_str(&word.to_lowercase()); + } else { + let mut chars = word.chars(); + if let Some(first) = chars.next() { + result.push_str(&first.to_uppercase().collect::()); + result.push_str(&chars.collect::().to_lowercase()); + } + } + } + result + } + + /// Convert to PascalCase format (BrewCoffee) + pub fn to_pascal_case(&self) -> String { + let mut result = String::new(); + for word in &self.content { + let mut chars = word.chars(); + if let Some(first) = chars.next() { + result.push_str(&first.to_uppercase().collect::()); + result.push_str(&chars.collect::().to_lowercase()); + } + } + result + } + + /// Convert to kebab-case format (brew-coffee) + pub fn to_kebab_case(&self) -> String { + self.content.join("-").to_lowercase() + } + + /// Convert to snake_case format (brew_coffee) + pub fn to_snake_case(&self) -> String { + self.content.join("_").to_lowercase() + } + + /// Convert to dot.case format (brew.coffee) + pub fn to_dot_case(&self) -> String { + self.content.join(".").to_lowercase() + } + + /// Convert to Title Case format (Brew Coffee) + pub fn to_title_case(&self) -> String { + let mut result = String::new(); + for word in &self.content { + let mut chars = word.chars(); + if let Some(first) = chars.next() { + result.push_str(&first.to_uppercase().collect::()); + result.push_str(&chars.collect::().to_lowercase()); + } + result.push(' '); + } + result.pop(); + result + } + + /// Convert to lower case format (brew coffee) + pub fn to_lower_case(&self) -> String { + self.content.join(" ").to_lowercase() + } + + /// Convert to UPPER CASE format (BREW COFFEE) + pub fn to_upper_case(&self) -> String { + self.content.join(" ").to_uppercase() + } +} diff --git a/crates/utils/string_proc/src/lib.rs b/crates/utils/string_proc/src/lib.rs index 1f24028..e5559b9 100644 --- a/crates/utils/string_proc/src/lib.rs +++ b/crates/utils/string_proc/src/lib.rs @@ -1,9 +1,10 @@ +pub mod format_processer; pub mod macros; -pub mod string_processer; +pub mod simple_processer; #[cfg(test)] mod tests { - use crate::string_processer::StringProcesser; + use crate::format_processer::FormatProcesser; #[test] fn test_processer() { @@ -22,7 +23,7 @@ mod tests { ]; for (input, expected) in test_cases { - let processor = StringProcesser::from(input); + let processor = FormatProcesser::from(input); assert_eq!( processor.to_camel_case(), expected, @@ -34,7 +35,7 @@ mod tests { #[test] fn test_conversions() { - let processor = StringProcesser::from("brewCoffee"); + let processor = FormatProcesser::from("brewCoffee"); assert_eq!(processor.to_upper_case(), "BREW COFFEE"); assert_eq!(processor.to_lower_case(), "brew coffee"); diff --git a/crates/utils/string_proc/src/macros.rs b/crates/utils/string_proc/src/macros.rs index 9d85338..135268e 100644 --- a/crates/utils/string_proc/src/macros.rs +++ b/crates/utils/string_proc/src/macros.rs @@ -1,63 +1,63 @@ #[macro_export] macro_rules! camel_case { ($input:expr) => {{ - use string_proc::string_processer::StringProcesser; - StringProcesser::from($input).to_camel_case() + use string_proc::format_processer::FormatProcesser; + FormatProcesser::from($input).to_camel_case() }}; } #[macro_export] macro_rules! upper_case { ($input:expr) => {{ - use string_proc::string_processer::StringProcesser; - StringProcesser::from($input).to_upper_case() + use string_proc::format_processer::FormatProcesser; + FormatProcesser::from($input).to_upper_case() }}; } #[macro_export] macro_rules! lower_case { ($input:expr) => {{ - use string_proc::string_processer::StringProcesser; - StringProcesser::from($input).to_lower_case() + use string_proc::format_processer::FormatProcesser; + FormatProcesser::from($input).to_lower_case() }}; } #[macro_export] macro_rules! title_case { ($input:expr) => {{ - use string_proc::string_processer::StringProcesser; - StringProcesser::from($input).to_title_case() + use string_proc::format_processer::FormatProcesser; + FormatProcesser::from($input).to_title_case() }}; } #[macro_export] macro_rules! dot_case { ($input:expr) => {{ - use string_proc::string_processer::StringProcesser; - StringProcesser::from($input).to_dot_case() + use string_proc::format_processer::FormatProcesser; + FormatProcesser::from($input).to_dot_case() }}; } #[macro_export] macro_rules! snake_case { ($input:expr) => {{ - use string_proc::string_processer::StringProcesser; - StringProcesser::from($input).to_snake_case() + use string_proc::format_processer::FormatProcesser; + FormatProcesser::from($input).to_snake_case() }}; } #[macro_export] macro_rules! kebab_case { ($input:expr) => {{ - use string_proc::string_processer::StringProcesser; - StringProcesser::from($input).to_kebab_case() + use string_proc::format_processer::FormatProcesser; + FormatProcesser::from($input).to_kebab_case() }}; } #[macro_export] macro_rules! pascal_case { ($input:expr) => {{ - use string_proc::string_processer::StringProcesser; - StringProcesser::from($input).to_pascal_case() + use string_proc::format_processer::FormatProcesser; + FormatProcesser::from($input).to_pascal_case() }}; } diff --git a/crates/utils/string_proc/src/simple_processer.rs b/crates/utils/string_proc/src/simple_processer.rs new file mode 100644 index 0000000..2de5dfc --- /dev/null +++ b/crates/utils/string_proc/src/simple_processer.rs @@ -0,0 +1,15 @@ +/// Sanitizes a file path by replacing special characters with underscores. +/// +/// This function takes a file path as input and returns a sanitized version +/// where characters that are not allowed in file paths (such as path separators +/// and other reserved characters) are replaced with underscores. +pub fn sanitize_file_path>(path: P) -> String { + let path_str = path.as_ref(); + path_str + .chars() + .map(|c| match c { + '/' | '\\' | ':' | '*' | '?' | '"' | '<' | '>' | '|' => '_', + _ => c, + }) + .collect() +} diff --git a/crates/utils/string_proc/src/string_processer.rs b/crates/utils/string_proc/src/string_processer.rs deleted file mode 100644 index 8b51c12..0000000 --- a/crates/utils/string_proc/src/string_processer.rs +++ /dev/null @@ -1,130 +0,0 @@ -pub struct StringProcesser { - content: Vec, -} - -impl From for StringProcesser { - fn from(value: String) -> Self { - Self { - content: Self::process_string(value), - } - } -} - -impl From<&str> for StringProcesser { - fn from(value: &str) -> Self { - Self { - content: Self::process_string(value.to_string()), - } - } -} - -impl StringProcesser { - /// Process the string into an intermediate format - fn process_string(input: String) -> Vec { - let mut result = String::new(); - let mut prev_space = false; - - for c in input.chars() { - match c { - 'a'..='z' | 'A'..='Z' | '0'..='9' => { - result.push(c); - prev_space = false; - } - '_' | ',' | '.' | '-' | ' ' => { - if !prev_space { - result.push(' '); - prev_space = true; - } - } - _ => {} - } - } - - let mut processed = String::new(); - let mut chars = result.chars().peekable(); - - while let Some(c) = chars.next() { - processed.push(c); - if let Some(&next) = chars.peek() - && c.is_lowercase() && next.is_uppercase() { - processed.push(' '); - } - } - - processed - .to_lowercase() - .split_whitespace() - .map(|s| s.to_string()) - .collect() - } - - /// Convert to camelCase format (brewCoffee) - pub fn to_camel_case(&self) -> String { - let mut result = String::new(); - for (i, word) in self.content.iter().enumerate() { - if i == 0 { - result.push_str(&word.to_lowercase()); - } else { - let mut chars = word.chars(); - if let Some(first) = chars.next() { - result.push_str(&first.to_uppercase().collect::()); - result.push_str(&chars.collect::().to_lowercase()); - } - } - } - result - } - - /// Convert to PascalCase format (BrewCoffee) - pub fn to_pascal_case(&self) -> String { - let mut result = String::new(); - for word in &self.content { - let mut chars = word.chars(); - if let Some(first) = chars.next() { - result.push_str(&first.to_uppercase().collect::()); - result.push_str(&chars.collect::().to_lowercase()); - } - } - result - } - - /// Convert to kebab-case format (brew-coffee) - pub fn to_kebab_case(&self) -> String { - self.content.join("-").to_lowercase() - } - - /// Convert to snake_case format (brew_coffee) - pub fn to_snake_case(&self) -> String { - self.content.join("_").to_lowercase() - } - - /// Convert to dot.case format (brew.coffee) - pub fn to_dot_case(&self) -> String { - self.content.join(".").to_lowercase() - } - - /// Convert to Title Case format (Brew Coffee) - pub fn to_title_case(&self) -> String { - let mut result = String::new(); - for word in &self.content { - let mut chars = word.chars(); - if let Some(first) = chars.next() { - result.push_str(&first.to_uppercase().collect::()); - result.push_str(&chars.collect::().to_lowercase()); - } - result.push(' '); - } - result.pop(); - result - } - - /// Convert to lower case format (brew coffee) - pub fn to_lower_case(&self) -> String { - self.content.join(" ").to_lowercase() - } - - /// Convert to UPPER CASE format (BREW COFFEE) - pub fn to_upper_case(&self) -> String { - self.content.join(" ").to_uppercase() - } -} diff --git a/crates/vcs/src/data/sheet.rs b/crates/vcs/src/data/sheet.rs index edf307a..a6765c0 100644 --- a/crates/vcs/src/data/sheet.rs +++ b/crates/vcs/src/data/sheet.rs @@ -2,6 +2,7 @@ use std::{collections::HashMap, path::PathBuf}; use cfg_file::{ConfigFile, config::ConfigFile}; use serde::{Deserialize, Serialize}; +use string_proc::simple_processer::sanitize_file_path; use crate::{ constants::SERVER_FILE_SHEET, @@ -16,14 +17,24 @@ pub type SheetPathBuf = PathBuf; pub type InputName = String; pub type InputRelativePathBuf = PathBuf; -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, Eq)] pub struct InputPackage { /// Name of the input package pub name: InputName, + + /// The sheet from which this input package was created + pub from: SheetName, + /// Files in this input package with their relative paths and virtual file IDs pub files: Vec<(InputRelativePathBuf, VirtualFileId)>, } +impl PartialEq for InputPackage { + fn eq(&self, other: &Self) -> bool { + self.name == other.name + } +} + const SHEET_NAME: &str = "{sheet-name}"; pub struct Sheet<'a> { @@ -66,15 +77,15 @@ impl<'a> Sheet<'a> { } /// Add an input package to the sheet - pub fn add_input( - &mut self, - input_name: InputName, - files: Vec<(InputRelativePathBuf, VirtualFileId)>, - ) { - self.data.inputs.push(InputPackage { - name: input_name, - files, - }); + pub fn add_input(&mut self, input_package: InputPackage) -> Result<(), std::io::Error> { + if self.data.inputs.iter().any(|input| input == &input_package) { + return Err(std::io::Error::new( + std::io::ErrorKind::AlreadyExists, + format!("Input package '{}' already exists", input_package.name), + )); + } + self.data.inputs.push(input_package); + Ok(()) } /// Remove an input package from the sheet @@ -116,4 +127,89 @@ impl<'a> Sheet<'a> { .vault_path() .join(SERVER_FILE_SHEET.replace(SHEET_NAME, name.as_ref())) } + + /// Export files from the current sheet as an InputPackage for importing into other sheets + /// + /// This is the recommended way to create InputPackages. It takes a list of sheet paths + /// and generates an InputPackage with optimized relative paths by removing the longest + /// common prefix from all provided paths, then placing the files under a directory + /// named with the output_name. + /// + /// # Example + /// Given paths: + /// - `MyProject/Art/Character/Model/final.fbx` + /// - `MyProject/Art/Character/Texture/final.png` + /// - `MyProject/Art/Character/README.md` + /// + /// With output_name = "MyExport", the resulting package will contain: + /// - `MyExport/Model/final.fbx` + /// - `MyExport/Texture/final.png` + /// - `MyExport/README.md` + /// + /// # Arguments + /// * `output_name` - Name of the output package (will be used as the root directory) + /// * `paths` - List of sheet paths to include in the package + /// + /// # Returns + /// Returns an InputPackage containing the exported files with optimized paths, + /// or an error if paths are empty or files are not found in the sheet mapping + pub fn output_mappings( + &self, + output_name: InputName, + paths: &[SheetPathBuf], + ) -> Result { + let output_name = sanitize_file_path(output_name); + + // Return error for empty paths since there's no need to generate an empty package + if paths.is_empty() { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidInput, + "Cannot generate output package with empty paths", + )); + } + + // Find the longest common prefix among all paths + let common_prefix = paths.iter().skip(1).fold(paths[0].clone(), |prefix, path| { + Self::common_path_prefix(prefix, path) + }); + + // Create output files with optimized relative paths + let files = paths + .iter() + .map(|path| { + let relative_path = path.strip_prefix(&common_prefix).unwrap_or(path); + let output_path = PathBuf::from(&output_name).join(relative_path); + + self.data + .mapping + .get(path) + .map(|vfid| (output_path, vfid.clone())) + .ok_or_else(|| { + std::io::Error::new( + std::io::ErrorKind::NotFound, + format!("File not found: {:?}", path), + ) + }) + }) + .collect::, _>>()?; + + Ok(InputPackage { + name: output_name, + from: self.name.clone(), + files, + }) + } + + /// Helper function to find common path prefix between two paths + fn common_path_prefix(path1: impl Into, path2: impl Into) -> PathBuf { + let path1 = path1.into(); + let path2 = path2.into(); + + path1 + .components() + .zip(path2.components()) + .take_while(|(a, b)| a == b) + .map(|(comp, _)| comp) + .collect() + } } diff --git a/crates/vcs/src/data/vault.rs b/crates/vcs/src/data/vault.rs index 5b34c6f..5d17a81 100644 --- a/crates/vcs/src/data/vault.rs +++ b/crates/vcs/src/data/vault.rs @@ -72,9 +72,7 @@ impl Vault { create_dir_all(vault_path.join(SERVER_PATH_VF_ROOT))?; let Some(vault) = Vault::init(config, &vault_path) else { - return Err(std::io::Error::other( - "Failed to initialize vault", - )); + return Err(std::io::Error::other("Failed to initialize vault")); }; // 6. Create host member diff --git a/crates/vcs/vcs_test/src/test_sheet_creation_management_and_persistence.rs b/crates/vcs/vcs_test/src/test_sheet_creation_management_and_persistence.rs index 1c7182b..8abcc4d 100644 --- a/crates/vcs/vcs_test/src/test_sheet_creation_management_and_persistence.rs +++ b/crates/vcs/vcs_test/src/test_sheet_creation_management_and_persistence.rs @@ -48,20 +48,23 @@ async fn test_sheet_creation_management_and_persistence() -> Result<(), std::io: // Test 2: Add input packages to the sheet let input_name = "source_files".to_string(); - let files = vec![ - ( - InputRelativePathBuf::from("src/main.rs"), - VirtualFileId::new(), - ), - ( - InputRelativePathBuf::from("src/lib.rs"), - VirtualFileId::new(), - ), - ]; - // Need to get a mutable reference to the sheet + // First add mapping entries that will be used to generate the input package let mut sheet = vault.sheet(&sheet_name).await?; - sheet.add_input(input_name.clone(), files.clone()); + + // Add mapping entries for the files + let main_rs_path = vcs::data::sheet::SheetPathBuf::from("src/main.rs"); + let lib_rs_path = vcs::data::sheet::SheetPathBuf::from("src/lib.rs"); + let main_rs_id = VirtualFileId::new(); + let lib_rs_id = VirtualFileId::new(); + + sheet.add_mapping(main_rs_path.clone(), main_rs_id.clone()); + sheet.add_mapping(lib_rs_path.clone(), lib_rs_id.clone()); + + // Use output_mappings to generate the InputPackage + let paths = vec![main_rs_path, lib_rs_path]; + let input_package = sheet.output_mappings(input_name.clone(), &paths)?; + sheet.add_input(input_package)?; // Verify input was added assert_eq!(sheet.inputs().len(), 1); @@ -70,11 +73,11 @@ async fn test_sheet_creation_management_and_persistence() -> Result<(), std::io: assert_eq!(added_input.files.len(), 2); assert_eq!( added_input.files[0].0, - InputRelativePathBuf::from("src/main.rs") + InputRelativePathBuf::from("source_files/main.rs") ); assert_eq!( added_input.files[1].0, - InputRelativePathBuf::from("src/lib.rs") + InputRelativePathBuf::from("source_files/lib.rs") ); // Test 3: Add mapping entries @@ -83,7 +86,7 @@ async fn test_sheet_creation_management_and_persistence() -> Result<(), std::io: sheet.add_mapping(mapping_path.clone(), virtual_file_id.clone()); // Verify mapping was added - assert_eq!(sheet.mapping().len(), 1); + assert_eq!(sheet.mapping().len(), 3); assert_eq!(sheet.mapping().get(&mapping_path), Some(&virtual_file_id)); // Test 4: Persist sheet to disk @@ -93,7 +96,7 @@ async fn test_sheet_creation_management_and_persistence() -> Result<(), std::io: let reloaded_sheet = vault.sheet(&sheet_name).await?; assert_eq!(reloaded_sheet.holder(), &member_id); assert_eq!(reloaded_sheet.inputs().len(), 1); - assert_eq!(reloaded_sheet.mapping().len(), 1); + assert_eq!(reloaded_sheet.mapping().len(), 3); // Test 5: Remove input package let mut sheet_for_removal = vault.sheet(&sheet_name).await?; @@ -107,7 +110,7 @@ async fn test_sheet_creation_management_and_persistence() -> Result<(), std::io: // Test 6: Remove mapping entry let removed_virtual_file_id = sheet_for_removal.remove_mapping(&mapping_path); assert_eq!(removed_virtual_file_id, Some(virtual_file_id)); - assert_eq!(sheet_for_removal.mapping().len(), 0); + assert_eq!(sheet_for_removal.mapping().len(), 2); // Test 7: List all sheets in vault let sheet_names = vault.sheet_names()?; @@ -244,7 +247,7 @@ async fn test_sheet_data_serialization() -> Result<(), std::io::Error> { // Add some inputs let input_name = "source_files".to_string(); - let files = vec![ + let _files = vec![ ( InputRelativePathBuf::from("src/main.rs"), VirtualFileId::new(), @@ -254,7 +257,19 @@ async fn test_sheet_data_serialization() -> Result<(), std::io::Error> { VirtualFileId::new(), ), ]; - sheet.add_input(input_name, files); + // First add mapping entries + let main_rs_path = vcs::data::sheet::SheetPathBuf::from("src/main.rs"); + let lib_rs_path = vcs::data::sheet::SheetPathBuf::from("src/lib.rs"); + let main_rs_id = VirtualFileId::new(); + let lib_rs_id = VirtualFileId::new(); + + sheet.add_mapping(main_rs_path.clone(), main_rs_id.clone()); + sheet.add_mapping(lib_rs_path.clone(), lib_rs_id.clone()); + + // Use output_mappings to generate the InputPackage + let paths = vec![main_rs_path, lib_rs_path]; + let input_package = sheet.output_mappings(input_name.clone(), &paths)?; + sheet.add_input(input_package)?; // Add some mappings sheet.add_mapping( -- cgit