Skip to content

Commit

Permalink
Custom Path added for attribute.
Browse files Browse the repository at this point in the history
  • Loading branch information
NimonSour committed Aug 16, 2024
1 parent 78591a2 commit a60028c
Show file tree
Hide file tree
Showing 4 changed files with 262 additions and 13 deletions.
5 changes: 3 additions & 2 deletions macros/src/attr/enum.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand All @@ -16,7 +17,7 @@ pub struct EnumAttr {
pub rename_all: Option<Inflection>,
pub rename_all_fields: Option<Inflection>,
pub rename: Option<String>,
pub export_to: Option<String>,
pub export_to: Option<CustomPath>,
pub export: bool,
pub docs: String,
pub concrete: HashMap<Ident, Type>,
Expand Down Expand Up @@ -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)?),
Expand Down
5 changes: 3 additions & 2 deletions macros/src/attr/struct.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand All @@ -18,7 +19,7 @@ pub struct StructAttr {
pub type_override: Option<String>,
pub rename_all: Option<Inflection>,
pub rename: Option<String>,
pub export_to: Option<String>,
pub export_to: Option<CustomPath>,
pub export: bool,
pub tag: Option<String>,
pub docs: String,
Expand Down Expand Up @@ -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)?),
}
Expand Down
21 changes: 12 additions & 9 deletions macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -30,26 +31,28 @@ struct DerivedTS {
bound: Option<Vec<WherePredicate>>,

export: bool,
export_to: Option<String>,
export_to: Option<CustomPath>,
}

impl DerivedTS {
fn into_impl(mut self, rust_ty: Ident, generics: Generics) -> TokenStream {
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))
}
}
Expand Down
244 changes: 244 additions & 0 deletions macros/src/path.rs
Original file line number Diff line number Diff line change
@@ -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<TokenStream> );

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<String> = 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<CustomPath> {
input.parse::<Token![=]>()?;
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<LitStr>)>{
let mut tokens = TokenStream::new();
let mut env_var_str = None;

if input.peek(Token![self]) {
let token = input.parse::<Token![self]>()?;
tokens.extend(quote!(#token));
}
if input.peek(Token![super]) {
let token = input.parse::<Token![super]>()?;
tokens.extend(quote!(#token));
}
if input.peek(Token![crate]) {
let token = input.parse::<Token![crate]>()?;
tokens.extend(quote!(#token));
}
if input.peek(Ident) {
let ident = input.parse::<Ident>()?;
tokens.extend(quote!(#ident));
}

while input.peek(Token![::]) {
let token = input.parse::<Token![::]>()?;
tokens.extend(quote!(#token));

if input.peek(Ident){
let ident = input.parse::<Ident>()?;
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::<LitStr>()?);
}

Ok((syn::parse2::<syn::Path>(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
}

0 comments on commit a60028c

Please sign in to comment.