summaryrefslogtreecommitdiff
path: root/systems/sheet/macros
diff options
context:
space:
mode:
Diffstat (limited to 'systems/sheet/macros')
-rw-r--r--systems/sheet/macros/Cargo.toml12
-rw-r--r--systems/sheet/macros/src/lib.rs374
2 files changed, 386 insertions, 0 deletions
diff --git a/systems/sheet/macros/Cargo.toml b/systems/sheet/macros/Cargo.toml
new file mode 100644
index 0000000..d7a59d1
--- /dev/null
+++ b/systems/sheet/macros/Cargo.toml
@@ -0,0 +1,12 @@
+[package]
+name = "sheet_system_macros"
+version.workspace = true
+edition = "2024"
+
+[lib]
+proc-macro = true
+
+[dependencies]
+syn = { version = "2.0", features = ["full", "extra-traits"] }
+quote = "1.0"
+proc-macro2 = "1.0"
diff --git a/systems/sheet/macros/src/lib.rs b/systems/sheet/macros/src/lib.rs
new file mode 100644
index 0000000..c0e936c
--- /dev/null
+++ b/systems/sheet/macros/src/lib.rs
@@ -0,0 +1,374 @@
+use proc_macro::TokenStream;
+use proc_macro2::{Span, TokenStream as TokenStream2};
+use quote::quote;
+use syn::parse_str;
+
+const LOCAL_MAPPING_PATH: &str = "just_enough_vcs::system::sheet_system::mapping::LocalMapping";
+const MAPPING_BUF_PATH: &str = "just_enough_vcs::system::sheet_system::mapping::MappingBuf";
+const MAPPING_PATH: &str = "just_enough_vcs::system::sheet_system::mapping::Mapping";
+const LOCAL_MAPPING_FORWARD_PATH: &str =
+ "just_enough_vcs::system::sheet_system::mapping::LocalMappingForward";
+
+/// Parse strings in the format "sheet:/path"
+fn parse_sheet_path(input: &str) -> Result<(String, Vec<String>), String> {
+ let parts: Vec<&str> = input.split(":/").collect();
+ if parts.len() != 2 {
+ return Err(format!(
+ "Invalid sheet path syntax. Expected: sheet:/path, got: {}",
+ input
+ ));
+ }
+
+ let sheet = parts[0].to_string();
+ let path = parts[1];
+
+ if path.is_empty() {
+ return Err("Path cannot be empty".to_string());
+ }
+
+ let path_parts: Vec<String> = path.split('/').map(|s| s.to_string()).collect();
+
+ Ok((sheet, path_parts))
+}
+
+/// Parse strings in the format "id/ver"
+fn parse_id_version(input: &str) -> Result<(String, String), String> {
+ let parts: Vec<&str> = input.split('/').collect();
+ if parts.len() != 2 {
+ return Err(format!(
+ "Invalid id/version syntax. Expected: id/ver, got: {}",
+ input
+ ));
+ }
+
+ let id = parts[0].trim().to_string();
+ let ver = parts[1].trim().to_string();
+
+ if id.is_empty() {
+ return Err("ID cannot be empty".to_string());
+ }
+ if ver.is_empty() {
+ return Err("Version cannot be empty".to_string());
+ }
+
+ Ok((id, ver))
+}
+
+/// Parse a path string into a vector of strings
+fn parse_path_string(input: &str) -> Vec<String> {
+ input.split('/').map(|s| s.trim().to_string()).collect()
+}
+
+/// Generate token stream for path vector
+fn path_vec_to_tokens(path_vec: &[String]) -> TokenStream2 {
+ let path_items: Vec<_> = path_vec.iter().map(|s| quote! { #s.to_string() }).collect();
+
+ quote! { vec![#(#path_items),*] }
+}
+
+/// Create a MappingBuf
+///
+/// Use the following syntax to create a MappingBuf
+/// ```ignore
+/// let mapping_buf = mapping_buf!(
+/// // Map the `version` of index `index_id`
+/// // to `your_dir/your_file.suffix` in `your_sheet`
+/// "your_sheet:/your_dir/your_file.suffix" => "index_id/version"
+/// );
+/// ```
+#[proc_macro]
+pub fn mapping_buf(input: TokenStream) -> TokenStream {
+ let input_str = input.to_string();
+ let parts: Vec<&str> = input_str.split("=>").collect();
+
+ if parts.len() != 2 {
+ return syn::Error::new(
+ Span::call_site(),
+ "Invalid mapping_buf syntax. Expected: mapping_buf!(\"sheet:/path\" => \"id/ver\")",
+ )
+ .to_compile_error()
+ .into();
+ }
+
+ let left = parts[0].trim().trim_matches('"').trim();
+ let right = parts[1].trim().trim_matches('"').trim();
+
+ let (sheet, path_vec) = match parse_sheet_path(left) {
+ Ok(result) => result,
+ Err(err) => {
+ return syn::Error::new(Span::call_site(), err)
+ .to_compile_error()
+ .into();
+ }
+ };
+
+ let (id, ver) = match parse_id_version(right) {
+ Ok(result) => result,
+ Err(err) => {
+ return syn::Error::new(Span::call_site(), err)
+ .to_compile_error()
+ .into();
+ }
+ };
+
+ let path_vec_tokens = path_vec_to_tokens(&path_vec);
+
+ let mapping_buf_path: syn::Path =
+ parse_str(MAPPING_BUF_PATH).expect("Failed to parse MAPPING_BUF_PATH");
+
+ let expanded = quote! {
+ #mapping_buf_path::new(
+ #sheet.to_string(),
+ #path_vec_tokens,
+ #id.to_string(),
+ #ver.to_string()
+ )
+ };
+
+ expanded.into()
+}
+
+/// Create a Mapping
+///
+/// Use the following syntax to create a Mapping
+/// ```ignore
+/// let mapping = mapping!(
+/// // Map the `version` of index `index_id`
+/// // to `your_dir/your_file.suffix` in `your_sheet`
+/// "your_sheet:/your_dir/your_file.suffix" => "index_id/version"
+/// );
+/// ```
+#[proc_macro]
+pub fn mapping(input: TokenStream) -> TokenStream {
+ let input_str = input.to_string();
+ let parts: Vec<&str> = input_str.split("=>").collect();
+
+ if parts.len() != 2 {
+ return syn::Error::new(
+ Span::call_site(),
+ "Invalid mapping syntax. Expected: mapping!(\"sheet:/path\" => \"id/ver\")",
+ )
+ .to_compile_error()
+ .into();
+ }
+
+ let left = parts[0].trim().trim_matches('"').trim();
+ let right = parts[1].trim().trim_matches('"').trim();
+
+ let (sheet, path_vec) = match parse_sheet_path(left) {
+ Ok(result) => result,
+ Err(err) => {
+ return syn::Error::new(Span::call_site(), err)
+ .to_compile_error()
+ .into();
+ }
+ };
+
+ let (id, ver) = match parse_id_version(right) {
+ Ok(result) => result,
+ Err(err) => {
+ return syn::Error::new(Span::call_site(), err)
+ .to_compile_error()
+ .into();
+ }
+ };
+
+ let path = path_vec.join("/");
+
+ let mapping_path: syn::Path = parse_str(MAPPING_PATH).expect("Failed to parse MAPPING_PATH");
+
+ let expanded = quote! {
+ #mapping_path::new(
+ #sheet,
+ #path,
+ #id,
+ #ver
+ )
+ };
+
+ expanded.into()
+}
+
+enum LocalMappingParts {
+ Latest(String, String, String),
+ Version(String, String, String),
+ WithRef(String, String, String, String),
+}
+
+impl LocalMappingParts {
+ fn parse(input: TokenStream) -> Result<Self, syn::Error> {
+ let input_str = input.to_string();
+
+ // LocalMapping does not have a sheet_name definition
+ // So when the user specifies a sheet prefix, an error should be reported
+ if input_str.contains(":/") {
+ return Err(syn::Error::new(
+ Span::call_site(),
+ "local_mapping is not related to sheets. Do not use 'sheet:/' prefix.",
+ ));
+ }
+
+ // When both "==" and "=>" appear
+ // It's impossible to determine whether to match the current version or point to a Ref
+ // Should report an error
+ if input_str.contains("==") && input_str.contains("=>") {
+ return Err(syn::Error::new(
+ Span::call_site(),
+ "Ambiguous forward direction. Use either '==' for version or '=>' for ref, not both.",
+ ));
+ }
+
+ if input_str.contains("==") {
+ let parts: Vec<&str> = input_str.split("==").collect();
+ if parts.len() != 2 {
+ return Err(syn::Error::new(
+ Span::call_site(),
+ "Invalid local_mapping syntax with '=='. Expected: local_mapping!(\"path\" == \"id/ver\")",
+ ));
+ }
+
+ let left = parts[0].trim().trim_matches('"').trim();
+ let right = parts[1].trim().trim_matches('"').trim();
+
+ let (id, ver) =
+ parse_id_version(right).map_err(|err| syn::Error::new(Span::call_site(), err))?;
+
+ return Ok(LocalMappingParts::Version(left.to_string(), id, ver));
+ }
+
+ let parts: Vec<&str> = input_str.split("=>").collect();
+
+ match parts.len() {
+ 2 => {
+ // local_mapping!("path" => "id/ver") - Latest
+ let left = parts[0].trim().trim_matches('"').trim();
+ let right = parts[1].trim().trim_matches('"').trim();
+
+ let (id, ver) = parse_id_version(right)
+ .map_err(|err| syn::Error::new(Span::call_site(), err))?;
+
+ Ok(LocalMappingParts::Latest(left.to_string(), id, ver))
+ }
+ 3 => {
+ // local_mapping!("path" => "id/ver" => "ref") - Ref
+ let left = parts[0].trim().trim_matches('"').trim();
+ let middle = parts[1].trim().trim_matches('"').trim();
+ let right = parts[2].trim().trim_matches('"').trim();
+
+ let (id, ver) = parse_id_version(middle)
+ .map_err(|err| syn::Error::new(Span::call_site(), err))?;
+
+ Ok(LocalMappingParts::WithRef(
+ left.to_string(),
+ id,
+ ver,
+ right.to_string(),
+ ))
+ }
+ _ => Err(syn::Error::new(
+ Span::call_site(),
+ "Invalid local_mapping syntax. Expected: local_mapping!(\"path\" => \"id/ver\") or local_mapping!(\"path\" == \"id/ver\") or local_mapping!(\"path\" => \"id/ver\" => \"ref\")",
+ )),
+ }
+ }
+}
+
+/// Create a LocalMapping
+///
+/// Use the following syntax to create a LocalMapping
+/// ```ignore
+/// let lcoal_mapping_to_latest = local_mapping!(
+/// // Map the `version` of index `index_id`
+/// // to `your_dir/your_file.suffix`
+/// // and expects to keep the latest version
+/// "your_dir/your_file.suffix" => "index_id/version"
+/// );
+///
+/// let lcoal_mapping_to_version = local_mapping!(
+/// // Map the `version` of index `index_id`
+/// // to `your_dir/your_file.suffix`
+/// // and expects to keep the current version
+/// "your_dir/your_file.suffix" == "index_id/version"
+/// );
+///
+/// let lcoal_mapping_latest = local_mapping!(
+/// // Map the `version` of index `index_id`
+/// // to `your_dir/your_file.suffix`
+/// // and expects to match the version declared in `ref`
+/// "your_dir/your_file.suffix" => "index_id/version" => "ref"
+/// );
+/// ```
+#[proc_macro]
+pub fn local_mapping(input: TokenStream) -> TokenStream {
+ let parts = match LocalMappingParts::parse(input) {
+ Ok(parts) => parts,
+ Err(err) => return err.to_compile_error().into(),
+ };
+
+ match parts {
+ LocalMappingParts::Latest(path_str, id, ver) => {
+ let path_vec = parse_path_string(&path_str);
+ let path_vec_tokens = path_vec_to_tokens(&path_vec);
+
+ let local_mapping_path: syn::Path =
+ parse_str(LOCAL_MAPPING_PATH).expect("Failed to parse LOCAL_MAPPING_PATH");
+ let local_mapping_forward_path: syn::Path = parse_str(LOCAL_MAPPING_FORWARD_PATH)
+ .expect("Failed to parse LOCAL_MAPPING_FORWARD_PATH");
+
+ let expanded = quote! {
+ #local_mapping_path::new(
+ #path_vec_tokens,
+ #id.to_string(),
+ #ver.to_string(),
+ #local_mapping_forward_path::Latest
+ )
+ };
+
+ expanded.into()
+ }
+ LocalMappingParts::Version(path_str, id, ver) => {
+ let path_vec = parse_path_string(&path_str);
+ let path_vec_tokens = path_vec_to_tokens(&path_vec);
+
+ let local_mapping_path: syn::Path =
+ parse_str(LOCAL_MAPPING_PATH).expect("Failed to parse LOCAL_MAPPING_PATH");
+ let local_mapping_forward_path: syn::Path = parse_str(LOCAL_MAPPING_FORWARD_PATH)
+ .expect("Failed to parse LOCAL_MAPPING_FORWARD_PATH");
+
+ let expanded = quote! {
+ #local_mapping_path::new(
+ #path_vec_tokens,
+ #id.to_string(),
+ #ver.to_string(),
+ #local_mapping_forward_path::Version {
+ version_name: #ver.to_string()
+ }
+ )
+ };
+
+ expanded.into()
+ }
+ LocalMappingParts::WithRef(path_str, id, ver, ref_name) => {
+ let path_vec = parse_path_string(&path_str);
+ let path_vec_tokens = path_vec_to_tokens(&path_vec);
+
+ let local_mapping_path: syn::Path =
+ parse_str(LOCAL_MAPPING_PATH).expect("Failed to parse LOCAL_MAPPING_PATH");
+ let local_mapping_forward_path: syn::Path = parse_str(LOCAL_MAPPING_FORWARD_PATH)
+ .expect("Failed to parse LOCAL_MAPPING_FORWARD_PATH");
+
+ let expanded = quote! {
+ #local_mapping_path::new(
+ #path_vec_tokens,
+ #id.to_string(),
+ #ver.to_string(),
+ #local_mapping_forward_path::Ref {
+ sheet_name: #ref_name.to_string()
+ }
+ )
+ };
+
+ expanded.into()
+ }
+ }
+}