summaryrefslogtreecommitdiff
path: root/rola-vcs
diff options
context:
space:
mode:
Diffstat (limited to 'rola-vcs')
-rw-r--r--rola-vcs/Cargo.toml9
-rw-r--r--rola-vcs/internal_macros/Cargo.toml14
-rw-r--r--rola-vcs/internal_macros/src/constants.rs115
-rw-r--r--rola-vcs/internal_macros/src/lib.rs39
-rw-r--r--rola-vcs/src/abstracts.rs2
-rw-r--r--rola-vcs/src/abstracts/dir_pointer.rs88
-rw-r--r--rola-vcs/src/bucket.rs18
-rw-r--r--rola-vcs/src/err.rs105
-rw-r--r--rola-vcs/src/err/io.rs10
-rw-r--r--rola-vcs/src/lib.rs24
-rw-r--r--rola-vcs/src/tools.rs1
-rw-r--r--rola-vcs/src/tools/dir_search.rs54
-rw-r--r--rola-vcs/src/workdraft.rs150
13 files changed, 629 insertions, 0 deletions
diff --git a/rola-vcs/Cargo.toml b/rola-vcs/Cargo.toml
new file mode 100644
index 0000000..6fec629
--- /dev/null
+++ b/rola-vcs/Cargo.toml
@@ -0,0 +1,9 @@
+[package]
+name = "rorolala"
+version.workspace = true
+authors.workspace = true
+license.workspace = true
+edition.workspace = true
+
+[dependencies]
+rorolala_internal_macros.workspace = true
diff --git a/rola-vcs/internal_macros/Cargo.toml b/rola-vcs/internal_macros/Cargo.toml
new file mode 100644
index 0000000..0d6f641
--- /dev/null
+++ b/rola-vcs/internal_macros/Cargo.toml
@@ -0,0 +1,14 @@
+[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
new file mode 100644
index 0000000..2e76bfe
--- /dev/null
+++ b/rola-vcs/internal_macros/src/constants.rs
@@ -0,0 +1,115 @@
+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<Item> = 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<str> }
+ })
+ .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<String> {
+ 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
new file mode 100644
index 0000000..f6c3cb7
--- /dev/null
+++ b/rola-vcs/internal_macros/src/lib.rs
@@ -0,0 +1,39 @@
+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/abstracts.rs b/rola-vcs/src/abstracts.rs
new file mode 100644
index 0000000..e707ec3
--- /dev/null
+++ b/rola-vcs/src/abstracts.rs
@@ -0,0 +1,2 @@
+mod dir_pointer;
+pub use dir_pointer::*;
diff --git a/rola-vcs/src/abstracts/dir_pointer.rs b/rola-vcs/src/abstracts/dir_pointer.rs
new file mode 100644
index 0000000..0e59960
--- /dev/null
+++ b/rola-vcs/src/abstracts/dir_pointer.rs
@@ -0,0 +1,88 @@
+use std::{
+ borrow::Borrow,
+ ops::{Deref, DerefMut},
+ path::PathBuf,
+};
+
+/// Directory Pointer Data
+pub trait DirPtrData {
+ /// Fix the given path
+ ///
+ /// Returns Some(path): use the fixed path
+ /// Returns None: path cannot be fixed, this pointer is invalid
+ #[doc(hidden)]
+ fn fix(raw_path: PathBuf) -> Option<PathBuf>;
+}
+
+#[derive(Debug, Default, Clone)]
+pub struct DirPtr<Data: DirPtrData> {
+ /// Whether the current directory pointer is valid
+ valid: bool,
+
+ /// Data for the directory pointer
+ data: Data,
+
+ /// Path to the directory
+ path: PathBuf,
+}
+
+impl<Data: DirPtrData> DirPtr<Data> {
+ /// Get a reference to the directory pointer's path
+ pub fn path_ref(&self) -> &PathBuf {
+ &self.path
+ }
+
+ /// Get the directory pointer's path
+ pub fn path(&self) -> PathBuf {
+ self.path.clone()
+ }
+
+ /// Returns whether the directory pointer is valid
+ pub fn is_valid(&self) -> bool {
+ self.valid
+ }
+}
+
+impl<Data: DirPtrData + Default> DirPtr<Data> {
+ /// Create a new directory pointer with the given path and default data
+ pub fn new(path: impl Into<PathBuf>) -> Self {
+ let path = path.into();
+ let fixed = Data::fix(path.clone());
+ Self {
+ valid: fixed.is_some(),
+ data: Data::default(),
+ path: fixed.unwrap_or(path),
+ }
+ }
+}
+
+impl<Data: DirPtrData> AsRef<DirPtr<Data>> for DirPtr<Data> {
+ fn as_ref(&self) -> &DirPtr<Data> {
+ self
+ }
+}
+
+impl<Data: DirPtrData> Deref for DirPtr<Data> {
+ type Target = Data;
+
+ fn deref(&self) -> &Self::Target {
+ &self.data
+ }
+}
+
+impl<Data: DirPtrData> DerefMut for DirPtr<Data> {
+ fn deref_mut(&mut self) -> &mut Self::Target {
+ &mut self.data
+ }
+}
+
+impl<Data: DirPtrData> Borrow<Data> for DirPtr<Data> {
+ fn borrow(&self) -> &Data {
+ &self.data
+ }
+}
+
+/// Create a new directory pointer with the given path and default data
+pub fn dir_ptr<Data: DirPtrData + Default>(path: impl Into<PathBuf>) -> DirPtr<Data> {
+ DirPtr::new(path)
+}
diff --git a/rola-vcs/src/bucket.rs b/rola-vcs/src/bucket.rs
new file mode 100644
index 0000000..40de6f8
--- /dev/null
+++ b/rola-vcs/src/bucket.rs
@@ -0,0 +1,18 @@
+//! Bucket - Rorolala Storage Unit
+
+use crate::{
+ DirPtrData, DirSearchPattern, bucket::constants::ROLA_BUCKET_CONFIG_FILE, dir_search_prev,
+};
+
+pub mod constants {
+ /// The name of the bucket config file
+ pub const ROLA_BUCKET_CONFIG_FILE: &str = "rorolala.toml";
+}
+
+pub struct Bucket;
+
+impl DirPtrData for Bucket {
+ fn fix(raw_path: std::path::PathBuf) -> Option<std::path::PathBuf> {
+ dir_search_prev(raw_path, DirSearchPattern::File(ROLA_BUCKET_CONFIG_FILE))
+ }
+}
diff --git a/rola-vcs/src/err.rs b/rola-vcs/src/err.rs
new file mode 100644
index 0000000..71e0a34
--- /dev/null
+++ b/rola-vcs/src/err.rs
@@ -0,0 +1,105 @@
+//! Error
+//!
+//! This module is used to create and log Rorolala standard errors
+
+use std::{
+ fmt::{Display, Formatter},
+ io::Error as IoError,
+};
+
+mod io;
+
+/// Rorolala standard error
+#[derive(Default)]
+pub struct RolaError {
+ pub module: RolaModule,
+ pub data: RolaErrorData,
+ pub message: String,
+}
+
+/// Rorolala module, used to locate the source of an error
+#[derive(Debug, Eq, Default)]
+#[repr(u8)]
+pub enum RolaModule {
+ #[default]
+ Empty,
+
+ Bucket,
+ Workdraft,
+}
+
+/// Error data
+#[derive(Debug, Default)]
+pub enum RolaErrorData {
+ #[default]
+ Empty,
+
+ /// IO error
+ IO(IoError),
+}
+
+impl RolaError {
+ /// Create a new RolaError
+ pub fn new(module: RolaModule, data: RolaErrorData, message: String) -> Self {
+ Self {
+ module,
+ data,
+ message,
+ }
+ }
+
+ /// Create an empty RolaError
+ pub fn empty() -> Self {
+ Self {
+ module: RolaModule::Empty,
+ data: RolaErrorData::Empty,
+ message: String::new(),
+ }
+ }
+
+ /// Set the module
+ pub fn with_module(mut self, module: RolaModule) -> Self {
+ self.module = module;
+ self
+ }
+
+ /// Set the message
+ pub fn with_message(mut self, message: String) -> Self {
+ self.message = message;
+ self
+ }
+
+ /// Set the error data
+ pub fn with_data(mut self, data: RolaErrorData) -> Self {
+ self.data = data;
+ self
+ }
+}
+
+impl Display for RolaError {
+ fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+ write!(f, "{}", self.message)
+ }
+}
+
+impl PartialEq for RolaError {
+ fn eq(&self, other: &Self) -> bool {
+ self.message == other.message && self.module == other.module
+ }
+}
+
+impl Display for RolaModule {
+ fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+ write!(f, "{:?}", self)
+ }
+}
+
+impl PartialEq for RolaModule {
+ fn eq(&self, other: &Self) -> bool {
+ matches!(
+ (self, other),
+ (RolaModule::Bucket, RolaModule::Bucket)
+ | (RolaModule::Workdraft, RolaModule::Workdraft)
+ )
+ }
+}
diff --git a/rola-vcs/src/err/io.rs b/rola-vcs/src/err/io.rs
new file mode 100644
index 0000000..8dd4c1a
--- /dev/null
+++ b/rola-vcs/src/err/io.rs
@@ -0,0 +1,10 @@
+use crate::{RolaError, RolaErrorData, RolaModule};
+
+impl From<(RolaModule, std::io::Error)> for RolaError {
+ fn from(val: (RolaModule, std::io::Error)) -> Self {
+ let (module, io_err) = val;
+ let message = io_err.to_string();
+ let data = RolaErrorData::IO(io_err);
+ RolaError::new(module, data, message)
+ }
+}
diff --git a/rola-vcs/src/lib.rs b/rola-vcs/src/lib.rs
new file mode 100644
index 0000000..7672ff9
--- /dev/null
+++ b/rola-vcs/src/lib.rs
@@ -0,0 +1,24 @@
+#![allow(dead_code)]
+
+mod bucket;
+
+mod abstracts;
+pub use abstracts::*;
+
+mod workdraft;
+pub use workdraft::*;
+
+mod tools;
+pub use tools::*;
+
+mod err;
+pub use err::*;
+
+#[doc(hidden)]
+#[macro_export]
+macro_rules! include_mod {
+ ($module:ident) => {
+ mod $module;
+ pub use $module::*;
+ };
+}
diff --git a/rola-vcs/src/tools.rs b/rola-vcs/src/tools.rs
new file mode 100644
index 0000000..43b4e33
--- /dev/null
+++ b/rola-vcs/src/tools.rs
@@ -0,0 +1 @@
+crate::include_mod!(dir_search);
diff --git a/rola-vcs/src/tools/dir_search.rs b/rola-vcs/src/tools/dir_search.rs
new file mode 100644
index 0000000..3d32c70
--- /dev/null
+++ b/rola-vcs/src/tools/dir_search.rs
@@ -0,0 +1,54 @@
+use std::path::PathBuf;
+
+pub enum DirSearchPattern<'a> {
+ File(&'a str),
+ Dir(&'a str),
+}
+
+/// Searches upward from the given path towards parent directories.
+/// If any ancestor directory contains a file or directory matching the pattern,
+/// returns that ancestor directory's path.
+pub fn dir_search_prev(path: impl Into<PathBuf>, pattern: DirSearchPattern) -> Option<PathBuf> {
+ let mut current: PathBuf = path.into();
+ // Canonicalize the path if possible to ensure absolute traversal
+ if let Ok(canonical) = current.canonicalize() {
+ current = canonical;
+ } else {
+ // If canonicalization fails (e.g. path does not exist yet),
+ // try to make it absolute using current dir
+ if current.is_relative()
+ && let Ok(cwd) = std::env::current_dir() {
+ current = cwd.join(&current);
+ }
+ }
+
+ loop {
+ // Check if the current directory exists and is a directory
+ if current.is_dir() {
+ let has_match = match &pattern {
+ DirSearchPattern::File(name) => {
+ let mut entry = current.clone();
+ entry.push(name);
+ entry.is_file()
+ }
+ DirSearchPattern::Dir(name) => {
+ let mut entry = current.clone();
+ entry.push(name);
+ entry.is_dir()
+ }
+ };
+
+ if has_match {
+ return Some(current);
+ }
+ }
+
+ // Try to go to the parent directory
+ if !current.pop() {
+ // pop() returns false when there's no parent
+ break;
+ }
+ }
+
+ None
+}
diff --git a/rola-vcs/src/workdraft.rs b/rola-vcs/src/workdraft.rs
new file mode 100644
index 0000000..45bb418
--- /dev/null
+++ b/rola-vcs/src/workdraft.rs
@@ -0,0 +1,150 @@
+//! Work Draft
+//!
+//! Work Draft is the local workspace of `Rorolala`, used to store files being modified
+
+use std::path::PathBuf;
+
+use crate::{
+ DirPtrData, DirSearchPattern, RolaError, RolaModule, dir_search_prev,
+ workdraft::constants::ROLA_DRAFT_DIR,
+};
+
+#[rorolala_internal_macros::constants]
+pub mod constants {
+ /// The name of the workdraft directory
+ pub const ROLA_DRAFT_DIR: &str = ".rola";
+
+ /// The name of the directory containing bucket bindings
+ pub const ROLA_BINDED_BUCKETS_DIR: &str = ".rola/BIND/";
+
+ /// The name of the bind file
+ pub const ROLA_BINDED_BUCKET_FILE: &str = ".rola/BIND/{bucket}";
+}
+
+/// Work Draft Pointer
+///
+/// This struct is used to point to an operable local work draft directory on disk
+#[derive(Debug, Default, Clone)]
+pub struct WorkDraft;
+
+impl DirPtrData for WorkDraft {
+ fn fix(raw_path: PathBuf) -> Option<PathBuf> {
+ let draft_dir = ROLA_DRAFT_DIR();
+ dir_search_prev(raw_path, DirSearchPattern::Dir(&draft_dir))
+ }
+}
+
+impl WorkDraft {
+ /// Creates a new work draft directory at the given path
+ pub fn create(path: PathBuf) -> Result<(), RolaError> {
+ let dir = path.join(ROLA_DRAFT_DIR());
+ std::fs::create_dir_all(dir).map_err(|e| RolaError::from((RolaModule::Workdraft, e)))?;
+ Ok(())
+ }
+}
+
+/// Module for managing workdraft bucket bindings
+pub mod bucket_bind_mgr {
+ use std::path::PathBuf;
+
+ use crate::{
+ DirPtr, RolaError, RolaModule, WorkDraft, constants::ROLA_BINDED_BUCKETS_DIR,
+ workdraft::constants::ROLA_BINDED_BUCKET_FILE,
+ };
+
+ impl DirPtr<WorkDraft> {
+ /// Returns the path to the bind file for this work draft
+ pub fn bind_bucket_path(&self, bucket: impl AsRef<str>) -> PathBuf {
+ self.path().join(ROLA_BINDED_BUCKET_FILE(bucket))
+ }
+
+ /// Returns the path to the bind file for this work draft
+ pub fn bind_bucket(&self, bucket: impl AsRef<str>) -> Result<String, RolaError> {
+ let bucket_path = self.bind_bucket_path(bucket);
+ let parent = bucket_path.parent().ok_or_else(|| -> RolaError {
+ RolaError::from((
+ RolaModule::Workdraft,
+ std::io::Error::new(
+ std::io::ErrorKind::InvalidInput,
+ format!("Invalid bucket path: {}", bucket_path.to_string_lossy()),
+ ),
+ ))
+ })?;
+
+ if !parent.exists() {
+ std::fs::create_dir_all(parent)
+ .map_err(|e| RolaError::from((RolaModule::Workdraft, e)))?;
+ }
+
+ match std::fs::read_to_string(bucket_path) {
+ Ok(str) => Ok(str),
+ Err(err) => Err(RolaError::from((RolaModule::Workdraft, err))),
+ }
+ }
+
+ /// Binds the work draft to the specified bucket
+ pub fn bind_bucket_to(&self, bucket: &str) -> Result<(), RolaError> {
+ let bucket_path = self.bind_bucket_path(bucket);
+ let parent = bucket_path.parent().ok_or_else(|| -> RolaError {
+ RolaError::from((
+ RolaModule::Workdraft,
+ std::io::Error::new(std::io::ErrorKind::InvalidInput, "invalid bucket path"),
+ ))
+ })?;
+ if !parent.exists() {
+ std::fs::create_dir_all(parent)
+ .map_err(|e| RolaError::from((RolaModule::Workdraft, e)))?;
+ }
+ std::fs::write(&bucket_path, bucket)
+ .map_err(|e| RolaError::from((RolaModule::Workdraft, e)))?;
+ Ok(())
+ }
+
+ /// Returns all bound bucket names for this work draft
+ pub fn binded_buckets(&self) -> Result<Vec<String>, RolaError> {
+ let bind_dir = self.path().join(ROLA_BINDED_BUCKETS_DIR());
+ if !bind_dir.exists() {
+ return Ok(Vec::new());
+ }
+
+ let mut buckets = Vec::new();
+ let entries = std::fs::read_dir(&bind_dir)
+ .map_err(|e| RolaError::from((RolaModule::Workdraft, e)))?;
+
+ for entry in entries {
+ let entry = entry.map_err(|e| RolaError::from((RolaModule::Workdraft, e)))?;
+ let path = entry.path();
+
+ if path.is_file() {
+ // Read the file content which contains the bucket name
+ if let Ok(content) = std::fs::read_to_string(&path) {
+ let bucket = content.trim().to_string();
+ if !bucket.is_empty() {
+ buckets.push(bucket);
+ }
+ }
+ }
+ }
+
+ Ok(buckets)
+ }
+
+ /// Removes the binding for the specified bucket
+ pub fn unbind_bucket(&self, bucket: impl AsRef<str>) -> Result<(), RolaError> {
+ let bucket_path = self.bind_bucket_path(bucket);
+ if bucket_path.exists() {
+ std::fs::remove_file(&bucket_path)
+ .map_err(|e| RolaError::from((RolaModule::Workdraft, e)))?;
+ }
+ Ok(())
+ }
+
+ /// Removes bindings for all specified buckets
+ pub fn unbind_buckets(&self, buckets: &[impl AsRef<str>]) -> Result<(), RolaError> {
+ for bucket in buckets {
+ self.unbind_bucket(bucket)?;
+ }
+ Ok(())
+ }
+ }
+}