diff --git a/Cargo.lock b/Cargo.lock index 152767c..4e60413 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1198,8 +1198,8 @@ dependencies = [ "thiserror 2.0.17", "tokio", "tokio-stream", - "ts-rs", "url", + "visit-rs", "yajrc", ] @@ -1495,15 +1495,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "termcolor" -version = "1.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" -dependencies = [ - "winapi-util", -] - [[package]] name = "thiserror" version = "1.0.69" @@ -1704,28 +1695,6 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" -[[package]] -name = "ts-rs" -version = "9.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b44017f9f875786e543595076374b9ef7d13465a518dd93d6ccdbf5b432dde8c" -dependencies = [ - "thiserror 1.0.69", - "ts-rs-macros", -] - -[[package]] -name = "ts-rs-macros" -version = "9.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c88cc88fd23b5a04528f3a8436024f20010a16ec18eb23c164b1242f65860130" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "termcolor", -] - [[package]] name = "unicode-ident" version = "1.0.22" @@ -1774,6 +1743,27 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "visit-rs" +version = "0.1.10" +source = "git+https://github.com/helix-nine/visit-rs?branch=fix%2Fts-bindings-metadata#8fb09da2abf4422fc52ed95973cc2d9876c07a9e" +dependencies = [ + "async-stream", + "futures", + "serde", + "visit-rs-derive", +] + +[[package]] +name = "visit-rs-derive" +version = "0.1.8" +source = "git+https://github.com/helix-nine/visit-rs?branch=fix%2Fts-bindings-metadata#8fb09da2abf4422fc52ed95973cc2d9876c07a9e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "want" version = "0.3.1" @@ -1866,15 +1856,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "winapi-util" -version = "0.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" -dependencies = [ - "windows-sys 0.61.2", -] - [[package]] name = "windows-link" version = "0.1.3" diff --git a/Cargo.toml b/Cargo.toml index 4ef9795..c600efc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,31 +1,32 @@ [package] authors = ["Aiden McClelland "] -edition = "2018" -name = "rpc-toolkit" -version = "0.3.2" description = "A toolkit for creating JSON-RPC 2.0 servers with automatic cli bindings" -license = "MIT" documentation = "https://docs.rs/rpc-toolkit" -keywords = ["json", "rpc", "cli"] +edition = "2024" +keywords = ["cli", "json", "rpc"] +license = "MIT" +name = "rpc-toolkit" repository = "https://github.com/Start9Labs/rpc-toolkit" +version = "0.3.2" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [features] cbor = ["serde_cbor"] default = ["cbor"] +ts = ["visit-rs"] [dependencies] -axum = "0.8" async-stream = "0.3" async-trait = "0.1" +axum = "0.8" clap = { version = "4", features = ["derive"] } futures = "0.3" http = "1" http-body-util = "0.1" # hyper = { version = "1", features = ["server", "http1", "http2", "client"] } -itertools = "0.14" imbl-value = "0.4.3" +itertools = "0.14" lazy_format = "2" lazy_static = "1.4" openssl = { version = "0.10", features = ["vendored"] } @@ -37,6 +38,19 @@ serde_json = "1.0" thiserror = "2.0" tokio = { version = "1", features = ["full"] } tokio-stream = { version = "0.1", features = ["io-util", "net"] } -ts-rs = { version = "9.0.1", optional = true } url = "2" +# TODO: point back at a published crates.io release once dr-bonez/visit-rs#1 is merged + published. +visit-rs = { version = "0.1.10", optional = true, default-features = false, features = ["serde", "meta"], git = "https://github.com/helix-nine/visit-rs", branch = "fix/ts-bindings-metadata" } yajrc = "0.1" + +[[test]] +name = "test" +required-features = ["ts"] + +[[test]] +name = "ts_bindings" +required-features = ["ts"] + +[[example]] +name = "generate_ts" +required-features = ["ts"] diff --git a/examples/generate_ts.rs b/examples/generate_ts.rs new file mode 100644 index 0000000..c461d79 --- /dev/null +++ b/examples/generate_ts.rs @@ -0,0 +1,59 @@ +//! Generate a TypeScript bindings module for a handler tree: +//! `cargo run --example generate_ts --features ts` (gated by `required-features`). + +use rpc_toolkit::ts::handler_bindings; +use rpc_toolkit::{ + Context, Empty, HandlerExt, ParentHandler, from_fn_async, impl_ts_enum, impl_ts_struct, +}; +use serde::{Deserialize, Serialize}; +use visit_rs::{VisitFields, VisitVariants}; +use yajrc::RpcError; + +#[derive(Clone)] +struct Ctx; +impl Context for Ctx {} + +#[derive(Serialize, Deserialize, VisitFields)] +#[serde(rename_all = "camelCase")] +struct CreateUser { + user_name: String, + age: u32, + email: Option, +} +impl_ts_struct!(CreateUser); + +#[derive(Serialize, Deserialize, VisitVariants)] +#[serde(tag = "status", content = "data")] +enum JobResult { + Pending, + Done { id: u64 }, + Failed(String), +} +impl_ts_enum!(JobResult); + +async fn create_user(_: Ctx, p: CreateUser) -> Result { + Ok(p.age as u64) +} +async fn job(_: Ctx, _: Empty) -> Result { + Ok(JobResult::Pending) +} +// A directly-callable parent: invoking `jobs` itself (empty sub-method) returns the latest job. +async fn jobs_root(_: Ctx, _: Empty) -> Result { + Ok(JobResult::Pending) +} + +fn main() { + let root = ParentHandler::::new() + .subcommand("createUser", from_fn_async(create_user).no_cli()) + .subcommand( + "jobs", + ParentHandler::::new() + .root_handler(from_fn_async(jobs_root).no_cli()) + .subcommand("run", from_fn_async(job).no_cli()), + ); + + match handler_bindings(&root, "Api") { + Some(module) => print!("{module}"), + None => eprintln!("root handler opted out of TS"), + } +} diff --git a/src/cli.rs b/src/cli.rs index 2b12dc4..3d008d7 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -13,9 +13,11 @@ use tokio::io::{AsyncBufReadExt, AsyncRead, AsyncWrite, AsyncWriteExt, BufReader use url::Url; use yajrc::{Id, RpcError}; +#[cfg(feature = "ts")] +use crate::ts::HandlerTSBindings; use crate::util::{internal_error, invalid_params, parse_error, without, Flat, PhantomData}; use crate::{ - AnyHandler, CliBindings, CliBindingsAny, Empty, HandleAny, HandleAnyArgs, HandlerArgs, + Adapter, AnyHandler, CliBindings, CliBindingsAny, Empty, HandleAny, HandleAnyArgs, HandlerArgs, HandlerArgsFor, HandlerFor, HandlerTypes, Name, ParentHandler, PrintCliResult, }; @@ -233,15 +235,22 @@ where type Err = RemoteHandler::Err; } -#[cfg(feature = "ts-rs")] -impl crate::handler::HandlerTS +impl Adapter + for CallRemoteHandler +{ + type Inner = RemoteHandler; + fn as_inner(&self) -> &Self::Inner { + &self.handler + } +} +#[cfg(feature = "ts")] +impl HandlerTSBindings for CallRemoteHandler where - RemoteHandler: crate::handler::HandlerTS, - Extra: Send + Sync + 'static, + RemoteHandler: HandlerTSBindings, { - fn type_info(&self) -> Option { - self.handler.type_info() + fn get_ts<'a>(&'a self) -> Option> { + self.handler.get_ts() } } diff --git a/src/handler/adapters.rs b/src/handler/adapters.rs index 844d813..817ede7 100644 --- a/src/handler/adapters.rs +++ b/src/handler/adapters.rs @@ -8,14 +8,29 @@ use imbl_value::imbl::OrdMap; use imbl_value::Value; use serde::de::DeserializeOwned; use serde::Serialize; +#[cfg(feature = "ts")] +use visit_rs::{Static, Visit}; use yajrc::RpcError; +#[cfg(feature = "ts")] +use crate::ts::{ + HandlerTS, HandlerTSBindings, PassthroughChildrenTS, PassthroughParamsTS, PassthroughReturnTS, + TSVisitor, +}; use crate::util::{Flat, PhantomData}; +#[cfg(feature = "ts")] +use crate::PassthroughCliBindings; use crate::{ CallRemote, CallRemoteHandler, CliBindings, DynHandler, Handler, HandlerArgs, HandlerArgsFor, - HandlerFor, HandlerTypes, LeafHandler, OrEmpty, PrintCliResult, WithContext, + HandlerFor, HandlerTypes, LeafHandler, OrEmpty, PassthroughHandlerFor, PassthroughHandlerTypes, + PrintCliResult, WithContext, }; +pub trait Adapter { + type Inner; + fn as_inner(&self) -> &Self::Inner; +} + pub trait HandlerExt: HandlerFor + Sized { fn no_cli(self) -> NoCli; fn no_display(self) -> NoDisplay; @@ -44,9 +59,24 @@ pub trait HandlerExt: HandlerFor + Sized { fn with_about(self, message: M) -> WithAbout where M: IntoResettable; + #[cfg(feature = "ts")] fn no_ts(self) -> NoTS; - fn unknown_ts(self) -> UnknownTS; - fn custom_ts(self, params_ty: String, return_ty: String) -> CustomTS; + #[cfg(feature = "ts")] + fn override_params_ts(self, params_ty: Params) -> OverrideParamsTS + where + Params: Visit; + #[cfg(feature = "ts")] + fn override_return_ts(self, params_ty: Params) -> OverrideReturnTS + where + Params: Visit; + #[cfg(feature = "ts")] + fn override_params_ts_as(self) -> OverrideParamsTS> + where + Static: Visit; + #[cfg(feature = "ts")] + fn override_return_ts_as(self) -> OverrideReturnTS> + where + Static: Visit; } impl + Sized> HandlerExt for T { @@ -111,97 +141,67 @@ impl + Sized> HandlerExt NoTS { NoTS(self) } - fn unknown_ts(self) -> UnknownTS { - UnknownTS(self) + #[cfg(feature = "ts")] + fn override_params_ts(self, params_ty: Params) -> OverrideParamsTS + where + Params: Visit, + { + OverrideParamsTS { + handler: self, + params_ts: params_ty, + } } - fn custom_ts(self, params_ty: String, return_ty: String) -> CustomTS { - CustomTS { + #[cfg(feature = "ts")] + fn override_return_ts(self, return_ty: Return) -> OverrideReturnTS + where + Return: Visit, + { + OverrideReturnTS { handler: self, - params_ty, return_ty, } } -} - -#[derive(Debug, Clone)] -pub struct NoCli(pub H); -impl LeafHandler for NoCli {} + #[cfg(feature = "ts")] + fn override_params_ts_as(self) -> OverrideParamsTS> + where + Static: Visit, + { + OverrideParamsTS { + handler: self, + params_ts: Static::::new(), + } + } -impl HandlerTypes for NoCli { - type Params = H::Params; - type InheritedParams = H::InheritedParams; - type Ok = H::Ok; - type Err = H::Err; -} -#[cfg(feature = "ts-rs")] -impl crate::handler::HandlerTS for NoCli -where - H: crate::handler::HandlerTS, -{ - fn type_info(&self) -> Option { - self.0.type_info() + #[cfg(feature = "ts")] + fn override_return_ts_as(self) -> OverrideReturnTS> + where + Static: Visit, + { + OverrideReturnTS { + handler: self, + return_ty: Static::::new(), + } } } -impl HandlerFor for NoCli -where - Context: crate::Context, - H: HandlerFor, -{ - fn handle_sync( - &self, - HandlerArgs { - context, - parent_method, - method, - params, - inherited_params, - raw_params, - }: HandlerArgsFor, - ) -> Result { - self.0.handle_sync(HandlerArgs { - context, - parent_method, - method, - params, - inherited_params, - raw_params, - }) - } - async fn handle_async( - &self, - HandlerArgs { - context, - parent_method, - method, - params, - inherited_params, - raw_params, - }: HandlerArgsFor, - ) -> Result { - self.0 - .handle_async(HandlerArgs { - context, - parent_method, - method, - params, - inherited_params, - raw_params, - }) - .await - } - fn metadata(&self, method: VecDeque<&'static str>) -> OrdMap<&'static str, Value> { - self.0.metadata(method) - } - fn method_from_dots(&self, method: &str) -> Option> { - self.0.method_from_dots(method) + +#[derive(Debug, Clone)] +pub struct NoCli(pub H); +impl Adapter for NoCli { + type Inner = H; + fn as_inner(&self) -> &Self::Inner { + &self.0 } } +impl LeafHandler for NoCli {} +impl PassthroughHandlerTypes for NoCli {} +impl PassthroughHandlerFor for NoCli {} impl CliBindings for NoCli where Context: crate::Context, @@ -221,82 +221,34 @@ where unimplemented!() } } - -#[derive(Debug, Clone)] -pub struct NoDisplay(pub H); - -impl LeafHandler for NoDisplay {} - -impl HandlerTypes for NoDisplay { - type Params = H::Params; - type InheritedParams = H::InheritedParams; - type Ok = H::Ok; - type Err = H::Err; -} -#[cfg(feature = "ts-rs")] -impl crate::handler::HandlerTS for NoDisplay +#[cfg(feature = "ts")] +impl HandlerTSBindings for NoCli where - H: crate::handler::HandlerTS, + H: HandlerTSBindings, { - fn type_info(&self) -> Option { - self.0.type_info() + fn get_ts<'a>(&'a self) -> Option> { + self.0.get_ts() } } -impl HandlerFor for NoDisplay -where - Context: crate::Context, - H: HandlerFor, -{ - fn handle_sync( - &self, - HandlerArgs { - context, - parent_method, - method, - params, - inherited_params, - raw_params, - }: HandlerArgsFor, - ) -> Result { - self.0.handle_sync(HandlerArgs { - context, - parent_method, - method, - params, - inherited_params, - raw_params, - }) - } - async fn handle_async( - &self, - HandlerArgs { - context, - parent_method, - method, - params, - inherited_params, - raw_params, - }: HandlerArgsFor, - ) -> Result { - self.0 - .handle_async(HandlerArgs { - context, - parent_method, - method, - params, - inherited_params, - raw_params, - }) - .await - } - fn metadata(&self, method: VecDeque<&'static str>) -> OrdMap<&'static str, Value> { - self.0.metadata(method) - } - fn method_from_dots(&self, method: &str) -> Option> { - self.0.method_from_dots(method) +#[derive(Debug, Clone)] +pub struct NoDisplay(pub H); + +impl Adapter for NoDisplay { + type Inner = H; + fn as_inner(&self) -> &Self::Inner { + &self.0 } } +impl LeafHandler for NoDisplay {} +impl PassthroughHandlerTypes for NoDisplay {} +impl PassthroughHandlerFor for NoDisplay {} +#[cfg(feature = "ts")] +impl PassthroughParamsTS for NoDisplay {} +#[cfg(feature = "ts")] +impl PassthroughReturnTS for NoDisplay {} +#[cfg(feature = "ts")] +impl PassthroughChildrenTS for NoDisplay {} impl PrintCliResult for NoDisplay where Context: crate::Context, @@ -344,83 +296,24 @@ pub struct CustomDisplay { handler: H, } -impl LeafHandler for CustomDisplay {} - -impl HandlerTypes for CustomDisplay -where - H: HandlerTypes, -{ - type Params = H::Params; - type InheritedParams = H::InheritedParams; - type Ok = H::Ok; - type Err = H::Err; -} -#[cfg(feature = "ts-rs")] -impl crate::handler::HandlerTS for CustomDisplay -where - H: crate::handler::HandlerTS, - P: Send + Sync + Clone + 'static, -{ - fn type_info(&self) -> Option { - self.handler.type_info() - } -} - -impl HandlerFor for CustomDisplay +impl Adapter for CustomDisplay where - Context: crate::Context, - H: HandlerFor, P: Send + Sync + Clone + 'static, { - fn handle_sync( - &self, - HandlerArgs { - context, - parent_method, - method, - params, - inherited_params, - raw_params, - }: HandlerArgsFor, - ) -> Result { - self.handler.handle_sync(HandlerArgs { - context, - parent_method, - method, - params, - inherited_params, - raw_params, - }) - } - async fn handle_async( - &self, - HandlerArgs { - context, - parent_method, - method, - params, - inherited_params, - raw_params, - }: HandlerArgsFor, - ) -> Result { - self.handler - .handle_async(HandlerArgs { - context, - parent_method, - method, - params, - inherited_params, - raw_params, - }) - .await - } - fn metadata(&self, method: VecDeque<&'static str>) -> OrdMap<&'static str, Value> { - self.handler.metadata(method) - } - fn method_from_dots(&self, method: &str) -> Option> { - self.handler.method_from_dots(method) + type Inner = H; + fn as_inner(&self) -> &Self::Inner { + &self.handler } } +impl LeafHandler for CustomDisplay {} +impl PassthroughHandlerTypes for CustomDisplay where P: Send + Sync + Clone + 'static {} +impl PassthroughHandlerFor for CustomDisplay where P: Send + Sync + Clone + 'static {} +#[cfg(feature = "ts")] +impl PassthroughParamsTS for CustomDisplay where P: Send + Sync + Clone + 'static {} +#[cfg(feature = "ts")] +impl PassthroughReturnTS for CustomDisplay where P: Send + Sync + Clone + 'static {} +#[cfg(feature = "ts")] +impl PassthroughChildrenTS for CustomDisplay where P: Send + Sync + Clone + 'static {} impl PrintCliResult for CustomDisplay where Context: crate::Context, @@ -498,6 +391,17 @@ pub struct CustomDisplayFn { handler: H, } +impl Adapter for CustomDisplayFn +where + F: Send + Sync + Clone + 'static, + Context: 'static, +{ + type Inner = H; + fn as_inner(&self) -> &Self::Inner { + &self.handler + } +} + impl LeafHandler for CustomDisplayFn {} impl Clone for CustomDisplayFn { @@ -517,90 +421,46 @@ impl Debug for CustomDisplayFn { .finish() } } -impl HandlerTypes for CustomDisplayFn +impl PassthroughHandlerTypes for CustomDisplayFn where - H: HandlerTypes, + F: Send + Sync + Clone + 'static, + Context: 'static, { - type Params = H::Params; - type InheritedParams = H::InheritedParams; - type Ok = H::Ok; - type Err = H::Err; } -#[cfg(feature = "ts-rs")] -impl crate::handler::HandlerTS for CustomDisplayFn +impl PassthroughHandlerFor for CustomDisplayFn where - H: crate::handler::HandlerTS, F: Send + Sync + Clone + 'static, Context: 'static, { - fn type_info(&self) -> Option { - self.handler.type_info() - } } - -impl HandlerFor for CustomDisplayFn +#[cfg(feature = "ts")] +impl PassthroughParamsTS for CustomDisplayFn where - Context: crate::Context, - C: 'static, - H: HandlerFor, F: Send + Sync + Clone + 'static, + Context: 'static, { - fn handle_sync( - &self, - HandlerArgs { - context, - parent_method, - method, - params, - inherited_params, - raw_params, - }: HandlerArgsFor, - ) -> Result { - self.handler.handle_sync(HandlerArgs { - context, - parent_method, - method, - params, - inherited_params, - raw_params, - }) - } - async fn handle_async( - &self, - HandlerArgs { - context, - parent_method, - method, - params, - inherited_params, - raw_params, - }: HandlerArgsFor, - ) -> Result { - self.handler - .handle_async(HandlerArgs { - context, - parent_method, - method, - params, - inherited_params, - raw_params, - }) - .await - } - fn metadata(&self, method: VecDeque<&'static str>) -> OrdMap<&'static str, Value> { - self.handler.metadata(method) - } - fn method_from_dots(&self, method: &str) -> Option> { - self.handler.method_from_dots(method) - } -} -impl PrintCliResult for CustomDisplayFn -where - Context: crate::Context, - H: HandlerTypes, - F: Fn(HandlerArgsFor, H::Ok) -> Result<(), H::Err> + Send + Sync + Clone + 'static, -{ - fn print( +} +#[cfg(feature = "ts")] +impl PassthroughReturnTS for CustomDisplayFn +where + F: Send + Sync + Clone + 'static, + Context: 'static, +{ +} +#[cfg(feature = "ts")] +impl PassthroughChildrenTS for CustomDisplayFn +where + F: Send + Sync + Clone + 'static, + Context: 'static, +{ +} +impl PrintCliResult for CustomDisplayFn +where + Context: crate::Context, + H: HandlerTypes, + F: Fn(HandlerArgsFor, H::Ok) -> Result<(), H::Err> + Send + Sync + Clone + 'static, +{ + fn print( &self, HandlerArgs { context, @@ -685,7 +545,7 @@ impl Handler where Context: crate::Context + CallRemote, RemoteContext: crate::Context, - H: HandlerFor + CliBindings + crate::handler::HandlerTS, + H: HandlerFor + CliBindings + crate::ts::HandlerTSBindings, H::Ok: Serialize + DeserializeOwned, H::Err: From, H::Params: Serialize + DeserializeOwned, @@ -713,6 +573,18 @@ pub struct InheritanceHandler { inherit: F, } +impl Adapter for InheritanceHandler +where + Params: Send + Sync + 'static, + InheritedParams: Send + Sync + 'static, + F: Send + Sync + Clone + 'static, +{ + type Inner = H; + fn as_inner(&self) -> &Self::Inner { + &self.handler + } +} + impl LeafHandler for InheritanceHandler { @@ -751,17 +623,32 @@ where type Err = H::Err; } -#[cfg(feature = "ts-rs")] -impl crate::handler::HandlerTS +#[cfg(feature = "ts")] +impl PassthroughParamsTS for InheritanceHandler where Params: Send + Sync + 'static, InheritedParams: Send + Sync + 'static, - H: crate::handler::HandlerTS, + F: Send + Sync + Clone + 'static, +{ +} +#[cfg(feature = "ts")] +impl PassthroughReturnTS + for InheritanceHandler +where + Params: Send + Sync + 'static, + InheritedParams: Send + Sync + 'static, + F: Send + Sync + Clone + 'static, +{ +} +#[cfg(feature = "ts")] +impl PassthroughChildrenTS + for InheritanceHandler +where + Params: Send + Sync + 'static, + InheritedParams: Send + Sync + 'static, + F: Send + Sync + Clone + 'static, { - fn type_info(&self) -> Option { - self.handler.type_info() - } } impl HandlerFor @@ -873,87 +760,30 @@ pub struct WithAbout { message: M, } -impl LeafHandler for WithAbout {} - -impl HandlerTypes for WithAbout +impl Adapter for WithAbout where - H: HandlerTypes, -{ - type Params = H::Params; - type InheritedParams = H::InheritedParams; - type Ok = H::Ok; - type Err = H::Err; -} -#[cfg(feature = "ts-rs")] -impl crate::handler::HandlerTS for WithAbout -where - H: crate::handler::HandlerTS, M: Clone + Send + Sync + 'static, { - fn type_info(&self) -> Option { - self.handler.type_info() - } -} -impl HandlerFor for WithAbout -where - Context: crate::Context, - H: HandlerFor, - M: Clone + Send + Sync + 'static, -{ - fn handle_sync( - &self, - HandlerArgs { - context, - parent_method, - method, - params, - inherited_params, - raw_params, - }: HandlerArgsFor, - ) -> Result { - self.handler.handle_sync(HandlerArgs { - context, - parent_method, - method, - params, - inherited_params, - raw_params, - }) - } - async fn handle_async( - &self, - HandlerArgs { - context, - parent_method, - method, - params, - inherited_params, - raw_params, - }: HandlerArgsFor, - ) -> Result { - self.handler - .handle_async(HandlerArgs { - context, - parent_method, - method, - params, - inherited_params, - raw_params, - }) - .await - } - fn metadata(&self, method: VecDeque<&'static str>) -> OrdMap<&'static str, Value> { - self.handler.metadata(method) - } - fn method_from_dots(&self, method: &str) -> Option> { - self.handler.method_from_dots(method) + type Inner = H; + fn as_inner(&self) -> &Self::Inner { + &self.handler } } + +impl LeafHandler for WithAbout {} +impl PassthroughHandlerTypes for WithAbout where M: Clone + Send + Sync + 'static {} +impl PassthroughHandlerFor for WithAbout where M: Clone + Send + Sync + 'static {} +#[cfg(feature = "ts")] +impl PassthroughParamsTS for WithAbout where M: Clone + Send + Sync + 'static {} +#[cfg(feature = "ts")] +impl PassthroughReturnTS for WithAbout where M: Clone + Send + Sync + 'static {} +#[cfg(feature = "ts")] +impl PassthroughChildrenTS for WithAbout where M: Clone + Send + Sync + 'static {} impl CliBindings for WithAbout where Context: crate::Context, H: CliBindings, - M: IntoResettable + Clone, + M: IntoResettable + Clone + Send + Sync + 'static, { fn cli_command(&self) -> clap::Command { self.handler.cli_command().about(self.message.clone()) @@ -973,309 +803,83 @@ where } } -#[derive(Debug, Clone)] -pub struct NoTS(pub H); - -impl LeafHandler for NoTS {} - -impl HandlerTypes for NoTS -where - H: HandlerTypes, -{ - type Params = H::Params; - type InheritedParams = H::InheritedParams; - type Ok = H::Ok; - type Err = H::Err; -} +#[cfg(feature = "ts")] +pub use ts::*; -#[cfg(feature = "ts-rs")] -impl crate::handler::HandlerTS for NoTS { - fn type_info(&self) -> Option { - None - } -} +#[cfg(feature = "ts")] +mod ts { + use super::*; + use crate::ts::{HandlerTSBindings, ParamsTS, ReturnTS}; -impl HandlerFor for NoTS -where - Context: crate::Context, - H: HandlerFor, -{ - fn handle_sync( - &self, - HandlerArgs { - context, - parent_method, - method, - params, - inherited_params, - raw_params, - }: HandlerArgsFor, - ) -> Result { - self.0.handle_sync(HandlerArgs { - context, - parent_method, - method, - params, - inherited_params, - raw_params, - }) - } - async fn handle_async( - &self, - HandlerArgs { - context, - parent_method, - method, - params, - inherited_params, - raw_params, - }: HandlerArgsFor, - ) -> Result { - self.0 - .handle_async(HandlerArgs { - context, - parent_method, - method, - params, - inherited_params, - raw_params, - }) - .await - } - fn metadata(&self, method: VecDeque<&'static str>) -> OrdMap<&'static str, Value> { - self.0.metadata(method) - } - fn method_from_dots(&self, method: &str) -> Option> { - self.0.method_from_dots(method) - } -} - -impl CliBindings for NoTS -where - Context: crate::Context, - H: CliBindings, -{ - fn cli_command(&self) -> clap::Command { - self.0.cli_command() - } - fn cli_parse( - &self, - arg_matches: &clap::ArgMatches, - ) -> Result<(VecDeque<&'static str>, Value), clap::Error> { - self.0.cli_parse(arg_matches) - } - fn cli_display( - &self, - handler: HandlerArgsFor, - result: Self::Ok, - ) -> Result<(), Self::Err> { - self.0.cli_display(handler, result) - } -} - -#[derive(Debug, Clone)] -pub struct UnknownTS(pub H); - -impl LeafHandler for UnknownTS {} - -impl HandlerTypes for UnknownTS -where - H: HandlerTypes, -{ - type Params = H::Params; - type InheritedParams = H::InheritedParams; - type Ok = H::Ok; - type Err = H::Err; -} - -#[cfg(feature = "ts-rs")] -impl crate::handler::HandlerTS for UnknownTS { - fn type_info(&self) -> Option { - Some("{_PARAMS:unknown,_RETURN:unknown}".to_string()) - } -} - -impl HandlerFor for UnknownTS -where - Context: crate::Context, - H: HandlerFor, -{ - fn handle_sync( - &self, - HandlerArgs { - context, - parent_method, - method, - params, - inherited_params, - raw_params, - }: HandlerArgsFor, - ) -> Result { - self.0.handle_sync(HandlerArgs { - context, - parent_method, - method, - params, - inherited_params, - raw_params, - }) - } - async fn handle_async( - &self, - HandlerArgs { - context, - parent_method, - method, - params, - inherited_params, - raw_params, - }: HandlerArgsFor, - ) -> Result { - self.0 - .handle_async(HandlerArgs { - context, - parent_method, - method, - params, - inherited_params, - raw_params, - }) - .await - } - fn metadata(&self, method: VecDeque<&'static str>) -> OrdMap<&'static str, Value> { - self.0.metadata(method) - } - fn method_from_dots(&self, method: &str) -> Option> { - self.0.method_from_dots(method) - } -} - -impl CliBindings for UnknownTS -where - Context: crate::Context, - H: CliBindings, -{ - fn cli_command(&self) -> clap::Command { - self.0.cli_command() - } - fn cli_parse( - &self, - arg_matches: &clap::ArgMatches, - ) -> Result<(VecDeque<&'static str>, Value), clap::Error> { - self.0.cli_parse(arg_matches) - } - fn cli_display( - &self, - handler: HandlerArgsFor, - result: Self::Ok, - ) -> Result<(), Self::Err> { - self.0.cli_display(handler, result) + #[derive(Debug, Clone)] + pub struct NoTS(pub H); + impl Adapter for NoTS { + type Inner = H; + fn as_inner(&self) -> &Self::Inner { + &self.0 + } } -} - -#[derive(Debug, Clone)] -pub struct CustomTS { - pub handler: H, - pub params_ty: String, - pub return_ty: String, -} - -impl LeafHandler for CustomTS {} - -impl HandlerTypes for CustomTS -where - H: HandlerTypes, -{ - type Params = H::Params; - type InheritedParams = H::InheritedParams; - type Ok = H::Ok; - type Err = H::Err; -} - -#[cfg(feature = "ts-rs")] -impl crate::handler::HandlerTS for CustomTS { - fn type_info(&self) -> Option { - Some(format!( - "{{_PARAMS:{},_RETURN:{}}}", - self.params_ty, self.return_ty - )) + impl LeafHandler for NoTS {} + impl PassthroughHandlerTypes for NoTS {} + impl PassthroughHandlerFor for NoTS {} + impl PassthroughCliBindings for NoTS {} + impl HandlerTSBindings for NoTS { + fn get_ts<'a>(&'a self) -> Option> { + None + } } -} -impl HandlerFor for CustomTS -where - Context: crate::Context, - H: HandlerFor, -{ - fn handle_sync( - &self, - HandlerArgs { - context, - parent_method, - method, - params, - inherited_params, - raw_params, - }: HandlerArgsFor, - ) -> Result { - self.handler.handle_sync(HandlerArgs { - context, - parent_method, - method, - params, - inherited_params, - raw_params, - }) + #[derive(Clone, Debug)] + pub struct OverrideParamsTS { + pub(super) handler: H, + pub(super) params_ts: P, } - async fn handle_async( - &self, - HandlerArgs { - context, - parent_method, - method, - params, - inherited_params, - raw_params, - }: HandlerArgsFor, - ) -> Result { - self.handler - .handle_async(HandlerArgs { - context, - parent_method, - method, - params, - inherited_params, - raw_params, - }) - .await - } - fn metadata(&self, method: VecDeque<&'static str>) -> OrdMap<&'static str, Value> { - self.handler.metadata(method) + impl Adapter for OverrideParamsTS { + type Inner = H; + fn as_inner(&self) -> &Self::Inner { + &self.handler + } } - fn method_from_dots(&self, method: &str) -> Option> { - self.handler.method_from_dots(method) + impl LeafHandler for OverrideParamsTS {} + impl PassthroughHandlerTypes for OverrideParamsTS {} + impl PassthroughHandlerFor for OverrideParamsTS {} + impl PassthroughCliBindings for OverrideParamsTS {} + impl ParamsTS for OverrideParamsTS + where + H: Send + Sync, + P: Visit + Send + Sync, + { + fn params_ts<'a>(&'a self) -> Box { + Box::new(move |visitor| self.params_ts.visit(visitor)) + } } -} + impl PassthroughReturnTS for OverrideParamsTS where P: Clone + Send + Sync + 'static {} + impl PassthroughChildrenTS for OverrideParamsTS where P: Clone + Send + Sync + 'static {} -impl CliBindings for CustomTS -where - Context: crate::Context, - H: CliBindings, -{ - fn cli_command(&self) -> clap::Command { - self.handler.cli_command() + #[derive(Clone, Debug)] + pub struct OverrideReturnTS { + pub(super) handler: H, + pub(super) return_ty: R, } - fn cli_parse( - &self, - arg_matches: &clap::ArgMatches, - ) -> Result<(VecDeque<&'static str>, Value), clap::Error> { - self.handler.cli_parse(arg_matches) + impl Adapter for OverrideReturnTS { + type Inner = H; + fn as_inner(&self) -> &Self::Inner { + &self.handler + } } - fn cli_display( - &self, - handler: HandlerArgsFor, - result: Self::Ok, - ) -> Result<(), Self::Err> { - self.handler.cli_display(handler, result) + impl LeafHandler for OverrideReturnTS {} + impl PassthroughHandlerTypes for OverrideReturnTS {} + impl PassthroughHandlerFor for OverrideReturnTS {} + impl PassthroughCliBindings for OverrideReturnTS {} + impl PassthroughParamsTS for OverrideReturnTS {} + impl ReturnTS for OverrideReturnTS + where + H: Send + Sync, + R: Visit + Send + Sync, + { + fn return_ts<'a>(&'a self) -> Option> { + Some(Box::new(move |visitor| self.return_ty.visit(visitor))) + } } + impl PassthroughChildrenTS for OverrideReturnTS {} } diff --git a/src/handler/from_fn.rs b/src/handler/from_fn.rs index 0f672c4..b0c320a 100644 --- a/src/handler/from_fn.rs +++ b/src/handler/from_fn.rs @@ -7,8 +7,6 @@ use imbl_value::imbl::OrdMap; use imbl_value::Value; use serde::de::DeserializeOwned; use serde::Serialize; -#[cfg(feature = "ts-rs")] -use ts_rs::TS; use crate::util::PhantomData; use crate::{ @@ -48,21 +46,6 @@ impl std::fmt::Debug for FromFn { } } -#[cfg(feature = "ts-rs")] -impl crate::handler::HandlerTS for FromFn -where - Self: HandlerTypes, - ::Params: ts_rs::TS, - ::Ok: ts_rs::TS, -{ - fn type_info(&self) -> Option { - Some(format!( - "{{_PARAMS:{},_RETURN:{}}}", - ::Params::inline_flattened(), - ::Ok::inline_flattened(), - )) - } -} impl PrintCliResult for FromFn where Context: crate::Context, @@ -173,21 +156,6 @@ impl std::fmt::Debug for FromFnAsync { } } -#[cfg(feature = "ts-rs")] -impl crate::handler::HandlerTS for FromFnAsync -where - Self: HandlerTypes, - ::Params: ts_rs::TS, - ::Ok: ts_rs::TS, -{ - fn type_info(&self) -> Option { - Some(format!( - "{{_PARAMS:{},_RETURN:{}}}", - ::Params::inline_flattened(), - ::Ok::inline_flattened(), - )) - } -} impl PrintCliResult for FromFnAsync where Context: crate::Context, @@ -285,21 +253,6 @@ impl std::fmt::Debug for FromFnAsyncLocal crate::handler::HandlerTS for FromFnAsyncLocal -where - Self: HandlerTypes, - ::Params: ts_rs::TS, - ::Ok: ts_rs::TS, -{ - fn type_info(&self) -> Option { - Some(format!( - "{{_PARAMS:{},_RETURN:{}}}", - ::Params::inline_flattened(), - ::Ok::inline_flattened(), - )) - } -} impl PrintCliResult for FromFnAsyncLocal where Context: crate::Context, diff --git a/src/handler/mod.rs b/src/handler/mod.rs index be4147b..8b04928 100644 --- a/src/handler/mod.rs +++ b/src/handler/mod.rs @@ -13,6 +13,9 @@ use serde::de::DeserializeOwned; use serde::{Deserialize, Serialize}; use yajrc::RpcError; +#[cfg(feature = "ts")] +use crate::impl_ts_struct; +use crate::ts::HandlerTSBindings; use crate::util::{internal_error, invalid_params, Flat}; pub mod adapters; @@ -55,21 +58,8 @@ impl HandleAnyArgs Option { - None - } -} - -impl HandleAnyTS for Arc { - fn type_info(&self) -> Option { - self.deref().type_info() - } -} - -pub(crate) trait HandleAnyRequires: HandleAnyTS + Send + Sync {} -impl HandleAnyRequires for T {} +pub(crate) trait HandleAnyRequires: HandlerTSBindings + Send + Sync {} +impl HandleAnyRequires for T {} #[async_trait::async_trait] pub(crate) trait HandleAny: HandleAnyRequires { @@ -149,7 +139,9 @@ pub trait PrintCliResult: HandlerTypes { } #[allow(private_interfaces)] -pub struct DynHandler(Arc>); +pub struct DynHandler( + pub(crate) Arc>, +); impl DynHandler { pub fn new(handler: H) -> Option where @@ -169,11 +161,6 @@ impl Debug for DynHandler { f.debug_struct("DynHandler").finish() } } -impl HandleAnyTS for DynHandler { - fn type_info(&self) -> Option { - self.0.type_info() - } -} #[async_trait::async_trait] impl HandleAny for DynHandler @@ -201,6 +188,12 @@ impl HandleAny self.0.cli() } } +#[cfg(feature = "ts")] +impl HandlerTSBindings for DynHandler { + fn get_ts<'a>(&'a self) -> Option> { + self.0.get_ts() + } +} #[allow(type_alias_bounds)] pub type HandlerArgsFor = @@ -227,18 +220,19 @@ pub trait HandlerTypes { type Err: Send + Sync; } -pub trait LeafHandler {} - -pub trait HandlerTS { - fn type_info(&self) -> Option; +pub trait PassthroughHandlerTypes: Adapter {} +impl HandlerTypes for T +where + T: PassthroughHandlerTypes, + T::Inner: HandlerTypes, +{ + type Params = ::Params; + type InheritedParams = ::InheritedParams; + type Ok = ::Ok; + type Err = ::Err; } -#[cfg(not(feature = "ts-rs"))] -impl HandlerTS for T { - fn type_info(&self) -> Option { - None - } -} +pub trait LeafHandler {} pub trait HandlerRequires: HandlerTypes + Clone + Send + Sync + 'static {} impl HandlerRequires for T {} @@ -293,6 +287,70 @@ pub trait HandlerFor: HandlerRequires { } } } +pub trait PassthroughHandlerFor: PassthroughHandlerTypes {} +impl HandlerFor for T +where + T: PassthroughHandlerFor + Clone + Send + Sync + 'static, + T::Inner: HandlerFor, + Context: crate::Context, +{ + fn handle_async( + &self, + handle_args: HandlerArgsFor, + ) -> impl Future> + Send { + self.as_inner().handle_async(handle_args) + } + fn handle_async_with_sync<'a>( + &'a self, + handle_args: HandlerArgsFor, + ) -> impl Future> + Send + 'a { + self.as_inner().handle_async_with_sync(handle_args) + } + fn handle_async_with_sync_blocking<'a>( + &'a self, + handle_args: HandlerArgsFor, + ) -> impl Future> + Send + 'a { + self.as_inner().handle_async_with_sync_blocking(handle_args) + } + fn handle_sync( + &self, + handle_args: HandlerArgsFor, + ) -> Result { + self.as_inner().handle_sync(handle_args) + } + fn metadata(&self, method: VecDeque<&'static str>) -> OrdMap<&'static str, Value> { + self.as_inner().metadata(method) + } + fn method_from_dots(&self, method: &str) -> Option> { + self.as_inner().method_from_dots(method) + } +} + +pub trait PassthroughCliBindings: PassthroughHandlerTypes {} +impl CliBindings for T +where + T: PassthroughCliBindings, + T::Inner: CliBindings, + Context: crate::Context, +{ + const NO_CLI: bool = >::NO_CLI; + fn cli_command(&self) -> Command { + self.as_inner().cli_command() + } + fn cli_parse( + &self, + matches: &ArgMatches, + ) -> Result<(VecDeque<&'static str>, Value), clap::Error> { + self.as_inner().cli_parse(matches) + } + fn cli_display( + &self, + handle_args: HandlerArgsFor, + result: Self::Ok, + ) -> Result<(), Self::Err> { + self.as_inner().cli_display(handle_args, result) + } +} pub trait Handler { type H: HandlerTypes; @@ -315,7 +373,7 @@ impl WithContext { impl Handler for WithContext where Context: crate::Context, - H: HandlerFor + CliBindings + HandlerTS, + H: HandlerFor + CliBindings + HandlerTSBindings, H::Ok: Serialize + DeserializeOwned, H::Params: DeserializeOwned, H::InheritedParams: OrEmpty, @@ -356,12 +414,13 @@ impl std::fmt::Debug for AnyHandler HandleAnyTS for AnyHandler +#[cfg(feature = "ts")] +impl HandlerTSBindings for AnyHandler where - H: HandlerTS, + H: HandlerTSBindings, { - fn type_info(&self) -> Option { - self.handler.type_info() + fn get_ts<'a>(&'a self) -> Option> { + self.handler.get_ts() } } @@ -369,7 +428,7 @@ where impl HandleAny for AnyHandler where Context: crate::Context, - H: HandlerFor + CliBindings + HandlerTS, + H: HandlerFor + CliBindings + HandlerTSBindings, H::Params: DeserializeOwned, H::Ok: Serialize + DeserializeOwned, H::InheritedParams: OrEmpty, @@ -450,10 +509,11 @@ where } #[derive(Debug, Clone, Copy, Deserialize, Serialize, Parser)] -#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))] -#[cfg_attr(feature = "ts-rs", ts(type = "{}"))] +#[cfg_attr(feature = "ts", derive(visit_rs::VisitFields))] #[group(skip)] pub struct Empty {} +#[cfg(feature = "ts")] +impl_ts_struct!(Empty); pub trait OrEmpty { fn from_t(t: T) -> Self; diff --git a/src/handler/parent.rs b/src/handler/parent.rs index c2406ae..567b78e 100644 --- a/src/handler/parent.rs +++ b/src/handler/parent.rs @@ -7,15 +7,11 @@ use imbl_value::Value; use serde::Serialize; use yajrc::RpcError; -#[cfg(feature = "ts-rs")] -use crate::handler::HandleAnyTS; use crate::util::{combine, Flat, PhantomData}; use crate::{ CliBindings, DynHandler, Empty, HandleAny, HandleAnyArgs, Handler, HandlerArgs, HandlerArgsFor, HandlerFor, HandlerRequires, HandlerTypes, WithContext, }; -#[cfg(feature = "ts-rs")] -use crate::{CustomTS, UnknownTS}; #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] pub(crate) struct Name(pub(crate) &'static str); @@ -88,31 +84,6 @@ impl ParentHandler Option { - use std::fmt::Write; - let mut res = "{".to_owned(); - res.push_str("_CHILDREN:{"); - for (name, handler) in &self.subcommands.1 { - let Some(ty) = handler.type_info() else { - continue; - }; - write!( - &mut res, - "{}:{};", - serde_json::to_string(&name.0).unwrap(), - ty, - ) - .ok()?; - } - res.push_str("};}"); - if let Some(ty) = self.subcommands.0.as_ref().and_then(|h| h.type_info()) { - write!(&mut res, "&{}", ty).ok()?; - } else { - write!(&mut res, "&{{_PARAMS:{}}}", params_ty).ok()?; - } - Some(res) - } } impl Clone for ParentHandler { fn clone(&self) -> Self { @@ -169,40 +140,6 @@ where type Err = RpcError; } -#[cfg(feature = "ts-rs")] -impl crate::handler::HandlerTS - for ParentHandler -where - Params: ts_rs::TS + Send + Sync + 'static, - InheritedParams: Send + Sync + 'static, -{ - fn type_info(&self) -> Option { - self.type_info_impl(&Params::inline_flattened()) - } -} -#[cfg(feature = "ts-rs")] -impl crate::handler::HandlerTS - for CustomTS> -where - Params: Send + Sync + 'static, - InheritedParams: Send + Sync + 'static, -{ - fn type_info(&self) -> Option { - self.handler.type_info_impl(&self.params_ty) - } -} -#[cfg(feature = "ts-rs")] -impl crate::handler::HandlerTS - for UnknownTS> -where - Params: Send + Sync + 'static, - InheritedParams: Send + Sync + 'static, -{ - fn type_info(&self) -> Option { - self.0.type_info_impl("unknown") - } -} - impl HandlerFor for ParentHandler where diff --git a/src/lib.rs b/src/lib.rs index 112db5f..7c9e353 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,9 +10,12 @@ pub mod command_helpers; mod context; mod handler; mod server; +#[cfg(feature = "ts")] +pub mod ts; pub mod util; -#[cfg(feature = "ts-rs")] -pub fn type_helpers() -> &'static str { - include_str!("./type-helpers.ts") +#[cfg(not(feature = "ts"))] +pub mod ts { + pub trait HandlerTSBindings {} + impl HandlerTSBindings for T {} } diff --git a/src/server/mod.rs b/src/server/mod.rs index b9467f6..61e0847 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -50,11 +50,11 @@ impl Server { &self, method: &str, params: Value, - ) -> impl Future> + Send + 'static { + ) -> impl Future> + Send + 'static + use { let (make_ctx, root_handler, method) = ( self.make_ctx.clone(), self.root_handler.clone(), - self.root_handler.method_from_dots(method), + self.root_handler.method_from_dots(method.as_ref()), ); async move { @@ -74,14 +74,11 @@ impl Server { &self, RpcRequest { id, method, params }: RpcRequest, ) -> impl Future + Send + 'static { - let handle = (|| Ok::<_, RpcError>(self.handle_command(method.as_str(), params)))(); + let handle = self.handle_command(method.as_str(), params); async move { RpcResponse { id, - result: match handle { - Ok(handle) => handle.await, - Err(e) => Err(e), - }, + result: handle.await, } } } diff --git a/src/ts.rs b/src/ts.rs new file mode 100644 index 0000000..5e40d75 --- /dev/null +++ b/src/ts.rs @@ -0,0 +1,764 @@ +use std::borrow::Cow; +use std::collections::BTreeMap; +use std::ops::Deref; +use std::rc::Rc; +use std::sync::Arc; + +use imbl_value::InternedString; +use imbl_value::imbl::{OrdMap, Vector}; +use visit_rs::metadata::{AttributeMeta, MetaValue}; +use visit_rs::{ + Named, Static, StructInfoData, Variant, Visit, VisitFieldsStatic, VisitFieldsStaticNamed, + VisitVariantFieldsStatic, VisitVariantFieldsStaticNamed, VisitVariantsStatic, Visitor, +}; + +use crate::{Adapter, FromFn, FromFnAsync, FromFnAsyncLocal, HandlerTypes, ParentHandler}; + +pub fn type_helpers() -> &'static str { + include_str!("./type-helpers.ts") +} + +pub trait TS { + const DEFINE: Option<&str> = None; + const IS_ENUMERABLE: bool = false; + fn visit_ts(visitor: &mut TSVisitor); +} + +impl Visit for Static +where + T: TS, +{ + fn visit(&self, visitor: &mut TSVisitor) -> ::Result { + T::visit_ts(visitor); + } +} + +pub trait ParamsTS { + fn params_ts<'a>(&'a self) -> Box; +} +pub trait ReturnTS { + fn return_ts<'a>(&'a self) -> Option>; +} +pub trait ChildrenTS { + fn children_ts<'a>(&'a self) -> Option>; +} + +pub trait HandlerTSBindings { + fn get_ts<'a>(&'a self) -> Option>; +} +impl HandlerTSBindings for T { + fn get_ts<'a>(&'a self) -> Option> { + Some(HandlerTS::new(self)) + } +} +impl HandlerTSBindings for Arc { + fn get_ts<'a>(&'a self) -> Option> { + self.deref().get_ts() + } +} + +pub struct HandlerTS<'a> { + params_ts: Box, + return_ts: Option>, + children: Option>, +} +impl<'a> HandlerTS<'a> { + pub fn new(handler: &'a H) -> Self + where + H: ParamsTS + ReturnTS + ChildrenTS, + { + Self { + params_ts: handler.params_ts(), + return_ts: handler.return_ts(), + children: handler.children_ts(), + } + } +} +impl<'a> Visit for HandlerTS<'a> { + fn visit(&self, visitor: &mut TSVisitor) -> ::Result { + visitor.ts.push_str("{_PARAMS:"); + (self.params_ts)(visitor); + if let Some(return_ty) = &self.return_ts { + visitor.ts.push_str(";_RETURN:"); + return_ty(visitor); + } + if let Some(children) = &self.children { + visitor.ts.push_str(";_CHILDREN:"); + children(visitor); + } + visitor.ts.push_str("}"); + } +} + +impl ParamsTS for FromFn +where + Self: HandlerTypes, + Static<::Params>: Visit, +{ + fn params_ts(&self) -> Box { + Box::new(|visitor| Static::<::Params>::new().visit(visitor)) + } +} +impl ParamsTS for FromFnAsync +where + Self: HandlerTypes, + Static<::Params>: Visit, +{ + fn params_ts(&self) -> Box { + Box::new(|visitor| Static::<::Params>::new().visit(visitor)) + } +} +impl ParamsTS for FromFnAsyncLocal +where + Self: HandlerTypes, + Static<::Params>: Visit, +{ + fn params_ts(&self) -> Box { + Box::new(|visitor| Static::<::Params>::new().visit(visitor)) + } +} +impl ParamsTS for ParentHandler +where + Self: HandlerTypes, + Static<::Params>: Visit, +{ + fn params_ts(&self) -> Box { + Box::new(|visitor| Static::<::Params>::new().visit(visitor)) + } +} + +impl ReturnTS for FromFn +where + Self: HandlerTypes, + Static<::Ok>: Visit, +{ + fn return_ts(&self) -> Option> { + Some(Box::new(|visitor| { + Static::<::Ok>::new().visit(visitor) + })) + } +} +impl ReturnTS for FromFnAsync +where + Self: HandlerTypes, + Static<::Ok>: Visit, +{ + fn return_ts(&self) -> Option> { + Some(Box::new(|visitor| { + Static::<::Ok>::new().visit(visitor) + })) + } +} +impl ReturnTS for FromFnAsyncLocal +where + Self: HandlerTypes, + Static<::Ok>: Visit, +{ + fn return_ts(&self) -> Option> { + Some(Box::new(|visitor| { + Static::<::Ok>::new().visit(visitor) + })) + } +} +impl ReturnTS + for ParentHandler +{ + fn return_ts<'a>(&'a self) -> Option> { + // A parent with a root handler is itself callable (at the empty method path); surface + // the root handler's return type as the parent node's `_RETURN`. Its params already + // match the parent's `_PARAMS` (enforced by `root_handler`'s `Params = Params` bound). + self.subcommands.0.as_ref()?.get_ts()?.return_ts + } +} + +impl ChildrenTS for FromFn { + fn children_ts(&self) -> Option> { + None + } +} +impl ChildrenTS for FromFnAsync { + fn children_ts(&self) -> Option> { + None + } +} +impl ChildrenTS for FromFnAsyncLocal { + fn children_ts(&self) -> Option> { + None + } +} +impl ChildrenTS + for ParentHandler +where + Context: crate::Context, + Params: Send + Sync + 'static, + InheritedParams: Send + Sync + 'static, +{ + fn children_ts<'a>(&'a self) -> Option> { + use std::fmt::Write; + + Some(Box::new(move |visitor| { + visitor.ts.push('{'); + for (name, handler) in &self.subcommands.1 { + write!( + &mut visitor.ts, + "{}:", + serde_json::to_string(&name.0).unwrap() + ) + .ok(); + // A `no_ts()` child still needs a well-formed leaf node, otherwise a bare + // `unknown` value fails the `RpcHandler` index-signature constraint in + // type-helpers.ts and poisons inference for the entire tree. + if let Some(ts) = handler.0.get_ts() { + ts.visit(visitor); + } else { + visitor.ts.push_str("{_PARAMS:unknown;_RETURN:unknown}"); + } + visitor.ts.push(';'); + } + visitor.ts.push('}'); + })) + } +} + +pub trait PassthroughParamsTS: Adapter {} +impl ParamsTS for T +where + T::Inner: ParamsTS, +{ + fn params_ts<'a>(&'a self) -> Box { + self.as_inner().params_ts() + } +} + +pub trait PassthroughReturnTS: Adapter {} +impl ReturnTS for T +where + T::Inner: ReturnTS, +{ + fn return_ts<'a>(&'a self) -> Option> { + self.as_inner().return_ts() + } +} + +pub trait PassthroughChildrenTS: Adapter {} +impl ChildrenTS for T +where + T::Inner: ChildrenTS, +{ + fn children_ts<'a>(&'a self) -> Option> { + self.as_inner().children_ts() + } +} + +#[derive(Default, Debug, Clone)] +pub struct TSVisitor { + pub definitions: BTreeMap<&'static str, String>, + pub ts: String, + /// Inner types of `#[serde(flatten)]` fields, collected while visiting the current object + /// and emitted as `& Inner` intersection members once its `{...}` body is closed. + flattened: Vec, +} +impl TSVisitor { + pub fn new() -> Self { + Self::default() + } + pub fn visit_ty>(&mut self, value: &V, define: Option<&'static str>) { + if let Some(name) = define { + self.ts.push_str(name); + let mut defn = Self::new(); + value.visit(&mut defn); + self.load_definition(name, defn); + } else { + value.visit(self); + } + } + pub fn append_type(&mut self) + where + T: TS, + { + self.visit_ty(&Static::::new(), T::DEFINE); + } + pub fn insert_definition(&mut self, name: &'static str, definition: String) { + // Two distinct types sharing a `DEFINE` name is a caller bug; flag it in debug + // rather than panicking a release build (this is a pure type-generation library). + if let Some(def) = self.definitions.get(&name) { + debug_assert_eq!(def, &definition, "Conflicting definitions for {name}"); + } + debug_assert!(!definition.is_empty()); + self.definitions.insert(name, definition); + } + pub fn load_definition( + &mut self, + name: &'static str, + TSVisitor { definitions, ts, .. }: TSVisitor, + ) { + for (name, definition) in definitions { + self.insert_definition(name, definition); + } + self.insert_definition(name, ts); + } + /// Render a complete TypeScript module: the `type-helpers.ts` declarations, every named + /// `DEFINE` definition collected while visiting, and an exported alias `root_name` for the + /// visited root type. The result type-checks against `RpcParamType`/`RpcReturnType`. + pub fn into_module(self, root_name: &str) -> String { + use std::fmt::Write; + let mut out = String::new(); + out.push_str(type_helpers()); + out.push('\n'); + for (name, def) in &self.definitions { + writeln!(&mut out, "export type {name} = {def};").ok(); + } + writeln!(&mut out, "export type {root_name} = {};", self.ts).ok(); + out + } +} + +/// Build a full TypeScript bindings module for a handler tree, or `None` if the root handler +/// opts out of TS (`no_ts()`). `root_name` is the exported alias for the root handler type. +pub fn handler_bindings(handler: &H, root_name: &str) -> Option { + let ts = handler.get_ts()?; + let mut visitor = TSVisitor::new(); + ts.visit(&mut visitor); + Some(visitor.into_module(root_name)) +} +impl Visitor for TSVisitor { + type Result = (); +} + +impl<'a, T> Visit for Named<'a, Static> +where + T: TS, + Static: Visit, +{ + fn visit(&self, visitor: &mut TSVisitor) -> ::Result { + use std::fmt::Write; + + // serde drops `skip` / `skip_serializing` fields from the wire format. + if field_is_skipped(self.metadata) { + return; + } + // serde `flatten` splices the inner type's fields into the parent object; model it as a + // TS intersection member. Capture the inner type and stash it for `visit_struct_impl`. + if field_is_flattened(self.metadata) { + let start = visitor.ts.len(); + visitor.append_type::(); + let inner = visitor.ts.split_off(start); + visitor.flattened.push(inner); + return; + } + if let Some(name) = self.name { + if name.chars().all(|c| c.is_alphanumeric() || c == '_') + && name + .chars() + .next() + .map_or(false, |c| c.is_alphabetic() || c == '_') + { + visitor.ts.push_str(name); + } else { + write!(&mut visitor.ts, "{}", serde_json::to_string(&name).unwrap()).unwrap(); + } + // `skip_serializing_if` / `default` make the key optional on the wire. + if field_is_optional(self.metadata) { + visitor.ts.push('?'); + } + visitor.ts.push_str(":"); + } + visitor.append_type::(); + if self.name.is_some() { + visitor.ts.push(';'); + } else { + visitor.ts.push(','); + } + } +} + +/// The serde enum representation, resolved at runtime from the enum's attribute metadata. +enum SerdeRepr { + External, + Internal { + tag: &'static str, + }, + Adjacent { + tag: &'static str, + content: &'static str, + }, + Untagged, +} + +fn meta_str(items: &'static [AttributeMeta], name: &str) -> Option<&'static str> { + items.iter().find_map(|m| match m { + AttributeMeta::NameValue { + name: n, + value: MetaValue::Str(s), + .. + } if *n == name => Some(*s), + _ => None, + }) +} + +fn meta_has_path(items: &'static [AttributeMeta], name: &str) -> bool { + items + .iter() + .any(|m| matches!(m, AttributeMeta::Path { path } if *path == name)) +} + +/// Whether a `#[serde(..)]` / `#[visit(..)]` list anywhere in `metadata` carries an item +/// named `name`, whether as a bare flag (`skip`) or a name-value (`skip_serializing_if = "…"`). +fn meta_flag(metadata: &'static [AttributeMeta], name: &str) -> bool { + metadata.iter().any(|m| match m { + AttributeMeta::List { items, .. } => items.iter().any(|i| match i { + AttributeMeta::Path { path } => *path == name, + AttributeMeta::NameValue { name: n, .. } => *n == name, + AttributeMeta::List { path, .. } => *path == name, + AttributeMeta::Unparsed { .. } => false, + }), + _ => false, + }) +} + +fn field_is_skipped(metadata: &'static [AttributeMeta]) -> bool { + meta_flag(metadata, "skip") || meta_flag(metadata, "skip_serializing") +} + +fn field_is_optional(metadata: &'static [AttributeMeta]) -> bool { + meta_flag(metadata, "skip_serializing_if") || meta_flag(metadata, "default") +} + +fn field_is_flattened(metadata: &'static [AttributeMeta]) -> bool { + meta_flag(metadata, "flatten") +} + +fn struct_is_transparent(metadata: &'static [AttributeMeta]) -> bool { + meta_flag(metadata, "transparent") +} + +fn enum_repr(metadata: &'static [AttributeMeta]) -> SerdeRepr { + let (mut tag, mut content, mut untagged) = (None, None, false); + for m in metadata { + if let AttributeMeta::List { items, .. } = m { + tag = tag.or_else(|| meta_str(items, "tag")); + content = content.or_else(|| meta_str(items, "content")); + untagged |= meta_has_path(items, "untagged"); + } + } + match (untagged, tag, content) { + (true, _, _) => SerdeRepr::Untagged, + (false, Some(tag), Some(content)) => SerdeRepr::Adjacent { tag, content }, + (false, Some(tag), None) => SerdeRepr::Internal { tag }, + (false, None, _) => SerdeRepr::External, + } +} + +fn push_json_str(visitor: &mut TSVisitor, s: &str) { + use std::fmt::Write; + write!(&mut visitor.ts, "{}", serde_json::to_string(s).unwrap()).unwrap(); +} + +impl<'a, T> Visit for Variant<'a, Static> +where + T: TS, + T: VisitVariantFieldsStaticNamed + VisitVariantFieldsStatic, +{ + fn visit(&self, visitor: &mut TSVisitor) -> ::Result { + if T::DATA.variant_count > 1 { + visitor.ts.push('|'); + } + let name = self.info.name; + // A unit variant (serde serializes it as a bare value) — distinct from an empty + // struct variant `V {}`, which still serializes as an object. + let is_unit = self.info.field_count == 0 && !self.info.named_fields; + let render_payload = |visitor: &mut TSVisitor| { + visit_struct_impl( + &self.info, + |named, visitor| { + if named { + T::visit_variant_fields_static_named(&self.info, visitor).for_each(drop); + } else { + T::visit_variant_fields_static(&self.info, visitor).for_each(drop); + } + }, + visitor, + ); + }; + match enum_repr(T::DATA.metadata) { + SerdeRepr::External => { + if is_unit { + push_json_str(visitor, name); + } else { + visitor.ts.push('{'); + push_json_str(visitor, name); + visitor.ts.push(':'); + render_payload(visitor); + visitor.ts.push('}'); + } + } + SerdeRepr::Internal { tag } => { + visitor.ts.push('{'); + push_json_str(visitor, tag); + visitor.ts.push(':'); + push_json_str(visitor, name); + visitor.ts.push('}'); + if !is_unit { + visitor.ts.push('&'); + render_payload(visitor); + } + } + SerdeRepr::Adjacent { tag, content } => { + visitor.ts.push('{'); + push_json_str(visitor, tag); + visitor.ts.push(':'); + push_json_str(visitor, name); + if !is_unit { + visitor.ts.push(';'); + push_json_str(visitor, content); + visitor.ts.push(':'); + render_payload(visitor); + } + visitor.ts.push('}'); + } + SerdeRepr::Untagged => { + if is_unit { + visitor.ts.push_str("null"); + } else { + render_payload(visitor); + } + } + } + } +} + +pub struct LiteralTS(Cow<'static, str>); +impl Visit for LiteralTS { + fn visit(&self, visitor: &mut TSVisitor) -> ::Result { + visitor.ts.push_str(&*self.0); + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Unknown; +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum Never {} + +#[macro_export] +macro_rules! impl_ts { + ($($ty:ty),+ => $ts:expr) => { + $( + impl $crate::ts::TS for $ty { + fn visit_ts(visitor: &mut $crate::ts::TSVisitor) { + visitor.ts.push_str($ts); + } + } + )+ + }; +} + +impl_ts!(bool => "boolean"); +impl_ts!(String,str,InternedString => "string"); +// serde_json serializes all of these as bare JSON numbers (not strings), so they map to +// `number` — even u64/i64/i128, which JSON.parse yields as `number`, never `bigint`. +impl_ts!(usize,u8,u16,u32,u64,u128,isize,i8,i16,i32,i64,i128,f32,f64 => "number"); +impl_ts!(Unknown,imbl_value::Value,serde_json::Value,serde_cbor::Value => "unknown"); +impl_ts!(Never => "never"); + +fn visit_struct_impl( + info: &StructInfoData, + fields: impl FnOnce(bool, &mut TSVisitor), + visitor: &mut TSVisitor, +) { + if !info.named_fields && info.field_count == 1 { + fields(false, visitor) + } else if info.named_fields { + // Named object: collect any flattened fields, close the body, then intersect them in. + let saved = std::mem::take(&mut visitor.flattened); + visitor.ts.push_str("{"); + fields(true, visitor); + visitor.ts.push_str("}"); + for inner in std::mem::replace(&mut visitor.flattened, saved) { + visitor.ts.push('&'); + visitor.ts.push_str(&inner); + } + } else { + visitor.ts.push_str("["); + fields(true, visitor); + visitor.ts.push_str("]"); + } +} + +pub fn visit_struct(visitor: &mut TSVisitor) +where + T: VisitFieldsStaticNamed + VisitFieldsStatic, +{ + // `#[serde(transparent)]` serializes the single field's value directly, with no wrapper. + if struct_is_transparent(T::DATA.metadata) { + T::visit_fields_static(visitor).for_each(drop); + return; + } + visit_struct_impl( + &T::DATA, + |named, visitor| { + if named { + T::visit_fields_static_named(visitor).for_each(drop); + } else { + T::visit_fields_static(visitor).for_each(drop); + } + }, + visitor, + ); +} + +#[macro_export] +macro_rules! impl_ts_struct { + ($ty:ty $({ define: $name:expr })?) => { + impl $crate::ts::TS for $ty { + $(const DEFINE: Option<&str> = Some($name);)? + fn visit_ts(visitor: &mut $crate::ts::TSVisitor) { + $crate::ts::visit_struct::<$ty>(visitor) + } + } + }; +} + +#[macro_export] +macro_rules! impl_ts_enum { + ($ty:ty $({ define: $name:expr })?) => { + impl $crate::ts::TS for $ty { + $(const DEFINE: Option<&str> = Some($name);)? + fn visit_ts(visitor: &mut $crate::ts::TSVisitor) { + $crate::ts::visit_enum::<$ty>(visitor) + } + } + }; +} + +pub fn visit_enum(visitor: &mut TSVisitor) +where + T: VisitVariantsStatic, +{ + if T::DATA.variant_count == 0 { + visitor.append_type::() + } else { + T::visit_variants_static(visitor).for_each(drop) + } +} + +pub fn visit_map(visitor: &mut TSVisitor) +where + K: TS, + V: TS, +{ + if K::IS_ENUMERABLE { + // A string-literal-union key (e.g. an all-unit enum): a mapped type with optional members. + visitor.ts.push_str("{[key in "); + visitor.append_type::(); + visitor.ts.push_str("]?:"); + visitor.append_type::(); + visitor.ts.push('}'); + return; + } + // JSON object keys are always strings on the wire, regardless of K's Rust type. + visitor.ts.push_str("{[key:string]:"); + visitor.append_type::(); + visitor.ts.push('}'); +} + +#[macro_export] +macro_rules! impl_ts_map { + ($( + $ty:ty + $(where [$($bounds:tt)*])? + ),+ $(,)?) => { + $( + impl $crate::ts::TS for $ty + where + K: $crate::ts::TS, + V: $crate::ts::TS, + $($($bounds)*,)? + { + fn visit_ts(visitor: &mut $crate::ts::TSVisitor) { + $crate::ts::visit_map::(visitor) + } + } + )+ + }; +} + +impl_ts_map!( + std::collections::HashMap, + imbl_value::imbl::HashMap, + imbl_value::InOMap where [K: Eq + Clone, V: Clone], + BTreeMap, + OrdMap, +); + +pub fn visit_array(visitor: &mut TSVisitor) +where + T: TS, +{ + visitor.ts.push('('); + visitor.append_type::(); + visitor.ts.push_str(")[]"); +} + +#[macro_export] +macro_rules! impl_ts_array { + ($( + $ty:ty + $(where [$($bounds:tt)*])? + ),+ $(,)?) => { + $( + impl $crate::ts::TS for $ty + where + T: $crate::ts::TS, + $($($bounds)*,)? + { + fn visit_ts(visitor: &mut $crate::ts::TSVisitor) { + $crate::ts::visit_array::(visitor) + } + } + )+ + }; +} + +impl_ts_array!(Vec, Vector); + +impl TS for Box { + const DEFINE: Option<&str> = T::DEFINE; + const IS_ENUMERABLE: bool = T::IS_ENUMERABLE; + fn visit_ts(visitor: &mut TSVisitor) { + T::visit_ts(visitor); + } +} +impl TS for Arc { + const DEFINE: Option<&str> = T::DEFINE; + const IS_ENUMERABLE: bool = T::IS_ENUMERABLE; + fn visit_ts(visitor: &mut TSVisitor) { + T::visit_ts(visitor); + } +} +impl TS for Rc { + const DEFINE: Option<&str> = T::DEFINE; + const IS_ENUMERABLE: bool = T::IS_ENUMERABLE; + fn visit_ts(visitor: &mut TSVisitor) { + T::visit_ts(visitor); + } +} + +// serde serializes `Option` as the inner value or JSON `null`. As a struct field this is +// `field: T | null` by default; `#[serde(skip_serializing_if = "Option::is_none")]` makes the +// key optional instead, which `Named::visit` renders as `field?:` via the field metadata. +impl TS for Option { + fn visit_ts(visitor: &mut TSVisitor) { + visitor.append_type::(); + visitor.ts.push_str("|null"); + } +} + +// `Flat` serializes as the merge of A's and B's fields (used for inherited params), which +// is a TS object intersection. +impl TS for crate::util::Flat { + fn visit_ts(visitor: &mut TSVisitor) { + visitor.append_type::(); + visitor.ts.push('&'); + visitor.append_type::(); + } +} diff --git a/src/type-helpers.ts b/src/type-helpers.ts index c9e0e17..61cf5da 100644 --- a/src/type-helpers.ts +++ b/src/type-helpers.ts @@ -13,29 +13,31 @@ export type LeafHandler = { _RETURN: unknown; }; +// An empty method resolves to the node itself (a directly-callable handler, e.g. a parent +// with a root handler). A single segment recurses into that child with an empty method, which +// unifies leaf children and directly-callable parent children. export type RpcParamType< Root extends RpcHandler, Method extends string -> = Root["_PARAMS"] & - (Root extends ParentHandler - ? Method extends `${infer A}.${infer B}` - ? RpcParamType - : Root["_CHILDREN"] extends { - [m in Method]: LeafHandler; - } - ? Root["_CHILDREN"][Method]["_PARAMS"] - : never - : never); +> = Method extends "" + ? Root["_PARAMS"] + : Root extends ParentHandler + ? Method extends `${infer A}.${infer B}` + ? Root["_PARAMS"] & RpcParamType + : Method extends keyof Root["_CHILDREN"] + ? Root["_PARAMS"] & RpcParamType + : never + : never; export type RpcReturnType< Root extends RpcHandler, Method extends string -> = Root extends ParentHandler +> = Method extends "" + ? Root["_RETURN"] + : Root extends ParentHandler ? Method extends `${infer A}.${infer B}` ? RpcReturnType - : Root["_CHILDREN"] extends { - [m in Method]: LeafHandler; - } - ? Root["_CHILDREN"][Method]["_RETURN"] + : Method extends keyof Root["_CHILDREN"] + ? RpcReturnType : never : never; diff --git a/tests/test.rs b/tests/test.rs index 44b9bb4..80557de 100644 --- a/tests/test.rs +++ b/tests/test.rs @@ -1,6 +1,9 @@ +#![recursion_limit = "512"] + use clap::Parser; +use rpc_toolkit::ts::HandlerTSBindings; use rpc_toolkit::{ - from_fn, from_fn_async, Context, Empty, HandlerExt, HandlerTS, ParentHandler, Server, + from_fn, from_fn_async, impl_ts_struct, Context, Empty, HandlerExt, ParentHandler, Server, }; use serde::{Deserialize, Serialize}; use yajrc::RpcError; @@ -11,10 +14,12 @@ struct TestContext; impl Context for TestContext {} #[derive(Debug, Deserialize, Serialize, Parser)] -#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))] +#[cfg_attr(feature = "ts", derive(visit_rs::VisitFields))] struct Thing1Params { thing: String, } +#[cfg(feature = "ts")] +impl_ts_struct!(Thing1Params); #[derive(Debug, Deserialize, Serialize, Parser)] struct NoTSParams { @@ -30,11 +35,13 @@ fn no_ts_handler(_ctx: TestContext, params: NoTSParams) -> Result() -> String { + let mut v = TSVisitor::new(); + v.append_type::(); + v.ts +} + +#[derive(Serialize, Deserialize, VisitFields)] +#[serde(rename_all = "camelCase")] +struct Profile { + user_name: String, + age: u32, + big_id: u64, + nickname: Option, + #[serde(skip)] + secret: String, + #[serde(skip_serializing_if = "Option::is_none")] + maybe: Option, + tags: Vec, +} +impl_ts_struct!(Profile); + +#[test] +fn struct_rename_option_skip_primitives() { + // camelCase rename, u64 -> number (not bigint), Option -> T|null, skip dropped, + // skip_serializing_if -> optional key, Vec -> (T)[]. + assert_eq!( + render::(), + "{userName:string;age:number;bigId:number;nickname:string|null;maybe?:boolean|null;tags:(string)[];}" + ); +} + +#[derive(Serialize, Deserialize, VisitVariants)] +enum External { + Unit, + NewType(u32), + Tuple(u32, String), + Struct { x: bool }, +} +impl_ts_enum!(External); + +#[test] +fn externally_tagged_enum() { + // serde default: unit -> "Unit"; data variants -> {"Variant": payload}. + assert_eq!( + render::(), + r#"|"Unit"|{"NewType":number}|{"Tuple":[number,string,]}|{"Struct":{x:boolean;}}"# + ); +} + +#[derive(Serialize, Deserialize, VisitVariants)] +#[serde(tag = "type")] +enum Internal { + A { x: u32 }, + B { y: String }, +} +impl_ts_enum!(Internal); + +#[test] +fn internally_tagged_enum() { + assert_eq!( + render::(), + r#"|{"type":"A"}&{x:number;}|{"type":"B"}&{y:string;}"# + ); +} + +#[derive(Serialize, Deserialize, VisitVariants)] +#[serde(tag = "t", content = "c")] +enum Adjacent { + Unit, + Data(u32), + Struct { z: bool }, +} +impl_ts_enum!(Adjacent); + +#[test] +fn adjacently_tagged_enum() { + // unit -> {"t":"Unit"} (no content); data -> {"t":..;"c":payload}. + assert_eq!( + render::(), + r#"|{"t":"Unit"}|{"t":"Data";"c":number}|{"t":"Struct";"c":{z:boolean;}}"# + ); +} + +#[derive(Serialize, Deserialize, VisitVariants)] +#[serde(untagged)] +enum Untagged { + A(u32), + B(String), +} +impl_ts_enum!(Untagged); + +#[test] +fn untagged_enum() { + assert_eq!(render::(), "|number|string"); +} + +// A generic enum (visit-rs now supports these for 'static type params) instantiated concretely. +#[derive(Serialize, Deserialize, VisitVariants)] +enum Resp { + Ok(T), + Err(String), +} +impl_ts_enum!(Resp); + +#[test] +fn generic_enum() { + assert_eq!(render::>(), r#"|{"Ok":number}|{"Err":string}"#); +} + +#[derive(Serialize, Deserialize, VisitFields)] +struct Maps { + by_str: BTreeMap, + by_num: BTreeMap, +} +impl_ts_struct!(Maps); + +#[test] +fn map_keys_are_strings() { + // JSON object keys are always strings, including for BTreeMap. + assert_eq!( + render::(), + "{by_str:{[key:string]:number};by_num:{[key:string]:string};}" + ); +} + +#[test] +fn option_of_collection() { + assert_eq!(render::>>(), "(number)[]|null"); +} + +#[derive(Serialize, Deserialize, VisitFields)] +struct Inner { + a: u32, +} +impl_ts_struct!(Inner { define: "Inner" }); + +#[derive(Serialize, Deserialize, VisitFields)] +struct Outer { + inner: Inner, + list: Vec, +} +impl_ts_struct!(Outer); + +#[test] +fn named_definitions() { + let mut v = TSVisitor::new(); + v.append_type::(); + assert_eq!(v.ts, "{inner:Inner;list:(Inner)[];}"); + assert_eq!( + v.definitions.get("Inner").map(String::as_str), + Some("{a:number;}") + ); + + let module = v.into_module("Root"); + assert!(module.contains("export type Inner = {a:number;};")); + assert!(module.contains("export type Root = {inner:Inner;list:(Inner)[];};")); + // helper types are included + assert!(module.contains("export type RpcParamType")); +} + +#[derive(Serialize, Deserialize, VisitFields)] +struct Address { + street: String, + zip: u32, +} +impl_ts_struct!(Address); + +#[derive(Serialize, Deserialize, VisitFields)] +struct Person { + name: String, + #[serde(flatten)] + address: Address, +} +impl_ts_struct!(Person); + +#[test] +fn flatten_intersects_inner_fields() { + // serde splices Address's fields into Person; modeled as a TS intersection. + assert_eq!( + render::(), + "{name:string;}&{street:string;zip:number;}" + ); +} + +#[derive(Serialize, Deserialize, VisitFields)] +#[serde(transparent)] +struct Wrapper { + value: Vec, +} +impl_ts_struct!(Wrapper); + +#[test] +fn transparent_unwraps_single_field() { + assert_eq!(render::(), "(number)[]"); +} + +#[derive(Clone)] +struct Ctx; +impl Context for Ctx {} + +async fn echo(_: Ctx, p: Profile) -> Result { + Ok(p.user_name) +} +fn ping(_: Ctx, _: Empty) -> Result { + Ok(true) +} + +#[test] +fn handler_tree_bindings() { + let root = ParentHandler::::new() + .subcommand("echo", from_fn_async(echo).no_cli()) + .subcommand("ping", from_fn(ping).no_ts()); + + let module = handler_bindings(&root, "Api").expect("root has ts"); + // echo is fully typed; ping opted out but must still be a well-formed leaf node. + assert!(module.contains("\"echo\":{_PARAMS:")); + assert!(module.contains("\"ping\":{_PARAMS:unknown;_RETURN:unknown}")); + assert!(module.contains("export type Api =")); +} + +#[test] +fn flat_is_object_intersection() { + assert_eq!( + render::>(), + "{street:string;zip:number;}&{by_str:{[key:string]:number};by_num:{[key:string]:string};}" + ); +} + +#[derive(Serialize, Deserialize, VisitFields)] +struct RootParams { + token: String, +} +impl_ts_struct!(RootParams); + +async fn root_call(_: Ctx, p: RootParams) -> Result { + Ok(p.token.len() as u32) +} + +#[test] +fn callable_parent_surfaces_root_handler() { + // A parent with a root handler is itself callable: its node carries the root handler's + // _RETURN (and its _PARAMS already match, per root_handler's bound). + let root = ParentHandler::::new() + .root_handler(from_fn_async(root_call).no_cli()) + .subcommand("ping", from_fn(ping).no_ts()); + + let module = handler_bindings(&root, "Api").expect("root has ts"); + assert!(module.contains("export type Api = {_PARAMS:{token:string;};_RETURN:number;_CHILDREN:")); +}