Skip to main content

vorma/
core.rs

1use std::borrow::Cow;
2use std::future::Future;
3#[cfg(test)]
4use std::sync::Arc;
5
6use http::Method;
7#[cfg(test)]
8use serde::Serialize;
9use serde_json::Value;
10#[cfg(test)]
11use vorma_tasks::Result as TaskResult;
12
13#[cfg(test)]
14use crate::api;
15#[cfg(test)]
16use crate::mux::RouteExecutionError;
17use crate::mux::{self, NestedRouter};
18#[cfg(test)]
19use crate::mux::{InputParser, None};
20use crate::searchparams;
21use crate::tsgen::{Type, TypePhase, TypeRef, TypeRegistry};
22
23mod context;
24mod contract;
25#[cfg(test)]
26use context::accepts_client_redirect;
27pub use context::{HeadHandle, MiddlewareCtx, Params, ResourceCtx, ResponseHandle, ViewCtx};
28use contract::RouteCollector;
29pub use contract::{Contract, ResourceEntry, ViewEntry, contract_for};
30#[cfg(test)]
31use pattern::default_resource_kind_for_method;
32use pattern::validate_declared_route_pattern;
33pub use pattern::{
34	default_resource_kind, params_for_pattern, pattern_is_splat, view_parents_for_patterns,
35};
36mod pattern;
37pub(crate) use runtime::{RuntimeRoutes, runtime_routes_for};
38mod runtime;
39#[cfg(test)]
40use runner::serialize_route_output;
41pub use runner::{
42	ErasedRequestCtx, ErasedRouteFuture, ErasedRouteHandler, PathParams, RouteFuture,
43	run_static_resource, run_static_view,
44};
45use runner::{RouteRunner, run_route_runner};
46mod runner;
47
48#[doc(hidden)]
49pub type TypeResolver = fn(TypePhase, &mut TypeRegistry) -> Result<TypeRef, String>;
50#[doc(hidden)]
51pub type SearchSchemaResolver = fn() -> Result<Value, String>;
52#[cfg(test)]
53type ResourceHandler<S, E, I, P, O> =
54	dyn Fn(ResourceCtx<S, E, I, P>) -> RouteFuture<O, E> + Send + Sync;
55#[cfg(test)]
56type ViewHandler<S, E, I, P, O> = dyn Fn(ViewCtx<S, E, I, P>) -> RouteFuture<O, E> + Send + Sync;
57
58#[doc(hidden)]
59pub fn type_resolver<T>(phase: TypePhase, registry: &mut TypeRegistry) -> Result<TypeRef, String>
60where
61	T: Type,
62{
63	T::collect_type_defs_for(phase, registry).map_err(|err| err.to_string())?;
64	Ok(T::type_ref_for(phase))
65}
66
67#[doc(hidden)]
68pub fn search_schema_resolver<T>() -> Result<Value, String>
69where
70	T: Type,
71{
72	searchparams::schema_for_type::<T>().map_err(|err| err.to_string())
73}
74
75/// Request-scoped middleware declaration shared by views and resources.
76pub struct Middleware<S, E = Box<dyn std::error::Error + Send + Sync>> {
77	mw: mux::Middleware<S, E>,
78}
79
80impl<S, E> Middleware<S, E>
81where
82	S: Send + Sync + 'static,
83	E: Send + Sync + 'static,
84{
85	/// Create middleware from an async handler.
86	pub fn new<F, Fut, O>(handler: F) -> Self
87	where
88		F: Fn(MiddlewareCtx<S, E>) -> Fut + Send + Sync + 'static,
89		Fut: Future<Output = vorma_tasks::Result<O, E>> + Send + 'static,
90		O: Send + Sync + 'static,
91	{
92		Self {
93			mw: mux::Middleware::new(move |ctx| {
94				let future = handler(MiddlewareCtx::new(ctx));
95				async move { future.await.map(|_| ()) }
96			}),
97		}
98	}
99
100	fn register_resource(&self, r: &mut mux::Router<S, E>) -> Result<(), mux::Error> {
101		r.use_middleware_entry(&self.mw);
102		Ok(())
103	}
104
105	fn register_view(&self, r: &mut NestedRouter<S, E>) -> Result<(), mux::Error> {
106		r.use_middleware_entry(&self.mw);
107		Ok(())
108	}
109
110	fn register_runtime(&self, routes: &mut RuntimeRoutes<S, E>) -> Result<(), mux::Error> {
111		self.register_resource(&mut routes.resources)?;
112		self.register_view(&mut routes.views)?;
113		Ok(())
114	}
115}
116
117/// Generated-client classification for a resource.
118#[derive(Clone, Copy, Debug, Eq, PartialEq)]
119pub enum ResourceKind {
120	/// Read-shaped resource. GET/HEAD resources default to this when no explicit kind is set.
121	Query,
122	/// Write-shaped resource. Non-GET/HEAD resources default to this when no explicit kind is set.
123	Mutation,
124}
125
126impl ResourceKind {
127	/// Return the generated TypeScript literal for this resource kind.
128	pub fn as_str(self) -> &'static str {
129		match self {
130			Self::Query => "query",
131			Self::Mutation => "mutation",
132		}
133	}
134}
135
136/// Nested view declaration.
137pub struct View<S, E = Box<dyn std::error::Error + Send + Sync>> {
138	pattern: Cow<'static, str>,
139	handler: RouteRunner<S, E>,
140	client_file: Cow<'static, str>,
141	input_type: TypeResolver,
142	output_type: TypeResolver,
143	search_schema: SearchSchemaResolver,
144}
145
146impl<S, E> View<S, E>
147where
148	S: Send + Sync + 'static,
149	E: Send + Sync + 'static,
150{
151	#[doc(hidden)]
152	pub const fn from_static(
153		pattern: &'static str,
154		client_file: &'static str,
155		input_type: TypeResolver,
156		output_type: TypeResolver,
157		search_schema: SearchSchemaResolver,
158		handler: ErasedRouteHandler<S, E>,
159	) -> Self {
160		Self {
161			pattern: Cow::Borrowed(pattern),
162			handler: RouteRunner::Static(handler),
163			client_file: Cow::Borrowed(client_file),
164			input_type,
165			output_type,
166			search_schema,
167		}
168	}
169
170	#[cfg(test)]
171	pub(crate) fn new<I, P, O, F, Fut>(
172		pattern: impl Into<String>,
173		client_file: impl Into<String>,
174		input_parser: InputParser<I>,
175		handler: F,
176	) -> Self
177	where
178		F: Fn(ViewCtx<S, E, I, P>) -> Fut + Send + Sync + 'static,
179		Fut: Future<Output = TaskResult<O, E>> + Send + 'static,
180		I: Type + api::ViewInput,
181		P: PathParams,
182		O: Type + Serialize + Send + Sync + 'static,
183	{
184		let handler: Arc<ViewHandler<S, E, I, P, O>> = Arc::new(move |ctx| Box::pin(handler(ctx)));
185		Self {
186			pattern: Cow::Owned(pattern.into()),
187			handler: RouteRunner::Dynamic(Arc::new(move |ctx| {
188				let input_parser = input_parser.clone();
189				let handler = handler.clone();
190				Box::pin(async move {
191					let input = input_parser
192						.parse(ctx.request())
193						.await
194						.map_err(RouteExecutionError::Input)?;
195					let params = P::from_raw_path_params(ctx.params())
196						.map_err(RouteExecutionError::Input)?;
197					let output = handler(ViewCtx::new(ctx.with_input(input), params))
198						.await
199						.map_err(RouteExecutionError::Task)?;
200					serialize_route_output(output)
201				})
202			})),
203			client_file: Cow::Owned(client_file.into()),
204			input_type: type_resolver::<I>,
205			output_type: type_resolver::<O>,
206			search_schema: search_schema_resolver::<I>,
207		}
208	}
209
210	/// Registered view pattern.
211	pub fn pattern(&self) -> &str {
212		&self.pattern
213	}
214
215	/// Frontend client module associated with this view.
216	pub fn client_file(&self) -> &str {
217		&self.client_file
218	}
219}
220
221/// Collection of middleware declarations.
222#[derive(Default)]
223pub struct Middlewares<S, E = Box<dyn std::error::Error + Send + Sync>> {
224	middlewares: Vec<Middleware<S, E>>,
225}
226
227impl<S, E> Middlewares<S, E>
228where
229	S: Send + Sync + 'static,
230	E: Send + Sync + 'static,
231{
232	/// Create an empty middleware collection.
233	pub fn new() -> Self {
234		Self {
235			middlewares: Vec::new(),
236		}
237	}
238
239	/// Append a middleware declaration.
240	pub fn push(&mut self, middleware: Middleware<S, E>) -> &mut Self {
241		self.middlewares.push(middleware);
242		self
243	}
244
245	fn register_runtime(&self, routes: &mut RuntimeRoutes<S, E>) -> Result<(), mux::Error> {
246		for middleware in &self.middlewares {
247			middleware.register_runtime(routes)?;
248		}
249		Ok(())
250	}
251}
252
253impl<S, E> View<S, E>
254where
255	S: Send + Sync + 'static,
256	E: Send + Sync + 'static,
257{
258	fn register_contract(&self, collector: &mut RouteCollector) -> Result<(), String> {
259		let input = (self.input_type)(TypePhase::Deserialize, &mut collector.types)?;
260		let output = (self.output_type)(TypePhase::Serialize, &mut collector.types)?;
261		collector.views.push(ViewEntry {
262			pattern: self.pattern.to_string(),
263			client_file: self.client_file.to_string(),
264			input,
265			output,
266			search_schema: (self.search_schema)()
267				.map_err(|err| format!("view {} input: {err}", self.pattern))?,
268		});
269		Ok(())
270	}
271
272	fn register_to_mux(&self, r: &mut NestedRouter<S, E>) -> Result<(), mux::Error> {
273		validate_declared_route_pattern("view", &self.pattern)
274			.map_err(mux::Error::InvalidPattern)?;
275		let handler = self.handler.clone();
276		r.add_handler_entry(
277			self.pattern.to_string(),
278			mux::erased_handler(move |ctx| {
279				let handler = handler.clone();
280				async move { run_route_runner(handler, ctx).await }
281			}),
282		)
283	}
284}
285
286/// Resource declaration.
287pub struct Resource<S, E = Box<dyn std::error::Error + Send + Sync>> {
288	method: Method,
289	pattern: Cow<'static, str>,
290	kind: Option<ResourceKind>,
291	handler: RouteRunner<S, E>,
292	input_type: TypeResolver,
293	output_type: TypeResolver,
294}
295
296impl<S, E> Resource<S, E>
297where
298	S: Send + Sync + 'static,
299	E: Send + Sync + 'static,
300{
301	#[doc(hidden)]
302	pub const fn from_static(
303		method: Method,
304		pattern: &'static str,
305		kind: Option<ResourceKind>,
306		input_type: TypeResolver,
307		output_type: TypeResolver,
308		handler: ErasedRouteHandler<S, E>,
309	) -> Self {
310		Self {
311			method,
312			pattern: Cow::Borrowed(pattern),
313			kind,
314			handler: RouteRunner::Static(handler),
315			input_type,
316			output_type,
317		}
318	}
319
320	#[cfg(test)]
321	pub(crate) fn new<I, P, O, F, Fut>(
322		method: Method,
323		pattern: impl Into<String>,
324		kind: Option<ResourceKind>,
325		input_parser: InputParser<I>,
326		handler: F,
327	) -> Self
328	where
329		F: Fn(ResourceCtx<S, E, I, P>) -> Fut + Send + Sync + 'static,
330		Fut: Future<Output = TaskResult<O, E>> + Send + 'static,
331		I: Type + api::ResourceInput,
332		P: PathParams,
333		O: Type + Serialize + Send + Sync + 'static,
334	{
335		let handler: Arc<ResourceHandler<S, E, I, P, O>> =
336			Arc::new(move |ctx| Box::pin(handler(ctx)));
337		Self {
338			method,
339			pattern: Cow::Owned(pattern.into()),
340			kind,
341			handler: RouteRunner::Dynamic(Arc::new(move |ctx| {
342				let input_parser = input_parser.clone();
343				let handler = handler.clone();
344				Box::pin(async move {
345					let input = input_parser
346						.parse(ctx.request())
347						.await
348						.map_err(RouteExecutionError::Input)?;
349					let params = P::from_raw_path_params(ctx.params())
350						.map_err(RouteExecutionError::Input)?;
351					let output = handler(ResourceCtx::new(ctx.with_input(input), params))
352						.await
353						.map_err(RouteExecutionError::Task)?;
354					serialize_route_output(output)
355				})
356			})),
357			input_type: type_resolver::<I>,
358			output_type: type_resolver::<O>,
359		}
360	}
361
362	/// HTTP method registered for this resource.
363	pub fn method(&self) -> &Method {
364		&self.method
365	}
366
367	/// Resource pattern relative to the configured API mount root.
368	pub fn pattern(&self) -> &str {
369		&self.pattern
370	}
371
372	/// Explicit generated-client kind override, when one was declared.
373	pub fn kind(&self) -> Option<ResourceKind> {
374		self.kind
375	}
376}
377
378impl<S, E> Resource<S, E>
379where
380	S: Send + Sync + 'static,
381	E: Send + Sync + 'static,
382{
383	#[cfg(test)]
384	pub(crate) fn without_handler(
385		method: Method,
386		pattern: impl Into<String>,
387		kind: Option<ResourceKind>,
388	) -> Self {
389		Self::new(
390			method,
391			pattern,
392			kind,
393			InputParser::default_input(),
394			|_: ResourceCtx<S, E, (), ()>| async { Ok(None) },
395		)
396	}
397}
398
399impl<S, E> Resource<S, E>
400where
401	S: Send + Sync + 'static,
402	E: Send + Sync + 'static,
403{
404	fn register_contract(&self, collector: &mut RouteCollector) -> Result<(), String> {
405		let input = (self.input_type)(TypePhase::Deserialize, &mut collector.types)?;
406		let output = (self.output_type)(TypePhase::Serialize, &mut collector.types)?;
407		collector.resources.push(ResourceEntry {
408			method: self.method.as_str().to_owned(),
409			pattern: self.pattern.to_string(),
410			kind: self.kind,
411			input,
412			output,
413		});
414		Ok(())
415	}
416
417	fn register_to_mux(&self, r: &mut mux::Router<S, E>) -> Result<(), mux::Error> {
418		validate_declared_route_pattern("resource", &self.pattern)
419			.map_err(mux::Error::InvalidPattern)?;
420		let handler = self.handler.clone();
421		r.add_handler_entry(
422			self.method.clone(),
423			self.pattern.to_string(),
424			mux::erased_handler(move |ctx| {
425				let handler = handler.clone();
426				async move { run_route_runner(handler, ctx).await }
427			}),
428		)?;
429		Ok(())
430	}
431}
432
433/// Collection of nested view declarations.
434#[derive(Default)]
435pub struct Views<S, E = Box<dyn std::error::Error + Send + Sync>> {
436	views: Vec<View<S, E>>,
437}
438
439impl<S, E> Views<S, E>
440where
441	S: Send + Sync + 'static,
442	E: Send + Sync + 'static,
443{
444	/// Create an empty view collection.
445	pub fn new() -> Self {
446		Self { views: Vec::new() }
447	}
448
449	/// Append a view declaration.
450	pub fn push(&mut self, view: View<S, E>) -> &mut Self {
451		self.views.push(view);
452		self
453	}
454
455	fn register_contract(&self, collector: &mut RouteCollector) -> Result<(), String> {
456		for view in &self.views {
457			view.register_contract(collector)?;
458		}
459		Ok(())
460	}
461
462	fn register_runtime(&self, routes: &mut RuntimeRoutes<S, E>) -> Result<(), mux::Error> {
463		for view in &self.views {
464			view.register_to_mux(&mut routes.views)?;
465		}
466		Ok(())
467	}
468}
469
470/// Collection of resource declarations.
471#[derive(Default)]
472pub struct Resources<S, E = Box<dyn std::error::Error + Send + Sync>> {
473	resources: Vec<Resource<S, E>>,
474}
475
476impl<S, E> Resources<S, E>
477where
478	S: Send + Sync + 'static,
479	E: Send + Sync + 'static,
480{
481	/// Create an empty resource collection.
482	pub fn new() -> Self {
483		Self {
484			resources: Vec::new(),
485		}
486	}
487
488	/// Append a resource declaration.
489	pub fn push(&mut self, resource: Resource<S, E>) -> &mut Self {
490		self.resources.push(resource);
491		self
492	}
493
494	fn register_contract(&self, collector: &mut RouteCollector) -> Result<(), String> {
495		for resource in &self.resources {
496			resource.register_contract(collector)?;
497		}
498		Ok(())
499	}
500
501	fn register_runtime(&self, routes: &mut RuntimeRoutes<S, E>) -> Result<(), mux::Error> {
502		for resource in &self.resources {
503			resource.register_to_mux(&mut routes.resources)?;
504		}
505		Ok(())
506	}
507}
508
509#[cfg(test)]
510#[path = "core_tests.rs"]
511mod core_tests;