1use std::{
2 borrow::Borrow,
3 collections::BTreeMap,
4 fs::{self, File},
5 io::Write,
6 marker::PhantomData,
7 path::{Path, PathBuf},
8 pin::Pin,
9 sync::Arc,
10};
11
12use futures::Stream;
13use rspc_procedure::Procedures;
14use serde_json::Value;
15use specta::{datatype::FunctionResultVariant, DataType, TypeCollection};
16use specta_typescript::{self as ts, datatype, export_named_datatype, Typescript};
17
18use crate::{
19 internal::{Procedure, ProcedureKind, ProcedureStore, RequestContext, ValueOrStream},
20 Config, ExecError, ExportError,
21};
22
23#[cfg_attr(
24 feature = "deprecated",
25 deprecated = "This is replaced by `rspc::Router`. Refer to the `rspc::legacy` module for bridging a legacy router into a modern one."
26)]
27pub struct Router<TCtx = (), TMeta = ()>
29where
30 TCtx: 'static,
31{
32 pub(crate) config: Config,
33 pub(crate) queries: ProcedureStore<TCtx>,
34 pub(crate) mutations: ProcedureStore<TCtx>,
35 pub(crate) subscriptions: ProcedureStore<TCtx>,
36 pub(crate) type_map: TypeCollection,
37 pub(crate) phantom: PhantomData<TMeta>,
38}
39
40pub enum ExecKind {
43 Query,
44 Mutation,
45}
46
47impl<TCtx, TMeta> Router<TCtx, TMeta>
48where
49 TCtx: 'static,
50{
51 pub async fn exec(
52 &self,
53 ctx: TCtx,
54 kind: ExecKind,
55 key: String,
56 input: Option<Value>,
57 ) -> Result<Value, ExecError> {
58 let (operations, kind) = match kind {
59 ExecKind::Query => (&self.queries.store, ProcedureKind::Query),
60 ExecKind::Mutation => (&self.mutations.store, ProcedureKind::Mutation),
61 };
62
63 match operations
64 .get(&key)
65 .ok_or_else(|| ExecError::OperationNotFound(key.clone()))?
66 .exec
67 .call(
68 ctx,
69 input.unwrap_or(Value::Null),
70 RequestContext {
71 kind,
72 path: key.clone(),
73 },
74 )?
75 .into_value_or_stream()
76 .await?
77 {
78 ValueOrStream::Value(v) => Ok(v),
79 ValueOrStream::Stream(_) => Err(ExecError::UnsupportedMethod(key)),
80 }
81 }
82
83 pub async fn exec_subscription(
84 &self,
85 ctx: TCtx,
86 key: String,
87 input: Option<Value>,
88 ) -> Result<Pin<Box<dyn Stream<Item = Result<Value, ExecError>> + Send>>, ExecError> {
89 match self
90 .subscriptions
91 .store
92 .get(&key)
93 .ok_or_else(|| ExecError::OperationNotFound(key.clone()))?
94 .exec
95 .call(
96 ctx,
97 input.unwrap_or(Value::Null),
98 RequestContext {
99 kind: ProcedureKind::Subscription,
100 path: key.clone(),
101 },
102 )?
103 .into_value_or_stream()
104 .await?
105 {
106 ValueOrStream::Value(_) => Err(ExecError::UnsupportedMethod(key)),
107 ValueOrStream::Stream(s) => Ok(s),
108 }
109 }
110
111 pub fn arced(self) -> Arc<Self> {
112 Arc::new(self)
113 }
114
115 #[deprecated = "Use `Self::type_map`"]
116 pub fn typ_store(&self) -> TypeCollection {
117 self.type_map.clone()
118 }
119
120 pub fn type_map(&self) -> TypeCollection {
121 self.type_map.clone()
122 }
123
124 pub fn queries(&self) -> &BTreeMap<String, Procedure<TCtx>> {
125 &self.queries.store
126 }
127
128 pub fn mutations(&self) -> &BTreeMap<String, Procedure<TCtx>> {
129 &self.mutations.store
130 }
131
132 pub fn subscriptions(&self) -> &BTreeMap<String, Procedure<TCtx>> {
133 &self.subscriptions.store
134 }
135
136 #[doc(hidden)] pub fn into_parts(
138 self,
139 ) -> (
140 BTreeMap<String, Procedure<TCtx>>,
141 BTreeMap<String, Procedure<TCtx>>,
142 BTreeMap<String, Procedure<TCtx>>,
143 TypeCollection,
144 ) {
145 if self.config.export_bindings_on_build.is_some() || self.config.bindings_header.is_some() {
146 panic!("Note: `rspc_legacy::Config` is ignored by `rspc::Router`. You should set the configuration on `rspc::Typescript` instead.");
147 }
148
149 (
150 self.queries.store,
151 self.mutations.store,
152 self.subscriptions.store,
153 self.type_map,
154 )
155 }
156
157 #[allow(clippy::unwrap_used)] pub fn export_ts<TPath: AsRef<Path>>(&self, export_path: TPath) -> Result<(), ExportError> {
159 let export_path = PathBuf::from(export_path.as_ref());
160 if let Some(export_dir) = export_path.parent() {
161 fs::create_dir_all(export_dir)?;
162 }
163 let mut file = File::create(export_path)?;
164 if let Some(header) = &self.config.bindings_header {
165 writeln!(file, "{}", header)?;
166 }
167 writeln!(file, "// This file was generated by [rspc](https://github.com/specta-rs/rspc). Do not edit this file manually.")?;
168
169 let config = Typescript::new().bigint(
170 ts::BigIntExportBehavior::FailWithReason(
171 "rspc does not support exporting bigint types (i64, u64, i128, u128) because they are lossily decoded by `JSON.parse` on the frontend. Tracking issue: https://github.com/specta-rs/rspc/issues/93",
172 )
173 );
174
175 let queries_ts = generate_procedures_ts(&config, &self.queries.store, &self.type_map);
176 let mutations_ts = generate_procedures_ts(&config, &self.mutations.store, &self.type_map);
177 let subscriptions_ts =
178 generate_procedures_ts(&config, &self.subscriptions.store, &self.type_map);
179
180 writeln!(
182 file,
183 r#"
184export type Procedures = {{
185 queries: {queries_ts},
186 mutations: {mutations_ts},
187 subscriptions: {subscriptions_ts}
188}};"#
189 )?;
190
191 for export in self
193 .type_map
194 .into_iter()
195 .map(|(_, ty)| export_named_datatype(&config, ty, &self.type_map).unwrap())
196 {
197 writeln!(file, "\n{}", export)?;
198 }
199
200 Ok(())
201 }
202}
203
204fn generate_procedures_ts<Ctx>(
206 config: &Typescript,
207 procedures: &BTreeMap<String, Procedure<Ctx>>,
208 type_map: &TypeCollection,
209) -> String {
210 match procedures.len() {
211 0 => "never".to_string(),
212 _ => procedures
213 .iter()
214 .map(|(key, operation)| {
215 let input = match &operation.ty.arg_ty {
216 DataType::Tuple(def)
217 if def.elements().is_empty() =>
219 {
220 "never".into()
221 }
222 #[allow(clippy::unwrap_used)] ty => datatype(config, &FunctionResultVariant::Value(ty.clone()), type_map).unwrap(),
224 };
225 #[allow(clippy::unwrap_used)] let result_ts = datatype(
227 config,
228 &FunctionResultVariant::Value(operation.ty.result_ty.clone()),
229 type_map,
230 )
231 .unwrap();
232
233 format!(
235 r#"
236 {{ key: "{key}", input: {input}, result: {result_ts} }}"#
237 )
238 })
239 .collect::<Vec<_>>()
240 .join(" | "),
241 }
242}