Skip to content

Code refactoring #5

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 20 commits into from
Aug 8, 2024
Merged
4 changes: 2 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ members = [ "crates/*" ]
resolver = "2"

[workspace.package]
version = "0.1.5"
version = "0.1.6"
license = "MIT"
edition = "2021"
repository = "https://github.com/dcodesdev/ts-bind"
Expand Down
27 changes: 19 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,13 +150,24 @@ export interface User {
}
```

## Attributes
## Struct-Level Attributes

The `ts_bind` attribute supports the following optional arguments:
The `ts_bind` attribute supports the following optional arguments for the entire struct:

| Argument | Description |
| -------- | ------------------------------- |
| `rename` | Rename the generated interface. |
| Argument | Description |
| ------------ | ------------------------------- |
| `rename` | Rename the generated interface. |
| `rename_all` | Rename all fields by case. |
| `export` | Custom export path. |

### Field-Level Attributes

The `ts_bind` attribute supports the following optional arguments for individual fields:

| Argument | Description |
| -------- | ----------------- |
| `rename` | Rename the field. |
| `skip` | Skip the field. |

```rust
#[derive(TsBind)]
Expand All @@ -178,10 +189,10 @@ export interface User {

The library is far from complete. Here are some of the features that are planned:

- [x] `#[ts_bind(export = "path/to/export")]` custom export path.
- [x] `#[ts_bind(rename_all = "camelCase")]` attribute to rename all fields.
- [x] `#[ts_bind(skip)]` attribute to skip fields.
- [ ] Support for enums.
- [ ] `#[ts_bind(export = "path/to/export")]` custom export path.
- [ ] `#[ts_bind(rename_all = "camelCase")]` attribute to rename all fields.
- [ ] `#[ts_bind(skip)]` attribute to skip fields.
- [ ] `#[ts_bind(skip_if = "condition")]` attribute to skip fields based on a condition.

## Contributing
Expand Down
17 changes: 17 additions & 0 deletions crates/macros/src/files.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
use std::{
fs::{create_dir_all, write},
path::PathBuf,
};

pub fn write_to_file(path: &PathBuf, content: &str) -> anyhow::Result<()> {
let parent = path.parent().ok_or(anyhow::anyhow!(
"Failed to get parent directory of path: {}",
path.display()
))?;

create_dir_all(parent)?;

write(path, content)?;

Ok(())
}
169 changes: 8 additions & 161 deletions crates/macros/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,175 +1,22 @@
use std::{
fs::{create_dir_all, write},
path::PathBuf,
};

use convert_case::{Case, Casing};
use error::ToCompileError;
use parsers::struc::{get_nested_value, parse_struct_fields};
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput};
use ts::ts_map::ts_rs_map;
use ts_bind::handle_ts_bind;

mod error;
mod files;
mod parsers;
mod rename_all;
mod struct_attrs;
mod ts;

#[derive(Debug)]
enum RenameAll {
CamelCase,
SnakeCase,
UpperCase,
LowerCase,
PascalCase,
// TODO: kebab
//KebabCase,
}

impl RenameAll {
pub fn to_case(&self, s: &str) -> String {
match self {
Self::CamelCase => s.to_case(Case::Camel),
Self::SnakeCase => s.to_case(Case::Snake),
Self::UpperCase => s.to_case(Case::Upper),
Self::LowerCase => s.to_case(Case::Lower),
Self::PascalCase => s.to_case(Case::Pascal),
}
}
}

#[derive(Default, Debug)]
struct StructAttributes {
pub rename_all: Option<RenameAll>,
pub rename: Option<String>,
pub export: Option<PathBuf>,
}

impl StructAttributes {
pub fn new() -> Self {
Self::default()
}
}
mod ts_bind;

#[proc_macro_derive(TsBind, attributes(ts_bind))]
pub fn ts_bind_derive(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);

let attrs = &input.attrs;

let mut struct_attrs = StructAttributes::new();
attrs.iter().for_each(|attr| {
if attr.path().is_ident("ts_bind") {
attr.parse_nested_meta(|meta| {
let path = &meta.path;
if path.is_ident("rename") {
let value = get_nested_value(&meta).expect("Failed to parse rename attribute");

struct_attrs.rename = Some(value);
}
if path.is_ident("rename_all") {
let value =
get_nested_value(&meta).expect("Failed to parse rename_all attribute");

match value.as_str() {
"camelCase" => {
struct_attrs.rename_all = Some(RenameAll::CamelCase);
}
"snake_case" => {
struct_attrs.rename_all = Some(RenameAll::SnakeCase);
}
"UPPERCASE" => {
struct_attrs.rename_all = Some(RenameAll::UpperCase);
}
"lowercase" => {
struct_attrs.rename_all = Some(RenameAll::LowerCase);
}
"PascalCase" => {
struct_attrs.rename_all = Some(RenameAll::PascalCase);
}
_ => {
panic!("Invalid attribute name: {}", value);
}
}
}
if path.is_ident("export") {
let value = get_nested_value(&meta).expect("Failed to parse export attribute");

struct_attrs.export = Some(PathBuf::from(value));
}

Ok(())
})
.expect("Failed to parse nested meta");
}
});

let name = if let Some(rename) = struct_attrs.rename {
rename
} else {
input.ident.to_string()
};

let fields = parse_struct_fields(&input);

if let Err(e) = fields {
return e.to_compile_error();
match handle_ts_bind(&input) {
Ok(ts) => ts,
Err(e) => e.to_compile_error(),
}

let fields = fields.unwrap();

let mut ts_bind = String::from(format!("\nexport interface {} {{\n", name));
let mut imports = Vec::new();
for (ident, ty, attrs) in fields.iter() {
if attrs.skip {
continue;
}

let field_name = if let Some(rename_all) = &struct_attrs.rename_all {
rename_all.to_case(&ident.to_string())
} else {
ident.to_string()
};

let field_name = attrs.rename.as_ref().unwrap_or(&field_name);

let map_result = ts_rs_map(ty, &mut imports);

ts_bind.push_str(&format!(" {}: {};\n", field_name, map_result));
}

ts_bind.push_str("}");

sort_imports(&mut imports);
for to_import in imports {
ts_bind = format!(
"import type {{ {} }} from \"./{}\";\n{}",
to_import, to_import, ts_bind
);
}

ts_bind = format!(
"// This file was automatically generated by ts_bind, do not modify it manually\n{}",
ts_bind
);

let lib_path = if let Some(export_path) = struct_attrs.export {
export_path.join(format!("{}.ts", name))
} else {
PathBuf::new().join("bindings").join(format!("{}.ts", name))
};

write_to_file(&lib_path, &ts_bind);

quote! {}.into()
}

fn write_to_file(path: &PathBuf, content: &str) {
create_dir_all(path.parent().unwrap()).unwrap();
write(path, content).unwrap();
}

fn sort_imports(imports: &mut Vec<String>) {
imports.sort();
imports.dedup();
}
52 changes: 22 additions & 30 deletions crates/macros/src/parsers/struc.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
use syn::{
meta::ParseNestedMeta, Attribute, Data, DeriveInput, Expr, Fields, Ident, Lit, LitStr, Meta,
MetaNameValue, Type,
};
use syn::{meta::ParseNestedMeta, Attribute, Data, DeriveInput, Fields, Ident, LitStr, Type};

#[derive(Default)]
pub struct FieldAttributes {
Expand All @@ -15,9 +12,9 @@ impl FieldAttributes {
}
}

pub fn parse_struct_fields(
input: &DeriveInput,
) -> anyhow::Result<Vec<(Ident, Type, FieldAttributes)>> {
pub type ParsedField = (Ident, Type, FieldAttributes);

pub fn parse_struct_fields(input: &DeriveInput) -> anyhow::Result<Vec<ParsedField>> {
let mut fields_info = Vec::new();

if let Data::Struct(data_struct) = &input.data {
Expand All @@ -39,38 +36,33 @@ fn parse_field_attributes(attrs: &[Attribute]) -> anyhow::Result<FieldAttributes

for attr in attrs.iter() {
if attr.path().is_ident("ts_bind") {
if let Ok(meta) = attr.parse_args() {
match meta {
Meta::NameValue(meta_name_value) => {
let path = &meta_name_value.path;
if path.is_ident("rename") {
field_attrs.rename = get_meta_name_value(&meta_name_value)?;
attr.parse_nested_meta(|meta| {
let path = &meta.path;

let ident = path.get_ident();
if let Some(ident) = ident {
let ident_str = ident.to_string();
match ident_str.as_str() {
"rename" => {
field_attrs.rename = Some(
get_nested_value(&meta).expect("Failed to parse rename attribute"),
);
}
}
Meta::Path(meta_path) => {
if meta_path.is_ident("skip") {
"skip" => {
field_attrs.skip = true;
}
_ => {
panic!("Invalid attribute name: {}", ident_str);
}
}
Meta::List(_meta_list) => {}
}
}
}
}

Ok(field_attrs)
}

pub fn get_meta_name_value(rename_meta: &MetaNameValue) -> anyhow::Result<Option<String>> {
if let Expr::Lit(lit) = &rename_meta.value {
if let Lit::Str(lit_str) = &lit.lit {
return Ok(Some(lit_str.value()));
Ok(())
})?;
}
} else {
return Err(anyhow::anyhow!("rename attribute must be a string literal"));
}

Ok(None)
Ok(field_attrs)
}

pub fn get_nested_value(meta: &ParseNestedMeta) -> anyhow::Result<String> {
Expand Down
24 changes: 24 additions & 0 deletions crates/macros/src/rename_all.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
use convert_case::{Case, Casing};

#[derive(Debug)]
pub enum RenameAll {
CamelCase,
SnakeCase,
UpperCase,
LowerCase,
PascalCase,
// TODO: kebab
//KebabCase,
}

impl RenameAll {
pub fn to_case(&self, s: &str) -> String {
match self {
Self::CamelCase => s.to_case(Case::Camel),
Self::SnakeCase => s.to_case(Case::Snake),
Self::UpperCase => s.to_case(Case::Upper),
Self::LowerCase => s.to_case(Case::Lower),
Self::PascalCase => s.to_case(Case::Pascal),
}
}
}
Loading
Loading