From 778a3fb7eb5b9b88bc4b5668d0ffea86beecfce4 Mon Sep 17 00:00:00 2001 From: novacrazy Date: Tue, 19 Nov 2024 18:34:48 -0600 Subject: [PATCH] Improve typescript generation --- examples/generate_ts_sdk.rs | 53 +++++++++++-- src/models/auth.rs | 4 +- src/models/party/prefs.rs | 7 +- src/models/room.rs | 4 +- src/models/stats.rs | 1 + src/models/user/mod.rs | 2 +- src/models/user/prefs.rs | 36 ++++++++- src/models/util/macros.rs | 29 ++++++-- ts-bindgen/src/registry.rs | 40 +++++++--- ts-bindgen/src/ty.rs | 11 +++ ts-bindgen/ts-bindgen-macros/src/lib.rs | 99 +++++++++++++++++++++---- 11 files changed, 238 insertions(+), 48 deletions(-) diff --git a/examples/generate_ts_sdk.rs b/examples/generate_ts_sdk.rs index 7bcf1409..fb58c038 100644 --- a/examples/generate_ts_sdk.rs +++ b/examples/generate_ts_sdk.rs @@ -1,6 +1,6 @@ use std::io::Write as _; -use ts_bindgen::{TypeRegistry, TypeScriptDef}; +use ts_bindgen::{TypeRegistry, TypeScriptDef, TypeScriptType}; fn main() -> Result<(), Box> { let mut registry = TypeRegistry::default(); @@ -10,23 +10,64 @@ fn main() -> Result<(), Box> { client_sdk::api::commands::register_routes(&mut registry); - let mut out = std::fs::File::create("api.ts")?; + let mut models = std::fs::File::create("autogenerated.ts")?; - write!(out, "import type {{ ")?; + write!(models, "import type {{ ")?; for (idx, name) in registry.external().iter().enumerate() { if idx > 0 { - write!(out, ", ")?; + write!(models, ", ")?; } - write!(out, "{name}")?; + write!(models, "{name}")?; } write!( - out, + models, " }} from './models';\nimport {{ command }} from './api';\n\n{}", registry.display() )?; + let mut api = std::fs::File::create("api.ts")?; + + for group in ["decl", "values", "types"] { + let mut first = true; + let mut len = 0; + + + if group == "types" { + writeln!(api, "export type {{")?; + } else { + writeln!(api, "export {{")?; + } + + for (name, ty) in registry.iter() { + match group { + "decl" if matches!(ty, TypeScriptType::ApiDecl { .. }) => {} + "values" if ty.is_value() && !matches!(ty, TypeScriptType::ApiDecl { .. }) => {} + "types" if !ty.is_value() => {} + _ => continue, + } + + if !first { + if len % 5 == 0 { + write!(api, ",\n ")?; + } else { + write!(api, ", ")?; + } + } else { + write!(api, " ")?; + } + + first = false; + + write!(api, "{}", name)?; + + len += 1; + } + + write!(api, "\n}} from './autogenerated';\n\n")?; + } + Ok(()) } diff --git a/src/models/auth.rs b/src/models/auth.rs index 02dfd508..dc4e30a1 100644 --- a/src/models/auth.rs +++ b/src/models/auth.rs @@ -30,9 +30,9 @@ const MAX_LENGTH: usize = { #[cfg_attr(feature = "ts", derive(ts_bindgen::TypeScriptDef))] #[serde(untagged)] pub enum AuthToken { - /// Bearer token for users + /// Bearer token for users, has a fixed length of 28 bytes. Bearer(BearerToken), - /// Bot token for bots + /// Bot token for bots, has a fixed length of 48 bytes. Bot(BotToken), } diff --git a/src/models/party/prefs.rs b/src/models/party/prefs.rs index cf1b8d75..67121467 100644 --- a/src/models/party/prefs.rs +++ b/src/models/party/prefs.rs @@ -5,7 +5,7 @@ use crate::models::Locale; bitflags2! { #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct PartyPrefsFlags: i32 { - const DEFAULT_FLAGS = 0; + const DEFAULT = 0; } } @@ -22,7 +22,7 @@ impl From for PartyPrefsFlags { impl Default for PartyPrefsFlags { fn default() -> Self { - Self::DEFAULT_FLAGS + Self::DEFAULT } } @@ -34,8 +34,11 @@ mod preferences { #[cfg_attr(feature = "rkyv", derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize))] #[cfg_attr(feature = "ts", derive(ts_bindgen::TypeScriptDef))] pub struct PartyPreferences { + /// Party locale (alias `locale`) #[serde(default, skip_serializing_if = "is_default", alias = "locale")] pub l: Locale, + + /// Party preferences flags (alias `flags`) #[serde(default, skip_serializing_if = "is_default", alias = "flags")] pub f: PartyPrefsFlags, } diff --git a/src/models/room.rs b/src/models/room.rs index e6e5217d..bd9f48e2 100644 --- a/src/models/room.rs +++ b/src/models/room.rs @@ -5,7 +5,7 @@ use super::*; #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, enum_primitive_derive::Primitive)] #[cfg_attr(feature = "rkyv", derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize))] -#[cfg_attr(feature = "ts", derive(ts_bindgen::TypeScriptDef))] +#[cfg_attr(feature = "ts", derive(ts_bindgen::TypeScriptDef), ts(max))] #[repr(u8)] pub enum RoomKind { Text = 0, @@ -48,7 +48,7 @@ impl From for RoomFlags { #[derive(Debug, Clone, Serialize, Deserialize)] #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] #[cfg_attr(feature = "rkyv", derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize))] -#[cfg_attr(feature = "ts", derive(ts_bindgen::TypeScriptDef))] +#[cfg_attr(feature = "ts", derive(ts_bindgen::TypeScriptDef), ts(include(RoomKind)))] // include RoomKind for RoomFlags pub struct Room { pub id: RoomId, diff --git a/src/models/stats.rs b/src/models/stats.rs index 266fed30..9fcee5ea 100644 --- a/src/models/stats.rs +++ b/src/models/stats.rs @@ -15,6 +15,7 @@ pub struct Statistics { #[cfg_attr(feature = "rkyv", derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize))] #[cfg_attr(feature = "ts", derive(ts_bindgen::TypeScriptDef))] pub struct RoomStatistics { + /// Total number of messages sent pub messages: u64, /// Total number of attachment files sent diff --git a/src/models/user/mod.rs b/src/models/user/mod.rs index 944c8e9c..f28f27c8 100644 --- a/src/models/user/mod.rs +++ b/src/models/user/mod.rs @@ -210,7 +210,7 @@ impl UserProfile { #[derive(Debug, Clone, Serialize, Deserialize)] #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] #[cfg_attr(feature = "rkyv", derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize))] -#[cfg_attr(feature = "ts", derive(ts_bindgen::TypeScriptDef))] +#[cfg_attr(feature = "ts", derive(ts_bindgen::TypeScriptDef), ts(include(ElevationLevel)))] // include ElevationLevel for UserFlags pub struct User { pub id: UserId, pub username: SmolStr, diff --git a/src/models/user/prefs.rs b/src/models/user/prefs.rs index 8a4eb574..d64b50bc 100644 --- a/src/models/user/prefs.rs +++ b/src/models/user/prefs.rs @@ -11,6 +11,7 @@ enum_codes! { enum_codes! { #[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash, serde_repr::Serialize_repr, serde_repr::Deserialize_repr)] + #[ts(non_const)] pub enum Font: u16 = SansSerif { #[default] 0 = SansSerif, @@ -21,7 +22,6 @@ enum_codes! { // third-party fonts 30 = OpenDyslexic, - 31 = AtkinsonHyperlegible, } } @@ -127,35 +127,67 @@ impl Default for UserPrefsFlags { pub mod preferences { decl_newtype_prefs! { + /// Color temperature in Kelvin Temperature: u16 = 7500u16, + + /// Font size in points FontSize: f32 = 16.0f32, + + /// Tab size in spaces TabSize: u8 = 4u8, + + /// Message padding in pixels Padding: u8 = 16u8, } } +/// User preferences +/// +/// Field names are shortened to reduce message size +/// when stored in a database or sent over the network. +/// However, fields can still be deserialized using the +/// provided aliases, documented for each field. #[derive(Debug, Clone, Serialize, Deserialize)] #[cfg_attr(feature = "rkyv", derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize))] #[cfg_attr(feature = "ts", derive(ts_bindgen::TypeScriptDef))] pub struct UserPreferences { + /// User locale (alias `locale`) #[serde(default, skip_serializing_if = "is_default", alias = "locale")] pub l: Locale, + + /// User preferences flags (alias `flags`) #[serde(default, skip_serializing_if = "is_default", alias = "flags")] pub f: UserPrefsFlags, + + /// Who can add you as a friend (alias `friend_add`) #[serde(default, skip_serializing_if = "is_default", alias = "friend_add")] - pub friend: FriendAddability, + pub fr: FriendAddability, + + /// Color temperature in Kelvin (alias `temperature`) #[serde(default, skip_serializing_if = "is_default", alias = "temperature")] pub temp: preferences::Temperature, + + /// Chat font (alias `chat_font`) #[serde(default, skip_serializing_if = "is_default", alias = "chat_font")] pub cf: Font, + + /// UI font (alias `ui_font`) #[serde(default, skip_serializing_if = "is_default", alias = "ui_font")] pub uf: Font, + + /// Chat font size in points (alias `chat_font_size`) #[serde(default, skip_serializing_if = "is_default", alias = "chat_font_size")] pub cfs: preferences::FontSize, + + /// UI font size in points (alias `ui_font_size`) #[serde(default, skip_serializing_if = "is_default", alias = "ui_font_size")] pub ufs: preferences::FontSize, + + /// message padding in pixels (alias `padding`) #[serde(default, skip_serializing_if = "is_default", alias = "padding")] pub pad: preferences::Padding, + + /// tab size in spaces (alias `tab_size`) #[serde(default, skip_serializing_if = "is_default", alias = "tab_size")] pub tab: preferences::TabSize, } diff --git a/src/models/util/macros.rs b/src/models/util/macros.rs index 11530799..6d8d7206 100644 --- a/src/models/util/macros.rs +++ b/src/models/util/macros.rs @@ -35,20 +35,20 @@ macro_rules! bitflags2 { // at the cost of excluding combinations of flags if size_of::() >= 16 { let bits_name = concat!(stringify!($BitFlags), "Bit"); + let raw_name = concat!("Raw", stringify!($BitFlags)); if registry.contains(bits_name) { - return TypeScriptType::Named(stringify!($BitFlags)); + return TypeScriptType::Named(raw_name); } - registry.add_external(stringify!($BitFlags)); + registry.add_external(raw_name); eprintln!( - "Note: Generating TypeScript for {} as bit positions, relying on external type for usage", + "Note: Generating TypeScript type for {} as bit positions, relying on external type {raw_name} for usage", stringify!($BitFlags) ); let mut members = Vec::new(); - let mut combinations = Vec::new(); $( @@ -93,7 +93,22 @@ macro_rules! bitflags2 { registry.insert(name, ty, doc); } - return TypeScriptType::Named(stringify!($BitFlags)); + // export const $BitFlagsBit_ALL = [...$BitFlagsBit.values()]; + registry.insert(concat!(stringify!($BitFlags), "Bit_ALL"), { + let mut bits = Vec::new(); + + for (name, value) in Self::all().iter_names() { + let v = value.bits(); + + if (v & (v - 1)) == 0 { + bits.push(TypeScriptType::EnumValue(concat!(stringify!($BitFlags), "Bit"), name)); + } + } + + TypeScriptType::ArrayLiteral(bits) + }, concat!("All bit positions of ", stringify!($BitFlags))); + + return TypeScriptType::Named(raw_name); } // regular enum @@ -153,8 +168,8 @@ macro_rules! enum_codes { } ) => { rkyv_rpc::enum_codes! { - $(#[$meta])* #[cfg_attr(feature = "ts", derive(ts_bindgen::TypeScriptDef))] + $(#[$meta])* $vis enum $name: $archived_vis $repr $(= $unknown)? { $($(#[$variant_meta])* $code = $variant,)* } @@ -170,8 +185,8 @@ macro_rules! enum_codes { $($(#[$variant_meta:meta])* $code:literal = $variant:ident,)* } ) => { - $(#[$meta])* #[cfg_attr(feature = "ts", derive(ts_bindgen::TypeScriptDef))] + $(#[$meta])* #[repr($repr)] $vis enum $name { $($(#[$variant_meta])* $variant = $code,)* diff --git a/ts-bindgen/src/registry.rs b/ts-bindgen/src/registry.rs index 56c6679e..145812dd 100644 --- a/ts-bindgen/src/registry.rs +++ b/ts-bindgen/src/registry.rs @@ -22,6 +22,10 @@ impl core::fmt::Display for DisplayRegistry<'_> { } impl TypeRegistry { + pub fn iter(&self) -> impl Iterator { + self.types.iter().map(|(&name, (ty, _))| (name, ty)) + } + pub fn display(&self) -> DisplayRegistry { DisplayRegistry { registry: self } } @@ -81,7 +85,7 @@ impl TypeRegistry { first = false; - fmt_comment(item_comment, &mut out)?; + fmt_comment(item_comment, &mut out, "")?; match ty { // values are just exported as constants @@ -136,7 +140,7 @@ impl TypeRegistry { writeln!(out, "export {specifier} {name} {{")?; for (name, value, comment) in vec { - fmt_comment(comment, &mut out)?; + fmt_comment(comment, &mut out, " ")?; match value { Some(value) => writeln!(out, " {name} = {value},")?, @@ -198,7 +202,7 @@ impl TypeRegistry { Err(ty) => ("", ty), }; - fmt_comment(member_comment, &mut out)?; + fmt_comment(member_comment, &mut out, " ")?; write!(out, " {name}{opt}: ")?; ty.fmt_depth(0, &mut out)?; @@ -286,12 +290,26 @@ impl TypeScriptType { TypeScriptType::ArrayLiteral(vec) => { f.write_str("[")?; + + if vec.len() > 5 { + f.write_str("\n ")?; + } + for (i, ty) in vec.iter().enumerate() { if i != 0 { - f.write_str(", ")?; + if i % 5 == 0 { + f.write_str(",\n ")?; + } else { + f.write_str(", ")?; + } } ty.fmt_depth(depth + 1, f)?; } + + if vec.len() > 5 { + f.write_str("\n")?; + } + f.write_str("]") } @@ -350,7 +368,7 @@ impl TypeScriptType { f.write_str(", ")?; } - fmt_comment(element_comment, f)?; + fmt_comment(element_comment, f, "")?; ty.fmt_depth(depth + 1, f)?; } @@ -371,7 +389,7 @@ impl TypeScriptType { Err(ty) => ("", ty), }; - fmt_comment(member_comment, f)?; + fmt_comment(member_comment, f, "")?; write!(f, "{name}{opt}: ")?; ty.fmt_depth(0, f)?; @@ -396,20 +414,20 @@ impl Display for TypeScriptType { } } -fn fmt_comment(comment: &str, out: &mut W) -> std::fmt::Result { +fn fmt_comment(comment: &str, out: &mut W, p: &str) -> std::fmt::Result { if comment.is_empty() { return Ok(()); } if !comment.contains('\n') { - return writeln!(out, "/** {} */", comment.trim()); + return writeln!(out, "{p}/** {} */", comment.trim()); } - out.write_str("/**\n")?; + writeln!(out, "{p}/**")?; for line in comment.lines() { - writeln!(out, " * {}", line.trim())?; + writeln!(out, "{p} * {}", line.trim())?; } - out.write_str(" */\n") + writeln!(out, "{p} */") } diff --git a/ts-bindgen/src/ty.rs b/ts-bindgen/src/ty.rs index f6c4c4e1..110d1942 100644 --- a/ts-bindgen/src/ty.rs +++ b/ts-bindgen/src/ty.rs @@ -156,6 +156,17 @@ impl TypeScriptType { } } + pub fn is_value(&self) -> bool { + if self.is_literal() { + return true; + } + + matches!( + self, + TypeScriptType::ConstEnum(_) | TypeScriptType::Enum(_) | TypeScriptType::ApiDecl { .. } + ) + } + /// Performs simple cleanup of nested unions and intersections and removes duplicates. pub fn unify(&mut self) { let is_union = matches!(self, TypeScriptType::Union(_)); diff --git a/ts-bindgen/ts-bindgen-macros/src/lib.rs b/ts-bindgen/ts-bindgen-macros/src/lib.rs index e5ce8f44..3c710e6c 100644 --- a/ts-bindgen/ts-bindgen-macros/src/lib.rs +++ b/ts-bindgen/ts-bindgen-macros/src/lib.rs @@ -18,6 +18,9 @@ pub fn derive_typescript_def(input: proc_macro::TokenStream) -> proc_macro::Toke let mut attrs = ItemAttributes { serde: SerdeContainer::from_ast(&ctxt, &input), inline: false, + non_const: false, + includes: Vec::new(), + max: false, comment: extract_doc_comments(&input.attrs), }; @@ -25,10 +28,17 @@ pub fn derive_typescript_def(input: proc_macro::TokenStream) -> proc_macro::Toke return e.into_compile_error().into(); } - attrs.parse_ts(&input.attrs); + if let Err(e) = attrs.parse_ts(&input.attrs) { + return e.into_compile_error().into(); + } let name = input.ident; + let includes = &attrs.includes; + let includes = quote! { + #( #includes::register(registry); )* + }; + let inner = match input.data { Data::Enum(data) => derive_enum(data, name.clone(), attrs), Data::Struct(data) => derive_struct(data, name.clone(), attrs), @@ -42,6 +52,8 @@ pub fn derive_typescript_def(input: proc_macro::TokenStream) -> proc_macro::Toke return ts_bindgen::TypeScriptType::Named(stringify!(#name)); } + #includes + #inner } } @@ -51,20 +63,55 @@ pub fn derive_typescript_def(input: proc_macro::TokenStream) -> proc_macro::Toke struct ItemAttributes { serde: SerdeContainer, + /// Item comment comment: String, /// Put interface definitions directly in unions, rather than as a named type. inline: bool, + + /// Prefer a regular enum for enums with explicit discriminants. + non_const: bool, + + /// Emit a __MAX discriminator for enums with explicit discriminants. + max: bool, + + /// Include other types in the generated register function. + includes: Vec, } impl ItemAttributes { - pub fn parse_ts(&mut self, attrs: &[syn::Attribute]) { + pub fn parse_ts(&mut self, attrs: &[syn::Attribute]) -> syn::Result<()> { for attr in attrs { - if attr.path().is_ident("ts") { - // TODO: parse ts attributes + if !attr.path().is_ident("ts") { continue; } + + attr.parse_nested_meta(|meta| { + if meta.path.is_ident("inline") { + self.inline = true; + } + + if meta.path.is_ident("max") { + self.max = true; + } + + if meta.path.is_ident("non_const") { + self.non_const = true; + } + + if meta.path.is_ident("include") { + meta.parse_nested_meta(|meta| { + self.includes.push(meta.path.get_ident().unwrap().clone()); + + Ok(()) + })?; + } + + Ok(()) + })?; } + + Ok(()) } } @@ -127,8 +174,8 @@ fn derive_struct(input: syn::DataStruct, name: Ident, attrs: ItemAttributes) -> }); } - if num_fields == 1 { - out.extend(if attrs.inline { + out.extend(if num_fields == 1 { + if attrs.inline { quote! { field } } else { quote! { @@ -148,13 +195,15 @@ fn derive_struct(input: syn::DataStruct, name: Ident, attrs: ItemAttributes) -> ts_bindgen::TypeScriptType::Named(stringify!(#name)) } - }); + } + } else if attrs.inline { + quote! { ts_bindgen::TypeScriptType::Tuple(fields) } } else { - out.extend(quote! { + quote! { registry.insert(stringify!(#name), ts_bindgen::TypeScriptType::Tuple(fields), #struct_comment); ts_bindgen::TypeScriptType::Named(stringify!(#name)) - }); - } + } + }); } else { let Fields::Named(fields) = input.fields else { unreachable!() }; @@ -198,12 +247,16 @@ fn derive_struct(input: syn::DataStruct, name: Ident, attrs: ItemAttributes) -> let num_extends = flattened.len(); - out.extend(quote! { - let ty = ts_bindgen::TypeScriptType::interface(members, #num_extends) #(.flatten(#flattened))*; + out.extend(if attrs.inline { + quote! { ts_bindgen::TypeScriptType::interface(members, #num_extends) #(.flatten(#flattened))*; } + } else { + quote! { + let ty = ts_bindgen::TypeScriptType::interface(members, #num_extends) #(.flatten(#flattened))*; - registry.insert(stringify!(#name), ty, #struct_comment); + registry.insert(stringify!(#name), ty, #struct_comment); - ts_bindgen::TypeScriptType::Named(stringify!(#name)) + ts_bindgen::TypeScriptType::Named(stringify!(#name)) + } }); } @@ -286,7 +339,23 @@ fn derive_enum(input: syn::DataEnum, name: Ident, attrs: ItemAttributes) -> Toke }); } - out.extend(quote! { let ty = ts_bindgen::TypeScriptType::ConstEnum(variants); }); + if attrs.max { + out.extend(quote! { + variants.push(( + "__MAX".into(), + None, + "Max value for the enum".into(), + )); + }); + } + + let ty = if attrs.non_const { + quote! { Enum } + } else { + quote! { ConstEnum } + }; + + out.extend(quote! { let ty = ts_bindgen::TypeScriptType::#ty(variants); }); } out.extend(quote! {