rspc_legacy/
router.rs

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)]
27/// TODO
28pub 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
40// TODO: Move this out of this file
41// TODO: Rename??
42pub 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)] // Used for `rspc::legacy` interop
137    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)] // TODO
158    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        // TODO: Specta API
181        writeln!(
182            file,
183            r#"
184export type Procedures = {{
185    queries: {queries_ts},
186    mutations: {mutations_ts},
187    subscriptions: {subscriptions_ts}
188}};"#
189        )?;
190
191        // Generate type exports (non-Procedures)
192        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
204// TODO: Move this out into a Specta API
205fn 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                        // This condition is met with an empty enum or `()`.
218                        if def.elements().is_empty() =>
219                    {
220                        "never".into()
221                    }
222                    #[allow(clippy::unwrap_used)] // TODO
223                    ty => datatype(config,  &FunctionResultVariant::Value(ty.clone()), type_map).unwrap(),
224                };
225                #[allow(clippy::unwrap_used)] // TODO
226                let result_ts = datatype(
227                    config,
228                    &FunctionResultVariant::Value(operation.ty.result_ty.clone()),
229                    type_map,
230                )
231                .unwrap();
232
233                // TODO: Specta API
234                format!(
235                    r#"
236        {{ key: "{key}", input: {input}, result: {result_ts} }}"#
237                )
238            })
239            .collect::<Vec<_>>()
240            .join(" | "),
241    }
242}