From 0b8e6e7d18abb94bd99553dc1d2b0ba5d4f265ea Mon Sep 17 00:00:00 2001 From: 魏曹先生 <1992414357@qq.com> Date: Thu, 18 Jun 2026 02:47:32 +0800 Subject: refactor: extract shared utilities and add space-system crate Extract rola-vcs/internal_macros into shared utils crates (shared_constants, shared_macros, space-system) and implement the Bucket enum with async space management --- Cargo.lock | 41 +- Cargo.toml | 16 +- rola-bucket/Cargo.toml | 5 + rola-bucket/src/bucket.rs | 135 ++++++ rola-bucket/src/bucket/init.rs | 1 + rola-bucket/src/bucket/space.rs | 22 + rola-bucket/src/lib.rs | 3 + rola-utils/constants/Cargo.toml | 11 + rola-utils/constants/src/common.rs | 7 + rola-utils/constants/src/lib.rs | 5 + rola-utils/macros/src/constants.rs | 122 +++++ rola-utils/macros/src/lib.rs | 38 ++ rola-utils/space-system/Cargo.toml | 12 + rola-utils/space-system/macros/Cargo.toml | 15 + rola-utils/space-system/macros/src/lib.rs | 9 + .../space-system/macros/src/space_root_test.rs | 110 +++++ rola-utils/space-system/src/lib.rs | 5 + rola-utils/space-system/src/space.rs | 537 +++++++++++++++++++++ rola-utils/space-system/src/space/error.rs | 23 + rola-vcs/Cargo.toml | 1 - rola-vcs/internal_macros/Cargo.toml | 14 - rola-vcs/internal_macros/src/constants.rs | 115 ----- rola-vcs/internal_macros/src/lib.rs | 39 -- rola-vcs/src/consts/common.rs | 7 - rola-vcs/src/consts/mod.rs | 6 - rola-vcs/src/lib.rs | 2 - 26 files changed, 1108 insertions(+), 193 deletions(-) create mode 100644 rola-bucket/src/bucket/init.rs create mode 100644 rola-bucket/src/bucket/space.rs create mode 100644 rola-utils/constants/Cargo.toml create mode 100644 rola-utils/constants/src/common.rs create mode 100644 rola-utils/constants/src/lib.rs create mode 100644 rola-utils/macros/src/constants.rs create mode 100644 rola-utils/space-system/Cargo.toml create mode 100644 rola-utils/space-system/macros/Cargo.toml create mode 100644 rola-utils/space-system/macros/src/lib.rs create mode 100644 rola-utils/space-system/macros/src/space_root_test.rs create mode 100644 rola-utils/space-system/src/lib.rs create mode 100644 rola-utils/space-system/src/space.rs create mode 100644 rola-utils/space-system/src/space/error.rs delete mode 100644 rola-vcs/internal_macros/Cargo.toml delete mode 100644 rola-vcs/internal_macros/src/constants.rs delete mode 100644 rola-vcs/internal_macros/src/lib.rs delete mode 100644 rola-vcs/src/consts/common.rs delete mode 100644 rola-vcs/src/consts/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 37ecfc2..6347833 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,12 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "just_fmt" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5454cda0d57db59778608d7a47bff5b16c6705598265869fb052b657f66cf05e" + [[package]] name = "pin-project-lite" version = "0.2.17" @@ -30,9 +36,12 @@ dependencies = [ name = "rola-bucket" version = "0.1.0" dependencies = [ + "shared_constants", "shared_functions", "shared_macros", + "space-system", "thiserror", + "tokio", ] [[package]] @@ -53,18 +62,16 @@ version = "0.1.0" dependencies = [ "rola-bucket", "rola-draft", - "rorolala_internal_macros", "shared_functions", "shared_macros", ] [[package]] -name = "rorolala_internal_macros" +name = "shared_constants" version = "0.1.0" dependencies = [ - "proc-macro2", - "quote", - "syn", + "shared_macros", + "tokio", ] [[package]] @@ -83,11 +90,31 @@ dependencies = [ "syn", ] +[[package]] +name = "space-macros" +version = "0.1.0" +dependencies = [ + "just_fmt", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "space-system" +version = "0.1.0" +dependencies = [ + "just_fmt", + "space-macros", + "thiserror", + "tokio", +] + [[package]] name = "syn" -version = "2.0.117" +version = "2.0.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 7a1522c..4ca7eb6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,9 +3,11 @@ resolver = "2" members = [ "rola-bucket", "rola-cli", - "rola-vcs/internal_macros", + "rola-utils/constants", "rola-utils/functions", "rola-utils/macros", + "rola-utils/space-system", + "rola-utils/space-system/macros", "rola-draft", "rola-vcs", ] @@ -19,13 +21,23 @@ license = "MIT" [workspace.dependencies] rorolala = { path = "rola-vcs" } -rorolala_internal_macros = { path = "rola-vcs/internal_macros" } + +quote = "1.0.45" +syn = "2.0.118" +proc-macro2 = "1.0.106" + +shared_constants = { path = "rola-utils/constants" } shared_functions = { path = "rola-utils/functions" } shared_macros = { path = "rola-utils/macros" } + +space-system = { path = "rola-utils/space-system" } +space-macros = { path = "rola-utils/space-system/macros" } + rola-bucket = { path = "rola-bucket" } rola-draft = { path = "rola-draft" } thiserror = "2.0.18" +just_fmt = "0.1.2" [workspace.dependencies.tokio] version = "1.52.3" diff --git a/rola-bucket/Cargo.toml b/rola-bucket/Cargo.toml index 001ec11..07c555e 100644 --- a/rola-bucket/Cargo.toml +++ b/rola-bucket/Cargo.toml @@ -6,6 +6,11 @@ authors.workspace = true license.workspace = true [dependencies] +shared_constants.workspace = true shared_functions.workspace = true shared_macros.workspace = true + +space-system.workspace = true + thiserror.workspace = true +tokio.workspace = true diff --git a/rola-bucket/src/bucket.rs b/rola-bucket/src/bucket.rs index e69de29..b70afd8 100644 --- a/rola-bucket/src/bucket.rs +++ b/rola-bucket/src/bucket.rs @@ -0,0 +1,135 @@ +use crate::AsyncBucketTransferProtocol; + +#[cfg(test)] +use crate::LocalFileSystemProtocol; +use space_system::SpaceRootTest; + +mod init; +// pub use init::*; + +mod space; + +/// Represents the state of a bucket in the transfer protocol. +/// +/// # Variants +/// +/// * `Uninit` - The bucket has not been initialized. This is the default state. +/// **Warning:** Most mutation methods (e.g., `set_extra_info`, `remove_extra_info`) will **panic** +/// when called on an `Uninit` bucket. Always ensure the bucket is in a `Local` or `Remote` state +/// before attempting to modify its `extra_info`. +/// +/// * `Local` - The bucket is on the local side of the transfer, with optional extra information. +/// +/// * `Remote` - The bucket is on the remote side of the transfer, with optional extra information. +#[derive(Default, Clone, SpaceRootTest)] +#[space_root_test_generic(LocalFileSystemProtocol)] +pub enum Bucket +where + Protocol: AsyncBucketTransferProtocol + Send + Sync, +{ + #[default] + Uninit, + + Local { + extra_info: Option, + }, + Remote { + extra_info: Option, + }, +} + +impl Bucket { + /// Creates a new `Bucket::Local` + pub fn new_local() -> Self { + Self::Local { extra_info: None } + } + + /// Creates a new `Bucket::Local` with extra information. + pub fn local_with_extra_info(extra_info: Protocol::ExtraInfo) -> Self { + Self::Local { + extra_info: Some(extra_info), + } + } + + /// Creates a new `Bucket::Remote` + pub fn new_remote() -> Self { + Self::Remote { extra_info: None } + } + + /// Creates a new `Bucket::Remote` with extra information. + pub fn remote_with_extra_info(extra_info: Protocol::ExtraInfo) -> Self { + Self::Remote { + extra_info: Some(extra_info), + } + } + + /// Returns a mutable reference to the extra_info field regardless of variant. + fn extra_info_mut(&mut self) -> &mut Option { + match self { + Self::Local { extra_info } | Self::Remote { extra_info } => extra_info, + Self::Uninit => panic!("Cannot access extra_info on an Uninit bucket"), + } + } + + /// Sets extra info on an existing bucket, returning the previous value if any. + pub fn set_extra_info( + &mut self, + extra_info: Protocol::ExtraInfo, + ) -> Option { + self.extra_info_mut().replace(extra_info) + } + + /// Removes extra info from the bucket, returning it if present. + pub fn remove_extra_info(&mut self) -> Option { + self.extra_info_mut().take() + } + + /// Checks if the bucket has extra information. + pub fn has_extra_info(&self) -> bool { + match self { + Self::Local { extra_info } | Self::Remote { extra_info } => extra_info.is_some(), + Self::Uninit => false, + } + } + + /// Gets a reference to the extra info, if present. + pub fn extra_info(&self) -> Option<&Protocol::ExtraInfo> { + match self { + Self::Local { extra_info } | Self::Remote { extra_info } => extra_info.as_ref(), + Self::Uninit => None, + } + } + + /// Returns true if this is a local bucket. + pub fn is_local(&self) -> bool { + matches!(self, Self::Local { .. }) + } + + /// Returns true if this is a remote bucket. + pub fn is_remote(&self) -> bool { + matches!(self, Self::Remote { .. }) + } + + /// Returns true if this bucket is uninitialized. + pub fn is_uninit(&self) -> bool { + matches!(self, Self::Uninit) + } + + /// Converts the bucket to a Remote variant, keeping extra_info if present. + pub fn force_to_remote(self) -> Self { + let extra_info = match self { + Self::Local { extra_info } | Self::Remote { extra_info } => extra_info, + Self::Uninit => None, + }; + Self::Remote { extra_info } + } + + /// Converts the bucket to a Local variant, keeping extra_info if present. + pub fn force_to_local(self) -> Self { + let extra_info = match self { + Self::Local { extra_info } | Self::Remote { extra_info } => extra_info, + Self::Uninit => None, + }; + Self::Local { extra_info } + } +} diff --git a/rola-bucket/src/bucket/init.rs b/rola-bucket/src/bucket/init.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/rola-bucket/src/bucket/init.rs @@ -0,0 +1 @@ + diff --git a/rola-bucket/src/bucket/space.rs b/rola-bucket/src/bucket/space.rs new file mode 100644 index 0000000..9559b1d --- /dev/null +++ b/rola-bucket/src/bucket/space.rs @@ -0,0 +1,22 @@ +use shared_constants::common::DRAFT_META_DIR; +use space_system::{SpaceError, SpaceRoot, SpaceRootFindPattern}; +use tokio::fs::create_dir_all; + +use crate::{AsyncBucketTransferProtocol, Bucket}; + +impl SpaceRoot for Bucket { + fn get_pattern() -> SpaceRootFindPattern { + SpaceRootFindPattern::IncludeDotDir(DRAFT_META_DIR.into()) + } + + async fn create_space(path: &std::path::Path) -> Result<(), space_system::SpaceError> { + let draft_meta_dir = path.join(DRAFT_META_DIR); + + // Create workspace directory + create_dir_all(&draft_meta_dir) + .await + .map_err(SpaceError::from)?; + + Ok(()) + } +} diff --git a/rola-bucket/src/lib.rs b/rola-bucket/src/lib.rs index c45452c..1f3470f 100644 --- a/rola-bucket/src/lib.rs +++ b/rola-bucket/src/lib.rs @@ -12,5 +12,8 @@ //! //! This module does **not** implement any **specific transport method**; it only implements the workflow for file storage and retrieval. +mod bucket; +pub use bucket::*; + mod protocol; pub use protocol::*; diff --git a/rola-utils/constants/Cargo.toml b/rola-utils/constants/Cargo.toml new file mode 100644 index 0000000..7277153 --- /dev/null +++ b/rola-utils/constants/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "shared_constants" +version.workspace = true +edition.workspace = true +authors.workspace = true +license.workspace = true + +[dependencies] +shared_macros.workspace = true + +tokio.workspace = true diff --git a/rola-utils/constants/src/common.rs b/rola-utils/constants/src/common.rs new file mode 100644 index 0000000..6ce6bd8 --- /dev/null +++ b/rola-utils/constants/src/common.rs @@ -0,0 +1,7 @@ +#[shared_macros::constants] +mod consts { + /// Directory name for Rorolala metadata storage in Workdraft + pub const DRAFT_META_DIR: &str = ".rola"; +} + +pub use consts::*; diff --git a/rola-utils/constants/src/lib.rs b/rola-utils/constants/src/lib.rs new file mode 100644 index 0000000..566440d --- /dev/null +++ b/rola-utils/constants/src/lib.rs @@ -0,0 +1,5 @@ +//! Rorolala Constants +//! +//! This module records all constant information for Rorolala + +pub mod common; diff --git a/rola-utils/macros/src/constants.rs b/rola-utils/macros/src/constants.rs new file mode 100644 index 0000000..e5fe668 --- /dev/null +++ b/rola-utils/macros/src/constants.rs @@ -0,0 +1,122 @@ +use proc_macro::TokenStream; +use quote::{format_ident, quote}; +use syn::{Expr, Item, ItemConst, ItemMod, Lit, parse_macro_input, parse_quote}; + +/// Entry point called from lib.rs. +pub fn expand(_attr: TokenStream, item: TokenStream) -> TokenStream { + let mut input_mod = parse_macro_input!(item as ItemMod); + + let (_, items) = match &mut input_mod.content { + Some(content) => content, + None => panic!("#[constants] can only be applied to a module with a body"), + }; + + let mut new_items: Vec = Vec::with_capacity(items.len()); + + for item in items.iter() { + if let Item::Const(const_item) = item { + let func = transform_const(const_item); + new_items.push(func); + } else { + new_items.push(item.clone()); + } + } + + let mod_ident = &input_mod.ident; + let vis = &input_mod.vis; + + let output = quote! { + #[allow(non_snake_case)] + #vis mod #mod_ident { + #(#new_items)* + } + }; + + output.into() +} + +/// Transforms a single `const` item into a functionif. +fn transform_const(const_item: &ItemConst) -> Item { + let name = &const_item.ident; + let attrs = &const_item.attrs; + + // Extract the string literal value from the const + let value_str = match &*const_item.expr { + Expr::Lit(expr_lit) => match &expr_lit.lit { + Lit::Str(lit_str) => lit_str.value(), + _ => panic!( + "#[constants] only supports `&str` literals, \ + but `{name}` has a non-string literal" + ), + }, + _ => panic!( + "#[constants] only supports literal expressions, \ + but `{name}` has a non-literal expression" + ), + }; + + let placeholders = extract_placeholders(&value_str); + + // Build a doc comment that shows the original constant value + let doc_comment = format!( + "Generated from const `{}` with value: \"{}\"", + name, + value_str.replace('\"', "\\\"") + ); + + if placeholders.is_empty() { + parse_quote! { + #(#attrs)* + #[doc = #doc_comment] + pub const #name: &'static str = #value_str; + } + } else { + let params: Vec<_> = placeholders + .iter() + .map(|p| { + let ident = format_ident!("{p}"); + quote! { #ident: impl ::core::convert::AsRef } + }) + .collect(); + + let format_args: Vec<_> = placeholders + .iter() + .map(|p| { + let ident = format_ident!("{p}"); + quote! { #ident = #ident.as_ref() } + }) + .collect(); + + parse_quote! { + #(#attrs)* + #[doc = #doc_comment] + pub fn #name(#(#params),*) -> String { + ::std::format!(#value_str, #(#format_args),*) + } + } + } +} + +/// Extracts all `{name}` placeholder identifiers from a format string. +fn extract_placeholders(s: &str) -> Vec { + let mut placeholders = Vec::new(); + let mut chars = s.char_indices().peekable(); + + while let Some((_, c)) = chars.next() { + if c == '{' { + let mut name = String::new(); + for (_, c) in &mut chars { + if c == '}' { + break; + } + name.push(c); + } + let trimmed = name.trim().to_string(); + if !trimmed.is_empty() { + placeholders.push(trimmed); + } + } + } + + placeholders +} diff --git a/rola-utils/macros/src/lib.rs b/rola-utils/macros/src/lib.rs index 8b13789..46762e3 100644 --- a/rola-utils/macros/src/lib.rs +++ b/rola-utils/macros/src/lib.rs @@ -1 +1,39 @@ +use proc_macro::TokenStream; +mod constants; + +/// Transforms `pub const` items in a module into equivalent functions. +/// +/// Constants without `{param}` placeholders become `fn NAME() -> String`. +/// Constants with `{param}` placeholders become `fn NAME(param: impl AsRef) -> String`, +/// using `format!()` to fill in the placeholders. +/// +/// The entire module is annotated with `#[allow(non_snake_case)]`. +/// +/// # Example +/// +/// ```ignore +/// #[rorolala_internal_macros::constants] +/// pub mod paths { +/// pub const ROLA_DRAFT_DIR: &str = ".rola"; +/// pub const ROLA_BINDED_BUCKET_FILE: &str = ".rola/BIND/{bucket}"; +/// } +/// ``` +/// +/// expands to: +/// +/// ```ignore +/// #[allow(non_snake_case)] +/// pub mod paths { +/// pub fn ROLA_DRAFT_DIR() -> String { +/// ".rola".to_string() +/// } +/// pub fn ROLA_BINDED_BUCKET_FILE(bucket: impl AsRef) -> String { +/// format!(".rola/BIND/{bucket}", bucket = bucket.as_ref()) +/// } +/// } +/// ``` +#[proc_macro_attribute] +pub fn constants(attr: TokenStream, item: TokenStream) -> TokenStream { + constants::expand(attr, item) +} diff --git a/rola-utils/space-system/Cargo.toml b/rola-utils/space-system/Cargo.toml new file mode 100644 index 0000000..5322cbf --- /dev/null +++ b/rola-utils/space-system/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "space-system" +version.workspace = true +edition.workspace = true +authors.workspace = true +license.workspace = true + +[dependencies] +space-macros.workspace = true +thiserror.workspace = true +just_fmt.workspace = true +tokio.workspace = true diff --git a/rola-utils/space-system/macros/Cargo.toml b/rola-utils/space-system/macros/Cargo.toml new file mode 100644 index 0000000..84ba691 --- /dev/null +++ b/rola-utils/space-system/macros/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "space-macros" +version.workspace = true +edition.workspace = true +authors.workspace = true +license.workspace = true + +[lib] +proc-macro = true + +[dependencies] +syn.workspace = true +quote.workspace = true +proc-macro2.workspace = true +just_fmt.workspace = true diff --git a/rola-utils/space-system/macros/src/lib.rs b/rola-utils/space-system/macros/src/lib.rs new file mode 100644 index 0000000..838155c --- /dev/null +++ b/rola-utils/space-system/macros/src/lib.rs @@ -0,0 +1,9 @@ +use crate::space_root_test::internal_space_root_test_derive; +use proc_macro::TokenStream; + +mod space_root_test; + +#[proc_macro_derive(SpaceRootTest, attributes(space_root_test_generic))] +pub fn space_root_test_derive(input: TokenStream) -> TokenStream { + internal_space_root_test_derive(input) +} diff --git a/rola-utils/space-system/macros/src/space_root_test.rs b/rola-utils/space-system/macros/src/space_root_test.rs new file mode 100644 index 0000000..71c48c0 --- /dev/null +++ b/rola-utils/space-system/macros/src/space_root_test.rs @@ -0,0 +1,110 @@ +use proc_macro::TokenStream; +use quote::quote; +use syn::{ + DeriveInput, Token, + parse::{Parse, ParseStream}, + parse_macro_input, +}; + +/// Parsed content of `#[space_root_test_generic(...)]`. +struct GenericArgs { + types: Vec, +} + +impl Parse for GenericArgs { + fn parse(input: ParseStream) -> syn::Result { + let mut types = Vec::new(); + if !input.is_empty() { + types.push(input.parse()?); + while !input.is_empty() { + let _: Token![,] = input.parse()?; + if !input.is_empty() { + types.push(input.parse()?); + } + } + } + Ok(GenericArgs { types }) + } +} + +pub(crate) fn internal_space_root_test_derive(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + + let name = &input.ident; + + // Extract generic args from `#[space_root_test_generic(...)]` + let generics = input + .attrs + .iter() + .find(|attr| attr.path().is_ident("space_root_test_generic")) + .and_then(|attr| attr.parse_args::().ok()) + .unwrap_or(GenericArgs { types: Vec::new() }); + + // Build the turbofish segment if there are generic args + let turbofish = if generics.types.is_empty() { + quote! {} + } else { + let params = &generics.types[..]; + quote! { ::< #(#params),* > } + }; + + let test_mod_name = syn::Ident::new( + &format!( + "test_{}_space_root", + just_fmt::snake_case!(name.to_string()) + ), + name.span(), + ); + + let expanded = quote! { + #[cfg(test)] + mod #test_mod_name { + use super::*; + use shared_functions::rola_test_sandbox; + use space_system::{Space, SpaceRoot, SpaceRootFindPattern}; + use std::env::set_current_dir; + + #[tokio::test] + async fn test_create_space() { + let sandbox = rola_test_sandbox(stringify!(#name)); + set_current_dir(&*sandbox).unwrap(); + + let mut space = Space::new(#name #turbofish ::default()); + + match #name #turbofish ::get_pattern() { + SpaceRootFindPattern::AbsolutePath(path_buf) => { + let dir = sandbox.join("root"); + println!("Redirect absolute path:"); + println!(" from: `{}`", path_buf.display()); + println!(" to: `{}`", dir.display()); + space.set_override_pattern(Some( + SpaceRootFindPattern::AbsolutePath(dir.clone()), + )); + + println!("Checking if {} absolute directory does not exist before initialization", stringify!(#name)); + assert!(!dir.exists()); + + space.init_here().await.unwrap(); + + println!("Checking if {} absolute directory exists after initialization", stringify!(#name)); + assert!(dir.exists()); + println!("\u{001b}[33;1mwarning\u{001b}[0m: Absolute path test completed in isolated environment, may not fully represent system runtime conditions"); + + return; + }, + _ => {} + } + + println!("Checking if {} does not exist before initialization", stringify!(#name)); + assert!(space.space_dir_current().is_err()); + + space.init_here().await.unwrap(); + + println!("Checking if {} exists after initialization", stringify!(#name)); + assert!(space.space_dir_current().is_ok()); + } + } + }; + + TokenStream::from(expanded) +} diff --git a/rola-utils/space-system/src/lib.rs b/rola-utils/space-system/src/lib.rs new file mode 100644 index 0000000..3d00063 --- /dev/null +++ b/rola-utils/space-system/src/lib.rs @@ -0,0 +1,5 @@ +mod space; +pub use space::*; + +#[allow(unused_imports)] +pub use space_macros::*; diff --git a/rola-utils/space-system/src/space.rs b/rola-utils/space-system/src/space.rs new file mode 100644 index 0000000..3fe3507 --- /dev/null +++ b/rola-utils/space-system/src/space.rs @@ -0,0 +1,537 @@ +use just_fmt::fmt_path::{PathFormatConfig, fmt_path, fmt_path_custom}; +use std::{ + env::current_dir, + ffi::OsString, + ops::Deref, + path::{Path, PathBuf}, + sync::RwLock, +}; + +mod error; +pub use error::*; + +pub struct Space { + path_format_cfg: PathFormatConfig, + + content: T, + space_dir: RwLock>, + current_dir: Option, + + pub(crate) override_pattern: Option, +} + +impl Space { + /// Create a new `Space` instance with the given content. + pub fn new(content: T) -> Self { + Space { + path_format_cfg: PathFormatConfig { + resolve_parent_dirs: true, + ..Default::default() + }, + content, + space_dir: RwLock::new(None), + current_dir: None, + override_pattern: None, + } + } + + /// Initialize a space at the given path. + /// + /// Checks if a space exists at the given path. If not, creates a new space + /// by calling `T::create_space()` at that path. + pub async fn init(&self, path: impl AsRef) -> Result<(), SpaceError> { + let path = path.as_ref(); + let pattern = match &self.override_pattern { + Some(pattern) => pattern, + None => &T::get_pattern(), + }; + + // If using Absolute, directly read the internal path + let path = match &pattern { + SpaceRootFindPattern::AbsolutePath(path_buf) => path_buf.clone(), + _ => path.to_path_buf(), + }; + + if find_space_root_with(&path, pattern).is_err() { + T::create_space(&path).await?; + } + Ok(()) + } + + /// Create a new space at the given path with the specified name. + /// + /// The full path is constructed as `path/name`. Checks if a space already + /// exists at that location. If not, creates a new space by calling + /// `T::create_space()` at that path. + pub async fn create(&self, path: impl AsRef, name: &str) -> Result<(), SpaceError> { + let full_path = path.as_ref().join(name); + self.init(full_path).await + } + + /// Initialize a space in the current directory. + /// + /// Checks if a space exists in the current directory. If not, creates a new space + /// by calling `T::create_space()` at the current directory. + pub async fn init_here(&self) -> Result<(), SpaceError> { + let current_dir = self.current_dir()?; + self.init(current_dir).await + } + + /// Create a new space in the current directory with the specified name. + /// + /// The full path is constructed as `current_dir/name`. Checks if a space already + /// exists at that location. If not, creates a new space by calling + /// `T::create_space()` at that path. + pub async fn create_here(&self, name: &str) -> Result<(), SpaceError> { + let current_dir = self.current_dir()?; + self.create(current_dir, name).await + } + + /// Consume the `Space`, returning the inner content. + pub fn into_inner(self) -> T { + self.content + } + + /// Get the space directory for the given current directory. + /// + /// If the space directory has already been found, it is returned from cache. + /// Otherwise, it is found using the pattern from `T::get_pattern()`. + pub fn space_dir(&self, current_dir: impl Into) -> Result { + // First try to read from cache + if let Ok(lock) = self.space_dir.read() + && let Some(cached_dir) = lock.as_ref() + { + return Ok(cached_dir.clone()); + } + + // Cache miss, find the space directory + let pattern = match &self.override_pattern { + Some(pattern) => pattern, + None => &T::get_pattern(), + }; + let result = find_space_root_with(current_dir.into(), pattern); + + match result { + Ok(dir) => { + // Update cache with the found directory + self.update_space_dir(Some(dir.clone())); + Ok(dir) + } + Err(e) => Err(e), + } + } + + /// Get the space directory using the current directory. + /// + /// The current directory is either the explicitly set directory or the process's current directory. + pub fn space_dir_current(&self) -> Result { + self.space_dir(self.current_dir()?) + } + + /// Set the current directory explicitly. + /// + /// This clears any cached space directory. + pub fn set_current_dir(&mut self, path: PathBuf) -> Result<(), SpaceError> { + self.update_space_dir(None); + self.current_dir = Some(fmt_path(path)?); + Ok(()) + } + + /// Reset the current directory to the process's current directory. + /// + /// This clears any cached space directory. + pub fn reset_current_dir(&mut self) { + self.update_space_dir(None); + self.current_dir = None + } + + /// Get the current directory. + /// + /// Returns the explicitly set directory if any, otherwise the process's current directory. + fn current_dir(&self) -> Result { + match &self.current_dir { + Some(d) => Ok(d.clone()), + None => Ok(fmt_path(current_dir()?)?), + } + } + + /// Update the cached space directory. + fn update_space_dir(&self, space_dir: Option) { + if let Ok(mut lock) = self.space_dir.write() { + *lock = space_dir; + } + } + + /// Tamper with space directory + /// + /// Forcefully modify the current Space's directory path + pub fn tamper_space_dir(&self, space_dir: Option) { + self.update_space_dir(space_dir); + } + + /// Set a custom pattern to override the default space root detection. + pub fn set_override_pattern(&mut self, pattern: Option) { + self.override_pattern = pattern; + // Clear cached space directory since pattern may have changed + self.update_space_dir(None); + } +} + +impl Space { + /// Convert a relative path to an absolute path within the space. + /// + /// The path is formatted according to the space's path format configuration. + pub fn local_path(&self, relative_path: impl AsRef) -> Result { + let path = fmt_path_custom(relative_path.as_ref().to_path_buf(), &self.path_format_cfg)?; + let raw_path = self.space_dir_current()?.join(path); + Ok(fmt_path(raw_path)?) + } + + /// Convert an absolute path to a relative path within the space, if possible. + /// + /// Returns `None` if the absolute path is not under the space directory. + pub fn to_local_path( + &self, + absolute_path: impl AsRef, + ) -> Result, SpaceError> { + let path = fmt_path(absolute_path.as_ref())?; + let current = self.space_dir_current()?; + match path.strip_prefix(current) { + Ok(result) => Ok(Some(result.to_path_buf())), + Err(_) => Ok(None), + } + } + + /// Canonicalize a relative path within the space. + pub async fn canonicalize( + &self, + relative_path: impl AsRef, + ) -> Result { + let path = self.local_path(relative_path)?; + Ok(tokio::fs::canonicalize(path).await?) + } + + /// Copy a file from one relative path to another within the space. + pub async fn copy( + &self, + from: impl AsRef, + to: impl AsRef, + ) -> Result { + let from_path = self.local_path(from)?; + let to_path = self.local_path(to)?; + Ok(tokio::fs::copy(from_path, to_path).await?) + } + + /// Create a directory at the given relative path within the space. + pub async fn create_dir(&self, relative_path: impl AsRef) -> Result<(), SpaceError> { + let path = self.local_path(relative_path)?; + Ok(tokio::fs::create_dir(path).await?) + } + + /// Recursively create a directory and all its parents at the given relative path within the space. + pub async fn create_dir_all(&self, relative_path: impl AsRef) -> Result<(), SpaceError> { + let path = self.local_path(relative_path)?; + Ok(tokio::fs::create_dir_all(path).await?) + } + + /// Create a hard link from `src` to `dst` within the space. + pub async fn hard_link( + &self, + src: impl AsRef, + dst: impl AsRef, + ) -> Result<(), SpaceError> { + let src_path = self.local_path(src)?; + let dst_path = self.local_path(dst)?; + Ok(tokio::fs::hard_link(src_path, dst_path).await?) + } + + /// Get metadata for a file or directory at the given relative path within the space. + pub async fn metadata( + &self, + relative_path: impl AsRef, + ) -> Result { + let path = self.local_path(relative_path)?; + Ok(tokio::fs::metadata(path).await?) + } + + /// Read the entire contents of a file at the given relative path within the space. + pub async fn read(&self, relative_path: impl AsRef) -> Result, SpaceError> { + let path = self.local_path(relative_path)?; + Ok(tokio::fs::read(path).await?) + } + + /// Read the directory entries at the given relative path within the space. + pub async fn read_dir( + &self, + relative_path: impl AsRef, + ) -> Result { + let path = self.local_path(relative_path)?; + Ok(tokio::fs::read_dir(path).await?) + } + + /// Read the target of a symbolic link at the given relative path within the space. + pub async fn read_link(&self, relative_path: impl AsRef) -> Result { + let path = self.local_path(relative_path)?; + Ok(tokio::fs::read_link(path).await?) + } + + /// Read the entire contents of a file as a string at the given relative path within the space. + pub async fn read_to_string( + &self, + relative_path: impl AsRef, + ) -> Result { + let path = self.local_path(relative_path)?; + Ok(tokio::fs::read_to_string(path).await?) + } + + /// Remove an empty directory at the given relative path within the space. + pub async fn remove_dir(&self, relative_path: impl AsRef) -> Result<(), SpaceError> { + let path = self.local_path(relative_path)?; + Ok(tokio::fs::remove_dir(path).await?) + } + + /// Remove a directory and all its contents at the given relative path within the space. + pub async fn remove_dir_all(&self, relative_path: impl AsRef) -> Result<(), SpaceError> { + let path = self.local_path(relative_path)?; + Ok(tokio::fs::remove_dir_all(path).await?) + } + + /// Remove a file at the given relative path within the space. + pub async fn remove_file(&self, relative_path: impl AsRef) -> Result<(), SpaceError> { + let path = self.local_path(relative_path)?; + Ok(tokio::fs::remove_file(path).await?) + } + + /// Rename a file or directory from one relative path to another within the space. + pub async fn rename( + &self, + from: impl AsRef, + to: impl AsRef, + ) -> Result<(), SpaceError> { + let from_path = self.local_path(from)?; + let to_path = self.local_path(to)?; + Ok(tokio::fs::rename(from_path, to_path).await?) + } + + /// Set permissions for a file or directory at the given relative path within the space. + pub async fn set_permissions( + &self, + relative_path: impl AsRef, + perm: std::fs::Permissions, + ) -> Result<(), SpaceError> { + let path = self.local_path(relative_path)?; + Ok(tokio::fs::set_permissions(path, perm).await?) + } + + /// Create a symbolic link from `src` to `dst` within the space (Unix only). + #[cfg(unix)] + pub async fn symlink( + &self, + src: impl AsRef, + dst: impl AsRef, + ) -> Result<(), SpaceError> { + let src_path = self.local_path(src)?; + let dst_path = self.local_path(dst)?; + Ok(tokio::fs::symlink(src_path, dst_path).await?) + } + + /// Create a directory symbolic link from `src` to `dst` within the space (Windows only). + #[cfg(windows)] + pub async fn symlink_dir( + &self, + src: impl AsRef, + dst: impl AsRef, + ) -> Result<(), SpaceError> { + let src_path = self.local_path(src)?; + let dst_path = self.local_path(dst)?; + Ok(tokio::fs::symlink_dir(src_path, dst_path).await?) + } + + /// Create a file symbolic link from `src` to `dst` within the space (Windows only). + #[cfg(windows)] + pub async fn symlink_file( + &self, + src: impl AsRef, + dst: impl AsRef, + ) -> Result<(), SpaceError> { + let src_path = self.local_path(src)?; + let dst_path = self.local_path(dst)?; + Ok(tokio::fs::symlink_file(src_path, dst_path).await?) + } + + /// Get metadata for a file or directory without following symbolic links. + pub async fn symlink_metadata( + &self, + relative_path: impl AsRef, + ) -> Result { + let path = self.local_path(relative_path)?; + Ok(tokio::fs::symlink_metadata(path).await?) + } + + /// Check if a file or directory exists at the given relative path within the space. + pub async fn try_exists(&self, relative_path: impl AsRef) -> Result { + let path = self.local_path(relative_path)?; + Ok(tokio::fs::try_exists(path).await?) + } + + /// Write data to a file at the given relative path within the space. + pub async fn write( + &self, + relative_path: impl AsRef, + contents: impl AsRef<[u8]>, + ) -> Result<(), SpaceError> { + let path = self.local_path(relative_path)?; + Ok(tokio::fs::write(path, contents).await?) + } + + /// Check if a file or directory exists at the given relative path within the space. + pub async fn exists(&self, relative_path: impl AsRef) -> Result { + let path = self.local_path(relative_path)?; + Ok(tokio::fs::try_exists(path).await?) + } +} + +impl From for Space { + fn from(content: T) -> Self { + Space::::new(content) + } +} + +impl AsRef for Space { + fn as_ref(&self) -> &T { + &self.content + } +} + +impl Deref for Space { + type Target = T; + fn deref(&self) -> &Self::Target { + self.as_ref() + } +} + +pub trait SpaceRoot: Sized { + /// Get the pattern used to identify the space root + fn get_pattern() -> SpaceRootFindPattern; + + /// Given a non-space directory, implement logic to make it a space-recognizable directory + fn create_space(path: &Path) -> impl Future> + Send; +} + +pub enum SpaceRootFindPattern { + /// Search upward from the given current directory to find a directory containing the specified `.dir` + IncludeDotDir(OsString), + + /// Search upward from the given current directory to find a directory containing the specified file name + IncludeFile(OsString), + + /// Given a specific directory + AbsolutePath(PathBuf), +} + +/// Find the space directory containing the current directory, +/// Use Pattern to specify the search method +/// +/// For the full implementation, see `find_space_root_with` +pub fn find_space_root(pattern: &SpaceRootFindPattern) -> Result { + find_space_root_with(¤t_dir()?, pattern) +} + +/// Find the space directory containing the specified directory, +/// Use Pattern to specify the search method +/// +/// IncludeDotDir(OsString) +/// - Contains a specific directory, e.g., to find `.git`, use `IncludeDotDir("git".into())` +/// +/// IncludeFile(OsString) +/// - Contains a specific file, e.g., to find `Cargo.toml`, use `IncludeFile("Cargo.toml".into())` +/// +/// ```rust +/// # use std::env::current_dir; +/// # use std::path::PathBuf; +/// # use framework::space::SpaceRootFindPattern; +/// # use framework::space::find_space_root_with; +/// // Find the `.cargo` directory +/// let path = find_space_root_with( +/// current_dir().unwrap(), +/// &SpaceRootFindPattern::IncludeDotDir( +/// "cargo".into() +/// ) +/// ); +/// assert!(path.is_ok()); +/// assert!(path.unwrap().join(".cargo").is_dir()) +/// ``` +/// ```rust +/// # use std::env::current_dir; +/// # use std::path::PathBuf; +/// # use framework::space::SpaceRootFindPattern; +/// # use framework::space::find_space_root_with; +/// // Find the `.cargo` directory +/// let path = find_space_root_with( +/// current_dir().unwrap(), +/// &SpaceRootFindPattern::IncludeDotDir( +/// ".cargo".into() +/// ) +/// ); +/// assert!(path.is_ok()); +/// assert!(path.unwrap().join(".cargo").is_dir()) +/// ``` +/// ```rust +/// # use std::env::current_dir; +/// # use std::path::PathBuf; +/// # use framework::space::SpaceRootFindPattern; +/// # use framework::space::find_space_root_with; +/// // Find the `Cargo.toml` file +/// let path = find_space_root_with( +/// current_dir().unwrap(), +/// &SpaceRootFindPattern::IncludeFile( +/// "Cargo.toml".into() +/// ) +/// ); +/// assert!(path.is_ok()); +/// assert!(path.unwrap().join("Cargo.toml").is_file()) +/// ``` +pub fn find_space_root_with( + current_dir: impl Into, + pattern: &SpaceRootFindPattern, +) -> Result { + // Get the pattern used for matching + let match_pattern: Box bool> = match pattern { + SpaceRootFindPattern::IncludeDotDir(dot_dir_name) => Box::new(move |path| { + let dir_name = dot_dir_name.to_string_lossy(); + let dir_name = if dir_name.starts_with('.') { + dir_name.to_string() + } else { + format!(".{}", dir_name) + }; + path.join(dir_name).is_dir() + }), + SpaceRootFindPattern::IncludeFile(file_name) => { + Box::new(move |path| path.join(file_name).is_file()) + } + + // For absolute paths, return directly + // No search is performed + SpaceRootFindPattern::AbsolutePath(path) => { + if path.exists() && path.is_dir() { + return Ok(path.clone()); + } else { + return Err(SpaceError::SpaceNotFound); + } + } + }; + + // Match parent directories + let mut current = current_dir.into(); + loop { + if match_pattern(current.as_path()) { + return Ok(current); + } + if let Some(parent) = current.parent() { + current = parent.to_path_buf(); + } else { + break; + } + } + Err(SpaceError::SpaceNotFound) +} diff --git a/rola-utils/space-system/src/space/error.rs b/rola-utils/space-system/src/space/error.rs new file mode 100644 index 0000000..33ee6e4 --- /dev/null +++ b/rola-utils/space-system/src/space/error.rs @@ -0,0 +1,23 @@ +#[derive(thiserror::Error, Debug)] +pub enum SpaceError { + #[error("Space not found")] + SpaceNotFound, + + #[error("Path format error: {0}")] + PathFormatError(#[from] just_fmt::fmt_path::PathFormatError), + + #[error("IO error: {0}")] + Io(#[from] std::io::Error), + + #[error("Other: {0}")] + Other(String), +} + +impl PartialEq for SpaceError { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (Self::Io(_), Self::Io(_)) => true, + _ => core::mem::discriminant(self) == core::mem::discriminant(other), + } + } +} diff --git a/rola-vcs/Cargo.toml b/rola-vcs/Cargo.toml index 8caf0cd..ce32721 100644 --- a/rola-vcs/Cargo.toml +++ b/rola-vcs/Cargo.toml @@ -10,7 +10,6 @@ name = "rorolala" path = "src/lib.rs" [dependencies] -rorolala_internal_macros.workspace = true shared_functions.workspace = true shared_macros.workspace = true rola-bucket.workspace = true diff --git a/rola-vcs/internal_macros/Cargo.toml b/rola-vcs/internal_macros/Cargo.toml deleted file mode 100644 index 0d6f641..0000000 --- a/rola-vcs/internal_macros/Cargo.toml +++ /dev/null @@ -1,14 +0,0 @@ -[package] -name = "rorolala_internal_macros" -version.workspace = true -authors.workspace = true -license.workspace = true -edition.workspace = true - -[lib] -proc-macro = true - -[dependencies] -syn = { version = "2", features = ["full"] } -quote = "1" -proc-macro2 = "1" diff --git a/rola-vcs/internal_macros/src/constants.rs b/rola-vcs/internal_macros/src/constants.rs deleted file mode 100644 index 2e76bfe..0000000 --- a/rola-vcs/internal_macros/src/constants.rs +++ /dev/null @@ -1,115 +0,0 @@ -use proc_macro::TokenStream; -use quote::{format_ident, quote}; -use syn::{Expr, Item, ItemConst, ItemMod, Lit, parse_macro_input, parse_quote}; - -/// Entry point called from lib.rs. -pub fn expand(_attr: TokenStream, item: TokenStream) -> TokenStream { - let mut input_mod = parse_macro_input!(item as ItemMod); - - let (_, items) = match &mut input_mod.content { - Some(content) => content, - None => panic!("#[constants] can only be applied to a module with a body"), - }; - - let mut new_items: Vec = Vec::with_capacity(items.len()); - - for item in items.iter() { - if let Item::Const(const_item) = item { - let func = transform_const(const_item); - new_items.push(func); - } else { - new_items.push(item.clone()); - } - } - - let mod_ident = &input_mod.ident; - let vis = &input_mod.vis; - - let output = quote! { - #[allow(non_snake_case)] - #vis mod #mod_ident { - #(#new_items)* - } - }; - - output.into() -} - -/// Transforms a single `const` item into a function. -fn transform_const(const_item: &ItemConst) -> Item { - let name = &const_item.ident; - let attrs = &const_item.attrs; - - // Extract the string literal value from the const - let value_str = match &*const_item.expr { - Expr::Lit(expr_lit) => match &expr_lit.lit { - Lit::Str(lit_str) => lit_str.value(), - _ => panic!( - "#[constants] only supports `&str` literals, \ - but `{name}` has a non-string literal" - ), - }, - _ => panic!( - "#[constants] only supports literal expressions, \ - but `{name}` has a non-literal expression" - ), - }; - - let placeholders = extract_placeholders(&value_str); - - if placeholders.is_empty() { - parse_quote! { - #(#attrs)* - pub fn #name() -> String { - #value_str.to_string() - } - } - } else { - let params: Vec<_> = placeholders - .iter() - .map(|p| { - let ident = format_ident!("{p}"); - quote! { #ident: impl ::core::convert::AsRef } - }) - .collect(); - - let format_args: Vec<_> = placeholders - .iter() - .map(|p| { - let ident = format_ident!("{p}"); - quote! { #ident = #ident.as_ref() } - }) - .collect(); - - parse_quote! { - #(#attrs)* - pub fn #name(#(#params),*) -> String { - ::std::format!(#value_str, #(#format_args),*) - } - } - } -} - -/// Extracts all `{name}` placeholder identifiers from a format string. -fn extract_placeholders(s: &str) -> Vec { - let mut placeholders = Vec::new(); - let mut chars = s.char_indices().peekable(); - - while let Some((_, c)) = chars.next() { - if c == '{' { - let mut name = String::new(); - for (_, c) in &mut chars { - if c == '}' { - break; - } - name.push(c); - } - let trimmed = name.trim().to_string(); - if !trimmed.is_empty() { - placeholders.push(trimmed); - } - } - } - - placeholders -} diff --git a/rola-vcs/internal_macros/src/lib.rs b/rola-vcs/internal_macros/src/lib.rs deleted file mode 100644 index f6c3cb7..0000000 --- a/rola-vcs/internal_macros/src/lib.rs +++ /dev/null @@ -1,39 +0,0 @@ -mod constants; - -use proc_macro::TokenStream; - -/// Transforms `pub const` items in a module into equivalent functions. -/// -/// Constants without `{param}` placeholders become `fn NAME() -> String`. -/// Constants with `{param}` placeholders become `fn NAME(param: impl AsRef) -> String`, -/// using `format!()` to fill in the placeholders. -/// -/// The entire module is annotated with `#[allow(non_snake_case)]`. -/// -/// # Example -/// -/// ```ignore -/// #[rorolala_internal_macros::constants] -/// pub mod paths { -/// pub const ROLA_DRAFT_DIR: &str = ".rola"; -/// pub const ROLA_BINDED_BUCKET_FILE: &str = ".rola/BIND/{bucket}"; -/// } -/// ``` -/// -/// expands to: -/// -/// ```ignore -/// #[allow(non_snake_case)] -/// pub mod paths { -/// pub fn ROLA_DRAFT_DIR() -> String { -/// ".rola".to_string() -/// } -/// pub fn ROLA_BINDED_BUCKET_FILE(bucket: impl AsRef) -> String { -/// format!(".rola/BIND/{bucket}", bucket = bucket.as_ref()) -/// } -/// } -/// ``` -#[proc_macro_attribute] -pub fn constants(attr: TokenStream, item: TokenStream) -> TokenStream { - constants::expand(attr, item) -} diff --git a/rola-vcs/src/consts/common.rs b/rola-vcs/src/consts/common.rs deleted file mode 100644 index 4960f1b..0000000 --- a/rola-vcs/src/consts/common.rs +++ /dev/null @@ -1,7 +0,0 @@ -#[rorolala_internal_macros::constants] -mod consts { - /// Directory name for Rorolala metadata storage in Workdraft - pub const DRAFT_META_DIR: &str = ".rola"; -} - -pub use consts::*; diff --git a/rola-vcs/src/consts/mod.rs b/rola-vcs/src/consts/mod.rs deleted file mode 100644 index 8b1113b..0000000 --- a/rola-vcs/src/consts/mod.rs +++ /dev/null @@ -1,6 +0,0 @@ -//! Rorolala Constants -//! -//! This module records all constant information for Rorolala - -mod common; -pub use common::*; diff --git a/rola-vcs/src/lib.rs b/rola-vcs/src/lib.rs index 4d8f5e3..237f89c 100644 --- a/rola-vcs/src/lib.rs +++ b/rola-vcs/src/lib.rs @@ -17,5 +17,3 @@ pub mod bucket { pub mod draft { pub use rola_draft::*; } - -pub mod consts; -- cgit