diff --git a/src/api/command.rs b/src/api/command.rs index 190b0535..98055ca8 100644 --- a/src/api/command.rs +++ b/src/api/command.rs @@ -292,7 +292,7 @@ macro_rules! command { }; // entry point - ($( + ($group_name:ident; $( // meta $(#[$($meta:tt)*])* @@ -337,7 +337,17 @@ macro_rules! command { } )? } - )*) => {paste::paste!{$( + )*) => {paste::paste!{ + + #[cfg(feature = "ts")] + #[allow(unused_variables)] + pub fn [](registry: &mut ts_bindgen::TypeRegistry) { + $( + $( <$body_name as ts_bindgen::TypeScriptDef>::register(registry); )? + )* + } + + $( // verify presence of exactly one `struct` without prefix command!(@STRUCT $($auth_struct)? $($noauth_struct)?); @@ -649,6 +659,13 @@ macro_rules! command_module { $($vis use super::$mod::*;)* } + #[cfg(feature = "ts")] + pub fn register_routes(registry: &mut ts_bindgen::TypeRegistry) { + $( + paste::paste! { $mod::[](registry); } + )* + } + // TODO: Collect schemas from each object } } diff --git a/src/api/commands/config.rs b/src/api/commands/config.rs index 00bb272c..b3de4dda 100644 --- a/src/api/commands/config.rs +++ b/src/api/commands/config.rs @@ -1,6 +1,7 @@ use super::*; -command! { +command! { Config; + /// Gets the global server configuration -struct GetServerConfig -> One ServerConfig: GET("config") {} } diff --git a/src/api/commands/file.rs b/src/api/commands/file.rs index 6c3547f0..f006811a 100644 --- a/src/api/commands/file.rs +++ b/src/api/commands/file.rs @@ -1,6 +1,7 @@ use super::*; -command! { +command! { File; + +struct CreateFile -> One FileId: POST("file") { ; #[cfg_attr(feature = "typed-builder", derive(typed_builder::TypedBuilder))] diff --git a/src/api/commands/invite.rs b/src/api/commands/invite.rs index 6bdd9274..9ec619a6 100644 --- a/src/api/commands/invite.rs +++ b/src/api/commands/invite.rs @@ -1,6 +1,7 @@ use super::*; -command! { +command! { Invite; + +struct GetInvite -> One Invite: GET("invite" / code) { pub code: SmolStr, } diff --git a/src/api/commands/party.rs b/src/api/commands/party.rs index 660ae4bf..2b544d5a 100644 --- a/src/api/commands/party.rs +++ b/src/api/commands/party.rs @@ -1,6 +1,7 @@ use super::*; -command! { +command! { Party; + +struct GetParty -> One Party: GET("party" / party_id) { pub party_id: PartyId, } @@ -252,7 +253,7 @@ command! { #[cfg_attr(feature = "typed-builder", derive(typed_builder::TypedBuilder))] #[cfg_attr(feature = "bon", derive(bon::Builder))] struct SearchQuery { - #[serde(flatten)] + #[serde(alias = "q")] #[cfg_attr(feature = "typed-builder", builder(setter(into)))] #[cfg_attr(feature = "bon", builder(into))] pub query: ThinString, diff --git a/src/api/commands/room.rs b/src/api/commands/room.rs index a952fdc0..e592abdd 100644 --- a/src/api/commands/room.rs +++ b/src/api/commands/room.rs @@ -1,6 +1,6 @@ use super::*; -command! { +command! { Room; /// Create message command +struct CreateMessage -> One Message: POST[100 ms, 2]("room" / room_id / "messages") where SEND_MESSAGES { pub room_id: RoomId, @@ -78,7 +78,7 @@ command! { #[cfg_attr(feature = "bon", derive(bon::Builder))] #[derive(Default)] struct StartTypingBody { /// Will only show within the parent context if set - #[serde(flatten, default, skip_serializing_if = "Option::is_none")] + #[serde(default, skip_serializing_if = "Option::is_none")] #[cfg_attr(feature = "typed-builder", builder(default))] pub parent: Option, } diff --git a/src/api/commands/user.rs b/src/api/commands/user.rs index 0d71e0b1..cfc8046b 100644 --- a/src/api/commands/user.rs +++ b/src/api/commands/user.rs @@ -1,6 +1,6 @@ use super::*; -command! { +command! { User; -struct UserRegister(U) -> One Session: POST[1000 ms, 1]("user") { ; #[cfg_attr(feature = "typed-builder", derive(typed_builder::TypedBuilder))] diff --git a/ts-bindgen/README.md b/ts-bindgen/README.md new file mode 100644 index 00000000..72caa64a --- /dev/null +++ b/ts-bindgen/README.md @@ -0,0 +1,4 @@ +ts-bindgen +========== + +This is a crate specific to Lantern's Client SDK used to generate TypeScript bindings for the SDK. It's not exactly intended for general use. \ No newline at end of file diff --git a/ts-bindgen/src/registry.rs b/ts-bindgen/src/registry.rs index 5ea5dbef..bbabdabd 100644 --- a/ts-bindgen/src/registry.rs +++ b/ts-bindgen/src/registry.rs @@ -9,7 +9,9 @@ pub struct TypeRegistry { } impl TypeRegistry { - pub fn insert(&mut self, name: &'static str, ty: TypeScriptType) { + pub fn insert(&mut self, name: &'static str, mut ty: TypeScriptType) { + ty.unify(); + self.types.insert(name, ty); } @@ -25,15 +27,8 @@ impl TypeRegistry { use core::fmt::{Display, Error as FmtError, Write}; impl TypeScriptType { - fn inner(&self) -> &TypeScriptType { - match self { - TypeScriptType::Ref(rc) => rc.inner(), - _ => self, - } - } - fn is_extendible(&self, registry: &TypeRegistry) -> bool { - match self.inner() { + match self { TypeScriptType::Interface { .. } => true, TypeScriptType::Named(name) => match registry.get(name) { Some(ty) => ty.is_extendible(registry), @@ -55,8 +50,6 @@ impl TypeRegistry { let mut first = true; for (name, ty) in &self.types { - let ty = ty.inner(); - if !first { out.write_str("\n\n")?; } @@ -81,6 +74,7 @@ impl TypeRegistry { | TypeScriptType::Undefined | TypeScriptType::Tuple(_) | TypeScriptType::Array(_, _) + | TypeScriptType::Partial(_) | TypeScriptType::Named(_) => { writeln!(out, "export type {name} = {ty};")?; } @@ -96,8 +90,6 @@ impl TypeRegistry { writeln!(out, "export type {name} = {{ [key: {key}]: {value} }};")?; } - TypeScriptType::Ref(_) => unreachable!(), - TypeScriptType::Enum(vec) | TypeScriptType::ConstEnum(vec) => { let is_const = match ty { TypeScriptType::ConstEnum(_) => " const", @@ -119,6 +111,14 @@ impl TypeRegistry { let mut do_extend = true; for extend in extends { + let extend = match extend { + TypeScriptType::Named(name) => name, + _ => { + do_extend = false; + break; + } + }; + do_extend &= match self.types.get(&**extend) { Some(ty) => ty.is_extendible(self), None => false, @@ -135,7 +135,7 @@ impl TypeRegistry { if i != 0 { out.write_str(", ")?; } - out.write_str(extend)?; + write!(out, "{extend}")?; } } } else { @@ -143,7 +143,7 @@ impl TypeRegistry { write!(out, "export type {name} = ")?; for extend in extends { - write!(out, "{extend} & ")?; + write!(out, "{extend} &")?; } } @@ -161,6 +161,10 @@ impl TypeRegistry { out.write_str(",\n")?; } out.write_str("}")?; + + if !do_extend { + out.write_str(";")?; + } } } } @@ -173,13 +177,13 @@ impl TypeScriptType { fn fmt_depth(&self, depth: usize, f: &mut W) -> std::fmt::Result { match self { TypeScriptType::Named(name) => f.write_str(name), - TypeScriptType::Ref(rc) => rc.fmt_depth(depth, f), TypeScriptType::Null => f.write_str("null"), TypeScriptType::Undefined => f.write_str("undefined"), TypeScriptType::EnumValue(e, v) => write!(f, "{e}.{v}"), TypeScriptType::Array(inner, _) => write!(f, "Array<{inner}>"), + TypeScriptType::Partial(inner) => write!(f, "Partial<{inner}>"), TypeScriptType::Boolean(value) => match value { Some(value) => write!(f, "{value}"), None => f.write_str("boolean"), diff --git a/ts-bindgen/src/ty.rs b/ts-bindgen/src/ty.rs index e62d7063..0ac1f72e 100644 --- a/ts-bindgen/src/ty.rs +++ b/ts-bindgen/src/ty.rs @@ -21,29 +21,29 @@ impl fmt::Display for Discriminator { } } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] pub enum TypeScriptType { - Ref(Rc), Null, Undefined, Number(Option), String(Option>), Boolean(Option), - Array(Rc, Option), + Array(Box, Option), Interface { members: Vec<(String, TypeScriptType)>, - extends: IndexSet, + extends: Vec, }, Union(Vec), Intersection(Vec), Enum(Vec<(String, Option)>), Tuple(Vec), + Partial(Box), /// [key: K]: T - Map(Rc, Rc), + Map(Box, Box), /// `export const enum` ConstEnum(Vec<(String, Option)>), @@ -55,6 +55,63 @@ pub enum TypeScriptType { Named(&'static str), } +impl TypeScriptType { + /// Performs simply cleanup of nested unions and intersections. + pub fn unify(&mut self) { + match self { + TypeScriptType::Union(types) => { + while types.iter().any(|ty| matches!(ty, TypeScriptType::Union(_))) { + let mut new_types = Vec::new(); + + for mut ty in types.drain(..) { + ty.unify(); + + match ty { + TypeScriptType::Union(mut inner_types) => new_types.append(&mut inner_types), + ty => new_types.push(ty), + } + } + + *types = new_types; + } + } + TypeScriptType::Intersection(types) => { + while types.iter().any(|ty| matches!(ty, TypeScriptType::Intersection(_))) { + let mut new_types = Vec::new(); + + for mut ty in types.drain(..) { + ty.unify(); + + match ty { + TypeScriptType::Intersection(mut inner_types) => new_types.append(&mut inner_types), + ty => new_types.push(ty), + } + } + + *types = new_types; + } + } + TypeScriptType::Array(ty, _) => ty.unify(), + TypeScriptType::Map(key, value) => { + key.unify(); + value.unify(); + } + TypeScriptType::Tuple(types) => { + for ty in types { + ty.unify(); + } + } + TypeScriptType::Interface { members, .. } => members.iter_mut().for_each(|(_, ty)| ty.unify()), + + TypeScriptType::Partial(ty) => { + // Partial should also remove the `undefined` type from T if union + ty.unify() + } + _ => {} + } + } +} + impl TypeScriptType { pub fn is_optional(&self) -> bool { match self { @@ -80,11 +137,8 @@ impl TypeScriptType { } impl TypeScriptType { - fn into_rc(self) -> Rc { - match self { - TypeScriptType::Ref(rc) => rc, - _ => Rc::new(self), - } + fn boxed(self) -> Box { + Box::new(self) } } @@ -92,7 +146,7 @@ impl TypeScriptType { pub fn interface(members: Vec<(String, TypeScriptType)>, extend_hint: usize) -> TypeScriptType { TypeScriptType::Interface { members, - extends: IndexSet::with_capacity(extend_hint), + extends: Vec::with_capacity(extend_hint), } } @@ -128,10 +182,6 @@ impl TypeScriptType { TypeScriptType::String(Some(value.into())) } - pub fn into_ref(self) -> TypeScriptType { - TypeScriptType::Ref(self.into_rc()) - } - pub fn into_nullable(self) -> TypeScriptType { if self.is_nullable() { return self; @@ -178,11 +228,11 @@ impl TypeScriptType { } pub fn into_array(self) -> TypeScriptType { - TypeScriptType::Array(self.into_rc(), None) + TypeScriptType::Array(self.boxed(), None) } pub fn into_sized_array(self, size: usize) -> TypeScriptType { - TypeScriptType::Array(Rc::new(self), Some(size)) + TypeScriptType::Array(self.boxed(), Some(size)) } pub fn union(self, other: TypeScriptType) -> TypeScriptType { @@ -232,7 +282,7 @@ impl TypeScriptType { TypeScriptType::Union(vec![TypeScriptType::Named(a), TypeScriptType::Named(b)]) } (TypeScriptType::Interface { members, mut extends }, TypeScriptType::Named(name)) => { - extends.insert(name.to_owned()); + extends.push(TypeScriptType::Named(name)); TypeScriptType::Interface { members, extends } } @@ -255,6 +305,17 @@ impl TypeScriptType { } } (a @ TypeScriptType::Interface { .. }, TypeScriptType::Null | TypeScriptType::Undefined) => a, + + // #[serde(flatten, default, skip_serializing_if = "...")] + (TypeScriptType::Interface { members, extends }, TypeScriptType::Union(types)) + if matches!(&types[..], &[TypeScriptType::Named(_), TypeScriptType::Undefined]) => + { + let mut extends = extends.clone(); + extends.push(TypeScriptType::Partial(types[0].clone().boxed())); + + TypeScriptType::Interface { members, extends } + } + (s, f) => unreachable!("flatten called with invalid types: {s:?}, {f:?}"), } } diff --git a/ts-bindgen/ts-bindgen-macros/src/lib.rs b/ts-bindgen/ts-bindgen-macros/src/lib.rs index 5089a246..47e0cf6e 100644 --- a/ts-bindgen/ts-bindgen-macros/src/lib.rs +++ b/ts-bindgen/ts-bindgen-macros/src/lib.rs @@ -240,6 +240,9 @@ fn derive_enum(input: syn::DataEnum, name: Ident, attrs: ItemAttributes) -> Toke variants.push((stringify!(#variant_ident).to_owned(), Some(ts_bindgen::Discriminator::String(#variant_name)))); }); } + + // use a real enum for enums with string values + out.extend(quote! { let ty = ts_bindgen::TypeScriptType::Enum(variants); }); } else { for (variant, ..) in variants { let name = variant.ident; @@ -253,10 +256,11 @@ fn derive_enum(input: syn::DataEnum, name: Ident, attrs: ItemAttributes) -> Toke variants.push((stringify!(#name).to_owned(), #discriminant)); }); } + + out.extend(quote! { let ty = ts_bindgen::TypeScriptType::ConstEnum(variants); }); } out.extend(quote! { - let ty = ts_bindgen::TypeScriptType::ConstEnum(variants); registry.insert(stringify!(#name), ty); ts_bindgen::TypeScriptType::Named(stringify!(#name))