diff --git a/Cargo.lock b/Cargo.lock index 5af7dc1..b3cd38a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,15 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "aho-corasick" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" +dependencies = [ + "memchr", +] + [[package]] name = "ansi_term" version = "0.11.0" @@ -28,6 +37,21 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" +[[package]] +name = "cc" +version = "1.0.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" +dependencies = [ + "libc", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + [[package]] name = "clap" version = "2.33.3" @@ -52,6 +76,19 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "generator" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cc16584ff22b460a382b7feec54b23d2908d858152e5739a120b949293bd74e" +dependencies = [ + "cc", + "libc", + "log", + "rustversion", + "windows", +] + [[package]] name = "heck" version = "0.3.1" @@ -70,6 +107,12 @@ dependencies = [ "libc", ] +[[package]] +name = "itoa" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" + [[package]] name = "lazy_static" version = "1.4.0" @@ -82,6 +125,70 @@ version = "0.2.80" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4d58d1b70b004888f764dfbf6a26a3b0342a1632d33968e4a179d8011c760614" +[[package]] +name = "log" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" + +[[package]] +name = "loom" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff50ecb28bb86013e935fb6683ab1f6d3a20016f123c76fd4c27470076ac30f5" +dependencies = [ + "cfg-if", + "generator", + "scoped-tls", + "serde", + "serde_json", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata 0.1.10", +] + +[[package]] +name = "memchr" +version = "2.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" + +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + +[[package]] +name = "once_cell" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" + +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + +[[package]] +name = "pin-project-lite" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" + [[package]] name = "proc-macro-error" version = "1.0.4" @@ -124,6 +231,62 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "regex" +version = "1.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata 0.4.3", + "regex-syntax 0.8.2", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax 0.6.29", +] + +[[package]] +name = "regex-automata" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax 0.8.2", +] + +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + +[[package]] +name = "regex-syntax" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" + +[[package]] +name = "rustversion" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" + +[[package]] +name = "ryu" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" + [[package]] name = "same-file" version = "1.0.6" @@ -133,6 +296,67 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + +[[package]] +name = "serde" +version = "1.0.193" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25dd9975e68d0cb5aa1120c288333fc98731bd1dd12f561e468ea4728c042b89" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.193" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.28", +] + +[[package]] +name = "serde_json" +version = "1.0.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "smallvec" +version = "1.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dccd0940a2dcdf68d092b8cbab7dc0ad8fa938bf95787e1b916b0e3d0e8e970" + +[[package]] +name = "state" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b8c4a4445d81357df8b1a650d0d0d6fbbbfe99d064aa5e02f3e4022061476d8" +dependencies = [ + "loom", +] + [[package]] name = "strsim" version = "0.8.0" @@ -194,6 +418,77 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "thread_local" +version = "1.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" +dependencies = [ + "cfg-if", + "once_cell", +] + +[[package]] +name = "tracing" +version = "0.1.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.28", +] + +[[package]] +name = "tracing-core" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + [[package]] name = "tsync" version = "2.0.1" @@ -201,6 +496,8 @@ dependencies = [ "convert_case", "proc-macro2", "quote", + "serde", + "state", "structopt", "syn 2.0.28", "tsync-macro", @@ -237,6 +534,12 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564" +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + [[package]] name = "vec_map" version = "0.8.2" @@ -289,3 +592,69 @@ name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" diff --git a/Cargo.toml b/Cargo.toml index 32dd894..b601c71 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,10 @@ quote = "1.0.32" walkdir = "2.3.3" tsync-macro = "0.1.0" convert_case = "0.6.0" +state = "0.6.0" + +[dev-dependencies] +serde = { version = "1", features = ["derive"]} [lib] name = "tsync" diff --git a/src/lib.rs b/src/lib.rs index a0e2556..37c2b0c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,23 +2,26 @@ mod to_typescript; mod typescript; pub mod utils; +use state::InitCell; +use std::ffi::OsStr; use std::fs::File; -use std::io::{BufRead, BufReader, Read, Write}; +use std::io::{BufRead, BufReader}; use std::path::{Path, PathBuf}; - -use walkdir::WalkDir; +use walkdir::{DirEntry, WalkDir}; /// the #[tsync] attribute macro which marks structs and types to be translated into the final typescript definitions file pub use tsync_macro::tsync; use crate::to_typescript::ToTypescript; +pub(crate) static DEBUG: InitCell = InitCell::new(); + /// macro to check from an syn::Item most of them have ident attribs /// that is the one we want to print but not sure! macro_rules! check_tsync { - ($x: ident, in: $y: tt, $z: tt, $debug: ident) => { + ($x: ident, in: $y: tt, $z: tt) => { let has_tsync_attribute = has_tsync_attribute(&$x.attrs); - if $debug { + if *DEBUG.get() { if has_tsync_attribute { println!("Encountered #[tsync] {}: {}", $y, $x.ident.to_string()); } else { @@ -32,6 +35,7 @@ macro_rules! check_tsync { }; } +#[derive(Default)] pub struct BuildState /*<'a>*/ { pub types: String, pub unprocessed_files: Vec, @@ -53,157 +57,159 @@ impl BuildState { let indentation = utils::build_indentation(indentation_amount); match comments.len() { 0 => (), - 1 => self - .types - .push_str(&format!("{}/** {} */\n", indentation, &comments[0])), + 1 => { + self.types + .push_str(&format!("{}/** {} */\n", indentation, &comments[0])) + } _ => { - self.types.push_str(&format!("{}/**\n", indentation)); + self.types + .push_str(&format!("{}/**\n", indentation)); for comment in comments { self.types .push_str(&format!("{} * {}\n", indentation, &comment)) } - self.types.push_str(&format!("{} */\n", indentation)) + self.types + .push_str(&format!("{} */\n", indentation)) } } } } -fn process_rust_file( - debug: bool, - input_path: PathBuf, +fn process_rust_item(item: syn::Item, state: &mut BuildState, uses_type_interface: bool) { + match item { + syn::Item::Const(exported_const) => { + check_tsync!(exported_const, in: "const", { + exported_const.convert_to_ts(state, uses_type_interface); + }); + } + syn::Item::Struct(exported_struct) => { + check_tsync!(exported_struct, in: "struct", { + exported_struct.convert_to_ts(state, uses_type_interface); + }); + } + syn::Item::Enum(exported_enum) => { + check_tsync!(exported_enum, in: "enum", { + exported_enum.convert_to_ts(state, uses_type_interface); + }); + } + syn::Item::Type(exported_type) => { + check_tsync!(exported_type, in: "type", { + exported_type.convert_to_ts(state, uses_type_interface); + }); + } + _ => {} + } +} + +fn process_rust_file>( + input_path: P, state: &mut BuildState, - uses_typeinterface: bool, + uses_type_interface: bool, ) { - if debug { - println!( - "processing rust file: {:?}", - input_path.clone().into_os_string().into_string().unwrap() - ); + if *DEBUG.get() { + println!("processing rust file: {:?}", input_path.as_ref().to_str()); } - let file = File::open(&input_path); + let Ok(src) = std::fs::read_to_string(input_path.as_ref()) else { + state.unprocessed_files.push(input_path.as_ref().to_path_buf()); + return; + }; - if file.is_err() { - state.unprocessed_files.push(input_path); + let Ok(syntax) = syn::parse_file(&src) else { + state.unprocessed_files.push(input_path.as_ref().to_path_buf()); return; - } + }; - let mut file = file.unwrap(); + syntax + .items + .into_iter() + .for_each(|item| process_rust_item(item, state, uses_type_interface)) +} - let mut src = String::new(); - if file.read_to_string(&mut src).is_err() { - state.unprocessed_files.push(input_path); - return; +fn check_path>(path: P, state: &mut BuildState) -> bool { + if !path.as_ref().exists() { + if *DEBUG.get() { println!("Path `{:#?}` does not exist", path.as_ref()); } + state.unprocessed_files.push(path.as_ref().to_path_buf()); + return false; } - let syntax = syn::parse_file(&src); + true +} - if syntax.is_err() { - state.unprocessed_files.push(input_path); - return; +fn check_extension>(ext: &OsStr, path: P) -> bool { + if !ext.eq_ignore_ascii_case("rs") { + if *DEBUG.get() { + println!("Encountered non-rust file `{:#?}`", path.as_ref()); + } + return false } - let syntax = syntax.unwrap(); + true +} - for item in syntax.items { - match item { - syn::Item::Const(exported_const) => { - check_tsync!(exported_const, in: "const", { - exported_const.convert_to_ts(state, debug, uses_typeinterface); - }, debug); - } - syn::Item::Struct(exported_struct) => { - check_tsync!(exported_struct, in: "struct", { - exported_struct.convert_to_ts(state, debug, uses_typeinterface); - }, debug); - } - syn::Item::Enum(exported_enum) => { - check_tsync!(exported_enum, in: "enum", { - exported_enum.convert_to_ts(state, debug, uses_typeinterface); - }, debug); - } - syn::Item::Type(exported_type) => { - check_tsync!(exported_type, in: "type", { - exported_type.convert_to_ts(state, debug, uses_typeinterface); - }, debug); +/// Ensure that the walked entry result is Ok and its path is a file. If not, +/// return `None`, otherwise return `Some(DirEntry)`. +fn validate_dir_entry(entry_result: walkdir::Result, path: &Path) -> Option { + match entry_result { + Ok(entry) => { + // skip dir files because they're going to be recursively crawled by WalkDir + if entry.path().is_dir() { + if *DEBUG.get() { + println!("Encountered directory `{}`", path.display()); + } + return None; } - _ => {} + + Some(entry) + } + Err(e) => { + println!("An error occurred whilst walking directory `{}`...", path.display()); + println!("Details: {e:?}"); + None } } } +fn process_dir_entry>(path: P, state: &mut BuildState, uses_type_interface: bool) { + WalkDir::new(path.as_ref()) + .sort_by_file_name() + .into_iter() + .filter_map(|res| validate_dir_entry(res, path.as_ref())) + .for_each(|entry| { + // make sure it is a rust file + if entry + .path() + .extension() + .is_some_and(|extension| check_extension(extension, path.as_ref())) + { + process_rust_file(entry.path(), state, uses_type_interface) + } + }) +} + pub fn generate_typescript_defs(input: Vec, output: PathBuf, debug: bool) { - let uses_typeinterface = output - .as_os_str() + DEBUG.set(debug); + + let uses_type_interface = output .to_str() .map(|x| x.ends_with(".d.ts")) .unwrap_or(true); - let mut state: BuildState = BuildState { - types: String::new(), - unprocessed_files: Vec::::new(), - // ignore_file_config: if args.clone().use_ignore_file.is_some() { - // match gitignore::File::new(&args.use_ignore_file.unwrap()) { - // Ok(gitignore) => Some(gitignore), - // Err(err) => { - // if args.debug { - // println!("Error: failed to use ignore file! {:#?}", err); - // } - // None - // } - // } - // } else { - // None - // }, - }; + let mut state = BuildState::default(); state .types .push_str("/* This file is generated and managed by tsync */\n"); - for input_path in input { - if !input_path.exists() { - if debug { - println!("Path `{:#?}` does not exist", input_path); - } - - state.unprocessed_files.push(input_path); - continue; - } - - if input_path.is_dir() { - for entry in WalkDir::new(input_path.clone()).sort_by_file_name() { - match entry { - Ok(dir_entry) => { - let path = dir_entry.into_path(); - - // skip dir files because they're going to be recursively crawled by WalkDir - if !path.is_dir() { - // make sure it is a rust file - let extension = path.extension(); - if extension.is_some() && extension.unwrap().eq_ignore_ascii_case("rs") - { - process_rust_file(debug, path, &mut state, uses_typeinterface); - } else if debug { - println!("Encountered non-rust file `{:#?}`", path); - } - } else if debug { - println!("Encountered directory `{:#?}`", path); - } - } - Err(_) => { - println!( - "An error occurred whilst walking directory `{:#?}`...", - input_path.clone() - ); - continue; - } - } + input.into_iter().for_each(|path| { + if check_path(&path, &mut state) { + if path.is_dir() { + process_dir_entry(&path, &mut state, uses_type_interface) + } else { + process_rust_file(&path, &mut state, uses_type_interface); } - } else { - process_rust_file(debug, input_path, &mut state, uses_typeinterface); } - } + }); if debug { println!("======================================"); @@ -215,12 +221,11 @@ pub fn generate_typescript_defs(input: Vec, output: PathBuf, debug: boo println!("======================================"); } else { // Verify that the output file either doesn't exists or has been generated by tsync. - let original_file_path = Path::new(&output); - if original_file_path.exists() { - if !original_file_path.is_file() { + if output.exists() { + if !output.is_file() { panic!("Specified output path is a directory but must be a file.") } - let original_file = File::open(original_file_path).expect("Couldn't open output file"); + let original_file = File::open(&output).expect("Couldn't open output file"); let mut buffer = BufReader::new(original_file); let mut first_line = String::new(); @@ -234,8 +239,7 @@ pub fn generate_typescript_defs(input: Vec, output: PathBuf, debug: boo } } - let mut file: File = File::create(&output).expect("Unable to write to file"); - match file.write_all(state.types.as_bytes()) { + match std::fs::write(&output, state.types.as_bytes()) { Ok(_) => println!("Successfully generated typescript types, see {:#?}", output), Err(_) => println!("Failed to generate types, an error occurred."), } diff --git a/src/to_typescript/consts.rs b/src/to_typescript/consts.rs index fc2fda3..379f424 100644 --- a/src/to_typescript/consts.rs +++ b/src/to_typescript/consts.rs @@ -1,12 +1,11 @@ use syn::__private::ToTokens; -use crate::utils; -use crate::BuildState; +use crate::{utils, BuildState}; impl super::ToTypescript for syn::ItemConst { - fn convert_to_ts(self, state: &mut BuildState, debug: bool, uses_typeinterface: bool) { + fn convert_to_ts(self, state: &mut BuildState, uses_type_interface: bool) { // ignore if we aren't in a type interface - if uses_typeinterface { + if uses_type_interface { return; } @@ -49,7 +48,7 @@ impl super::ToTypescript for syn::ItemConst { state.types.push('\n'); } _ => { - if debug { + if crate::DEBUG.try_get().is_some_and(|d| *d) { println!("#[tsync] failed for const {}", self.to_token_stream()); } } diff --git a/src/to_typescript/enums.rs b/src/to_typescript/enums.rs index a8f9576..9cc9a1e 100644 --- a/src/to_typescript/enums.rs +++ b/src/to_typescript/enums.rs @@ -3,23 +3,12 @@ use crate::{utils, BuildState}; use convert_case::{Case, Casing}; use syn::__private::ToTokens; -static RENAME_RULES: &[(&str, convert_case::Case)] = &[ - ("lowercase", Case::Lower), - ("UPPERCASE", Case::Upper), - ("PascalCase", Case::Pascal), - ("camelCase", Case::Camel), - ("snake_case", Case::Snake), - ("SCREAMING_SNAKE_CASE", Case::ScreamingSnake), - ("kebab-case", Case::Kebab), - // ("SCREAMING-KEBAB-CASE", _), // not supported by convert_case -]; - /// Conversion of Rust Enum to Typescript using external tagging as per https://serde.rs/enum-representations.html /// however conversion will adhere to the `serde` `tag` such that enums are intenrally tagged /// (while the other forms such as adjacent tagging aren't supported). /// `rename_all` attributes for the name of the tag will also be adhered to. impl super::ToTypescript for syn::ItemEnum { - fn convert_to_ts(self, state: &mut BuildState, debug: bool, uses_typeinterface: bool) { + fn convert_to_ts(self, state: &mut BuildState, uses_type_interface: bool) { // check we don't have any tuple structs that could mess things up. // if we do ignore this struct for variant in self.variants.iter() { @@ -29,7 +18,7 @@ impl super::ToTypescript for syn::ItemEnum { if f.ident.is_none() { // If we already marked this variant as a newtype, we have a multi-field tuple struct if is_newtype { - if debug { + if crate::DEBUG.try_get().is_some_and(|d| *d) { println!("#[tsync] failed for enum {}", self.ident); } return; @@ -44,34 +33,34 @@ impl super::ToTypescript for syn::ItemEnum { let comments = utils::get_comments(self.clone().attrs); let casing = utils::get_attribute_arg("serde", "rename_all", &self.attrs); - let casing = to_enum_case(casing); + let casing = utils::parse_serde_case(casing); let is_single = !self.variants.iter().any(|x| !x.fields.is_empty()); state.write_comments(&comments, 0); if is_single { if utils::has_attribute_arg("derive", "Serialize_repr", &self.attrs) { - add_numeric_enum(self, state, casing, uses_typeinterface) + add_numeric_enum(self, state, casing, uses_type_interface) } else { - add_enum(self, state, casing, uses_typeinterface) + add_enum(self, state, casing, uses_type_interface) } } else if let Some(tag_name) = utils::get_attribute_arg("serde", "tag", &self.attrs) { - add_internally_tagged_enum(tag_name, self, state, casing, uses_typeinterface) + add_internally_tagged_enum(tag_name, self, state, casing, uses_type_interface) } else { - add_externally_tagged_enum(self, state, casing, uses_typeinterface) + add_externally_tagged_enum(self, state, casing, uses_type_interface) } } } /// This convert an all unit enums to a union of const strings in Typescript. -/// It will ignore any discriminants. +/// It will ignore any discriminants. fn add_enum( exported_struct: syn::ItemEnum, state: &mut BuildState, casing: Option, - uses_typeinterface: bool, + uses_type_interface: bool, ) { - let export = if uses_typeinterface { "" } else { "export " }; + let export = if uses_type_interface { "" } else { "export " }; state.types.push_str(&format!( "{export}type {interface_name} =\n{space}", interface_name = exported_struct.ident, @@ -104,9 +93,9 @@ fn add_enum( /// ``` to the following /// ```ignore /// enum Foo { -/// Bar = 0, -/// Baz = 123, -/// Quux = 124, +/// Bar = 0, +/// Baz = 123, +/// Quux = 124, /// } /// enum Animal { /// Dog = 0, @@ -118,13 +107,9 @@ fn add_numeric_enum( exported_struct: syn::ItemEnum, state: &mut BuildState, casing: Option, - uses_typeinterface: bool, + uses_type_interface: bool, ) { - let declare = if uses_typeinterface { - "declare " - } else { - "export " - }; + let declare = if uses_type_interface { "declare " } else { "export " }; state.types.push_str(&format!( "{declare}enum {interface_name} {{", interface_name = exported_struct.ident @@ -178,9 +163,9 @@ fn add_numeric_enum( /// ``` to the following /// ```ignore /// enum Foo { -/// Bar = 0, -/// Baz = 123, -/// Quux = 124, +/// Bar = 0, +/// Baz = 123, +/// Quux = 124, /// } /// enum Animal { /// Dog = 0, @@ -192,9 +177,9 @@ fn add_internally_tagged_enum( exported_struct: syn::ItemEnum, state: &mut BuildState, casing: Option, - uses_typeinterface: bool, + uses_type_interface: bool, ) { - let export = if uses_typeinterface { "" } else { "export " }; + let export = if uses_type_interface { "" } else { "export " }; state.types.push_str(&format!( "{export}type {interface_name}{generics} =", interface_name = exported_struct.ident, @@ -203,11 +188,7 @@ fn add_internally_tagged_enum( for variant in exported_struct.variants.iter() { // Assumes that non-newtype tuple variants have already been filtered out - let is_newtype = variant - .fields - .iter() - .fold(false, |state, v| state || v.ident.is_none()); - if is_newtype { + if variant.fields.iter().any(|v| v.ident.is_none()) { // TODO: Generate newtype structure // This should contain the discriminant plus all fields of the inner structure as a flat structure // TODO: Check for case where discriminant name matches an inner structure field name @@ -226,11 +207,7 @@ fn add_internally_tagged_enum( for variant in exported_struct.variants { // Assumes that non-newtype tuple variants have already been filtered out - let is_newtype = variant - .fields - .iter() - .fold(false, |state, v| state || v.ident.is_none()); - if !is_newtype { + if !variant.fields.iter().any(|v| v.ident.is_none()) { state.types.push('\n'); let comments = utils::get_comments(variant.attrs); state.write_comments(&comments, 0); @@ -252,11 +229,11 @@ fn add_internally_tagged_enum( tag_name, field_name, )); - super::structs::process_fields(variant.fields, state, 2); + super::structs::process_fields(variant.fields, state, 2, casing); state.types.push_str("};"); } } - state.types.push_str("\n"); + state.types.push('\n'); } /// This follows serde's default approach of external tagging @@ -264,9 +241,9 @@ fn add_externally_tagged_enum( exported_struct: syn::ItemEnum, state: &mut BuildState, casing: Option, - uses_typeinterface: bool, + uses_type_interface: bool, ) { - let export = if uses_typeinterface { "" } else { "export " }; + let export = if uses_type_interface { "" } else { "export " }; state.types.push_str(&format!( "{export}type {interface_name}{generics} =", interface_name = exported_struct.ident, @@ -283,20 +260,15 @@ fn add_externally_tagged_enum( variant.ident.to_string() }; // Assumes that non-newtype tuple variants have already been filtered out - let is_newtype = variant - .fields - .iter() - .fold(false, |state, v| state || v.ident.is_none()); + let is_newtype = variant.fields.iter().any(|v| v.ident.is_none()); if is_newtype { // add discriminant state.types.push_str(&format!(" | {{ \"{}\":", field_name)); for field in variant.fields { - state - .types - .push_str(&format!(" {}", convert_type(&field.ty).ts_type,)); + state.types.push_str(&format!(" {}", convert_type(&field.ty).ts_type,)); } - state.types.push_str(&format!(" }}")); + state.types.push_str(" }"); } else { // add discriminant state.types.push_str(&format!( @@ -310,7 +282,7 @@ fn add_externally_tagged_enum( } else { prepend = utils::build_indentation(6); state.types.push('\n'); - super::structs::process_fields(variant.fields, state, 8); + super::structs::process_fields(variant.fields, state, 8, casing); } state .types @@ -319,14 +291,3 @@ fn add_externally_tagged_enum( } state.types.push_str(";\n"); } - -fn to_enum_case(val: impl Into>) -> Option { - val.into().and_then(|x| { - for (name, rule) in RENAME_RULES { - if x == *name { - return Some(*rule); - } - } - None - }) -} diff --git a/src/to_typescript/mod.rs b/src/to_typescript/mod.rs index 3623ca0..f4b1997 100644 --- a/src/to_typescript/mod.rs +++ b/src/to_typescript/mod.rs @@ -4,5 +4,5 @@ pub mod structs; pub mod type_item; pub trait ToTypescript { - fn convert_to_ts(self, state: &mut crate::BuildState, debug: bool, uses_typeinterface: bool); + fn convert_to_ts(self, state: &mut crate::BuildState, uses_type_interface: bool); } diff --git a/src/to_typescript/structs.rs b/src/to_typescript/structs.rs index a35363f..d46f3be 100644 --- a/src/to_typescript/structs.rs +++ b/src/to_typescript/structs.rs @@ -1,10 +1,12 @@ use crate::typescript::convert_type; use crate::{utils, BuildState}; +use convert_case::{Case, Casing}; impl super::ToTypescript for syn::ItemStruct { - fn convert_to_ts(self, state: &mut BuildState, _debug: bool, uses_typeinterface: bool) { - let export = if uses_typeinterface { "" } else { "export " }; - + fn convert_to_ts(self, state: &mut BuildState, uses_type_interface: bool) { + let export = if uses_type_interface { "" } else { "export " }; + let casing = utils::get_attribute_arg("serde", "rename_all", &self.attrs); + let casing = utils::parse_serde_case(casing); state.types.push('\n'); let comments = utils::get_comments(self.clone().attrs); @@ -15,19 +17,34 @@ impl super::ToTypescript for syn::ItemStruct { interface_name = self.ident, generics = utils::extract_struct_generics(self.generics.clone()) )); - process_fields(self.fields, state, 2); - state.types.push('}'); + process_fields(self.fields, state, 2, casing); + state.types.push('}'); state.types.push('\n'); } } -pub fn process_fields(fields: syn::Fields, state: &mut BuildState, indentation_amount: i8) { +pub fn process_fields( + fields: syn::Fields, + state: &mut BuildState, + indentation_amount: i8, + case: impl Into>, +) { let space = utils::build_indentation(indentation_amount); + let case = case.into(); for field in fields { let comments = utils::get_comments(field.attrs); + state.write_comments(&comments, 2); - let field_name = field.ident.unwrap().to_string(); + let field_name = if let Some(name_case) = case { + field + .ident + .map(|id| id.to_string().to_case(name_case)) + .unwrap() + } else { + field.ident.map(|i| i.to_string()).unwrap() + }; + let field_type = convert_type(&field.ty); state.types.push_str(&format!( "{space}{field_name}{optional_parameter_token}: {field_type};\n", diff --git a/src/to_typescript/type_item.rs b/src/to_typescript/type_item.rs index c53b1ea..c24d334 100644 --- a/src/to_typescript/type_item.rs +++ b/src/to_typescript/type_item.rs @@ -1,8 +1,8 @@ use crate::BuildState; impl super::ToTypescript for syn::ItemType { - fn convert_to_ts(self, state: &mut BuildState, _debug: bool, uses_typeinterface: bool) { - let export = if uses_typeinterface { "" } else { "export " }; + fn convert_to_ts(self, state: &mut BuildState, uses_type_interface: bool) { + let export = if uses_type_interface { "" } else { "export " }; state.types.push('\n'); let name = self.ident.to_string(); let ty = crate::typescript::convert_type(&self.ty); diff --git a/src/typescript.rs b/src/typescript.rs index 22f4f53..a0b22c3 100644 --- a/src/typescript.rs +++ b/src/typescript.rs @@ -20,54 +20,94 @@ fn convert_generic(gen_ty: &syn::GenericArgument) -> TsType { } } -pub fn convert_type(ty: &syn::Type) -> TsType { - match ty { - syn::Type::Reference(p) => convert_type(&p.elem), - syn::Type::Path(p) => { - let segment = p.path.segments.last().unwrap(); - let ident = &segment.ident; - let arguments = &segment.arguments; - let identifier = ident.to_string(); - match identifier.as_str() { - "i8" => "number".to_string().into(), - "u8" => "number".to_string().into(), - "i16" => "number".to_string().into(), - "u16" => "number".to_string().into(), - "i32" => "number".to_string().into(), - "u32" => "number".to_string().into(), - "i64" => "number".to_string().into(), - "u64" => "number".to_string().into(), - "i128" => "number".to_string().into(), - "u128" => "number".to_string().into(), - "isize" => "number".to_string().into(), - "usize" => "number".to_string().into(), - "f32" => "number".to_string().into(), - "f64" => "number".to_string().into(), - "bool" => "boolean".to_string().into(), - "char" => "string".to_string().into(), - "str" => "string".to_string().into(), - "String" => "string".to_string().into(), - "NaiveDateTime" => "Date".to_string().into(), - "DateTime" => "Date".to_string().into(), - "Option" => TsType { - is_optional: true, - ts_type: match arguments { - syn::PathArguments::Parenthesized(parenthesized_argument) => { - format!("{:?}", parenthesized_argument) - } - syn::PathArguments::AngleBracketed(anglebracketed_argument) => { - convert_generic(anglebracketed_argument.args.first().unwrap()).ts_type - } - _ => "unknown".to_string(), - }, +fn check_cow(cow: &str) -> String { + if !cow.contains("Cow<") { + return cow.to_owned(); + } + + if let Some(comma_pos) = cow + .chars() + .enumerate() + .find(|(_, c)| c == &',') + .map(|(i, _)| i) + { + let cow = cow[comma_pos + 1..].trim(); + if let Some(c) = cow.strip_suffix('>') { + return try_match_ident_str(c); + } + } + + cow.to_owned() +} + +fn try_match_ident_str(ident: &str) -> String { + match ident { + "i8" => "number".to_owned(), + "u8" => "number".to_owned(), + "i16" => "number".to_owned(), + "u16" => "number".to_owned(), + "i32" => "number".to_owned(), + "u32" => "number".to_owned(), + "i64" => "number".to_owned(), + "u64" => "number".to_owned(), + "i128" => "number".to_owned(), + "u128" => "number".to_owned(), + "isize" => "number".to_owned(), + "usize" => "number".to_owned(), + "f32" => "number".to_owned(), + "f64" => "number".to_owned(), + "bool" => "boolean".to_owned(), + "char" => "string".to_owned(), + "str" => "string".to_owned(), + "String" => "string".to_owned(), + "NaiveDateTime" => "Date".to_owned(), + "DateTime" => "Date".to_owned(), + "Uuid" => "string".to_owned(), + _ => ident.to_owned(), + } +} + +fn try_match_with_args(ident: &str, args: &syn::PathArguments) -> TsType { + match ident { + "Cow" => { + match &args { + syn::PathArguments::AngleBracketed(angle_bracketed_argument) => { + let Some(arg) = angle_bracketed_argument + .args + .iter() + .find(|arg| matches!(arg, syn::GenericArgument::Type(_))) + else { + return "unknown".to_owned().into(); + }; + + convert_generic(arg).ts_type.into() }, - "Vec" => match arguments { + _ => "unknown".to_owned().into(), + } + } + "Option" => { + TsType { + is_optional: true, + ts_type: match &args { syn::PathArguments::Parenthesized(parenthesized_argument) => { format!("{:?}", parenthesized_argument) } - syn::PathArguments::AngleBracketed(anglebracketed_argument) => format!( + syn::PathArguments::AngleBracketed(angle_bracketed_argument) => { + convert_generic(angle_bracketed_argument.args.first().unwrap()).ts_type + } + _ => "unknown".to_owned(), + }, + } + } + "Vec" => { + match &args { + syn::PathArguments::Parenthesized(parenthesized_argument) => { + format!("{:?}", parenthesized_argument).into() + } + syn::PathArguments::AngleBracketed(angle_bracketed_argument) => { + format!( "Array<{}>", - match convert_generic(anglebracketed_argument.args.first().unwrap()) { + match convert_generic(angle_bracketed_argument.args.first().unwrap()) { TsType { is_optional: true, ts_type, @@ -77,38 +117,62 @@ pub fn convert_type(ty: &syn::Type) -> TsType { ts_type, } => ts_type, } - ), - _ => "unknown".to_string(), + ) + .into() } - .into(), - "HashMap" => match arguments { - syn::PathArguments::Parenthesized(parenthesized_argument) => { - format!("{:?}", parenthesized_argument) - } - syn::PathArguments::AngleBracketed(anglebracketed_argument) => format!( + _ => "unknown".to_owned().into(), + } + } + "HashMap" => { + match &args { + syn::PathArguments::Parenthesized(parenthesized_argument) => { + format!("{:?}", parenthesized_argument).into() + } + syn::PathArguments::AngleBracketed(angle_bracketed_argument) => { + format!( "Record<{}>", - anglebracketed_argument + angle_bracketed_argument .args .iter() - .map(|arg| match convert_generic(arg) { - TsType { - is_optional: true, - ts_type, - } => format!("{} | undefined", ts_type), - TsType { - is_optional: false, - ts_type, - } => ts_type, + .map(|arg| { + match convert_generic(arg) { + TsType { + is_optional: true, + ts_type, + } => format!("{} | undefined", ts_type), + TsType { + is_optional: false, + ts_type, + } => ts_type, + } }) .collect::>() .join(", ") - ), - _ => "unknown".to_string(), + ) + .into() } - .into(), - _ => identifier.to_string().into(), + _ => "unknown".to_owned().into(), } } - _ => "unknown".to_string().into(), + _ => ident.to_owned().into(), + } +} + +const COMPLEX_TYPES: [&str; 4usize] = ["Option", "Vec", "HashMap", "Cow"]; + +pub fn convert_type(ty: &syn::Type) -> TsType { + match ty { + syn::Type::Reference(p) => convert_type(&p.elem), + syn::Type::Path(p) => { + let segment = p.path.segments.last().unwrap(); + let identifier = segment.ident.to_string(); + + if COMPLEX_TYPES.contains(&identifier.as_str()) { + try_match_with_args(&identifier, &segment.arguments) + } else { + try_match_ident_str(&identifier).into() + } + } + _ => "unknown".to_owned().into(), } } diff --git a/src/utils.rs b/src/utils.rs index b6fc4ab..521b7bc 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,5 +1,18 @@ use quote::ToTokens; -use syn::{punctuated::Punctuated, Attribute, ExprPath, MetaNameValue, Token}; +use syn::parse::Parser; +use syn::punctuated::Punctuated; +use syn::{Expr, ExprPath, MetaNameValue, Token}; + +pub(crate) static RENAME_RULES: &[(&str, convert_case::Case)] = &[ + ("lowercase", convert_case::Case::Lower), + ("UPPERCASE", convert_case::Case::Upper), + ("PascalCase", convert_case::Case::Pascal), + ("camelCase", convert_case::Case::Camel), + ("snake_case", convert_case::Case::Snake), + ("SCREAMING_SNAKE_CASE", convert_case::Case::ScreamingSnake), + ("kebab-case", convert_case::Case::Kebab), + // ("SCREAMING-KEBAB-CASE", _), // not supported by convert_case +]; pub fn has_attribute(needle: &str, attributes: &[syn::Attribute]) -> bool { attributes.iter().any(|attr| { @@ -10,95 +23,92 @@ pub fn has_attribute(needle: &str, attributes: &[syn::Attribute]) -> bool { }) } -/// Get the value matching an attribute and argument combination +/// Ensures that a parsed expression is a valid Rust module-like path and the +/// path contains valid segments. /// -/// For #[serde(tag = "type")], get_attribute_arg("serde", "tag", attributes) will return Some("type") -/// For #[derive(Serialize_repr)], get_attribute_arg("derive", "Serialize_repr", attributes) will return Some("Serialize_repr") -pub fn get_attribute_arg(needle: &str, arg: &str, attributes: &[syn::Attribute]) -> Option { - if let Some(attr) = get_attribute(needle, attributes) { - // check if attribute list contains the argument we are interested in - let mut found = false; - let mut value = String::new(); - - // TODO: don't use a for loop here or iterator here - let tokens = attr.meta.to_token_stream().into_iter(); - for token in tokens { - if let proc_macro2::TokenTree::Ident(ident) = token { - // this detects the 'serde' part in #[serde(rename_all = "UPPERCASE")] - // we use get_attribute to make sure we've gotten the right attribute, - // hence, we'll ignore it here - } else if let proc_macro2::TokenTree::Group(group) = token { - // this detects the '(...)' part in #[serde(rename_all = "UPPERCASE", tag = "type")] - // we can use this to get the value of a particular argument - // or to see if it exists at all - - // make sure the delimiter is what we're expecting - if group.delimiter() != proc_macro2::Delimiter::Parenthesis { - continue; - } - - let name_value_pairs = ::syn::parse::Parser::parse2( - Punctuated::::parse_terminated, - group.stream(), - ); - - if name_value_pairs.is_err() { - let comma_seperated_values = ::syn::parse::Parser::parse2( - Punctuated::::parse_terminated, - group.stream(), - ); - - if comma_seperated_values.is_err() { - continue; - } - - let comma_seperated_values = comma_seperated_values.unwrap(); - - for comma_seperated_value in comma_seperated_values { - match comma_seperated_value { - syn::Expr::Path(expr_path) => { - let segments = expr_path.path.segments; - - if segments.is_empty() { - continue; - } - - if segments[0].ident.to_string().eq(arg) { - found = true; - value = String::from(arg); - - break; - } - } - _ => continue, - } - } - - continue; - } - - let name_value_pairs = name_value_pairs.unwrap(); - - for name_value_pair in name_value_pairs { - if name_value_pair.path.is_ident(arg) { - found = true; - value = name_value_pair.value.to_token_stream().to_string(); - // removes quotes around the value - value = value[1..value.len() - 1].to_string(); - - break; - } - } - } - } +/// # Example +/// ```rust +/// let expression: syn::Result = syn::parse_str("std::mem::replace"); +/// assert!(expression.is_ok()); +/// let exp_path = expression.unwrap(); +/// let is_path = match exp_path { +/// syn::Expr::Path(path_expression) if !path_expression.path.segments.is_empty() => Some(path_expression), +/// _ => None, +/// }; +/// assert!(is_path.is_some()); +/// let path = is_path.unwrap(); +/// // Segments are syn::PathSegment objects, where their `ident`s are +/// // syn::Ident objects, representing the text between `::` separators. +/// assert_eq!(path.path.segments.len(), 3); +/// ``` +fn check_expression_is_path(expr: Expr) -> Option { + match expr { + Expr::Path(expr_path) if !expr_path.path.segments.is_empty() => Some(expr_path), + _ => None, + } +} + +fn check_token(token: proc_macro2::TokenTree, arg: &str) -> Option { + // this detects the '(...)' part in #[serde(rename_all = "UPPERCASE", tag = "type")] + // we can use this to get the value of a particular argument + // or to see if it exists at all + let proc_macro2::TokenTree::Group(group) = token else { return None; }; + + // Make sure the delimiter is what we're expecting, otherwise return right away. + if group.delimiter() != proc_macro2::Delimiter::Parenthesis { + return None; + } - if found { - return Some(value); - } else { - return None; + // First check to see if the group is a `MetaNameValue`, (.e.g `feature = "nightly"`) + match Parser::parse2(Punctuated::::parse_terminated, group.stream()) { + Ok(name_value_pairs) => { + // If so move the pairs into an iterator + name_value_pairs + .into_iter() + // checking that the `path` component is of length 1 equal to the given arg. + .find(|nvp| nvp.path.is_ident(arg)) + // If it is, get the `value` component, ("nightly" from the example above). + .map(|nvp| nvp.value.to_token_stream().to_string()) + // Then remove the literal quotes around the value. + .map(|value| value[1..value.len() - 1].to_owned()) + } + Err(_) => { + // Otherwise, check to see if the group is a `Expr` of `Punctuated<_, P>` attributes, + // separated by `P`, `Token![,]` in this case. + // (.e.g `default, skip_serializing`) + Parser::parse2(Punctuated::::parse_terminated, group.stream()) + // If the expression cannot be parsed, return None + .map_or(None, |comma_seperated_values| { + // Otherwise move the pairs into an iterator + comma_seperated_values + .into_iter() + // Checking each is a `ExprPath`, object, yielding elements while the method + // returns true. + .map_while(check_expression_is_path) + // Check if any yielded paths equal `arg` + .any(|expr_path| expr_path.path.segments[0].ident.to_string().eq(arg)) + // If so, return `Some(arg)`, otherwise `None`. + .then_some(arg.to_owned()) + }) } } - None +} + +/// Get the value matching an attribute and argument combination +/// +/// For #[serde(tag = "type")], get_attribute_arg("serde", "tag", attributes) will return Some("type") +/// For #[derive(Serialize_repr)], get_attribute_arg("derive", "Serialize_repr", attributes) will +/// return Some("Serialize_repr") +pub fn get_attribute_arg(needle: &str, arg: &str, attributes: &[syn::Attribute]) -> Option { + // check if attribute list contains the argument we are interested in + // TODO: don't use a for loop here or iterator here + get_attribute(needle, attributes).and_then(|attr| { + attr.meta + .to_token_stream() + .into_iter() + .filter_map(|token| check_token(token, arg)) + .next() + }) } /// Check has an attribute arg. @@ -106,81 +116,120 @@ pub fn has_attribute_arg(needle: &str, arg: &str, attributes: &[syn::Attribute]) get_attribute_arg(needle, arg, attributes).is_some() } +/// Checks if a [`proc_macro2::TokenTree`] is a literal character ('a'), string ("hello"), +/// number (2.3), etc. If so, trim it to retain only the comment body, returning `Some(comment)`, +/// otherwise returns `None`. +/// +/// Given an attribute like `#[doc = "Single line doc comments"]`, only `Single line doc comments` +/// should be returned. +fn check_doc_tokens(tt: proc_macro2::TokenTree) -> Option { + let proc_macro2::TokenTree::Literal(comment) = tt else { return None; }; + let c = comment.to_string(); + Some(c[1..c.len() - 1].trim().to_owned()) +} + +/// Checks if an attribute's [`syn::Meta`] property is a name-value pair, like +/// `doc = "Single line doc comments"`. if so, continues to check that the value +/// (.e.g. "Single line doc comments") is a valid [`proc_macro2::TokenTree::Literal`], +/// if so, add it to the collection of comment strings. +fn check_doc_attribute(attr: &syn::Attribute) -> Vec { + // Check if the attribute's meta is a NameValue, otherwise return + // right away. + let syn::Meta::NameValue(ref nv) = attr.meta else { return Default::default(); }; + + // Convert the value to a token stream, then iterate it, collecting + // only valid comment string. + nv.value + .to_token_stream() + .into_iter() + .filter_map(check_doc_tokens) + .collect::>() +} + /// Get the doc string comments from the syn::attributes /// note: the compiler transforms doc comments into attributes /// see: https://docs.rs/syn/2.0.28/syn/struct.Attribute.html#doc-comments -pub fn get_comments(attributes: Vec) -> Vec { - let mut comments: Vec = vec![]; - - for attribute in attributes { - let mut is_doc = false; - for segment in attribute.path().segments.clone() { - if segment.ident == "doc" { - is_doc = true; - break; - } - } - - if is_doc { - match attribute.meta { - syn::Meta::NameValue(name_value) => { - let comment = name_value.value.to_token_stream(); - - for token in comment.into_iter() { - if let proc_macro2::TokenTree::Literal(comment) = token { - let comment = comment.to_string(); - let comment = comment[1..comment.len() - 1].trim(); - comments.push(comment.to_string()); - } - } - } - _ => continue, - } - } - } +pub fn get_comments(mut attributes: Vec) -> Vec { + // Retains only attributes that have segments equal to "doc". + // (.e.g. #[doc = "Single line doc comments"]) + attributes.retain(|x| { + x.path() + .segments + .iter() + .any(|seg| seg.ident == "doc") + }); - comments + attributes + .iter() + .flat_map(check_doc_attribute) + .collect::>() } +/// Generate a string filled with `indentation_amount` white-space +/// literal `chars`. +/// +/// # Example +/// ```rust +/// for i in 0..64 { +/// let indentations: String = (0..i).map(|_| '\u{0020}').collect(); +/// assert_eq!(indentations.len(), i); +/// } +/// ``` pub fn build_indentation(indentation_amount: i8) -> String { - let mut indent = "".to_string(); - for _ in 0..indentation_amount { - indent.push(' '); - } - indent + // Change from empty whitespace char to Unicode whitespace + // representation for a bit more clarity. + (0..indentation_amount).map(|_| '\u{0020}').collect() } pub fn extract_struct_generics(s: syn::Generics) -> String { - let mut generic_params: Vec = vec![]; - - for generic_param in s.params { - if let syn::GenericParam::Type(ty) = generic_param { - generic_params.push(ty.ident.to_string()); - } - } + let out: Vec = s + .params + .into_iter() + .filter_map(|gp| { + if let syn::GenericParam::Type(ty) = gp { + Some(ty) + } else { + None + } + }) + .map(|ty| ty.ident.to_string()) + .collect(); - if generic_params.is_empty() { - "".to_string() - } else { - format!("<{list}>", list = generic_params.join(", ")) - } + out.is_empty() + .then(Default::default) + .unwrap_or(format!("<{}>", out.join(", "))) } /// Get the attribute matching needle name. -pub fn get_attribute(needle: &str, attributes: &[syn::Attribute]) -> Option { +pub fn get_attribute<'a>( + needle: &'a str, + attributes: &'a [syn::Attribute], +) -> Option<&'a syn::Attribute> { // if multiple attributes pass the conditions // we still want to return the last - for attr in attributes.iter().rev() { - // check if correct attribute - if attr - .meta - .path() - .segments + attributes + .iter() + // Reverse the iterator to check the last attribute first. + .rev() + // From the `find` documentation: + // "find() is short-circuiting; + // in other words, it will stop processing as soon as the closure returns true." + .find(|attr| { + // Checks if any segments in the iterator equal `needle`. Returns + // true if a match is found, otherwise false. + attr.meta + .path() + .segments + .iter() + .any(|segment| segment.ident == needle) + }) +} + +pub(crate) fn parse_serde_case(val: impl Into>) -> Option { + val.into().and_then(|x| { + RENAME_RULES .iter() - .any(|segment| segment.ident == needle) - { - return Some(attr.clone()); - } - } - None + .find(|(name, _)| name == &x) + .map(|(_, rule)| *rule) + }) } diff --git a/test/directory_input/directory/1_book.rs b/test/directory_input/directory/1_book.rs index c2d94f6..a436819 100644 --- a/test/directory_input/directory/1_book.rs +++ b/test/directory_input/directory/1_book.rs @@ -12,3 +12,17 @@ struct Book { /// by users. user_reviews: Option>, } + +#[tsync] +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +/// Book struct with camelCase field names. +struct BookCamel { + /// Name of the book. + name: String, + /// Chapters of the book. + chapters: Vec, + /// Reviews of the book + /// by users. + user_reviews: Option>, +} \ No newline at end of file diff --git a/test/directory_input/typescript.d.ts b/test/directory_input/typescript.d.ts index 25e5f0b..cc28851 100644 --- a/test/directory_input/typescript.d.ts +++ b/test/directory_input/typescript.d.ts @@ -13,6 +13,19 @@ interface Book { user_reviews?: Array; } +/** Book struct with camelCase field names. */ +interface BookCamel { + /** Name of the book. */ + name: string; + /** Chapters of the book. */ + chapters: Array; + /** + * Reviews of the book + * by users. + */ + userReviews?: Array; +} + /** * Multiple line comments * are formatted on diff --git a/test/doc_comments/typescript.d.ts b/test/doc_comments/typescript.d.ts index b732e36..e168fba 100644 --- a/test/doc_comments/typescript.d.ts +++ b/test/doc_comments/typescript.d.ts @@ -13,7 +13,7 @@ type EnumTest__One = { type EnumTest__Three = { type: "THREE"; /** enum struct property comment */ - id: string; + ID: string; }; /** struct comment */ diff --git a/test/doc_comments/typescript.ts b/test/doc_comments/typescript.ts index 4eaf9d9..df1256e 100644 --- a/test/doc_comments/typescript.ts +++ b/test/doc_comments/typescript.ts @@ -13,7 +13,7 @@ type EnumTest__One = { type EnumTest__Three = { type: "THREE"; /** enum struct property comment */ - id: string; + ID: string; }; /** struct comment */ diff --git a/test/enum/rust.rs b/test/enum/rust.rs index 06b6f77..75b8311 100644 --- a/test/enum/rust.rs +++ b/test/enum/rust.rs @@ -41,6 +41,14 @@ struct CustomTopping { expires_in: NaiveDateTime, } +#[tsync] +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +struct CustomToppingCamel { + name: String, + expires_in: NaiveDateTime, +} + /// All Unit Enums go to union of constant strings /// even if have explicit numeric annotations /// There is no case renaming on default diff --git a/test/enum/typescript.d.ts b/test/enum/typescript.d.ts index e337703..fa836ed 100644 --- a/test/enum/typescript.d.ts +++ b/test/enum/typescript.d.ts @@ -18,7 +18,7 @@ type InternalTopping__Pepperoni = { /** For cheese lovers */ type InternalTopping__ExtraCheese = { type: "EXTRA CHEESE"; - kind: string; + KIND: string; }; /** @@ -51,6 +51,11 @@ interface CustomTopping { expires_in: Date; } +interface CustomToppingCamel { + name: string; + expiresIn: Date; +} + /** * All Unit Enums go to union of constant strings * even if have explicit numeric annotations diff --git a/test/enum/typescript.ts b/test/enum/typescript.ts index 94bbbd6..1d4e3ed 100644 --- a/test/enum/typescript.ts +++ b/test/enum/typescript.ts @@ -18,7 +18,7 @@ type InternalTopping__Pepperoni = { /** For cheese lovers */ type InternalTopping__ExtraCheese = { type: "EXTRA CHEESE"; - kind: string; + KIND: string; }; /** @@ -51,6 +51,11 @@ export interface CustomTopping { expires_in: Date; } +export interface CustomToppingCamel { + name: string; + expiresIn: Date; +} + /** * All Unit Enums go to union of constant strings * even if have explicit numeric annotations diff --git a/test/struct/rust.rs b/test/struct/rust.rs index de3ebe0..dddb6ba 100644 --- a/test/struct/rust.rs +++ b/test/struct/rust.rs @@ -1,6 +1,6 @@ /// test/rust.rs use tsync::tsync; - +use serde::Serialize; /// Doc comments are preserved too! #[tsync] struct Book { @@ -13,6 +13,20 @@ struct Book { user_reviews: Option>, } +#[tsync] +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +/// Book struct with camelCase field names. +struct BookCamel { + /// Name of the book. + name: String, + /// Chapters of the book. + chapters: Vec, + /// Reviews of the book + /// by users. + user_reviews: Option>, +} + /// Multiple line comments /// are formatted on /// separate lines @@ -28,3 +42,13 @@ struct PaginationResult { items: Vec, total_items: number, } + + +#[tsync] +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +/// Generic struct test with camelCase field names. +struct PaginationResultCamel { + items: Vec, + total_items: number, +} \ No newline at end of file diff --git a/test/struct/typescript.d.ts b/test/struct/typescript.d.ts index 29fbdaa..5d70aba 100644 --- a/test/struct/typescript.d.ts +++ b/test/struct/typescript.d.ts @@ -13,6 +13,19 @@ interface Book { user_reviews?: Array; } +/** Book struct with camelCase field names. */ +interface BookCamel { + /** Name of the book. */ + name: string; + /** Chapters of the book. */ + chapters: Array; + /** + * Reviews of the book + * by users. + */ + userReviews?: Array; +} + /** * Multiple line comments * are formatted on @@ -28,3 +41,9 @@ interface PaginationResult { items: Array; total_items: number; } + +/** Generic struct test with camelCase field names. */ +interface PaginationResultCamel { + items: Array; + totalItems: number; +} diff --git a/test/struct/typescript.ts b/test/struct/typescript.ts index 7f15440..0093618 100644 --- a/test/struct/typescript.ts +++ b/test/struct/typescript.ts @@ -13,6 +13,19 @@ export interface Book { user_reviews?: Array; } +/** Book struct with camelCase field names. */ +export interface BookCamel { + /** Name of the book. */ + name: string; + /** Chapters of the book. */ + chapters: Array; + /** + * Reviews of the book + * by users. + */ + userReviews?: Array; +} + /** * Multiple line comments * are formatted on @@ -28,3 +41,9 @@ export interface PaginationResult { items: Array; total_items: number; } + +/** Generic struct test with camelCase field names. */ +export interface PaginationResultCamel { + items: Array; + totalItems: number; +}