From a60028c69ad6ab7406308fb6a6163679dd70e130 Mon Sep 17 00:00:00 2001 From: Simon Nour Date: Fri, 16 Aug 2024 22:22:45 +0300 Subject: [PATCH] Custom Path added for attribute. --- macros/src/attr/enum.rs | 5 +- macros/src/attr/struct.rs | 5 +- macros/src/lib.rs | 21 ++-- macros/src/path.rs | 244 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 262 insertions(+), 13 deletions(-) create mode 100644 macros/src/path.rs diff --git a/macros/src/attr/enum.rs b/macros/src/attr/enum.rs index 21ff59c1..37447387 100644 --- a/macros/src/attr/enum.rs +++ b/macros/src/attr/enum.rs @@ -6,6 +6,7 @@ use super::{parse_assign_from_str, parse_bound, Attr, ContainerAttr, Serde}; use crate::{ attr::{parse_assign_inflection, parse_assign_str, parse_concrete, Inflection}, utils::{parse_attrs, parse_docs}, + path::CustomPath, }; #[derive(Default)] @@ -16,7 +17,7 @@ pub struct EnumAttr { pub rename_all: Option, pub rename_all_fields: Option, pub rename: Option, - pub export_to: Option, + pub export_to: Option, pub export: bool, pub docs: String, pub concrete: HashMap, @@ -212,7 +213,7 @@ impl_parse! { "rename" => out.rename = Some(parse_assign_str(input)?), "rename_all" => out.rename_all = Some(parse_assign_inflection(input)?), "rename_all_fields" => out.rename_all_fields = Some(parse_assign_inflection(input)?), - "export_to" => out.export_to = Some(parse_assign_str(input)?), + "export_to" => out.export_to = Some(CustomPath::parse(input)?), "export" => out.export = true, "tag" => out.tag = Some(parse_assign_str(input)?), "content" => out.content = Some(parse_assign_str(input)?), diff --git a/macros/src/attr/struct.rs b/macros/src/attr/struct.rs index fc4d4cbc..b583933b 100644 --- a/macros/src/attr/struct.rs +++ b/macros/src/attr/struct.rs @@ -9,6 +9,7 @@ use super::{ use crate::{ attr::{parse_assign_str, EnumAttr, Inflection, VariantAttr}, utils::{parse_attrs, parse_docs}, + path::CustomPath, }; #[derive(Default, Clone)] @@ -18,7 +19,7 @@ pub struct StructAttr { pub type_override: Option, pub rename_all: Option, pub rename: Option, - pub export_to: Option, + pub export_to: Option, pub export: bool, pub tag: Option, pub docs: String, @@ -149,7 +150,7 @@ impl_parse! { "rename_all" => out.rename_all = Some(parse_assign_inflection(input)?), "tag" => out.tag = Some(parse_assign_str(input)?), "export" => out.export = true, - "export_to" => out.export_to = Some(parse_assign_str(input)?), + "export_to" => out.export_to = Some(CustomPath::parse(input)?), "concrete" => out.concrete = parse_concrete(input)?, "bound" => out.bound = Some(parse_bound(input)?), } diff --git a/macros/src/lib.rs b/macros/src/lib.rs index 4be1ce53..1673b359 100644 --- a/macros/src/lib.rs +++ b/macros/src/lib.rs @@ -11,13 +11,14 @@ use syn::{ WhereClause, WherePredicate, }; -use crate::{deps::Dependencies, utils::format_generics}; +use crate::{deps::Dependencies, utils::format_generics, path::CustomPath}; #[macro_use] mod utils; mod attr; mod deps; mod types; +mod path; struct DerivedTS { crate_rename: Path, @@ -30,7 +31,7 @@ struct DerivedTS { bound: Option>, export: bool, - export_to: Option, + export_to: Option, } impl DerivedTS { @@ -38,18 +39,20 @@ impl DerivedTS { let export = self .export .then(|| self.generate_export_test(&rust_ty, &generics)); - + let output_path_fn = { - let path = match self.export_to.as_deref() { - Some(dirname) if dirname.ends_with('/') => { - format!("{}{}.ts", dirname, self.ts_name) - } - Some(filename) => filename.to_owned(), - None => format!("{}.ts", self.ts_name), + let (path,path_decl) = + + if let Some(cust_path) = &self.export_to{ + cust_path.get_path_and_some_decl(&self.ts_name) + } else { + let path = format!("{}.ts", self.ts_name); + (quote!( #path ), None) }; quote! { fn output_path() -> Option<&'static std::path::Path> { + #path_decl Some(std::path::Path::new(#path)) } } diff --git a/macros/src/path.rs b/macros/src/path.rs new file mode 100644 index 00000000..556519eb --- /dev/null +++ b/macros/src/path.rs @@ -0,0 +1,244 @@ +use quote::{format_ident, quote}; +use proc_macro2::TokenStream; +use syn::{ + parse::{Parse, ParseStream}, + Error, Lit, Ident,LitStr, Token, Result, +}; + + +#[derive(Clone,Debug)] +pub enum CustomPath{ + Str(String), + Static(syn::Path), + Fn(syn::Path), + Env(syn::LitStr), +} + +type FnOutputPathBody = ( TokenStream, Option ); + +impl CustomPath { + + pub fn get_path_and_some_decl(&self, ts_name: &String ) -> FnOutputPathBody { + + match self { + + Self::Str(input) => { Self::str_path(input,ts_name) }, + + Self::Static(input) => { Self::static_path(input,ts_name) }, + + Self::Fn(input) => { Self::fn_path(input,ts_name) }, + + Self::Env(input) => { Self::env_path(input,ts_name) }, + + } + } + + fn str_path( input: &String, ts_name: &String ) -> FnOutputPathBody { + + let path = + if input.ends_with('/') { + format!("{}{}.ts", input, ts_name) + } else { + input.to_owned() + }; + + return (quote!(#path),None); + } + + fn static_path( input: &syn::Path, ts_name: &String ) -> FnOutputPathBody { + + let path_ident = format_ident!("path"); + let stat_path_ident = format_ident!("PATH"); + let path_decl = quote! { + + static #stat_path_ident: std::sync::OnceLock = std::sync::OnceLock::new(); + + let #path_ident = #stat_path_ident.get_or_init( || + { + if #input.ends_with('/') { + format!("{}{}.ts", #input, #ts_name) + } else { + format!("{}",#input) + } + } + ); + }; + + ( quote!(#path_ident), Some(path_decl) ) + } + + fn fn_path( input: &syn::Path, ts_name: &String ) -> FnOutputPathBody { + + ( quote!{#input (#ts_name)?}, None) + } + + fn env_path( input: &LitStr, ts_name: &String ) -> FnOutputPathBody { + + let path_ident = format_ident!("path"); + + let path_decl = quote!{ + + let #path_ident = if std::env!(#input).ends_with('/') { + std::concat!(std::env!(#input),#ts_name,".ts") + } else { + std::env!(#input) + }; + }; + + ( quote!(#path_ident), Some(path_decl) ) + } + +} + +impl Parse for CustomPath { + + fn parse(input: ParseStream) -> Result { + input.parse::()?; + let span = input.span(); + + let msg = +"expected arguments for 'export_to': + +1) string literal + #[ts(export_to = \"my/path\")] + +2) static or constant variable name + + #[ts(export_to = MY_STATIC_PATH)] + #[ts(export_to = crate::MY_STATIC_PATH)] + +Note: This option is available for Rust 1.7.0 and higher! + +3) function name of a `Fn(&'static str) -> Option<&'static str>` + + #[ts(export_to = get_path)] + #[ts(export_to = crate::get_path)] + +Note: This option overrides the original `TS::output_path` logic`! + +4) environment variable name + + #[ts(export_to = env(\"MY_ENV_VAR_PATH\"))] + +Note: This option is for environment variables defined in the '.cargo/config.toml' file only, accessible through the `env!` macro! +"; + let get_path = |input: ParseStream| -> Result<(syn::Path,Option)>{ + let mut tokens = TokenStream::new(); + let mut env_var_str = None; + + if input.peek(Token![self]) { + let token = input.parse::()?; + tokens.extend(quote!(#token)); + } + if input.peek(Token![super]) { + let token = input.parse::()?; + tokens.extend(quote!(#token)); + } + if input.peek(Token![crate]) { + let token = input.parse::()?; + tokens.extend(quote!(#token)); + } + if input.peek(Ident) { + let ident = input.parse::()?; + tokens.extend(quote!(#ident)); + } + + while input.peek(Token![::]) { + let token = input.parse::()?; + tokens.extend(quote!(#token)); + + if input.peek(Ident){ + let ident = input.parse::()?; + tokens.extend(quote!(#ident)); + } else { return Err(Error::new(input.span(),"expected ident")) } + } + + if input.peek(syn::token::Paren){ + let content; + syn::parenthesized!(content in input); + env_var_str = Some(content.parse::()?); + } + + Ok((syn::parse2::(tokens)?,env_var_str)) + }; + + + // string literal + if input.peek(LitStr){ + if let Ok(lit) = Lit::parse(input){ + match lit { + Lit::Str(string) => { return Ok(CustomPath::Str(string.value())); }, + _ => { return Err(Error::new(span, msg)); }, + } + } + } + + match get_path(input) { + + Ok((path,arg)) => { + + if !path.segments.is_empty(){ + + if let Some( env_var_str ) = arg { + + if path.is_ident("env") { + return Ok(CustomPath::Env(env_var_str)); + } + + } else { + + let last = &path.segments.last().unwrap().ident; + + // static or const + if is_screaming_snake_case(&last.to_string()) { + return Ok(CustomPath::Static(path)); + } + + // function + if is_snake_case(&last.to_string()) { + return Ok(CustomPath::Fn(path)); + } + } + } + return Err(Error::new(span, msg)); + }, + + Err(e) => return Err(Error::new(e.span(), msg)), + } + } +} + + +// These functions mimic Rust's naming conventions for +// statics, constants, and function . +// To be replaced with proper, more robust validation. + +fn is_screaming_snake_case(s: &str) -> bool { + + if s.is_empty() || s.starts_with('_') || s.ends_with('_') || s.contains("__") { + return false; + } + + for c in s.chars() { + if !c.is_ascii_uppercase() && c != '_' { + return false; + } + } + + true +} + +fn is_snake_case(s: &str) -> bool { + + if s.is_empty() || s.starts_with('_') { + return false; + } + + for c in s.chars() { + if !c.is_ascii_lowercase() && c != '_' { + return false; + } + } + + true +} \ No newline at end of file