Skip to content

Commit

Permalink
Various TypeScript generation improvements
Browse files Browse the repository at this point in the history
  • Loading branch information
novacrazy committed Nov 20, 2024
1 parent 9d7c1c6 commit 4a2e584
Show file tree
Hide file tree
Showing 15 changed files with 204 additions and 113 deletions.
92 changes: 60 additions & 32 deletions examples/generate_ts_sdk.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use std::fmt::Write as _;
use std::io::Write as _;

use ts_bindgen::{TypeRegistry, TypeScriptDef, TypeScriptType};
Expand All @@ -8,65 +9,92 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
client_sdk::models::gateway::message::ServerMsg::register(&mut registry);
client_sdk::models::gateway::message::ClientMsg::register(&mut registry);

client_sdk::api::error::ApiError::register(&mut registry);

client_sdk::api::commands::register_routes(&mut registry);

let mut models = std::fs::File::create("autogenerated.ts")?;
// generate TypeScript bindings, all of them

let mut autogenerated = std::fs::File::create("out/autogenerated.ts")?;

write!(models, "import type {{ ")?;
write!(autogenerated, "import type {{ ")?;

for (idx, name) in registry.external().iter().enumerate() {
if idx > 0 {
write!(models, ", ")?;
write!(autogenerated, ", ")?;
}

write!(models, "{name}")?;
write!(autogenerated, "{name}")?;
}

write!(
models,
" }} from './models';\nimport {{ command }} from './api';\n\n{}",
autogenerated,
" }} from './models';\nimport {{ command }} from './api/command';\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;
let models = std::fs::File::create("out/models.ts")?;
let api = std::fs::File::create("out/api.ts")?;
let gateway = std::fs::File::create("out/gateway.ts")?;

let mut out = String::new();

if group == "types" {
writeln!(api, "export type {{")?;
} else {
writeln!(api, "export {{")?;
}
for (mut file, tag) in [(models, ""), (api, "command"), (gateway, "gateway")] {
for group in ["decl", "values", "types"] {
let tys = registry.iter().filter(|(name, _)| {
if tag.is_empty() {
registry.type_tags(name).count() == 0
} else {
registry.has_tag(name, tag)
}
});

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,
}
let mut idx = 0;

for (name, ty) in tys {
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 ")?;
if idx == 0 {
write!(out, " {name}")?;
} else if idx % 5 == 0 {
write!(out, ",\n {name}")?;
} else {
write!(api, ", ")?;
write!(out, ", {name}")?;
}

idx += 1;
}

if out.is_empty() {
continue;
}

let comment = match group {
"decl" => "API Command declarations",
"values" => "Exported const values",
"types" => "Exported types",
_ => unreachable!(),
};

if group == "types" {
writeln!(file, "/** {comment} */\nexport type {{")?;
} else {
write!(api, " ")?;
writeln!(file, "/** {comment} */\nexport {{")?;
}

first = false;
file.write_all(out.as_bytes())?;

write!(api, "{}", name)?;
out.clear();

len += 1;
write!(file, "\n}} from '../autogenerated';\n\n")?;
}

write!(api, "\n}} from './autogenerated';\n\n")?;
}

Ok(())
Expand Down
38 changes: 21 additions & 17 deletions src/api/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,18 @@ use crate::models::Permissions;
bitflags2! {
/// Flags for command functionality.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct CommandFlags: u8 {
pub struct CommandFlags: u8 where "command" {
/// Command requires authorization to execute.
const AUTHORIZED = 1 << 0;

/// Command has a request body.
const HAS_BODY = 1 << 1;
const HAS_RESPONSE = 1 << 2;
const STREAMING = 1 << 3;

const BOTS_ONLY = 1 << 2;
const USERS_ONLY = 1 << 3;
const ADMIN_ONLY = 1 << 4;
const BOTS_ONLY = 1 << 5;
const USERS_ONLY = 1 << 6;
const ADMIN_ONLY = 1 << 7;
}
}

Expand Down Expand Up @@ -120,9 +122,6 @@ impl core::error::Error for MissingItemError {}
///
/// For the case of `GET`/`OPTIONS` commands, the body becomes query parameters.
pub trait Command: sealed::Sealed {
/// Whether the command returns one or many items
const STREAM: bool;

/// Whether the command has a query string or sends a body
const IS_QUERY: bool;

Expand Down Expand Up @@ -275,9 +274,9 @@ macro_rules! command {
(@GET TRACE $c:block) => {$c};
(@GET $other:ident $c:block) => {};

(@IS_STREAM One) => { false };
(@IS_STREAM Many) => { true };
(@IS_STREAM $other:ident) => { compile_error!("Must use One or Many for Command result") };
(@STREAMING One) => { CommandFlags::empty() };
(@STREAMING Many) => { CommandFlags::STREAMING };
(@STREAMING $other:ident) => { compile_error!("Must use One or Many for Command result") };

(@AGGREGATE One $ty:ty) => { $ty };
(@AGGREGATE Many $ty:ty) => { Vec<$ty> };
Expand Down Expand Up @@ -378,8 +377,6 @@ macro_rules! command {

impl $crate::api::command::sealed::Sealed for $name {}
impl $crate::api::command::Command for $name {
const STREAM: bool = command!(@IS_STREAM $count);

const IS_QUERY: bool = matches!(
http::Method::$method,
http::Method::GET | http::Method::OPTIONS | http::Method::HEAD | http::Method::CONNECT | http::Method::TRACE
Expand All @@ -393,7 +390,14 @@ macro_rules! command {

const HTTP_METHOD: http::Method = http::Method::$method;

const FLAGS: CommandFlags = CommandFlags::empty()
const FLAGS: CommandFlags = CommandFlags::empty().union(command!(@STREAMING $count))
.union(const {
if size_of::<$result>() != 0 {
CommandFlags::HAS_RESPONSE
} else {
CommandFlags::empty()
}
})
$(.union((stringify!($body_name), CommandFlags::HAS_BODY).1))?
$(.union((stringify!($auth_struct), CommandFlags::AUTHORIZED).1))?
$( $(.union(CommandFlags::$flag))* )?
Expand Down Expand Up @@ -634,6 +638,8 @@ macro_rules! command {

registry.insert(stringify!($name), ty, concat!($(command!(@DOC #[$($meta)*])),*).trim());

registry.tag(stringify!($name), "command");

TypeScriptType::Named(stringify!($name))
}
}
Expand All @@ -649,7 +655,7 @@ macro_rules! command {
#[derive(Debug, 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(tag = "command"))]
$(#[$body_meta])*
pub struct $body_name {
$( $(#[$($body_field_meta)*])* $body_field_vis $body_field_name: $body_field_ty ),*
Expand Down Expand Up @@ -750,9 +756,7 @@ macro_rules! command_module {

#[cfg(feature = "ts")]
pub fn register_routes(registry: &mut ts_bindgen::TypeRegistry) {
$(
paste::paste! { $mod::[<register_ $mod _routes>](registry); }
)*
paste::paste! { $( $mod::[<register_ $mod _routes>](registry); )* }
}

// TODO: Collect schemas from each object
Expand Down
4 changes: 2 additions & 2 deletions src/api/commands/file.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ command! { File;
#[derive(Debug, Clone, Copy, PartialEq, 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(tag = "command"))]
pub struct FilesystemStatus {
pub quota_used: i64,
pub quota_total: i64,
Expand All @@ -52,7 +52,7 @@ pub struct FilesystemStatus {
#[derive(Debug, Clone, Copy, PartialEq, 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(tag = "command"))]
pub struct FileStatus {
pub complete: u32,
pub upload_offset: u64,
Expand Down
3 changes: 2 additions & 1 deletion src/api/commands/party.rs
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,7 @@ command! { Party;

decl_enum! {
#[derive(Default, serde_repr::Serialize_repr, serde_repr::Deserialize_repr)]
#[cfg_attr(feature = "ts", derive(ts_bindgen::TypeScriptDef), ts(tag = "command"))]
pub enum CreateRoomKind: u8 {
#[default]
0 = Text,
Expand All @@ -280,7 +281,7 @@ decl_enum! {
#[cfg_attr(feature = "typed-builder", derive(typed_builder::TypedBuilder))]
#[cfg_attr(feature = "bon", derive(bon::Builder))]
#[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(tag = "command"))]
pub struct PartySettings {
pub flags: PartyFlags,
pub prefs: PartyPreferences,
Expand Down
5 changes: 3 additions & 2 deletions src/api/commands/user.rs
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,7 @@ impl From<UserPreferences> for UpdateUserPrefsBody {

decl_enum! {
#[derive(Default, serde_repr::Deserialize_repr, serde_repr::Serialize_repr)]
#[cfg_attr(feature = "ts", derive(ts_bindgen::TypeScriptDef), ts(tag = "command"))]
pub enum BannerAlign: u8 {
#[default]
0 = Top,
Expand All @@ -236,9 +237,9 @@ decl_enum! {

#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
#[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(tag = "command"))]
pub struct Added2FA {
/// URL to be display as a QR code and added to an authenticator app
/// URL to be displayed as a QR code and added to an authenticator app
pub url: String,
/// Backup codes to be stored in a safe place
pub backup: Vec<String>,
Expand Down
6 changes: 6 additions & 0 deletions src/api/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ use http::StatusCode;
#[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),
ts(tag = "command", rename = "RawApiError") // we use a separate ApiError class in TypeScript
)]
pub struct ApiError {
/// Error code
pub code: ApiErrorCode,
Expand Down Expand Up @@ -73,6 +78,7 @@ error_codes! {
/// Standard API error codes.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde_repr::Serialize_repr, serde_repr::Deserialize_repr)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema_repr))]
#[cfg_attr(feature = "ts", derive(ts_bindgen::TypeScriptDef), ts(tag = "command", non_const))]
#[derive(enum_primitive_derive::Primitive)]
pub enum ApiErrorCode: u16 = Unknown {
// Server errors
Expand Down
3 changes: 2 additions & 1 deletion src/models/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ const MAX_LENGTH: usize = {
#[derive(Debug, Clone, Copy, Serialize)]
#[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))]
// rename the exported TypeScript to "RawAuthToken" since it's just a string
#[cfg_attr(feature = "ts", derive(ts_bindgen::TypeScriptDef), ts(rename = "RawAuthToken"))]
#[serde(untagged)]
pub enum AuthToken {
/// Bearer token for users, has a fixed length of 28 bytes.
Expand Down
Loading

0 comments on commit 4a2e584

Please sign in to comment.