From 72e84f4e380160f7fc74ffafa8d0c489bf5fe229 Mon Sep 17 00:00:00 2001 From: "Ifiok Jr." Date: Sat, 22 Mar 2025 01:18:32 +0000 Subject: [PATCH] feat: support aliased `Result` return types for `server_fn` - Enhanced error messages for better clarity when server functions do not meet the expected return type requirements. - Added `trybuild` tests to ensure behaviour is not broken in future releases. --- Cargo.lock | 1 + Cargo.toml | 1 + leptos_macro/Cargo.toml | 4 +- server_fn/Cargo.toml | 3 + server_fn/src/error.rs | 22 +++++++ .../tests/invalid/aliased_return_full.rs | 16 ++++++ .../tests/invalid/aliased_return_full.stderr | 51 +++++++++++++++++ .../tests/invalid/aliased_return_none.rs | 14 +++++ .../tests/invalid/aliased_return_none.stderr | 50 ++++++++++++++++ .../tests/invalid/aliased_return_part.rs | 16 ++++++ .../tests/invalid/aliased_return_part.stderr | 51 +++++++++++++++++ server_fn/tests/invalid/empty_return.rs | 8 +++ server_fn/tests/invalid/empty_return.stderr | 57 +++++++++++++++++++ server_fn/tests/invalid/no_return.rs | 6 ++ server_fn/tests/invalid/no_return.stderr | 5 ++ server_fn/tests/invalid/not_async.rs | 9 +++ server_fn/tests/invalid/not_async.stderr | 13 +++++ server_fn/tests/invalid/not_result.rs | 23 ++++++++ server_fn/tests/invalid/not_result.stderr | 57 +++++++++++++++++++ server_fn/tests/server_macro.rs | 8 +++ server_fn/tests/valid/aliased_return_full.rs | 11 ++++ server_fn/tests/valid/aliased_return_none.rs | 10 ++++ server_fn/tests/valid/aliased_return_part.rs | 11 ++++ .../valid/custom_error_aliased_return_full.rs | 25 ++++++++ .../valid/custom_error_aliased_return_none.rs | 23 ++++++++ .../valid/custom_error_aliased_return_part.rs | 25 ++++++++ server_fn_macro/src/lib.rs | 49 ++++++++-------- 27 files changed, 544 insertions(+), 25 deletions(-) create mode 100644 server_fn/tests/invalid/aliased_return_full.rs create mode 100644 server_fn/tests/invalid/aliased_return_full.stderr create mode 100644 server_fn/tests/invalid/aliased_return_none.rs create mode 100644 server_fn/tests/invalid/aliased_return_none.stderr create mode 100644 server_fn/tests/invalid/aliased_return_part.rs create mode 100644 server_fn/tests/invalid/aliased_return_part.stderr create mode 100644 server_fn/tests/invalid/empty_return.rs create mode 100644 server_fn/tests/invalid/empty_return.stderr create mode 100644 server_fn/tests/invalid/no_return.rs create mode 100644 server_fn/tests/invalid/no_return.stderr create mode 100644 server_fn/tests/invalid/not_async.rs create mode 100644 server_fn/tests/invalid/not_async.stderr create mode 100644 server_fn/tests/invalid/not_result.rs create mode 100644 server_fn/tests/invalid/not_result.stderr create mode 100644 server_fn/tests/server_macro.rs create mode 100644 server_fn/tests/valid/aliased_return_full.rs create mode 100644 server_fn/tests/valid/aliased_return_none.rs create mode 100644 server_fn/tests/valid/aliased_return_part.rs create mode 100644 server_fn/tests/valid/custom_error_aliased_return_full.rs create mode 100644 server_fn/tests/valid/custom_error_aliased_return_none.rs create mode 100644 server_fn/tests/valid/custom_error_aliased_return_part.rs diff --git a/Cargo.lock b/Cargo.lock index c66b2f615..e650783a1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3279,6 +3279,7 @@ dependencies = [ "tokio-tungstenite", "tower", "tower-layer", + "trybuild", "url", "wasm-bindgen", "wasm-bindgen-futures", diff --git a/Cargo.toml b/Cargo.toml index 24873b482..084de78e2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -73,6 +73,7 @@ server_fn_macro = { path = "./server_fn_macro", version = "0.8.0-beta" } server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.8.0-beta" } tachys = { path = "./tachys", version = "0.2.0-beta" } wasm-bindgen = { version = "0.2.100" } +trybuild = "1" [profile.release] codegen-units = 1 diff --git a/leptos_macro/Cargo.toml b/leptos_macro/Cargo.toml index 9348ea919..1bd5d8708 100644 --- a/leptos_macro/Cargo.toml +++ b/leptos_macro/Cargo.toml @@ -32,7 +32,7 @@ tracing = { version = "0.1.41", optional = true } [dev-dependencies] log = "0.4.22" typed-builder = "0.20.0" -trybuild = "1.0" +trybuild = { workspace = true } leptos = { path = "../leptos" } leptos_router = { path = "../router", features = ["ssr"] } server_fn = { path = "../server_fn", features = ["cbor"] } @@ -58,7 +58,7 @@ generic = ["server_fn_macro/generic"] # https://github.com/rust-lang/cargo/issues/4423 # TLDR proc macros will ignore RUSTFLAGS when --target is specified on the cargo command. # This works around the issue by the non proc-macro crate which does see RUSTFLAGS enabling the replacement feature on the proc-macro crate, which wouldn't. -# This is automatic as long as the leptos crate is depended upon, +# This is automatic as long as the leptos crate is depended upon, # downstream usage should never manually enable this feature. __internal_erase_components = [] diff --git a/server_fn/Cargo.toml b/server_fn/Cargo.toml index 5bdefd8d7..f4f92a7ae 100644 --- a/server_fn/Cargo.toml +++ b/server_fn/Cargo.toml @@ -81,6 +81,9 @@ url = "2" pin-project-lite = "0.2.15" tokio = { version = "1.43.0", features = ["rt"], optional = true } +[dev-dependencies] +trybuild = { workspace = true } + [features] axum-no-default = [ "ssr", diff --git a/server_fn/src/error.rs b/server_fn/src/error.rs index 432833b5f..f58701486 100644 --- a/server_fn/src/error.rs +++ b/server_fn/src/error.rs @@ -559,6 +559,28 @@ where } } +#[doc(hidden)] +#[diagnostic::on_unimplemented( + message = "{Self} is not a `Result` or aliased `Result`. Server functions \ + must return a `Result` or aliased `Result`.", + label = "Must return a `Result` or aliased `Result`.", + note = "If you are trying to return an alias of `Result`, you must also \ + implement `FromServerFnError` for the error type." +)] +/// A trait for extracting the error and ok types from a [`Result`]. This is used to allow alias types to be returned from server functions. +pub trait ServerFnMustReturnResult { + /// The error type of the [`Result`]. + type Err; + /// The ok type of the [`Result`]. + type Ok; +} + +#[doc(hidden)] +impl ServerFnMustReturnResult for Result { + type Err = E; + type Ok = T; +} + #[test] fn assert_from_server_fn_error_impl() { fn assert_impl() {} diff --git a/server_fn/tests/invalid/aliased_return_full.rs b/server_fn/tests/invalid/aliased_return_full.rs new file mode 100644 index 000000000..fd95446ae --- /dev/null +++ b/server_fn/tests/invalid/aliased_return_full.rs @@ -0,0 +1,16 @@ +use server_fn_macro_default::server; + +#[derive(Debug, thiserror::Error, Clone, serde::Serialize, serde::Deserialize)] +pub enum InvalidError { + #[error("error a")] + A, +} + +type FullAlias = Result; + +#[server] +pub async fn full_alias_result() -> FullAlias { + Ok("hello".to_string()) +} + +fn main() {} \ No newline at end of file diff --git a/server_fn/tests/invalid/aliased_return_full.stderr b/server_fn/tests/invalid/aliased_return_full.stderr new file mode 100644 index 000000000..8a55413b2 --- /dev/null +++ b/server_fn/tests/invalid/aliased_return_full.stderr @@ -0,0 +1,51 @@ +error[E0277]: the trait bound `InvalidError: FromServerFnError` is not satisfied + --> tests/invalid/aliased_return_full.rs:11:1 + | +11 | #[server] + | ^^^^^^^^^ the trait `FromServerFnError` is not implemented for `InvalidError` + | + = help: the trait `FromServerFnError` is implemented for `ServerFnError` + = note: required for `BrowserClient` to implement `Client` +note: required by a bound in `server_fn::ServerFn::Client` + --> src/lib.rs + | + | type Client: Client; + | ^^^^^^^^^^^^^^^^^^^ required by this bound in `ServerFn::Client` + = note: this error originates in the attribute macro `server` (in Nightly builds, run with -Z macro-backtrace for more info) + +error[E0277]: the trait bound `InvalidError: FromServerFnError` is not satisfied + --> tests/invalid/aliased_return_full.rs:11:1 + | +11 | #[server] + | ^^^^^^^^^ the trait `FromServerFnError` is not implemented for `InvalidError` + | + = help: the trait `FromServerFnError` is implemented for `ServerFnError` + = note: required for `BrowserClient` to implement `Client` + = note: required for `Http>` to implement `Protocol` +note: required by a bound in `server_fn::ServerFn::Protocol` + --> src/lib.rs + | + | type Protocol: Protocol< + | ____________________^ + | | Self, + | | Self::Output, + | | Self::Client, + | | Self::Server, + | | Self::Error, + | | >; + | |_____^ required by this bound in `ServerFn::Protocol` + = note: this error originates in the attribute macro `server` (in Nightly builds, run with -Z macro-backtrace for more info) + +error[E0277]: the trait bound `InvalidError: FromServerFnError` is not satisfied + --> tests/invalid/aliased_return_full.rs:11:1 + | +11 | #[server] + | ^^^^^^^^^ the trait `FromServerFnError` is not implemented for `InvalidError` + | + = help: the trait `FromServerFnError` is implemented for `ServerFnError` +note: required by a bound in `server_fn::ServerFn::Error` + --> src/lib.rs + | + | type Error: FromServerFnError + Send + Sync; + | ^^^^^^^^^^^^^^^^^ required by this bound in `ServerFn::Error` + = note: this error originates in the attribute macro `server` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/server_fn/tests/invalid/aliased_return_none.rs b/server_fn/tests/invalid/aliased_return_none.rs new file mode 100644 index 000000000..d00b9fbf8 --- /dev/null +++ b/server_fn/tests/invalid/aliased_return_none.rs @@ -0,0 +1,14 @@ +use server_fn_macro_default::server; + +#[derive(Debug, thiserror::Error, Clone, serde::Serialize, serde::Deserialize)] +pub enum InvalidError { + #[error("error a")] + A, +} + +#[server] +pub async fn no_alias_result() -> Result { + Ok("hello".to_string()) +} + +fn main() {} \ No newline at end of file diff --git a/server_fn/tests/invalid/aliased_return_none.stderr b/server_fn/tests/invalid/aliased_return_none.stderr new file mode 100644 index 000000000..edfacac7e --- /dev/null +++ b/server_fn/tests/invalid/aliased_return_none.stderr @@ -0,0 +1,50 @@ +error[E0277]: the trait bound `InvalidError: FromServerFnError` is not satisfied + --> tests/invalid/aliased_return_none.rs:9:1 + | +9 | #[server] + | ^^^^^^^^^ the trait `FromServerFnError` is not implemented for `InvalidError` + | + = help: the trait `FromServerFnError` is implemented for `ServerFnError` + = note: required for `BrowserClient` to implement `Client` +note: required by a bound in `server_fn::ServerFn::Client` + --> src/lib.rs + | + | type Client: Client; + | ^^^^^^^^^^^^^^^^^^^ required by this bound in `ServerFn::Client` + = note: this error originates in the attribute macro `server` (in Nightly builds, run with -Z macro-backtrace for more info) + +error[E0277]: the trait bound `InvalidError: FromServerFnError` is not satisfied + --> tests/invalid/aliased_return_none.rs:9:1 + | +9 | #[server] + | ^^^^^^^^^ the trait `FromServerFnError` is not implemented for `InvalidError` + | + = help: the trait `FromServerFnError` is implemented for `ServerFnError` + = note: required for `BrowserClient` to implement `Client` + = note: required for `Http>` to implement `Protocol` +note: required by a bound in `server_fn::ServerFn::Protocol` + --> src/lib.rs + | + | type Protocol: Protocol< + | ____________________^ + | | Self, + | | Self::Output, + | | Self::Client, + | | Self::Server, + | | Self::Error, + | | >; + | |_____^ required by this bound in `ServerFn::Protocol` + = note: this error originates in the attribute macro `server` (in Nightly builds, run with -Z macro-backtrace for more info) + +error[E0277]: the trait bound `InvalidError: FromServerFnError` is not satisfied + --> tests/invalid/aliased_return_none.rs:10:50 + | +10 | pub async fn no_alias_result() -> Result { + | ^^^^^^^^^^^^ the trait `FromServerFnError` is not implemented for `InvalidError` + | + = help: the trait `FromServerFnError` is implemented for `ServerFnError` +note: required by a bound in `server_fn::ServerFn::Error` + --> src/lib.rs + | + | type Error: FromServerFnError + Send + Sync; + | ^^^^^^^^^^^^^^^^^ required by this bound in `ServerFn::Error` diff --git a/server_fn/tests/invalid/aliased_return_part.rs b/server_fn/tests/invalid/aliased_return_part.rs new file mode 100644 index 000000000..2585d4702 --- /dev/null +++ b/server_fn/tests/invalid/aliased_return_part.rs @@ -0,0 +1,16 @@ +use server_fn_macro_default::server; + +#[derive(Debug, thiserror::Error, Clone, serde::Serialize, serde::Deserialize)] +pub enum InvalidError { + #[error("error a")] + A, +} + +type PartAlias = Result; + +#[server] +pub async fn part_alias_result() -> PartAlias { + Ok("hello".to_string()) +} + +fn main() {} \ No newline at end of file diff --git a/server_fn/tests/invalid/aliased_return_part.stderr b/server_fn/tests/invalid/aliased_return_part.stderr new file mode 100644 index 000000000..634e58f73 --- /dev/null +++ b/server_fn/tests/invalid/aliased_return_part.stderr @@ -0,0 +1,51 @@ +error[E0277]: the trait bound `InvalidError: FromServerFnError` is not satisfied + --> tests/invalid/aliased_return_part.rs:11:1 + | +11 | #[server] + | ^^^^^^^^^ the trait `FromServerFnError` is not implemented for `InvalidError` + | + = help: the trait `FromServerFnError` is implemented for `ServerFnError` + = note: required for `BrowserClient` to implement `Client` +note: required by a bound in `server_fn::ServerFn::Client` + --> src/lib.rs + | + | type Client: Client; + | ^^^^^^^^^^^^^^^^^^^ required by this bound in `ServerFn::Client` + = note: this error originates in the attribute macro `server` (in Nightly builds, run with -Z macro-backtrace for more info) + +error[E0277]: the trait bound `InvalidError: FromServerFnError` is not satisfied + --> tests/invalid/aliased_return_part.rs:11:1 + | +11 | #[server] + | ^^^^^^^^^ the trait `FromServerFnError` is not implemented for `InvalidError` + | + = help: the trait `FromServerFnError` is implemented for `ServerFnError` + = note: required for `BrowserClient` to implement `Client` + = note: required for `Http>` to implement `Protocol` +note: required by a bound in `server_fn::ServerFn::Protocol` + --> src/lib.rs + | + | type Protocol: Protocol< + | ____________________^ + | | Self, + | | Self::Output, + | | Self::Client, + | | Self::Server, + | | Self::Error, + | | >; + | |_____^ required by this bound in `ServerFn::Protocol` + = note: this error originates in the attribute macro `server` (in Nightly builds, run with -Z macro-backtrace for more info) + +error[E0277]: the trait bound `InvalidError: FromServerFnError` is not satisfied + --> tests/invalid/aliased_return_part.rs:11:1 + | +11 | #[server] + | ^^^^^^^^^ the trait `FromServerFnError` is not implemented for `InvalidError` + | + = help: the trait `FromServerFnError` is implemented for `ServerFnError` +note: required by a bound in `server_fn::ServerFn::Error` + --> src/lib.rs + | + | type Error: FromServerFnError + Send + Sync; + | ^^^^^^^^^^^^^^^^^ required by this bound in `ServerFn::Error` + = note: this error originates in the attribute macro `server` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/server_fn/tests/invalid/empty_return.rs b/server_fn/tests/invalid/empty_return.rs new file mode 100644 index 000000000..ca4ef25ab --- /dev/null +++ b/server_fn/tests/invalid/empty_return.rs @@ -0,0 +1,8 @@ +use server_fn_macro_default::server; + +#[server] +pub async fn empty_return() -> () { + () +} + +fn main() {} \ No newline at end of file diff --git a/server_fn/tests/invalid/empty_return.stderr b/server_fn/tests/invalid/empty_return.stderr new file mode 100644 index 000000000..ad0236b2b --- /dev/null +++ b/server_fn/tests/invalid/empty_return.stderr @@ -0,0 +1,57 @@ +error[E0277]: () is not a `Result` or aliased `Result`. Server functions must return a `Result` or aliased `Result`. + --> tests/invalid/empty_return.rs:3:1 + | +3 | #[server] + | ^^^^^^^^^ Must return a `Result` or aliased `Result`. + | + = help: the trait `ServerFnMustReturnResult` is not implemented for `()` + = note: If you are trying to return an alias of `Result`, you must also implement `FromServerFnError` for the error type. + = help: the trait `ServerFnMustReturnResult` is implemented for `Result` + +error[E0271]: expected `impl Future` to be a future that resolves to `Result<_, _>`, but it resolves to `()` + --> tests/invalid/empty_return.rs:3:1 + | +3 | #[server] + | ^^^^^^^^^ expected `Result<_, _>`, found `()` + | + = note: expected enum `Result<_, _>` + found unit type `()` +note: required by a bound in `ServerFn::{synthetic#0}` + --> src/lib.rs + | + | ) -> impl Future> + Send; + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `ServerFn::{synthetic#0}` + +error[E0277]: () is not a `Result` or aliased `Result`. Server functions must return a `Result` or aliased `Result`. + --> tests/invalid/empty_return.rs:3:1 + | +3 | #[server] + | ^^^^^^^^^ Must return a `Result` or aliased `Result`. + | + = help: the trait `ServerFnMustReturnResult` is not implemented for `()` + = note: If you are trying to return an alias of `Result`, you must also implement `FromServerFnError` for the error type. + = help: the trait `ServerFnMustReturnResult` is implemented for `Result` + = note: this error originates in the attribute macro `server` (in Nightly builds, run with -Z macro-backtrace for more info) + +error[E0277]: () is not a `Result` or aliased `Result`. Server functions must return a `Result` or aliased `Result`. + --> tests/invalid/empty_return.rs:3:1 + | +3 | #[server] + | ^^^^^^^^^ Must return a `Result` or aliased `Result`. + | + = help: the trait `ServerFnMustReturnResult` is not implemented for `()` + = note: If you are trying to return an alias of `Result`, you must also implement `FromServerFnError` for the error type. + = help: the trait `ServerFnMustReturnResult` is implemented for `Result` + +error[E0308]: mismatched types + --> tests/invalid/empty_return.rs:3:1 + | +3 | #[server] + | ^^^^^^^^^ expected `()`, found `Result<_, _>` + | + = note: expected unit type `()` + found enum `Result<_, _>` +help: consider using `Result::expect` to unwrap the `Result<_, _>` value, panicking if the value is a `Result::Err` + | +3 | #[server].expect("REASON") + | +++++++++++++++++ diff --git a/server_fn/tests/invalid/no_return.rs b/server_fn/tests/invalid/no_return.rs new file mode 100644 index 000000000..b942c0158 --- /dev/null +++ b/server_fn/tests/invalid/no_return.rs @@ -0,0 +1,6 @@ +use server_fn_macro_default::server; + +#[server] +pub async fn no_return() {} + +fn main() {} \ No newline at end of file diff --git a/server_fn/tests/invalid/no_return.stderr b/server_fn/tests/invalid/no_return.stderr new file mode 100644 index 000000000..e70bf25c8 --- /dev/null +++ b/server_fn/tests/invalid/no_return.stderr @@ -0,0 +1,5 @@ +error: expected `->` + --> tests/invalid/no_return.rs:4:26 + | +4 | pub async fn no_return() {} + | ^ diff --git a/server_fn/tests/invalid/not_async.rs b/server_fn/tests/invalid/not_async.rs new file mode 100644 index 000000000..a4cefa00a --- /dev/null +++ b/server_fn/tests/invalid/not_async.rs @@ -0,0 +1,9 @@ +use server_fn_macro_default::server; +use server_fn::error::ServerFnError; + +#[server] +pub fn not_async() -> Result { + Ok("hello".to_string()) +} + +fn main() {} \ No newline at end of file diff --git a/server_fn/tests/invalid/not_async.stderr b/server_fn/tests/invalid/not_async.stderr new file mode 100644 index 000000000..ed04ab184 --- /dev/null +++ b/server_fn/tests/invalid/not_async.stderr @@ -0,0 +1,13 @@ +error: expected `async` + --> tests/invalid/not_async.rs:5:5 + | +5 | pub fn not_async() -> Result { + | ^^ + +warning: unused import: `server_fn::error::ServerFnError` + --> tests/invalid/not_async.rs:2:5 + | +2 | use server_fn::error::ServerFnError; + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | + = note: `#[warn(unused_imports)]` on by default diff --git a/server_fn/tests/invalid/not_result.rs b/server_fn/tests/invalid/not_result.rs new file mode 100644 index 000000000..735fffc04 --- /dev/null +++ b/server_fn/tests/invalid/not_result.rs @@ -0,0 +1,23 @@ +use server_fn_macro_default::server; +use server_fn::error::{FromServerFnError, ServerFnErrorErr}; + +#[derive(Debug, thiserror::Error, Clone, serde::Serialize, serde::Deserialize)] +pub enum CustomError { + #[error("error a")] + A, + #[error("error b")] + B, +} + +impl FromServerFnError for CustomError { + fn from_server_fn_error(_: ServerFnErrorErr) -> Self { + Self::A + } +} + +#[server] +pub async fn full_alias_result() -> CustomError { + CustomError::A +} + +fn main() {} \ No newline at end of file diff --git a/server_fn/tests/invalid/not_result.stderr b/server_fn/tests/invalid/not_result.stderr new file mode 100644 index 000000000..64301fdce --- /dev/null +++ b/server_fn/tests/invalid/not_result.stderr @@ -0,0 +1,57 @@ +error[E0277]: CustomError is not a `Result` or aliased `Result`. Server functions must return a `Result` or aliased `Result`. + --> tests/invalid/not_result.rs:18:1 + | +18 | #[server] + | ^^^^^^^^^ Must return a `Result` or aliased `Result`. + | + = help: the trait `ServerFnMustReturnResult` is not implemented for `CustomError` + = note: If you are trying to return an alias of `Result`, you must also implement `FromServerFnError` for the error type. + = help: the trait `ServerFnMustReturnResult` is implemented for `Result` + +error[E0271]: expected `impl Future` to be a future that resolves to `Result<_, _>`, but it resolves to `CustomError` + --> tests/invalid/not_result.rs:18:1 + | +18 | #[server] + | ^^^^^^^^^ expected `Result<_, _>`, found `CustomError` + | + = note: expected enum `Result<_, _>` + found enum `CustomError` +note: required by a bound in `ServerFn::{synthetic#0}` + --> src/lib.rs + | + | ) -> impl Future> + Send; + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `ServerFn::{synthetic#0}` + +error[E0277]: CustomError is not a `Result` or aliased `Result`. Server functions must return a `Result` or aliased `Result`. + --> tests/invalid/not_result.rs:18:1 + | +18 | #[server] + | ^^^^^^^^^ Must return a `Result` or aliased `Result`. + | + = help: the trait `ServerFnMustReturnResult` is not implemented for `CustomError` + = note: If you are trying to return an alias of `Result`, you must also implement `FromServerFnError` for the error type. + = help: the trait `ServerFnMustReturnResult` is implemented for `Result` + = note: this error originates in the attribute macro `server` (in Nightly builds, run with -Z macro-backtrace for more info) + +error[E0277]: CustomError is not a `Result` or aliased `Result`. Server functions must return a `Result` or aliased `Result`. + --> tests/invalid/not_result.rs:18:1 + | +18 | #[server] + | ^^^^^^^^^ Must return a `Result` or aliased `Result`. + | + = help: the trait `ServerFnMustReturnResult` is not implemented for `CustomError` + = note: If you are trying to return an alias of `Result`, you must also implement `FromServerFnError` for the error type. + = help: the trait `ServerFnMustReturnResult` is implemented for `Result` + +error[E0308]: mismatched types + --> tests/invalid/not_result.rs:18:1 + | +18 | #[server] + | ^^^^^^^^^ expected `CustomError`, found `Result<_, _>` + | + = note: expected enum `CustomError` + found enum `Result<_, _>` +help: consider using `Result::expect` to unwrap the `Result<_, _>` value, panicking if the value is a `Result::Err` + | +18 | #[server].expect("REASON") + | +++++++++++++++++ diff --git a/server_fn/tests/server_macro.rs b/server_fn/tests/server_macro.rs new file mode 100644 index 000000000..c2f87a5d1 --- /dev/null +++ b/server_fn/tests/server_macro.rs @@ -0,0 +1,8 @@ +#![cfg(feature = "browser")] + +#[test] +fn aliased_results() { + let t = trybuild::TestCases::new(); + t.pass("tests/valid/*.rs"); + t.compile_fail("tests/invalid/*.rs") +} diff --git a/server_fn/tests/valid/aliased_return_full.rs b/server_fn/tests/valid/aliased_return_full.rs new file mode 100644 index 000000000..2cdd6c3da --- /dev/null +++ b/server_fn/tests/valid/aliased_return_full.rs @@ -0,0 +1,11 @@ +use server_fn_macro_default::server; +use server_fn::error::ServerFnError; + +type FullAlias = Result; + +#[server] +pub async fn full_alias_result() -> FullAlias { + Ok("hello".to_string()) +} + +fn main() {} \ No newline at end of file diff --git a/server_fn/tests/valid/aliased_return_none.rs b/server_fn/tests/valid/aliased_return_none.rs new file mode 100644 index 000000000..db1eefe97 --- /dev/null +++ b/server_fn/tests/valid/aliased_return_none.rs @@ -0,0 +1,10 @@ +use server_fn_macro_default::server; +use server_fn::error::ServerFnError; + +#[server] +pub async fn no_alias_result() -> Result { + Ok("hello".to_string()) +} + + +fn main() {} \ No newline at end of file diff --git a/server_fn/tests/valid/aliased_return_part.rs b/server_fn/tests/valid/aliased_return_part.rs new file mode 100644 index 000000000..90f842638 --- /dev/null +++ b/server_fn/tests/valid/aliased_return_part.rs @@ -0,0 +1,11 @@ +use server_fn_macro_default::server; +use server_fn::error::ServerFnError; + +type PartAlias = Result; + +#[server] +pub async fn part_alias_result() -> PartAlias { + Ok("hello".to_string()) +} + +fn main() {} \ No newline at end of file diff --git a/server_fn/tests/valid/custom_error_aliased_return_full.rs b/server_fn/tests/valid/custom_error_aliased_return_full.rs new file mode 100644 index 000000000..9ea56685a --- /dev/null +++ b/server_fn/tests/valid/custom_error_aliased_return_full.rs @@ -0,0 +1,25 @@ +use server_fn_macro_default::server; +use server_fn::error::{FromServerFnError, ServerFnErrorErr}; + +#[derive(Debug, thiserror::Error, Clone, serde::Serialize, serde::Deserialize)] +pub enum CustomError { + #[error("error a")] + ErrorA, + #[error("error b")] + ErrorB, +} + +impl FromServerFnError for CustomError { + fn from_server_fn_error(_: ServerFnErrorErr) -> Self { + Self::ErrorA + } +} + +type FullAlias = Result; + +#[server] +pub async fn full_alias_result() -> FullAlias { + Ok("hello".to_string()) +} + +fn main() {} \ No newline at end of file diff --git a/server_fn/tests/valid/custom_error_aliased_return_none.rs b/server_fn/tests/valid/custom_error_aliased_return_none.rs new file mode 100644 index 000000000..351e39496 --- /dev/null +++ b/server_fn/tests/valid/custom_error_aliased_return_none.rs @@ -0,0 +1,23 @@ +use server_fn_macro_default::server; +use server_fn::error::{FromServerFnError, ServerFnErrorErr}; + +#[derive(Debug, thiserror::Error, Clone, serde::Serialize, serde::Deserialize)] +pub enum CustomError { + #[error("error a")] + ErrorA, + #[error("error b")] + ErrorB, +} + +impl FromServerFnError for CustomError { + fn from_server_fn_error(_: ServerFnErrorErr) -> Self { + Self::ErrorA + } +} + +#[server] +pub async fn no_alias_result() -> Result { + Ok("hello".to_string()) +} + +fn main() {} \ No newline at end of file diff --git a/server_fn/tests/valid/custom_error_aliased_return_part.rs b/server_fn/tests/valid/custom_error_aliased_return_part.rs new file mode 100644 index 000000000..f19ee0542 --- /dev/null +++ b/server_fn/tests/valid/custom_error_aliased_return_part.rs @@ -0,0 +1,25 @@ +use server_fn_macro_default::server; +use server_fn::error::{FromServerFnError, ServerFnErrorErr}; + +#[derive(Debug, thiserror::Error, Clone, serde::Serialize, serde::Deserialize)] +pub enum CustomError { + #[error("error a")] + ErrorA, + #[error("error b")] + ErrorB, +} + +impl FromServerFnError for CustomError { + fn from_server_fn_error(_: ServerFnErrorErr) -> Self { + Self::ErrorA + } +} + +type PartAlias = Result; + +#[server] +pub async fn part_alias_result() -> PartAlias { + Ok("hello".to_string()) +} + +fn main() {} \ No newline at end of file diff --git a/server_fn_macro/src/lib.rs b/server_fn_macro/src/lib.rs index 3e0878115..4be5beacd 100644 --- a/server_fn_macro/src/lib.rs +++ b/server_fn_macro/src/lib.rs @@ -551,16 +551,25 @@ impl ServerFnCall { let protocol = self.protocol(); let middlewares = &self.body.middlewares; let return_ty = &self.body.return_ty; - let output_ty = &self.body.output_ty; + let output_ty = self.body.output_ty + .as_ref() + .map_or_else(|| { + quote! { + <#return_ty as #server_fn_path::error::ServerFnMustReturnResult>::Ok + } + }, + ToTokens::to_token_stream, + ); let error_ty = &self.body.error_ty; let error_ty = error_ty .as_ref() - .map(ToTokens::to_token_stream) - .unwrap_or_else(|| { + .map_or_else(|| { quote! { - #server_fn_path::error::NoCustomError + <#return_ty as #server_fn_path::error::ServerFnMustReturnResult>::Err } - }); + }, + ToTokens::to_token_stream, + ); let field_names = self.field_names(); // run_body in the trait implementation @@ -877,7 +886,7 @@ impl Parse for Middleware { } } -fn output_type(return_ty: &Type) -> Result<&GenericArgument> { +fn output_type(return_ty: &Type) -> Option<&Type> { if let syn::Type::Path(pat) = &return_ty { if pat.path.segments[0].ident == "Result" { if pat.path.segments.is_empty() { @@ -885,19 +894,17 @@ fn output_type(return_ty: &Type) -> Result<&GenericArgument> { } else if let PathArguments::AngleBracketed(args) = &pat.path.segments[0].arguments { - return Ok(&args.args[0]); + if let GenericArgument::Type(ty) = &args.args[0] { + return Some(ty); + } } } }; - Err(syn::Error::new( - return_ty.span(), - "server functions should return Result where E: \ - FromServerFnError", - )) + None } -fn err_type(return_ty: &Type) -> Result> { +fn err_type(return_ty: &Type) -> Option<&Type> { if let syn::Type::Path(pat) = &return_ty { if pat.path.segments[0].ident == "Result" { if let PathArguments::AngleBracketed(args) = @@ -905,21 +912,17 @@ fn err_type(return_ty: &Type) -> Result> { { // Result if args.args.len() == 1 { - return Ok(None); + return None; } // Result else if let GenericArgument::Type(ty) = &args.args[1] { - return Ok(Some(ty)); + return Some(ty); } } } }; - Err(syn::Error::new( - return_ty.span(), - "server functions should return Result where E: \ - FromServerFnError", - )) + None } /// The arguments to the `server` macro. @@ -1369,7 +1372,7 @@ pub struct ServerFnBody { /// The return type of the server function. pub return_ty: syn::Type, /// The Ok output type of the server function. - pub output_ty: syn::GenericArgument, + pub output_ty: Option, /// The error output type of the server function. pub error_ty: Option, /// The body of the server function. @@ -1399,8 +1402,8 @@ impl Parse for ServerFnBody { let output_arrow = input.parse()?; let return_ty = input.parse()?; - let output_ty = output_type(&return_ty)?.clone(); - let error_ty = err_type(&return_ty)?.cloned(); + let output_ty = output_type(&return_ty).cloned(); + let error_ty = err_type(&return_ty).cloned(); let block = input.parse()?;