rspc/
legacy.rs

1//! TODO: Explain how to do it.
2
3use std::{borrow::Cow, collections::BTreeMap, panic::Location};
4
5use futures_util::{stream, FutureExt, StreamExt, TryStreamExt};
6use rspc_legacy::internal::{Layer, RequestContext, ValueOrStream};
7use rspc_procedure::{ProcedureStream, ResolverError};
8use serde_json::Value;
9use specta::{
10    datatype::{DataType, EnumRepr, EnumVariant, LiteralType},
11    NamedType, Type,
12};
13
14use crate::{
15    procedure::{ErasedProcedure, ProcedureType},
16    types::TypesOrType,
17    util::literal_object,
18    ProcedureKind,
19};
20
21impl<TCtx> From<rspc_legacy::Router<TCtx>> for crate::Router<TCtx> {
22    fn from(router: rspc_legacy::Router<TCtx>) -> Self {
23        let mut r = crate::Router::new();
24
25        let (queries, mutations, subscriptions, mut type_map) = router.into_parts();
26
27        let bridged_procedures = queries
28            .into_iter()
29            .map(|v| (ProcedureKind::Query, v))
30            .chain(mutations.into_iter().map(|v| (ProcedureKind::Mutation, v)))
31            .chain(
32                subscriptions
33                    .into_iter()
34                    .map(|v| (ProcedureKind::Subscription, v)),
35            )
36            .map(|(kind, (key, p))| {
37                (
38                    key.split(".")
39                        .map(|s| s.to_string().into())
40                        .collect::<Vec<Cow<'static, str>>>(),
41                    ErasedProcedure {
42                        kind,
43                        location: Location::caller().clone(), // TODO: This needs to actually be correct
44                        setup: Default::default(),
45                        inner: Box::new(move |_, types| {
46                            (
47                                layer_to_procedure(key.to_string(), kind, p.exec),
48                                ProcedureType {
49                                    kind,
50                                    input: p.ty.arg_ty.clone(),
51                                    output: p.ty.result_ty.clone(),
52                                    error: specta::datatype::DataType::Unknown,
53                                    // TODO: This location is obviously wrong but the legacy router has no location information.
54                                    // This will work properly with the new procedure syntax.
55                                    location: Location::caller().clone(), // TODO: This needs to actually be correct
56                                },
57                            )
58                        }),
59                    },
60                )
61            });
62
63        for (key, procedure) in bridged_procedures {
64            if r.procedures.insert(key.clone(), procedure).is_some() {
65                panic!("Attempted to mount '{key:?}' multiple times.\nrspc no longer supports different operations (query/mutation/subscription) with overlapping names.")
66            }
67        }
68
69        r.types.extend(&mut type_map);
70        r
71    }
72}
73
74pub(crate) fn layer_to_procedure<TCtx: 'static>(
75    path: String,
76    kind: ProcedureKind,
77    value: Box<dyn Layer<TCtx>>,
78) -> rspc_procedure::Procedure<TCtx> {
79    rspc_procedure::Procedure::new(move |ctx, input| {
80        let result = input.deserialize::<Value>().and_then(|input| {
81            value
82                .call(
83                    ctx,
84                    input,
85                    RequestContext {
86                        kind: match kind {
87                            ProcedureKind::Query => rspc_legacy::internal::ProcedureKind::Query,
88                            ProcedureKind::Mutation => {
89                                rspc_legacy::internal::ProcedureKind::Mutation
90                            }
91                            ProcedureKind::Subscription => {
92                                rspc_legacy::internal::ProcedureKind::Subscription
93                            }
94                        },
95                        path: path.clone(),
96                    },
97                )
98                .map_err(|err| {
99                    let err: rspc_legacy::Error = err.into();
100                    ResolverError::new(
101                        (), /* typesafe errors aren't supported in legacy router */
102                        Some(rspc_procedure::LegacyErrorInterop(err.message().into())),
103                    )
104                    .into()
105                })
106        });
107
108        match result {
109            Ok(result) => ProcedureStream::from_stream(
110                async move {
111                    match result.into_value_or_stream().await {
112                        Ok(ValueOrStream::Value(value)) => {
113                            stream::once(async { Ok(value) }).boxed()
114                        }
115                        Ok(ValueOrStream::Stream(s)) => s
116                            .map_err(|err| {
117                                let err = rspc_legacy::Error::from(err);
118                                ResolverError::new(
119                                    (), /* typesafe errors aren't supported in legacy router */
120                                    Some(rspc_procedure::LegacyErrorInterop(err.message().into())),
121                                )
122                                .into()
123                            })
124                            .boxed(),
125                        Err(err) => {
126                            let err: rspc_legacy::Error = err.into();
127                            let err = ResolverError::new(err.message().to_string(), err.cause());
128                            stream::once(async { Err(err.into()) }).boxed()
129                        }
130                    }
131                }
132                .into_stream()
133                .flatten(),
134            ),
135            Err(err) => {
136                ProcedureStream::from_stream(stream::once(async move { Err::<(), _>(err) }))
137            }
138        }
139    })
140}
141
142fn map_method(
143    kind: ProcedureKind,
144    p: &BTreeMap<Vec<Cow<'static, str>>, ProcedureType>,
145) -> Vec<(Cow<'static, str>, EnumVariant)> {
146    p.iter()
147        .filter(|(_, p)| p.kind == kind)
148        .map(|(key, p)| {
149            let key = key.join(".").to_string();
150            (
151                key.clone().into(),
152                specta::internal::construct::enum_variant(
153                    false,
154                    None,
155                    "".into(),
156                    specta::internal::construct::enum_variant_unnamed(vec![
157                        specta::internal::construct::field(
158                            false,
159                            false,
160                            None,
161                            "".into(),
162                            Some(literal_object(
163                                "".into(),
164                                None,
165                                vec![
166                                    ("key".into(), LiteralType::String(key.clone()).into()),
167                                    ("input".into(), p.input.clone()),
168                                    ("result".into(), p.output.clone()),
169                                ]
170                                .into_iter(),
171                            )),
172                        ),
173                    ]),
174                ),
175            )
176        })
177        .collect::<Vec<_>>()
178}
179
180// TODO: Remove this block with the interop system
181pub(crate) fn construct_legacy_bindings_type(
182    map: &BTreeMap<Cow<'static, str>, TypesOrType>,
183) -> Vec<(Cow<'static, str>, DataType)> {
184    #[derive(Type)]
185    struct Queries;
186    #[derive(Type)]
187    struct Mutations;
188    #[derive(Type)]
189    struct Subscriptions;
190
191    let mut p = BTreeMap::new();
192    for (k, v) in map {
193        flatten_procedures_for_legacy(&mut p, vec![k.clone()], v.clone());
194    }
195
196    vec![
197        (
198            "queries".into(),
199            specta::internal::construct::r#enum(
200                "Queries".into(),
201                Queries::sid(),
202                EnumRepr::Untagged,
203                false,
204                Default::default(),
205                map_method(ProcedureKind::Query, &p),
206            )
207            .into(),
208        ),
209        (
210            "mutations".into(),
211            specta::internal::construct::r#enum(
212                "Mutations".into(),
213                Mutations::sid(),
214                EnumRepr::Untagged,
215                false,
216                Default::default(),
217                map_method(ProcedureKind::Mutation, &p),
218            )
219            .into(),
220        ),
221        (
222            "subscriptions".into(),
223            specta::internal::construct::r#enum(
224                "Subscriptions".into(),
225                Subscriptions::sid(),
226                EnumRepr::Untagged,
227                false,
228                Default::default(),
229                map_method(ProcedureKind::Subscription, &p),
230            )
231            .into(),
232        ),
233    ]
234}
235
236fn flatten_procedures_for_legacy(
237    p: &mut BTreeMap<Vec<Cow<'static, str>>, ProcedureType>,
238    key: Vec<Cow<'static, str>>,
239    item: TypesOrType,
240) {
241    match item {
242        TypesOrType::Type(ty) => {
243            p.insert(key, ty);
244        }
245        TypesOrType::Types(types) => {
246            for (k, v) in types {
247                let mut key = key.clone();
248                key.push(k.clone());
249                flatten_procedures_for_legacy(p, key, v);
250            }
251        }
252    }
253}