├── .github ├── FUNDING.yml ├── dependabot.yml ├── logo.png └── logo.svg ├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md ├── biome.json ├── crates ├── binario │ ├── Cargo.toml │ ├── README.md │ └── src │ │ └── lib.rs ├── cache │ ├── Cargo.toml │ ├── README.md │ └── src │ │ ├── lib.rs │ │ ├── memory.rs │ │ ├── state.rs │ │ └── store.rs ├── client │ ├── Cargo.toml │ ├── README.md │ └── src │ │ └── lib.rs ├── core │ ├── Cargo.toml │ ├── README.md │ └── src │ │ └── lib.rs ├── devtools │ ├── Cargo.toml │ ├── README.md │ └── src │ │ ├── lib.rs │ │ ├── tracing.rs │ │ └── types.rs ├── invalidation │ ├── Cargo.toml │ ├── README.md │ ├── RESEARCH.md │ └── src │ │ └── lib.rs ├── legacy │ ├── Cargo.toml │ ├── README.md │ └── src │ │ ├── config.rs │ │ ├── error.rs │ │ ├── internal │ │ ├── jsonrpc.rs │ │ ├── jsonrpc_exec.rs │ │ ├── middleware.rs │ │ ├── mod.rs │ │ ├── procedure_builder.rs │ │ └── procedure_store.rs │ │ ├── lib.rs │ │ ├── middleware.rs │ │ ├── resolver.rs │ │ ├── resolver_result.rs │ │ ├── router.rs │ │ ├── router_builder.rs │ │ └── selection.rs ├── openapi │ ├── Cargo.toml │ ├── README.md │ └── src │ │ ├── lib.rs │ │ └── swagger.html ├── procedure │ ├── Cargo.toml │ ├── README.md │ └── src │ │ ├── dyn_input.rs │ │ ├── dyn_output.rs │ │ ├── error.rs │ │ ├── interop.rs │ │ ├── lib.rs │ │ ├── logger.rs │ │ ├── procedure.rs │ │ ├── procedures.rs │ │ ├── state.rs │ │ └── stream.rs ├── tracing │ ├── Cargo.toml │ ├── README.md │ └── src │ │ ├── lib.rs │ │ └── traceable.rs ├── validator │ ├── Cargo.toml │ ├── README.md │ └── src │ │ └── lib.rs └── zer │ ├── Cargo.toml │ ├── README.md │ └── src │ └── lib.rs ├── examples ├── anyhow │ ├── Cargo.toml │ ├── bindings.ts │ └── src │ │ └── main.rs ├── astro │ ├── .astro │ │ ├── settings.json │ │ └── types.d.ts │ ├── astro.config.mjs │ ├── package.json │ ├── src │ │ ├── components │ │ │ ├── playground.ts │ │ │ ├── react.tsx │ │ │ └── solid.tsx │ │ ├── env.d.ts │ │ └── pages │ │ │ └── index.astro │ ├── test │ │ ├── client.test.ts │ │ ├── index.md │ │ ├── react.test.tsx │ │ └── solid.test.tsx │ └── tsconfig.json ├── axum │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── binario │ ├── Cargo.toml │ ├── client.ts │ └── src │ │ └── main.rs ├── bindings.ts ├── bindings_t.d.ts ├── bindings_t.d.ts.map ├── client │ ├── Cargo.toml │ └── src │ │ ├── bindings.rs │ │ └── main.rs ├── core │ ├── Cargo.toml │ └── src │ │ └── lib.rs ├── legacy │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── nextjs │ ├── .gitignore │ ├── README.md │ ├── next-env.d.ts │ ├── next.config.js │ ├── package.json │ ├── pages │ │ ├── _app.tsx │ │ ├── index.tsx │ │ ├── using-ssp.tsx │ │ ├── using-use-mutation.tsx │ │ ├── using-use-query.tsx │ │ └── using-use-subscription.tsx │ ├── src │ │ └── rspc.ts │ ├── styles │ │ ├── Home.module.css │ │ └── globals.css │ └── tsconfig.json └── tauri │ ├── .gitignore │ ├── .vscode │ └── extensions.json │ ├── README.md │ ├── index.html │ ├── package.json │ ├── public │ ├── tauri.svg │ └── vite.svg │ ├── src-tauri │ ├── .gitignore │ ├── Cargo.toml │ ├── build.rs │ ├── capabilities │ │ └── default.json │ ├── icons │ │ ├── 128x128.png │ │ ├── 128x128@2x.png │ │ ├── 32x32.png │ │ ├── Square107x107Logo.png │ │ ├── Square142x142Logo.png │ │ ├── Square150x150Logo.png │ │ ├── Square284x284Logo.png │ │ ├── Square30x30Logo.png │ │ ├── Square310x310Logo.png │ │ ├── Square44x44Logo.png │ │ ├── Square71x71Logo.png │ │ ├── Square89x89Logo.png │ │ ├── StoreLogo.png │ │ ├── icon.icns │ │ ├── icon.ico │ │ └── icon.png │ ├── src │ │ ├── api.rs │ │ ├── lib.rs │ │ └── main.rs │ └── tauri.conf.json │ ├── src │ ├── App.css │ ├── App.tsx │ ├── assets │ │ └── logo.svg │ ├── index.tsx │ └── vite-env.d.ts │ ├── tsconfig.json │ ├── tsconfig.node.json │ └── vite.config.ts ├── integrations ├── axum │ ├── Cargo.toml │ └── src │ │ ├── endpoint.rs │ │ ├── extractors.rs │ │ ├── jsonrpc.rs │ │ ├── jsonrpc_exec.rs │ │ ├── legacy.rs │ │ ├── lib.rs │ │ ├── request.rs │ │ └── v2.rs └── tauri │ ├── Cargo.toml │ ├── build.rs │ ├── permissions │ ├── autogenerated │ │ ├── commands │ │ │ └── handle_rpc.toml │ │ └── reference.md │ ├── default.toml │ └── schemas │ │ └── schema.json │ └── src │ └── lib.rs ├── package.json ├── packages ├── client │ ├── package.json │ ├── src │ │ ├── client.ts │ │ ├── error.ts │ │ ├── index.ts │ │ ├── next │ │ │ ├── UntypedClient.ts │ │ │ ├── index.test.ts │ │ │ ├── index.ts │ │ │ └── types.ts │ │ ├── transport.ts │ │ └── typescript.ts │ └── tsconfig.json ├── query-core │ ├── package.json │ ├── src │ │ └── index.ts │ └── tsconfig.json ├── react-query │ ├── package.json │ ├── src │ │ └── index.tsx │ └── tsconfig.json ├── solid-query │ ├── package.json │ ├── rollup.config.js │ ├── src │ │ └── index.tsx │ └── tsconfig.json ├── svelte-query │ ├── package.json │ ├── src │ │ ├── RspcProvider.svelte │ │ ├── context.ts │ │ └── index.ts │ └── tsconfig.json ├── tanstack-query │ ├── package.json │ ├── src │ │ └── index.ts │ └── tsconfig.json ├── tauri │ ├── package.json │ ├── src │ │ ├── index.ts │ │ └── next.ts │ └── tsconfig.json └── tsconfig.json ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── publish.sh ├── rspc ├── Cargo.toml ├── src │ ├── as_date.rs │ ├── error.rs │ ├── extension.rs │ ├── languages.rs │ ├── languages │ │ ├── rust.rs │ │ └── typescript.rs │ ├── legacy.rs │ ├── lib.rs │ ├── middleware.rs │ ├── middleware │ │ ├── into_middleware.rs │ │ ├── middleware.rs │ │ └── next.rs │ ├── mod.rs │ ├── procedure.rs │ ├── procedure │ │ ├── builder.rs │ │ ├── erased.rs │ │ ├── meta.rs │ │ ├── resolver_input.rs │ │ └── resolver_output.rs │ ├── procedure_kind.rs │ ├── router.rs │ ├── stream.rs │ ├── types.rs │ └── util.rs └── tests │ ├── router.rs │ └── typescript.rs └── turbo.json /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [oscartbeaumont] 2 | custom: ["https://paypal.me/oscartbeaumont"] -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | 8 | # TODO: Rust 9 | # TODO: npm 10 | -------------------------------------------------------------------------------- /.github/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/specta-rs/rspc/a9caf9b7070cc0017406be1d8322e02baf932645/.github/logo.png -------------------------------------------------------------------------------- /.github/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | target/ 4 | 5 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 6 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 7 | Cargo.lock 8 | 9 | # These are backup files generated by rustfmt 10 | **/*.rs.bk 11 | 12 | # System Files 13 | .DS_Store 14 | 15 | # Node 16 | node_modules 17 | .pnpm-debug.log* 18 | *.tsbuildinfo 19 | /packages/*/dist 20 | 21 | # Compiled vscode extension 22 | *.vsix 23 | 24 | .svelte-kit 25 | .turbo 26 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "2" 3 | members = [ 4 | "./crates/*", 5 | "./rspc", 6 | "./integrations/*", 7 | "./examples/core", 8 | "./examples/axum", 9 | "./examples/client", 10 | "./examples/tauri/src-tauri", 11 | "./examples/legacy", 12 | "./examples/binario", 13 | "./examples/anyhow", 14 | ] 15 | 16 | [workspace.dependencies] 17 | # Private 18 | specta-typescript = { version = "0.0.9", default-features = false } 19 | pin-project-lite = { version = "0.2", default-features = false } 20 | erased-serde = { version = "0.4", default-features = false } 21 | 22 | # Public 23 | specta = { version = "=2.0.0-rc.22", default-features = false } 24 | serde = { version = "1", default-features = false } 25 | serde_json = { version = "1", default-features = false } 26 | futures = { version = "0.3", default-features = false } 27 | futures-core = { version = "0.3", default-features = false } 28 | futures-util = { version = "0.3", default-features = false } 29 | tracing = { version = "0.1", default-features = false } 30 | 31 | [workspace.lints.clippy] 32 | all = { level = "warn", priority = -1 } 33 | cargo = { level = "warn", priority = -1 } 34 | unwrap_used = { level = "warn", priority = -1 } 35 | panic = { level = "warn", priority = -1 } 36 | todo = { level = "warn", priority = -1 } 37 | panic_in_result_fn = { level = "warn", priority = -1 } 38 | 39 | # [patch.crates-io] 40 | # specta = { git = "https://github.com/specta-rs/specta", rev = "bf3a0937cceb29eca11df207076b9e1b942ba7bb" } 41 | # specta-serde = { git = "https://github.com/specta-rs/specta", rev = "bf3a0937cceb29eca11df207076b9e1b942ba7bb" } 42 | # specta-typescript = { git = "https://github.com/specta-rs/specta", rev = "bf3a0937cceb29eca11df207076b9e1b942ba7bb" } 43 | 44 | # specta = { path = "/Users/oscar/Desktop/specta/specta" } 45 | # specta-typescript = { path = "/Users/oscar/Desktop/specta/specta-typescript" } 46 | # specta-serde = { path = "/Users/oscar/Desktop/specta/specta-serde" } 47 | # specta-util = { path = "/Users/oscar/Desktop/specta/specta-util" } 48 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Oscar Beaumont 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Logo 3 |

4 |

rspc

5 |
6 | 7 | A blazing fast and easy to use TRPC-like server for Rust. 8 | 9 |
10 | 11 |

Website

12 |
13 | 14 |
15 | 16 |
17 | Discord 18 | 19 | crates.io 21 | 22 | 23 | download count badge 25 | 26 | 27 | docs.rs 29 | 30 | 31 | npm (scoped) 32 | 33 |
34 |
35 | 36 | 37 | > [!WARNING] 38 | > rspc is no longer being maintained. [Learn more](https://github.com/specta-rs/rspc/discussions/351). 39 | 40 | ## Example 41 | 42 | You define a `rspc` router and attach procedures to it like below. This will be very familiar if you have used [trpc](https://trpc.io/) or [GraphQL](https://graphql.org) before. 43 | 44 | ```rust 45 | let router = ::new() 46 | .query("version", |t| { 47 | t(|ctx, input: ()| "0.0.1") 48 | }) 49 | .mutation("helloWorld", |t| { 50 | t(|ctx, input: ()| async { "Hello World!" }) 51 | }); 52 | ``` 53 | 54 | ## Features: 55 | 56 | - Per Request Context - Great for database connection & authentication data 57 | - Middleware - With support for context switching 58 | - Merging routers - Great for separating code between files 59 | 60 | ### Inspiration 61 | 62 | This project is based off [trpc](https://trpc.io) and was inspired by the bridge system [Jamie Pine](https://github.com/jamiepine) designed for [Spacedrive](https://www.spacedrive.com). A huge thanks to everyone who helped inspire this project! 63 | -------------------------------------------------------------------------------- /biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://biomejs.dev/schemas/1.7.3/schema.json", 3 | "files": { 4 | "include": ["packages/**"], 5 | "ignore": ["node_modules", "dist"] 6 | }, 7 | "organizeImports": { 8 | "enabled": true 9 | }, 10 | "linter": { 11 | "enabled": true, 12 | "rules": { 13 | "recommended": true, 14 | "suspicious": { 15 | "noExplicitAny": "off" 16 | }, 17 | "style": { 18 | "noNonNullAssertion": "off" 19 | }, 20 | "correctness": { 21 | "useJsxKeyInIterable": "off" 22 | } 23 | } 24 | }, 25 | "formatter": { 26 | "indentStyle": "tab" 27 | }, 28 | "vcs": { 29 | "enabled": true, 30 | "clientKind": "git", 31 | "useIgnoreFile": true, 32 | "defaultBranch": "main" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /crates/binario/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rspc-binario" 3 | description = "Binario support for rspc" 4 | version = "0.0.0" 5 | edition = "2021" 6 | publish = false # TODO: Crate metadata & publish 7 | 8 | [dependencies] 9 | binario = "0.0.3" 10 | futures-util.workspace = true 11 | rspc = { path = "../../rspc" } 12 | specta = { workspace = true } 13 | tokio = "1.43.0" 14 | 15 | # /bin/sh RUSTDOCFLAGS="--cfg docsrs" cargo +nightly doc --all-features 16 | [package.metadata."docs.rs"] 17 | all-features = true 18 | rustdoc-args = ["--cfg", "docsrs"] 19 | 20 | [lints] 21 | workspace = true 22 | -------------------------------------------------------------------------------- /crates/binario/README.md: -------------------------------------------------------------------------------- 1 | # rspc 🤝 Binario 2 | 3 | [![docs.rs](https://img.shields.io/crates/v/rspc-binario)](https://docs.rs/rspc-binario) 4 | 5 | > [!CAUTION] 6 | > This crate is a proof of concept. 7 | > 8 | > It is not intended for production use and will likely remain that way. 9 | 10 | Use [Binario](https://github.com/oscartbeaumont/binario) instead of [Serde](https://serde.rs) for serialization and deserialization. 11 | 12 | This is a proof of concept to show that rspc has the ability to support any serialization libraries. 13 | -------------------------------------------------------------------------------- /crates/cache/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rspc-cache" 3 | version = "0.0.0" 4 | edition = "2021" 5 | publish = false 6 | 7 | [dependencies] 8 | moka = { version = "0.12.10", features = ["sync"] } 9 | pin-project-lite = { workspace = true } 10 | rspc = { path = "../../rspc" } 11 | 12 | # /bin/sh RUSTDOCFLAGS="--cfg docsrs" cargo +nightly doc --all-features 13 | [package.metadata."docs.rs"] 14 | all-features = true 15 | rustdoc-args = ["--cfg", "docsrs"] 16 | 17 | [lints] 18 | workspace = true 19 | -------------------------------------------------------------------------------- /crates/cache/README.md: -------------------------------------------------------------------------------- 1 | # rspc cache 2 | 3 | [![docs.rs](https://img.shields.io/crates/v/rspc-cache)](https://docs.rs/rspc-cache) 4 | 5 | > [!CAUTION] 6 | > This crate is still a work in progress. You can use it but we can't guarantee that it's API won't change. 7 | 8 | Provides a simple way to cache the results of rspc queries with pluggable backends. 9 | 10 | Features: 11 | - Simple to use 12 | - Pluggable backends (memory, redis, etc.) 13 | - Configurable cache TTL 14 | 15 | ## Example 16 | 17 | ```rust 18 | // TODO: imports 19 | 20 | fn todo() -> Router2 { 21 | Router2::new() 22 | .setup(CacheState::builder(Memory::new()).mount()) 23 | .procedure("my_query", { 24 | ::builder() 25 | .with(cache()) 26 | .query(|_, _: ()| async { 27 | // if input.some_arg {} 28 | cache_ttl(10); 29 | 30 | Ok(SystemTime::now()) 31 | }) 32 | }) 33 | } 34 | ``` 35 | -------------------------------------------------------------------------------- /crates/cache/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! rspc-cache: Caching middleware for rspc 2 | #![forbid(unsafe_code)] 3 | #![cfg_attr(docsrs, feature(doc_cfg))] 4 | #![doc( 5 | html_logo_url = "https://github.com/specta-rs/rspc/blob/main/.github/logo.png?raw=true", 6 | html_favicon_url = "https://github.com/specta-rs/rspc/blob/main/.github/logo.png?raw=true" 7 | )] 8 | 9 | mod memory; 10 | mod state; 11 | mod store; 12 | 13 | use std::{ 14 | cell::Cell, 15 | future::{poll_fn, Future}, 16 | pin::pin, 17 | }; 18 | 19 | pub use memory::Memory; 20 | pub use state::CacheState; 21 | pub use store::Store; 22 | 23 | use rspc::middleware::Middleware; 24 | use store::Value; 25 | 26 | thread_local! { 27 | static CACHE_TTL: Cell> = Cell::new(None); 28 | } 29 | 30 | /// Set the cache time-to-live (TTL) in seconds 31 | pub fn cache_ttl(ttl: usize) { 32 | CACHE_TTL.set(Some(ttl)); 33 | } 34 | 35 | pub fn cache() -> Middleware 36 | where 37 | TError: Send + 'static, 38 | TCtx: Send + 'static, 39 | TInput: Clone + Send + 'static, 40 | TResult: Clone + Send + Sync + 'static, 41 | { 42 | Middleware::new(move |ctx: TCtx, input: TInput, next| { 43 | async move { 44 | let meta = next.meta(); 45 | let cache = meta.state().get::().unwrap(); // TODO: Error handling 46 | 47 | let key = "todo"; // TODO: Work this out properly 48 | // TODO: Keyed to `TInput` 49 | 50 | if let Some(value) = cache.store().get(key) { 51 | let value: &TResult = value.downcast_ref().unwrap(); // TODO: Error 52 | return Ok(value.clone()); 53 | } 54 | 55 | let fut = next.exec(ctx, input); 56 | let mut fut = pin!(fut); 57 | 58 | let (ttl, result): (Option, Result) = 59 | poll_fn(|cx| fut.as_mut().poll(cx).map(|v| (CACHE_TTL.get(), v))).await; 60 | 61 | if let Some(ttl) = ttl { 62 | // TODO: Caching error responses? 63 | if let Ok(value) = &result { 64 | cache.store().set(key, Value::new(value.clone()), ttl); 65 | }; 66 | } 67 | 68 | result 69 | } 70 | }) 71 | } 72 | -------------------------------------------------------------------------------- /crates/cache/src/memory.rs: -------------------------------------------------------------------------------- 1 | use moka::sync::Cache; 2 | 3 | use crate::{store::Value, Store}; 4 | 5 | pub struct Memory(Cache); 6 | 7 | impl Memory { 8 | pub fn new() -> Self { 9 | Self(Cache::new(100)) // TODO: Configurable 10 | } 11 | } 12 | 13 | impl Store for Memory { 14 | fn get(&self, key: &str) -> Option { 15 | self.0.get(key).map(|v| v.clone()) 16 | } 17 | 18 | fn set(&self, key: &str, value: Value, ttl: usize) { 19 | // TODO: Properly set ttl 20 | self.0.insert(key.to_string(), value); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /crates/cache/src/state.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use rspc::State; 4 | 5 | use crate::Store; 6 | 7 | pub struct CacheState> { 8 | store: S, 9 | } 10 | 11 | impl CacheState { 12 | pub fn builder(store: S) -> Self { 13 | Self { store } 14 | } 15 | 16 | pub fn store(&self) -> &S { 17 | &self.store 18 | } 19 | 20 | // TODO: Default ttl 21 | 22 | pub fn mount(self) -> impl FnOnce(&mut State) { 23 | let cache = CacheState::>::builder(Arc::new(self.store)); 24 | move |state: &mut State| { 25 | state.insert(cache); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /crates/cache/src/store.rs: -------------------------------------------------------------------------------- 1 | use std::{any::Any, sync::Arc}; 2 | 3 | pub trait Store: Send + Sync + 'static { 4 | fn get(&self, key: &str) -> Option; 5 | 6 | fn set(&self, key: &str, value: Value, ttl: usize); 7 | } 8 | 9 | impl Store for Arc { 10 | fn get(&self, key: &str) -> Option { 11 | self.as_ref().get(key) 12 | } 13 | 14 | fn set(&self, key: &str, value: Value, ttl: usize) { 15 | self.as_ref().set(key, value, ttl) 16 | } 17 | } 18 | 19 | impl Store for Arc { 20 | fn get(&self, key: &str) -> Option { 21 | self.as_ref().get(key) 22 | } 23 | 24 | fn set(&self, key: &str, value: Value, ttl: usize) { 25 | self.as_ref().set(key, value, ttl) 26 | } 27 | } 28 | 29 | pub struct Value(Box); 30 | 31 | impl Value { 32 | pub fn new(v: T) -> Self { 33 | Self(Box::new(v)) 34 | } 35 | 36 | pub fn downcast_ref(&self) -> Option<&T> { 37 | self.0.inner().downcast_ref() 38 | } 39 | } 40 | 41 | impl Clone for Value { 42 | fn clone(&self) -> Self { 43 | Self(self.0.dyn_clone()) 44 | } 45 | } 46 | 47 | // TODO: Sealing this better. 48 | trait Repr: Send + Sync + 'static { 49 | // Return `Value` instead of `Box` directly for sealing 50 | fn dyn_clone(&self) -> Box; 51 | 52 | fn inner(&self) -> &dyn Any; 53 | } 54 | impl Repr for T { 55 | fn dyn_clone(&self) -> Box { 56 | Box::new(self.clone()) 57 | } 58 | 59 | fn inner(&self) -> &dyn Any { 60 | self 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /crates/client/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rspc-client" 3 | description = "Rust client for rspc" 4 | version = "0.0.1" 5 | authors = ["Oscar Beaumont "] 6 | edition = "2021" 7 | # license = "MIT" 8 | repository = "https://github.com/specta-rs/rspc" 9 | documentation = "https://docs.rs/rspc-client" 10 | keywords = ["async", "specta", "rust-to-ts", "typescript", "typesafe"] 11 | categories = ["web-programming", "asynchronous"] 12 | publish = false # TODO: This is still very unstable 13 | 14 | # /bin/sh RUSTDOCFLAGS="--cfg docsrs" cargo +nightly doc --all-features 15 | [package.metadata."docs.rs"] 16 | all-features = true 17 | rustdoc-args = ["--cfg", "docsrs"] 18 | 19 | [dependencies] 20 | reqwest = { version = "0.12.12", features = ["json"] } 21 | rspc-procedure = { version = "0.0.1", path = "../procedure" } 22 | serde = { workspace = true, features = ["derive"] } # TODO: Drop derive feature? 23 | serde_json = { workspace = true } 24 | -------------------------------------------------------------------------------- /crates/client/README.md: -------------------------------------------------------------------------------- 1 | # Rust client 2 | 3 | [![docs.rs](https://img.shields.io/crates/v/rspc-client)](https://docs.rs/rspc-client) 4 | 5 | > [!CAUTION] 6 | > This crate is still a work in progress. You can use it but we can't guarantee that it's API won't change. 7 | 8 | Allows you to make queries from a Rust client to an rspc server. 9 | 10 | ## Example 11 | 12 | ```rust 13 | // This file is generated via the `rspc::Rust` language on your server 14 | mod bindings; 15 | 16 | #[tokio::main] 17 | async fn main() { 18 | let client = rspc_client::Client::new("http://[::]:4000/rspc"); 19 | 20 | println!("{:?}", client.exec::(()).await); 21 | println!( 22 | "{:?}", 23 | client 24 | .exec::("Some random string!".into()) 25 | .await 26 | ); 27 | println!( 28 | "{:?}", 29 | client 30 | .exec::("Hello from rspc Rust client!".into()) 31 | .await 32 | ); 33 | } 34 | ``` 35 | -------------------------------------------------------------------------------- /crates/core/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rspc-core" 3 | description = "Core types and traits for rspc" 4 | version = "0.0.1" 5 | authors = ["Oscar Beaumont "] 6 | edition = "2021" 7 | license = "MIT" 8 | repository = "https://github.com/specta-rs/rspc" 9 | documentation = "https://docs.rs/rspc-procedure" 10 | keywords = ["async", "specta", "rust-to-ts", "typescript", "typesafe"] 11 | categories = ["web-programming", "asynchronous"] 12 | 13 | # /bin/sh RUSTDOCFLAGS="--cfg docsrs" cargo +nightly doc --all-features 14 | [package.metadata."docs.rs"] 15 | all-features = true 16 | rustdoc-args = ["--cfg", "docsrs"] 17 | 18 | [dependencies] 19 | # Public 20 | futures-core = { workspace = true, default-features = false } 21 | serde = { workspace = true, default-features = false } 22 | 23 | # Private 24 | erased-serde = { workspace = true, default-features = false, features = [ 25 | "std", 26 | ] } 27 | pin-project-lite = { workspace = true, default-features = false } 28 | 29 | [lints] 30 | workspace = true 31 | -------------------------------------------------------------------------------- /crates/core/README.md: -------------------------------------------------------------------------------- 1 | # Core 2 | 3 | [![docs.rs](https://img.shields.io/crates/v/rspc-core)](https://docs.rs/rspc-core) 4 | 5 | Core types and traits for rspc. 6 | -------------------------------------------------------------------------------- /crates/core/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Core types and traits for [`rspc`]. 2 | //! 3 | //! Middleware and extension authors should prefer to depend on this crate instead of `rspc`. 4 | #![forbid(unsafe_code)] 5 | #![cfg_attr(docsrs, feature(doc_cfg))] 6 | #![doc( 7 | html_logo_url = "https://github.com/specta-rs/rspc/raw/main/.github/logo.png", 8 | html_favicon_url = "https://github.com/specta-rs/rspc/raw/main/.github/logo.png" 9 | )] 10 | -------------------------------------------------------------------------------- /crates/devtools/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rspc-devtools" 3 | version = "0.0.0" 4 | edition = "2021" 5 | publish = false 6 | 7 | [dependencies] 8 | futures = { workspace = true } 9 | rspc-procedure = { path = "../../crates/procedure" } 10 | serde = { workspace = true, features = ["derive"] } 11 | specta = { workspace = true, features = ["derive"] } 12 | tracing = { workspace = true } 13 | 14 | # /bin/sh RUSTDOCFLAGS="--cfg docsrs" cargo +nightly doc --all-features 15 | [package.metadata."docs.rs"] 16 | all-features = true 17 | rustdoc-args = ["--cfg", "docsrs"] 18 | 19 | [lints] 20 | workspace = true 21 | -------------------------------------------------------------------------------- /crates/devtools/README.md: -------------------------------------------------------------------------------- 1 | # rspc devtools 2 | 3 | [![docs.rs](https://img.shields.io/crates/v/rspc-devtools)](https://docs.rs/rspc-devtools) 4 | 5 | > [!CAUTION] 6 | > This crate is an experiment. You shouldn't use it unless you really know what you are doing. 7 | -------------------------------------------------------------------------------- /crates/devtools/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! rspc-devtools: Devtools for rspc applications 2 | #![forbid(unsafe_code)] 3 | #![cfg_attr(docsrs, feature(doc_cfg))] 4 | #![doc( 5 | html_logo_url = "https://github.com/specta-rs/rspc/blob/main/.github/logo.png?raw=true", 6 | html_favicon_url = "https://github.com/specta-rs/rspc/blob/main/.github/logo.png?raw=true" 7 | )] 8 | 9 | // http://[::]:4000/rspc/~rspc.devtools.meta 10 | // http://[::]:4000/rspc/~rspc.devtools.history 11 | 12 | mod tracing; 13 | mod types; 14 | 15 | use std::{ 16 | any::Any, 17 | future, 18 | sync::{Arc, Mutex, PoisonError}, 19 | }; 20 | 21 | use futures::stream; 22 | use rspc_procedure::{Procedure, ProcedureStream, Procedures}; 23 | use types::{Metadata, ProcedureMetadata}; 24 | 25 | pub fn mount( 26 | procedures: impl Into>, 27 | types: &impl Any, 28 | ) -> Procedures { 29 | // let procedures = procedures.into(); 30 | // let meta = Metadata { 31 | // crate_name: env!("CARGO_PKG_NAME"), 32 | // crate_version: env!("CARGO_PKG_VERSION"), 33 | // rspc_version: env!("CARGO_PKG_VERSION"), 34 | // procedures: procedures 35 | // .iter() 36 | // .map(|(name, _)| (name.to_string(), ProcedureMetadata {})) 37 | // .collect(), 38 | // }; 39 | // let history = Arc::new(Mutex::new(Vec::new())); // TODO: Stream to clients instead of storing in memory 40 | 41 | // let mut procedures = procedures 42 | // .into_iter() 43 | // .map(|(name, procedure)| { 44 | // let history = history.clone(); 45 | 46 | // ( 47 | // name.clone(), 48 | // Procedure::new(move |ctx, input| { 49 | // let start = std::time::Instant::now(); 50 | // let result = procedure.exec(ctx, input); 51 | // history 52 | // .lock() 53 | // .unwrap_or_else(PoisonError::into_inner) 54 | // .push((name.to_string(), format!("{:?}", start.elapsed()))); 55 | // result 56 | // }), 57 | // ) 58 | // }) 59 | // .collect::>(); 60 | 61 | // procedures.insert( 62 | // "~rspc.devtools.meta".into(), 63 | // Procedure::new(move |ctx, input| { 64 | // let value = Ok(meta.clone()); 65 | // ProcedureStream::from_stream(stream::once(future::ready(value))) 66 | // }), 67 | // ); 68 | // procedures.insert( 69 | // "~rspc.devtools.history".into(), 70 | // Procedure::new({ 71 | // let history = history.clone(); 72 | // move |ctx, input| { 73 | // let value = Ok(history 74 | // .lock() 75 | // .unwrap_or_else(PoisonError::into_inner) 76 | // .clone()); 77 | // ProcedureStream::from_stream(stream::once(future::ready(value))) 78 | // } 79 | // }), 80 | // ); 81 | 82 | // procedures 83 | 84 | todo!(); 85 | } 86 | -------------------------------------------------------------------------------- /crates/devtools/src/tracing.rs: -------------------------------------------------------------------------------- 1 | // pub fn init() { 2 | // ConsoleLayer::builder().with_default_env().init(); 3 | // } 4 | 5 | // pub struct ConsoleLayer { 6 | // current_spans: ThreadLocal>, 7 | // tx: mpsc::Sender, 8 | // } 9 | -------------------------------------------------------------------------------- /crates/devtools/src/types.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use serde::Serialize; 4 | use specta::Type; 5 | 6 | #[derive(Clone, Serialize, Type)] 7 | pub struct Metadata { 8 | pub crate_name: &'static str, 9 | pub crate_version: &'static str, 10 | pub rspc_version: &'static str, 11 | pub procedures: HashMap, 12 | } 13 | 14 | #[derive(Clone, Serialize, Type)] 15 | pub struct ProcedureMetadata { 16 | // TODO: input type 17 | // TOOD: output type 18 | // TODO: p99's 19 | } 20 | -------------------------------------------------------------------------------- /crates/invalidation/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rspc-invalidation" 3 | version = "0.0.0" 4 | edition = "2021" 5 | publish = false # TODO: Crate metadata & publish 6 | 7 | [dependencies] 8 | rspc = { path = "../../rspc" } 9 | serde = { workspace = true } 10 | serde_json = { workspace = true } 11 | 12 | # /bin/sh RUSTDOCFLAGS="--cfg docsrs" cargo +nightly doc --all-features 13 | [package.metadata."docs.rs"] 14 | all-features = true 15 | rustdoc-args = ["--cfg", "docsrs"] 16 | 17 | [lints] 18 | workspace = true 19 | -------------------------------------------------------------------------------- /crates/invalidation/README.md: -------------------------------------------------------------------------------- 1 | # rspc invalidation 2 | 3 | [![docs.rs](https://img.shields.io/crates/v/rspc-invalidation)](https://docs.rs/rspc-invalidation) 4 | 5 | > [!CAUTION] 6 | > This crate is still a work in progress. You can use it but we can't guarantee that it's API won't change. 7 | 8 | Support for server-initiated invalidation of data. This can be utilised to achieve Single Flight Mutation. 9 | 10 | Features: 11 | - Support for Single Flight Mutations 12 | - Support for subscription-based invalidation to invalidate data across all clients 13 | 14 | ## Example 15 | 16 | ```rust 17 | // TODO 18 | ``` 19 | -------------------------------------------------------------------------------- /crates/legacy/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rspc-legacy" 3 | description = "The rspc 0.3 syntax implemented on top of the 0.4 core" 4 | version = "0.0.1" 5 | authors = ["Oscar Beaumont "] 6 | edition = "2021" 7 | license = "MIT" 8 | include = ["/src", "/LICENCE", "/README.md"] 9 | repository = "https://github.com/specta-rs/rspc" 10 | documentation = "https://docs.rs/rspc-legacy/latest/rspc-legacy" 11 | keywords = ["async", "specta", "rust-to-ts", "typescript", "typesafe"] 12 | categories = ["web-programming", "asynchronous"] 13 | 14 | [features] 15 | default = [] 16 | # Warnings for deprecations 17 | deprecated = [] 18 | 19 | [dependencies] 20 | rspc-procedure = { version = "0.0.1", path = "../procedure" } 21 | serde = { workspace = true } 22 | futures = { workspace = true } 23 | specta = { workspace = true, features = [ 24 | "serde", 25 | "serde_json", 26 | "derive", # TODO: remove this 27 | ] } 28 | specta-typescript = { workspace = true, features = [] } 29 | serde_json = { workspace = true } 30 | thiserror = "2.0.11" 31 | tokio = { version = "1.43.0", features = ["macros", "sync", "rt"] } 32 | 33 | # /bin/sh RUSTDOCFLAGS="--cfg docsrs" cargo +nightly doc --all-features 34 | [package.metadata."docs.rs"] 35 | all-features = true 36 | rustdoc-args = ["--cfg", "docsrs"] 37 | 38 | [lints] 39 | workspace = true 40 | -------------------------------------------------------------------------------- /crates/legacy/README.md: -------------------------------------------------------------------------------- 1 | # rspc Legacy 2 | 3 | [![docs.rs](https://img.shields.io/crates/v/rspc-legacy)](https://docs.rs/rspc-legacy) 4 | 5 | The rspc 0.3.1 syntax implemented on top of the 0.4.0 core. This is designed to make incremental migration easier. 6 | -------------------------------------------------------------------------------- /crates/legacy/src/config.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | /// TODO 4 | #[derive(Default)] 5 | pub struct Config { 6 | pub(crate) export_bindings_on_build: Option, 7 | pub(crate) bindings_header: Option<&'static str>, 8 | } 9 | 10 | impl Config { 11 | pub fn new() -> Self { 12 | Default::default() 13 | } 14 | 15 | /// will export the bindings of the generated router to a folder every time the router is built. 16 | /// Note: The bindings are only exported when `debug_assertions` are enabled (Rust is in debug mode). 17 | pub fn export_ts_bindings(mut self, export_path: TPath) -> Self 18 | where 19 | PathBuf: From, 20 | { 21 | self.export_bindings_on_build = Some(PathBuf::from(export_path)); 22 | self 23 | } 24 | 25 | /// allows you to add a custom string to the top of the exported Typescript bindings file. 26 | /// This is useful if you want to disable ESLint or Prettier. 27 | pub fn set_ts_bindings_header(mut self, custom: &'static str) -> Self { 28 | self.bindings_header = Some(custom); 29 | self 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /crates/legacy/src/internal/mod.rs: -------------------------------------------------------------------------------- 1 | //! Internal types which power rspc. The module provides no guarantee of compatibility between updates, so you should be careful rely on types from it. 2 | 3 | mod jsonrpc_exec; 4 | mod middleware; 5 | mod procedure_builder; 6 | mod procedure_store; 7 | 8 | pub(crate) use middleware::*; 9 | pub(crate) use procedure_builder::*; 10 | pub(crate) use procedure_store::*; 11 | 12 | // Used by `rspc_axum` 13 | pub use middleware::ProcedureKind; 14 | pub mod jsonrpc; 15 | 16 | // Were not exported by rspc 0.3.0 but required by `rspc::legacy` interop layer 17 | #[doc(hidden)] 18 | pub use middleware::{Layer, RequestContext, ValueOrStream}; 19 | -------------------------------------------------------------------------------- /crates/legacy/src/internal/procedure_builder.rs: -------------------------------------------------------------------------------- 1 | use std::{marker::PhantomData, ops::Deref}; 2 | 3 | pub struct UnbuiltProcedureBuilder { 4 | deref_handler: fn(TResolver) -> BuiltProcedureBuilder, 5 | phantom: PhantomData, 6 | } 7 | 8 | impl Default for UnbuiltProcedureBuilder { 9 | fn default() -> Self { 10 | Self { 11 | deref_handler: |resolver| BuiltProcedureBuilder { resolver }, 12 | phantom: PhantomData, 13 | } 14 | } 15 | } 16 | 17 | impl UnbuiltProcedureBuilder { 18 | pub fn resolver(self, resolver: TResolver) -> BuiltProcedureBuilder { 19 | (self.deref_handler)(resolver) 20 | } 21 | } 22 | 23 | impl Deref for UnbuiltProcedureBuilder { 24 | type Target = fn(resolver: TResolver) -> BuiltProcedureBuilder; 25 | 26 | fn deref(&self) -> &Self::Target { 27 | &self.deref_handler 28 | } 29 | } 30 | 31 | pub struct BuiltProcedureBuilder { 32 | pub resolver: TResolver, 33 | } 34 | -------------------------------------------------------------------------------- /crates/legacy/src/internal/procedure_store.rs: -------------------------------------------------------------------------------- 1 | use std::collections::BTreeMap; 2 | 3 | use specta::DataType; 4 | 5 | use super::Layer; 6 | 7 | // TODO: Make private 8 | #[derive(Debug)] 9 | pub struct ProcedureDataType { 10 | pub arg_ty: DataType, 11 | pub result_ty: DataType, 12 | } 13 | 14 | // TODO: Make private 15 | pub struct Procedure { 16 | pub exec: Box>, 17 | pub ty: ProcedureDataType, 18 | } 19 | 20 | pub struct ProcedureStore { 21 | name: &'static str, 22 | pub store: BTreeMap>, 23 | } 24 | 25 | impl ProcedureStore { 26 | pub fn new(name: &'static str) -> Self { 27 | Self { 28 | name, 29 | store: Default::default(), 30 | } 31 | } 32 | 33 | pub fn append(&mut self, key: String, exec: Box>, ty: ProcedureDataType) { 34 | #[allow(clippy::panic)] 35 | if key.is_empty() || key == "ws" || key.starts_with("rpc.") || key.starts_with("rspc.") { 36 | panic!( 37 | "rspc error: attempted to create {} operation named '{}', however this name is not allowed.", 38 | self.name, 39 | key 40 | ); 41 | } 42 | 43 | #[allow(clippy::panic)] 44 | if self.store.contains_key(&key) { 45 | panic!( 46 | "rspc error: {} operation already has resolver with name '{}'", 47 | self.name, key 48 | ); 49 | } 50 | 51 | self.store.insert(key, Procedure { exec, ty }); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /crates/legacy/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! The rspc 0.3.1 syntax implemented on top of the 0.4.0 core. 2 | //! 3 | //! This allows incremental migration from the old syntax to the new syntax with the minimal breaking changes. 4 | #![forbid(unsafe_code)] 5 | #![cfg_attr(docsrs, feature(doc_cfg))] 6 | #![doc( 7 | html_logo_url = "https://github.com/specta-rs/rspc/blob/main/.github/logo.png?raw=true", 8 | html_favicon_url = "https://github.com/specta-rs/rspc/blob/main/.github/logo.png?raw=true" 9 | )] 10 | 11 | mod config; 12 | mod error; 13 | mod middleware; 14 | mod resolver; 15 | mod resolver_result; 16 | mod router; 17 | mod router_builder; 18 | mod selection; 19 | 20 | #[cfg_attr( 21 | feature = "deprecated", 22 | deprecated = "This is replaced by `rspc::Typescript`" 23 | )] 24 | pub use config::Config; 25 | pub use error::{Error, ErrorCode, ExecError, ExportError}; 26 | pub use middleware::{ 27 | Middleware, MiddlewareBuilder, MiddlewareContext, MiddlewareLike, MiddlewareWithResponseHandler, 28 | }; 29 | pub use resolver::{typedef, DoubleArgMarker, DoubleArgStreamMarker, Resolver, StreamResolver}; 30 | pub use resolver_result::{FutureMarker, RequestLayer, ResultMarker, SerializeMarker}; 31 | pub use router::{ExecKind, Router}; 32 | pub use router_builder::RouterBuilder; 33 | 34 | pub mod internal; 35 | 36 | #[cfg_attr( 37 | feature = "deprecated", 38 | deprecated = "This is no longer going to included. You can copy it into your project if you need it." 39 | )] 40 | #[cfg(debug_assertions)] 41 | #[allow(clippy::panic)] 42 | pub fn test_result_type() { 43 | panic!("You should not call `test_type` at runtime. This is just a debugging tool."); 44 | } 45 | 46 | #[cfg_attr( 47 | feature = "deprecated", 48 | deprecated = "This is no longer going to included. You can copy it into your project if you need it." 49 | )] 50 | #[cfg(debug_assertions)] 51 | #[allow(clippy::panic)] 52 | pub fn test_result_value(_: T) { 53 | panic!("You should not call `test_type` at runtime. This is just a debugging tool."); 54 | } 55 | -------------------------------------------------------------------------------- /crates/legacy/src/resolver_result.rs: -------------------------------------------------------------------------------- 1 | use std::{future::Future, marker::PhantomData}; 2 | 3 | use serde::Serialize; 4 | use specta::Type; 5 | 6 | use crate::{ 7 | internal::{LayerResult, ValueOrStream}, 8 | Error, ExecError, 9 | }; 10 | 11 | pub trait RequestLayer { 12 | type Result: Type; 13 | 14 | fn into_layer_result(self) -> Result; 15 | } 16 | 17 | pub struct SerializeMarker(PhantomData<()>); 18 | impl RequestLayer for T 19 | where 20 | T: Serialize + Type, 21 | { 22 | type Result = T; 23 | 24 | fn into_layer_result(self) -> Result { 25 | Ok(LayerResult::Ready(Ok( 26 | serde_json::to_value(self).map_err(ExecError::SerializingResultErr)? 27 | ))) 28 | } 29 | } 30 | 31 | pub struct ResultMarker(PhantomData<()>); 32 | impl RequestLayer for Result 33 | where 34 | T: Serialize + Type, 35 | { 36 | type Result = T; 37 | 38 | fn into_layer_result(self) -> Result { 39 | Ok(LayerResult::Ready(Ok(serde_json::to_value( 40 | self.map_err(ExecError::ErrResolverError)?, 41 | ) 42 | .map_err(ExecError::SerializingResultErr)?))) 43 | } 44 | } 45 | 46 | pub struct FutureMarker(PhantomData); 47 | impl RequestLayer> for TFut 48 | where 49 | TFut: Future + Send + 'static, 50 | T: RequestLayer + Send, 51 | { 52 | type Result = T::Result; 53 | 54 | fn into_layer_result(self) -> Result { 55 | Ok(LayerResult::Future(Box::pin(async move { 56 | match self 57 | .await 58 | .into_layer_result()? 59 | .into_value_or_stream() 60 | .await? 61 | { 62 | ValueOrStream::Stream(_) => unreachable!(), 63 | ValueOrStream::Value(v) => Ok(v), 64 | } 65 | }))) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /crates/legacy/src/selection.rs: -------------------------------------------------------------------------------- 1 | //! The selection macro. 2 | //! 3 | //! WARNING: Wherever this is called you must have the `specta` crate installed. 4 | #[macro_export] 5 | #[cfg_attr( 6 | feature = "deprecated", 7 | deprecated = "Use `specta_util::selection` instead" 8 | )] 9 | macro_rules! selection { 10 | ( $s:expr, { $($n:ident),+ } ) => {{ 11 | #[allow(non_camel_case_types)] 12 | mod selection { 13 | #[derive(serde::Serialize, specta::Type)] 14 | #[specta(inline)] 15 | pub struct Selection<$($n,)*> { 16 | $(pub $n: $n),* 17 | } 18 | } 19 | use selection::Selection; 20 | #[allow(non_camel_case_types)] 21 | Selection { $($n: $s.$n,)* } 22 | }}; 23 | ( $s:expr, [{ $($n:ident),+ }] ) => {{ 24 | #[allow(non_camel_case_types)] 25 | mod selection { 26 | #[derive(serde::Serialize, specta::Type)] 27 | #[specta(inline)] 28 | pub struct Selection<$($n,)*> { 29 | $(pub $n: $n,)* 30 | } 31 | } 32 | use selection::Selection; 33 | #[allow(non_camel_case_types)] 34 | $s.into_iter().map(|v| Selection { $($n: v.$n,)* }).collect::>() 35 | }}; 36 | } 37 | 38 | #[cfg(test)] 39 | mod tests { 40 | use specta::Type; 41 | use specta_typescript::inline; 42 | 43 | fn ts_export_ref(_t: &T) -> String { 44 | inline::(&Default::default()).unwrap() 45 | } 46 | 47 | #[derive(Clone)] 48 | #[allow(dead_code)] 49 | struct User { 50 | pub id: i32, 51 | pub name: String, 52 | pub email: String, 53 | pub age: i32, 54 | pub password: String, 55 | } 56 | 57 | #[test] 58 | fn test_selection_macros() { 59 | let user = User { 60 | id: 1, 61 | name: "Monty Beaumont".into(), 62 | email: "monty@otbeaumont.me".into(), 63 | age: 7, 64 | password: "password123".into(), 65 | }; 66 | 67 | let s1 = selection!(user.clone(), { name, age }); 68 | assert_eq!(s1.name, "Monty Beaumont".to_string()); 69 | assert_eq!(s1.age, 7); 70 | assert_eq!(ts_export_ref(&s1), "{ name: string; age: number }"); 71 | 72 | let users = vec![user; 3]; 73 | let s2 = selection!(users, [{ name, age }]); 74 | assert_eq!(s2[0].name, "Monty Beaumont".to_string()); 75 | assert_eq!(s2[0].age, 7); 76 | assert_eq!(ts_export_ref(&s2), "{ name: string; age: number }[]"); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /crates/openapi/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rspc-openapi" 3 | version = "0.0.0" 4 | edition = "2021" 5 | publish = false # TODO: Crate metadata & publish 6 | 7 | [dependencies] 8 | rspc = { path = "../../rspc" } 9 | axum = { version = "0.8.1", default-features = false } # TODO: Remove this 10 | serde_json = { workspace = true } 11 | futures = { workspace = true } 12 | 13 | # /bin/sh RUSTDOCFLAGS="--cfg docsrs" cargo +nightly doc --all-features 14 | [package.metadata."docs.rs"] 15 | all-features = true 16 | rustdoc-args = ["--cfg", "docsrs"] 17 | 18 | [lints] 19 | workspace = true 20 | -------------------------------------------------------------------------------- /crates/openapi/README.md: -------------------------------------------------------------------------------- 1 | # rspc OpenAPI 2 | 3 | [![docs.rs](https://img.shields.io/crates/v/rspc-openapi)](https://docs.rs/rspc-openapi) 4 | 5 | > [!CAUTION] 6 | > This crate is still a work in progress. You can use it but we can't guarantee that it's API won't change. 7 | 8 | Support for generating an [OpenAPI](https://www.openapis.org) schema and endpoints from your rspc router. 9 | 10 | Features: 11 | - Support for [Swagger](https://swagger.io) and [Scalar](https://scalar.com) 12 | 13 | ## Example 14 | 15 | ```rust 16 | // Coming soon... 17 | ``` 18 | -------------------------------------------------------------------------------- /crates/openapi/src/swagger.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | SwaggerUI 8 | 12 | 13 | 14 |
15 | 19 | 23 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /crates/procedure/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rspc-procedure" 3 | description = "Interface for a single type-erased operation that the server can execute" 4 | version = "0.0.1" 5 | authors = ["Oscar Beaumont "] 6 | edition = "2021" 7 | license = "MIT" 8 | repository = "https://github.com/specta-rs/rspc" 9 | documentation = "https://docs.rs/rspc-procedure" 10 | keywords = ["async", "specta", "rust-to-ts", "typescript", "typesafe"] 11 | categories = ["web-programming", "asynchronous"] 12 | 13 | # /bin/sh RUSTDOCFLAGS="--cfg docsrs" cargo +nightly doc --all-features 14 | [package.metadata."docs.rs"] 15 | all-features = true 16 | rustdoc-args = ["--cfg", "docsrs"] 17 | 18 | [dependencies] 19 | # Public 20 | futures-core = { workspace = true, default-features = false } 21 | serde = { workspace = true, default-features = false } 22 | 23 | # Private 24 | erased-serde = { workspace = true, default-features = false, features = [ 25 | "std", 26 | ] } 27 | pin-project-lite = { workspace = true, default-features = false } 28 | 29 | [lints] 30 | workspace = true 31 | -------------------------------------------------------------------------------- /crates/procedure/README.md: -------------------------------------------------------------------------------- 1 | # rspc Procedure 2 | 3 | [![docs.rs](https://img.shields.io/crates/v/rspc-procedure)](https://docs.rs/rspc-procedure) 4 | 5 | Interface for a single type-erased operation that the server can execute. 6 | -------------------------------------------------------------------------------- /crates/procedure/src/dyn_input.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | any::{type_name, Any}, 3 | fmt, 4 | }; 5 | 6 | use serde::{de::Error, Deserialize}; 7 | 8 | use crate::{DeserializeError, DowncastError, ProcedureError}; 9 | 10 | // It would be really nice if this with `&'a DynInput<'de>` but that would require `#[repr(transparent)]` with can only be constructed with unsafe which is probally not worth it. 11 | 12 | /// TODO 13 | pub struct DynInput<'a, 'de> { 14 | inner: Repr<'a, 'de>, 15 | pub(crate) type_name: &'static str, 16 | } 17 | 18 | enum Repr<'a, 'de> { 19 | Value(&'a mut (dyn Any + Send)), 20 | Deserializer(&'a mut (dyn erased_serde::Deserializer<'de> + Send)), 21 | } 22 | 23 | impl<'a, 'de> DynInput<'a, 'de> { 24 | // TODO: Discuss using `Option` as a workaround for ownership 25 | pub fn new_value(value: &'a mut T) -> Self { 26 | Self { 27 | inner: Repr::Value(value), 28 | type_name: type_name::(), 29 | } 30 | } 31 | 32 | // TODO: In a perfect world this would be public. 33 | pub(crate) fn new_deserializer + Send>( 34 | deserializer: &'a mut D, 35 | ) -> Self { 36 | Self { 37 | inner: Repr::Deserializer(deserializer), 38 | type_name: type_name::(), 39 | } 40 | } 41 | 42 | /// TODO 43 | pub fn deserialize>(self) -> Result { 44 | let Repr::Deserializer(deserializer) = self.inner else { 45 | return Err(ProcedureError::Deserialize(DeserializeError( 46 | erased_serde::Error::custom(format!( 47 | "attempted to deserialize from value '{}' but expected deserializer", 48 | self.type_name 49 | )), 50 | ))); 51 | }; 52 | 53 | erased_serde::deserialize(deserializer) 54 | .map_err(|err| ProcedureError::Deserialize(DeserializeError(err))) 55 | } 56 | 57 | /// TODO 58 | pub fn value(&mut self) -> Result<&mut T, ProcedureError> { 59 | let Repr::Value(ref mut value) = self.inner else { 60 | return Err(DowncastError { 61 | from: None, 62 | to: type_name::(), 63 | } 64 | .into()); 65 | }; 66 | Ok(value.downcast_mut::().ok_or(DowncastError { 67 | from: Some(self.type_name), 68 | to: type_name::(), 69 | })?) 70 | } 71 | } 72 | 73 | impl<'a, 'de> fmt::Debug for DynInput<'a, 'de> { 74 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 75 | todo!(); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /crates/procedure/src/dyn_output.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | any::{type_name, Any}, 3 | fmt, 4 | }; 5 | 6 | use serde::Serialize; 7 | 8 | use crate::ProcedureError; 9 | 10 | /// TODO 11 | pub struct DynOutput<'a> { 12 | inner: Repr<'a>, 13 | pub(crate) type_name: &'static str, 14 | } 15 | 16 | enum Repr<'a> { 17 | Serialize(&'a (dyn erased_serde::Serialize + Send + Sync)), 18 | Value(&'a mut (dyn Any + Send)), 19 | } 20 | 21 | // TODO: `Debug`, etc traits 22 | 23 | impl<'a> DynOutput<'a> { 24 | // TODO: We depend on the type of `T` can we either force it so this can be public? 25 | pub(crate) fn new_value(value: &'a mut T) -> Self { 26 | Self { 27 | inner: Repr::Value(value), 28 | type_name: type_name::(), 29 | } 30 | } 31 | 32 | pub fn new_serialize(value: &'a mut T) -> Self { 33 | Self { 34 | inner: Repr::Serialize(value), 35 | type_name: type_name::(), 36 | } 37 | } 38 | 39 | /// TODO 40 | pub fn as_serialize(self) -> Option { 41 | match self.inner { 42 | Repr::Serialize(v) => Some(v), 43 | Repr::Value(_) => None, 44 | } 45 | } 46 | 47 | /// TODO 48 | pub fn as_value(self) -> Option { 49 | match self.inner { 50 | Repr::Serialize(_) => None, 51 | Repr::Value(v) => v 52 | .downcast_mut::>>()? 53 | .take() 54 | .expect("unreachable") 55 | .ok(), 56 | } 57 | } 58 | } 59 | 60 | impl<'a> fmt::Debug for DynOutput<'a> { 61 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 62 | todo!(); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /crates/procedure/src/interop.rs: -------------------------------------------------------------------------------- 1 | //! TODO: A temporary module to allow for interop between modern and legacy code. 2 | 3 | // TODO: Remove this once we remove the legacy executor. 4 | #[doc(hidden)] 5 | #[derive(Clone)] 6 | pub struct LegacyErrorInterop(pub String); 7 | impl std::fmt::Debug for LegacyErrorInterop { 8 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 9 | write!(f, "LegacyErrorInterop({})", self.0) 10 | } 11 | } 12 | impl std::fmt::Display for LegacyErrorInterop { 13 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 14 | write!(f, "LegacyErrorInterop({})", self.0) 15 | } 16 | } 17 | impl std::error::Error for LegacyErrorInterop {} 18 | -------------------------------------------------------------------------------- /crates/procedure/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Interface for a single type-erased operation that the server can execute. 2 | //! 3 | //! HTTP integrations should prefer to depend on this crate instead of `rspc`. 4 | //! 5 | //! TODO: Describe all the types and why the split? 6 | //! TODO: This is kinda like `tower::Service` 7 | //! TODO: Why this crate doesn't depend on Specta. 8 | //! TODO: Discuss the traits that need to be layered on for this to be useful. 9 | //! TODO: Discuss how middleware don't exist here. 10 | //! 11 | //! TODO: Results must be `'static` because they have to escape the closure. 12 | #![forbid(unsafe_code)] 13 | #![cfg_attr(docsrs, feature(doc_cfg))] 14 | #![doc( 15 | html_logo_url = "https://github.com/specta-rs/rspc/raw/main/.github/logo.png", 16 | html_favicon_url = "https://github.com/specta-rs/rspc/raw/main/.github/logo.png" 17 | )] 18 | 19 | mod dyn_input; 20 | mod dyn_output; 21 | mod error; 22 | mod interop; 23 | mod logger; 24 | mod procedure; 25 | mod procedures; 26 | mod state; 27 | mod stream; 28 | 29 | pub use dyn_input::DynInput; 30 | pub use dyn_output::DynOutput; 31 | pub use error::{DeserializeError, DowncastError, ProcedureError, ResolverError}; 32 | #[doc(hidden)] 33 | pub use interop::LegacyErrorInterop; 34 | pub use procedure::Procedure; 35 | pub use procedures::Procedures; 36 | pub use state::State; 37 | pub use stream::{flush, ProcedureStream, ProcedureStreamMap}; 38 | -------------------------------------------------------------------------------- /crates/procedure/src/logger.rs: -------------------------------------------------------------------------------- 1 | // #[derive(Clone, Debug)] 2 | // #[non_exhaustive] 3 | // pub enum LogMessage<'a> { 4 | // Execute { name: &'a str }, // TODO: Give enough information to collect time metrics, etc. 5 | // SerializerError(&'a str), 6 | // Custom { reason: &'a str, message: &'a str }, 7 | // } 8 | 9 | // TODO: Clone `ProcedureError` instead of `Event`? 10 | -------------------------------------------------------------------------------- /crates/procedure/src/procedure.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | any::type_name, 3 | fmt, 4 | panic::{catch_unwind, AssertUnwindSafe}, 5 | sync::Arc, 6 | }; 7 | 8 | use serde::Deserializer; 9 | 10 | use crate::{DynInput, ProcedureError, ProcedureStream}; 11 | 12 | // TODO: Discuss cancellation safety 13 | 14 | /// a single type-erased operation that the server can execute. 15 | /// 16 | /// TODO: Show constructing and executing procedure. 17 | pub struct Procedure { 18 | handler: Arc ProcedureStream + Send + Sync>, 19 | 20 | #[cfg(debug_assertions)] 21 | handler_name: &'static str, 22 | } 23 | 24 | impl Procedure { 25 | pub fn new ProcedureStream + Send + Sync + 'static>( 26 | handler: F, 27 | ) -> Self { 28 | Self { 29 | handler: Arc::new(handler), 30 | #[cfg(debug_assertions)] 31 | handler_name: type_name::(), 32 | } 33 | } 34 | 35 | pub fn exec(&self, ctx: TCtx, input: DynInput) -> ProcedureStream { 36 | let (Ok(v) | Err(v)) = catch_unwind(AssertUnwindSafe(|| (self.handler)(ctx, input))) 37 | .map_err(|err| ProcedureError::Unwind(err).into()); 38 | v 39 | } 40 | 41 | pub fn exec_with_deserializer<'de, D: Deserializer<'de> + Send>( 42 | &self, 43 | ctx: TCtx, 44 | input: D, 45 | ) -> ProcedureStream { 46 | let mut deserializer = ::erase(input); 47 | let value = DynInput::new_deserializer(&mut deserializer); 48 | 49 | let (Ok(v) | Err(v)) = catch_unwind(AssertUnwindSafe(|| (self.handler)(ctx, value))) 50 | .map_err(|err| ProcedureError::Unwind(err).into()); 51 | v 52 | } 53 | } 54 | 55 | impl Clone for Procedure { 56 | fn clone(&self) -> Self { 57 | Self { 58 | handler: self.handler.clone(), 59 | #[cfg(debug_assertions)] 60 | handler_name: self.handler_name, 61 | } 62 | } 63 | } 64 | 65 | impl fmt::Debug for Procedure { 66 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 67 | let mut t = f.debug_tuple("Procedure"); 68 | #[cfg(debug_assertions)] 69 | let t = t.field(&self.handler_name); 70 | t.finish() 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /crates/procedure/src/procedures.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | borrow::Cow, 3 | collections::HashMap, 4 | fmt, 5 | ops::{Deref, DerefMut}, 6 | sync::Arc, 7 | }; 8 | 9 | use crate::{Procedure, State}; 10 | 11 | pub struct Procedures { 12 | // TODO: Probally `Arc` around map and share that with `State`? 13 | procedures: HashMap, Procedure>, 14 | state: Arc, 15 | } 16 | 17 | impl Procedures { 18 | // TODO: Work out this API. I'm concerned how `rspc_devtools` and `rspc_tracing` fit into this. 19 | // TODO: Also accept `Into` maybe? 20 | pub fn new(procedures: HashMap, Procedure>, state: Arc) -> Self { 21 | Self { procedures, state } 22 | } 23 | 24 | pub fn state(&self) -> &Arc { 25 | &self.state 26 | } 27 | } 28 | 29 | // TODO: Should this come back?? `State` makes it rough. 30 | // impl From, Procedure>> for Procedures { 31 | // fn from(procedures: HashMap, Procedure>) -> Self { 32 | // Self { 33 | // procedures: procedures.into_iter().map(|(k, v)| (k.into(), v)).collect(), 34 | // } 35 | // } 36 | // } 37 | 38 | impl Clone for Procedures { 39 | fn clone(&self) -> Self { 40 | Self { 41 | procedures: self.procedures.clone(), 42 | state: self.state.clone(), 43 | } 44 | } 45 | } 46 | 47 | impl Into> for &Procedures { 48 | fn into(self) -> Procedures { 49 | self.clone() 50 | } 51 | } 52 | 53 | impl fmt::Debug for Procedures { 54 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 55 | f.debug_map().entries(self.procedures.iter()).finish() 56 | } 57 | } 58 | 59 | impl IntoIterator for Procedures { 60 | type Item = (Cow<'static, str>, Procedure); 61 | type IntoIter = std::collections::hash_map::IntoIter, Procedure>; 62 | 63 | fn into_iter(self) -> Self::IntoIter { 64 | self.procedures.into_iter() 65 | } 66 | } 67 | 68 | // impl FromIterator<(Cow<'static, str>, Procedure)> for Procedures { 69 | // fn from_iter, Procedure)>>(iter: I) -> Self { 70 | // Self(iter.into_iter().collect()) 71 | // } 72 | // } 73 | 74 | // TODO: Is `Deref` okay for this usecase? 75 | impl Deref for Procedures { 76 | type Target = HashMap, Procedure>; 77 | 78 | fn deref(&self) -> &Self::Target { 79 | &self.procedures 80 | } 81 | } 82 | 83 | impl DerefMut for Procedures { 84 | fn deref_mut(&mut self) -> &mut Self::Target { 85 | &mut self.procedures 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /crates/procedure/src/state.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | any::{Any, TypeId}, 3 | collections::HashMap, 4 | fmt, 5 | hash::{BuildHasherDefault, Hasher}, 6 | }; 7 | 8 | /// A hasher for `TypeId`s that takes advantage of its known characteristics. 9 | /// 10 | /// Author of `anymap` crate has done research on this topic: 11 | /// https://github.com/chris-morgan/anymap/blob/2e9a5704/src/lib.rs#L599 12 | #[derive(Debug, Default)] 13 | struct NoOpHasher(u64); 14 | 15 | impl Hasher for NoOpHasher { 16 | fn write(&mut self, _bytes: &[u8]) { 17 | unimplemented!("This NoOpHasher can only handle u64s") 18 | } 19 | 20 | fn write_u64(&mut self, i: u64) { 21 | self.0 = i; 22 | } 23 | 24 | fn finish(&self) -> u64 { 25 | self.0 26 | } 27 | } 28 | 29 | pub struct State( 30 | HashMap, BuildHasherDefault>, 31 | ); 32 | 33 | impl fmt::Debug for State { 34 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 35 | f.debug_tuple("State").field(&self.0.keys()).finish() 36 | } 37 | } 38 | 39 | impl Default for State { 40 | fn default() -> Self { 41 | Self(Default::default()) 42 | } 43 | } 44 | 45 | impl State { 46 | pub fn get(&self) -> Option<&T> { 47 | self.0.get(&TypeId::of::()).map(|v| { 48 | v.downcast_ref::() 49 | .expect("unreachable: TypeId matches but downcast failed") 50 | }) 51 | } 52 | 53 | pub fn get_mut(&self) -> Option<&T> { 54 | self.0.get(&TypeId::of::()).map(|v| { 55 | v.downcast_ref::() 56 | .expect("unreachable: TypeId matches but downcast failed") 57 | }) 58 | } 59 | 60 | pub fn get_or_init(&mut self, init: impl FnOnce() -> T) -> &T { 61 | self.0 62 | .entry(TypeId::of::()) 63 | .or_insert_with(|| Box::new(init())) 64 | .downcast_ref::() 65 | .expect("unreachable: TypeId matches but downcast failed") 66 | } 67 | 68 | pub fn get_mut_or_init( 69 | &mut self, 70 | init: impl FnOnce() -> T, 71 | ) -> &mut T { 72 | self.0 73 | .entry(TypeId::of::()) 74 | .or_insert_with(|| Box::new(init())) 75 | .downcast_mut::() 76 | .expect("unreachable: TypeId matches but downcast failed") 77 | } 78 | 79 | pub fn contains_key(&self) -> bool { 80 | self.0.contains_key(&TypeId::of::()) 81 | } 82 | 83 | pub fn insert(&mut self, t: T) { 84 | self.0.insert(TypeId::of::(), Box::new(t)); 85 | } 86 | 87 | pub fn remove(&mut self) -> Option { 88 | self.0.remove(&TypeId::of::()).map(|v| { 89 | *v.downcast::() 90 | .expect("unreachable: TypeId matches but downcast failed") 91 | }) 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /crates/tracing/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rspc-tracing" 3 | version = "0.0.0" 4 | edition = "2021" 5 | publish = false # TODO: Crate metadata & publish 6 | 7 | [dependencies] 8 | rspc = { path = "../../rspc" } 9 | tracing = { workspace = true } 10 | futures = { workspace = true } 11 | tracing-futures = "0.2.5" 12 | 13 | # /bin/sh RUSTDOCFLAGS="--cfg docsrs" cargo +nightly doc --all-features 14 | [package.metadata."docs.rs"] 15 | all-features = true 16 | rustdoc-args = ["--cfg", "docsrs"] 17 | 18 | [lints] 19 | workspace = true 20 | -------------------------------------------------------------------------------- /crates/tracing/README.md: -------------------------------------------------------------------------------- 1 | # rspc tracing 2 | 3 | [![docs.rs](https://img.shields.io/crates/v/rspc-tracing)](https://docs.rs/rspc-tracing) 4 | 5 | > [!CAUTION] 6 | > This crate is still a work in progress. You can use it but we can't guarantee that it's API won't change. 7 | 8 | Support for [tracing](https://github.com/tokio-rs/tracing) with rspc to collect detailed span information. 9 | 10 | ## Example 11 | 12 | ```rust 13 | // Coming soon... 14 | ``` 15 | -------------------------------------------------------------------------------- /crates/tracing/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! rspc-tracing: Tracing support for rspc 2 | #![forbid(unsafe_code)] 3 | #![cfg_attr(docsrs, feature(doc_cfg))] 4 | #![doc( 5 | html_logo_url = "https://github.com/specta-rs/rspc/blob/main/.github/logo.png?raw=true", 6 | html_favicon_url = "https://github.com/specta-rs/rspc/blob/main/.github/logo.png?raw=true" 7 | )] 8 | 9 | use std::{fmt, marker::PhantomData}; 10 | 11 | use rspc::middleware::Middleware; 12 | use tracing::info; 13 | 14 | mod traceable; 15 | 16 | pub use traceable::{DebugMarker, StreamMarker, Traceable}; 17 | use tracing_futures::Instrument; 18 | 19 | // TODO: Support for Prometheus metrics and structured logging 20 | 21 | // TODO: Capturing serialization errors in `rspc-axum` 22 | 23 | /// TODO 24 | pub fn tracing() -> Middleware 25 | where 26 | TError: fmt::Debug + Send + 'static, 27 | TCtx: Send + 'static, 28 | TInput: fmt::Debug + Send + 'static, 29 | TResult: Traceable + Send + 'static, 30 | { 31 | Middleware::new(|ctx, input, next| { 32 | let span = tracing::info_span!( 33 | "", 34 | "{} {}", 35 | next.meta().kind().to_string().to_uppercase(), // TODO: Maybe adding color? 36 | next.meta().name() 37 | ); 38 | 39 | async move { 40 | let input_str = format!("{input:?}"); 41 | let start = std::time::Instant::now(); 42 | let result = next.exec(ctx, input).await; 43 | info!( 44 | "took {:?} with input {input_str:?} and returned {:?}", 45 | start.elapsed(), 46 | DebugWrapper(&result, PhantomData::) 47 | ); 48 | 49 | result 50 | } 51 | .instrument(span) 52 | }) 53 | } 54 | 55 | struct DebugWrapper<'a, T: Traceable, TErr, M>(&'a Result, PhantomData); 56 | 57 | impl<'a, T: Traceable, TErr: fmt::Debug, M> fmt::Debug for DebugWrapper<'a, T, TErr, M> { 58 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 59 | match &self.0 { 60 | Ok(v) => v.fmt(f), 61 | Err(e) => write!(f, "{e:?}"), 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /crates/tracing/src/traceable.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | pub trait Traceable { 4 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result; 5 | } 6 | 7 | #[doc(hidden)] 8 | pub enum DebugMarker {} 9 | impl Traceable for T { 10 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 11 | self.fmt(f) 12 | } 13 | } 14 | 15 | #[doc(hidden)] 16 | pub enum StreamMarker {} 17 | // `rspc::Stream: !Debug` so the marker will never overlap 18 | impl Traceable for rspc::Stream 19 | where 20 | S: futures::Stream, 21 | S::Item: fmt::Debug, 22 | { 23 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 24 | todo!(); // TODO: Finish this 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /crates/validator/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rspc-validator" 3 | version = "0.0.0" 4 | edition = "2021" 5 | publish = false # TODO: Crate metadata & publish 6 | 7 | [dependencies] 8 | rspc = { path = "../../rspc" } 9 | serde = { workspace = true } 10 | specta = { workspace = true } 11 | validator = "0.20" 12 | 13 | # /bin/sh RUSTDOCFLAGS="--cfg docsrs" cargo +nightly doc --all-features 14 | [package.metadata."docs.rs"] 15 | all-features = true 16 | rustdoc-args = ["--cfg", "docsrs"] 17 | 18 | [lints] 19 | workspace = true 20 | -------------------------------------------------------------------------------- /crates/validator/README.md: -------------------------------------------------------------------------------- 1 | # rspc Validator 2 | 3 | [![docs.rs](https://img.shields.io/crates/v/rspc-validator)](https://docs.rs/rspc-validator) 4 | 5 | > [!CAUTION] 6 | > This crate is still a work in progress. You can use it but we can't guarantee that it's API won't change. 7 | 8 | Support for [validator](https://docs.rs/validator) with rspc for easy input validation. 9 | 10 | Features: 11 | - Return validator errors to the client. 12 | - JavaScript client for typesafe parsing of errors. // TODO 13 | 14 | ## Example 15 | 16 | ```rust 17 | // Coming soon... 18 | ``` 19 | -------------------------------------------------------------------------------- /crates/validator/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Support for [`validator`] with [`rspc`] for easy input validation. 2 | #![forbid(unsafe_code)] 3 | #![cfg_attr(docsrs, feature(doc_cfg))] 4 | #![doc( 5 | html_logo_url = "https://github.com/specta-rs/rspc/blob/main/.github/logo.png?raw=true", 6 | html_favicon_url = "https://github.com/specta-rs/rspc/blob/main/.github/logo.png?raw=true" 7 | )] 8 | 9 | use std::fmt; 10 | 11 | use rspc::middleware::Middleware; 12 | use serde::{ser::SerializeStruct, Serialize}; 13 | use specta::{datatype::DataType, Type}; 14 | use validator::{Validate, ValidationErrors}; 15 | 16 | /// TODO 17 | pub fn validate() -> Middleware 18 | where 19 | TError: From + Send + 'static, 20 | TCtx: Send + 'static, 21 | TInput: Validate + Send + 'static, 22 | TResult: Send + 'static, 23 | { 24 | Middleware::new(|ctx, input: TInput, next| async move { 25 | match input.validate() { 26 | Ok(()) => next.exec(ctx, input).await, 27 | Err(err) => Err(RspcValidatorError(err).into()), 28 | } 29 | }) 30 | } 31 | 32 | #[derive(Clone)] 33 | pub struct RspcValidatorError(ValidationErrors); 34 | 35 | impl fmt::Debug for RspcValidatorError { 36 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 37 | write!(f, "{:?}", self.0) 38 | } 39 | } 40 | 41 | impl fmt::Display for RspcValidatorError { 42 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 43 | write!(f, "{:?}", self.0) 44 | } 45 | } 46 | 47 | impl std::error::Error for RspcValidatorError {} 48 | 49 | impl Serialize for RspcValidatorError { 50 | fn serialize(&self, serializer: S) -> Result 51 | where 52 | S: serde::Serializer, 53 | { 54 | let mut s = serializer.serialize_struct("RspcValidatorError", 2)?; 55 | s.serialize_field("~rspc.validator", &true)?; 56 | s.serialize_field("errors", &self.0.field_errors())?; 57 | s.end() 58 | } 59 | } 60 | 61 | // TODO: Proper implementation 62 | impl Type for RspcValidatorError { 63 | fn inline( 64 | _type_map: &mut specta::TypeCollection, 65 | _generics: specta::Generics, 66 | ) -> specta::datatype::DataType { 67 | DataType::Any 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /crates/zer/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rspc-zer" 3 | version = "0.0.0" 4 | edition = "2021" 5 | publish = false # TODO: Crate metadata & publish 6 | 7 | [dependencies] 8 | rspc = { path = "../../rspc" } 9 | serde = { workspace = true } 10 | specta = { workspace = true } 11 | serde_json = { workspace = true } 12 | jsonwebtoken = { version = "9", default-features = false } 13 | cookie = { version = "0.18.1", features = ["percent-encode"] } 14 | 15 | # /bin/sh RUSTDOCFLAGS="--cfg docsrs" cargo +nightly doc --all-features 16 | [package.metadata."docs.rs"] 17 | all-features = true 18 | rustdoc-args = ["--cfg", "docsrs"] 19 | 20 | [lints] 21 | workspace = true 22 | -------------------------------------------------------------------------------- /crates/zer/README.md: -------------------------------------------------------------------------------- 1 | # rspc Zer 2 | 3 | [![docs.rs](https://img.shields.io/crates/v/rspc-zer)](https://docs.rs/rspc-zer) 4 | 5 | > [!CAUTION] 6 | > This crate is still a work in progress. You can use it but we can't guarantee that it's API won't change. 7 | 8 | Authorization library for rspc. 9 | 10 | Features: 11 | - Secure session managemen with short-lived JWT tokens. 12 | - OAuth maybe? 13 | 14 | ## Example 15 | 16 | ```rust 17 | // Coming soon... 18 | ``` 19 | -------------------------------------------------------------------------------- /examples/anyhow/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "example-anyhow" 3 | version = "0.0.0" 4 | edition = "2021" 5 | publish = false 6 | 7 | [dependencies] 8 | rspc = { path = "../../rspc", features = ["typescript"] } 9 | specta.workspace = true 10 | anyhow = "1.0.95" 11 | thiserror = "2.0.11" 12 | 13 | [lints] 14 | workspace = true 15 | -------------------------------------------------------------------------------- /examples/anyhow/bindings.ts: -------------------------------------------------------------------------------- 1 | // This file was generated by [rspc](https://github.com/specta-rs/rspc). Do not edit this file manually. 2 | 3 | export type Procedures = { 4 | anyhow: { kind: "query", input: any, output: any, error: any }, 5 | } -------------------------------------------------------------------------------- /examples/anyhow/src/main.rs: -------------------------------------------------------------------------------- 1 | use rspc::{Procedure, Router}; 2 | use specta::Type; 3 | 4 | //////////////////////////////////////////////////////////////////////////////////////////////////// 5 | 6 | fn main() { 7 | let (_procedures, _types) = Router::new() 8 | .procedure( 9 | // 10 | "anyhow", 11 | Procedure::builder().query(anyhow_procedure), 12 | ) 13 | .build() 14 | .expect("router should be built"); 15 | } 16 | 17 | // Some procedure that needs `anyhow::Error` to be compatible with `rspc`. 18 | async fn anyhow_procedure(_ctx: (), _input: ()) -> Result { 19 | let response = fallible()?; // `?` converts `anyhow::Error` into `AnyhowError`. 20 | Ok(response) 21 | } 22 | 23 | fn fallible() -> Result { 24 | anyhow::bail!("oh no!") 25 | } 26 | 27 | //////////////////////////////////////////////////////////////////////////////////////////////////// 28 | 29 | // Make `anyhow::Error` work where `std::error::Error + Send + 'static` is expected. 30 | // NB: Define this only once; afterwards, you can import and use it anywhere. 31 | // See: https://github.com/dtolnay/anyhow/issues/153#issuecomment-833718851 32 | #[derive(Debug, thiserror::Error, Type)] 33 | #[error(transparent)] 34 | struct AnyhowError( 35 | #[from] 36 | #[serde(skip)] 37 | anyhow::Error, 38 | ); 39 | 40 | impl rspc::Error for AnyhowError { 41 | fn into_procedure_error(self) -> rspc::ProcedureError { 42 | let message = format!("something bad happened: {}", self); 43 | rspc::ResolverError::new(message, Some(self)).into() 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /examples/astro/.astro/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "_variables": { 3 | "lastUpdateCheck": 1738052413768 4 | } 5 | } -------------------------------------------------------------------------------- /examples/astro/.astro/types.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /examples/astro/astro.config.mjs: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "astro/config"; 2 | 3 | import react from "@astrojs/react"; 4 | import solid from "@astrojs/solid-js"; 5 | 6 | export default defineConfig({ 7 | integrations: [ 8 | react(), 9 | solid({ 10 | exclude: "**/react.tsx", 11 | }), 12 | ], 13 | }); 14 | -------------------------------------------------------------------------------- /examples/astro/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@rspc/examples-astro", 3 | "version": "0.0.0", 4 | "description": "A project to test the RSPC frontend libraries", 5 | "keywords": [], 6 | "author": "Oscar Beaumont", 7 | "license": "MIT", 8 | "private": true, 9 | "scripts": { 10 | "dev": "astro dev" 11 | }, 12 | "dependencies": { 13 | "@rspc/client": "workspace:*", 14 | "@rspc/react-query": "workspace:*", 15 | "@rspc/solid-query": "workspace:*", 16 | "@tanstack/react-query": "^5.66.0", 17 | "@tanstack/solid-query": "^5.66.0", 18 | "astro": "5.2.5", 19 | "react": "^19.0.0", 20 | "react-dom": "^19.0.0", 21 | "solid-js": "^1.9.4" 22 | }, 23 | "devDependencies": { 24 | "@astrojs/react": "^4.2.0", 25 | "@astrojs/solid-js": "^5.0.4", 26 | "@types/react": "^19.0.8" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /examples/astro/src/components/playground.ts: -------------------------------------------------------------------------------- 1 | // TODO: Remove this in the future 2 | 3 | import { Procedures } from "../../../bindings"; 4 | 5 | function createProxy(): { [K in keyof T]: () => T[K] } { 6 | return undefined as any; 7 | } 8 | 9 | const procedures = createProxy(); 10 | 11 | procedures.version(); 12 | 13 | procedures.newstuff(); 14 | -------------------------------------------------------------------------------- /examples/astro/src/components/solid.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource solid-js */ 2 | 3 | import { createClient, FetchTransport } from "@rspc/client"; 4 | import { createSolidQueryHooks } from "@rspc/solid-query"; 5 | import { QueryClient } from "@tanstack/solid-query"; 6 | 7 | // Export from Rust. Run `cargo run -p example-axum` to start server and export it! 8 | import { Procedures } from "../../../bindings"; 9 | 10 | const fetchQueryClient = new QueryClient(); 11 | const fetchClient = createClient({ 12 | transport: new FetchTransport("http://localhost:4000/rspc"), 13 | }); 14 | 15 | export const rspc = createSolidQueryHooks(); 16 | 17 | function Example() { 18 | const echo = rspc.createQuery(() => ["echo", "somevalue"]); 19 | const sendMsg = rspc.createMutation(() => "sendMsg"); 20 | 21 | sendMsg.mutate("Sending"); 22 | 23 | return ( 24 |
25 |

SolidJS

26 |

{echo.data}

27 | {/* TODO: Finish SolidJS example */} 28 |
29 | ); 30 | } 31 | 32 | function App() { 33 | return ( 34 | 35 | 36 | 37 | ); 38 | } 39 | 40 | export default App; 41 | -------------------------------------------------------------------------------- /examples/astro/src/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// -------------------------------------------------------------------------------- /examples/astro/src/pages/index.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import ReactComponent from "../components/react"; 3 | import SolidComponent from "../components/solid"; 4 | --- 5 | 6 | 7 | 8 | 9 | 10 | RSPC Example with Astro 11 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /examples/astro/test/client.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | inferProcedures, 3 | inferMutationInput, 4 | inferMutationResult, 5 | inferQueryInput, 6 | inferQueryResult, 7 | inferSubscriptionInput, 8 | inferSubscriptionResult, 9 | NoOpTransport, 10 | createClient, 11 | inferInfiniteQueries, 12 | inferInfiniteQueryResult, 13 | inferInfiniteQueryInput, 14 | } from "@rspc/client"; 15 | import { createReactQueryHooks } from "@rspc/react-query"; 16 | import { MyPaginatedData, Procedures } from "./bindings"; 17 | 18 | export const rspc = createReactQueryHooks(); 19 | 20 | const client = createClient({ 21 | transport: new NoOpTransport(), 22 | }); 23 | 24 | function assert(t: T) {} 25 | 26 | // inferProcedureOutput 27 | type A1 = inferProcedures; 28 | assert(undefined as unknown as Procedures); 29 | type B1 = inferProcedures; 30 | assert(undefined as unknown as Procedures); 31 | type C1 = inferProcedures; 32 | assert(undefined as unknown as Procedures); 33 | 34 | // inferQuery* 35 | type A2 = inferQueryResult; 36 | assert(undefined as unknown as string); 37 | type B2 = inferQueryInput; 38 | assert(undefined as unknown as never); 39 | type C2 = inferQueryResult; 40 | assert(undefined as unknown as number); 41 | type D2 = inferQueryInput; 42 | assert(undefined as unknown as number); 43 | 44 | // inferMutation* 45 | type A3 = inferMutationResult; 46 | assert(undefined as unknown as string); 47 | type B3 = inferMutationInput; 48 | assert(undefined as unknown as never); 49 | type C3 = inferMutationResult; 50 | assert(undefined as unknown as number); 51 | type D3 = inferMutationInput; 52 | assert(undefined as unknown as number); 53 | 54 | // inferSubscriptions* 55 | type A4 = inferSubscriptionResult; 56 | assert(undefined as unknown as string); 57 | type B4 = inferSubscriptionInput; 58 | assert(undefined as unknown as never); 59 | type C4 = inferSubscriptionResult; 60 | assert(undefined as unknown as boolean); 61 | type D4 = inferSubscriptionInput; 62 | assert(undefined as unknown as boolean); 63 | 64 | // inferInfiniteQuery 65 | type A5 = inferInfiniteQueries["key"]; 66 | assert( 67 | undefined as unknown as 68 | | "paginatedQueryOnlyCursor" 69 | | "paginatedQueryCursorAndArg", 70 | ); 71 | type B5 = inferInfiniteQueryResult; 72 | assert(undefined as unknown as MyPaginatedData); 73 | type C5 = inferInfiniteQueryInput; 74 | assert(undefined as unknown as never); 75 | type D5 = inferInfiniteQueryResult; 76 | assert(undefined as unknown as MyPaginatedData); 77 | type E5 = inferInfiniteQueryInput; 78 | assert(undefined as unknown as { my_param: number }); 79 | -------------------------------------------------------------------------------- /examples/astro/test/index.md: -------------------------------------------------------------------------------- 1 | # Rust/Typescript End to End Tests 2 | 3 | The files in this folder are typechecked in a Rust unit test to ensure all of the types are inferring correctly from end to end. -------------------------------------------------------------------------------- /examples/astro/test/react.test.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { RSPCError, Client, createClient, NoOpTransport } from "@rspc/client"; 3 | import { createReactQueryHooks } from "@rspc/react-query"; 4 | import { QueryClient } from "@tanstack/react-query"; 5 | import { Procedures } from "./bindings"; 6 | 7 | export const rspc = createReactQueryHooks(); 8 | 9 | function assert(t: T) {} 10 | 11 | // useContext 12 | assert>(rspc.useContext().client); 13 | 14 | // useQuery 15 | const { data, error } = rspc.useQuery(["noArgQuery"], { 16 | onSuccess(data) { 17 | assert(data); 18 | }, 19 | onError(err) { 20 | assert(err); 21 | }, 22 | }); 23 | assert(data); 24 | assert(error); 25 | 26 | const { data: data2, error: error2 } = rspc.useQuery(["singleArgQuery", 42], { 27 | onSuccess(data) { 28 | assert(data); 29 | }, 30 | onError(err) { 31 | assert(err); 32 | }, 33 | }); 34 | assert(data2); 35 | assert(error2); 36 | 37 | // useInfiniteQuery 38 | // TODO 39 | // rspc.useInfiniteQuery(["paginatedQueryOnlyCursor"]); 40 | // rspc.useInfiniteQuery(["paginatedQueryCursorAndArg", { my_param: 42 }]); 41 | 42 | // useMutation 43 | const { 44 | mutate, 45 | error: error3, 46 | data: data3, 47 | } = rspc.useMutation("noArgMutation", { 48 | onSuccess(data) { 49 | assert(data); 50 | }, 51 | onError(err) { 52 | assert(err); 53 | }, 54 | }); 55 | mutate(undefined); 56 | assert(error3); 57 | assert(data3); 58 | 59 | const { 60 | mutate: mutate2, 61 | error: error4, 62 | data: data4, 63 | } = rspc.useMutation("singleArgMutation", { 64 | onSuccess(data) { 65 | assert(data); 66 | }, 67 | onError(err) { 68 | assert(err); 69 | }, 70 | }); 71 | mutate2(42); 72 | assert(error4); 73 | assert(data4); 74 | 75 | // useSubscription 76 | rspc.useSubscription(["noArgSubscription"], { 77 | onStarted() {}, 78 | onData(data) { 79 | assert(data); 80 | }, 81 | onError(err) { 82 | assert(err); 83 | }, 84 | enabled: false, 85 | }); 86 | 87 | rspc.useSubscription(["singleArgSubscription", true], { 88 | onStarted() {}, 89 | onData(data) { 90 | assert(data); 91 | }, 92 | onError(err) { 93 | assert(err); 94 | }, 95 | enabled: false, 96 | }); 97 | 98 | // Provider 99 | const queryClient = new QueryClient(); 100 | const client = createClient({ 101 | transport: new NoOpTransport(), 102 | }); 103 | 104 | function NoChildrenWithProvider() { 105 | return ( 106 |
107 | 108 |
109 | ); 110 | } 111 | 112 | function ChildrenWithProvider() { 113 | return ( 114 |
115 | 116 |

My App

117 |
118 |
119 | ); 120 | } 121 | -------------------------------------------------------------------------------- /examples/astro/test/solid.test.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource solid-js */ 2 | import { RSPCError, Client, createClient, NoOpTransport } from "@rspc/client"; 3 | import { createSolidQueryHooks } from "@rspc/solid-query"; 4 | import { QueryClient } from "@tanstack/solid-query"; 5 | import { Procedures } from "./bindings"; 6 | 7 | export const rspc = createSolidQueryHooks(); 8 | 9 | function assert(t: T) {} 10 | 11 | // createContext 12 | assert>(rspc.useContext().client); 13 | 14 | // createQuery 15 | const { data, error } = rspc.createQuery(() => ["noArgQuery"], { 16 | onSuccess(data) { 17 | assert(data); 18 | }, 19 | onError(err) { 20 | assert(err); 21 | }, 22 | }); 23 | assert(data); 24 | assert(error); 25 | 26 | const { data: data2, error: error2 } = rspc.createQuery( 27 | () => ["singleArgQuery", 42], 28 | { 29 | onSuccess(data) { 30 | assert(data); 31 | }, 32 | onError(err) { 33 | assert(err); 34 | }, 35 | }, 36 | ); 37 | assert(data2); 38 | assert(error2); 39 | 40 | // createInfiniteQuery 41 | // TODO 42 | // rspc.createInfiniteQuery(["paginatedQueryOnlyCursor"]); 43 | // rspc.createInfiniteQuery(["paginatedQueryCursorAndArg", { my_param: 42 }]); 44 | 45 | // createMutation 46 | const { 47 | mutate, 48 | error: error3, 49 | data: data3, 50 | } = rspc.createMutation("noArgMutation", { 51 | onSuccess(data) { 52 | assert(data); 53 | }, 54 | onError(err) { 55 | assert(err); 56 | }, 57 | }); 58 | mutate(undefined); 59 | assert(error3); 60 | assert(data3); 61 | 62 | const { 63 | mutate: mutate2, 64 | error: error4, 65 | data: data4, 66 | } = rspc.createMutation("singleArgMutation", { 67 | onSuccess(data) { 68 | assert(data); 69 | }, 70 | onError(err) { 71 | assert(err); 72 | }, 73 | }); 74 | mutate2(42); 75 | assert(error4); 76 | assert(data4); 77 | 78 | // createSubscription 79 | // rspc.createSubscription(() => ["noArgSubscription"], { 80 | // onStarted() {}, 81 | // onData(data) { 82 | // assert(data); 83 | // }, 84 | // onError(err) { 85 | // assert(err); 86 | // }, 87 | // enabled: false, 88 | // }); 89 | 90 | // rspc.createSubscription(() => ["singleArgSubscription", true], { 91 | // onStarted() {}, 92 | // onData(data) { 93 | // assert(data); 94 | // }, 95 | // onError(err) { 96 | // assert(err); 97 | // }, 98 | // enabled: false, 99 | // }); 100 | 101 | // Provider 102 | const queryClient = new QueryClient(); 103 | const client = createClient({ 104 | transport: new NoOpTransport(), 105 | }); 106 | 107 | function NoChildrenWithProvider() { 108 | return ( 109 |
110 | 111 |
112 | ); 113 | } 114 | 115 | function ChildrenWithProvider() { 116 | return ( 117 |
118 | 119 |

My App

120 |
121 |
122 | ); 123 | } 124 | -------------------------------------------------------------------------------- /examples/astro/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../packages/tsconfig.json", 3 | "compilerOptions": { 4 | "esModuleInterop": true, 5 | "noEmit": true, 6 | "emitDeclarationOnly": false, 7 | "jsx": "preserve", 8 | "lib": ["ES2017", "DOM"] 9 | }, 10 | "include": ["src"] 11 | } 12 | -------------------------------------------------------------------------------- /examples/axum/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "example-axum" 3 | version = "0.0.0" 4 | edition = "2021" 5 | publish = false 6 | 7 | [dependencies] 8 | rspc = { path = "../../rspc", features = ["typescript", "rust"] } 9 | rspc-axum = { path = "../../integrations/axum", features = ["ws"] } 10 | rspc-devtools = { version = "0.0.0", path = "../../crates/devtools" } 11 | rspc-invalidation = { version = "0.0.0", path = "../../crates/invalidation" } 12 | rspc-zer = { version = "0.0.0", path = "../../crates/zer" } 13 | example-core = { path = "../core" } 14 | 15 | tokio = { version = "1.43.0", features = ["full"] } 16 | axum = { version = "0.8.1", features = ["multipart"] } 17 | tower-http = { version = "0.6.2", default-features = false, features = [ 18 | "cors", 19 | ] } 20 | futures = "0.3" 21 | serde_json = "1.0.138" 22 | streamunordered = "0.5.4" 23 | serde.workspace = true 24 | -------------------------------------------------------------------------------- /examples/axum/src/main.rs: -------------------------------------------------------------------------------- 1 | use axum::{ 2 | body::Body, 3 | extract::Multipart, 4 | http::{header, request::Parts, HeaderMap}, 5 | routing::{get, post}, 6 | }; 7 | use example_core::{mount, Ctx}; 8 | use futures::{Stream, StreamExt}; 9 | use rspc::{DynOutput, ProcedureError, ProcedureStream, ProcedureStreamMap, Procedures}; 10 | use rspc_invalidation::Invalidator; 11 | use serde_json::Value; 12 | use std::{ 13 | convert::Infallible, 14 | future::poll_fn, 15 | path::PathBuf, 16 | pin::Pin, 17 | task::{Context, Poll}, 18 | }; 19 | use streamunordered::{StreamUnordered, StreamYield}; 20 | use tower_http::cors::{Any, CorsLayer}; 21 | 22 | #[tokio::main] 23 | async fn main() { 24 | let router = mount(); 25 | let (procedures, types) = router.build().unwrap(); 26 | 27 | rspc::Typescript::default() 28 | // .formatter(specta_typescript::formatter::prettier) 29 | .header("// My custom header") 30 | // .enable_source_maps() // TODO: Fix this 31 | .export_to( 32 | PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../bindings.ts"), 33 | &types, 34 | ) 35 | .unwrap(); 36 | 37 | // Be aware this is very experimental and doesn't support many types yet. 38 | // rspc::Rust::default() 39 | // // .header("// My custom header") 40 | // .export_to( 41 | // PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../client/src/bindings.rs"), 42 | // &types, 43 | // ) 44 | // .unwrap(); 45 | 46 | // let procedures = rspc_devtools::mount(procedures, &types); // TODO 47 | 48 | // We disable CORS because this is just an example. DON'T DO THIS IN PRODUCTION! 49 | let cors = CorsLayer::new() 50 | .allow_methods(Any) 51 | .allow_headers(Any) 52 | .allow_origin(Any); 53 | 54 | let app = axum::Router::new() 55 | .route("/", get(|| async { "rspc 🤝 Axum!" })) 56 | .nest( 57 | "/rspc", 58 | rspc_axum::endpoint(procedures, |parts: Parts| { 59 | println!("Client requested operation '{}'", parts.uri.path()); 60 | Ctx {} 61 | }), 62 | ) 63 | .layer(cors); 64 | 65 | let addr = "[::]:4000".parse::().unwrap(); // This listens on IPv6 and IPv4 66 | println!("listening on http://{}/rspc/version", addr); 67 | axum::serve(tokio::net::TcpListener::bind(addr).await.unwrap(), app) 68 | .await 69 | .unwrap(); 70 | } 71 | -------------------------------------------------------------------------------- /examples/binario/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "example-binario" 3 | version = "0.0.0" 4 | edition = "2021" 5 | publish = false 6 | 7 | [dependencies] 8 | rspc = { path = "../../rspc", features = ["typescript", "rust"] } 9 | rspc-binario = { version = "0.0.0", path = "../../crates/binario" } 10 | specta = { workspace = true, features = ["derive"] } 11 | tokio = { version = "1.43.0", features = ["full"] } 12 | axum = { version = "0.8.1", features = ["multipart"] } 13 | tower-http = { version = "0.6.2", default-features = false, features = [ 14 | "cors", 15 | ] } 16 | futures = "0.3" 17 | form_urlencoded = "1.2.1" 18 | tokio-util = { version = "0.7.13", features = ["compat"] } 19 | binario = "0.0.3" 20 | pin-project = "1.1.9" 21 | -------------------------------------------------------------------------------- /examples/binario/client.ts: -------------------------------------------------------------------------------- 1 | // TODO: This is not stable, just a demonstration of how it could work. 2 | 3 | (async () => { 4 | const resp = await fetch( 5 | "http://localhost:4000/rspc/binario?procedure=binario", 6 | { 7 | method: "POST", 8 | headers: { 9 | "Content-Type": "text/x-binario", 10 | }, 11 | // { name: "Oscar" } 12 | body: new Uint8Array([5, 0, 0, 0, 0, 0, 0, 0, 79, 115, 99, 97, 114]), 13 | }, 14 | ); 15 | if (!resp.ok) throw new Error(`Failed to fetch ${resp.status}`); 16 | if (resp.headers.get("content-type") !== "text/x-binario") 17 | throw new Error("Invalid content type"); 18 | 19 | const result = new Uint8Array(await resp.clone().arrayBuffer()); 20 | const expected = new Uint8Array([ 21 | 5, 0, 0, 0, 0, 0, 0, 0, 79, 115, 99, 97, 114, 22 | ]); 23 | if (!isEqualBytes(result, expected)) 24 | throw new Error(`Result doesn't match expected value. Got ${result}`); 25 | 26 | console.log("Success!", result); 27 | 28 | const resp2 = await fetch( 29 | "http://localhost:4000/rspc/binario?procedure=streaming", 30 | { 31 | method: "POST", 32 | headers: { 33 | "Content-Type": "text/x-binario", 34 | }, 35 | // { name: "Oscar" } 36 | body: new Uint8Array([5, 0, 0, 0, 0, 0, 0, 0, 79, 115, 99, 97, 114]), 37 | }, 38 | ); 39 | if (!resp2.ok) throw new Error(`Failed to fetch ${resp2.status}`); 40 | if (resp2.headers.get("content-type") !== "text/x-binario") 41 | throw new Error("Invalid content type"); 42 | 43 | console.log(await resp2.arrayBuffer()); 44 | })(); 45 | 46 | function isEqualBytes(bytes1: Uint8Array, bytes2: Uint8Array): boolean { 47 | if (bytes1.length !== bytes2.length) { 48 | return false; 49 | } 50 | 51 | for (let i = 0; i < bytes1.length; i++) { 52 | if (bytes1[i] !== bytes2[i]) { 53 | return false; 54 | } 55 | } 56 | 57 | return true; 58 | } 59 | -------------------------------------------------------------------------------- /examples/bindings.ts: -------------------------------------------------------------------------------- 1 | // My custom header 2 | // This file was generated by [rspc](https://github.com/specta-rs/rspc). Do not edit this file manually. 3 | 4 | export type Error = { type: "Mistake"; error: string } | { type: "Validator"; error: any } | { type: "Authorization"; error: string } 5 | 6 | export type ProceduresLegacy = { queries: { key: "cached"; input: null; result: number } | { key: "newstuff"; input: null; result: string } | { key: "newstuff2"; input: null; result: string } | { key: "newstuffpanic"; input: null; result: null } | { key: "newstuffser"; input: null; result: null } | { key: "sendMsg"; input: string; result: string } | { key: "sfmPost"; input: [string, null]; result: string } | { key: "streamInStreamInStreamInStream"; input: null; result: number } | { key: "validator"; input: { mail: string }; result: null } | { key: "withoutBaseProcedure"; input: string; result: null }; mutations: never; subscriptions: never } 7 | 8 | export type Procedures = { 9 | cached: { kind: "query", input: null, output: number, error: Error }, 10 | newstuff: { kind: "query", input: null, output: string, error: Error }, 11 | newstuff2: { kind: "query", input: null, output: string, error: Error }, 12 | newstuffpanic: { kind: "query", input: null, output: null, error: Error }, 13 | newstuffser: { kind: "query", input: null, output: null, error: Error }, 14 | sendMsg: { kind: "query", input: string, output: string, error: Error }, 15 | sfmPost: { kind: "query", input: [string, null], output: string, error: Error }, 16 | streamInStreamInStreamInStream: { kind: "query", input: null, output: number, error: Error }, 17 | validator: { kind: "query", input: { mail: string }, output: null, error: Error }, 18 | withoutBaseProcedure: { kind: "query", input: string, output: null, error: Error }, 19 | } -------------------------------------------------------------------------------- /examples/bindings_t.d.ts: -------------------------------------------------------------------------------- 1 | // My custom header 2 | // This file was generated by [rspc](https://github.com/specta-rs/rspc). Do not edit this file manually. 3 | 4 | export type Procedures = { 5 | echo: { input: string, output: string, error: unknown }, 6 | error: { input: null, output: string, error: unknown }, 7 | nested: { 8 | hello: { input: null, output: string, error: unknown }, 9 | }, 10 | newstuff: { input: any, output: any, error: any }, 11 | pings: { input: null, output: string, error: unknown }, 12 | sendMsg: { input: string, output: string, error: unknown }, 13 | transformMe: { input: null, output: string, error: unknown }, 14 | version: { input: null, output: string, error: unknown }, 15 | } 16 | //# sourceMappingURL=bindings_t.d.ts.map -------------------------------------------------------------------------------- /examples/bindings_t.d.ts.map: -------------------------------------------------------------------------------- 1 | {"file":"bindings_t.d.ts","mappings":";;;;CAqDmCA;CAAAC;;CAAAC;;CAAAC;CAAAC;CAAAC;CAAAC;","names":["echo","error","hello","pings","sendMsg","transformMe","version"],"sources":["/Users/oscar/Desktop/rspc/src/interop.rs"],"version":3} -------------------------------------------------------------------------------- /examples/client/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "example-client" 3 | version = "0.0.0" 4 | edition = "2021" 5 | publish = false 6 | 7 | [dependencies] 8 | rspc-client = { path = "../../crates/client" } 9 | tokio = { version = "1.43.0", features = ["macros", "rt-multi-thread"] } 10 | -------------------------------------------------------------------------------- /examples/client/src/bindings.rs: -------------------------------------------------------------------------------- 1 | //! This file was generated by [rspc](https://github.com/specta-rs/rspc). Do not edit this file manually. 2 | 3 | pub struct Procedures; 4 | 5 | pub struct echo; 6 | 7 | impl rspc_client::Procedure for echo { 8 | type Input = String; 9 | type Output = String; 10 | type Error = (); 11 | type Procedures = Procedures; 12 | const KIND: rspc_client::ProcedureKind = rspc_client::ProcedureKind::Query; 13 | const KEY: &'static str = "echo"; 14 | } 15 | 16 | pub struct error; 17 | 18 | impl rspc_client::Procedure for error { 19 | type Input = (); 20 | type Output = String; 21 | type Error = (); 22 | type Procedures = Procedures; 23 | const KIND: rspc_client::ProcedureKind = rspc_client::ProcedureKind::Query; 24 | const KEY: &'static str = "error"; 25 | } 26 | 27 | 28 | pub mod hello { 29 | pub use super::Procedures; 30 | pub struct hello; 31 | 32 | impl rspc_client::Procedure for hello { 33 | type Input = (); 34 | type Output = String; 35 | type Error = (); 36 | type Procedures = Procedures; 37 | const KIND: rspc_client::ProcedureKind = rspc_client::ProcedureKind::Query; 38 | const KEY: &'static str = "hello"; 39 | } 40 | 41 | } 42 | pub struct pings; 43 | 44 | impl rspc_client::Procedure for pings { 45 | type Input = (); 46 | type Output = String; 47 | type Error = (); 48 | type Procedures = Procedures; 49 | const KIND: rspc_client::ProcedureKind = rspc_client::ProcedureKind::Subscription; 50 | const KEY: &'static str = "pings"; 51 | } 52 | 53 | pub struct sendMsg; 54 | 55 | impl rspc_client::Procedure for sendMsg { 56 | type Input = String; 57 | type Output = String; 58 | type Error = (); 59 | type Procedures = Procedures; 60 | const KIND: rspc_client::ProcedureKind = rspc_client::ProcedureKind::Mutation; 61 | const KEY: &'static str = "sendMsg"; 62 | } 63 | 64 | pub struct transformMe; 65 | 66 | impl rspc_client::Procedure for transformMe { 67 | type Input = (); 68 | type Output = String; 69 | type Error = (); 70 | type Procedures = Procedures; 71 | const KIND: rspc_client::ProcedureKind = rspc_client::ProcedureKind::Query; 72 | const KEY: &'static str = "transformMe"; 73 | } 74 | 75 | pub struct version; 76 | 77 | impl rspc_client::Procedure for version { 78 | type Input = (); 79 | type Output = String; 80 | type Error = (); 81 | type Procedures = Procedures; 82 | const KIND: rspc_client::ProcedureKind = rspc_client::ProcedureKind::Query; 83 | const KEY: &'static str = "version"; 84 | } 85 | 86 | -------------------------------------------------------------------------------- /examples/client/src/main.rs: -------------------------------------------------------------------------------- 1 | // This file is generated by the Axum example at `./examples/axum. 2 | mod bindings; 3 | 4 | #[tokio::main] 5 | async fn main() { 6 | let client = rspc_client::Client::new("http://[::]:4000/rspc"); 7 | 8 | println!("{:?}", client.exec::(()).await); 9 | println!( 10 | "{:?}", 11 | client 12 | .exec::("Some random string!".into()) 13 | .await 14 | ); 15 | println!( 16 | "{:?}", 17 | client 18 | .exec::("Hello from rspc Rust client!".into()) 19 | .await 20 | ); 21 | 22 | // TODO: Subscription example. 23 | } 24 | -------------------------------------------------------------------------------- /examples/core/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "example-core" 3 | version = "0.0.0" 4 | edition = "2021" 5 | publish = false 6 | 7 | [dependencies] 8 | rspc = { path = "../../rspc", features = ["typescript", "rust"] } 9 | # rspc-devtools = { version = "0.0.0", path = "../../crates/devtools" } 10 | rspc-cache = { version = "0.0.0", path = "../../crates/cache" } 11 | rspc-invalidation = { version = "0.0.0", path = "../../crates/invalidation" } 12 | rspc-validator = { version = "0.0.0", path = "../../crates/validator" } 13 | rspc-tracing = { version = "0.0.0", path = "../../crates/tracing" } 14 | # rspc-openapi = { version = "0.0.0", path = "../../crates/openapi" } 15 | rspc-zer = { version = "0.0.0", path = "../../crates/zer" } 16 | serde = { workspace = true, features = ["derive"] } 17 | specta = { workspace = true, features = ["derive"] } 18 | tracing = { workspace = true } 19 | async-stream = "0.3.6" 20 | thiserror = "2.0.11" 21 | validator = { version = "0.20.0", features = ["derive"] } 22 | anyhow = "1.0.95" 23 | futures.workspace = true 24 | -------------------------------------------------------------------------------- /examples/legacy/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "example-legacy" 3 | version = "0.0.0" 4 | edition = "2021" 5 | publish = false 6 | 7 | [dependencies] 8 | tokio = { version = "1", features = ["full"] } 9 | rspc = { path = "../../rspc", features = ["typescript", "rust", "legacy"] } 10 | rspc-axum = { path = "../../integrations/axum", features = ["ws"] } 11 | rspc-legacy = { path = "../../crates/legacy" } 12 | axum = { version = "0.8.1", features = ["multipart"] } 13 | tower-http = { version = "0.6.2", default-features = false, features = [ 14 | "cors", 15 | ] } 16 | futures.workspace = true 17 | serde_json.workspace = true 18 | async-stream = "0.3.6" 19 | -------------------------------------------------------------------------------- /examples/legacy/src/main.rs: -------------------------------------------------------------------------------- 1 | //! This example shows using `rspc_legacy` directly. 2 | //! This is not intended for permanent use, but instead it is designed to allow an incremental migration from rspc 0.3.0. 3 | 4 | use std::{path::PathBuf, time::Duration}; 5 | 6 | use async_stream::stream; 7 | use axum::{http::request::Parts, routing::get}; 8 | use rspc_legacy::{Error, ErrorCode, Router, RouterBuilder}; 9 | use tokio::time::sleep; 10 | use tower_http::cors::{Any, CorsLayer}; 11 | 12 | pub(crate) struct Ctx {} 13 | 14 | fn mount() -> RouterBuilder { 15 | Router::::new() 16 | .query("version", |t| t(|_, _: ()| env!("CARGO_PKG_VERSION"))) 17 | .query("echo", |t| t(|_, v: String| v)) 18 | .query("error", |t| { 19 | t(|_, _: ()| { 20 | Err(Error::new( 21 | ErrorCode::InternalServerError, 22 | "Something went wrong".into(), 23 | )) as Result 24 | }) 25 | }) 26 | .query("transformMe", |t| t(|_, _: ()| "Hello, world!".to_string())) 27 | .mutation("sendMsg", |t| { 28 | t(|_, v: String| { 29 | println!("Client said '{}'", v); 30 | v 31 | }) 32 | }) 33 | .subscription("pings", |t| { 34 | t(|_ctx, _args: ()| { 35 | stream! { 36 | println!("Client subscribed to 'pings'"); 37 | for i in 0..5 { 38 | println!("Sending ping {}", i); 39 | yield "ping".to_string(); 40 | sleep(Duration::from_secs(1)).await; 41 | } 42 | } 43 | }) 44 | }) 45 | } 46 | 47 | #[tokio::main] 48 | async fn main() { 49 | let (procedures, types) = rspc::Router::from(mount().build()).build().unwrap(); 50 | 51 | rspc::Typescript::default() 52 | .export_to( 53 | PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../bindings.ts"), 54 | &types, 55 | ) 56 | .unwrap(); 57 | 58 | // We disable CORS because this is just an example. DON'T DO THIS IN PRODUCTION! 59 | let cors = CorsLayer::new() 60 | .allow_methods(Any) 61 | .allow_headers(Any) 62 | .allow_origin(Any); 63 | 64 | let app = axum::Router::new() 65 | .route("/", get(|| async { "Hello from rspc legacy!" })) 66 | .nest( 67 | "/rspc", 68 | rspc_axum::endpoint(procedures, |parts: Parts| { 69 | println!("Client requested operation '{}'", parts.uri.path()); 70 | Ctx {} 71 | }), 72 | ) 73 | .layer(cors); 74 | 75 | let addr = "[::]:4000".parse::().unwrap(); // This listens on IPv6 and IPv4 76 | println!("listening on http://{}/rspc/version", addr); 77 | axum::serve(tokio::net::TcpListener::bind(addr).await.unwrap(), app) 78 | .await 79 | .unwrap(); 80 | } 81 | -------------------------------------------------------------------------------- /examples/nextjs/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | -------------------------------------------------------------------------------- /examples/nextjs/README.md: -------------------------------------------------------------------------------- 1 |

rspc - Example with Next.js

2 |
3 | 4 | This example shows basic usage with Next.js and `@tanstack/react-query`. 5 | 6 |
7 | 8 |
9 | 10 | - `pages/__app.tsx` - Adds global provider 11 | - `pages/using-ssp.tsx` - Shows how to execuse queries server side 12 | - `pages/using-use-mutation.tsx` - Shows how to use `useMutation` hook 13 | - `pages/using-use-query.tsx` - Shows how to use `useQuery` hook 14 | - `pages/using-use-subscription.tsx` - Shows how to use `useSubscription` hook 15 | - `src/rspc.ts` - Shows how to set up RSPC client and hooks 16 | 17 | ## Getting started 18 | 1. Run `pnpm i` in the repository root folder 19 | 2. Start example projects with `pnpm examples dev` 20 | -------------------------------------------------------------------------------- /examples/nextjs/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /examples/nextjs/next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | swcMinify: true, 5 | } 6 | 7 | module.exports = nextConfig 8 | -------------------------------------------------------------------------------- /examples/nextjs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@rspc/examples-nextjs", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev -p 3001" 7 | }, 8 | "dependencies": { 9 | "@rspc/client": "workspace:*", 10 | "@rspc/react-query": "workspace:*", 11 | "@tanstack/react-query": "^5.66.0", 12 | "next": "^15.1.6", 13 | "react": "^19.0.0", 14 | "react-dom": "^19.0.0" 15 | }, 16 | "devDependencies": { 17 | "@types/node": "^22.13.1", 18 | "@types/react": "^19.0.8", 19 | "@types/react-dom": "^19.0.3", 20 | "typescript": "^5.7.3" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /examples/nextjs/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import "../styles/globals.css"; 2 | import type { AppProps } from "next/app"; 3 | import { client, queryClient, RSPCProvider } from "../src/rspc"; 4 | 5 | function MyApp({ Component, pageProps }: AppProps) { 6 | return ( 7 | 8 | 9 | 10 | ); 11 | } 12 | 13 | export default MyApp; 14 | -------------------------------------------------------------------------------- /examples/nextjs/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import type { NextPage } from "next"; 2 | import Head from "next/head"; 3 | import Link from "next/link"; 4 | import styles from "../styles/Home.module.css"; 5 | 6 | const Home: NextPage = () => ( 7 |
8 | 9 | RSPC Example with Next.js 10 | 11 | 12 |
13 |

14 | Welcome to{" "} 15 | 16 | RSPC 17 | {" "} 18 | with{" "} 19 | 20 | Next.js! 21 | 22 |

23 | 24 |
25 | 26 |

Using useQuery →

27 | 28 | 29 | 30 |

Using useMutation →

31 | 32 | 33 | 34 |

Using useSubscription →

35 | 36 | 37 | 38 |

Using ServerSideProps →

39 | 40 |
41 |
42 |
43 | ); 44 | 45 | export default Home; 46 | -------------------------------------------------------------------------------- /examples/nextjs/pages/using-ssp.tsx: -------------------------------------------------------------------------------- 1 | import { GetServerSideProps, NextPage } from "next"; 2 | import { client } from "../src/rspc"; 3 | import Head from "next/head"; 4 | import styles from "../styles/Home.module.css"; 5 | 6 | interface UsingServerSideProps { 7 | data?: string; 8 | error?: string; 9 | } 10 | 11 | export const getServerSideProps: GetServerSideProps< 12 | UsingServerSideProps 13 | > = async () => { 14 | try { 15 | return { props: { data: await client.query(["version"]) } }; 16 | } catch (error) { 17 | return { props: { error: (error as Error)?.message } }; 18 | } 19 | }; 20 | 21 | const UsingServerSideProps: NextPage = ({ 22 | data, 23 | error, 24 | }) => ( 25 |
26 | 27 | Using getServerSideProps | RSPC Example with Next.js 28 | 29 | 30 |
31 |

32 | getServerSideProps 33 |

34 |

35 | {data && `The server version is: ${data}`} 36 | {error} 37 |

38 |
39 |
40 | ); 41 | 42 | export default UsingServerSideProps; 43 | -------------------------------------------------------------------------------- /examples/nextjs/pages/using-use-mutation.tsx: -------------------------------------------------------------------------------- 1 | import { NextPage } from "next"; 2 | import Head from "next/head"; 3 | import { useMutation } from "../src/rspc"; 4 | import styles from "../styles/Home.module.css"; 5 | 6 | const UsingUseMutation: NextPage = () => { 7 | const { mutate, data, isPending, error } = useMutation({ 8 | mutationKey: "sendMsg", 9 | }); 10 | 11 | const handleSubmit = async ( 12 | event: React.FormEvent, 13 | ): Promise => { 14 | event.preventDefault(); 15 | mutate(event.currentTarget.message.value); 16 | }; 17 | 18 | return ( 19 |
20 | 21 | Using useMutation | RSPC Example with Next.js 22 | 23 | 24 |
25 |

26 | useMutation 27 |

28 | 29 |
30 | 36 | 37 |
38 | 39 |

40 | {isPending && "Loading data ..."} 41 | {data && `Server received message: ${data}`} 42 | {error?.message} 43 |

44 |
45 |
46 | ); 47 | }; 48 | 49 | export default UsingUseMutation; 50 | -------------------------------------------------------------------------------- /examples/nextjs/pages/using-use-query.tsx: -------------------------------------------------------------------------------- 1 | import { NextPage } from "next"; 2 | import Head from "next/head"; 3 | import { useQuery } from "../src/rspc"; 4 | import styles from "../styles/Home.module.css"; 5 | 6 | const UsingUseQuery: NextPage = () => { 7 | const { data, isLoading, error } = useQuery({ queryKey: ["echo", "Hello!"] }); 8 | 9 | return ( 10 |
11 | 12 | Using useQuery | RSPC Example with Next.js 13 | 14 | 15 |
16 |

17 | useQuery 18 |

19 |

20 | {isLoading && "Loading data ..."} 21 | {data && `RSPC says: ${data}`} 22 | {error?.message} 23 |

24 |
25 |
26 | ); 27 | }; 28 | 29 | export default UsingUseQuery; 30 | -------------------------------------------------------------------------------- /examples/nextjs/pages/using-use-subscription.tsx: -------------------------------------------------------------------------------- 1 | import { NextPage } from "next"; 2 | import Head from "next/head"; 3 | import { useState } from "react"; 4 | import { useSubscription } from "../src/rspc"; 5 | import styles from "../styles/Home.module.css"; 6 | 7 | const UsingUseSubscription: NextPage = () => { 8 | const [pings, setPings] = useState(0); 9 | 10 | useSubscription(["pings"], { 11 | onData: () => setPings((currentPings) => currentPings + 1), 12 | }); 13 | 14 | return ( 15 |
16 | 17 | Using useSubscription | RSPC Example with Next.js 18 | 19 | 20 |
21 |

22 | useSubscription 23 |

24 | 25 |

WS Pings received: {pings}

26 |
27 |
28 | ); 29 | }; 30 | 31 | export default UsingUseSubscription; 32 | -------------------------------------------------------------------------------- /examples/nextjs/src/rspc.ts: -------------------------------------------------------------------------------- 1 | import { createClient, FetchTransport, WebsocketTransport } from "@rspc/client"; 2 | import { createReactQueryHooks } from "@rspc/react-query"; 3 | import { QueryClient } from "@tanstack/react-query"; 4 | import type { Procedures } from "../../bindings"; 5 | 6 | export const client = createClient({ 7 | transport: 8 | typeof window === "undefined" 9 | ? // WebsocketTransport can not be used Server Side, so we provide FetchTransport instead. 10 | // If you do not plan on using Subscriptions you can use FetchTransport on Client Side as well. 11 | new FetchTransport("http://localhost:4000/rspc") 12 | : new WebsocketTransport("ws://localhost:4000/rspc/ws"), 13 | }); 14 | 15 | export const queryClient = new QueryClient({ 16 | defaultOptions: { 17 | queries: { 18 | retry: false, // If you want to retry when requests fail, remove this. 19 | }, 20 | }, 21 | }); 22 | 23 | export const { 24 | useContext, 25 | useMutation, 26 | useQuery, 27 | useSubscription, 28 | Provider: RSPCProvider, 29 | } = createReactQueryHooks(); 30 | -------------------------------------------------------------------------------- /examples/nextjs/styles/Home.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | padding: 0 2rem; 3 | } 4 | 5 | .main { 6 | min-height: 100vh; 7 | padding: 4rem 0; 8 | flex: 1; 9 | display: flex; 10 | flex-direction: column; 11 | justify-content: center; 12 | align-items: center; 13 | } 14 | 15 | .title a { 16 | color: #0070f3; 17 | text-decoration: none; 18 | } 19 | 20 | .title a:hover, 21 | .title a:focus, 22 | .title a:active { 23 | text-decoration: underline; 24 | } 25 | 26 | .title { 27 | margin: 0; 28 | line-height: 1.15; 29 | font-size: 4rem; 30 | } 31 | 32 | .title, 33 | .description { 34 | text-align: center; 35 | } 36 | 37 | .description { 38 | margin: 4rem 0; 39 | line-height: 1.5; 40 | font-size: 1.5rem; 41 | } 42 | 43 | .code { 44 | background: #fafafa; 45 | border-radius: 5px; 46 | padding: 0.75rem; 47 | font-size: 1.1rem; 48 | font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, 49 | Bitstream Vera Sans Mono, Courier New, monospace; 50 | } 51 | 52 | .grid { 53 | display: flex; 54 | align-items: center; 55 | justify-content: center; 56 | flex-wrap: wrap; 57 | max-width: 800px; 58 | } 59 | 60 | .card { 61 | margin: 1rem; 62 | padding: 1.5rem; 63 | text-align: left; 64 | color: inherit; 65 | text-decoration: none; 66 | border: 1px solid #eaeaea; 67 | border-radius: 10px; 68 | transition: color 0.15s ease, border-color 0.15s ease; 69 | max-width: 340px; 70 | } 71 | 72 | .card:hover, 73 | .card:focus, 74 | .card:active { 75 | color: #0070f3; 76 | border-color: #0070f3; 77 | } 78 | 79 | .card h2 { 80 | margin: 0 0 0 0; 81 | font-size: 1.5rem; 82 | } 83 | 84 | .card p { 85 | margin: 0; 86 | font-size: 1.25rem; 87 | line-height: 1.5; 88 | } 89 | 90 | .logo { 91 | height: 1em; 92 | margin-left: 0.5rem; 93 | } 94 | 95 | @media (max-width: 600px) { 96 | .grid { 97 | width: 100%; 98 | flex-direction: column; 99 | } 100 | } 101 | 102 | @media (prefers-color-scheme: dark) { 103 | .card, 104 | .code { 105 | background: #111; 106 | } 107 | .logo img { 108 | filter: invert(1); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /examples/nextjs/styles/globals.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | padding: 0; 4 | margin: 0; 5 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, 6 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; 7 | } 8 | 9 | a { 10 | color: inherit; 11 | text-decoration: none; 12 | } 13 | 14 | * { 15 | box-sizing: border-box; 16 | } 17 | 18 | @media (prefers-color-scheme: dark) { 19 | html { 20 | color-scheme: dark; 21 | } 22 | body { 23 | color: white; 24 | background: black; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /examples/nextjs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true 17 | }, 18 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 19 | "exclude": ["node_modules"] 20 | } 21 | -------------------------------------------------------------------------------- /examples/tauri/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist -------------------------------------------------------------------------------- /examples/tauri/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["tauri-apps.tauri-vscode", "rust-lang.rust-analyzer"] 3 | } 4 | -------------------------------------------------------------------------------- /examples/tauri/README.md: -------------------------------------------------------------------------------- 1 | # Tauri + Solid + Typescript 2 | 3 | This template should help get you started developing with Tauri, Solid and Typescript in Vite. 4 | 5 | ## Recommended IDE Setup 6 | 7 | - [VS Code](https://code.visualstudio.com/) + [Tauri](https://marketplace.visualstudio.com/items?itemName=tauri-apps.tauri-vscode) + [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer) 8 | -------------------------------------------------------------------------------- /examples/tauri/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Tauri + Solid + Typescript App 9 | 10 | 11 | 12 | 13 |
14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /examples/tauri/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tauri", 3 | "version": "0.1.0", 4 | "description": "", 5 | "type": "module", 6 | "scripts": { 7 | "start": "vite", 8 | "dev": "vite", 9 | "build": "vite build", 10 | "serve": "vite preview", 11 | "tauri": "tauri" 12 | }, 13 | "license": "MIT", 14 | "dependencies": { 15 | "@rspc/client": "workspace:*", 16 | "@rspc/tauri": "workspace:*", 17 | "@tauri-apps/api": "^2.2.0", 18 | "@tauri-apps/plugin-shell": "^2.2.0", 19 | "solid-js": "^1.9.4" 20 | }, 21 | "devDependencies": { 22 | "@tauri-apps/cli": "^2.2.7", 23 | "typescript": "^5.7.3", 24 | "vite": "^6.1.0", 25 | "vite-plugin-solid": "^2.11.1" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /examples/tauri/public/tauri.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /examples/tauri/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/tauri/src-tauri/.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | 5 | # Generated by Tauri 6 | # will have schema files for capabilities auto-completion 7 | /gen/schemas 8 | -------------------------------------------------------------------------------- /examples/tauri/src-tauri/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "example-tauri" 3 | version = "0.0.0" 4 | description = "A Tauri App" 5 | edition = "2021" 6 | publish = false 7 | 8 | [lib] 9 | # The `_lib` suffix may seem redundant but it is necessary 10 | # to make the lib name unique and wouldn't conflict with the bin name. 11 | # This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519 12 | name = "tauri_lib" 13 | crate-type = ["staticlib", "cdylib", "rlib"] 14 | 15 | [build-dependencies] 16 | tauri-build = { version = "2", features = [] } 17 | 18 | [dependencies] 19 | tauri = { version = "2", features = [] } 20 | serde = { version = "1", features = ["derive"] } 21 | serde_json = "1" 22 | rspc = { path = "../../../rspc", features = ["typescript"] } 23 | tauri-plugin-rspc = { path = "../../../integrations/tauri" } 24 | specta = { workspace = true, features = ["derive"] } 25 | example-core = { path = "../../core" } 26 | -------------------------------------------------------------------------------- /examples/tauri/src-tauri/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | tauri_build::build() 3 | } 4 | -------------------------------------------------------------------------------- /examples/tauri/src-tauri/capabilities/default.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../gen/schemas/desktop-schema.json", 3 | "identifier": "default", 4 | "description": "Capability for the main window", 5 | "windows": ["main"], 6 | "permissions": ["core:default", "rspc:default"] 7 | } 8 | -------------------------------------------------------------------------------- /examples/tauri/src-tauri/icons/128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/specta-rs/rspc/a9caf9b7070cc0017406be1d8322e02baf932645/examples/tauri/src-tauri/icons/128x128.png -------------------------------------------------------------------------------- /examples/tauri/src-tauri/icons/128x128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/specta-rs/rspc/a9caf9b7070cc0017406be1d8322e02baf932645/examples/tauri/src-tauri/icons/128x128@2x.png -------------------------------------------------------------------------------- /examples/tauri/src-tauri/icons/32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/specta-rs/rspc/a9caf9b7070cc0017406be1d8322e02baf932645/examples/tauri/src-tauri/icons/32x32.png -------------------------------------------------------------------------------- /examples/tauri/src-tauri/icons/Square107x107Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/specta-rs/rspc/a9caf9b7070cc0017406be1d8322e02baf932645/examples/tauri/src-tauri/icons/Square107x107Logo.png -------------------------------------------------------------------------------- /examples/tauri/src-tauri/icons/Square142x142Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/specta-rs/rspc/a9caf9b7070cc0017406be1d8322e02baf932645/examples/tauri/src-tauri/icons/Square142x142Logo.png -------------------------------------------------------------------------------- /examples/tauri/src-tauri/icons/Square150x150Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/specta-rs/rspc/a9caf9b7070cc0017406be1d8322e02baf932645/examples/tauri/src-tauri/icons/Square150x150Logo.png -------------------------------------------------------------------------------- /examples/tauri/src-tauri/icons/Square284x284Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/specta-rs/rspc/a9caf9b7070cc0017406be1d8322e02baf932645/examples/tauri/src-tauri/icons/Square284x284Logo.png -------------------------------------------------------------------------------- /examples/tauri/src-tauri/icons/Square30x30Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/specta-rs/rspc/a9caf9b7070cc0017406be1d8322e02baf932645/examples/tauri/src-tauri/icons/Square30x30Logo.png -------------------------------------------------------------------------------- /examples/tauri/src-tauri/icons/Square310x310Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/specta-rs/rspc/a9caf9b7070cc0017406be1d8322e02baf932645/examples/tauri/src-tauri/icons/Square310x310Logo.png -------------------------------------------------------------------------------- /examples/tauri/src-tauri/icons/Square44x44Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/specta-rs/rspc/a9caf9b7070cc0017406be1d8322e02baf932645/examples/tauri/src-tauri/icons/Square44x44Logo.png -------------------------------------------------------------------------------- /examples/tauri/src-tauri/icons/Square71x71Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/specta-rs/rspc/a9caf9b7070cc0017406be1d8322e02baf932645/examples/tauri/src-tauri/icons/Square71x71Logo.png -------------------------------------------------------------------------------- /examples/tauri/src-tauri/icons/Square89x89Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/specta-rs/rspc/a9caf9b7070cc0017406be1d8322e02baf932645/examples/tauri/src-tauri/icons/Square89x89Logo.png -------------------------------------------------------------------------------- /examples/tauri/src-tauri/icons/StoreLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/specta-rs/rspc/a9caf9b7070cc0017406be1d8322e02baf932645/examples/tauri/src-tauri/icons/StoreLogo.png -------------------------------------------------------------------------------- /examples/tauri/src-tauri/icons/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/specta-rs/rspc/a9caf9b7070cc0017406be1d8322e02baf932645/examples/tauri/src-tauri/icons/icon.icns -------------------------------------------------------------------------------- /examples/tauri/src-tauri/icons/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/specta-rs/rspc/a9caf9b7070cc0017406be1d8322e02baf932645/examples/tauri/src-tauri/icons/icon.ico -------------------------------------------------------------------------------- /examples/tauri/src-tauri/icons/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/specta-rs/rspc/a9caf9b7070cc0017406be1d8322e02baf932645/examples/tauri/src-tauri/icons/icon.png -------------------------------------------------------------------------------- /examples/tauri/src-tauri/src/api.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | use serde::Serialize; 4 | use specta::Type; 5 | 6 | #[derive(Type, Debug)] 7 | pub enum Infallible {} 8 | 9 | impl fmt::Display for Infallible { 10 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 11 | write!(f, "{self:?}") 12 | } 13 | } 14 | 15 | impl Serialize for Infallible { 16 | fn serialize(&self, _: S) -> Result 17 | where 18 | S: serde::Serializer, 19 | { 20 | unreachable!() 21 | } 22 | } 23 | 24 | impl std::error::Error for Infallible {} 25 | 26 | impl rspc::Error for Infallible { 27 | fn into_procedure_error(self) -> rspc::ProcedureError { 28 | unreachable!() 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /examples/tauri/src-tauri/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | use example_core::{mount, Ctx}; 3 | 4 | mod api; 5 | 6 | #[cfg_attr(mobile, tauri::mobile_entry_point)] 7 | pub fn run() { 8 | let router = mount(); 9 | let (procedures, types) = router.build().unwrap(); 10 | 11 | rspc::Typescript::default() 12 | // .formatter(specta_typescript::formatter::prettier) 13 | .header("// My custom header") 14 | // .enable_source_maps() // TODO: Fix this 15 | .export_to( 16 | PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../bindings.ts"), 17 | &types, 18 | ) 19 | .unwrap(); 20 | 21 | tauri::Builder::default() 22 | .plugin(tauri_plugin_rspc::init(procedures, |_| Ctx {})) 23 | .run(tauri::generate_context!()) 24 | .expect("error while running tauri application"); 25 | } 26 | -------------------------------------------------------------------------------- /examples/tauri/src-tauri/src/main.rs: -------------------------------------------------------------------------------- 1 | // Prevents additional console window on Windows in release, DO NOT REMOVE!! 2 | #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] 3 | 4 | fn main() { 5 | tauri_lib::run() 6 | } 7 | -------------------------------------------------------------------------------- /examples/tauri/src-tauri/tauri.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.tauri.app/config/2", 3 | "productName": "tauri", 4 | "version": "0.1.0", 5 | "identifier": "dev.specta.rspc.desktop", 6 | "build": { 7 | "beforeDevCommand": "pnpm dev", 8 | "devUrl": "http://localhost:1420", 9 | "beforeBuildCommand": "pnpm build", 10 | "frontendDist": "../dist" 11 | }, 12 | "app": { 13 | "windows": [ 14 | { 15 | "title": "tauri", 16 | "width": 800, 17 | "height": 600 18 | } 19 | ], 20 | "security": { 21 | "csp": null 22 | } 23 | }, 24 | "bundle": { 25 | "active": true, 26 | "targets": "all", 27 | "icon": [ 28 | "icons/32x32.png", 29 | "icons/128x128.png", 30 | "icons/128x128@2x.png", 31 | "icons/icon.icns", 32 | "icons/icon.ico" 33 | ] 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /examples/tauri/src/App.css: -------------------------------------------------------------------------------- 1 | .logo.vite:hover { 2 | filter: drop-shadow(0 0 2em #747bff); 3 | } 4 | 5 | .logo.solid:hover { 6 | filter: drop-shadow(0 0 2em #2f5d90); 7 | } 8 | :root { 9 | font-family: Inter, Avenir, Helvetica, Arial, sans-serif; 10 | font-size: 16px; 11 | line-height: 24px; 12 | font-weight: 400; 13 | 14 | color: #0f0f0f; 15 | background-color: #f6f6f6; 16 | 17 | font-synthesis: none; 18 | text-rendering: optimizeLegibility; 19 | -webkit-font-smoothing: antialiased; 20 | -moz-osx-font-smoothing: grayscale; 21 | -webkit-text-size-adjust: 100%; 22 | } 23 | 24 | .container { 25 | margin: 0; 26 | padding-top: 10vh; 27 | display: flex; 28 | flex-direction: column; 29 | justify-content: center; 30 | text-align: center; 31 | } 32 | 33 | .logo { 34 | height: 6em; 35 | padding: 1.5em; 36 | will-change: filter; 37 | transition: 0.75s; 38 | } 39 | 40 | .logo.tauri:hover { 41 | filter: drop-shadow(0 0 2em #24c8db); 42 | } 43 | 44 | .row { 45 | display: flex; 46 | justify-content: center; 47 | } 48 | 49 | a { 50 | font-weight: 500; 51 | color: #646cff; 52 | text-decoration: inherit; 53 | } 54 | 55 | a:hover { 56 | color: #535bf2; 57 | } 58 | 59 | h1 { 60 | text-align: center; 61 | } 62 | 63 | input, 64 | button { 65 | border-radius: 8px; 66 | border: 1px solid transparent; 67 | padding: 0.6em 1.2em; 68 | font-size: 1em; 69 | font-weight: 500; 70 | font-family: inherit; 71 | color: #0f0f0f; 72 | background-color: #ffffff; 73 | transition: border-color 0.25s; 74 | box-shadow: 0 2px 2px rgba(0, 0, 0, 0.2); 75 | } 76 | 77 | button { 78 | cursor: pointer; 79 | } 80 | 81 | button:hover { 82 | border-color: #396cd8; 83 | } 84 | button:active { 85 | border-color: #396cd8; 86 | background-color: #e8e8e8; 87 | } 88 | 89 | input, 90 | button { 91 | outline: none; 92 | } 93 | 94 | #greet-input { 95 | margin-right: 5px; 96 | } 97 | 98 | @media (prefers-color-scheme: dark) { 99 | :root { 100 | color: #f6f6f6; 101 | background-color: #2f2f2f; 102 | } 103 | 104 | a:hover { 105 | color: #24c8db; 106 | } 107 | 108 | input, 109 | button { 110 | color: #ffffff; 111 | background-color: #0f0f0f98; 112 | } 113 | button:active { 114 | background-color: #0f0f0f69; 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /examples/tauri/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { createClient } from "@rspc/client/next"; 2 | import { tauriExecute } from "@rspc/tauri/next"; 3 | 4 | import { Procedures } from "../../bindings"; 5 | 6 | import "./App.css"; 7 | 8 | const client = createClient(tauriExecute); 9 | 10 | function App() { 11 | client.sendMsg.mutate("bruh").then(console.log); 12 | 13 | return ( 14 |
15 |

Welcome to Tauri + Solid

16 |
17 | ); 18 | } 19 | 20 | export default App; 21 | -------------------------------------------------------------------------------- /examples/tauri/src/assets/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/tauri/src/index.tsx: -------------------------------------------------------------------------------- 1 | /* @refresh reload */ 2 | import { render } from "solid-js/web"; 3 | import App from "./App"; 4 | 5 | render(() => , document.getElementById("root") as HTMLElement); 6 | -------------------------------------------------------------------------------- /examples/tauri/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /examples/tauri/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "module": "ESNext", 6 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "noEmit": true, 15 | "jsx": "preserve", 16 | "jsxImportSource": "solid-js", 17 | 18 | /* Linting */ 19 | "strict": true, 20 | "noUnusedLocals": true, 21 | "noUnusedParameters": true, 22 | "noFallthroughCasesInSwitch": true 23 | }, 24 | "include": ["src"], 25 | "references": [{ "path": "./tsconfig.node.json" }] 26 | } 27 | -------------------------------------------------------------------------------- /examples/tauri/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": ["vite.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /examples/tauri/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import solid from "vite-plugin-solid"; 3 | 4 | // @ts-expect-error process is a nodejs global 5 | const host = process.env.TAURI_DEV_HOST; 6 | 7 | // https://vitejs.dev/config/ 8 | export default defineConfig(async () => ({ 9 | plugins: [solid()], 10 | 11 | // Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build` 12 | // 13 | // 1. prevent vite from obscuring rust errors 14 | clearScreen: false, 15 | // 2. tauri expects a fixed port, fail if that port is not available 16 | server: { 17 | port: 1420, 18 | strictPort: true, 19 | host: host || false, 20 | hmr: host 21 | ? { 22 | protocol: "ws", 23 | host, 24 | port: 1421, 25 | } 26 | : undefined, 27 | watch: { 28 | // 3. tell vite to ignore watching `src-tauri` 29 | ignored: ["**/src-tauri/**"], 30 | }, 31 | }, 32 | })); 33 | -------------------------------------------------------------------------------- /integrations/axum/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rspc-axum" 3 | description = "Axum adapter for rspc" 4 | version = "0.3.0" 5 | authors = ["Oscar Beaumont "] 6 | edition = "2021" 7 | license = "MIT" 8 | repository = "https://github.com/specta-rs/rspc" 9 | documentation = "https://docs.rs/rspc-axum/latest/rspc-axum" 10 | keywords = ["async", "specta", "rust-to-ts", "typescript", "typesafe"] 11 | categories = ["web-programming", "asynchronous"] 12 | 13 | # /bin/sh RUSTDOCFLAGS="--cfg docsrs" cargo +nightly doc --all-features 14 | [package.metadata."docs.rs"] 15 | all-features = true 16 | rustdoc-args = ["--cfg", "docsrs"] 17 | 18 | [features] 19 | default = [] 20 | ws = ["axum/ws"] 21 | 22 | [dependencies] 23 | rspc-procedure = { version = "0.0.1", path = "../../crates/procedure" } 24 | axum = { version = "0.8.1", features = ["ws", "json"] } 25 | serde_json = "1" 26 | 27 | # TODO: Drop these 28 | form_urlencoded = "1.2.1" # TODO: use Axum's built in extractor 29 | futures = "0.3" # TODO: No blocking execution, etc 30 | tokio = { version = "1", features = ["sync", "macros"] } # TODO: No more `tokio::select` + spawning threads. Axum's Websocket upgrade handles that. 31 | serde = { version = "1", features = ["derive"] } # TODO: Remove features 32 | serde_urlencoded = "0.7.1" 33 | mime = "0.3.17" 34 | # rspc-invalidation = { version = "0.0.0", path = "../../crates/invalidation" } 35 | 36 | [lints] 37 | workspace = true 38 | -------------------------------------------------------------------------------- /integrations/axum/src/extractors.rs: -------------------------------------------------------------------------------- 1 | use axum::{extract::FromRequestParts, http::request::Parts}; 2 | use std::future::Future; 3 | 4 | use std::marker::PhantomData; 5 | 6 | pub trait TCtxFunc: Clone + Send + Sync + 'static 7 | where 8 | TState: Send + Sync, 9 | TCtx: Send + 'static, 10 | { 11 | fn exec(&self, parts: Parts, state: &TState) -> impl Future> + Send; 12 | } 13 | 14 | pub struct ZeroArgMarker; 15 | 16 | impl TCtxFunc for TFunc 17 | where 18 | TFunc: Fn() -> TCtx + Clone + Send + Sync + 'static, 19 | TState: Send + Sync, 20 | TCtx: Send + 'static, 21 | { 22 | async fn exec(&self, _: Parts, _: &TState) -> Result { 23 | Ok(self.clone()()) 24 | } 25 | } 26 | 27 | macro_rules! impl_fn { 28 | ($marker:ident; $($generics:ident),*) => { 29 | #[allow(unused_parens)] 30 | pub struct $marker<$($generics),*>(PhantomData<($($generics),*)>); 31 | 32 | impl + Send),*> TCtxFunc> for TFunc 33 | where 34 | TFunc: Fn($($generics),*) -> TCtx +Clone + Send + Sync + 'static, 35 | TState: Send + Sync, 36 | TCtx: Send + 'static 37 | { 38 | async fn exec(&self, mut parts: Parts, state: &TState) -> Result 39 | { 40 | $( 41 | #[allow(non_snake_case)] 42 | let Ok($generics) = $generics::from_request_parts(&mut parts, &state).await else { 43 | return Err(()) 44 | }; 45 | )* 46 | 47 | Ok(self.clone()($($generics),*)) 48 | } 49 | } 50 | }; 51 | } 52 | 53 | impl_fn!(OneArgMarker; T1); 54 | impl_fn!(TwoArgMarker; T1, T2); 55 | impl_fn!(ThreeArgMarker; T1, T2, T3); 56 | impl_fn!(FourArgMarker; T1, T2, T3, T4); 57 | impl_fn!(FiveArgMarker; T1, T2, T3, T4, T5); 58 | impl_fn!(SixArgMarker; T1, T2, T3, T4, T5, T6); 59 | impl_fn!(SevenArgMarker; T1, T2, T3, T4, T5, T6, T7); 60 | impl_fn!(EightArgMarker; T1, T2, T3, T4, T5, T6, T7, T8); 61 | impl_fn!(NineArgMarker; T1, T2, T3, T4, T5, T6, T7, T8, T9); 62 | impl_fn!(TenArgMarker; T1, T2, T3, T4, T5, T6, T7, T8, T9, T10); 63 | impl_fn!(ElevenArgMarker; T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11); 64 | impl_fn!(TwelveArgMarker; T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12); 65 | impl_fn!(ThirteenArgMarker; T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13); 66 | impl_fn!(FourteenArgMarker; T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14); 67 | impl_fn!(FifteenArgMarker; T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15); 68 | impl_fn!(SixteenArgMarker; T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16); 69 | -------------------------------------------------------------------------------- /integrations/axum/src/jsonrpc.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use serde_json::Value; 3 | 4 | #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, Hash)] 5 | #[serde(untagged)] 6 | pub enum RequestId { 7 | Null, 8 | Number(u32), 9 | String(String), 10 | } 11 | 12 | #[derive(Debug, Clone, Deserialize, Serialize)] // TODO: Type on this 13 | pub struct Request { 14 | pub jsonrpc: Option, // This is required in the JsonRPC spec but I make it optional. 15 | pub id: RequestId, 16 | #[serde(flatten)] 17 | pub inner: RequestInner, 18 | } 19 | 20 | #[derive(Debug, Clone, Deserialize, Serialize)] 21 | #[serde(tag = "method", content = "params", rename_all = "camelCase")] 22 | pub enum RequestInner { 23 | Query { 24 | path: String, 25 | input: Option, 26 | }, 27 | Mutation { 28 | path: String, 29 | input: Option, 30 | }, 31 | Subscription { 32 | path: String, 33 | input: (RequestId, Option), 34 | }, 35 | SubscriptionStop { 36 | input: RequestId, 37 | }, 38 | } 39 | 40 | #[derive(Debug, Clone, Serialize)] // TODO: Add `specta::Type` when supported 41 | pub struct Response { 42 | pub jsonrpc: &'static str, 43 | pub id: RequestId, 44 | pub result: ResponseInner, 45 | } 46 | 47 | #[derive(Debug, Clone, Serialize)] 48 | #[serde(tag = "type", content = "data", rename_all = "camelCase")] 49 | pub enum ResponseInner { 50 | Event(Value), 51 | Response(Value), 52 | Error(JsonRPCError), 53 | } 54 | 55 | #[derive(Debug, Clone, Serialize)] 56 | pub struct JsonRPCError { 57 | pub code: i32, 58 | pub message: String, 59 | pub data: Option, 60 | } 61 | 62 | // TODO: BREAK 63 | 64 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 65 | pub enum ProcedureKind { 66 | Query, 67 | Mutation, 68 | Subscription, 69 | } 70 | 71 | impl ProcedureKind { 72 | pub fn to_str(&self) -> &'static str { 73 | match self { 74 | ProcedureKind::Query => "query", 75 | ProcedureKind::Mutation => "mutation", 76 | ProcedureKind::Subscription => "subscription", 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /integrations/axum/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! rspc-axum: [Axum](https://docs.rs/axum) integration for [rspc](https://rspc.dev). 2 | #![forbid(unsafe_code)] 3 | #![cfg_attr(docsrs, feature(doc_cfg))] 4 | #![doc( 5 | html_logo_url = "https://github.com/specta-rs/rspc/raw/main/.github/logo.png", 6 | html_favicon_url = "https://github.com/specta-rs/rspc/raw/main/.github/logo.png" 7 | )] 8 | 9 | mod endpoint; 10 | mod extractors; 11 | mod jsonrpc; 12 | mod jsonrpc_exec; 13 | // mod legacy; 14 | mod request; 15 | mod v2; 16 | 17 | // pub use endpoint::Endpoint; 18 | pub use request::AxumRequest; 19 | pub use v2::endpoint; 20 | -------------------------------------------------------------------------------- /integrations/axum/src/request.rs: -------------------------------------------------------------------------------- 1 | //! TODO: Use `axum_core` not `axum` 2 | 3 | use axum::{ 4 | body::HttpBody, 5 | extract::{FromRequest, Request}, 6 | }; 7 | use rspc_procedure::{Procedure, ProcedureStream}; 8 | use serde::Deserializer; 9 | 10 | // TODO: rename? 11 | pub struct AxumRequest { 12 | req: Request, 13 | } 14 | 15 | impl AxumRequest { 16 | pub fn deserialize(self, exec: impl FnOnce(&[u8]) -> T) -> T { 17 | let hint = self.req.body().size_hint(); 18 | let has_body = hint.lower() != 0 || hint.upper() != Some(0); 19 | 20 | // TODO: Matching on incoming method??? 21 | 22 | // let mut bytes = None; 23 | // let input = if !has_body { 24 | // ExecuteInput::Query(req.uri().query().unwrap_or_default()) 25 | // } else { 26 | // // TODO: bring this back 27 | // // if !json_content_type(req.headers()) { 28 | // // let err: ProcedureError = rspc_procedure::DeserializeError::custom( 29 | // // "Client did not set correct valid 'Content-Type' header", 30 | // // ) 31 | // // .into(); 32 | // // let buf = serde_json::to_vec(&err).unwrap(); // TODO 33 | 34 | // // return ( 35 | // // StatusCode::BAD_REQUEST, 36 | // // [(header::CONTENT_TYPE, "application/json")], 37 | // // Body::from(buf), 38 | // // ) 39 | // // .into_response(); 40 | // // } 41 | 42 | // // TODO: Error handling 43 | // bytes = Some(Bytes::from_request(req, &()).await.unwrap()); 44 | // ExecuteInput::Body( 45 | // bytes.as_ref().expect("assigned on previous line"), 46 | // ) 47 | // }; 48 | 49 | // let (status, stream) = 50 | // rspc_http::execute(&procedure, input, || ctx_fn()).await; 51 | 52 | exec(b"null") 53 | } 54 | } 55 | 56 | impl FromRequest for AxumRequest 57 | where S: Send + Sync 58 | { 59 | type Rejection = (); // TODO: What should this be? 60 | 61 | async fn from_request(req: Request, state: &S) -> Result { 62 | Ok(Self { req }) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /integrations/tauri/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tauri-plugin-rspc" 3 | description = "Tauri adapter for rspc" 4 | version = "0.2.2" 5 | authors = ["Oscar Beaumont "] 6 | edition = "2021" 7 | license = "MIT" 8 | repository = "https://github.com/specta-rs/rspc" 9 | documentation = "https://docs.rs/rspc-axum/latest/rspc-axum" 10 | keywords = ["async", "specta", "rust-to-ts", "typescript", "typesafe"] 11 | categories = ["web-programming", "asynchronous"] 12 | links = "tauri-plugin-rspc" 13 | 14 | # /bin/sh RUSTDOCFLAGS="--cfg docsrs" cargo +nightly doc --all-features 15 | [package.metadata."docs.rs"] 16 | all-features = true 17 | rustc-args = ["--cfg", "docsrs"] 18 | rustdoc-args = ["--cfg", "docsrs"] 19 | 20 | [dependencies] 21 | rspc-procedure = { version = "0.0.1", path = "../../crates/procedure" } 22 | tauri = "2" 23 | serde = { version = "1", features = [ 24 | "derive", 25 | ] } # is a dependency of Tauri anyway 26 | serde_json = { version = "1", features = [ 27 | "raw_value", 28 | ] } # is a dependency of Tauri anyway 29 | 30 | [lints] 31 | workspace = true 32 | 33 | [build-dependencies] 34 | tauri-plugin = { version = "2.0.4", features = ["build"] } 35 | -------------------------------------------------------------------------------- /integrations/tauri/build.rs: -------------------------------------------------------------------------------- 1 | const COMMANDS: &[&str] = &["handle_rpc"]; 2 | 3 | fn main() { 4 | tauri_plugin::Builder::new(COMMANDS).build(); 5 | } 6 | -------------------------------------------------------------------------------- /integrations/tauri/permissions/autogenerated/commands/handle_rpc.toml: -------------------------------------------------------------------------------- 1 | # Automatically generated - DO NOT EDIT! 2 | 3 | "$schema" = "../../schemas/schema.json" 4 | 5 | [[permission]] 6 | identifier = "allow-handle-rpc" 7 | description = "Enables the handle_rpc command without any pre-configured scope." 8 | commands.allow = ["handle_rpc"] 9 | 10 | [[permission]] 11 | identifier = "deny-handle-rpc" 12 | description = "Denies the handle_rpc command without any pre-configured scope." 13 | commands.deny = ["handle_rpc"] 14 | -------------------------------------------------------------------------------- /integrations/tauri/permissions/autogenerated/reference.md: -------------------------------------------------------------------------------- 1 | ## Default Permission 2 | 3 | Allows making rspc requests 4 | 5 | - `allow-handle-rpc` 6 | 7 | ## Permission Table 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 22 | 27 | 28 | 29 | 30 | 35 | 40 | 41 |
IdentifierDescription
18 | 19 | `rspc:allow-handle-rpc` 20 | 21 | 23 | 24 | Enables the handle_rpc command without any pre-configured scope. 25 | 26 |
31 | 32 | `rspc:deny-handle-rpc` 33 | 34 | 36 | 37 | Denies the handle_rpc command without any pre-configured scope. 38 | 39 |
42 | -------------------------------------------------------------------------------- /integrations/tauri/permissions/default.toml: -------------------------------------------------------------------------------- 1 | "$schema" = "schemas/schema.json" 2 | [default] 3 | description = "Allows making rspc requests" 4 | permissions = ["allow-handle-rpc"] 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rspc", 3 | "version": "0.0.0", 4 | "description": "A blazing fast and easy to use TRPC-like server for Rust.", 5 | "keywords": [], 6 | "author": "Oscar Beaumont", 7 | "license": "MIT", 8 | "private": true, 9 | "scripts": { 10 | "dev": "pnpm -r --parallel --filter=!@rspc/docs --filter=!@rspc/examples-* --filter=!rspc-vscode exec pnpm dev", 11 | "build": "pnpm turbo run build --filter='./packages/*'", 12 | "typecheck": "pnpm -r --filter=!rspc-vscode exec tsc --noEmit", 13 | "docs": "pnpm --filter @rspc/docs -- ", 14 | "client": "pnpm --filter @rspc/client -- ", 15 | "examples": "pnpm --filter @rspc/examples-* -- ", 16 | "playground": "pnpm --filter @rspc/playground -- ", 17 | "react": "pnpm --filter @rspc/react -- ", 18 | "solid": "pnpm --filter @rspc/solid -- ", 19 | "tauri": "pnpm --filter @rspc/tauri -- ", 20 | "fix": "biome lint --apply . && biome format --write . && biome check . --apply" 21 | }, 22 | "engines": { 23 | "pnpm": ">=7.0.0", 24 | "npm": "pnpm", 25 | "yarn": "pnpm", 26 | "node": ">=16.0.0" 27 | }, 28 | "devDependencies": { 29 | "biome": "^0.3.3", 30 | "turbo": "^2.4.0" 31 | }, 32 | "packageManager": "pnpm@9.5.0" 33 | } 34 | -------------------------------------------------------------------------------- /packages/client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@rspc/client", 3 | "version": "0.3.1", 4 | "description": "A blazing fast and easy to use TRPC-like server for Rust.", 5 | "keywords": [], 6 | "author": "Oscar Beaumont", 7 | "license": "MIT", 8 | "type": "module", 9 | "main": "dist/index.cjs", 10 | "types": "dist/index.d.ts", 11 | "exports": { 12 | "./package.json": "./package.json", 13 | ".": { 14 | "types": "./dist/index.d.ts", 15 | "import": "./dist/index.js", 16 | "default": "./dist/index.cjs" 17 | }, 18 | "./next": { 19 | "types": "./dist/next/index.d.cts", 20 | "import": "./dist/next/index.js", 21 | "default": "./dist/next/index.cjs" 22 | } 23 | }, 24 | "files": [ 25 | "dist" 26 | ], 27 | "scripts": { 28 | "dev": "tsup --watch", 29 | "build": "tsup", 30 | "prepare": "tsup" 31 | }, 32 | "devDependencies": { 33 | "tsup": "^8.3.6", 34 | "typescript": "^5.7.3" 35 | }, 36 | "tsup": { 37 | "entry": [ 38 | "src/index.ts", 39 | "src/next/index.ts" 40 | ], 41 | "format": [ 42 | "esm", 43 | "cjs" 44 | ], 45 | "dts": true, 46 | "splitting": true, 47 | "clean": true, 48 | "sourcemap": true 49 | }, 50 | "dependencies": { 51 | "vitest": "^3.0.5" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /packages/client/src/error.ts: -------------------------------------------------------------------------------- 1 | export class RSPCError { 2 | code: number; 3 | message: string; 4 | 5 | constructor(code: number, message: string) { 6 | this.code = code; 7 | this.message = message; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/client/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./client"; 2 | export * from "./transport"; 3 | export * from "./error"; 4 | export * from "./typescript"; 5 | -------------------------------------------------------------------------------- /packages/client/src/next/UntypedClient.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | ExeceuteData, 3 | ExecuteArgs, 4 | ExecuteFn, 5 | SubscriptionObserver, 6 | } from "./types"; 7 | 8 | export function observable( 9 | cb: (subscriber: { next: (value: T) => void; complete(): void }) => void, 10 | ) { 11 | let callbacks: Array<(v: T) => void> = []; 12 | let completeCallbacks: Array<() => void> = []; 13 | let done = false; 14 | 15 | cb({ 16 | next: (v) => { 17 | if (done) return; 18 | callbacks.forEach((cb) => cb(v)); 19 | }, 20 | complete: () => { 21 | if (done) return; 22 | done = true; 23 | completeCallbacks.forEach((cb) => cb()); 24 | }, 25 | }); 26 | 27 | return { 28 | subscribe(cb: (v: T) => void) { 29 | if (done) return Promise.resolve(); 30 | 31 | callbacks.push(cb); 32 | return new Promise((res) => { 33 | completeCallbacks.push(() => res()); 34 | }); 35 | }, 36 | get done() { 37 | return done; 38 | }, 39 | }; 40 | } 41 | 42 | export type Observable = ReturnType>; 43 | 44 | export const fetchExecute = ( 45 | config: { url: string }, 46 | args: ExecuteArgs, 47 | ): ReturnType => { 48 | if (args.type === "subscription") 49 | throw new Error("Subscriptions are not possible with the `fetch` executor"); 50 | 51 | let promise; 52 | if (args.type === "query") { 53 | promise = fetch( 54 | `${config.url}/${args.path}?${new URLSearchParams({ 55 | input: JSON.stringify(args.input), 56 | })}`, 57 | { 58 | method: "GET", 59 | headers: { 60 | Accept: "application/json", 61 | }, 62 | }, 63 | ); 64 | } else { 65 | promise = fetch(`${config.url}/${args.path}`, { 66 | method: "POST", 67 | headers: { 68 | "Content-Type": "application/json", 69 | Accept: "application/json", 70 | }, 71 | body: JSON.stringify(args.input), 72 | }); 73 | } 74 | 75 | return observable((subscriber) => { 76 | promise 77 | .then(async (r) => { 78 | let json; 79 | 80 | if (r.status === 200) { 81 | subscriber.next(await r.json()); 82 | } else json = (await r.json()) as []; 83 | 84 | json; 85 | }) 86 | .finally(() => subscriber.complete()); 87 | }); 88 | }; 89 | 90 | export class UntypedClient { 91 | constructor(public execute: ExecuteFn) {} 92 | 93 | private async executeAsPromise(args: ExecuteArgs) { 94 | const obs = this.execute(args); 95 | 96 | let data: ExeceuteData | undefined; 97 | 98 | await obs.subscribe((d) => { 99 | if (data === undefined) data = d; 100 | }); 101 | 102 | if (!data) throw new Error("No data received"); 103 | if (data.code !== 200) 104 | throw new Error(`Error with code '${data.code}' occurred`, data.value); 105 | 106 | return data.value; 107 | } 108 | 109 | public query(path: string, input: unknown) { 110 | return this.executeAsPromise({ type: "query", path, input }); 111 | } 112 | public mutation(path: string, input: unknown) { 113 | return this.executeAsPromise({ type: "mutation", path, input }); 114 | } 115 | public subscription( 116 | path: string, 117 | input: unknown, 118 | opts?: Partial>, 119 | ) { 120 | const observable = this.execute({ type: "subscription", path, input }); 121 | 122 | // observable.subscribe((response) => { 123 | // if (response.result.type === "started") { 124 | // opts?.onStarted?.(); 125 | // } else if (response.result.type === "stopped") { 126 | // opts?.onStopped?.(); 127 | // } else { 128 | // opts?.onData?.(response.result.data); 129 | // } 130 | // }); 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /packages/client/src/next/index.test.ts: -------------------------------------------------------------------------------- 1 | import { test } from "vitest"; 2 | import { createClient, observable } from "."; 3 | import { fetchExecute } from "./UntypedClient"; 4 | 5 | type NestedProcedures = { 6 | nested: { 7 | procedures: { 8 | one: { 9 | kind: "query"; 10 | input: string; 11 | output: number; 12 | error: boolean; 13 | }; 14 | two: { 15 | kind: "mutation"; 16 | input: string; 17 | output: { id: string; name: string }; 18 | error: { status: "NOT_FOUND" }; 19 | }; 20 | three: { 21 | kind: "subscription"; 22 | input: string; 23 | output: { id: string; name: string }; 24 | error: { status: "NOT_FOUND" }; 25 | }; 26 | }; 27 | }; 28 | }; 29 | 30 | const client = createClient((args) => 31 | fetchExecute({ url: "..." }, args), 32 | ); 33 | 34 | test("proxy", () => { 35 | client.nested.procedures.one.query("test"); 36 | client.nested.procedures.two.mutate("test"); 37 | client.nested.procedures.three.subscribe("test"); 38 | }); 39 | -------------------------------------------------------------------------------- /packages/client/src/next/index.ts: -------------------------------------------------------------------------------- 1 | import { UntypedClient } from "./UntypedClient"; 2 | import type { 3 | ProcedureResult, 4 | ProcedureKind as ProcedureKind, 5 | SubscriptionObserver, 6 | ExecuteFn, 7 | Procedure, 8 | } from "./types"; 9 | 10 | export * from "./types"; 11 | export { Observable, observable } from "./UntypedClient"; 12 | 13 | export type ProcedureWithKind = Omit< 14 | Procedure, 15 | "kind" 16 | > & { kind: V }; 17 | 18 | export type Procedures = { 19 | [K in string]: Procedure | Procedures; 20 | }; 21 | 22 | type Unsubscribable = { unsubscribe: () => void }; 23 | 24 | export type VoidIfInputNull< 25 | P extends Procedure, 26 | Else = P["input"], 27 | > = P["input"] extends null ? void : Else; 28 | 29 | type Resolver

= ( 30 | input: VoidIfInputNull

, 31 | ) => Promise>; 32 | 33 | type SubscriptionResolver

= ( 34 | input: VoidIfInputNull

, 35 | opts?: Partial>, 36 | ) => Unsubscribable; 37 | 38 | export type ProcedureProxyMethods

= 39 | P["kind"] extends "query" 40 | ? { query: Resolver

} 41 | : P["kind"] extends "mutation" 42 | ? { mutate: Resolver

} 43 | : P["kind"] extends "subscription" 44 | ? { subscribe: SubscriptionResolver

} 45 | : never; 46 | 47 | type ClientProceduresProxy

= { 48 | [K in keyof P]: P[K] extends Procedure 49 | ? ProcedureProxyMethods 50 | : P[K] extends Procedures 51 | ? ClientProceduresProxy 52 | : never; 53 | }; 54 | 55 | export type Client

= ClientProceduresProxy

; 56 | 57 | const noop = () => { 58 | // noop 59 | }; 60 | 61 | interface ProxyCallbackOptions { 62 | path: string[]; 63 | args: any[]; 64 | } 65 | type ProxyCallback = (opts: ProxyCallbackOptions) => unknown; 66 | 67 | const clientMethodMap = { 68 | query: "query", 69 | mutate: "mutation", 70 | subscribe: "subscription", 71 | } as const; 72 | 73 | export function createProceduresProxy( 74 | callback: ProxyCallback, 75 | path: string[] = [], 76 | ): T { 77 | return new Proxy(noop, { 78 | get(_, key) { 79 | if (typeof key !== "string") return; 80 | 81 | return createProceduresProxy(callback, [...path, key]); 82 | }, 83 | apply(_1, _2, args) { 84 | return callback({ args, path }); 85 | }, 86 | }) as T; 87 | } 88 | 89 | export function createClient

( 90 | execute: ExecuteFn, 91 | ): Client

{ 92 | const client = new UntypedClient(execute); 93 | 94 | return createProceduresProxy>(({ args, path }) => { 95 | const procedureType = 96 | clientMethodMap[path.pop() as keyof typeof clientMethodMap]; 97 | 98 | const pathString = path.join("."); 99 | 100 | return (client[procedureType] as any)(pathString, ...args); 101 | }); 102 | } 103 | 104 | export function getQueryKey( 105 | path: string, 106 | input: unknown, 107 | ): [string] | [string, unknown] { 108 | return input === undefined ? [path] : [path, input]; 109 | } 110 | 111 | export function traverseClient

( 112 | client: Client, 113 | path: string[], 114 | ): ProcedureProxyMethods

{ 115 | let ret: ClientProceduresProxy = client; 116 | 117 | for (const segment of path) { 118 | ret = ret[segment]; 119 | } 120 | 121 | return ret as any; 122 | } 123 | -------------------------------------------------------------------------------- /packages/client/src/next/types.ts: -------------------------------------------------------------------------------- 1 | import { Observable } from "./UntypedClient"; 2 | 3 | export type JoinPath< 4 | TPath extends string, 5 | TNext extends string, 6 | > = TPath extends "" ? TNext : `${TPath}.${TNext}`; 7 | 8 | export type ProcedureKind = "query" | "mutation" | "subscription"; 9 | 10 | export type Procedure = { 11 | kind: ProcedureKind; 12 | input: unknown; 13 | output: unknown; 14 | error: unknown; 15 | }; 16 | 17 | export type Procedures = { 18 | [K in string]: Procedure | Procedures; 19 | }; 20 | 21 | export type Result = 22 | | { status: "ok"; data: Ok } 23 | | { status: "err"; error: Err }; 24 | 25 | export type ProcedureResult

= Result< 26 | P["output"], 27 | P["error"] 28 | >; 29 | 30 | export interface SubscriptionObserver { 31 | onStarted: () => void; 32 | onData: (value: TValue) => void; 33 | onError: (err: TError) => void; 34 | onStopped: () => void; 35 | onComplete: () => void; 36 | } 37 | 38 | export type ExecuteArgs = { 39 | type: ProcedureKind; 40 | path: string; 41 | input: unknown; 42 | }; 43 | export type ExecuteFn = (args: ExecuteArgs) => Observable; 44 | 45 | export type ExeceuteData = { code: number; value: any } | null; 46 | // | { type: "event"; data: any } 47 | // | { type: "response"; data: any } 48 | // | { 49 | // type: "error"; 50 | // data: { code: number; data: any }; 51 | // }; 52 | -------------------------------------------------------------------------------- /packages/client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "dist", 5 | "declarationMap": true 6 | }, 7 | "include": ["src/**/*"], 8 | "exclude": ["dist/**/*"] 9 | } 10 | -------------------------------------------------------------------------------- /packages/query-core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@rspc/query-core", 3 | "version": "0.3.1", 4 | "description": "A blazing fast and easy to use TRPC-like server for Rust.", 5 | "keywords": [], 6 | "author": "Oscar Beaumont", 7 | "license": "MIT", 8 | "type": "module", 9 | "main": "dist/index.cjs", 10 | "types": "dist/index.d.ts", 11 | "exports": { 12 | "./package.json": "./package.json", 13 | ".": { 14 | "types": "./dist/index.d.ts", 15 | "import": "./dist/index.js", 16 | "default": "./dist/index.cjs" 17 | } 18 | }, 19 | "files": [ 20 | "dist" 21 | ], 22 | "scripts": { 23 | "dev": "tsup --watch", 24 | "build": "tsup", 25 | "prepare": "tsup" 26 | }, 27 | "dependencies": { 28 | "@tanstack/query-core": "^5.66.0" 29 | }, 30 | "devDependencies": { 31 | "tsup": "^8.3.6", 32 | "typescript": "^5.7.3" 33 | }, 34 | "peerDependencies": { 35 | "@rspc/client": "workspace:*" 36 | }, 37 | "tsup": { 38 | "entry": [ 39 | "src/index.ts" 40 | ], 41 | "format": [ 42 | "esm", 43 | "cjs" 44 | ], 45 | "dts": true, 46 | "splitting": true, 47 | "clean": true, 48 | "sourcemap": true 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /packages/query-core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "dist" 5 | }, 6 | "include": ["src/**/*"], 7 | "exclude": ["dist/**/*"] 8 | } 9 | -------------------------------------------------------------------------------- /packages/react-query/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@rspc/react-query", 3 | "version": "0.3.1", 4 | "description": "A blazing fast and easy to use TRPC-like server for Rust.", 5 | "keywords": [], 6 | "author": "Oscar Beaumont", 7 | "license": "MIT", 8 | "type": "module", 9 | "main": "dist/index.cjs", 10 | "types": "dist/index.d.ts", 11 | "exports": { 12 | "./package.json": "./package.json", 13 | ".": { 14 | "types": "./dist/index.d.ts", 15 | "import": "./dist/index.js", 16 | "default": "./dist/index.cjs" 17 | } 18 | }, 19 | "files": [ 20 | "dist" 21 | ], 22 | "scripts": { 23 | "dev": "tsup --watch", 24 | "build": "tsup", 25 | "prepare": "tsup" 26 | }, 27 | "tsup": { 28 | "entry": [ 29 | "src/index.tsx" 30 | ], 31 | "format": [ 32 | "esm", 33 | "cjs" 34 | ], 35 | "dts": true, 36 | "splitting": true, 37 | "clean": true, 38 | "sourcemap": true 39 | }, 40 | "dependencies": { 41 | "@rspc/client": "workspace:*", 42 | "@rspc/query-core": "workspace:*" 43 | }, 44 | "devDependencies": { 45 | "@tanstack/react-query": "^5.66.0", 46 | "@types/react": "^19.0.8", 47 | "react": "^19.0.0", 48 | "tsup": "^8.3.6", 49 | "typescript": "^5.7.3" 50 | }, 51 | "peerDependencies": { 52 | "@tanstack/react-query": "^5.0.0", 53 | "react": "^18.0.0" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /packages/react-query/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "jsx": "react-jsx", 5 | "outDir": "dist" 6 | }, 7 | "include": ["src/**/*"], 8 | "exclude": ["dist/**/*"] 9 | } 10 | -------------------------------------------------------------------------------- /packages/solid-query/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@rspc/solid-query", 3 | "version": "0.3.1", 4 | "description": "A blazing fast and easy to use TRPC-like server for Rust.", 5 | "keywords": [], 6 | "author": "Oscar Beaumont", 7 | "license": "MIT", 8 | "type": "module", 9 | "source": "src/index.tsx", 10 | "main": "dist/cjs/index.js", 11 | "module": "dist/esm/index.js", 12 | "types": "dist/types/index.d.ts", 13 | "files": [ 14 | "dist" 15 | ], 16 | "exports": { 17 | ".": { 18 | "types": "./dist/types/index.d.ts", 19 | "solid": "./dist/source/index.jsx", 20 | "import": "./dist/esm/index.js", 21 | "browser": "./dist/esm/index.js", 22 | "require": "./dist/cjs/index.js", 23 | "node": "./dist/cjs/index.js" 24 | } 25 | }, 26 | "scripts": { 27 | "dev": "rollup -c -w", 28 | "build": "rollup -c && tsc", 29 | "prepare": "rollup -c && tsc" 30 | }, 31 | "dependencies": { 32 | "@rspc/client": "workspace:*", 33 | "@rspc/query-core": "workspace:*" 34 | }, 35 | "devDependencies": { 36 | "@tanstack/solid-query": "^5.66.0", 37 | "rollup": "^4.34.4", 38 | "rollup-preset-solid": "^2.0.1", 39 | "solid-js": "^1.9.4", 40 | "turbo": "^2.4.0", 41 | "typescript": "^5.7.3" 42 | }, 43 | "peerDependencies": { 44 | "@tanstack/solid-query": "^5.0.0", 45 | "solid-js": "^1.0.0" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /packages/solid-query/rollup.config.js: -------------------------------------------------------------------------------- 1 | import withSolid from "rollup-preset-solid"; 2 | 3 | export default withSolid({ 4 | targets: ["esm", "cjs"], 5 | }); 6 | -------------------------------------------------------------------------------- /packages/solid-query/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "jsx": "preserve", 5 | "jsxImportSource": "solid-js", 6 | "outDir": "dist/types" 7 | }, 8 | "include": ["src/**/*"], 9 | "exclude": ["dist/**/*"] 10 | } 11 | -------------------------------------------------------------------------------- /packages/svelte-query/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@rspc/svelte-query", 3 | "version": "0.3.1", 4 | "description": "A blazingly fast and easy to use tRPC-like server for Rust.", 5 | "keywords": [], 6 | "author": "Oscar Beaumont", 7 | "license": "MIT", 8 | "type": "module", 9 | "source": "./src/index.tsx", 10 | "types": "./dist/index.d.ts", 11 | "main": "./dist/index.js", 12 | "module": "./dist/index.js", 13 | "svelte": "./dist/index.js", 14 | "exports": { 15 | ".": { 16 | "types": "./dist/index.d.ts", 17 | "import": "./dist/index.js", 18 | "svelte": "./dist/index.js", 19 | "default": "./dist/index.js" 20 | }, 21 | "./package.json": "./package.json" 22 | }, 23 | "files": [ 24 | "dist" 25 | ], 26 | "scripts": { 27 | "build": "svelte-package --input ./src --output ./dist", 28 | "test": "vitest" 29 | }, 30 | "dependencies": { 31 | "@rspc/client": "workspace:*", 32 | "@rspc/query-core": "workspace:*" 33 | }, 34 | "devDependencies": { 35 | "@rspc/client": "workspace:*", 36 | "@sveltejs/package": "^2.3.10", 37 | "@tanstack/svelte-query": "^5.66.0", 38 | "tslib": "^2.8.1", 39 | "typescript": "^5.7.3", 40 | "vitest": "^3.0.5" 41 | }, 42 | "peerDependencies": { 43 | "@tanstack/svelte-query": "^5.0.0", 44 | "svelte": ">=3 <5" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /packages/svelte-query/src/RspcProvider.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /packages/svelte-query/src/context.ts: -------------------------------------------------------------------------------- 1 | import type { ProceduresDef } from "@rspc/client"; 2 | import { getContext, setContext } from "svelte"; 3 | import type { Context } from "@rspc/query-core"; 4 | 5 | const _contextKey = "$$_rspcClient"; 6 | 7 | /** Retrieves a Client from Svelte's context */ 8 | export const getRspcClientContext = < 9 | P extends ProceduresDef, 10 | >(): Context

| null => { 11 | const ctx = getContext(_contextKey) ?? null; 12 | return ctx as Context

| null; 13 | }; 14 | 15 | /** Sets a Client on Svelte's context */ 16 | export const setRspcClientContext = (ctx: Context) => 17 | setContext(_contextKey, ctx); 18 | -------------------------------------------------------------------------------- /packages/svelte-query/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "dist/types" 5 | }, 6 | "include": ["src/**/*"], 7 | "exclude": ["dist/**/*"] 8 | } 9 | -------------------------------------------------------------------------------- /packages/tanstack-query/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@rspc/query-core", 3 | "version": "0.3.1", 4 | "description": "A blazing fast and easy to use TRPC-like server for Rust.", 5 | "keywords": [], 6 | "author": "Oscar Beaumont", 7 | "license": "MIT", 8 | "type": "module", 9 | "main": "dist/index.cjs", 10 | "types": "dist/index.d.ts", 11 | "exports": { 12 | "./package.json": "./package.json", 13 | ".": { 14 | "types": "./dist/index.d.ts", 15 | "import": "./dist/index.js", 16 | "default": "./dist/index.cjs" 17 | } 18 | }, 19 | "files": [ 20 | "dist" 21 | ], 22 | "scripts": { 23 | "dev": "tsup --watch", 24 | "build": "tsup", 25 | "prepare": "tsup" 26 | }, 27 | "dependencies": { 28 | "@tanstack/query-core": "^5.66.0" 29 | }, 30 | "devDependencies": { 31 | "tsup": "^8.3.6", 32 | "typescript": "^5.7.3" 33 | }, 34 | "peerDependencies": { 35 | "@rspc/client": "workspace:*" 36 | }, 37 | "tsup": { 38 | "entry": [ 39 | "src/index.ts" 40 | ], 41 | "format": [ 42 | "esm", 43 | "cjs" 44 | ], 45 | "dts": true, 46 | "splitting": true, 47 | "clean": true, 48 | "sourcemap": true 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /packages/tanstack-query/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "dist" 5 | }, 6 | "include": ["src/**/*"], 7 | "exclude": ["dist/**/*"] 8 | } 9 | -------------------------------------------------------------------------------- /packages/tauri/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@rspc/tauri", 3 | "version": "0.3.1", 4 | "description": "A blazing fast and easy to use TRPC-like server for Rust.", 5 | "keywords": [], 6 | "author": "Oscar Beaumont", 7 | "license": "MIT", 8 | "type": "module", 9 | "main": "dist/index.cjs", 10 | "exports": { 11 | "./package.json": "./package.json", 12 | ".": { 13 | "types": "./dist/index.d.ts", 14 | "import": "./dist/index.js", 15 | "default": "./dist/index.cjs" 16 | }, 17 | "./next": { 18 | "types": "./dist/next.d.ts", 19 | "import": "./dist/next.js", 20 | "default": "./dist/next.cjs" 21 | } 22 | }, 23 | "types": "dist/index.d.ts", 24 | "files": [ 25 | "dist" 26 | ], 27 | "scripts": { 28 | "dev": "tsup --watch", 29 | "build": "tsup", 30 | "prepare": "tsup" 31 | }, 32 | "tsup": { 33 | "entry": [ 34 | "src/index.ts", 35 | "src/next.ts" 36 | ], 37 | "format": [ 38 | "esm", 39 | "cjs" 40 | ], 41 | "dts": { 42 | "resolve": true 43 | }, 44 | "splitting": true, 45 | "clean": true, 46 | "sourcemap": true 47 | }, 48 | "dependencies": { 49 | "@rspc/client": "workspace:*" 50 | }, 51 | "devDependencies": { 52 | "@tauri-apps/api": "^2.2.0", 53 | "tsup": "^8.3.6", 54 | "typescript": "^5.7.3" 55 | }, 56 | "peerDependencies": { 57 | "@tauri-apps/api": "^2.1.1" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /packages/tauri/src/index.ts: -------------------------------------------------------------------------------- 1 | import { randomId, OperationType, Transport, RSPCError } from "@rspc/client"; 2 | import { listen, UnlistenFn } from "@tauri-apps/api/event"; 3 | import { getCurrentWindow } from "@tauri-apps/api/window"; 4 | 5 | export class TauriTransport implements Transport { 6 | private requestMap = new Map void>(); 7 | private listener?: Promise; 8 | clientSubscriptionCallback?: (id: string, value: any) => void; 9 | 10 | constructor() { 11 | this.listener = listen("plugin:rspc:transport:resp", (event) => { 12 | const { id, result } = event.payload as any; 13 | if (result.type === "event") { 14 | if (this.clientSubscriptionCallback) 15 | this.clientSubscriptionCallback(id, result.data); 16 | } else if (result.type === "response") { 17 | if (this.requestMap.has(id)) { 18 | this.requestMap.get(id)?.({ type: "response", result: result.data }); 19 | this.requestMap.delete(id); 20 | } 21 | } else if (result.type === "error") { 22 | const { message, code } = result.data; 23 | if (this.requestMap.has(id)) { 24 | this.requestMap.get(id)?.({ type: "error", message, code }); 25 | this.requestMap.delete(id); 26 | } 27 | } else { 28 | console.error(`Received event of unknown method '${result.type}'`); 29 | } 30 | }); 31 | } 32 | 33 | async doRequest( 34 | operation: OperationType, 35 | key: string, 36 | input: any, 37 | ): Promise { 38 | if (!this.listener) { 39 | await this.listener; 40 | } 41 | 42 | const id = randomId(); 43 | let resolve: (data: any) => void; 44 | const promise = new Promise((res) => { 45 | resolve = res; 46 | }); 47 | 48 | // @ts-ignore 49 | this.requestMap.set(id, resolve); 50 | 51 | await getCurrentWindow().emit("plugin:rspc:transport", { 52 | id, 53 | method: operation, 54 | params: { 55 | path: key, 56 | input, 57 | }, 58 | }); 59 | 60 | const body = (await promise) as any; 61 | if (body.type === "error") { 62 | const { code, message } = body; 63 | throw new RSPCError(code, message); 64 | } else if (body.type === "response") { 65 | return body.result; 66 | } else { 67 | throw new Error( 68 | `RSPC Tauri doRequest received invalid body type '${body?.type}'`, 69 | ); 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /packages/tauri/src/next.ts: -------------------------------------------------------------------------------- 1 | import { Channel, invoke } from "@tauri-apps/api/core"; 2 | import { ExecuteArgs, ExecuteFn, observable } from "@rspc/client/next"; 3 | 4 | type Request = 5 | | { method: "request"; params: { path: string; input: any } } 6 | | { method: "abort"; params: number }; 7 | 8 | type Response = { code: number; value: T } | null; 9 | 10 | // TODO: Seal `Channel` within a standard interface for all "modern links"? 11 | // TODO: handle detect and converting to rspc error class 12 | // TODO: Catch Tauri errors -> Assuming it would happen on `tauri::Error` which happens when serialization fails in Rust. 13 | // TODO: Return closure for cleanup 14 | 15 | export async function handleRpc(req: Request, channel: Channel>) { 16 | await invoke("plugin:rspc|handle_rpc", { req, channel }); 17 | } 18 | 19 | export const tauriExecute: ExecuteFn = (args: ExecuteArgs) => { 20 | return observable((subscriber) => { 21 | const channel = new Channel>(); 22 | 23 | channel.onmessage = (response) => { 24 | if (response === null) subscriber.complete(); 25 | return subscriber.next(response); 26 | }; 27 | 28 | handleRpc( 29 | { 30 | method: "request", 31 | params: { 32 | path: args.path, 33 | input: 34 | args.input === undefined || args.input === null ? null : args.input, 35 | }, 36 | }, 37 | channel, 38 | ); 39 | }); 40 | }; 41 | -------------------------------------------------------------------------------- /packages/tauri/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "include": ["src/**/*"], 4 | "exclude": ["dist/**/*"], 5 | "compilerOptions": { 6 | "moduleResolution": "bundler" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "moduleResolution": "node", 4 | "lib": ["ESNext", "DOM"], 5 | "strict": true, 6 | "target": "ESNext", 7 | "skipLibCheck": true, 8 | "declaration": true, 9 | "emitDeclarationOnly": true, 10 | "declarationMap": true, 11 | "paths": { 12 | "@rspc/client": ["./client/src"], 13 | "@rspc/query-core": ["./query-core/src"], 14 | "@rspc/react-query": ["./react-query/src"], 15 | "@rspc/solid-query": ["./react-query/src"], 16 | "@rspc/svelte-query": ["./react-query/src"], 17 | "@rspc/tauri": ["./tauri/src"] 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - packages/* 3 | - examples/astro 4 | - examples/nextjs 5 | - examples/tauri 6 | - examples/sfm 7 | -------------------------------------------------------------------------------- /publish.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # TODO: Replace this with some proper tool 3 | # TODO: Detect if the package has changed and a release is required 4 | 5 | set -e 6 | 7 | cd crates/core/ 8 | cargo publish 9 | cd ../../ 10 | 11 | cd crates/procedure/ 12 | cargo publish 13 | cd ../../ 14 | 15 | cd crates/legacy/ 16 | cargo publish 17 | cd ../../ 18 | 19 | cd integrations/axum/ 20 | cargo publish 21 | cd .. 22 | 23 | cd integrations/tauri/ 24 | cargo publish 25 | cd .. 26 | 27 | cd rspc/ 28 | cargo publish 29 | cd .. 30 | -------------------------------------------------------------------------------- /rspc/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rspc" 3 | description = "A blazing fast and easy to use TRPC server for Rust." 4 | version = "0.4.1" 5 | authors = ["Oscar Beaumont "] 6 | edition = "2021" 7 | license = "MIT" 8 | include = ["/src", "/LICENCE", "/README.md"] 9 | repository = "https://github.com/specta-rs/rspc" 10 | documentation = "https://docs.rs/rspc/latest/rspc" 11 | keywords = ["async", "specta", "rust-to-ts", "typescript", "typesafe"] 12 | categories = ["web-programming", "asynchronous"] 13 | 14 | # /bin/sh RUSTDOCFLAGS="--cfg docsrs" cargo +nightly doc --all-features 15 | [package.metadata."docs.rs"] 16 | all-features = true 17 | rustdoc-args = ["--cfg", "docsrs"] 18 | 19 | [features] 20 | default = ["legacy"] # TODO: Legacy shouldn't be a default feature -> we need it for the legacy bindings syntax 21 | 22 | typescript = ["dep:specta-typescript", "dep:serde_json"] 23 | rust = [] # TODO: "dep:specta-rust"] 24 | 25 | # TODO: Remove 26 | legacy = ["dep:rspc-legacy", "dep:serde_json"] 27 | 28 | [dependencies] 29 | # Public 30 | rspc-procedure = { version = "0.0.1", path = "../crates/procedure" } 31 | rspc-legacy = { version = "0.0.1", path = "../crates/legacy", optional = true } 32 | serde = { workspace = true } 33 | futures-util = { workspace = true, features = ["alloc"] } 34 | specta = { workspace = true, features = [ 35 | "serde", 36 | "serde_json", 37 | "derive", # TODO: remove this 38 | ] } 39 | 40 | # Private 41 | specta-typescript = { workspace = true, optional = true, features = [] } 42 | serde_json = { workspace = true, optional = true } # TODO: Make this optional. Right now the legacy stuff needs it. 43 | # specta-rust = { git = "https://github.com/specta-rs/specta", optional = true, rev = "bf3a0937cceb29eca11df207076b9e1b942ba7bb" } 44 | 45 | [lints] 46 | workspace = true 47 | -------------------------------------------------------------------------------- /rspc/src/as_date.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | pub struct AsDate(T); 4 | 5 | impl fmt::Debug for AsDate { 6 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 7 | self.0.fmt(f) 8 | } 9 | } 10 | 11 | // TODO: Trait passthroughs (`Debug`, `Clone`, `Deserialize`, etc) + `Deref` & `Into` impls 12 | // TODO: make generic over any `T: Serialize`??? 13 | // impl Serialize for AsDate> { 14 | // fn serialize(&self, serializer: S) -> Result 15 | // where 16 | // S: serde::Serializer, 17 | // { 18 | // // TODO: Should we require a `thread_local` to enable this impl so types are reusable??? 19 | // // TODO: What if the rspc client wants it in a string format? 20 | // let mut s = serializer.serialize_struct("AsDate", 2)?; 21 | // s.serialize_field("~rspc~.date", &true)?; 22 | // s.serialize_field("~rspc~.value", &self.0)?; 23 | // s.end() 24 | // } 25 | // } 26 | -------------------------------------------------------------------------------- /rspc/src/error.rs: -------------------------------------------------------------------------------- 1 | use rspc_procedure::ProcedureError; 2 | use specta::Type; 3 | 4 | // TODO: Drop bounds on this cause they can be added at the impl. 5 | pub trait Error: Type + 'static { 6 | fn into_procedure_error(self) -> ProcedureError; 7 | } 8 | -------------------------------------------------------------------------------- /rspc/src/extension.rs: -------------------------------------------------------------------------------- 1 | use std::marker::PhantomData; 2 | 3 | use rspc_procedure::State; 4 | 5 | use crate::ProcedureMeta; 6 | 7 | // TODO: `TError`? 8 | // TODO: Explain executor order and why to use over `Middleware`? 9 | pub struct Extension { 10 | pub(crate) setup: Option>, 11 | pub(crate) phantom: PhantomData (TCtx, TInput, TResult)>, 12 | // pub(crate) inner: Box< 13 | // dyn FnOnce( 14 | // MiddlewareHandler, 15 | // ) -> MiddlewareHandler, 16 | // >, 17 | } 18 | 19 | // TODO: Debug impl 20 | 21 | impl Extension { 22 | // TODO: Take in map function 23 | pub fn new() -> Self { 24 | Self { 25 | setup: None, 26 | phantom: PhantomData, 27 | } 28 | } 29 | 30 | // TODO: Allow multiple or error if defined multiple times? 31 | pub fn setup(mut self, func: impl FnOnce(&mut State, ProcedureMeta) + 'static) -> Self { 32 | self.setup = Some(Box::new(func)); 33 | self 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /rspc/src/languages.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "rust")] 2 | #[cfg_attr(docsrs, doc(cfg(feature = "rust")))] 3 | mod rust; 4 | #[cfg(feature = "typescript")] 5 | #[cfg_attr(docsrs, doc(cfg(feature = "typescript")))] 6 | mod typescript; 7 | 8 | // #[cfg(feature = "rust")] 9 | // #[cfg_attr(docsrs, doc(cfg(feature = "rust")))] 10 | // pub use rust::Rust; // TODO 11 | #[cfg(feature = "typescript")] 12 | #[cfg_attr(docsrs, doc(cfg(feature = "typescript")))] 13 | pub use typescript::Typescript; 14 | -------------------------------------------------------------------------------- /rspc/src/languages/rust.rs: -------------------------------------------------------------------------------- 1 | //! TODO: Bring this back when published. 2 | // use std::{borrow::Cow, collections::HashMap, path::Path}; 3 | 4 | // use specta::datatype::DataType; 5 | // use specta_typescript::ExportError; 6 | 7 | // use crate::{procedure::ProcedureType, types::TypesOrType, ProcedureKind, Types}; 8 | 9 | // pub struct Rust(()); // TODO: specta_rust::Rust 10 | 11 | // // TODO: Traits - `Debug`, `Clone`, etc 12 | 13 | // impl Default for Rust { 14 | // fn default() -> Self { 15 | // Self(()) // TODO: specta_typescript::Typescript::default().framework_header("// This file was generated by [rspc](https://github.com/specta-rs/rspc). Do not edit this file manually.") 16 | // } 17 | // } 18 | 19 | // impl Rust { 20 | // // TODO: Clone all methods from `specta_rust::Rust` 21 | 22 | // pub fn export_to(&self, path: impl AsRef, types: &Types) -> Result<(), ExportError> { 23 | // std::fs::write(path, self.export(types)?)?; 24 | // // TODO: Format file 25 | // Ok(()) 26 | // } 27 | 28 | // pub fn export(&self, types: &Types) -> Result { 29 | // println!("WARNING: `rspc::Rust` is an unstable feature! Use at your own discretion!"); 30 | 31 | // let mut s = "//! This file was generated by [rspc](https://github.com/specta-rs/rspc). Do not edit this file manually.\n\npub struct Procedures;\n\n".to_string(); 32 | 33 | // // TODO: Move to `specta_rust::Rust` which should handle this like we do with Typescript. 34 | // for (_, ty) in types.types.into_iter() { 35 | // s.push_str(&specta_rust::export_named_datatype(ty).unwrap()) 36 | // } 37 | 38 | // // TODO: disabling warning on the output??? 39 | 40 | // for (key, item) in types.procedures.clone().into_iter() { 41 | // export(&mut s, key.to_string(), key, item); 42 | // } 43 | 44 | // Ok(s) 45 | // } 46 | // } 47 | 48 | // fn export(s: &mut String, full_key: String, ident: Cow<'static, str>, item: TypesOrType) { 49 | // match item { 50 | // TypesOrType::Type(ty) => { 51 | // let kind = match ty.kind { 52 | // ProcedureKind::Query => "Query", 53 | // ProcedureKind::Mutation => "Mutation", 54 | // ProcedureKind::Subscription => "Subscription", 55 | // }; 56 | 57 | // let input = specta_rust::datatype(&ty.input).unwrap(); 58 | // let output = specta_rust::datatype(&ty.output).unwrap(); 59 | // let error = "()"; // TODO: specta_rust::datatype(&ty.error).unwrap(); 60 | 61 | // s.push_str(&format!( 62 | // r#"pub struct {ident}; 63 | 64 | // impl rspc_client::Procedure for {ident} {{ 65 | // type Input = {input}; 66 | // type Output = {output}; 67 | // type Error = {error}; 68 | // type Procedures = Procedures; 69 | // const KIND: rspc_client::ProcedureKind = rspc_client::ProcedureKind::{kind}; 70 | // const KEY: &'static str = "{ident}"; 71 | // }} 72 | 73 | // "# 74 | // )); 75 | // } 76 | // TypesOrType::Types(inner) => { 77 | // for (key, item) in inner { 78 | // s.push_str(&format!("\npub mod {key} {{\n")); 79 | // s.push_str(&"\tpub use super::Procedures;\n"); 80 | // export(s, format!("{full_key}.{key}"), key, item); // TODO: Inset all items by the correct nuber of tabs 81 | // s.push_str("}\n"); 82 | // } 83 | // } 84 | // } 85 | // } 86 | -------------------------------------------------------------------------------- /rspc/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! rspc is a framework for building typesafe web backends in Rust. 2 | //! 3 | //! Powered by [Specta](https://docs.rs/specta)'s powerful language exporting, rspc comes with integrations for [Axum](https://docs.rs/axum) and [Tauri](https://docs.rs/tauri) out of the box. This project brings the next level developer experience inspired by [tRPC](https://trpc.io) to your Rust stack. 4 | //! 5 | //! ## WARNING 6 | //! 7 | //! Checkout the official docs at . This documentation is generally written **for authors of middleware and adapter**. 8 | //! 9 | // #![forbid(unsafe_code)] // TODO 10 | #![cfg_attr(docsrs, feature(doc_cfg))] 11 | #![doc( 12 | html_logo_url = "https://github.com/specta-rs/rspc/raw/main/.github/logo.png", 13 | html_favicon_url = "https://github.com/specta-rs/rspc/raw/main/.github/logo.png" 14 | )] 15 | 16 | pub mod middleware; 17 | 18 | mod as_date; 19 | mod error; 20 | mod extension; 21 | mod languages; 22 | mod procedure; 23 | mod procedure_kind; 24 | mod router; 25 | mod stream; 26 | mod types; 27 | pub(crate) mod util; 28 | 29 | #[cfg(feature = "legacy")] 30 | #[cfg_attr(docsrs, doc(cfg(feature = "legacy")))] 31 | pub mod legacy; 32 | 33 | pub use as_date::AsDate; 34 | pub use error::Error; 35 | pub use extension::Extension; 36 | #[allow(unused)] 37 | pub use languages::*; 38 | pub use procedure::{ 39 | ErasedProcedure, Procedure, ProcedureBuilder, ProcedureMeta, ResolverInput, ResolverOutput, 40 | }; 41 | pub use procedure_kind::ProcedureKind; 42 | pub use router::Router; 43 | pub use stream::Stream; 44 | pub use types::Types; 45 | 46 | // We only re-export types that are useful for a general user. 47 | pub use rspc_procedure::{ 48 | flush, DynInput, ProcedureError, ProcedureStream, Procedures, ResolverError, State, 49 | }; 50 | 51 | // TODO: Potentially remove these once Axum stuff is sorted. 52 | pub use rspc_procedure::{DynOutput, ProcedureStreamMap}; 53 | -------------------------------------------------------------------------------- /rspc/src/middleware.rs: -------------------------------------------------------------------------------- 1 | mod into_middleware; 2 | mod middleware; 3 | mod next; 4 | 5 | pub use middleware::Middleware; 6 | pub use next::Next; 7 | 8 | pub(crate) use into_middleware::IntoMiddleware; 9 | pub(crate) use middleware::MiddlewareHandler; 10 | -------------------------------------------------------------------------------- /rspc/src/middleware/into_middleware.rs: -------------------------------------------------------------------------------- 1 | use std::marker::PhantomData; 2 | 3 | use crate::{Error, Extension, ProcedureBuilder}; 4 | 5 | use super::Middleware; 6 | 7 | // TODO: Expose in public API or seal??? 8 | // TODO: This API could lead to bad errors 9 | pub trait IntoMiddleware { 10 | type TNextCtx; 11 | type I; 12 | type R; 13 | 14 | fn build( 15 | self, 16 | this: ProcedureBuilder, 17 | ) -> ProcedureBuilder; 18 | } 19 | 20 | impl 21 | IntoMiddleware 22 | for Middleware 23 | where 24 | // TODO: This stuff could lead to bad errors 25 | // TODO: Could we move them onto the function instead and constrain on `with` too??? 26 | TError: Error, 27 | TRootCtx: 'static, 28 | TNextCtx: 'static, 29 | TCtx: 'static, 30 | TInput: 'static, 31 | TResult: 'static, 32 | TBaseInput: 'static, 33 | I: 'static, 34 | TBaseResult: 'static, 35 | R: 'static, 36 | { 37 | type TNextCtx = TNextCtx; 38 | type I = I; 39 | type R = R; 40 | 41 | fn build( 42 | self, 43 | this: ProcedureBuilder, 44 | ) -> ProcedureBuilder 45 | { 46 | ProcedureBuilder { 47 | build: Box::new(|ty, mut setups, handler| { 48 | if let Some(setup) = self.setup { 49 | setups.push(setup); 50 | } 51 | 52 | (this.build)(ty, setups, (self.inner)(handler)) 53 | }), 54 | phantom: PhantomData, 55 | } 56 | } 57 | } 58 | 59 | // TODO: Constrain to base types 60 | impl 61 | IntoMiddleware 62 | for Extension 63 | where 64 | // TODO: This stuff could lead to bad errors 65 | // TODO: Could we move them onto the function instead and constrain on `with` too??? 66 | TError: Error, 67 | TRootCtx: 'static, 68 | TCtx: 'static, 69 | TBaseInput: 'static, 70 | I: 'static, 71 | TBaseResult: 'static, 72 | R: 'static, 73 | { 74 | type TNextCtx = TCtx; 75 | type I = I; 76 | type R = R; 77 | 78 | fn build( 79 | self, 80 | this: ProcedureBuilder, 81 | ) -> ProcedureBuilder 82 | { 83 | ProcedureBuilder { 84 | build: Box::new(|ty, mut setups, handler| { 85 | if let Some(setup) = self.setup { 86 | setups.push(setup); 87 | } 88 | 89 | (this.build)(ty, setups, handler) 90 | }), 91 | phantom: PhantomData, 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /rspc/src/middleware/next.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | use crate::{middleware::MiddlewareHandler, procedure::ProcedureMeta}; 4 | 5 | pub struct Next { 6 | // TODO: `pub(super)` over `pub(crate)` 7 | pub(crate) meta: ProcedureMeta, 8 | pub(crate) next: MiddlewareHandler, 9 | } 10 | 11 | impl fmt::Debug for Next { 12 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 13 | f.debug_struct("Next").finish() 14 | } 15 | } 16 | 17 | impl Next 18 | where 19 | TCtx: 'static, 20 | TInput: 'static, 21 | TReturn: 'static, 22 | { 23 | pub fn meta(&self) -> ProcedureMeta { 24 | self.meta.clone() 25 | } 26 | 27 | pub async fn exec(&self, ctx: TCtx, input: TInput) -> Result { 28 | (self.next)(ctx, input, self.meta.clone()).await 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /rspc/src/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod middleware; 2 | pub mod procedure; 3 | 4 | mod error; 5 | mod extension; 6 | mod infallible; 7 | mod stream; 8 | // pub use crate::procedure::Procedure2; 9 | pub use error::Error; 10 | // pub use infallible::Infallible; 11 | pub use extension::Extension; 12 | pub use stream::Stream; 13 | -------------------------------------------------------------------------------- /rspc/src/procedure/erased.rs: -------------------------------------------------------------------------------- 1 | use std::{panic::Location, sync::Arc}; 2 | 3 | use specta::TypeCollection; 4 | 5 | use crate::{procedure::ProcedureType, ProcedureKind, State}; 6 | 7 | pub struct ErasedProcedure { 8 | pub(crate) setup: Vec>, 9 | pub(crate) location: Location<'static>, 10 | pub(crate) kind: ProcedureKind, 11 | pub(crate) inner: Box< 12 | dyn FnOnce( 13 | Arc, 14 | &mut TypeCollection, 15 | ) -> (rspc_procedure::Procedure, ProcedureType), 16 | >, 17 | } 18 | 19 | // TODO: `Debug`, `PartialEq`, `Eq`, `Hash` 20 | 21 | impl ErasedProcedure { 22 | // TODO: Expose all fields 23 | 24 | // TODO: Make `pub` 25 | // pub(crate) fn kind(&self) -> ProcedureKind2 { 26 | // self.kind 27 | // } 28 | 29 | // /// Export the [Specta](https://docs.rs/specta) types for this procedure. 30 | // /// 31 | // /// TODO - Use this with `rspc::typescript` 32 | // /// 33 | // /// # Usage 34 | // /// 35 | // /// ```rust 36 | // /// todo!(); # TODO: Example 37 | // /// ``` 38 | // pub fn ty(&self) -> &ProcedureTypeDefinition { 39 | // &self.ty 40 | // } 41 | 42 | // /// Execute a procedure with the given context and input. 43 | // /// 44 | // /// This will return a [`ProcedureStream`] which can be used to stream the result of the procedure. 45 | // /// 46 | // /// # Usage 47 | // /// 48 | // /// ```rust 49 | // /// use serde_json::Value; 50 | // /// 51 | // /// fn run_procedure(procedure: Procedure) -> Vec { 52 | // /// procedure 53 | // /// .exec((), Value::Null) 54 | // /// .collect::>() 55 | // /// .await 56 | // /// .into_iter() 57 | // /// .map(|result| result.serialize(serde_json::value::Serializer).unwrap()) 58 | // /// .collect::>() 59 | // /// } 60 | // /// ``` 61 | // pub fn exec<'de, T: ProcedureInput<'de>>( 62 | // &self, 63 | // ctx: TCtx, 64 | // input: T, 65 | // ) -> Result { 66 | // match input.into_deserializer() { 67 | // Ok(deserializer) => { 68 | // let mut input = ::erase(deserializer); 69 | // (self.handler)(ctx, &mut input) 70 | // } 71 | // Err(input) => (self.handler)(ctx, &mut AnyInput(Some(input.into_value()))), 72 | // } 73 | // } 74 | } 75 | -------------------------------------------------------------------------------- /rspc/src/procedure/meta.rs: -------------------------------------------------------------------------------- 1 | use std::{borrow::Cow, sync::Arc}; 2 | 3 | // #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, specta::Type)] 4 | // #[specta(rename_all = "camelCase")] 5 | // pub enum ProcedureKind { 6 | // Query, 7 | // Mutation, 8 | // Subscription, 9 | // } 10 | 11 | // impl fmt::Display for ProcedureKind { 12 | // fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 13 | // match self { 14 | // Self::Query => write!(f, "Query"), 15 | // Self::Mutation => write!(f, "Mutation"), 16 | // Self::Subscription => write!(f, "Subscription"), 17 | // } 18 | // } 19 | // } 20 | 21 | use crate::{ProcedureKind, State}; 22 | 23 | #[derive(Debug, Clone)] 24 | enum ProcedureName { 25 | Static(&'static str), 26 | Dynamic(Arc), 27 | } 28 | 29 | #[derive(Debug, Clone)] 30 | pub struct ProcedureMeta { 31 | name: ProcedureName, 32 | kind: ProcedureKind, 33 | state: Arc, 34 | } 35 | 36 | impl ProcedureMeta { 37 | pub(crate) fn new(name: Cow<'static, str>, kind: ProcedureKind, state: Arc) -> Self { 38 | Self { 39 | name: ProcedureName::Dynamic(Arc::new(name.into_owned())), 40 | kind, 41 | state, 42 | } 43 | } 44 | } 45 | 46 | impl ProcedureMeta { 47 | pub fn name(&self) -> &str { 48 | match &self.name { 49 | ProcedureName::Static(name) => name, 50 | ProcedureName::Dynamic(name) => name.as_str(), 51 | } 52 | } 53 | 54 | pub fn kind(&self) -> ProcedureKind { 55 | self.kind 56 | } 57 | 58 | pub fn state(&self) -> &Arc { 59 | &self.state 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /rspc/src/procedure/resolver_input.rs: -------------------------------------------------------------------------------- 1 | // /// The input to a procedure which is derived from an [`ProcedureInput`](crate::procedure::Argument). 2 | // /// 3 | // /// This trait has a built in implementation for any type which implements [`DeserializeOwned`](serde::de::DeserializeOwned). 4 | // /// 5 | // /// ## How this works? 6 | // /// 7 | // /// [`Self::from_value`] will be provided with a [`ProcedureInput`] which wraps the [`Argument::Value`](super::Argument::Value) from the argument provided to the [`Procedure::exec`](super::Procedure) call. 8 | // /// 9 | // /// Input is responsible for converting this value into the type the user specified for the procedure. 10 | // /// 11 | // /// If the type implements [`DeserializeOwned`](serde::de::DeserializeOwned) we will use Serde, otherwise we will attempt to downcast the value. 12 | // /// 13 | // /// ## Implementation for custom types 14 | // /// 15 | // /// Say you have a type `MyCoolThing` which you want to use as an argument to an rspc procedure: 16 | // /// 17 | // /// ``` 18 | // /// pub struct MyCoolThing(pub String); 19 | // /// 20 | // /// impl ResolverInput for MyCoolThing { 21 | // /// fn from_value(value: ProcedureInput) -> Result { 22 | // /// Ok(todo!()) // Refer to ProcedureInput's docs 23 | // /// } 24 | // /// } 25 | // /// 26 | // /// // You should also implement `ProcedureInput`. 27 | // /// 28 | // /// fn usage_within_rspc() { 29 | // /// ::builder().query(|_, _: MyCoolThing| async move { () }); 30 | // /// } 31 | // /// ``` 32 | 33 | // TODO: Should this be in `rspc_procedure`??? 34 | // TODO: Maybe rename? 35 | 36 | use serde::de::DeserializeOwned; 37 | use specta::{datatype::DataType, Type, TypeCollection}; 38 | 39 | /// TODO: Restore the above docs but check they are correct 40 | pub trait ResolverInput: Sized + Send + 'static { 41 | fn data_type(types: &mut TypeCollection) -> DataType; 42 | 43 | /// Convert the [`DynInput`] into the type the user specified for the procedure. 44 | fn from_input(input: rspc_procedure::DynInput) -> Result; 45 | } 46 | 47 | impl ResolverInput for T { 48 | fn data_type(types: &mut TypeCollection) -> DataType { 49 | T::inline(types, specta::Generics::Definition) 50 | } 51 | 52 | fn from_input(input: rspc_procedure::DynInput) -> Result { 53 | Ok(input.deserialize()?) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /rspc/src/procedure_kind.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | use specta::Type; 4 | 5 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Type)] 6 | #[specta(rename_all = "camelCase")] 7 | pub enum ProcedureKind { 8 | Query, 9 | Mutation, 10 | Subscription, 11 | } 12 | 13 | impl fmt::Display for ProcedureKind { 14 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 15 | match self { 16 | Self::Query => write!(f, "Query"), 17 | Self::Mutation => write!(f, "Mutation"), 18 | Self::Subscription => write!(f, "Subscription"), 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /rspc/src/stream.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | pin::Pin, 3 | task::{Context, Poll}, 4 | }; 5 | 6 | use futures_util::StreamExt; 7 | 8 | /// Return a [`Stream`](futures::Stream) of values from a [`Procedure::query`](procedure::ProcedureBuilder::query) or [`Procedure::mutation`](procedure::ProcedureBuilder::mutation). 9 | /// 10 | /// ## Why not a subscription? 11 | /// 12 | /// A [`subscription`](procedure::ProcedureBuilder::subscription) must return a [`Stream`](futures::Stream) so it would be fair to question when you would use this. 13 | /// 14 | /// A [`query`](procedure::ProcedureBuilder::query) or [`mutation`](procedure::ProcedureBuilder::mutation) produce a single result where a subscription produces many discrete values. 15 | /// 16 | /// Using [`rspc::Stream`](Self) within a query or mutation will result in your procedure returning a collection (Eg. `Vec`) of [`Stream::Item`](futures::Stream) on the frontend. 17 | /// 18 | /// This means it would be well suited for streaming the result of a computation or database query while a subscription would be well suited for a chat room. 19 | /// 20 | /// ## Usage 21 | /// **WARNING**: This example shows the low-level procedure API. You should refer to [`Rspc`](crate::Rspc) for the high-level API. 22 | /// ```rust 23 | /// use futures::stream::once; 24 | /// 25 | /// ::builder().query(|_, _: ()| async move { rspc::Stream(once(async move { 42 })) }); 26 | /// ``` 27 | /// 28 | pub struct Stream(pub S); 29 | 30 | // WARNING: We can not add an implementation for `Debug` without breaking `rspc_tracing` 31 | 32 | impl Default for Stream { 33 | fn default() -> Self { 34 | Self(Default::default()) 35 | } 36 | } 37 | 38 | impl Clone for Stream { 39 | fn clone(&self) -> Self { 40 | Self(self.0.clone()) 41 | } 42 | } 43 | 44 | impl futures_util::Stream for Stream { 45 | type Item = S::Item; 46 | 47 | fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 48 | // TODO: Using `pin-project-lite` would be nice but I don't think it supports tuple variants and I don't want the macros of `pin-project`. 49 | unsafe { self.map_unchecked_mut(|v| &mut v.0) }.poll_next_unpin(cx) 50 | } 51 | 52 | fn size_hint(&self) -> (usize, Option) { 53 | self.0.size_hint() 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /rspc/src/types.rs: -------------------------------------------------------------------------------- 1 | use std::{borrow::Cow, collections::BTreeMap, fmt}; 2 | 3 | use specta::TypeCollection; 4 | 5 | use crate::procedure::ProcedureType; 6 | 7 | #[derive(Clone)] 8 | pub(crate) enum TypesOrType { 9 | Type(ProcedureType), 10 | Types(BTreeMap, TypesOrType>), 11 | } 12 | 13 | pub struct Types { 14 | pub(crate) types: TypeCollection, 15 | pub(crate) procedures: BTreeMap, TypesOrType>, 16 | } 17 | 18 | impl fmt::Debug for Types { 19 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 20 | f.debug_struct("Types") 21 | // TODO: Finish this 22 | .finish() 23 | } 24 | } 25 | 26 | // TODO: Traits 27 | 28 | impl Types { 29 | // TODO: Expose inners for manual exporting logic 30 | } 31 | -------------------------------------------------------------------------------- /rspc/src/util.rs: -------------------------------------------------------------------------------- 1 | // TODO: Move into `langauge/typescript.rs` once legacy stuff is removed 2 | 3 | use std::borrow::Cow; 4 | 5 | use specta::{datatype::DataType, SpectaID}; 6 | 7 | // TODO: Probally using `DataTypeFrom` stuff cause we shouldn't be using `specta::internal` 8 | pub(crate) fn literal_object( 9 | name: Cow<'static, str>, 10 | sid: Option, 11 | fields: impl Iterator, DataType)>, 12 | ) -> DataType { 13 | specta::internal::construct::r#struct( 14 | name, 15 | sid, 16 | Default::default(), 17 | specta::internal::construct::struct_named( 18 | fields 19 | .into_iter() 20 | .map(|(name, ty)| { 21 | ( 22 | name.into(), 23 | specta::internal::construct::field(false, false, None, "".into(), Some(ty)), 24 | ) 25 | }) 26 | .collect(), 27 | None, 28 | ), 29 | ) 30 | .into() 31 | } 32 | -------------------------------------------------------------------------------- /rspc/tests/router.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | use rspc::{Procedure, ProcedureError, Router}; 4 | use rspc_procedure::ResolverError; 5 | use serde::Serialize; 6 | use specta::Type; 7 | 8 | #[test] 9 | fn errors() { 10 | let router = ::new() 11 | .procedure( 12 | "abc", 13 | Procedure::builder().query(|_, _: ()| async { Ok::<_, Infallible>(()) }), 14 | ) 15 | .procedure( 16 | "abc", 17 | Procedure::builder().query(|_, _: ()| async { Ok::<_, Infallible>(()) }), 18 | ); 19 | 20 | assert_eq!( 21 | format!("{:?}", router.build().unwrap_err()), 22 | "[Duplicate procedure at path [\"abc\"]. Original: rspc/tests/router.rs:13:13 Duplicate: rspc/tests/router.rs:15:10\n]" 23 | ); 24 | 25 | let router = ::new() 26 | .procedure( 27 | "abc", 28 | Procedure::builder().query(|_, _: ()| async { Ok::<_, Infallible>(()) }), 29 | ) 30 | .merge(::new().procedure( 31 | "abc", 32 | Procedure::builder().query(|_, _: ()| async { Ok::<_, Infallible>(()) }), 33 | )); 34 | 35 | assert_eq!(format!("{:?}", router.build().unwrap_err()), "[Duplicate procedure at path [\"abc\"]. Original: rspc/tests/router.rs:32:13 Duplicate: rspc/tests/router.rs:28:13\n]"); 36 | 37 | let router = ::new() 38 | .nest( 39 | "abc", 40 | ::new().procedure( 41 | "kjl", 42 | Procedure::builder().query(|_, _: ()| async { Ok::<_, Infallible>(()) }), 43 | ), 44 | ) 45 | .nest( 46 | "abc", 47 | ::new().procedure( 48 | "def", 49 | Procedure::builder().query(|_, _: ()| async { Ok::<_, Infallible>(()) }), 50 | ), 51 | ); 52 | 53 | assert_eq!(format!("{:?}", router.build().unwrap_err()), "[Duplicate procedure at path [\"abc\"]. Original: rspc/tests/router.rs:42:17 Duplicate: rspc/tests/router.rs:45:10\n]"); 54 | } 55 | 56 | #[derive(Type, Debug)] 57 | pub enum Infallible {} 58 | 59 | impl fmt::Display for Infallible { 60 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 61 | write!(f, "{self:?}") 62 | } 63 | } 64 | 65 | impl Serialize for Infallible { 66 | fn serialize(&self, _: S) -> Result 67 | where 68 | S: serde::Serializer, 69 | { 70 | unreachable!() 71 | } 72 | } 73 | 74 | impl std::error::Error for Infallible {} 75 | 76 | impl rspc::Error for Infallible { 77 | fn into_procedure_error(self) -> ProcedureError { 78 | unreachable!() 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /rspc/tests/typescript.rs: -------------------------------------------------------------------------------- 1 | // TODO: Sort this out 2 | // use std::{ 3 | // path::PathBuf, 4 | // process::{Command, Stdio}, 5 | // }; 6 | 7 | // use async_stream::stream; 8 | // use rspc::{Config, Router, Type}; 9 | // use serde::{Deserialize, Serialize}; 10 | 11 | // #[derive(Type, Deserialize)] 12 | // pub struct PaginatedQueryArg { 13 | // cursor: String, 14 | // } 15 | 16 | // #[derive(Type, Deserialize)] 17 | // pub struct PaginatedQueryArg2 { 18 | // cursor: String, 19 | // my_param: i32, 20 | // } 21 | 22 | // #[derive(Type, Serialize)] 23 | // pub struct MyPaginatedData { 24 | // data: Vec, 25 | // next_cursor: Option, 26 | // } 27 | 28 | // fn export_rspc_types() { 29 | // let _r = ::new() 30 | // .config(Config::new().export_ts_bindings( 31 | // PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("./examples/astro/test/bindings.ts"), 32 | // )) 33 | // .query("noArgQuery", |t| t(|_, _: ()| "demo")) 34 | // .query("singleArgQuery", |t| t(|_, i: i32| i)) 35 | // .query("paginatedQueryOnlyCursor", |t| { 36 | // t(|_, _: PaginatedQueryArg| MyPaginatedData { 37 | // data: vec!["a".to_string(), "b".to_string(), "c".to_string()], 38 | // next_cursor: None, 39 | // }) 40 | // }) 41 | // .query("paginatedQueryCursorAndArg", |t| { 42 | // t(|_, _: PaginatedQueryArg2| MyPaginatedData { 43 | // data: vec!["a".to_string(), "b".to_string(), "c".to_string()], 44 | // next_cursor: None, 45 | // }) 46 | // }) 47 | // .mutation("noArgMutation", |t| t(|_, _: ()| "demo")) 48 | // .mutation("singleArgMutation", |t| t(|_, i: i32| i)) 49 | // .subscription("noArgSubscription", |t| { 50 | // t(|_ctx, _args: ()| { 51 | // stream! { 52 | // yield "ping".to_string(); 53 | // } 54 | // }) 55 | // }) 56 | // .subscription("singleArgSubscription", |t| { 57 | // t(|_ctx, input: bool| { 58 | // stream! { 59 | // yield input; 60 | // } 61 | // }) 62 | // }) 63 | // .build(); 64 | // } 65 | 66 | // pub enum JSXMode { 67 | // React, 68 | // Solid, 69 | // } 70 | 71 | // fn tsc(file: &str, jsx_mode: JSXMode) { 72 | // let output = Command::new("tsc") 73 | // .arg("--esModuleInterop") 74 | // .arg("--strict") 75 | // .arg("--jsx") 76 | // .arg(match jsx_mode { 77 | // JSXMode::React => "react", 78 | // JSXMode::Solid => "preserve", 79 | // }) 80 | // .arg("--lib") 81 | // .arg("es2015,dom") 82 | // .arg("--noEmit") 83 | // .arg(file) 84 | // .stdin(Stdio::null()) 85 | // .stdout(Stdio::inherit()) 86 | // .output() 87 | // .expect("failed to execute process"); 88 | // assert!(output.status.success()); 89 | // } 90 | 91 | // #[test] 92 | // fn test_typescript_client() { 93 | // export_rspc_types(); 94 | // tsc("examples/astro/test/client.test.ts", JSXMode::React); 95 | // } 96 | 97 | // #[test] 98 | // fn test_typescript_react() { 99 | // export_rspc_types(); 100 | // tsc("examples/astro/test/react.test.tsx", JSXMode::React); 101 | // } 102 | 103 | // #[test] 104 | // fn test_typescript_sold() { 105 | // export_rspc_types(); 106 | // tsc("examples/astro/test/solid.test.tsx", JSXMode::Solid); 107 | // } 108 | -------------------------------------------------------------------------------- /turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turbo.build/schema.json", 3 | "tasks": { 4 | "build": { 5 | "dependsOn": ["^build"], 6 | "outputs": ["dist/**"] 7 | } 8 | } 9 | } 10 | --------------------------------------------------------------------------------