Skip to content

Commit 05ec83a

Browse files
authored
Code refactoring (#5)
1 parent bc6933c commit 05ec83a

File tree

11 files changed

+266
-202
lines changed

11 files changed

+266
-202
lines changed

Cargo.lock

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ members = [ "crates/*" ]
33
resolver = "2"
44

55
[workspace.package]
6-
version = "0.1.5"
6+
version = "0.1.6"
77
license = "MIT"
88
edition = "2021"
99
repository = "https://github.com/dcodesdev/ts-bind"

README.md

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -150,13 +150,24 @@ export interface User {
150150
}
151151
```
152152

153-
## Attributes
153+
## Struct-Level Attributes
154154

155-
The `ts_bind` attribute supports the following optional arguments:
155+
The `ts_bind` attribute supports the following optional arguments for the entire struct:
156156

157-
| Argument | Description |
158-
| -------- | ------------------------------- |
159-
| `rename` | Rename the generated interface. |
157+
| Argument | Description |
158+
| ------------ | ------------------------------- |
159+
| `rename` | Rename the generated interface. |
160+
| `rename_all` | Rename all fields by case. |
161+
| `export` | Custom export path. |
162+
163+
### Field-Level Attributes
164+
165+
The `ts_bind` attribute supports the following optional arguments for individual fields:
166+
167+
| Argument | Description |
168+
| -------- | ----------------- |
169+
| `rename` | Rename the field. |
170+
| `skip` | Skip the field. |
160171

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

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

192+
- [x] `#[ts_bind(export = "path/to/export")]` custom export path.
193+
- [x] `#[ts_bind(rename_all = "camelCase")]` attribute to rename all fields.
194+
- [x] `#[ts_bind(skip)]` attribute to skip fields.
181195
- [ ] Support for enums.
182-
- [ ] `#[ts_bind(export = "path/to/export")]` custom export path.
183-
- [ ] `#[ts_bind(rename_all = "camelCase")]` attribute to rename all fields.
184-
- [ ] `#[ts_bind(skip)]` attribute to skip fields.
185196
- [ ] `#[ts_bind(skip_if = "condition")]` attribute to skip fields based on a condition.
186197

187198
## Contributing

crates/macros/src/files.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
use std::{
2+
fs::{create_dir_all, write},
3+
path::PathBuf,
4+
};
5+
6+
pub fn write_to_file(path: &PathBuf, content: &str) -> anyhow::Result<()> {
7+
let parent = path.parent().ok_or(anyhow::anyhow!(
8+
"Failed to get parent directory of path: {}",
9+
path.display()
10+
))?;
11+
12+
create_dir_all(parent)?;
13+
14+
write(path, content)?;
15+
16+
Ok(())
17+
}

crates/macros/src/lib.rs

Lines changed: 8 additions & 161 deletions
Original file line numberDiff line numberDiff line change
@@ -1,175 +1,22 @@
1-
use std::{
2-
fs::{create_dir_all, write},
3-
path::PathBuf,
4-
};
5-
6-
use convert_case::{Case, Casing};
71
use error::ToCompileError;
8-
use parsers::struc::{get_nested_value, parse_struct_fields};
92
use proc_macro::TokenStream;
10-
use quote::quote;
113
use syn::{parse_macro_input, DeriveInput};
12-
use ts::ts_map::ts_rs_map;
4+
use ts_bind::handle_ts_bind;
135

146
mod error;
7+
mod files;
158
mod parsers;
9+
mod rename_all;
10+
mod struct_attrs;
1611
mod ts;
17-
18-
#[derive(Debug)]
19-
enum RenameAll {
20-
CamelCase,
21-
SnakeCase,
22-
UpperCase,
23-
LowerCase,
24-
PascalCase,
25-
// TODO: kebab
26-
//KebabCase,
27-
}
28-
29-
impl RenameAll {
30-
pub fn to_case(&self, s: &str) -> String {
31-
match self {
32-
Self::CamelCase => s.to_case(Case::Camel),
33-
Self::SnakeCase => s.to_case(Case::Snake),
34-
Self::UpperCase => s.to_case(Case::Upper),
35-
Self::LowerCase => s.to_case(Case::Lower),
36-
Self::PascalCase => s.to_case(Case::Pascal),
37-
}
38-
}
39-
}
40-
41-
#[derive(Default, Debug)]
42-
struct StructAttributes {
43-
pub rename_all: Option<RenameAll>,
44-
pub rename: Option<String>,
45-
pub export: Option<PathBuf>,
46-
}
47-
48-
impl StructAttributes {
49-
pub fn new() -> Self {
50-
Self::default()
51-
}
52-
}
12+
mod ts_bind;
5313

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

58-
let attrs = &input.attrs;
59-
60-
let mut struct_attrs = StructAttributes::new();
61-
attrs.iter().for_each(|attr| {
62-
if attr.path().is_ident("ts_bind") {
63-
attr.parse_nested_meta(|meta| {
64-
let path = &meta.path;
65-
if path.is_ident("rename") {
66-
let value = get_nested_value(&meta).expect("Failed to parse rename attribute");
67-
68-
struct_attrs.rename = Some(value);
69-
}
70-
if path.is_ident("rename_all") {
71-
let value =
72-
get_nested_value(&meta).expect("Failed to parse rename_all attribute");
73-
74-
match value.as_str() {
75-
"camelCase" => {
76-
struct_attrs.rename_all = Some(RenameAll::CamelCase);
77-
}
78-
"snake_case" => {
79-
struct_attrs.rename_all = Some(RenameAll::SnakeCase);
80-
}
81-
"UPPERCASE" => {
82-
struct_attrs.rename_all = Some(RenameAll::UpperCase);
83-
}
84-
"lowercase" => {
85-
struct_attrs.rename_all = Some(RenameAll::LowerCase);
86-
}
87-
"PascalCase" => {
88-
struct_attrs.rename_all = Some(RenameAll::PascalCase);
89-
}
90-
_ => {
91-
panic!("Invalid attribute name: {}", value);
92-
}
93-
}
94-
}
95-
if path.is_ident("export") {
96-
let value = get_nested_value(&meta).expect("Failed to parse export attribute");
97-
98-
struct_attrs.export = Some(PathBuf::from(value));
99-
}
100-
101-
Ok(())
102-
})
103-
.expect("Failed to parse nested meta");
104-
}
105-
});
106-
107-
let name = if let Some(rename) = struct_attrs.rename {
108-
rename
109-
} else {
110-
input.ident.to_string()
111-
};
112-
113-
let fields = parse_struct_fields(&input);
114-
115-
if let Err(e) = fields {
116-
return e.to_compile_error();
18+
match handle_ts_bind(&input) {
19+
Ok(ts) => ts,
20+
Err(e) => e.to_compile_error(),
11721
}
118-
119-
let fields = fields.unwrap();
120-
121-
let mut ts_bind = String::from(format!("\nexport interface {} {{\n", name));
122-
let mut imports = Vec::new();
123-
for (ident, ty, attrs) in fields.iter() {
124-
if attrs.skip {
125-
continue;
126-
}
127-
128-
let field_name = if let Some(rename_all) = &struct_attrs.rename_all {
129-
rename_all.to_case(&ident.to_string())
130-
} else {
131-
ident.to_string()
132-
};
133-
134-
let field_name = attrs.rename.as_ref().unwrap_or(&field_name);
135-
136-
let map_result = ts_rs_map(ty, &mut imports);
137-
138-
ts_bind.push_str(&format!(" {}: {};\n", field_name, map_result));
139-
}
140-
141-
ts_bind.push_str("}");
142-
143-
sort_imports(&mut imports);
144-
for to_import in imports {
145-
ts_bind = format!(
146-
"import type {{ {} }} from \"./{}\";\n{}",
147-
to_import, to_import, ts_bind
148-
);
149-
}
150-
151-
ts_bind = format!(
152-
"// This file was automatically generated by ts_bind, do not modify it manually\n{}",
153-
ts_bind
154-
);
155-
156-
let lib_path = if let Some(export_path) = struct_attrs.export {
157-
export_path.join(format!("{}.ts", name))
158-
} else {
159-
PathBuf::new().join("bindings").join(format!("{}.ts", name))
160-
};
161-
162-
write_to_file(&lib_path, &ts_bind);
163-
164-
quote! {}.into()
165-
}
166-
167-
fn write_to_file(path: &PathBuf, content: &str) {
168-
create_dir_all(path.parent().unwrap()).unwrap();
169-
write(path, content).unwrap();
170-
}
171-
172-
fn sort_imports(imports: &mut Vec<String>) {
173-
imports.sort();
174-
imports.dedup();
17522
}

crates/macros/src/parsers/struc.rs

Lines changed: 22 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,4 @@
1-
use syn::{
2-
meta::ParseNestedMeta, Attribute, Data, DeriveInput, Expr, Fields, Ident, Lit, LitStr, Meta,
3-
MetaNameValue, Type,
4-
};
1+
use syn::{meta::ParseNestedMeta, Attribute, Data, DeriveInput, Fields, Ident, LitStr, Type};
52

63
#[derive(Default)]
74
pub struct FieldAttributes {
@@ -15,9 +12,9 @@ impl FieldAttributes {
1512
}
1613
}
1714

18-
pub fn parse_struct_fields(
19-
input: &DeriveInput,
20-
) -> anyhow::Result<Vec<(Ident, Type, FieldAttributes)>> {
15+
pub type ParsedField = (Ident, Type, FieldAttributes);
16+
17+
pub fn parse_struct_fields(input: &DeriveInput) -> anyhow::Result<Vec<ParsedField>> {
2118
let mut fields_info = Vec::new();
2219

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

4037
for attr in attrs.iter() {
4138
if attr.path().is_ident("ts_bind") {
42-
if let Ok(meta) = attr.parse_args() {
43-
match meta {
44-
Meta::NameValue(meta_name_value) => {
45-
let path = &meta_name_value.path;
46-
if path.is_ident("rename") {
47-
field_attrs.rename = get_meta_name_value(&meta_name_value)?;
39+
attr.parse_nested_meta(|meta| {
40+
let path = &meta.path;
41+
42+
let ident = path.get_ident();
43+
if let Some(ident) = ident {
44+
let ident_str = ident.to_string();
45+
match ident_str.as_str() {
46+
"rename" => {
47+
field_attrs.rename = Some(
48+
get_nested_value(&meta).expect("Failed to parse rename attribute"),
49+
);
4850
}
49-
}
50-
Meta::Path(meta_path) => {
51-
if meta_path.is_ident("skip") {
51+
"skip" => {
5252
field_attrs.skip = true;
5353
}
54+
_ => {
55+
panic!("Invalid attribute name: {}", ident_str);
56+
}
5457
}
55-
Meta::List(_meta_list) => {}
5658
}
57-
}
58-
}
59-
}
6059

61-
Ok(field_attrs)
62-
}
63-
64-
pub fn get_meta_name_value(rename_meta: &MetaNameValue) -> anyhow::Result<Option<String>> {
65-
if let Expr::Lit(lit) = &rename_meta.value {
66-
if let Lit::Str(lit_str) = &lit.lit {
67-
return Ok(Some(lit_str.value()));
60+
Ok(())
61+
})?;
6862
}
69-
} else {
70-
return Err(anyhow::anyhow!("rename attribute must be a string literal"));
7163
}
7264

73-
Ok(None)
65+
Ok(field_attrs)
7466
}
7567

7668
pub fn get_nested_value(meta: &ParseNestedMeta) -> anyhow::Result<String> {

crates/macros/src/rename_all.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
use convert_case::{Case, Casing};
2+
3+
#[derive(Debug)]
4+
pub enum RenameAll {
5+
CamelCase,
6+
SnakeCase,
7+
UpperCase,
8+
LowerCase,
9+
PascalCase,
10+
// TODO: kebab
11+
//KebabCase,
12+
}
13+
14+
impl RenameAll {
15+
pub fn to_case(&self, s: &str) -> String {
16+
match self {
17+
Self::CamelCase => s.to_case(Case::Camel),
18+
Self::SnakeCase => s.to_case(Case::Snake),
19+
Self::UpperCase => s.to_case(Case::Upper),
20+
Self::LowerCase => s.to_case(Case::Lower),
21+
Self::PascalCase => s.to_case(Case::Pascal),
22+
}
23+
}
24+
}

0 commit comments

Comments
 (0)