Skip to content

Commit

Permalink
modified to repect serde rename directive for struct fields (#47)
Browse files Browse the repository at this point in the history
* modified to include repect serde rename directive for struct fields

* Updated test results.

* Add comments according to PR review.

* Add better method to handle "Cow<'_, T>"

* Fix empty directory, used the wrong path object.

* fix typo

* Fix Cow<'_, T> types.

---------

Co-authored-by: Aaron Hughes <33838207+ahughes-42@users.noreply.github.com>
  • Loading branch information
aaronh-mood and aaronh-mood authored Jan 2, 2024
1 parent 9cdf01c commit 9f5b950
Show file tree
Hide file tree
Showing 20 changed files with 996 additions and 422 deletions.
369 changes: 369 additions & 0 deletions Cargo.lock

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
246 changes: 125 additions & 121 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<bool> = 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 {
Expand All @@ -32,6 +35,7 @@ macro_rules! check_tsync {
};
}

#[derive(Default)]
pub struct BuildState /*<'a>*/ {
pub types: String,
pub unprocessed_files: Vec<PathBuf>,
Expand All @@ -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<P: AsRef<Path>>(
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<P: AsRef<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<P: AsRef<Path>>(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<DirEntry>, path: &Path) -> Option<DirEntry> {
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<P: AsRef<Path>>(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<PathBuf>, 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::<PathBuf>::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!("======================================");
Expand All @@ -215,12 +221,11 @@ pub fn generate_typescript_defs(input: Vec<PathBuf>, 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();
Expand All @@ -234,8 +239,7 @@ pub fn generate_typescript_defs(input: Vec<PathBuf>, 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."),
}
Expand Down
9 changes: 4 additions & 5 deletions src/to_typescript/consts.rs
Original file line number Diff line number Diff line change
@@ -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;
}

Expand Down Expand Up @@ -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());
}
}
Expand Down
Loading

0 comments on commit 9f5b950

Please sign in to comment.