From a60028c69ad6ab7406308fb6a6163679dd70e130 Mon Sep 17 00:00:00 2001 From: Simon Nour Date: Fri, 16 Aug 2024 22:22:45 +0300 Subject: [PATCH 1/3] 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 From 38296957f1416fac237a43e1583a9027564193d8 Mon Sep 17 00:00:00 2001 From: Simon Nour Date: Sun, 1 Sep 2024 22:26:26 +0300 Subject: [PATCH 2/3] the reminder for ? notation, version typo, check for function pointer --- macros/src/attr/enum.rs | 2 +- macros/src/attr/struct.rs | 348 +++++++++++++++++++------------------- macros/src/lib.rs | 10 +- macros/src/path.rs | 272 +++++++++++++++-------------- 4 files changed, 327 insertions(+), 305 deletions(-) diff --git a/macros/src/attr/enum.rs b/macros/src/attr/enum.rs index 37447387..2019eda0 100644 --- a/macros/src/attr/enum.rs +++ b/macros/src/attr/enum.rs @@ -5,8 +5,8 @@ use syn::{parse_quote, Attribute, Ident, ItemEnum, Path, Result, Type, WherePred 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, + utils::{parse_attrs, parse_docs}, }; #[derive(Default)] diff --git a/macros/src/attr/struct.rs b/macros/src/attr/struct.rs index b583933b..43cba30f 100644 --- a/macros/src/attr/struct.rs +++ b/macros/src/attr/struct.rs @@ -1,174 +1,174 @@ -use std::collections::HashMap; - -use syn::{parse_quote, Attribute, Fields, Ident, Path, Result, Type, WherePredicate}; - -use super::{ - parse_assign_from_str, parse_assign_inflection, parse_bound, parse_concrete, Attr, - ContainerAttr, Serde, Tagged, -}; -use crate::{ - attr::{parse_assign_str, EnumAttr, Inflection, VariantAttr}, - utils::{parse_attrs, parse_docs}, - path::CustomPath, -}; - -#[derive(Default, Clone)] -pub struct StructAttr { - crate_rename: Option, - pub type_as: Option, - pub type_override: Option, - pub rename_all: Option, - pub rename: Option, - pub export_to: Option, - pub export: bool, - pub tag: Option, - pub docs: String, - pub concrete: HashMap, - pub bound: Option>, -} - -impl StructAttr { - pub fn from_attrs(attrs: &[Attribute]) -> Result { - let mut result = parse_attrs::(attrs)?; - - if cfg!(feature = "serde-compat") { - let serde_attr = crate::utils::parse_serde_attrs::(attrs); - result = result.merge(serde_attr.0); - } - - let docs = parse_docs(attrs)?; - result.docs = docs; - - Ok(result) - } - - pub fn from_variant( - enum_attr: &EnumAttr, - variant_attr: &VariantAttr, - variant_fields: &Fields, - ) -> Self { - Self { - crate_rename: Some(enum_attr.crate_rename()), - rename: variant_attr.rename.clone(), - rename_all: variant_attr.rename_all.or(match variant_fields { - Fields::Named(_) => enum_attr.rename_all_fields, - Fields::Unnamed(_) | Fields::Unit => None, - }), - tag: match variant_fields { - Fields::Named(_) => match enum_attr - .tagged() - .expect("The variant attribute is known to be valid at this point") - { - Tagged::Internally { tag } => Some(tag.to_owned()), - _ => None, - }, - _ => None, - }, - - // inline and skip are not supported on StructAttr - ..Self::default() - } - } -} - -impl Attr for StructAttr { - type Item = Fields; - - fn merge(self, other: Self) -> Self { - Self { - crate_rename: self.crate_rename.or(other.crate_rename), - type_as: self.type_as.or(other.type_as), - type_override: self.type_override.or(other.type_override), - rename: self.rename.or(other.rename), - rename_all: self.rename_all.or(other.rename_all), - export_to: self.export_to.or(other.export_to), - export: self.export || other.export, - tag: self.tag.or(other.tag), - docs: other.docs, - concrete: self.concrete.into_iter().chain(other.concrete).collect(), - bound: match (self.bound, other.bound) { - (Some(a), Some(b)) => Some(a.into_iter().chain(b).collect()), - (Some(bound), None) | (None, Some(bound)) => Some(bound), - (None, None) => None, - }, - } - } - - fn assert_validity(&self, item: &Self::Item) -> Result<()> { - if self.type_override.is_some() { - if self.type_as.is_some() { - syn_err!("`as` is not compatible with `type`"); - } - - if self.rename_all.is_some() { - syn_err!("`rename_all` is not compatible with `type`"); - } - - if self.tag.is_some() { - syn_err!("`tag` is not compatible with `type`"); - } - } - - if self.type_as.is_some() { - if self.tag.is_some() { - syn_err!("`tag` is not compatible with `as`"); - } - - if self.rename_all.is_some() { - syn_err!("`rename_all` is not compatible with `as`"); - } - } - - if !matches!(item, Fields::Named(_)) { - if self.tag.is_some() { - syn_err!("`tag` cannot be used with unit or tuple structs"); - } - - if self.rename_all.is_some() { - syn_err!("`rename_all` cannot be used with unit or tuple structs"); - } - } - - Ok(()) - } -} - -impl ContainerAttr for StructAttr { - fn crate_rename(&self) -> Path { - self.crate_rename - .clone() - .unwrap_or_else(|| parse_quote!(::ts_rs)) - } -} - -impl_parse! { - StructAttr(input, out) { - "crate" => out.crate_rename = Some(parse_assign_from_str(input)?), - "as" => out.type_as = Some(parse_assign_from_str(input)?), - "type" => out.type_override = Some(parse_assign_str(input)?), - "rename" => out.rename = Some(parse_assign_str(input)?), - "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(CustomPath::parse(input)?), - "concrete" => out.concrete = parse_concrete(input)?, - "bound" => out.bound = Some(parse_bound(input)?), - } -} - -impl_parse! { - Serde(input, out) { - "rename" => out.0.rename = Some(parse_assign_str(input)?), - "rename_all" => out.0.rename_all = Some(parse_assign_inflection(input)?), - "tag" => out.0.tag = Some(parse_assign_str(input)?), - "bound" => out.0.bound = Some(parse_bound(input)?), - // parse #[serde(default)] to not emit a warning - "deny_unknown_fields" | "default" => { - use syn::Token; - if input.peek(Token![=]) { - input.parse::()?; - parse_assign_str(input)?; - } - }, - } -} +use std::collections::HashMap; + +use syn::{parse_quote, Attribute, Fields, Ident, Path, Result, Type, WherePredicate}; + +use super::{ + parse_assign_from_str, parse_assign_inflection, parse_bound, parse_concrete, Attr, + ContainerAttr, Serde, Tagged, +}; +use crate::{ + attr::{parse_assign_str, EnumAttr, Inflection, VariantAttr}, + path::CustomPath, + utils::{parse_attrs, parse_docs}, +}; + +#[derive(Default, Clone)] +pub struct StructAttr { + crate_rename: Option, + pub type_as: Option, + pub type_override: Option, + pub rename_all: Option, + pub rename: Option, + pub export_to: Option, + pub export: bool, + pub tag: Option, + pub docs: String, + pub concrete: HashMap, + pub bound: Option>, +} + +impl StructAttr { + pub fn from_attrs(attrs: &[Attribute]) -> Result { + let mut result = parse_attrs::(attrs)?; + + if cfg!(feature = "serde-compat") { + let serde_attr = crate::utils::parse_serde_attrs::(attrs); + result = result.merge(serde_attr.0); + } + + let docs = parse_docs(attrs)?; + result.docs = docs; + + Ok(result) + } + + pub fn from_variant( + enum_attr: &EnumAttr, + variant_attr: &VariantAttr, + variant_fields: &Fields, + ) -> Self { + Self { + crate_rename: Some(enum_attr.crate_rename()), + rename: variant_attr.rename.clone(), + rename_all: variant_attr.rename_all.or(match variant_fields { + Fields::Named(_) => enum_attr.rename_all_fields, + Fields::Unnamed(_) | Fields::Unit => None, + }), + tag: match variant_fields { + Fields::Named(_) => match enum_attr + .tagged() + .expect("The variant attribute is known to be valid at this point") + { + Tagged::Internally { tag } => Some(tag.to_owned()), + _ => None, + }, + _ => None, + }, + + // inline and skip are not supported on StructAttr + ..Self::default() + } + } +} + +impl Attr for StructAttr { + type Item = Fields; + + fn merge(self, other: Self) -> Self { + Self { + crate_rename: self.crate_rename.or(other.crate_rename), + type_as: self.type_as.or(other.type_as), + type_override: self.type_override.or(other.type_override), + rename: self.rename.or(other.rename), + rename_all: self.rename_all.or(other.rename_all), + export_to: self.export_to.or(other.export_to), + export: self.export || other.export, + tag: self.tag.or(other.tag), + docs: other.docs, + concrete: self.concrete.into_iter().chain(other.concrete).collect(), + bound: match (self.bound, other.bound) { + (Some(a), Some(b)) => Some(a.into_iter().chain(b).collect()), + (Some(bound), None) | (None, Some(bound)) => Some(bound), + (None, None) => None, + }, + } + } + + fn assert_validity(&self, item: &Self::Item) -> Result<()> { + if self.type_override.is_some() { + if self.type_as.is_some() { + syn_err!("`as` is not compatible with `type`"); + } + + if self.rename_all.is_some() { + syn_err!("`rename_all` is not compatible with `type`"); + } + + if self.tag.is_some() { + syn_err!("`tag` is not compatible with `type`"); + } + } + + if self.type_as.is_some() { + if self.tag.is_some() { + syn_err!("`tag` is not compatible with `as`"); + } + + if self.rename_all.is_some() { + syn_err!("`rename_all` is not compatible with `as`"); + } + } + + if !matches!(item, Fields::Named(_)) { + if self.tag.is_some() { + syn_err!("`tag` cannot be used with unit or tuple structs"); + } + + if self.rename_all.is_some() { + syn_err!("`rename_all` cannot be used with unit or tuple structs"); + } + } + + Ok(()) + } +} + +impl ContainerAttr for StructAttr { + fn crate_rename(&self) -> Path { + self.crate_rename + .clone() + .unwrap_or_else(|| parse_quote!(::ts_rs)) + } +} + +impl_parse! { + StructAttr(input, out) { + "crate" => out.crate_rename = Some(parse_assign_from_str(input)?), + "as" => out.type_as = Some(parse_assign_from_str(input)?), + "type" => out.type_override = Some(parse_assign_str(input)?), + "rename" => out.rename = Some(parse_assign_str(input)?), + "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(CustomPath::parse(input)?), + "concrete" => out.concrete = parse_concrete(input)?, + "bound" => out.bound = Some(parse_bound(input)?), + } +} + +impl_parse! { + Serde(input, out) { + "rename" => out.0.rename = Some(parse_assign_str(input)?), + "rename_all" => out.0.rename_all = Some(parse_assign_inflection(input)?), + "tag" => out.0.tag = Some(parse_assign_str(input)?), + "bound" => out.0.bound = Some(parse_bound(input)?), + // parse #[serde(default)] to not emit a warning + "deny_unknown_fields" | "default" => { + use syn::Token; + if input.peek(Token![=]) { + input.parse::()?; + parse_assign_str(input)?; + } + }, + } +} diff --git a/macros/src/lib.rs b/macros/src/lib.rs index 1673b359..f359d88a 100644 --- a/macros/src/lib.rs +++ b/macros/src/lib.rs @@ -11,14 +11,14 @@ use syn::{ WhereClause, WherePredicate, }; -use crate::{deps::Dependencies, utils::format_generics, path::CustomPath}; +use crate::{deps::Dependencies, path::CustomPath, utils::format_generics}; #[macro_use] mod utils; mod attr; mod deps; -mod types; mod path; +mod types; struct DerivedTS { crate_rename: Path, @@ -39,11 +39,9 @@ impl DerivedTS { let export = self .export .then(|| self.generate_export_test(&rust_ty, &generics)); - - let output_path_fn = { - let (path,path_decl) = - if let Some(cust_path) = &self.export_to{ + let output_path_fn = { + 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); diff --git a/macros/src/path.rs b/macros/src/path.rs index 556519eb..d4049523 100644 --- a/macros/src/path.rs +++ b/macros/src/path.rs @@ -1,58 +1,104 @@ -use quote::{format_ident, quote}; use proc_macro2::TokenStream; +use quote::{format_ident, quote}; use syn::{ parse::{Parse, ParseStream}, - Error, Lit, Ident,LitStr, Token, Result, + Error, Ident, Lit, LitStr, Result, Token, }; -#[derive(Clone,Debug)] -pub enum CustomPath{ +const PATH_CONVENTION_MSG: &'static str = r###" +Remider on `TS` trait export path convention. + +#[ts(export)] + + Generates a test which will export the type, by default to bindings/.ts when running cargo test. + The default base directory can be overridden with the `TS_RS_EXPORT_DIR` environment variable. Adding the variable to a project's config.toml can make it easier to manage. + + +# /.cargo/config.toml +[env] +TS_RS_EXPORT_DIR = { value = "", relative = true } + + +#[ts(export_to = "..")] + + Specifies where the type should be exported to. Defaults to .ts. + The path given to the export_to attribute is relative to the `TS_RS_EXPORT_DIR` environment variable, or, if `TS_RS_EXPORT_DIR` is not set, to ./bindings . + If the provided path ends in a trailing /, it is interpreted as a directory. + Note that you need to add the 'export' attribute as well, in order to generate a test which exports the type. +"###; + +// If the crate MSRV is increased to 1.70.0 than the 'Note ' from point 4 should be removed ! + +const PARSING_ERROR_MSG: &'static str = + +"`export_to` expects as arguments the following types: + +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.70.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! +"; + + +#[derive(Clone, Debug)] +pub enum CustomPath { Str(String), - Static(syn::Path), - Fn(syn::Path), - Env(syn::LitStr), + Static(syn::Path), + Fn(syn::Path), + Env(syn::LitStr), } -type FnOutputPathBody = ( TokenStream, Option ); +type FnOutputPathBody = (TokenStream, Option); impl CustomPath { - - pub fn get_path_and_some_decl(&self, ts_name: &String ) -> FnOutputPathBody { - + pub fn get_path_and_some_decl(&self, ts_name: &String) -> FnOutputPathBody { match self { + Self::Str(input) => Self::str_path(input, ts_name), - 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::Static(input) => Self::static_path(input, ts_name), - Self::Env(input) => { Self::env_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('/') { + fn str_path(input: &String, ts_name: &String) -> FnOutputPathBody { + let path = if input.ends_with('/') { format!("{}{}.ts", input, ts_name) - } else { + } else { input.to_owned() }; - return (quote!(#path),None); + return (quote!(#path), None); } - fn static_path( input: &syn::Path, ts_name: &String ) -> FnOutputPathBody { - - let path_ident = format_ident!("path"); + 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('/') { @@ -60,23 +106,28 @@ impl CustomPath { } else { format!("{}",#input) } - } + } ); }; - ( quote!(#path_ident), Some(path_decl) ) + (quote!(#path_ident), Some(path_decl)) } - fn fn_path( input: &syn::Path, ts_name: &String ) -> FnOutputPathBody { - - ( quote!{#input (#ts_name)?}, None) - } + fn fn_path(input: &syn::Path, ts_name: &String) -> FnOutputPathBody { + let path_ident = format_ident!("path"); + + // check the type of the function pointer + let path_decl = quote! { + let path : fn(&'static str) -> Option<&'static str> = #input; + }; - fn env_path( input: &LitStr, ts_name: &String ) -> FnOutputPathBody { + (quote! {#path_ident(#ts_name)?}, Some(path_decl)) + } + fn env_path(input: &LitStr, ts_name: &String) -> FnOutputPathBody { let path_ident = format_ident!("path"); - let path_decl = quote!{ + let path_decl = quote! { let #path_ident = if std::env!(#input).ends_with('/') { std::concat!(std::env!(#input),#ts_name,".ts") @@ -85,48 +136,24 @@ impl CustomPath { }; }; - ( quote!(#path_ident), Some(path_decl) ) + (quote!(#path_ident), Some(path_decl)) } - } impl Parse for CustomPath { - fn parse(input: ParseStream) -> Result { input.parse::()?; let span = input.span(); + let mut some_ident = None; - 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 get_path = |pont_ident: Option, input: ParseStream| -> Result { let mut tokens = TokenStream::new(); - let mut env_var_str = None; - if input.peek(Token![self]) { + if let Some(ident) = pont_ident { + tokens.extend(quote!(#ident)) + } + + if input.peek(Token![self]) { let token = input.parse::()?; tokens.extend(quote!(#token)); } @@ -147,74 +174,74 @@ Note: This option is for environment variables defined in the '.cargo/config.tom let token = input.parse::()?; tokens.extend(quote!(#token)); - if input.peek(Ident){ + if input.peek(Ident) { let ident = input.parse::()?; tokens.extend(quote!(#ident)); - } else { return Err(Error::new(input.span(),"expected ident")) } + } else { + return Err(Error::new(input.span(), "expected ident")); + } + } + + Ok(syn::parse2::(tokens)?) + }; + + let get_str = |input: ParseStream| -> Result { + match Lit::parse(input)? { + Lit::Str(string) => Ok(string), + _ => Err(Error::new(span, PARSING_ERROR_MSG)), } + }; + + // reminder + if input.peek(Token![?]) { + let msg = format!("{PATH_CONVENTION_MSG}\n{PARSING_ERROR_MSG}"); + return Err(Error::new(span, msg)); + } + + // string literal + if input.peek(LitStr) { + return Ok(CustomPath::Str(get_str(input)?.value())); + } + + // environment variable + if input.peek(Ident) && input.peek2(syn::token::Paren) && input.peek3(LitStr) { + // needs a check for the ident + let ident = input.parse::()?; - if input.peek(syn::token::Paren){ + if ident == "env" { let content; syn::parenthesized!(content in input); - env_var_str = Some(content.parse::()?); + let env_str = content.parse::()?; + return Ok(CustomPath::Env(env_str)); + } else { + some_ident = Some(ident); } + } - Ok((syn::parse2::(tokens)?,env_var_str)) - }; + // path to a const | static | function + if let Ok(path) = get_path(some_ident, input) { + let last = &path.segments.last().unwrap().ident; + // const | static + if is_screaming_snake_case(&last.to_string()) { + return Ok(CustomPath::Static(path)); + } - // 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)), + // function + if is_snake_case(&last.to_string()) { + return Ok(CustomPath::Fn(path)); + } } - } -} + Err(Error::new(span, PARSING_ERROR_MSG)) + } +} -// These functions mimic Rust's naming conventions for +// These functions mimic Rust's naming conventions for // statics, constants, and function . -// To be replaced with proper, more robust validation. +// 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; } @@ -224,12 +251,10 @@ fn is_screaming_snake_case(s: &str) -> bool { return false; } } - true } fn is_snake_case(s: &str) -> bool { - if s.is_empty() || s.starts_with('_') { return false; } @@ -239,6 +264,5 @@ fn is_snake_case(s: &str) -> bool { return false; } } - true -} \ No newline at end of file +} From 475666fb244ab3b53316ebcf0f71e0a71f5f3fd7 Mon Sep 17 00:00:00 2001 From: Simon Nour Date: Fri, 6 Sep 2024 15:06:05 +0300 Subject: [PATCH 3/3] CustomPath::parse condition for environment variable variant fixed. Added some simple test to prevent it. --- macros/src/path.rs | 104 ++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 98 insertions(+), 6 deletions(-) diff --git a/macros/src/path.rs b/macros/src/path.rs index d4049523..d6bf2f84 100644 --- a/macros/src/path.rs +++ b/macros/src/path.rs @@ -204,15 +204,21 @@ impl Parse for CustomPath { } // environment variable - if input.peek(Ident) && input.peek2(syn::token::Paren) && input.peek3(LitStr) { + if input.peek(Ident) { + // needs a check for the ident let ident = input.parse::()?; - if ident == "env" { - let content; - syn::parenthesized!(content in input); - let env_str = content.parse::()?; - return Ok(CustomPath::Env(env_str)); + if input.peek(syn::token::Paren){ + + let content; + syn::parenthesized!(content in input); + let env_str = content.parse::()?; + return Ok(CustomPath::Env(env_str)); + + } else { + return Err(Error::new(span, PARSING_ERROR_MSG)); + } } else { some_ident = Some(ident); } @@ -266,3 +272,89 @@ fn is_snake_case(s: &str) -> bool { } true } + + + + + +#[cfg(test)] +mod tests { + use super::*; + use syn::parse_str; + + #[test] + fn test_str_literal() { + let input = r#"= "my/path""#; + let parsed: CustomPath = parse_str(input).unwrap(); + + if let CustomPath::Str(path) = parsed { + assert_eq!(path, "my/path"); + } else { + panic!("Expected CustomPath::Str variant"); + } + } + + #[test] + fn test_static_variable_single() { + let input = "= MY_STATIC_PATH"; + let parsed: CustomPath = parse_str(input).unwrap(); + + if let CustomPath::Static(path) = parsed { + assert_eq!(path.segments.last().unwrap().ident, "MY_STATIC_PATH"); + } else { + panic!("Expected CustomPath::Static variant"); + } + } + + #[test] + fn test_static_variable_full_path() { + let input = "= crate::MY_STATIC_PATH"; + let parsed: CustomPath = parse_str(input).unwrap(); + + if let CustomPath::Static(path) = parsed { + assert_eq!(path.segments.len(), 2); + assert_eq!(path.segments[0].ident, "crate"); + assert_eq!(path.segments[1].ident, "MY_STATIC_PATH"); + } else { + panic!("Expected CustomPath::Static variant"); + } + } + + #[test] + fn test_function_name_single() { + let input = "= my_func_get_path"; + let parsed: CustomPath = parse_str(input).unwrap(); + + if let CustomPath::Fn(path) = parsed { + assert_eq!(path.segments.last().unwrap().ident, "my_func_get_path"); + } else { + panic!("Expected CustomPath::Fn variant"); + } + } + + #[test] + fn test_function_name_full_path() { + let input = "= crate::my_func_get_path"; + let parsed: CustomPath = parse_str(input).unwrap(); + + if let CustomPath::Fn(path) = parsed { + assert_eq!(path.segments.len(), 2); + assert_eq!(path.segments[0].ident, "crate"); + assert_eq!(path.segments[1].ident, "my_func_get_path"); + } else { + panic!("Expected CustomPath::Fn variant"); + } + } + + #[test] + fn test_env_variable() { + let input = r#"= env("MY_ENV_VAR_PATH")"#; + let parsed: CustomPath = parse_str(input).unwrap(); + + if let CustomPath::Env(lit) = parsed { + assert_eq!(lit.value(), "MY_ENV_VAR_PATH"); + } else { + panic!("Expected CustomPath::Env variant"); + } + } +}