Skip to content

Commit 2db0fa5

Browse files
committed
Initial commit
0 parents  commit 2db0fa5

17 files changed

+1644
-0
lines changed

Diff for: .gitignore

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
/target
2+
node_modules/
3+
tests/.tmp*
4+
Cargo.lock
5+
yarn.lock

Diff for: Cargo.toml

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
[package]
2+
name = "bincode-typescript"
3+
license = "MIT"
4+
version = "0.1.0"
5+
authors = ["Tim Fish <[email protected]>"]
6+
edition = "2018"
7+
repository = "https://github.com/timfish/bincode-typescript"
8+
description = "Generates TypeScript serialisation and deserialisation code from Rust structs and enums"
9+
readme = "README.md"
10+
11+
[lib]
12+
name = "bincode_typescript"
13+
path = "src/lib.rs"
14+
15+
[[bin]]
16+
name = "bincode-typescript"
17+
path = "src/bin.rs"
18+
19+
20+
[dependencies]
21+
syn = { version = "1.0", features = ["full", "extra-traits", "visit"] }
22+
askama = "0.8"
23+
24+
[dev-dependencies]
25+
serde = { version = "1", features = ["derive"] }
26+
bincode = "1.3"
27+
tempfile = "3"

Diff for: README.md

+85
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
# bincode-typescript
2+
3+
Generates TypeScript serialisation and deserialisation code from Rust structs
4+
and enums
5+
6+
## Goals
7+
8+
- Generate TypeScript code directly from Rust source
9+
- Work from `build.rs`
10+
- Avoid Object Orientated TypeScript for better tree-shaking and optimisation
11+
- TypeScript for enums/structs should be ergonomic and high performance
12+
- Use `const enum` for Unit enums and respect discriminant values!
13+
- Use `TypedArray`s for `Vec<{integer,float}>` for greater performance
14+
15+
## Status
16+
17+
I'm pretty new to Rust and I've just hacked around until the tests pass 🤷‍♂️
18+
19+
There is much room for improvement and PRs are welcome!
20+
21+
## Current Issues & Limitations
22+
23+
- All values must be owned
24+
- Generic structs/enums will almost certainly panic
25+
- All types must be in a single file
26+
- Serde attributes are not currently respected
27+
- `Vec<T>` are always converted to `Uint8Array/Int8Array/etc` whenever possible
28+
and this might not be particularly ergonomic from TypeScript.
29+
30+
## Example via `build.rs`
31+
32+
There is currently a single `bool` option to enable support for node.js
33+
`Buffer`, so if you are running in the browser you probably don't want this enabled.
34+
35+
```rust
36+
bincode_typescript::from_file("./src/types.rs", "./ts/types.ts", false);
37+
```
38+
39+
## Example via CLI
40+
41+
There is currently a single option (`--support-buffer`) to enable support for node.js
42+
`Buffer`.
43+
44+
```shell
45+
./bincode-typescript --support-buffer ./src/types.rs > ./ts/types.ts
46+
```
47+
48+
## Tests
49+
50+
Before running the tests, ensure that you have all the node.js dependencies
51+
installed by running `yarn` or `npm i`.
52+
53+
The tests check serialisation and deserialisation from generated TypeScript by
54+
round-tripping encoded data via stdio and asserting the expected values.
55+
56+
## Prior Art
57+
58+
This builds on (ie. much TypeScript stolen from) the following projects.
59+
60+
_The stated pros and cons are just personal opinion!_
61+
62+
### [`ts-rust-bridge`](https://github.com/twop/ts-rust-bridge)
63+
64+
### Pros:
65+
66+
- Function based TypeScript API
67+
- Great ergonomics for enums with combination of `type` + `interface` + `module`
68+
69+
### Cons:
70+
71+
- Generates both Rust and TypeScript from a DSL. (**I want Rust to be the source
72+
of truth**).
73+
- Does not use `const enum` for Unit enums
74+
75+
### [`serde-reflection/serde-generate`](https://github.com/novifinancial/serde-reflection/pull/59)
76+
77+
### Pros:
78+
79+
- Uses `serde` so no messing around parsing Rust
80+
81+
### Cons:
82+
83+
- All types have to be run through the registry after build so wont work from `build.rs`
84+
- TypeScript classes wrap every type and use inheritance (ie. no `const enum`)
85+
- Runtime TypeScript is separate

Diff for: package.json

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"name": "bincode-typescript",
3+
"version": "0.1.0",
4+
"main": "index.js",
5+
"author": "Tim Fish <[email protected]>",
6+
"license": "MIT",
7+
"devDependencies": {
8+
"@types/node": "^12.12.2",
9+
"ts-node": "^9.0.0",
10+
"typescript": "^4.0.3",
11+
"get-stdin": "^8.0.0"
12+
}
13+
}

Diff for: src/bin.rs

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
use bincode_typescript::from_string;
2+
use std::{
3+
error::Error,
4+
fs::{self},
5+
};
6+
7+
fn main() -> Result<(), Box<dyn Error>> {
8+
let args: Vec<String> = std::env::args().collect();
9+
let support_buffer = args.contains(&"--support-buffer".to_string());
10+
let input = args.last().expect("Could not find input file name");
11+
12+
let rust = fs::read_to_string(input)?;
13+
println!("{}", from_string(&rust, support_buffer)?);
14+
15+
Ok(())
16+
}

Diff for: src/enums.rs

+115
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
use crate::types::RustType;
2+
use askama::Template;
3+
use syn::{Expr, Fields, ItemEnum, Lit};
4+
5+
#[derive(Clone, Debug)]
6+
pub enum EnumVariantDeclaration {
7+
Unit {
8+
ident: String,
9+
value: String,
10+
},
11+
Unnamed {
12+
ident: String,
13+
types: Vec<RustType>,
14+
},
15+
Named {
16+
ident: String,
17+
types: Vec<(String, RustType)>,
18+
},
19+
}
20+
21+
impl EnumVariantDeclaration {
22+
pub fn ident(&self) -> String {
23+
match self {
24+
EnumVariantDeclaration::Unit { ident, .. } => ident.to_string(),
25+
EnumVariantDeclaration::Unnamed { ident, .. } => ident.to_string(),
26+
EnumVariantDeclaration::Named { ident, .. } => ident.to_string(),
27+
}
28+
}
29+
30+
pub fn value(&self) -> String {
31+
match self {
32+
EnumVariantDeclaration::Unit { value, .. } => value.to_string(),
33+
_ => panic!("should not happen"),
34+
}
35+
}
36+
37+
pub fn first_type(&self) -> RustType {
38+
match self {
39+
EnumVariantDeclaration::Unnamed { types, .. } => types[0].clone(),
40+
_ => panic!("should not happen"),
41+
}
42+
}
43+
}
44+
45+
#[derive(Clone, Debug, Template)]
46+
#[template(path = "enum.ts.j2", escape = "txt")]
47+
pub struct EnumDeclaration {
48+
ident: String,
49+
variants: Vec<EnumVariantDeclaration>,
50+
}
51+
52+
impl EnumDeclaration {
53+
pub fn new(ident: String) -> Self {
54+
EnumDeclaration {
55+
ident,
56+
variants: Default::default(),
57+
}
58+
}
59+
60+
pub fn is_unit(&self) -> bool {
61+
self.variants
62+
.iter()
63+
.all(|v| matches!(v, EnumVariantDeclaration::Unit { .. }))
64+
}
65+
}
66+
67+
impl From<ItemEnum> for EnumDeclaration {
68+
fn from(node: ItemEnum) -> Self {
69+
let mut e = EnumDeclaration::new(node.ident.to_string());
70+
71+
for (i, variant) in node.variants.into_iter().enumerate() {
72+
match variant.fields {
73+
Fields::Unit => {
74+
let value = if let Some(discriminant) = variant.discriminant {
75+
if let Expr::Lit(lit) = discriminant.1 {
76+
if let Lit::Int(val) = lit.lit {
77+
Some(val.to_string())
78+
} else {
79+
None
80+
}
81+
} else {
82+
None
83+
}
84+
} else {
85+
None
86+
};
87+
88+
e.variants.push(EnumVariantDeclaration::Unit {
89+
ident: variant.ident.to_string(),
90+
value: value.unwrap_or_else(|| i.to_string()),
91+
})
92+
}
93+
Fields::Named(fields) => e.variants.push(EnumVariantDeclaration::Named {
94+
ident: variant.ident.to_string(),
95+
types: fields
96+
.named
97+
.into_iter()
98+
.map(|v| {
99+
(
100+
v.ident.expect("Should have an ident").to_string(),
101+
v.ty.into(),
102+
)
103+
})
104+
.collect(),
105+
}),
106+
Fields::Unnamed(fields) => e.variants.push(EnumVariantDeclaration::Unnamed {
107+
ident: variant.ident.to_string(),
108+
types: fields.unnamed.into_iter().map(|v| v.ty.into()).collect(),
109+
}),
110+
}
111+
}
112+
113+
e
114+
}
115+
}

Diff for: src/lib.rs

+74
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
use askama::Template;
2+
use enums::EnumDeclaration;
3+
use std::{
4+
error::Error,
5+
fs::{self},
6+
path::Path,
7+
};
8+
use structs::StructDeclaration;
9+
use syn::{
10+
visit::{self, Visit},
11+
ItemEnum, ItemStruct,
12+
};
13+
14+
mod enums;
15+
mod structs;
16+
mod types;
17+
18+
const NAME: &str = env!("CARGO_PKG_NAME");
19+
const VERSION: &str = env!("CARGO_PKG_VERSION");
20+
21+
#[derive(Clone, Debug, Template)]
22+
#[template(path = "module.ts.j2", escape = "txt")]
23+
struct CodeVisitor {
24+
pub pkg_name: String,
25+
pub pkg_version: String,
26+
support_buffer: bool,
27+
pub structs: Vec<StructDeclaration>,
28+
pub enums: Vec<EnumDeclaration>,
29+
}
30+
31+
impl CodeVisitor {
32+
pub fn new(support_buffer: bool) -> Self {
33+
CodeVisitor {
34+
pkg_name: NAME.to_string(),
35+
pkg_version: VERSION.to_string(),
36+
support_buffer,
37+
structs: Default::default(),
38+
enums: Default::default(),
39+
}
40+
}
41+
}
42+
43+
impl<'ast> Visit<'ast> for CodeVisitor {
44+
fn visit_item_struct(&mut self, node: &'ast ItemStruct) {
45+
self.structs.push(node.clone().into());
46+
visit::visit_item_struct(self, node);
47+
}
48+
49+
fn visit_item_enum(&mut self, node: &'ast ItemEnum) {
50+
self.enums.push(node.clone().into());
51+
visit::visit_item_enum(self, node);
52+
}
53+
}
54+
55+
pub fn from_string(input: &str, support_buffer: bool) -> Result<String, Box<dyn Error>> {
56+
let ast = syn::parse_file(input)?;
57+
let mut enum_visit = CodeVisitor::new(support_buffer);
58+
enum_visit.visit_file(&ast);
59+
60+
Ok(enum_visit.render()?)
61+
}
62+
63+
pub fn from_file<P1: AsRef<Path>, P2: AsRef<Path>>(
64+
input: P1,
65+
output: P2,
66+
support_buffer: bool,
67+
) -> Result<(), Box<dyn Error>> {
68+
fs::create_dir_all(&output.as_ref().parent().unwrap())?;
69+
let rust = fs::read_to_string(input)?;
70+
let typescript = from_string(&rust, support_buffer)?;
71+
fs::write(output, typescript)?;
72+
73+
Ok(())
74+
}

Diff for: src/structs.rs

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
use crate::types::RustType;
2+
use askama::Template;
3+
use syn::ItemStruct;
4+
5+
#[derive(Clone, Debug, Template)]
6+
#[template(path = "struct.ts.j2", escape = "txt")]
7+
pub struct StructDeclaration {
8+
ident: String,
9+
fields: Vec<(Option<String>, RustType)>,
10+
}
11+
12+
impl StructDeclaration {
13+
pub fn new(ident: String) -> Self {
14+
StructDeclaration {
15+
ident,
16+
fields: Default::default(),
17+
}
18+
}
19+
20+
pub fn is_tuple(&self) -> bool {
21+
self.fields.iter().all(|(id, _)| id.is_none())
22+
}
23+
}
24+
25+
impl From<ItemStruct> for StructDeclaration {
26+
fn from(node: ItemStruct) -> Self {
27+
let mut e = StructDeclaration::new(node.ident.to_string());
28+
29+
for field in node.fields.into_iter() {
30+
e.fields
31+
.push((field.ident.map(|i| i.to_string()), field.ty.into()))
32+
}
33+
34+
e
35+
}
36+
}

0 commit comments

Comments
 (0)