diff --git a/Cargo.toml b/Cargo.toml index e5354fea..893f13a7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,13 +27,15 @@ harness = false [features] default = ["typescript"] -typescript = ["specta/typescript"] -tracing = ["dep:tracing"] -tokio = ["dep:tokio", "specta/tokio"] +typescript = ["rspc-core/typescript", "specta/typescript"] +tracing = ["rspc-core/tracing", "dep:tracing"] +tokio = ["rspc-core/tokio", "dep:tokio", "specta/tokio"] unstable = [] # APIs where one line of code can blow up your whole app [dependencies] +rspc-core = { path = "./crates/core" } + specta = { workspace = true } serde = { workspace = true } thiserror = { workspace = true } @@ -48,10 +50,9 @@ serde_json = { version = "1", default-features = false } tracing = { version = "0.1.37", default-features = false, optional = true } tokio = { version = "1", default-features = false, features = ["rt", "time"], optional = true } -# TODO: Does this negatively affect compile times? Should it be flipped? -# # Even though this `cfg` can never be enabled, it still forces cargo to keep `serde_derive` in lockstep with `serde`. -# [target.'cfg(any())'.dependencies] -# rspc_httpz = { version = "=1.0.185", path = "../serde_derive" } +# Even though this `cfg` can never be enabled, it still forces cargo to keep `rspc-core` in lockstep with `rspc`. +[target.'cfg(any())'.dependencies] +rspc-core = { version = "=1.0.0-rc.5", path = "./crates/core" } [dev-dependencies] # Tests diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index d40f1023..e5479200 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -3,4 +3,28 @@ name = "rspc-core" version = "1.0.0-rc.5" edition = "2021" +# TODO: Remove all features from this crate cause they mean we can optimise build time +[features] +default = [] +typescript = ["specta/typescript"] +tracing = ["dep:tracing"] +tokio = ["dep:tokio", "specta/tokio"] + [dependencies] +specta = { workspace = true, features = ["typescript"] } # TODO: `typescript` should be required +serde = { workspace = true } +thiserror = { workspace = true } +futures = { version = "0.3.28", default-features = false, features = ["std", "async-await"] } # TODO: Drop for `futures_core` if possible +pin-project-lite = "0.2.13" +serde_json = { version = "1", default-features = false } +streamunordered = "0.5.3" + +# TODO: Remove these from core +tracing = { version = "0.1.37", default-features = false, optional = true } +tokio = { version = "1", default-features = false, features = ["rt", "time"], optional = true } + +# TODO: Make something like this work +# # Even though this `cfg` can never be enabled, it still forces cargo to keep `rspc-core` in lockstep with `rspc-*`. +# [target.'cfg(any())'.dependencies] +# rspc-httpz = { version = "=1.0.0-rc.5", path = "../httpz" } +# rspc-tauri = { version = "=1.0.0-rc.5", path = "../tauri" } diff --git a/src/internal/exec/async_runtime.rs b/crates/core/src/async_runtime.rs similarity index 100% rename from src/internal/exec/async_runtime.rs rename to crates/core/src/async_runtime.rs diff --git a/src/internal/body/body.rs b/crates/core/src/body/body.rs similarity index 96% rename from src/internal/body/body.rs rename to crates/core/src/body/body.rs index 448f48b3..b47807f9 100644 --- a/src/internal/body/body.rs +++ b/crates/core/src/body/body.rs @@ -5,7 +5,7 @@ use std::{ use serde_json::Value; -use crate::ExecError; +use crate::error::ExecError; /// The resulting body from an rspc operation. /// diff --git a/src/internal/body/map.rs b/crates/core/src/body/map.rs similarity index 100% rename from src/internal/body/map.rs rename to crates/core/src/body/map.rs diff --git a/src/internal/body/mod.rs b/crates/core/src/body/mod.rs similarity index 50% rename from src/internal/body/mod.rs rename to crates/core/src/body/mod.rs index d632c20d..b96d07f7 100644 --- a/src/internal/body/mod.rs +++ b/crates/core/src/body/mod.rs @@ -3,5 +3,5 @@ mod map; mod once; pub use body::*; -pub use map::*; -pub use once::*; +pub(crate) use map::*; +pub(crate) use once::*; diff --git a/src/internal/body/once.rs b/crates/core/src/body/once.rs similarity index 97% rename from src/internal/body/once.rs rename to crates/core/src/body/once.rs index a78309c2..a19039cc 100644 --- a/src/internal/body/once.rs +++ b/crates/core/src/body/once.rs @@ -8,7 +8,7 @@ use futures::ready; use pin_project_lite::pin_project; use serde_json::Value; -use crate::ExecError; +use crate::error::ExecError; use super::Body; diff --git a/src/error.rs b/crates/core/src/error.rs similarity index 90% rename from src/error.rs rename to crates/core/src/error.rs index 0c476ae3..fedfb3ed 100644 --- a/src/error.rs +++ b/crates/core/src/error.rs @@ -9,32 +9,28 @@ pub enum ProcedureError { Resolver(serde_json::Value), } -mod private { - use super::*; - - pub trait IntoResolverError: Serialize + Type + std::error::Error { - fn into_resolver_error(self) -> ResolverError - where - Self: Sized, - { - ResolverError { - value: serde_json::to_value(&self).unwrap_or_default(), - message: self.to_string(), - } +pub trait IntoResolverError: Serialize + Type + std::error::Error { + fn into_resolver_error(self) -> ResolverError + where + Self: Sized, + { + ResolverError { + value: serde_json::to_value(&self).unwrap_or_default(), + message: self.to_string(), } } +} - #[derive(thiserror::Error, Debug, Clone)] - #[error("{message}")] - pub struct ResolverError { - pub(crate) value: serde_json::Value, - pub(crate) message: String, - } +#[derive(thiserror::Error, Debug, Clone)] +#[error("{message}")] +pub struct ResolverError { + pub(crate) value: serde_json::Value, + pub(crate) message: String, +} - impl From for ProcedureError { - fn from(v: ResolverError) -> Self { - Self::Resolver(v.value) - } +impl From for ProcedureError { + fn from(v: ResolverError) -> Self { + Self::Resolver(v.value) } } @@ -43,9 +39,6 @@ pub enum Infallible {} impl IntoResolverError for T where T: Serialize + Type + std::error::Error {} -// TODO: `ResolverError` should probs be public from rspc-core but not rspc -pub(crate) use private::{IntoResolverError, ResolverError}; - // TODO: Context based `ExecError`. Always include the `path` of the procedure on it. // TODO: Cleanup this #[derive(thiserror::Error, Debug)] diff --git a/src/internal/exec/arc_ref.rs b/crates/core/src/exec/arc_ref.rs similarity index 95% rename from src/internal/exec/arc_ref.rs rename to crates/core/src/exec/arc_ref.rs index 85ac55b8..03dba7db 100644 --- a/src/internal/exec/arc_ref.rs +++ b/crates/core/src/exec/arc_ref.rs @@ -14,15 +14,14 @@ use std::{ use serde_json::Value; use crate::{ - internal::{ - middleware::{ProcedureKind, RequestContext}, - procedure::ProcedureTodo, - Body, - }, - ProcedureMap, Router, + body::Body, + middleware::{ProcedureKind, RequestContext}, + procedure_store::ProcedureTodo, + router_builder::ProcedureMap, + Router, }; -use super::{ExecutorResult, RequestData}; +use super::RequestData; pub(crate) struct ArcRef { // The lifetime here is invalid. This type is actually valid as long as the `Arc` in `self.mem` is ok. diff --git a/src/internal/exec/connection.rs b/crates/core/src/exec/connection.rs similarity index 97% rename from src/internal/exec/connection.rs rename to crates/core/src/exec/connection.rs index 5ebb0cf8..04b63f3e 100644 --- a/src/internal/exec/connection.rs +++ b/crates/core/src/exec/connection.rs @@ -22,13 +22,11 @@ use pin_project_lite::pin_project; use serde_json::Value; use streamunordered::{StreamUnordered, StreamYield}; -use super::{AsyncRuntime, ExecutorResult, IncomingMessage, Request, Requests, Response, Task}; +use super::{ExecutorResult, IncomingMessage, Request, Requests, Response, Task}; use crate::{ - internal::{ - exec::{self, ResponseInner}, - PinnedOption, PinnedOptionProj, - }, - Router, + exec, + util::{PinnedOption, PinnedOptionProj}, + AsyncRuntime, Router, }; // Time to wait for more messages before sending them over the websocket connection. diff --git a/src/internal/exec/execute.rs b/crates/core/src/exec/execute.rs similarity index 92% rename from src/internal/exec/execute.rs rename to crates/core/src/exec/execute.rs index 410e7671..a4794b54 100644 --- a/src/internal/exec/execute.rs +++ b/crates/core/src/exec/execute.rs @@ -16,17 +16,18 @@ use futures::{channel::oneshot, stream::FuturesUnordered, Stream, StreamExt}; use serde_json::Value; use crate::{ - internal::{ - exec::{ - arc_ref::{self, get_subscription, ArcRef}, - request_future::RequestFuture, - Request, Response, ResponseInner, Task, - }, - middleware::{ProcedureKind, RequestContext}, - procedure::ProcedureTodo, - Body, FutureValueOrStream, + body::Body, + error::ExecError, + exec::{ + arc_ref::{self, get_subscription, ArcRef}, + request_future::RequestFuture, + Request, Response, ResponseInner, Task, }, - ExecError, ProcedureMap, Router, + layer::FutureValueOrStream, + middleware::{ProcedureKind, RequestContext}, + procedure_store::ProcedureTodo, + router_builder::ProcedureMap, + Router, }; use super::{task, Connection, RequestData}; diff --git a/src/internal/exec/mod.rs b/crates/core/src/exec/mod.rs similarity index 80% rename from src/internal/exec/mod.rs rename to crates/core/src/exec/mod.rs index e06a8b79..d5f1a69f 100644 --- a/src/internal/exec/mod.rs +++ b/crates/core/src/exec/mod.rs @@ -1,9 +1,6 @@ //! TODO: Module docs -#![allow(unused_imports)] - pub(crate) mod arc_ref; -mod async_runtime; mod connection; mod execute; mod request_future; @@ -11,7 +8,6 @@ mod sink_and_stream; mod task; mod types; -pub use async_runtime::*; pub use connection::*; #[allow(unused_imports)] pub use execute::*; diff --git a/src/internal/exec/request_future.rs b/crates/core/src/exec/request_future.rs similarity index 90% rename from src/internal/exec/request_future.rs rename to crates/core/src/exec/request_future.rs index c7901110..44939478 100644 --- a/src/internal/exec/request_future.rs +++ b/crates/core/src/exec/request_future.rs @@ -11,12 +11,12 @@ use pin_project_lite::pin_project; use serde_json::Value; use crate::{ - internal::{ - exec::{self, Response, ResponseInner}, - middleware::RequestContext, - Body, PinnedOption, PinnedOptionProj, - }, - ExecError, Router, + body::Body, + error::ExecError, + exec::{self, Response, ResponseInner}, + middleware::RequestContext, + util::{PinnedOption, PinnedOptionProj}, + Router, }; use super::arc_ref::ArcRef; diff --git a/src/internal/exec/sink_and_stream.rs b/crates/core/src/exec/sink_and_stream.rs similarity index 95% rename from src/internal/exec/sink_and_stream.rs rename to crates/core/src/exec/sink_and_stream.rs index ad1e889c..2a8ec1c3 100644 --- a/src/internal/exec/sink_and_stream.rs +++ b/crates/core/src/exec/sink_and_stream.rs @@ -5,6 +5,7 @@ use std::{ use futures::{Sink, Stream}; +// TODO: Surely the `futures` crate has something that can replace this? pin_project_lite::pin_project! { pub struct SinkAndStream { #[pin] diff --git a/src/internal/exec/task.rs b/crates/core/src/exec/task.rs similarity index 92% rename from src/internal/exec/task.rs rename to crates/core/src/exec/task.rs index c9663011..6f7c2067 100644 --- a/src/internal/exec/task.rs +++ b/crates/core/src/exec/task.rs @@ -1,16 +1,15 @@ -use std::{fmt, pin::Pin, sync::Arc, task::Poll}; +use std::{fmt, pin::Pin, task::Poll}; use futures::{ready, Stream}; -use crate::{ - internal::{exec, Body}, - Router, -}; +use crate::body::Body; +use crate::exec; use super::{arc_ref::ArcRef, request_future::RequestFuture}; // TODO: Should this be called `Task` or `StreamWrapper`? Will depend on it's final form. +// TODO: Replace with FusedStream in dev if possible??? pub enum Status { ShouldBePolled { done: bool }, DoNotPoll, @@ -22,16 +21,10 @@ pub struct Task { // You will notice this is a `Stream` not a `Future` like would be implied by the struct. // rspc's whole middleware system only uses `Stream`'s cause it makes life easier so we change to & from a `Future` at the start/end. pub(crate) stream: ArcRef>>, - // pub(crate) shutdown // Mark when the stream is done. This means `self.reference` returned `None` but we still had to yield the complete message so we haven't returned `None` yet. pub(crate) status: Status, } -// pub enum Inner { -// Task(Task), -// Response(exec::Response), -// } - impl fmt::Debug for Task { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("StreamWrapper") diff --git a/crates/core/src/exec/types.rs b/crates/core/src/exec/types.rs new file mode 100644 index 00000000..6019f616 --- /dev/null +++ b/crates/core/src/exec/types.rs @@ -0,0 +1,73 @@ +use std::borrow::Cow; + +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use specta::Type; + +use crate::error::ProcedureError; + +#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Type)] +pub struct RequestData { + /// A unique ID used to identify the request + /// It is the client's responsibility to ensure that this ID is unique. + /// When using the HTTP Link this will always be `0`. + pub id: u32, + pub path: Cow<'static, str>, + pub input: Option, +} + +/// The type of a request to rspc. +/// +/// @internal +#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Type)] +#[serde(tag = "method", rename_all = "camelCase")] +pub enum Request { + Query(RequestData), + Mutation(RequestData), + Subscription(RequestData), + SubscriptionStop { id: u32 }, +} + +/// A value that can be a successful result or an error. +/// +/// @internal +#[derive(Clone, Debug, Serialize, PartialEq, Eq, Type)] +// #[cfg_attr(test, derive(specta::Type))] +#[serde(tag = "type", content = "value", rename_all = "camelCase")] +pub enum ResponseInner { + /// The result of a successful operation. + Value(Value), + /// The result of a failed operation. + Error(ProcedureError), + /// A message to indicate that the operation is complete. + Complete, +} + +/// The type of a response from rspc. +/// +/// @internal +#[derive(Clone, Debug, Serialize, PartialEq, Eq, Type)] +// #[cfg_attr(test, derive(specta::Type))] +#[serde(rename_all = "camelCase")] +pub struct Response { + pub id: u32, + #[serde(flatten)] + pub inner: ResponseInner, +} + +#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Type)] +pub enum Requests { + One(Request), + Many(Vec), +} + +/// The type of an incoming message to the [`Connection`] abstraction. +/// +/// This allows it to be used with any socket that can convert into this type. +#[derive(Debug)] +#[allow(dead_code)] +pub enum IncomingMessage { + Msg(Result), + Close, + Skip, +} diff --git a/crates/core/src/internal/mod.rs b/crates/core/src/internal/mod.rs deleted file mode 100644 index c6039fad..00000000 --- a/crates/core/src/internal/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -//! rspc core internals -//! -//! Anything in the module does *NOT* follow semantic versioning and may change at any time. diff --git a/crates/core/src/layer.rs b/crates/core/src/layer.rs new file mode 100644 index 00000000..8d2e9947 --- /dev/null +++ b/crates/core/src/layer.rs @@ -0,0 +1,61 @@ +use std::{future::ready, pin::Pin}; + +use serde_json::Value; + +use crate::body::{Body, Once}; +use crate::error::ExecError; +use crate::middleware::RequestContext; + +// TODO: Remove `SealedLayer` + +// TODO: Make this an enum so it can be `Value || Pin>`? +pub(crate) type FutureValueOrStream<'a> = Pin>; + +#[doc(hidden)] +pub trait Layer: SealedLayer {} + +// TODO: Can we avoid the `TLayerCtx` by building it into the layer +pub trait DynLayer: Send + Sync + 'static { + fn dyn_call( + &self, + ctx: TLayerCtx, + input: Value, + req: RequestContext, + ) -> FutureValueOrStream<'_>; +} + +impl> DynLayer for L { + fn dyn_call( + &self, + ctx: TLayerCtx, + input: Value, + req: RequestContext, + ) -> FutureValueOrStream<'_> { + match self.call(ctx, input, req) { + Ok(stream) => Box::pin(stream), + // TODO: Avoid allocating error future here + Err(err) => Box::pin(Once::new(ready(Err(err)))), + } + } +} + +/// Prevents the end user implementing the `Layer` trait and hides the internals +pub trait SealedLayer: DynLayer { + type Stream<'a>: Body + Send + 'a; + + fn call( + &self, + ctx: TLayerCtx, + input: Value, + req: RequestContext, + ) -> Result, ExecError>; + + fn erase(self) -> Box> + where + Self: Sized, + { + Box::new(self) + } +} + +impl> Layer for L {} diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index 2742b7ca..0941604f 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -1,5 +1,61 @@ //! Core traits and types for [rspc](https://docs.rs/rspc) -// TODO: move over lints +#![warn( + clippy::all, + clippy::cargo, + clippy::unwrap_used, + clippy::panic, + clippy::todo, + clippy::panic_in_result_fn, + // missing_docs +)] +#![deny(unsafe_code)] +#![allow(clippy::module_inception)] +#![cfg_attr(docsrs, feature(doc_cfg))] + +mod async_runtime; +mod body; +mod layer; +mod middleware; +mod procedure_store; +mod router; +mod router_builder; +mod util; + +// TODO: Reduce API surface in this?? +pub mod error; + +// TODO: Reduce API surface in this?? +pub mod exec; + +#[allow(deprecated)] +pub use async_runtime::{AsyncRuntime, TokioRuntime}; + +pub use router_builder::BuildError; + +pub use router::Router; #[doc(hidden)] -pub mod internal; +pub mod internal { + //! rspc core internals. + //! + //! WARNING: Anything in this module or it's submodules does not follow semantic versioning as it's considered an implementation detail. + + pub mod router { + pub use super::super::router::*; + } + + pub use super::body::Body; + pub use super::util::{PinnedOption, PinnedOptionProj}; + + pub use super::layer::{Layer, SealedLayer}; + pub use super::procedure_store::{build, ProcedureDef, ProcedureTodo, ProceduresDef}; + + pub use super::middleware::{ + new_mw_ctx, Executable2, MiddlewareContext, MwV2Result, ProcedureKind, RequestContext, + }; + + pub use super::router_builder::{ + edit_build_error_name, new_build_error, BuildError, BuildErrorCause, BuildResult, + ProcedureMap, + }; +} diff --git a/crates/core/src/middleware/mod.rs b/crates/core/src/middleware/mod.rs new file mode 100644 index 00000000..de2c039b --- /dev/null +++ b/crates/core/src/middleware/mod.rs @@ -0,0 +1,5 @@ +mod mw_ctx; +mod mw_result; + +pub use mw_ctx::*; +pub use mw_result::*; diff --git a/src/internal/middleware/mw_ctx.rs b/crates/core/src/middleware/mw_ctx.rs similarity index 94% rename from src/internal/middleware/mw_ctx.rs rename to crates/core/src/middleware/mw_ctx.rs index 103dc112..35f8779d 100644 --- a/src/internal/middleware/mw_ctx.rs +++ b/crates/core/src/middleware/mw_ctx.rs @@ -4,6 +4,14 @@ use serde_json::Value; use super::{Executable2Placeholder, MwResultWithCtx}; +pub fn new_mw_ctx(input: serde_json::Value, req: RequestContext) -> MiddlewareContext { + MiddlewareContext { + input, + req, + _priv: (), + } +} + pub struct MiddlewareContext { pub input: Value, pub req: RequestContext, @@ -12,14 +20,6 @@ pub struct MiddlewareContext { } impl MiddlewareContext { - pub(crate) fn new(input: Value, req: RequestContext) -> Self { - Self { - input, - req, - _priv: (), - } - } - #[cfg(feature = "tracing")] pub fn with_span(mut self, span: Option) -> Self { self.req.span = Some(span); diff --git a/src/internal/middleware/mw_result.rs b/crates/core/src/middleware/mw_result.rs similarity index 96% rename from src/internal/middleware/mw_result.rs rename to crates/core/src/middleware/mw_result.rs index 62f809da..75f8e091 100644 --- a/src/internal/middleware/mw_result.rs +++ b/crates/core/src/middleware/mw_result.rs @@ -5,11 +5,8 @@ use std::{ use serde_json::Value; -use crate::{internal::middleware::RequestContext, ExecError, IntoResolverError}; - -mod private { - // TODO -} +use super::RequestContext; +use crate::error::{ExecError, IntoResolverError}; pub trait Ret: Debug + Send + Sync + 'static {} impl Ret for T {} diff --git a/crates/core/src/procedure_store.rs b/crates/core/src/procedure_store.rs new file mode 100644 index 00000000..dec4f957 --- /dev/null +++ b/crates/core/src/procedure_store.rs @@ -0,0 +1,166 @@ +use std::{borrow::Cow, convert::Infallible}; + +use specta::{ + ts::TsExportError, DataType, DataTypeFrom, DefOpts, NamedDataType, StructType, TupleType, Type, + TypeMap, +}; + +use crate::{ + layer::{DynLayer, Layer}, + middleware::ProcedureKind, + router::Router, +}; + +/// @internal +#[derive(DataTypeFrom)] +#[cfg_attr(test, derive(specta::Type))] +pub struct ProceduresDef { + #[specta(type = ProcedureDef)] + queries: Vec, + #[specta(type = ProcedureDef)] + mutations: Vec, + #[specta(type = ProcedureDef)] + subscriptions: Vec, +} + +impl ProceduresDef { + pub fn new<'a, TCtx: 'a>( + queries: impl Iterator>, + mutations: impl Iterator>, + subscriptions: impl Iterator>, + ) -> Self { + ProceduresDef { + queries: queries.map(|i| &i.ty).cloned().collect(), + mutations: mutations.map(|i| &i.ty).cloned().collect(), + subscriptions: subscriptions.map(|i| &i.ty).cloned().collect(), + } + } + + pub fn to_named(self) -> NamedDataType { + let struct_type: StructType = self.into(); + struct_type.to_named("Procedures") + } +} + +/// Represents a Typescript procedure file which is generated by the Rust code. +/// This is codegenerated Typescript file is how we can validate the types on the frontend match Rust. +/// +/// @internal +#[derive(Debug, Clone, DataTypeFrom)] +#[cfg_attr(test, derive(specta::Type))] +pub struct ProcedureDef { + pub key: Cow<'static, str>, + #[specta(type = serde_json::Value)] + pub input: DataType, + #[specta(type = serde_json::Value)] + pub result: DataType, + #[specta(type = serde_json::Value)] + pub error: DataType, +} + +fn never() -> DataType { + Infallible::inline( + DefOpts { + parent_inline: false, + type_map: &mut Default::default(), + }, + &[], + ) + .unwrap() +} + +impl ProcedureDef { + pub fn from_tys( + key: Cow<'static, str>, + type_map: &mut TypeMap, + ) -> Result + where + TArg: Type, + TResult: Type, + TError: Type, + { + Ok(ProcedureDef { + key, + input: match TArg::reference( + DefOpts { + parent_inline: false, + type_map, + }, + &[], + )? { + DataType::Tuple(TupleType::Named { fields, .. }) if fields.len() == 0 => never(), + t => t, + }, + result: TResult::reference( + DefOpts { + parent_inline: false, + type_map, + }, + &[], + )?, + error: TError::reference( + DefOpts { + parent_inline: false, + type_map, + }, + &[], + )?, + }) + } +} + +// TODO: Rename this +pub struct ProcedureTodo { + pub(crate) exec: Box>, + pub(crate) ty: ProcedureDef, +} + +impl ProcedureTodo { + #[cfg(feature = "unstable")] + pub fn ty(&self) -> &ProcedureDef { + &self.ty + } +} + +// TODO: Using track caller style thing for the panics in this function +pub fn build( + key: Cow<'static, str>, + ctx: &mut Router, + kind: ProcedureKind, + layer: impl Layer + 'static, +) where + TCtx: 'static, + TArg: Type, + TResult: Type, + TError: Type, +{ + let (map, type_name) = match kind { + ProcedureKind::Query => (&mut ctx.queries, "query"), + ProcedureKind::Mutation => (&mut ctx.mutations, "mutation"), + ProcedureKind::Subscription => (&mut ctx.subscriptions, "subscription"), + }; + + let key_org = key; + let key = key_org.to_string(); + let type_def = ProcedureDef::from_tys::(key_org, &mut ctx.typ_store) + .expect("error exporting types"); // TODO: Error handling using `#[track_caller]` + + // TODO: Cleanup this logic and do better router merging + #[allow(clippy::panic)] + if key.is_empty() || key == "ws" || key.starts_with("rpc.") || key.starts_with("rspc.") { + panic!("rspc error: attempted to create {type_name} operation named '{key}', however this name is not allowed."); + } + + #[allow(clippy::panic)] + if map.contains_key(&key) { + panic!("rspc error: {type_name} operation already has resolver with name '{key}'"); + } + + map.insert( + key, + ProcedureTodo { + exec: layer.erase(), + ty: type_def, + }, + ); +} diff --git a/src/router.rs b/crates/core/src/router.rs similarity index 97% rename from src/router.rs rename to crates/core/src/router.rs index eabf9c06..9d25109a 100644 --- a/src/router.rs +++ b/crates/core/src/router.rs @@ -14,8 +14,11 @@ use specta::{ }; use crate::{ - internal::procedure::{ProcedureTodo, ProceduresDef}, - ExportError, + error::ExportError, + internal::ProcedureDef, + middleware::ProcedureKind, + procedure_store::{ProcedureTodo, ProceduresDef}, + router_builder::ProcedureMap, }; // TODO: Break this out into it's own file @@ -50,8 +53,6 @@ impl ExportConfig { } } -pub(crate) type ProcedureMap = BTreeMap>; - /// Router is a router that has been constructed and validated. It is ready to be attached to an integration to serve it to the outside world! pub struct Router { pub(crate) queries: ProcedureMap, diff --git a/src/router_builder/error.rs b/crates/core/src/router_builder.rs similarity index 75% rename from src/router_builder/error.rs rename to crates/core/src/router_builder.rs index b985af83..bf39af23 100644 --- a/src/router_builder/error.rs +++ b/crates/core/src/router_builder.rs @@ -1,8 +1,11 @@ -use std::{borrow::Cow, panic::Location}; +use std::{borrow::Cow, collections::BTreeMap, panic::Location}; use thiserror::Error; -use crate::Router; +use crate::{internal::ProcedureTodo, router::Router}; + +// TODO: Move into `procedure_store` module??? +pub type ProcedureMap = BTreeMap>; /// TODO #[derive(Debug, PartialEq, Eq)] @@ -14,6 +17,22 @@ pub struct BuildError { pub(crate) loc: &'static Location<'static>, } +#[cfg(debug_assertions)] +pub fn edit_build_error_name( + error: &mut BuildError, + func: impl FnOnce(Cow<'static, str>) -> Cow<'static, str>, +) { + error.name = func(std::mem::replace(&mut error.name, Cow::Borrowed(""))); +} + +pub fn new_build_error( + cause: BuildErrorCause, + #[cfg(debug_assertions)] name: Cow<'static, str>, + #[cfg(debug_assertions)] loc: &'static Location<'static>, +) -> BuildError { + BuildError { cause, name, loc } +} + impl BuildError { /// DO NOT USE IT, it's for unit testing only and may change without a major version bump. #[doc(hidden)] @@ -25,7 +44,7 @@ impl BuildError { #[allow(clippy::enum_variant_names)] #[derive(Debug, Error, PartialEq, Eq)] -pub(crate) enum BuildErrorCause { +pub enum BuildErrorCause { #[error( "a procedure or router name must be more than 1 character and less than 255 characters" )] diff --git a/crates/core/src/util.rs b/crates/core/src/util.rs new file mode 100644 index 00000000..2a5a8404 --- /dev/null +++ b/crates/core/src/util.rs @@ -0,0 +1,72 @@ +// pin_project_lite::pin_project! { +// #[project = _PinnedOptionProj] +pub enum PinnedOption { + Some { + // #[pin] + v: T, + }, + None, +} +// } + +impl From for PinnedOption { + fn from(value: T) -> Self { + Self::Some { v: value } + } +} + +/* The `cargo expand` output for `pin_project!` so that we can make `PinnedOptionProj` public */ +#[doc(hidden)] +#[allow(dead_code)] +#[allow(single_use_lifetimes)] +#[allow(clippy::unknown_clippy_lints)] +#[allow(clippy::mut_mut)] +#[allow(clippy::redundant_pub_crate)] +#[allow(clippy::ref_option_ref)] +#[allow(clippy::type_repetition_in_bounds)] +pub enum PinnedOptionProj<'__pin, T> +where + PinnedOption: '__pin, +{ + Some { + v: ::pin_project_lite::__private::Pin<&'__pin mut T>, + }, + None, +} +#[allow(single_use_lifetimes)] +#[allow(clippy::unknown_clippy_lints)] +#[allow(clippy::used_underscore_binding)] +#[allow(unsafe_code)] // <- Custom +#[allow(warnings)] // <- Custom +const _: () = { + impl PinnedOption { + #[doc(hidden)] + #[inline] + pub fn project<'__pin>( + self: ::pin_project_lite::__private::Pin<&'__pin mut Self>, + ) -> PinnedOptionProj<'__pin, T> { + unsafe { + match self.get_unchecked_mut() { + Self::Some { v } => PinnedOptionProj::Some { + v: ::pin_project_lite::__private::Pin::new_unchecked(v), + }, + Self::None => PinnedOptionProj::None, + } + } + } + } + #[allow(non_snake_case)] + pub struct __Origin<'__pin, T> { + __dummy_lifetime: ::pin_project_lite::__private::PhantomData<&'__pin ()>, + Some: (T), + None: (), + } + impl<'__pin, T> ::pin_project_lite::__private::Unpin for PinnedOption where + __Origin<'__pin, T>: ::pin_project_lite::__private::Unpin + { + } + trait MustNotImplDrop {} + #[allow(clippy::drop_bounds, drop_bounds)] + impl MustNotImplDrop for T {} + impl MustNotImplDrop for PinnedOption {} +}; diff --git a/crates/httpz/Cargo.toml b/crates/httpz/Cargo.toml index 75095ac4..8b7194e0 100644 --- a/crates/httpz/Cargo.toml +++ b/crates/httpz/Cargo.toml @@ -16,7 +16,7 @@ workers = ["httpz/workers", "httpz/ws"] vercel = ["httpz/vercel", "httpz/ws", "axum"] # TODO: Shouldn't rely on Axum [dependencies] -rspc = { version = "1.0.0-rc.5", path = "../../", default-features = false, features = ["tokio"] } # TODO: Shouldn't rely on Tokio +rspc-core = { version = "1.0.0-rc.5", path = "../core", default-features = false, features = ["tokio"] } # TODO: Shouldn't rely on Tokio httpz = { version = "0.0.5", default-features = false, features = ["cookies"] } tokio = { version = "1", default-features = false, features = ["sync"] } serde_json = "1.0.107" diff --git a/crates/httpz/src/extractors.rs b/crates/httpz/src/extractors.rs index eaa64bd3..b499be33 100644 --- a/crates/httpz/src/extractors.rs +++ b/crates/httpz/src/extractors.rs @@ -1,7 +1,7 @@ //! The future of this code in unsure. It will probs be removed or refactored once we support more than just Axum because all of the feature gating is bad. use super::{CookieJar, Request}; -use rspc::ExecError; +use rspc_core::error::ExecError; use std::marker::PhantomData; diff --git a/crates/httpz/src/httpz_endpoint.rs b/crates/httpz/src/httpz_endpoint.rs index 353d6d87..e2faaf50 100644 --- a/crates/httpz/src/httpz_endpoint.rs +++ b/crates/httpz/src/httpz_endpoint.rs @@ -10,8 +10,8 @@ use std::{ sync::{Arc, Mutex}, }; -use rspc::{ - internal::exec::{self, Connection, ExecutorResult}, +use rspc_core::{ + exec::{self, Connection, ExecutorResult}, Router, }; diff --git a/crates/httpz/src/websocket.rs b/crates/httpz/src/websocket.rs index 3465aef1..f2159343 100644 --- a/crates/httpz/src/websocket.rs +++ b/crates/httpz/src/websocket.rs @@ -6,10 +6,9 @@ use httpz::{ ws::{Message, WebsocketUpgrade}, HttpResponse, }; - -use rspc::{ - internal::exec::{run_connection, IncomingMessage, Response, TokioRuntime}, - Router, +use rspc_core::{ + exec::{run_connection, IncomingMessage, Response}, + Router, TokioRuntime, }; use super::TCtxFunc; diff --git a/crates/tauri/Cargo.toml b/crates/tauri/Cargo.toml index f53c0c4b..d620a9c7 100644 --- a/crates/tauri/Cargo.toml +++ b/crates/tauri/Cargo.toml @@ -5,7 +5,10 @@ edition = "2021" [dependencies] # TODO: Typescript support should be optional futures = "0.3.28" -rspc = { version = "1.0.0-rc.5", path = "../../", default-features = false, features = ["typescript", "tokio"] } +rspc-core = { version = "1.0.0-rc.5", path = "../core", default-features = false, features = [ + "typescript", + "tokio", +] } # TODO: Avoid tokio and typescript features serde = { workspace = true, features = ["derive"] } serde_json = "1.0.107" tauri = { version = "1.4.1", default-features = false, features = ["wry"] } # TODO: Work without wry diff --git a/crates/tauri/src/lib.rs b/crates/tauri/src/lib.rs index 95e00dd6..4c56ba20 100644 --- a/crates/tauri/src/lib.rs +++ b/crates/tauri/src/lib.rs @@ -14,11 +14,9 @@ use tauri::{ }; use tauri_specta::Event; -use rspc::{ - internal::exec::{ - run_connection, AsyncRuntime, IncomingMessage, Response, SinkAndStream, TokioRuntime, - }, - Router, +use rspc_core::{ + exec::{run_connection, IncomingMessage, Response, SinkAndStream}, + AsyncRuntime, Router, TokioRuntime, }; #[derive(Clone, Debug, serde::Deserialize, specta::Type, tauri_specta::Event)] diff --git a/src/internal/exec/types.rs b/src/internal/exec/types.rs deleted file mode 100644 index 41315b3f..00000000 --- a/src/internal/exec/types.rs +++ /dev/null @@ -1,84 +0,0 @@ -mod private { - use std::borrow::Cow; - - use serde::{Deserialize, Serialize}; - use serde_json::Value; - use specta::Type; - - use crate::{ExecError, ProcedureError, ResolverError}; - - #[derive(Clone, Debug, Deserialize, PartialEq, Eq, Type)] - pub struct RequestData { - /// A unique ID used to identify the request - /// It is the client's responsibility to ensure that this ID is unique. - /// When using the HTTP Link this will always be `0`. - pub id: u32, - pub path: Cow<'static, str>, - pub input: Option, - } - - /// The type of a request to rspc. - /// - /// @internal - #[derive(Clone, Debug, Deserialize, PartialEq, Eq, Type)] - #[serde(tag = "method", rename_all = "camelCase")] - pub enum Request { - Query(RequestData), - Mutation(RequestData), - Subscription(RequestData), - SubscriptionStop { id: u32 }, - } - - /// A value that can be a successful result or an error. - /// - /// @internal - #[derive(Clone, Debug, Serialize, PartialEq, Eq, Type)] - // #[cfg_attr(test, derive(specta::Type))] - #[serde(tag = "type", content = "value", rename_all = "camelCase")] - pub enum ResponseInner { - /// The result of a successful operation. - Value(Value), - /// The result of a failed operation. - Error(ProcedureError), - /// A message to indicate that the operation is complete. - Complete, - } - - /// The type of a response from rspc. - /// - /// @internal - #[derive(Clone, Debug, Serialize, PartialEq, Eq, Type)] - // #[cfg_attr(test, derive(specta::Type))] - #[serde(rename_all = "camelCase")] - pub struct Response { - pub id: u32, - #[serde(flatten)] - pub inner: ResponseInner, - } - - #[derive(Clone, Debug, Deserialize, PartialEq, Eq, Type)] - pub enum Requests { - One(Request), - Many(Vec), - } - - /// The type of an incoming message to the [`Connection`] abstraction. - /// - /// This allows it to be used with any socket that can convert into this type. - #[derive(Debug)] - #[allow(dead_code)] - pub enum IncomingMessage { - Msg(Result), - Close, - Skip, - } -} - -// TODO: Should some of this stuff be public or private. Removing the `unstable` feature would be nice! - -#[cfg(feature = "unstable")] -#[cfg_attr(docsrs, doc(cfg(feature = "unstable")))] -pub use private::*; - -#[cfg(not(feature = "unstable"))] -pub(crate) use private::*; diff --git a/src/internal/layer.rs b/src/internal/layer.rs deleted file mode 100644 index 126f461e..00000000 --- a/src/internal/layer.rs +++ /dev/null @@ -1,66 +0,0 @@ -use std::{future::ready, pin::Pin}; - -use serde_json::Value; - -use super::Body; -use crate::internal::middleware::RequestContext; - -// TODO: Make this an enum so it can be `Value || Pin>`? -pub(crate) type FutureValueOrStream<'a> = Pin>; - -#[doc(hidden)] -pub trait Layer: SealedLayer {} - -mod private { - use crate::{internal::Once, ExecError}; - - use super::*; - - // TODO: Can we avoid the `TLayerCtx` by building it into the layer - pub trait DynLayer: Send + Sync + 'static { - fn dyn_call( - &self, - ctx: TLayerCtx, - input: Value, - req: RequestContext, - ) -> FutureValueOrStream<'_>; - } - - impl> DynLayer for L { - fn dyn_call( - &self, - ctx: TLayerCtx, - input: Value, - req: RequestContext, - ) -> FutureValueOrStream<'_> { - match self.call(ctx, input, req) { - Ok(stream) => Box::pin(stream), - // TODO: Avoid allocating error future here - Err(err) => Box::pin(Once::new(ready(Err(err)))), - } - } - } - - /// Prevents the end user implementing the `Layer` trait and hides the internals - pub trait SealedLayer: DynLayer { - type Stream<'a>: Body + Send + 'a; - - fn call( - &self, - ctx: TLayerCtx, - input: Value, - req: RequestContext, - ) -> Result, ExecError>; - - fn erase(self) -> Box> - where - Self: Sized, - { - Box::new(self) - } - } - - impl> Layer for L {} -} - -pub(crate) use private::{DynLayer, SealedLayer}; diff --git a/src/internal/middleware/base.rs b/src/internal/middleware/base.rs index 1262b665..ee7e91e6 100644 --- a/src/internal/middleware/base.rs +++ b/src/internal/middleware/base.rs @@ -4,7 +4,8 @@ mod private { use serde::de::DeserializeOwned; use specta::Type; - use crate::internal::{middleware::SealedMiddlewareBuilder, Layer}; + use crate::internal::middleware::SealedMiddlewareBuilder; + use rspc_core::internal::Layer; pub struct BaseMiddleware(PhantomData); diff --git a/src/internal/middleware/builder.rs b/src/internal/middleware/builder.rs index 1669d783..ec66d662 100644 --- a/src/internal/middleware/builder.rs +++ b/src/internal/middleware/builder.rs @@ -3,7 +3,8 @@ use std::marker::PhantomData; use serde::de::DeserializeOwned; use specta::Type; -use crate::internal::{middleware::Middleware, Layer}; +use crate::internal::middleware::Middleware; +use rspc_core::internal::Layer; // TODO: Can this be made completely internal? #[doc(hidden)] diff --git a/src/internal/middleware/middleware_layer.rs b/src/internal/middleware/middleware_layer.rs index 2071a895..11fb7e14 100644 --- a/src/internal/middleware/middleware_layer.rs +++ b/src/internal/middleware/middleware_layer.rs @@ -9,13 +9,13 @@ mod private { use pin_project_lite::pin_project; use serde_json::Value; - use crate::{ + use crate::internal::middleware::Middleware; + use rspc_core::{ + error::ExecError, internal::{ - middleware::Middleware, - middleware::{Executable2, MiddlewareContext, MwV2Result, RequestContext}, - Body, Layer, PinnedOption, PinnedOptionProj, SealedLayer, + new_mw_ctx, Body, Executable2, Layer, MwV2Result, PinnedOption, PinnedOptionProj, + RequestContext, SealedLayer, }, - ExecError, }; #[doc(hidden)] @@ -40,7 +40,7 @@ mod private { input: Value, req: RequestContext, ) -> Result, ExecError> { - let fut = self.mw.run_me(ctx, MiddlewareContext::new(input, req)); + let fut = self.mw.run_me(ctx, new_mw_ctx(input, req)); Ok(MiddlewareLayerFuture::Resolve { fut, diff --git a/src/internal/middleware/mod.rs b/src/internal/middleware/mod.rs index dbbf944a..f3039bef 100644 --- a/src/internal/middleware/mod.rs +++ b/src/internal/middleware/mod.rs @@ -4,12 +4,8 @@ mod base; mod builder; mod middleware_layer; mod mw; -mod mw_ctx; -mod mw_result; pub(crate) use base::*; pub use builder::*; pub(crate) use middleware_layer::*; pub use mw::*; -pub use mw_ctx::*; -pub use mw_result::*; diff --git a/src/internal/middleware/mw.rs b/src/internal/middleware/mw.rs index 54e902ca..7fd1fa8b 100644 --- a/src/internal/middleware/mw.rs +++ b/src/internal/middleware/mw.rs @@ -3,7 +3,7 @@ use std::future::Future; use serde::de::DeserializeOwned; use specta::Type; -use crate::internal::middleware::{MiddlewareContext, MwV2Result}; +use rspc_core::internal::{MiddlewareContext, MwV2Result}; /// TODO /// diff --git a/src/internal/mod.rs b/src/internal/mod.rs index 6f413774..1c5467b6 100644 --- a/src/internal/mod.rs +++ b/src/internal/mod.rs @@ -3,101 +3,75 @@ //! WARNING: Anything in this module or submodules does not follow semantic versioning as it's considered an implementation detail. //! -pub mod exec; pub mod middleware; pub mod procedure; +pub mod procedure_store; pub mod resolver; -mod body; -mod layer; - -pub(crate) use body::*; -pub use layer::*; - -mod private { - pin_project_lite::pin_project! { - #[project = PinnedOptionProj] - pub enum PinnedOption { - Some { - #[pin] - v: T, - }, - None, - } - } - - impl From for PinnedOption { - fn from(value: T) -> Self { - Self::Some { v: value } - } - } -} - -pub(crate) use private::{PinnedOption, PinnedOptionProj}; - -#[cfg(test)] -mod tests { - use std::{fs::File, io::Write, path::PathBuf}; - - use specta::{ts::export_named_datatype, DefOpts, Type, TypeMap}; - - use crate::internal::exec; - - macro_rules! collect_datatypes { - ($( $i:path ),* $(,)? ) => {{ - use specta::DataType; - - let mut tys = TypeMap::default(); - - $({ - let def = <$i as Type>::definition(DefOpts { - parent_inline: true, - type_map: &mut tys, - }); - - if let Ok(def) = def { - if let DataType::Named(n) = def { - if let Some(sid) = n.ext().as_ref().map(|e| *e.sid()) { - tys.insert(sid, Some(n)); - } - } - } - })* - tys - }}; - } - - // rspc has internal types that are shared between the frontend and backend. We use Specta directly to share these to avoid a whole class of bugs within the library itself. - #[test] - fn export_internal_types() { - let mut file = File::create( - PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("./packages/client/src/bindings.ts"), - ) - .unwrap(); - - file.write_all( - b"// DO NOT MODIFY. This file was generated by Specta and is used to keep rspc internally type safe.\n// Checkout the unit test 'export_internal_types' to see where this files comes from!", - ) - .unwrap(); - - let tys = collect_datatypes![ - super::procedure::ProceduresDef, - // crate::Procedures, // TODO - exec::Request, - exec::Response, - ]; - - for (_, ty) in tys - .iter() - .filter_map(|(sid, v)| v.as_ref().map(|v| (sid, v))) - { - file.write_all(b"\n\n").unwrap(); - file.write_all( - export_named_datatype(&Default::default(), &ty, &tys) - .unwrap() - .as_bytes(), - ) - .unwrap(); - } - } -} +// TODO: Fix this +// #[cfg(test)] +// mod tests { +// use std::{fs::File, io::Write, path::PathBuf}; + +// use specta::{ts::export_named_datatype, DefOpts, Type, TypeMap}; + +// use rspc_core::exec; + +// macro_rules! collect_datatypes { +// ($( $i:path ),* $(,)? ) => {{ +// use specta::DataType; + +// let mut tys = TypeMap::default(); + +// $({ +// let def = <$i as Type>::definition(DefOpts { +// parent_inline: true, +// type_map: &mut tys, +// }); + +// if let Ok(def) = def { +// if let DataType::Named(n) = def { +// if let Some(sid) = n.ext().as_ref().map(|e| *e.sid()) { +// tys.insert(sid, Some(n)); +// } +// } +// } +// })* +// tys +// }}; +// } + +// // rspc has internal types that are shared between the frontend and backend. We use Specta directly to share these to avoid a whole class of bugs within the library itself. +// #[test] +// fn export_internal_types() { +// let mut file = File::create( +// PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("./packages/client/src/bindings.ts"), +// ) +// .unwrap(); + +// file.write_all( +// b"// DO NOT MODIFY. This file was generated by Specta and is used to keep rspc internally type safe.\n// Checkout the unit test 'export_internal_types' to see where this files comes from!", +// ) +// .unwrap(); + +// let tys = collect_datatypes![ +// rspc_core::internal::ProceduresDef, +// // crate::Procedures, // TODO +// exec::Request, +// exec::Response, +// ]; + +// for (_, ty) in tys +// .iter() +// .filter_map(|(sid, v)| v.as_ref().map(|v| (sid, v))) +// { +// file.write_all(b"\n\n").unwrap(); +// file.write_all( +// export_named_datatype(&Default::default(), &ty, &tys) +// .unwrap() +// .as_bytes(), +// ) +// .unwrap(); +// } +// } +// } diff --git a/src/internal/procedure/procedure.rs b/src/internal/procedure.rs similarity index 78% rename from src/internal/procedure/procedure.rs rename to src/internal/procedure.rs index 1541b927..73ed980b 100644 --- a/src/internal/procedure/procedure.rs +++ b/src/internal/procedure.rs @@ -3,19 +3,14 @@ use std::{borrow::Cow, marker::PhantomData}; use serde::de::DeserializeOwned; use specta::Type; -use crate::{ - internal::{ - middleware::{ - ConstrainedMiddleware, MiddlewareBuilder, MiddlewareLayerBuilder, ProcedureKind, - }, - procedure::ProcedureDef, - resolver::{ - FutureMarkerType, HasResolver, RequestLayer, ResolverFunction, ResolverLayer, - StreamMarkerType, - }, +use crate::internal::{ + middleware::{ConstrainedMiddleware, MiddlewareBuilder, MiddlewareLayerBuilder}, + resolver::{ + FutureMarkerType, HasResolver, RequestLayer, ResolverFunction, ResolverLayer, + StreamMarkerType, }, - Router, }; +use rspc_core::internal::{router::Router, ProcedureDef, ProcedureKind}; /// TODO: Explain pub struct MissingResolver(PhantomData); @@ -35,8 +30,6 @@ mod private { pub(crate) use private::Procedure; -use super::procedure_store; - impl Procedure where TMiddleware: MiddlewareBuilder, @@ -117,27 +110,18 @@ where pub(crate) fn build(self, key: Cow<'static, str>, ctx: &mut Router) { let HasResolver(resolver, kind, _) = self.resolver; - let m = match kind { - ProcedureKind::Query => (&mut ctx.queries, "query"), - ProcedureKind::Mutation => (&mut ctx.mutations, "mutation"), - ProcedureKind::Subscription => (&mut ctx.subscriptions, "subscription"), - }; - - let key_str = key.to_string(); - let type_def = ProcedureDef::from_tys::< + rspc_core::internal::build::< + TMiddleware::Ctx, TMiddleware::Arg, TResult::Result, TResult::Error, - >(key, &mut ctx.typ_store) - .expect("error exporting types"); // TODO: Error handling using `#[track_caller]` - - procedure_store::append( - m, - key_str, + >( + key, + ctx, + kind, self.mw.build(ResolverLayer::new(move |ctx, input, _| { Ok((resolver)(ctx, input).exec()) })), - type_def, ); } } diff --git a/src/internal/procedure/mod.rs b/src/internal/procedure/mod.rs deleted file mode 100644 index b379d8ff..00000000 --- a/src/internal/procedure/mod.rs +++ /dev/null @@ -1,7 +0,0 @@ -//! TODO: Docs - -mod procedure; -mod procedure_store; - -pub(crate) use procedure::*; -pub(crate) use procedure_store::*; diff --git a/src/internal/procedure/procedure_store.rs b/src/internal/procedure/procedure_store.rs deleted file mode 100644 index 049c6821..00000000 --- a/src/internal/procedure/procedure_store.rs +++ /dev/null @@ -1,175 +0,0 @@ -// TODO: Probs unseal a heap of this -use std::{borrow::Cow, convert::Infallible}; - -use specta::{ - ts::TsExportError, DataType, DataTypeFrom, DefOpts, NamedDataType, StructType, TupleType, Type, - TypeMap, -}; - -use crate::internal::{DynLayer, Layer}; - -/// @internal -#[derive(DataTypeFrom)] -#[cfg_attr(test, derive(specta::Type))] -pub(crate) struct ProceduresDef { - #[specta(type = ProcedureDef)] - queries: Vec, - #[specta(type = ProcedureDef)] - mutations: Vec, - #[specta(type = ProcedureDef)] - subscriptions: Vec, -} - -mod private { - use super::*; - - impl ProceduresDef { - pub fn new<'a, TCtx: 'a>( - queries: impl Iterator>, - mutations: impl Iterator>, - subscriptions: impl Iterator>, - ) -> Self { - ProceduresDef { - queries: queries.map(|i| &i.ty).cloned().collect(), - mutations: mutations.map(|i| &i.ty).cloned().collect(), - subscriptions: subscriptions.map(|i| &i.ty).cloned().collect(), - } - } - - pub fn to_named(self) -> NamedDataType { - let struct_type: StructType = self.into(); - struct_type.to_named("Procedures") - } - } - - /// Represents a Typescript procedure file which is generated by the Rust code. - /// This is codegenerated Typescript file is how we can validate the types on the frontend match Rust. - /// - /// @internal - #[derive(Debug, Clone, DataTypeFrom)] - #[cfg_attr(test, derive(specta::Type))] - pub struct ProcedureDef { - pub key: Cow<'static, str>, - #[specta(type = serde_json::Value)] - pub input: DataType, - #[specta(type = serde_json::Value)] - pub result: DataType, - #[specta(type = serde_json::Value)] - pub error: DataType, - } - - fn never() -> DataType { - Infallible::inline( - DefOpts { - parent_inline: false, - type_map: &mut Default::default(), - }, - &[], - ) - .unwrap() - } - - impl ProcedureDef { - pub fn from_tys( - key: Cow<'static, str>, - type_map: &mut TypeMap, - ) -> Result - where - TArg: Type, - TResult: Type, - TError: Type, - { - Ok(ProcedureDef { - key, - input: match TArg::reference( - DefOpts { - parent_inline: false, - type_map, - }, - &[], - )? { - DataType::Tuple(TupleType::Named { fields, .. }) if fields.len() == 0 => { - never() - } - t => t, - }, - result: TResult::reference( - DefOpts { - parent_inline: false, - type_map, - }, - &[], - )?, - error: TError::reference( - DefOpts { - parent_inline: false, - type_map, - }, - &[], - )?, - }) - } - } - - // TODO: Rename this - pub struct ProcedureTodo { - pub(crate) exec: Box>, - pub(crate) ty: ProcedureDef, - } - - impl ProcedureTodo { - #[cfg(feature = "unstable")] - pub fn ty(&self) -> &ProcedureDef { - &self.ty - } - } -} - -use crate::{BuildErrorCause, ProcedureMap}; - -pub(crate) fn is_valid_name(name: &str) -> Option { - if name.is_empty() || name.len() > 255 { - return Some(BuildErrorCause::InvalidName); - } - - for c in name.chars() { - if !(c.is_alphanumeric() || c == '_' || c == '-' || c == '~') { - return Some(BuildErrorCause::InvalidCharInName(c)); - } - } - - if name == "rspc" || name == "_batch" { - return Some(BuildErrorCause::ReservedName(name.to_string())); - } - - None -} - -// TODO: Using track caller style thing for the panics in this function -pub(crate) fn append>( - (map, type_name): (&mut ProcedureMap, &'static str), - key: String, - exec: L, - ty: ProcedureDef, -) { - // TODO: Cleanup this logic and do better router merging - #[allow(clippy::panic)] - if key.is_empty() || key == "ws" || key.starts_with("rpc.") || key.starts_with("rspc.") { - panic!("rspc error: attempted to create {type_name} operation named '{key}', however this name is not allowed."); - } - - #[allow(clippy::panic)] - if map.contains_key(&key) { - panic!("rspc error: {type_name} operation already has resolver with name '{key}'"); - } - - map.insert( - key, - ProcedureTodo { - exec: exec.erase(), - ty, - }, - ); -} - -pub(crate) use private::{ProcedureDef, ProcedureTodo}; diff --git a/src/internal/procedure_store.rs b/src/internal/procedure_store.rs new file mode 100644 index 00000000..7c684081 --- /dev/null +++ b/src/internal/procedure_store.rs @@ -0,0 +1,19 @@ +use rspc_core::internal::{BuildError, BuildErrorCause, Layer, ProcedureDef, ProcedureMap}; + +pub(crate) fn is_valid_name(name: &str) -> Option { + if name.is_empty() || name.len() > 255 { + return Some(BuildErrorCause::InvalidName); + } + + for c in name.chars() { + if !(c.is_alphanumeric() || c == '_' || c == '-' || c == '~') { + return Some(BuildErrorCause::InvalidCharInName(c)); + } + } + + if name == "rspc" || name == "_batch" { + return Some(BuildErrorCause::ReservedName(name.to_string())); + } + + None +} diff --git a/src/internal/resolver/function.rs b/src/internal/resolver/function.rs index 76a13be0..69917fae 100644 --- a/src/internal/resolver/function.rs +++ b/src/internal/resolver/function.rs @@ -12,7 +12,7 @@ pub trait ResolverFunction: } mod private { - use crate::internal::middleware::ProcedureKind; + use rspc_core::internal::ProcedureKind; use super::*; diff --git a/src/internal/resolver/layer.rs b/src/internal/resolver/layer.rs index 1400d5f8..95f7c7eb 100644 --- a/src/internal/resolver/layer.rs +++ b/src/internal/resolver/layer.rs @@ -3,9 +3,9 @@ use serde_json::Value; use specta::Type; use std::marker::PhantomData; -use crate::{ - internal::{middleware::RequestContext, Body, SealedLayer}, - ExecError, +use rspc_core::{ + error::ExecError, + internal::{Body, RequestContext, SealedLayer}, }; #[cfg(feature = "tracing")] diff --git a/src/internal/resolver/result.rs b/src/internal/resolver/result.rs index eb991abf..bdd729ea 100644 --- a/src/internal/resolver/result.rs +++ b/src/internal/resolver/result.rs @@ -5,7 +5,6 @@ use std::{ task::{ready, Context, Poll}, }; -use crate::{internal::Body, Infallible, IntoResolverError}; use futures::{ stream::{once, Once}, Stream, @@ -14,13 +13,17 @@ use serde::Serialize; use serde_json::Value; use specta::Type; -use crate::ExecError; +use rspc_core::{ + error::{ExecError, IntoResolverError}, + internal::Body, +}; #[doc(hidden)] pub trait RequestLayer: private::SealedRequestLayer {} mod private { use pin_project_lite::pin_project; + use rspc_core::error::Infallible; use super::*; diff --git a/src/lib.rs b/src/lib.rs index ff24bc5b..2af046ac 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -11,23 +11,18 @@ clippy::panic_in_result_fn, // missing_docs )] -// #![deny(unsafe_code)] // TODO: Enable this +#![forbid(unsafe_code)] #![allow(clippy::module_inception)] -#![allow(clippy::type_complexity)] // TODO: Fix this and disable it #![cfg_attr(docsrs, feature(doc_cfg))] -#[cfg(feature = "unstable")] -#[cfg_attr(docsrs, doc(cfg(feature = "unstable")))] -pub mod unstable; - -mod error; -mod router; mod router_builder; mod rspc; pub use crate::rspc::*; -pub use error::*; -pub use router::*; pub use router_builder::*; +pub use rspc_core::internal::router::*; pub mod internal; + +// TODO: Only reexport certain types +pub use rspc_core::error::*; diff --git a/src/router_builder/router_builder.rs b/src/router_builder.rs similarity index 84% rename from src/router_builder/router_builder.rs rename to src/router_builder.rs index 4ab7e604..0a30cb86 100644 --- a/src/router_builder/router_builder.rs +++ b/src/router_builder.rs @@ -6,11 +6,13 @@ use specta::Type; use crate::{ internal::{ middleware::MiddlewareBuilder, - procedure::{is_valid_name, Procedure}, + procedure::Procedure, + procedure_store::is_valid_name, resolver::{HasResolver, RequestLayer}, }, - BuildError, BuildResult, Router, + Router, }; +use rspc_core::internal::{edit_build_error_name, new_build_error, BuildError, BuildResult}; type ProcedureBuildFn = Box, &mut Router)>; @@ -52,13 +54,13 @@ where TMiddleware: MiddlewareBuilder, { if let Some(cause) = is_valid_name(key) { - self.errors.push(BuildError { + self.errors.push(new_build_error( cause, #[cfg(debug_assertions)] - name: Cow::Borrowed(key), + Cow::Borrowed(key), #[cfg(debug_assertions)] - loc: Location::caller(), - }); + Location::caller(), + )); } self.procedures.push(( @@ -73,13 +75,13 @@ where #[allow(unused_mut)] pub fn merge(mut self, prefix: &'static str, mut r: RouterBuilder) -> Self { if let Some(cause) = is_valid_name(prefix) { - self.errors.push(BuildError { + self.errors.push(new_build_error( cause, #[cfg(debug_assertions)] - name: Cow::Borrowed(prefix), + Cow::Borrowed(prefix), #[cfg(debug_assertions)] - loc: Location::caller(), - }); + Location::caller(), + )); } #[cfg(not(debug_assertions))] @@ -90,7 +92,7 @@ where #[cfg(debug_assertions)] { self.errors.extend(&mut r.errors.into_iter().map(|mut err| { - err.name = Cow::Owned(format!("{}.{}", prefix, err.name)); + edit_build_error_name(&mut err, |name| Cow::Owned(format!("{}.{}", prefix, name))); err })); } diff --git a/src/router_builder/mod.rs b/src/router_builder/mod.rs deleted file mode 100644 index 49de3a47..00000000 --- a/src/router_builder/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -mod error; -mod router_builder; - -pub use error::*; -pub use router_builder::*; diff --git a/src/rspc.rs b/src/rspc.rs index 3a802e73..8bae0f86 100644 --- a/src/rspc.rs +++ b/src/rspc.rs @@ -2,14 +2,13 @@ use std::marker::PhantomData; use crate::{ internal::{ - middleware::{ - BaseMiddleware, ConstrainedMiddleware, MiddlewareLayerBuilder, ProcedureKind, - }, + middleware::{BaseMiddleware, ConstrainedMiddleware, MiddlewareLayerBuilder}, procedure::{MissingResolver, Procedure}, resolver::{FutureMarkerType, RequestLayer, ResolverFunction, StreamMarkerType}, }, Infallible, IntoResolverError, RouterBuilder, }; +use rspc_core::internal::ProcedureKind; /// Rspc is a starting point for constructing rspc procedures or routers. /// diff --git a/src/unstable/mod.rs b/src/unstable/mod.rs deleted file mode 100644 index b3097d14..00000000 --- a/src/unstable/mod.rs +++ /dev/null @@ -1,9 +0,0 @@ -//! Some work in progress API that are not typesafe. -//! -//! WARNING: This module does not follow semver so may change at any time and can also break rspc's typesafe guarantees if not used correctly. - -#![allow(clippy::unwrap_used)] // TODO: Fix this - -mod mw_arg_mapper; - -pub use mw_arg_mapper::*; diff --git a/src/unstable/mw_arg_mapper.rs b/src/unstable/mw_arg_mapper.rs deleted file mode 100644 index 46e7fa88..00000000 --- a/src/unstable/mw_arg_mapper.rs +++ /dev/null @@ -1,99 +0,0 @@ -use std::{future::Future, marker::PhantomData}; - -use serde::{de::DeserializeOwned, Serialize}; -use specta::Type; - -// TODO: This should be possible without `internal` API's -use crate::internal::{ - middleware::Middleware, - middleware::{MiddlewareContext, MwV2Result}, -}; - -/// A trait for modifying a procedures argument type. -/// -/// This trait primarily exists to workaround Rust's lack of generic closures. -/// -/// To explain it more say you had `{ library_id: Uuid, data: T }` as your input from the frontend. -/// Your `Self::State` would be `Uuid` and your `Self::Output` would be `T`. -/// This way `Self::State` can be passed into the middleware closure "erasing" the generic `T`. -/// -/// This is very powerful for multi-tenant applications but also breaks all rspc typesafe guarantees. -pub trait MwArgMapper: Send + Sync { - /// the output of the mapper for consumption in your middleware. - type State: Send + Sync + 'static; - - /// the output of the mapper to be passed on to the following procedure. - /// - /// WARNING: This is not typesafe. If you get it wrong it will runtime panic! - type Input: DeserializeOwned + Type + 'static - where - T: DeserializeOwned + Type + 'static; - - /// Apply the mapping to the input argument. - fn map( - arg: Self::Input, - ) -> (T, Self::State); -} - -/// A middleware that allows you to modify the input arguments of a procedure. -pub struct MwArgMapperMiddleware(PhantomData); - -impl MwArgMapperMiddleware { - pub const fn new() -> Self { - Self(PhantomData) - } - - pub fn mount( - &self, - handler: impl Fn(MiddlewareContext, TLCtx, M::State) -> F + Send + Sync + 'static, - ) -> impl Middleware::Ctx> - where - TLCtx: Send + Sync + 'static, - F: Future + Send + Sync + 'static, - F::Output: MwV2Result + Send + 'static, - { - // TODO: Make this passthrough to new handler but provide the owned `State` as an arg - private::MiddlewareFnWithTypeMapper( - move |mw: MiddlewareContext, ctx| { - let (out, state) = - M::map::(serde_json::from_value(mw.input).unwrap()); // TODO: Error handling - - handler( - MiddlewareContext::new( - serde_json::to_value(out).unwrap(), // TODO: Error handling - mw.req, - ), - ctx, - state, - ) - }, - PhantomData::, - ) - } -} - -mod private { - use crate::internal::middleware::SealedMiddleware; - - use super::*; - - pub struct MiddlewareFnWithTypeMapper(pub(super) F, pub(super) PhantomData); - - impl SealedMiddleware for MiddlewareFnWithTypeMapper - where - TLCtx: Send + Sync + 'static, - F: Fn(MiddlewareContext, TLCtx) -> Fu + Send + Sync + 'static, - Fu: Future + Send + 'static, - Fu::Output: MwV2Result + Send + 'static, - M: MwArgMapper + 'static, - { - type Fut = Fu; - type Result = Fu::Output; - type NewCtx = ::Ctx; // TODO: Make this work with context switching - type Arg = M::Input; - - fn run_me(&self, ctx: TLCtx, mw: MiddlewareContext) -> Self::Fut { - (self.0)(mw, ctx) - } - } -}