summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author魏曹先生 <1992414357@qq.com>2026-06-18 02:47:32 +0800
committer魏曹先生 <1992414357@qq.com>2026-06-18 02:47:32 +0800
commit0b8e6e7d18abb94bd99553dc1d2b0ba5d4f265ea (patch)
tree97a7c3430d56bfcb885cbfff0b011362671dd474
parentebd46942c3fcc7939e5567a797a55198148301ea (diff)
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
-rw-r--r--Cargo.lock41
-rw-r--r--Cargo.toml16
-rw-r--r--rola-bucket/Cargo.toml5
-rw-r--r--rola-bucket/src/bucket.rs135
-rw-r--r--rola-bucket/src/bucket/init.rs1
-rw-r--r--rola-bucket/src/bucket/space.rs22
-rw-r--r--rola-bucket/src/lib.rs3
-rw-r--r--rola-utils/constants/Cargo.toml11
-rw-r--r--rola-utils/constants/src/common.rs (renamed from rola-vcs/src/consts/common.rs)2
-rw-r--r--rola-utils/constants/src/lib.rs (renamed from rola-vcs/src/consts/mod.rs)3
-rw-r--r--rola-utils/macros/src/constants.rs (renamed from rola-vcs/internal_macros/src/constants.rs)15
-rw-r--r--rola-utils/macros/src/lib.rs38
-rw-r--r--rola-utils/space-system/Cargo.toml12
-rw-r--r--rola-utils/space-system/macros/Cargo.toml (renamed from rola-vcs/internal_macros/Cargo.toml)11
-rw-r--r--rola-utils/space-system/macros/src/lib.rs9
-rw-r--r--rola-utils/space-system/macros/src/space_root_test.rs110
-rw-r--r--rola-utils/space-system/src/lib.rs5
-rw-r--r--rola-utils/space-system/src/space.rs537
-rw-r--r--rola-utils/space-system/src/space/error.rs23
-rw-r--r--rola-vcs/Cargo.toml1
-rw-r--r--rola-vcs/internal_macros/src/lib.rs39
-rw-r--r--rola-vcs/src/lib.rs2
22 files changed, 978 insertions, 63 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 37ecfc2..6347833 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -3,6 +3,12 @@
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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -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]]
@@ -84,10 +91,30 @@ dependencies = [
]
[[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<Protocol>
+where
+ Protocol: AsyncBucketTransferProtocol + Send + Sync,
+{
+ #[default]
+ Uninit,
+
+ Local {
+ extra_info: Option<Protocol::ExtraInfo>,
+ },
+ Remote {
+ extra_info: Option<Protocol::ExtraInfo>,
+ },
+}
+
+impl<Protocol: AsyncBucketTransferProtocol + Send + Sync> Bucket<Protocol> {
+ /// 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<Protocol::ExtraInfo> {
+ 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<Protocol::ExtraInfo> {
+ 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<Protocol::ExtraInfo> {
+ 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<Protocol: AsyncBucketTransferProtocol + Send + Sync> SpaceRoot for Bucket<Protocol> {
+ 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-vcs/src/consts/common.rs b/rola-utils/constants/src/common.rs
index 4960f1b..6ce6bd8 100644
--- a/rola-vcs/src/consts/common.rs
+++ b/rola-utils/constants/src/common.rs
@@ -1,4 +1,4 @@
-#[rorolala_internal_macros::constants]
+#[shared_macros::constants]
mod consts {
/// Directory name for Rorolala metadata storage in Workdraft
pub const DRAFT_META_DIR: &str = ".rola";
diff --git a/rola-vcs/src/consts/mod.rs b/rola-utils/constants/src/lib.rs
index 8b1113b..566440d 100644
--- a/rola-vcs/src/consts/mod.rs
+++ b/rola-utils/constants/src/lib.rs
@@ -2,5 +2,4 @@
//!
//! This module records all constant information for Rorolala
-mod common;
-pub use common::*;
+pub mod common;
diff --git a/rola-vcs/internal_macros/src/constants.rs b/rola-utils/macros/src/constants.rs
index 2e76bfe..e5fe668 100644
--- a/rola-vcs/internal_macros/src/constants.rs
+++ b/rola-utils/macros/src/constants.rs
@@ -35,7 +35,7 @@ pub fn expand(_attr: TokenStream, item: TokenStream) -> TokenStream {
output.into()
}
-/// Transforms a single `const` item into a function.
+/// 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;
@@ -57,12 +57,18 @@ fn transform_const(const_item: &ItemConst) -> Item {
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)*
- pub fn #name() -> String {
- #value_str.to_string()
- }
+ #[doc = #doc_comment]
+ pub const #name: &'static str = #value_str;
}
} else {
let params: Vec<_> = placeholders
@@ -83,6 +89,7 @@ fn transform_const(const_item: &ItemConst) -> Item {
parse_quote! {
#(#attrs)*
+ #[doc = #doc_comment]
pub fn #name(#(#params),*) -> String {
::std::format!(#value_str, #(#format_args),*)
}
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<str>) -> 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<str>) -> 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-vcs/internal_macros/Cargo.toml b/rola-utils/space-system/macros/Cargo.toml
index 0d6f641..84ba691 100644
--- a/rola-vcs/internal_macros/Cargo.toml
+++ b/rola-utils/space-system/macros/Cargo.toml
@@ -1,14 +1,15 @@
[package]
-name = "rorolala_internal_macros"
+name = "space-macros"
version.workspace = true
+edition.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"
+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<syn::Type>,
+}
+
+impl Parse for GenericArgs {
+ fn parse(input: ParseStream) -> syn::Result<Self> {
+ 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::<GenericArgs>().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<T: SpaceRoot> {
+ path_format_cfg: PathFormatConfig,
+
+ content: T,
+ space_dir: RwLock<Option<PathBuf>>,
+ current_dir: Option<PathBuf>,
+
+ pub(crate) override_pattern: Option<SpaceRootFindPattern>,
+}
+
+impl<T: SpaceRoot> Space<T> {
+ /// 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<Path>) -> 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<Path>, 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<PathBuf>) -> Result<PathBuf, SpaceError> {
+ // 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<PathBuf, SpaceError> {
+ 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<PathBuf, SpaceError> {
+ 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<PathBuf>) {
+ 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<PathBuf>) {
+ 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<SpaceRootFindPattern>) {
+ self.override_pattern = pattern;
+ // Clear cached space directory since pattern may have changed
+ self.update_space_dir(None);
+ }
+}
+
+impl<T: SpaceRoot> Space<T> {
+ /// 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<Path>) -> Result<PathBuf, SpaceError> {
+ 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<Path>,
+ ) -> Result<Option<PathBuf>, 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<Path>,
+ ) -> Result<PathBuf, SpaceError> {
+ 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<Path>,
+ to: impl AsRef<Path>,
+ ) -> Result<u64, SpaceError> {
+ 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<Path>) -> 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<Path>) -> 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<Path>,
+ dst: impl AsRef<Path>,
+ ) -> 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<Path>,
+ ) -> Result<std::fs::Metadata, SpaceError> {
+ 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<Path>) -> Result<Vec<u8>, 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<Path>,
+ ) -> Result<tokio::fs::ReadDir, SpaceError> {
+ 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<Path>) -> Result<PathBuf, SpaceError> {
+ 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<Path>,
+ ) -> Result<String, SpaceError> {
+ 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<Path>) -> 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<Path>) -> 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<Path>) -> 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<Path>,
+ to: impl AsRef<Path>,
+ ) -> 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<Path>,
+ 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<Path>,
+ dst: impl AsRef<Path>,
+ ) -> 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<Path>,
+ dst: impl AsRef<Path>,
+ ) -> 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<Path>,
+ dst: impl AsRef<Path>,
+ ) -> 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<Path>,
+ ) -> Result<std::fs::Metadata, SpaceError> {
+ 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<Path>) -> Result<bool, SpaceError> {
+ 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<Path>,
+ 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<Path>) -> Result<bool, SpaceError> {
+ let path = self.local_path(relative_path)?;
+ Ok(tokio::fs::try_exists(path).await?)
+ }
+}
+
+impl<T: SpaceRoot> From<T> for Space<T> {
+ fn from(content: T) -> Self {
+ Space::<T>::new(content)
+ }
+}
+
+impl<T: SpaceRoot> AsRef<T> for Space<T> {
+ fn as_ref(&self) -> &T {
+ &self.content
+ }
+}
+
+impl<T: SpaceRoot> Deref for Space<T> {
+ 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<Output = Result<(), SpaceError>> + 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<PathBuf, SpaceError> {
+ find_space_root_with(&current_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<PathBuf>,
+ pattern: &SpaceRootFindPattern,
+) -> Result<PathBuf, SpaceError> {
+ // Get the pattern used for matching
+ let match_pattern: Box<dyn Fn(&Path) -> 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/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<str>) -> 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<str>) -> 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/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;