vorma 0.86.0-pre.7

Rust-first full-stack web framework: typed views and resources, generated TypeScript contracts, and a Vite-integrated frontend.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
//! Task middleware, view loader, and API endpoint declarations. Creating a
//! declaration starts nothing and registers nothing.

use std::any::TypeId;
use std::fmt;
use std::future::Future;
use std::panic::Location;
use std::sync::Arc;

use bytes::Bytes;
use futures_util::future::{self, BoxFuture, FutureExt};
use http::Method;
use primitives_rs::async_work::memoized_tasks::TaskCtx;
use primitives_rs::tsgen::{self, TsGen, TypePhase, TypeRef, TypeRegistry};
use serde::Serialize;
use serde::de::DeserializeOwned;

use crate::ctx::{ApiEndpointCtx, HandlerFacts, RequestFacts, TaskMiddlewareCtx, ViewLoaderCtx};
use crate::form::{FormBody, FormInput};
use crate::json_objects::JsonObjectPositions;
use crate::outcome::{
	ApiEndpointTypes, HandlerLabel, Outcome, RedirectingApiEndpoint, from_api_endpoint_result,
	from_task_middleware_result, from_view_loader_result, invalid_input_outcome,
	invalid_server_result_outcome,
};
use crate::pattern::{ApiPatternParams, DecodedMatch, ViewPatternParams};
use crate::result::{ApiEndpointResult, Continue, Render, RequestRejected, TerminalOutcome};
use crate::search::{SearchShape, SearchShapeError, derive_search_shape};
use primitives_rs::http::form_data::{
	FormContentType, FormDecodeConfig, FormDecodeError, FormLimits, decode_form_data,
};

/// One handler invocation's request-scoped inputs.
pub(crate) struct Invocation<State, Input> {
	pub(crate) state: Arc<State>,
	pub(crate) facts: Arc<RequestFacts>,
	pub(crate) task_ctx: TaskCtx<State>,
	pub(crate) decoded_match: DecodedMatch,
	pub(crate) input: Input,
	/// What the handler's reports name.
	pub(crate) label: HandlerLabel,
}

type HandlerInvoker<Input> = Arc<dyn Fn(Input) -> BoxFuture<'static, Outcome> + Send + Sync>;

/// The TypeScript types of one handler's input, output, and declared errors.
/// `None` when the handler has no input or no output.
pub(crate) struct HandlerTypeRefs {
	pub(crate) input: Option<TypeRef>,
	pub(crate) output: Option<TypeRef>,
	pub(crate) errors: TypeRef,
}

type CollectTypes = fn(&mut TypeRegistry) -> tsgen::Result<HandlerTypeRefs>;

fn collect_types<Input: TsGen, Output: TsGen, DeclaredError: TsGen>(
	registry: &mut TypeRegistry,
) -> tsgen::Result<HandlerTypeRefs> {
	Input::collect_type_defs_for(TypePhase::Deserialize, registry)?;
	// Input inspection must succeed before an app can serve requests.
	primitives_rs::tsgen::TypeGraph::new(
		Input::type_ref_for(TypePhase::Deserialize),
		registry.clone(),
	)?;
	Output::collect_type_defs_for(TypePhase::Serialize, registry)?;
	DeclaredError::collect_type_defs_for(TypePhase::Serialize, registry)?;
	Ok(HandlerTypeRefs {
		input: (!is_unit::<Input>()).then(|| Input::type_ref_for(TypePhase::Deserialize)),
		output: (!is_unit::<Output>()).then(|| Output::type_ref_for(TypePhase::Serialize)),
		errors: DeclaredError::type_ref_for(TypePhase::Serialize),
	})
}

fn is_unit<Value: 'static>() -> bool {
	TypeId::of::<Value>() == TypeId::of::<()>()
}

/// A view loader's search shape. A loader with the input `()` has none.
fn derive_view_search_shape<Input: TsGen + 'static>()
-> Result<Option<SearchShape>, SearchShapeError> {
	if is_unit::<Input>() {
		return Ok(None);
	}
	derive_search_shape::<Input>().map(Some)
}

fn is_search_input_valid<Input: DeserializeOwned>(value: serde_json::Value) -> bool {
	serde_json::from_value::<Input>(value).is_ok()
}

impl<State> ViewLoader<State> {
	/// How messages identify the loader: its location, list, and pattern.
	pub(crate) fn describe(&self) -> String {
		format!("{} view_loaders {:?}", self.location, self.pattern)
	}
}

impl<State> ApiEndpoint<State> {
	/// How messages identify the endpoint: its location, list, method, and
	/// pattern.
	pub(crate) fn describe(&self) -> String {
		format!(
			"{} api_endpoints {} {:?}",
			self.location, self.method, self.pattern
		)
	}
}

impl<State> TaskMiddleware<State> {
	/// How messages identify the middleware: its location and list.
	pub(crate) fn describe(&self) -> String {
		format!("{} task_middlewares", self.location)
	}
}

/// A view loader: the server half of one view.
///
/// A view that defines no explicit component is a pass-through view: the
/// client renders its child in its place. A view with a loader and no client
/// definition is one, and so is a view whose `createView` definition has no
/// `component`; its client loader still runs and its `childErrorComponent`
/// still renders a failed child.
///
/// The handler runs in its own Tokio task, so a `tokio::task_local!` value that
/// the host sets is not visible to it. Per-request values that the host
/// computes travel in the request's extensions, which the context's `request_parts`
/// holds.
pub struct ViewLoader<State> {
	/// Where the application declared the loader.
	pub(crate) location: &'static Location<'static>,
	pub(crate) pattern: &'static str,
	pub(crate) derive_search_shape_fn: fn() -> Result<Option<SearchShape>, SearchShapeError>,
	/// Whether a search value's fields decode as this loader's input.
	pub(crate) is_search_input_valid_fn: fn(serde_json::Value) -> bool,
	pub(crate) collect_types_fn: CollectTypes,
	pub(crate) invoke_handler_fn: HandlerInvoker<Invocation<State, serde_json::Value>>,
}

impl<State> Clone for ViewLoader<State> {
	fn clone(&self) -> Self {
		Self {
			location: self.location,
			pattern: self.pattern,
			derive_search_shape_fn: self.derive_search_shape_fn,
			is_search_input_valid_fn: self.is_search_input_valid_fn,
			collect_types_fn: self.collect_types_fn,
			invoke_handler_fn: Arc::clone(&self.invoke_handler_fn),
		}
	}
}

impl<State> fmt::Debug for ViewLoader<State> {
	fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
		formatter
			.debug_tuple("ViewLoader")
			.field(&self.pattern)
			.finish()
	}
}

impl<State: Send + Sync + 'static> ViewLoader<State> {
	/// Declares the loader of the view pattern that the handler's params type
	/// belongs to: a handler that takes
	/// `ViewLoaderCtx<State, UserParams, Input>` loads the pattern
	/// `UserParams::PATTERN`.
	///
	/// The handler's input is the pattern's search input: `()` reads none, and
	/// a serde struct declares the search shape that the browser shares.
	/// The contract cannot declare a top-level field named `vorma-json`, which
	/// belongs to Vorma's wire protocol, and loading the app fails when it
	/// does. While Vercel skew protection is on, it cannot declare one named
	/// `dpl` either, because Vercel reserves that name, and loading the app
	/// fails then as well. While skew protection is off, `dpl` is an ordinary
	/// search name.
	/// Its output is the view's data: `()` means none. Its declared
	/// errors are the public errors that the browser's error components
	/// receive.
	#[track_caller]
	pub fn new<Params, Input, Output, DeclaredError, HandlerFn, Fut>(handler: HandlerFn) -> Self
	where
		Params: ViewPatternParams,
		Input: DeserializeOwned + TsGen,
		Output: Serialize + TsGen,
		DeclaredError: Serialize + TsGen,
		HandlerFn: Fn(ViewLoaderCtx<State, Params, Input>) -> Fut + Send + Sync + 'static,
		Fut: Future<Output = Result<Render<Output>, TerminalOutcome<DeclaredError>>>
			+ Send
			+ 'static,
	{
		let pattern = Params::PATTERN;
		let handler = Arc::new(handler);
		let invoke_handler =
			move |invocation: Invocation<State, serde_json::Value>| -> BoxFuture<'static, Outcome> {
				let handler = Arc::clone(&handler);
				// Decoding and the handler run inside the returned future, where
				// execution captures a panic, including one before the first await.
				async move {
					let label = invocation.label;
					let params = match Params::from_decoded_match(invocation.decoded_match) {
						Ok(params) => params,
						Err(error) => {
							return invalid_server_result_outcome(&label, &error.to_string());
						}
					};
					let Ok(input) = serde_json::from_value::<Input>(invocation.input) else {
						return invalid_input_outcome();
					};
					let facts = invocation.facts;
					let ctx = facts.to_view_loader_ctx(HandlerFacts {
						state: invocation.state,
						params,
						input,
						pattern,
						task_ctx: invocation.task_ctx,
					});
					from_view_loader_result(
						&label,
						handler(ctx).await,
						&*facts.target_resolver,
						facts.head_rules.as_deref(),
					)
				}
				.boxed()
			};
		Self {
			location: Location::caller(),
			pattern,
			derive_search_shape_fn: derive_view_search_shape::<Input>,
			is_search_input_valid_fn: is_search_input_valid::<Input>,
			collect_types_fn: collect_types::<Input, Output, DeclaredError>,
			invoke_handler_fn: Arc::new(invoke_handler),
		}
	}
}

/// How the browser treats an endpoint call. After a successful mutation,
/// Vorma refreshes the current page's data.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ApiEndpointKind {
	/// A call that reads.
	Query,
	/// A call that changes something.
	Mutation,
}

/// Where an endpoint's input arrives.
#[doc(hidden)]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum ApiEndpointInputKind {
	/// The endpoint reads no input and Vorma does not read the body.
	None,
	/// A `GET` endpoint reads the URL's search parameters.
	Search,
	/// The body is a JSON value.
	Json,
	/// The body is a form.
	FormData,
}

/// An endpoint's raw input, read according to its input kind.
#[doc(hidden)]
pub enum ApiEndpointInputSource {
	/// No input.
	None,
	/// Search parameters, parsed with the endpoint's search shape.
	Search(serde_json::Value),
	/// A collected JSON body.
	Json(Bytes),
	/// A form body, which is read as it is decoded.
	FormData {
		/// The form's media type, read from the request's content type.
		media_type: FormContentType,
		/// The endpoint's request-body limit.
		limits: FormLimits,
		/// The body's chunks.
		body: FormBody,
	},
}

/// Why endpoint input could not be decoded.
#[doc(hidden)]
#[derive(Debug)]
pub enum InputError {
	/// The input does not fit its type.
	Invalid,
	/// The request's `Content-Type` is not one that the endpoint reads: for a
	/// JSON body, `application/json` or a media type that ends in `+json`; for a
	/// form, `application/x-www-form-urlencoded` or `multipart/form-data`.
	UnsupportedMediaType,
	/// The form exceeded the endpoint's input-size limit.
	PayloadTooLarge,
	/// The request body could not be read.
	BodyRead(Box<dyn std::error::Error + Send + Sync>),
	/// An uploaded file could not be stored.
	Storage(std::io::Error),
}

impl From<FormDecodeError> for InputError {
	fn from(error: FormDecodeError) -> Self {
		match error {
			FormDecodeError::MalformedMultipart => Self::Invalid,
			FormDecodeError::LimitExceeded(_) => Self::PayloadTooLarge,
			FormDecodeError::BodyRead(error) => Self::BodyRead(error),
			FormDecodeError::Storage(error) => Self::Storage(error),
		}
	}
}

/// The outcome of input that could not be decoded: 400 for invalid input or a
/// body that could not be read, 413 for a body that a body limit stopped, 415
/// for a body whose media type the endpoint does not read, and 500 for an upload that could
/// not be stored.
pub(crate) fn input_failure_outcome(label: &HandlerLabel, error: InputError) -> Outcome {
	match error {
		InputError::Invalid => invalid_input_outcome(),
		InputError::PayloadTooLarge => Outcome::Failure {
			contribution: crate::outcome::Contribution::default(),
			status: http::StatusCode::PAYLOAD_TOO_LARGE,
			public_error: None,
		},
		InputError::UnsupportedMediaType => Outcome::Failure {
			contribution: crate::outcome::Contribution::default(),
			status: http::StatusCode::UNSUPPORTED_MEDIA_TYPE,
			public_error: None,
		},
		InputError::BodyRead(error) => {
			let status = if is_length_limit_error(&*error) {
				http::StatusCode::PAYLOAD_TOO_LARGE
			} else {
				http::StatusCode::BAD_REQUEST
			};
			Outcome::Failure {
				contribution: crate::outcome::Contribution::default(),
				status,
				public_error: None,
			}
		}
		InputError::Storage(error) => {
			tracing::error!(
				handler = label.kind.span_name(),
				pattern = &*label.pattern,
				path = &*label.request_path,
				error = &error as &(dyn std::error::Error + 'static),
				"Vorma could not store an uploaded file",
			);
			Outcome::internal_server_error()
		}
	}
}

/// Whether a body limit caused `error`. The limit's error can be the outermost
/// error, as with Vorma's own limit, or it can sit anywhere in the source chain
/// of a host server's body error: axum wraps every body error in its own error
/// type.
fn is_length_limit_error(error: &(dyn std::error::Error + 'static)) -> bool {
	let mut current = Some(error);
	while let Some(error) = current {
		if error.is::<http_body_util::LengthLimitError>() {
			return true;
		}
		current = error.source();
	}
	false
}

/// An API endpoint's input type: `()` for none, [`FormInput`] for a form body,
/// or a serde type. A `GET` endpoint reads a serde type from the URL's search
/// parameters, and any other method reads it from the JSON body. A request
/// whose `Content-Type` is not `application/json` or a media type that ends in
/// `+json` gets a 415 failure, because a cross-site form can send a body with
/// another media type without a CORS preflight.
///
/// A JSON body must have an object wherever the input type has a struct, at
/// any depth. serde's derived `Deserialize` would also accept an array of the
/// struct's field values in declaration order, but Vorma answers such a body
/// with a 400 failure and does not run the handler. In an enum, the structs
/// are those of the variant that the body's tag selects; in an untagged enum
/// whose variants have the same JSON shape, a body that one of them admits is
/// admitted. For this check, Vorma parses the whole body as a
/// `serde_json::Value`, so a body that `serde_json` cannot parse that way gets
/// a 400 failure even when the problem is in a field that the input type
/// ignores: nesting deeper than 128 levels, a number outside the range of
/// `f64`, or an escaped lone surrogate.
///
/// That rule does not protect an endpoint without JSON input from a cross-site
/// request. An endpoint with input `()` reads no body and checks no media type,
/// and a form endpoint reads the media types that a cross-site form sends, so
/// both run for a cross-site form request. Vorma checks neither `Origin` nor
/// `Sec-Fetch-Site`. An application that authenticates requests by a cookie
/// must make that check itself, in the host's middleware or in a task
/// middleware.
///
/// A `GET`
/// endpoint's search shape cannot declare a top-level field named
/// `vorma-json`, which belongs to Vorma's wire protocol, and loading the app
/// fails when it does. While Vercel skew protection is on, it cannot declare
/// one named `dpl` either, because Vercel reserves that name, and loading the
/// app fails then as well. While skew protection is off, `dpl` is an ordinary
/// search name.
pub trait ApiEndpointInput: TsGen + Sized + Send + 'static {
	/// Where this input arrives for an endpoint with this method.
	#[doc(hidden)]
	fn __input_kind(method: &Method) -> Result<ApiEndpointInputKind, &'static str>;

	/// Decodes the input from its raw source.
	#[doc(hidden)]
	fn __decode(source: ApiEndpointInputSource) -> BoxFuture<'static, Result<Self, InputError>>;

	/// Whether this search input, as an endpoint target supplies it, fits the
	/// input type.
	#[doc(hidden)]
	fn __is_search_input_valid(value: serde_json::Value) -> bool;
}

impl<Input: DeserializeOwned + TsGen + Send + 'static> ApiEndpointInput for Input {
	fn __input_kind(method: &Method) -> Result<ApiEndpointInputKind, &'static str> {
		Ok(if is_unit::<Input>() {
			ApiEndpointInputKind::None
		} else if method == Method::GET {
			ApiEndpointInputKind::Search
		} else {
			ApiEndpointInputKind::Json
		})
	}

	fn __decode(source: ApiEndpointInputSource) -> BoxFuture<'static, Result<Self, InputError>> {
		let decoded = match source {
			ApiEndpointInputSource::None => {
				serde_json::from_value(serde_json::Value::Null).map_err(|_| InputError::Invalid)
			}
			ApiEndpointInputSource::Search(value) => {
				serde_json::from_value(value).map_err(|_| InputError::Invalid)
			}
			ApiEndpointInputSource::Json(bytes) => {
				serde_json::from_slice(&bytes).map_err(|_| InputError::Invalid)
			}
			ApiEndpointInputSource::FormData { .. } => Err(InputError::Invalid),
		};
		future::ready(decoded).boxed()
	}

	fn __is_search_input_valid(value: serde_json::Value) -> bool {
		is_search_input_valid::<Input>(value)
	}
}

impl<Form: DeserializeOwned + TsGen> ApiEndpointInput for FormInput<Form> {
	fn __input_kind(method: &Method) -> Result<ApiEndpointInputKind, &'static str> {
		if method == Method::GET {
			return Err("a GET endpoint reads search parameters, so it cannot take form input");
		}
		Ok(ApiEndpointInputKind::FormData)
	}

	fn __decode(source: ApiEndpointInputSource) -> BoxFuture<'static, Result<Self, InputError>> {
		async move {
			let ApiEndpointInputSource::FormData {
				media_type,
				body,
				limits,
			} = source
			else {
				return Err(InputError::Invalid);
			};
			Ok(FormInput(
				decode_form_data(
					media_type,
					body,
					FormDecodeConfig {
						limits,
						upload_dir: None,
					},
				)
				.await?
				.into_deserialized::<Form>()
				.map_err(|_| InputError::Invalid)?,
			))
		}
		.boxed()
	}

	fn __is_search_input_valid(_value: serde_json::Value) -> bool {
		false
	}
}

/// An API endpoint: one method and pattern that answers with data.
///
/// The handler runs in its own Tokio task, so a `tokio::task_local!` value that
/// the host sets is not visible to it. Per-request values that the host
/// computes travel in the request's extensions, which the context's `request_parts`
/// holds.
pub struct ApiEndpoint<State> {
	/// Where the application declared the endpoint.
	pub(crate) location: &'static Location<'static>,
	pub(crate) kind: ApiEndpointKind,
	pub(crate) method: Method,
	pub(crate) pattern: &'static str,
	pub(crate) determine_input_kind_fn: fn(&Method) -> Result<ApiEndpointInputKind, &'static str>,
	pub(crate) derive_search_shape_fn: fn() -> Result<SearchShape, SearchShapeError>,
	pub(crate) is_search_input_valid_fn: fn(serde_json::Value) -> bool,
	pub(crate) max_request_body_size: Option<usize>,
	pub(crate) types: ApiEndpointTypes,
	pub(crate) collect_types_fn: CollectTypes,
	pub(crate) invoke_handler_fn: HandlerInvoker<Invocation<State, ApiEndpointInputSource>>,
}

impl<State> Clone for ApiEndpoint<State> {
	fn clone(&self) -> Self {
		Self {
			location: self.location,
			kind: self.kind,
			method: self.method.clone(),
			pattern: self.pattern,
			determine_input_kind_fn: self.determine_input_kind_fn,
			derive_search_shape_fn: self.derive_search_shape_fn,
			is_search_input_valid_fn: self.is_search_input_valid_fn,
			max_request_body_size: self.max_request_body_size,
			types: self.types,
			collect_types_fn: self.collect_types_fn,
			invoke_handler_fn: Arc::clone(&self.invoke_handler_fn),
		}
	}
}

impl<State> fmt::Debug for ApiEndpoint<State> {
	fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
		formatter
			.debug_struct("ApiEndpoint")
			.field("kind", &self.kind)
			.field("method", &self.method)
			.field("pattern", &self.pattern)
			.finish()
	}
}

impl<State: Send + Sync + 'static> ApiEndpoint<State> {
	/// Declares an endpoint of the API pattern that the handler's params type
	/// belongs to: a handler that takes
	/// `ApiEndpointCtx<State, UserApiParams, Input>` serves the pattern
	/// `UserApiParams::PATTERN`. `kind` decides how the browser treats a
	/// call, and `method` decides where input arrives, as
	/// [`ApiEndpointInput`] describes.
	///
	/// Vorma reads at most
	/// [`App::max_request_body_size`](crate::App::max_request_body_size) of
	/// a JSON or form body, including uploaded files, unless
	/// [`Self::set_max_request_body_size`] sets another limit for this
	/// endpoint.
	#[track_caller]
	pub fn new<Params, Input, Output, DeclaredError, HandlerFn, Fut>(
		kind: ApiEndpointKind,
		method: Method,
		handler: HandlerFn,
	) -> Self
	where
		Params: ApiPatternParams,
		Input: ApiEndpointInput,
		Output: Serialize + TsGen + 'static,
		DeclaredError: Serialize + TsGen + 'static,
		HandlerFn: Fn(ApiEndpointCtx<State, Params, Input>) -> Fut + Send + Sync + 'static,
		Fut: Future<Output = Result<ApiEndpointResult<Output>, TerminalOutcome<DeclaredError>>>
			+ Send
			+ 'static,
	{
		let types = ApiEndpointTypes {
			input: TypeId::of::<Input>(),
			output: TypeId::of::<Output>(),
			declared_error: TypeId::of::<DeclaredError>(),
		};
		let redirecting = Arc::new(RedirectingApiEndpoint {
			method: method.clone(),
			types,
		});
		let pattern = Params::PATTERN;
		let handler = Arc::new(handler);
		let json_objects = Arc::new(JsonObjectPositions::of::<Input>());
		let invoke_handler = move |invocation: Invocation<State, ApiEndpointInputSource>| -> BoxFuture<'static, Outcome> {
			let redirecting = Arc::clone(&redirecting);
			let handler = Arc::clone(&handler);
			let json_objects = Arc::clone(&json_objects);
			async move {
				let label = invocation.label;
				let params = match Params::from_decoded_match(invocation.decoded_match) {
					Ok(params) => params,
					Err(error) => return invalid_server_result_outcome(&label, &error.to_string()),
				};
				// The input type's own decoding then reads the same bytes, so that it keeps
				// rejecting what it rejects, such as a repeated field.
				if let (ApiEndpointInputSource::Json(bytes), Some(objects)) =
					(&invocation.input, json_objects.as_ref())
					&& !serde_json::from_slice::<serde_json::Value>(bytes)
						.is_ok_and(|value| objects.admits(&value))
				{
					return input_failure_outcome(&label, InputError::Invalid);
				}
				let input = match Input::__decode(invocation.input).await {
					Ok(input) => input,
					Err(error) => return input_failure_outcome(&label, error),
				};
				let ctx = invocation.facts.to_api_endpoint_ctx(HandlerFacts {
					state: invocation.state,
					params,
					input,
					pattern,
					task_ctx: invocation.task_ctx,
				});
				from_api_endpoint_result(
					&label,
					&redirecting,
					handler(ctx).await,
					&*invocation.facts.target_resolver,
					invocation.facts.head_rules.as_deref(),
				)
			}
			.boxed()
		};
		Self {
			location: Location::caller(),
			kind,
			method,
			pattern,
			determine_input_kind_fn: Input::__input_kind,
			derive_search_shape_fn: derive_search_shape::<Input>,
			is_search_input_valid_fn: Input::__is_search_input_valid,
			max_request_body_size: None,
			types,
			collect_types_fn: collect_types::<Input, Output, DeclaredError>,
			invoke_handler_fn: Arc::new(invoke_handler),
		}
	}

	/// Sets the largest request body, in bytes, that Vorma reads for this
	/// endpoint, instead of [`App::max_request_body_size`](crate::App::max_request_body_size).
	/// The limit applies to an endpoint with JSON or form input, and a form
	/// body's limit includes its uploaded files. A larger body fails the
	/// request with status 413. A limit that the host server sets still
	/// applies, and the lower limit wins.
	pub fn set_max_request_body_size(mut self, bytes: usize) -> Self {
		self.max_request_body_size = Some(bytes);
		self
	}
}

/// Task middleware: request work that runs before the request's view loaders
/// or API endpoint.
///
/// The middleware that matches a request starts concurrently: no middleware
/// waits for an earlier one to finish. Only the commit of each middleware's
/// contributions (its status, headers, cookies, and document changes) follows
/// the order in [`App::task_middlewares`](crate::App::task_middlewares),
/// whatever order the middleware finishes in. A middleware that redirects or
/// fails ends the request, and the contributions of the middleware after it
/// do not apply.
///
/// The handler runs in its own Tokio task, so a `tokio::task_local!` value that
/// the host sets is not visible to it. Per-request values that the host
/// computes travel in the request's extensions, which the context's `request_parts`
/// holds.
pub struct TaskMiddleware<State> {
	/// Where the application declared the middleware.
	pub(crate) location: &'static Location<'static>,
	pub(crate) patterns: Vec<String>,
	pub(crate) methods: Vec<Method>,
	pub(crate) invoke_handler_fn: HandlerInvoker<Invocation<State, ()>>,
}

impl<State> Clone for TaskMiddleware<State> {
	fn clone(&self) -> Self {
		Self {
			location: self.location,
			patterns: self.patterns.clone(),
			methods: self.methods.clone(),
			invoke_handler_fn: Arc::clone(&self.invoke_handler_fn),
		}
	}
}

impl<State> fmt::Debug for TaskMiddleware<State> {
	fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
		formatter
			.debug_struct("TaskMiddleware")
			.field("patterns", &self.patterns)
			.field("methods", &self.methods)
			.finish()
	}
}

impl<State: Send + Sync + 'static> TaskMiddleware<State> {
	/// Declares middleware that runs for every request that Vorma handles
	/// unless [`Self::set_patterns`] or [`Self::set_methods`] restricts it.
	#[track_caller]
	pub fn new<HandlerFn, Fut>(handler: HandlerFn) -> Self
	where
		HandlerFn: Fn(TaskMiddlewareCtx<State>) -> Fut + Send + Sync + 'static,
		Fut: Future<Output = Result<Continue, TerminalOutcome<RequestRejected>>> + Send + 'static,
	{
		let handler = Arc::new(handler);
		let invoke_handler =
			move |invocation: Invocation<State, ()>| -> BoxFuture<'static, Outcome> {
				let Invocation {
					state,
					facts,
					task_ctx,
					label,
					..
				} = invocation;
				let handler = Arc::clone(&handler);
				// The handler runs inside the returned future, where execution
				// captures a panic, including one before its first await.
				async move {
					let ctx = facts.to_task_middleware_ctx(state, task_ctx);
					from_task_middleware_result(
						&label,
						handler(ctx).await,
						&*facts.target_resolver,
						facts.head_rules.as_deref(),
					)
				}
				.boxed()
			};
		Self {
			location: Location::caller(),
			patterns: Vec::new(),
			methods: Vec::new(),
			invoke_handler_fn: Arc::new(invoke_handler),
		}
	}

	/// Runs the middleware only for requests with a path that matches one of
	/// these flat patterns. The list replaces any earlier list, and an empty
	/// list imposes no restriction.
	///
	/// A pattern that ends with a splat matches the paths below its prefix and
	/// not the prefix itself: `/admin/*` matches `/admin/users` and does not
	/// match `/admin`. A guard for an area lists both patterns, such as
	/// `["/admin", "/admin/*"]`.
	pub fn set_patterns<Pattern: Into<String>>(
		mut self,
		patterns: impl IntoIterator<Item = Pattern>,
	) -> Self {
		self.patterns = patterns.into_iter().map(Into::into).collect();
		self
	}

	/// Runs the middleware only for requests with one of these methods, where
	/// a `HEAD` request counts as `GET`. The list replaces any earlier list,
	/// and an empty list imposes no restriction. Listing `HEAD`, or a method
	/// that Vorma does not handle, is a startup error.
	pub fn set_methods(mut self, methods: impl IntoIterator<Item = Method>) -> Self {
		self.methods = methods.into_iter().collect();
		self
	}
}