Code Monkey home page Code Monkey logo

rspc's Introduction

Logo

rspc

🚧 Work In Progress 🚧

A blazing fast and easy to use tRPC-like server for Rust.

Website


Warning

This branch contains some major changes for the 0.3.0 release.

For 0.2.x checkout v0.1.4-dev

For v1.0.0-rc.x (deprecated for now) checkout mainmain


Example

You define a rspc router and attach procedures to it like below. This will be very familiar if you have used tRPC or GraphQL before.

let router = <rspc::Router>::new()
    .query("version", |t| {
        t(|ctx, input: ()| "0.0.1")
    })
    .mutation("helloWorld", |t| {
        t(|ctx, input: ()| async { "Hello World!" })
    });

Features

  • Per Request Context - Great for database connection & authentication data
  • Middleware - With support for context switching
  • Merging routers - Great for separating code between files

Inspiration

This project is based off tRPC and was inspired by the bridge system Jamie Pine designed for Spacedrive. A huge thanks to everyone who helped inspire this project!

rspc's People

Contributors

brendonovich avatar dependabot[bot] avatar desdaemon avatar kandros avatar marcustut avatar marjorg avatar milo123459 avatar oscartbeaumont avatar tomheaton avatar yolo2h avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

rspc's Issues

Specta

v0.0.2:

  • serde_json::Value -> any
  • uuid::Uuid -> string
  • chrono::DateTime -> string
  • HashMap
  • BTreeMap
  • IndexMap
  • Generics
  • Exporting rspc key as tuple
  • Enums shouldn't automatically be inlined by rspc
  • Maintain user defined generic

[Tauri] Debug mode

Make a way of viewing all messages goes through rspc. This is useful in Tauri for example where the messages are not shown in the devtools.

Exporting dependencies of dependencies

If you refer to a type directly on the router (Eg. as an argument or result) it will be automatically exported when you export the router bindings however if the type your return depends on another type the dependant type will not be exported.

As a temporary workaround, you can use the ts_rs derive macros export functionality.

I can't fix this without changes to ts_rs or moving off of ts_rs due to ts_rs not storing enough information to export the dependencies of each dependency.

Route metadata

Defined on the route and accessible in middleware. Opposite direction to the context in control flow.

Gats

Original GAT's demo
use std::{any::type_name, fmt::Debug};

pub struct Router<TPlugin: Plugin = BasePlugin> {
    plugin: TPlugin,
}

impl Router<BasePlugin> {
    pub fn new() -> Self {
        Self {
            plugin: BasePlugin {},
        }
    }
}

impl<TPlugin: Plugin> Router<TPlugin> {
    pub fn plugin<T: Plugin>(self, t: T) -> Router<PluginJoiner<TPlugin, T>> {
        Router {
            plugin: PluginJoiner {
                a: self.plugin,
                b: t,
            },
        }
    }

    pub fn query<T: 'static>(self, func: impl Fn() -> T) -> Self
    where
        <TPlugin as Plugin>::Result<T>: Debug,
    {
        println!("{}", type_name::<TPlugin>());
        println!("{}", type_name::<TPlugin::Result<T>>());
        println!("{:?}\n", self.plugin.map(func()));

        self
    }
}

pub trait Plugin {
    type Result<T: 'static>: 'static;

    fn map<T: 'static>(&self, t: T) -> Self::Result<T>;
}

pub struct PluginJoiner<A: Plugin, B: Plugin> {
    a: A,
    b: B,
}

impl<A: Plugin, B: Plugin> Plugin for PluginJoiner<A, B> {
    type Result<T: 'static> = B::Result<A::Result<T>>;

    fn map<T: 'static>(&self, t: T) -> Self::Result<T> {
        self.b.map(self.a.map(t))
    }
}

pub struct BasePlugin;

impl Plugin for BasePlugin {
    type Result<T: 'static> = T;

    fn map<T: 'static>(&self, t: T) -> Self::Result<T> {
        t
    }
}

pub struct MapPlugin;

#[derive(Debug)]
pub struct CustomType<T>(String, T);

impl Plugin for MapPlugin {
    type Result<T: 'static> = CustomType<T>;

    fn map<T: 'static>(&self, t: T) -> Self::Result<T> {
        CustomType("Demo".to_string(), t)
    }
}

pub struct OverridePlugin;

impl Plugin for OverridePlugin {
    type Result<T: 'static> = bool;

    fn map<T: 'static>(&self, _t: T) -> Self::Result<T> {
        true
    }
}

fn main() {
    let r = <Router>::new().query(|| "Query!".to_string());

    let r = <Router>::new()
        .plugin(MapPlugin {})
        .query(|| "Query!".to_string());

    let r = <Router>::new()
        .plugin(MapPlugin {})
        .plugin(OverridePlugin {})
        .query(|| "Query!".to_string());
}

Playground

Upgraded GAT's demo (Closures + Futures + Return Type)
use std::{fmt::Debug, future::Future, pin::Pin};

pub trait Ret: Debug + 'static {}
impl<T: Debug + 'static> Ret for T {}

pub trait Fut<TRet: Ret>: Future<Output = TRet> + Send + 'static {}
impl<TRet: Ret, TFut: Future<Output = TRet> + Send + 'static> Fut<TRet> for TFut {}

pub trait Func<TRet: Ret, TFut: Fut<TRet>>: FnOnce() -> TFut + Send + 'static {}
impl<TRet: Ret, TFut: Fut<TRet>, TFunc: FnOnce() -> TFut + Send + 'static> Func<TRet, TFut>
    for TFunc
{
}

pub struct Router<TPlugin: Plugin = BasePlugin> {
    plugin: TPlugin,
}

impl Router<BasePlugin> {
    pub fn new() -> Self {
        Self {
            plugin: BasePlugin {},
        }
    }
}

impl<TPlugin: Plugin> Router<TPlugin> {
    pub fn plugin<T: Plugin>(self, t: T) -> Router<PluginJoiner<TPlugin, T>> {
        Router {
            plugin: PluginJoiner {
                a: self.plugin,
                b: t,
            },
        }
    }

    pub async fn query<TRet: Ret, TFut: Fut<TRet>>(&self, func: impl Func<TRet, TFut>) {
        let y = self.plugin.map(func);
        println!("\nBUILT\n");
        println!("{:?}\n", y().await);
    }
}

pub trait Plugin {
    type Ret<TRet: Ret>: Ret;
    type Fut<TRet: Ret, TFut: Fut<TRet>>: Fut<Self::Ret<TRet>>;
    type Result<TRet: Ret, TFut: Fut<TRet>, T: Func<TRet, TFut>>: Func<
        Self::Ret<TRet>,
        Self::Fut<TRet, TFut>,
    >;

    fn map<TRet: Ret, TFut: Fut<TRet>, T: Func<TRet, TFut>>(
        &self,
        t: T,
    ) -> Self::Result<TRet, TFut, T>;
}

pub struct PluginJoiner<A: Plugin, B: Plugin> {
    a: A,
    b: B,
}

impl<A: Plugin, B: Plugin> Plugin for PluginJoiner<A, B> {
    type Ret<TRet: Ret> = A::Ret<B::Ret<TRet>>;
    type Fut<TRet: Ret, TFut: Fut<TRet>> = A::Fut<B::Ret<TRet>, B::Fut<TRet, TFut>>;
    type Result<TRet: Ret, TFut: Fut<TRet>, T: Func<TRet, TFut>> =
        A::Result<B::Ret<TRet>, B::Fut<TRet, TFut>, B::Result<TRet, TFut, T>>;

    fn map<TRet: Ret, TFut: Fut<TRet>, T: Func<TRet, TFut>>(
        &self,
        t: T,
    ) -> Self::Result<TRet, TFut, T> {
        self.a.map(self.b.map(t))
    }
}

pub struct BasePlugin;

impl Plugin for BasePlugin {
    type Ret<TRet: Ret> = TRet;
    type Fut<TRet: Ret, TFut: Fut<TRet>> = TFut;
    type Result<TRet: Ret, TFut: Fut<TRet>, T: Func<TRet, TFut>> = T;

    fn map<TRet: Ret, TFut: Fut<TRet>, T: Func<TRet, TFut>>(
        &self,
        t: T,
    ) -> Self::Result<TRet, TFut, T> {
        println!("BUILD BASE");
        t
    }
}

pub struct MapPlugin(String);

impl Plugin for MapPlugin {
    type Ret<TRet: Ret> = TRet;
    type Fut<TRet: Ret, TFut: Fut<TRet>> = Pin<Box<dyn Fut<Self::Ret<TRet>>>>;
    type Result<TRet: Ret, TFut: Fut<TRet>, T: Func<TRet, TFut>> =
        Box<dyn Func<Self::Ret<TRet>, Self::Fut<TRet, TFut>>>;

    fn map<TRet: Ret, TFut: Fut<TRet>, T: Func<TRet, TFut>>(
        &self,
        t: T,
    ) -> Self::Result<TRet, TFut, T> {
        let id = self.0.clone();
        println!("BUILD {}", id);
        Box::new(move || {
            Box::pin(async move {
                println!("MAP {} - BEFORE", id);
                let data = t().await;
                println!("MAP {} - AFTER", id);
                data
            })
        })
    }
}

#[tokio::main]
async fn main() {
    let r = <Router>::new()
        .plugin(MapPlugin("A".into()))
        .plugin(MapPlugin("B".into()))
        .query(|| async move {
            println!("QUERY");
            "Query!".to_string()
        })
        .await;

    // let r = <Router>::new()
    //     .plugin(MapPlugin {})
    //     .query(|| "Query!".to_string());

    // let r = <Router>::new()
    //     .plugin(MapPlugin {})
    //     .plugin(OverridePlugin {})
    //     .query(|| "Query!".to_string());
}

Improve Prisma Client Rust Support

Exporting include as BaseType & { relation1: T } where each relation is joined to the base type and the base type doesn't contain relations.

Solution to selections done using with as we can't know if the relation will be included or not in the response.

Unit tests of Specta types added to Prisma Client Rust.

Question: Global state in Axum/rspc?

"Per Request Context" seems to create a new context "per request", which is not what i need.
In actix-web i can just create a "AppState(WebData)", and that state is shared between all requests.

A simple example with a global "request hit counter" would be appreciated.

Invalidate Query

Add the option to invalidate queries by key on the frontend when a mutation is submitted (like trpc does).

For now, you could use a subscription to work around the lack of this although that's not gonna be the best DX.

Also, add typesafe invalidate hook to the frontend.

Development Overview

Pre open-source:

  • Website
  • Basic Typescript runtime
  • Do query & mutation over websocket
  • Primitive Subscriptions working
  • Publish Crate and NPM package
  • Add npm link to sidebar on website

Spacedrive Features:

  • Rename CompiledRouter to Router and Router to RouterBuilder + Give CompiledRouter a set of default generics
  • selection!(x, { field_one, field_two }) macro
  • selection! macro on lists
  • Error Handling
  • Provide ctx function access to context/session/data/extensions + Alllow it to capture data
  • #5
  • Subscriptions
    • Access Context
    • Export type definitions
    • Subscription start/stop
  • Subscriptions working with middleware
  • React-query support
  • Tauri bindings
  • Handling tenants or libraries. I am thinking maybe the ID comes from the request context but how would the frontend inject context. Maybe something on client but that has React context???
  • Remove trpc compatibility to clean up code + typescript support for error handling
  • Mattrax bindings are not generating correctly! This is #6

Fixes:

  • Axum extrators are broken
  • Allow resolver returning &'static str I think it needs a ts_rs::TS impl
  • Fix all Rust warnings
  • #3

Finish Documentation Website

  • Set favicon
  • Set logo
  • OpenGraph Tags

Pages:

  • Document enum_key and full_type_safety mode
  • Document Enum Keys + Full Typesaftey mode
  • Roadmap
  • Route Metadata
  • Error Handling
  • Add "Request Context" heading to "Axum" page
  • Tauri page

Benchmarks

Using criterion add a bunch of benchmarks into the repository and setup a CI workflow to track changes over time.

Vscode extension

Shortcuts for creating a router and quickly adding an operation to the existing router.

feat: generate code as side effect of build

Hey @oscartbeaumont, I'm super excited about this library.

I believe it would be cleaner and simpler if .build() would generate the typescript definitions as side effect instead of having to call router.export("..").unwrap();

Additionally, it may be a good idea to have a default name for the exported file or dir {ts_root}/rspc/index.ts/{ts_src_root}/rspc.ts;

For that to be configurable, a method like set_export_ts_root. I'm adding ts here because maybe in the future another languages can be supported, for example I'm hoping to add lua support somehow

Content Types

Usecases:

  • Uploading files
    • The current workaround is using base64 or presigned URL's for an object storage bucket.
  • Better Compression
  • Use with OpenAPI plugin

tRPC issue trpc/trpc#1937.

OpenAPI

Support for exporting a valid OpenAPI schema from your rspc router.

Input validation

Validate the arguments to a resolver fit some requirements. The goal is to replicate the functionality of something like zod. It would be awesome if the validation logic could be exported to the frontend but that's not a requirement.

Normalised Caching

Normalised cache using global IDs + schema build time validate ID's are defined on all types

Problems:

  • Single item
  • Array of items
  • Realtionships -> Eg. User with list of Pets

Identifying types and validating the keys exists on types returned from Rust

Create rspc app

Framework:

  • Tauri
  • Axum
  • Actix-web (not supported by rspc yet)
  • Netlify functions (working in alpha rspc)
  • Cloudflare workers (working in alpha rspc)

Setup framework with rspc.

Database:

  • Prisma Client Rust
  • Sqlx (Long term goal, not supported by rspc yet)
  • SeaORM (Long term goal, not supported by rspc yet)
  • None

Setup in rspc context.

Frontend:

  • SolidJS Typescript
  • React Typescript
  • Next.js (Long term goal)
  • None

Pass off the Vite for this setup process if possible.

Extras:

  • TailwindCSS
  • Tokio Tracing
  • Zer -> Unreleased auth library I am working on.

  • Automated system for testing every combination.
  • Randomly picking default port for the webserver when scaffolding project would be nice. Maybe 3005-3050 or something.

bug: passing string as second param

Hey @oscartbeaumont, so I've started experimenting with rspc.

I had odd issue, that I spent quite sometimes to try to fix it myself, but it seems most likely a bug.

server

    let config = rspc::Config::new().export_ts_bindings("../client/src/rspc");
    let router = rspc::Router::<()>::new()
        .config(config)
        .query("ping", |_, name: String| {
            tokio::time::sleep(std::time::Duration::from_secs(1)).await;
            format!("pong {name}")
        })
        .build()
        .pipe(Arc::new);
    let addr = "[::]:4000".parse::<std::net::SocketAddr>().unwrap();
    println!("listening on http://{addr}/rspc/version?batch=1&input=%7B%7D",);

    axum::Server::bind(&addr)
        .serve(
            axum::Router::new()
                .route("/rspc/:id", router.clone().axum_handler(|| ()))
                .layer(
                    CorsLayer::new()
                        .allow_methods(Any)
                        .allow_headers(Any)
                        .allow_origin(Any),
                )
                .into_make_service(),
        )
        .await
        .unwrap();

client

const transport = new FetchTransport('http://localhost:4000/rspc')
export const client = createClient<Operations>({ transport })
const message = await client.query('ping', `${id}`)
console.log(message)
message

The above seems to send http://localhost:4000/rspc/ping?batch=1&input=32323.

the console log shows:

Unhandled Promise Rejection: TypeError: null is not an object (evaluating '(await resp.json())[0].result')

With curl I get the following error:

│Failed to deserialize query string. Expected something of type `rspc::integrations::axum::GetParams`. Error: missing field `input`[1]

When I used a number u32 type instead of string, it passes. Any ideas

Server batched subscriptions

Allow a client to subscribe with x different sets of input arguments while only using a single subscription (hence thread) on the server.

Wasm Transport

Using rspc as a typesafe bridge between wasm and Typescript.

Specta types for other crates & limitations

Specta currently makes the assumption rust_decimal::Decimal is using the serde-with-str feature and that the type is a string in Typescript. This could be an incorrect assumption but I don't really know how to handle the alternatives at this stage due to it being controlled in the downstream crate by a feature.

The assumption is also made that serde is not set to show human-readable types as this would result in a potential mismatch between JSON and Specta types.

Specta currently has a DataType::Any type forserde_json::Value. It shoud instead be an enum of all possible json types as these are subtly not the same thing.

Specta is also missing support for:

Adding `bigdecimal::BigDecimal` as specta compatible type results in string.

I was using sqlx with MySQL through RSPC. The database is connected successfully, however there are some types that are not currently supported for example chrono::NaiveDate, bigdecimal::BigDecimal. A list of what sqlx maps SQL types into rust types can be found here.

As a workaround, I could get it working by creating another struct and manually mapping the values into the values I need, see following:

// this struct contains the results fetched from db
#[derive(Debug, Serialize)]
struct ExpenseQueryResult {
    id: i32,
    name: String,
    amount: BigDecimal,
    date: chrono::NaiveDate,
    category: String,
    comment: Option<String>,
    tags: Option<String>,
}

// this is an additional struct for mapping unsupported values into supported values
#[derive(Debug)]
struct ListExpensesResponse {
    id: i32,
    name: String,
    amount: f64,
    date: String,
    category: String,
    comment: Option<String>,
    tags: Option<String>,
}

// my router definition
let router = rspc::Router::<Ctx>::new()
    .config(Config::new().export_ts_bindings("../generated/bindings.ts"))
    .query("listExpenses", |ctx: Ctx, _: ()| async move {
        let expenses: Vec<ExpenseQueryResult> = sqlx::query_as!(
            ExpenseQueryResult,
            r#"
select
    e. `id`,
    e. `name`,
    e. `amount`,
    e. `date`,
    ec. `name` as `category`,
    e. `comment`,
    group_concat(t. `name` separator ', ') as `tags`
from
    `expenses` e
    inner join `expenses_categories` ec on e. `expenses_categories_id` = ec. `id`
    left join `tags_expenses` te on e. `id` = te. `expenses_id`
    left join `tags` t on t. `id` = te. `tags_id`
group by
    e. `id`,
    e. `name`,
    e. `amount`,
    e. `date`,
    ec. `name`,
    e. `comment`
order by
    e. `date` desc;
        "#,
        )
        .fetch_all(&ctx.pool)
        .await
        .unwrap();

        selection!(
            expenses
                .into_iter()
                .map(|e| {
                    ListExpensesResponse {
                        id: e.id,
                        name: e.name,
                        amount: e.amount.to_f64().unwrap_or(-1.0),
                        date: e.date.to_string(),
                        category: e.category,
                        comment: e.comment,
                        tags: e.tags,
                    }
                })
                .collect::<Vec<ListExpensesResponse>>(),
            [{ id, name, amount, date, category, comment, tags }]
        )
    })
    .build()
    .arced();

So this code above is working well and fine and produce the following JSON response:

[
	{
		"type": "response",
		"id": null,
		"result": [
			{
				"amount": 118.00,
				"category": "Tithe",
				"comment": "Any comments",
				"date": "2022-08-06",
				"id": 4,
				"name": "August Tithe",
				"tags": "Touch n' Go"
			}
		]
	}
]

However, since I'm learning Rust and wanted to try making chrono::NaiveDate and bigdecimal::BigDecimal works by implementing the rspc::internal::specta::Type trait for it.

The following shows my implementation

#[derive(Debug, Serialize)]
struct SpectaCompatibleNaiveDate(chrono::NaiveDate);

impl From<chrono::NaiveDate> for SpectaCompatibleNaiveDate {
    fn from(date: chrono::NaiveDate) -> Self {
        Self(date)
    }
}

impl rspc::internal::specta::Type for SpectaCompatibleNaiveDate {
    const NAME: &'static str = "NaiveDate";

    fn inline(
        _: rspc::internal::specta::DefOpts,
        _: &[rspc::internal::specta::DataType],
    ) -> rspc::internal::specta::DataType {
        rspc::internal::specta::DataType::Primitive(rspc::internal::specta::PrimitiveType::String)
    }

    fn reference(
        _: rspc::internal::specta::DefOpts,
        _: &[rspc::internal::specta::DataType],
    ) -> rspc::internal::specta::DataType {
        rspc::internal::specta::DataType::Primitive(rspc::internal::specta::PrimitiveType::String)
    }

    fn definition(_: rspc::internal::specta::DefOpts) -> rspc::internal::specta::DataType {
        panic!()
    }
}

#[derive(Debug, Serialize)]
struct SpectaCompatibleBigDecimal(bigdecimal::BigDecimal);

impl From<bigdecimal::BigDecimal> for SpectaCompatibleBigDecimal {
    fn from(decimal: bigdecimal::BigDecimal) -> Self {
        Self(decimal)
    }
}

impl rspc::internal::specta::Type for SpectaCompatibleBigDecimal {
    const NAME: &'static str = "BigDecimal";

    fn inline(
        _: rspc::internal::specta::DefOpts,
        _: &[rspc::internal::specta::DataType],
    ) -> rspc::internal::specta::DataType {
        rspc::internal::specta::DataType::Primitive(rspc::internal::specta::PrimitiveType::f64)
    }

    fn reference(
        _: rspc::internal::specta::DefOpts,
        _: &[rspc::internal::specta::DataType],
    ) -> rspc::internal::specta::DataType {
        rspc::internal::specta::DataType::Primitive(rspc::internal::specta::PrimitiveType::f64)
    }

    fn definition(_: rspc::internal::specta::DefOpts) -> rspc::internal::specta::DataType {
        panic!()
    }
}

For SpectaCompatibleNaiveDate, it's working flawlessly. However for SpectaCompatibleBigDecimal, it returned String to the client instead of a number value. Here's the JSON response being returned.

[
	{
		"type": "response",
		"id": null,
		"result": [
			{
				"amount": "118.00",
				"category": "Tithe",
				"comment": "Any comments",
				"date": "2022-08-06",
				"id": 4,
				"name": "August Tithe",
				"tags": "Touch n' Go"
			}
		]
	}
]

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.