tide_disco/api.rs
1// Copyright (c) 2022 Espresso Systems (espressosys.com)
2// This file is part of the tide-disco library.
3
4// You should have received a copy of the MIT License
5// along with the tide-disco library. If not, see <https://mit-license.org/>.
6
7use crate::{
8 Html,
9 healthcheck::{HealthCheck, HealthStatus},
10 method::{Method, ReadState, WriteState},
11 metrics::Metrics,
12 middleware::{ErrorHandler, error_handler},
13 request::RequestParams,
14 route::{self, *},
15 socket,
16};
17use async_std::sync::Arc;
18use async_trait::async_trait;
19use derivative::Derivative;
20use futures::{
21 future::{BoxFuture, FutureExt},
22 stream::BoxStream,
23};
24use maud::{PreEscaped, html};
25use semver::Version;
26use serde::{Deserialize, Serialize, de::DeserializeOwned};
27use serde_with::{DisplayFromStr, serde_as};
28use snafu::{OptionExt, ResultExt, Snafu};
29use std::{
30 borrow::Cow,
31 collections::hash_map::{Entry, HashMap, IntoValues, Values},
32 convert::Infallible,
33 fmt::Display,
34 fs,
35 marker::PhantomData,
36 ops::Index,
37 path::{Path, PathBuf},
38};
39use tide::http::content::Accept;
40use vbs::version::StaticVersionType;
41
42/// An error encountered when parsing or constructing an [Api].
43#[derive(Clone, Debug, Snafu, PartialEq, Eq)]
44pub enum ApiError {
45 Route { source: RouteParseError },
46 ApiMustBeTable,
47 MissingRoutesTable,
48 RoutesMustBeTable,
49 UndefinedRoute,
50 HandlerAlreadyRegistered,
51 IncorrectMethod { expected: Method, actual: Method },
52 InvalidMetaTable { source: toml::de::Error },
53 MissingFormatVersion,
54 InvalidFormatVersion,
55 AmbiguousRoutes { route1: String, route2: String },
56 CannotReadToml { reason: String },
57}
58
59/// Version information about an API.
60#[serde_as]
61#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
62pub struct ApiVersion {
63 /// The version of this API.
64 #[serde_as(as = "Option<DisplayFromStr>")]
65 pub api_version: Option<Version>,
66
67 /// The format version of the TOML specification used to load this API.
68 #[serde_as(as = "DisplayFromStr")]
69 pub spec_version: Version,
70}
71
72/// Metadata used for describing and documenting an API.
73///
74/// [ApiMetadata] contains version information about the API, as well as optional HTML fragments to
75/// customize the formatting of automatically generated API documentation. Each of the supported
76/// HTML fragments is optional and will be filled in with a reasonable default if not provided. Some
77/// of the HTML fragments may contain "placeholders", which are identifiers enclosed in `{{ }}`,
78/// like `{{SOME_PLACEHOLDER}}`. These will be replaced by contextual information when the
79/// documentation is generated. The placeholders supported by each HTML fragment are documented
80/// below.
81#[serde_as]
82#[derive(Clone, Debug, Deserialize, Serialize)]
83#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
84pub struct ApiMetadata {
85 /// The name of this API.
86 ///
87 /// Note that the name of the API may be overridden if the API is registered with an app using
88 /// a different name.
89 #[serde(default = "meta_defaults::name")]
90 pub name: String,
91
92 /// A description of this API.
93 #[serde(default = "meta_defaults::description")]
94 pub description: String,
95
96 /// The version of the Tide Disco API specification format.
97 ///
98 /// If not specified, the version of this crate will be used.
99 #[serde_as(as = "DisplayFromStr")]
100 #[serde(default = "meta_defaults::format_version")]
101 pub format_version: Version,
102
103 /// HTML to be prepended to automatically generated documentation.
104 ///
105 /// # Placeholders
106 ///
107 /// * `NAME`: the name of the API
108 /// * `DESCRIPTION`: the description provided in `Cargo.toml`
109 /// * `VERSION`: the version of the API
110 /// * `FORMAT_VERSION`: the `FORMAT_VERSION` of the API
111 /// * `PUBLIC`: the URL where the public directory for this API is being served
112 #[serde(default = "meta_defaults::html_top")]
113 pub html_top: String,
114
115 /// HTML to be appended to automatically generated documentation.
116 #[serde(default = "meta_defaults::html_bottom")]
117 pub html_bottom: String,
118
119 /// The heading for documentation of a route.
120 ///
121 /// # Placeholders
122 ///
123 /// * `METHOD`: the method of the route
124 /// * `NAME`: the name of the route
125 #[serde(default = "meta_defaults::heading_entry")]
126 pub heading_entry: String,
127
128 /// The heading preceding documentation of all routes in this API.
129 #[serde(default = "meta_defaults::heading_routes")]
130 pub heading_routes: String,
131
132 /// The heading preceding documentation of route parameters.
133 #[serde(default = "meta_defaults::heading_parameters")]
134 pub heading_parameters: String,
135
136 /// The heading preceding documentation of a route description.
137 #[serde(default = "meta_defaults::heading_description")]
138 pub heading_description: String,
139
140 /// HTML formatting the path of a route.
141 ///
142 /// # Placeholders
143 ///
144 /// * `PATH`: the path being formatted
145 #[serde(default = "meta_defaults::route_path")]
146 pub route_path: String,
147
148 /// HTML preceding the contents of a table documenting the parameters of a route.
149 #[serde(default = "meta_defaults::parameter_table_open")]
150 pub parameter_table_open: String,
151
152 /// HTML closing a table documenting the parameters of a route.
153 #[serde(default = "meta_defaults::parameter_table_close")]
154 pub parameter_table_close: String,
155
156 /// HTML formatting an entry in a table documenting the parameters of a route.
157 ///
158 /// # Placeholders
159 ///
160 /// * `NAME`: the parameter being documented
161 /// * `TYPE`: the type of the parameter being documented
162 #[serde(default = "meta_defaults::parameter_row")]
163 pub parameter_row: String,
164
165 /// Documentation to insert in the parameters section of a route with no parameters.
166 #[serde(default = "meta_defaults::parameter_none")]
167 pub parameter_none: String,
168}
169
170impl Default for ApiMetadata {
171 fn default() -> Self {
172 // Deserialize an empty table, using the `serde` defaults for every field.
173 toml::Value::Table(Default::default()).try_into().unwrap()
174 }
175}
176
177mod meta_defaults {
178 use super::Version;
179
180 pub fn name() -> String {
181 "default-tide-disco-api".to_string()
182 }
183
184 pub fn description() -> String {
185 "Default Tide Disco API".to_string()
186 }
187
188 pub fn format_version() -> Version {
189 "0.1.0".parse().unwrap()
190 }
191
192 pub fn html_top() -> String {
193 "
194 <!DOCTYPE html>
195 <html lang='en'>
196 <head>
197 <meta charset='utf-8'>
198 <title>{{NAME}} Reference</title>
199 <link rel='stylesheet' href='{{PUBLIC}}/css/style.css'>
200 <script src='{{PUBLIC}}/js/script.js'></script>
201 <link rel='icon' type='image/svg+xml'
202 href='/public/favicon.svg'>
203 </head>
204 <body>
205 <div><a href='/'><img src='{{PUBLIC}}/espressosys_logo.svg'
206 alt='Espresso Systems Logo'
207 /></a></div>
208 <h1>{{NAME}} API {{VERSION}} Reference</h1>
209 <p>{{SHORT_DESCRIPTION}}</p><br/>
210 {{LONG_DESCRIPTION}}
211 "
212 .to_string()
213 }
214
215 pub fn html_bottom() -> String {
216 "
217 <h1> </h1>
218 <p>Copyright © 2022 Espresso Systems. All rights reserved.</p>
219 </body>
220 </html>
221 "
222 .to_string()
223 }
224
225 pub fn heading_entry() -> String {
226 "<a name='{{NAME}}'><h3 class='entry'><span class='meth'>{{METHOD}}</span> {{NAME}}</h3></a>\n".to_string()
227 }
228
229 pub fn heading_routes() -> String {
230 "<h3>Routes</h3>\n".to_string()
231 }
232 pub fn heading_parameters() -> String {
233 "<h3>Parameters</h3>\n".to_string()
234 }
235 pub fn heading_description() -> String {
236 "<h3>Description</h3>\n".to_string()
237 }
238
239 pub fn route_path() -> String {
240 "<p class='path'>{{PATH}}</p>\n".to_string()
241 }
242
243 pub fn parameter_table_open() -> String {
244 "<table>\n".to_string()
245 }
246 pub fn parameter_table_close() -> String {
247 "</table>\n\n".to_string()
248 }
249 pub fn parameter_row() -> String {
250 "<tr><td class='parameter'>{{NAME}}</td><td class='type'>{{TYPE}}</td></tr>\n".to_string()
251 }
252 pub fn parameter_none() -> String {
253 "<div class='meta'>None</div>".to_string()
254 }
255}
256
257/// A description of an API.
258///
259/// An [Api] is a structured representation of an `api.toml` specification. It contains API-level
260/// metadata and descriptions of all of the routes in the specification. It can be parsed from a
261/// TOML file and registered as a module of an [App](crate::App).
262#[derive(Derivative)]
263#[derivative(Debug(bound = ""))]
264pub struct Api<State, Error, VER> {
265 inner: ApiInner<State, Error>,
266 _version: PhantomData<VER>,
267}
268
269/// A version-erased description of an API.
270///
271/// This type contains all the details of the API, with the version of the binary serialization
272/// format type-erased and encapsulated into the route handlers. This type is used internally by
273/// [`App`], to allow dynamic registration of different versions of APIs with different versions of
274/// the binary format.
275///
276/// It is exposed publicly and manipulated _only_ via [`Api`], which wraps this type with a static
277/// format version type parameter, which provides compile-time enforcement of format version
278/// consistency as the API is being constructed, until it is registered with an [`App`] and
279/// type-erased.
280#[derive(Derivative)]
281#[derivative(Debug(bound = ""))]
282pub(crate) struct ApiInner<State, Error> {
283 meta: Arc<ApiMetadata>,
284 name: String,
285 routes: HashMap<String, Route<State, Error>>,
286 routes_by_path: HashMap<String, Vec<String>>,
287 #[derivative(Debug = "ignore")]
288 health_check: HealthCheckHandler<State>,
289 api_version: Option<Version>,
290 /// Error handler encapsulating the serialization format version for errors.
291 ///
292 /// This field is optional so it can be bound late, potentially after a `map_err` changes the
293 /// error type. However, it will always be set after `Api::into_inner` is called.
294 #[derivative(Debug = "ignore")]
295 error_handler: Option<Arc<dyn ErrorHandler<Error>>>,
296 /// Response handler encapsulating the serialization format version for version requests
297 #[derivative(Debug = "ignore")]
298 version_handler: Arc<dyn VersionHandler>,
299 public: Option<PathBuf>,
300 short_description: String,
301 long_description: String,
302}
303
304pub(crate) trait VersionHandler:
305 Send + Sync + Fn(&Accept, ApiVersion) -> Result<tide::Response, RouteError<Infallible>>
306{
307}
308impl<F> VersionHandler for F where
309 F: Send + Sync + Fn(&Accept, ApiVersion) -> Result<tide::Response, RouteError<Infallible>>
310{
311}
312
313impl<'a, State, Error> IntoIterator for &'a ApiInner<State, Error> {
314 type Item = &'a Route<State, Error>;
315 type IntoIter = Values<'a, String, Route<State, Error>>;
316
317 fn into_iter(self) -> Self::IntoIter {
318 self.routes.values()
319 }
320}
321
322impl<State, Error> IntoIterator for ApiInner<State, Error> {
323 type Item = Route<State, Error>;
324 type IntoIter = IntoValues<String, Route<State, Error>>;
325
326 fn into_iter(self) -> Self::IntoIter {
327 self.routes.into_values()
328 }
329}
330
331impl<State, Error> Index<&str> for ApiInner<State, Error> {
332 type Output = Route<State, Error>;
333
334 fn index(&self, index: &str) -> &Route<State, Error> {
335 &self.routes[index]
336 }
337}
338
339/// Iterator for [routes_by_path](ApiInner::routes_by_path).
340///
341/// This type iterates over all of the routes that have a given path.
342/// [routes_by_path](ApiInner::routes_by_path), in turn, returns an iterator over paths whose items
343/// contain a [RoutesWithPath] iterator.
344pub(crate) struct RoutesWithPath<'a, State, Error> {
345 routes: std::slice::Iter<'a, String>,
346 api: &'a ApiInner<State, Error>,
347}
348
349impl<'a, State, Error> Iterator for RoutesWithPath<'a, State, Error> {
350 type Item = &'a Route<State, Error>;
351
352 fn next(&mut self) -> Option<Self::Item> {
353 Some(&self.api.routes[self.routes.next()?])
354 }
355}
356
357impl<State, Error> ApiInner<State, Error> {
358 /// Iterate over groups of routes with the same path.
359 pub(crate) fn routes_by_path(
360 &self,
361 ) -> impl Iterator<Item = (&str, RoutesWithPath<'_, State, Error>)> {
362 self.routes_by_path.iter().map(|(path, routes)| {
363 (
364 path.as_str(),
365 RoutesWithPath {
366 routes: routes.iter(),
367 api: self,
368 },
369 )
370 })
371 }
372
373 /// Check the health status of a server with the given state.
374 pub(crate) async fn health(&self, req: RequestParams, state: &State) -> tide::Response {
375 (self.health_check)(req, state).await
376 }
377
378 /// Get the version of this API.
379 pub(crate) fn version(&self) -> ApiVersion {
380 ApiVersion {
381 api_version: self.api_version.clone(),
382 spec_version: self.meta.format_version.clone(),
383 }
384 }
385
386 pub(crate) fn public(&self) -> Option<&PathBuf> {
387 self.public.as_ref()
388 }
389
390 pub(crate) fn set_name(&mut self, name: String) {
391 self.name = name;
392 }
393
394 /// Compose an HTML page documenting all the routes in this API.
395 pub(crate) fn documentation(&self) -> Html {
396 html! {
397 (PreEscaped(self.meta.html_top
398 .replace("{{NAME}}", &self.name)
399 .replace("{{SHORT_DESCRIPTION}}", &self.short_description)
400 .replace("{{LONG_DESCRIPTION}}", &self.long_description)
401 .replace("{{VERSION}}", &match &self.api_version {
402 Some(version) => version.to_string(),
403 None => "(no version)".to_string(),
404 })
405 .replace("{{FORMAT_VERSION}}", &self.meta.format_version.to_string())
406 .replace("{{PUBLIC}}", &format!("/public/{}", self.name))))
407 @for route in self.routes.values() {
408 (route.documentation())
409 }
410 (PreEscaped(&self.meta.html_bottom))
411 }
412 }
413
414 /// The short description of this API from the specification.
415 pub(crate) fn short_description(&self) -> &str {
416 &self.short_description
417 }
418
419 pub(crate) fn error_handler(&self) -> Arc<dyn ErrorHandler<Error>> {
420 self.error_handler.clone().unwrap()
421 }
422
423 pub(crate) fn version_handler(&self) -> Arc<dyn VersionHandler> {
424 self.version_handler.clone()
425 }
426}
427
428impl<State, Error, VER> Api<State, Error, VER>
429where
430 State: 'static,
431 Error: 'static,
432 VER: StaticVersionType + 'static,
433{
434 /// Parse an API from a TOML specification.
435 pub fn new(api: impl Into<toml::Value>) -> Result<Self, ApiError> {
436 let mut api = api.into();
437 let meta = match api
438 .as_table_mut()
439 .context(ApiMustBeTableSnafu)?
440 .remove("meta")
441 {
442 Some(meta) => toml::Value::try_into(meta)
443 .map_err(|source| ApiError::InvalidMetaTable { source })?,
444 None => ApiMetadata::default(),
445 };
446 let meta = Arc::new(meta);
447 let routes = match api.get("route") {
448 Some(routes) => routes.as_table().context(RoutesMustBeTableSnafu)?,
449 None => return Err(ApiError::MissingRoutesTable),
450 };
451 // Collect routes into a [HashMap] indexed by route name.
452 let routes = routes
453 .into_iter()
454 .map(|(name, spec)| {
455 let route = Route::new(name.clone(), spec, meta.clone()).context(RouteSnafu)?;
456 Ok((route.name(), route))
457 })
458 .collect::<Result<HashMap<_, _>, _>>()?;
459 // Collect routes into groups of route names indexed by route pattern.
460 let mut routes_by_path = HashMap::new();
461 for route in routes.values() {
462 for path in route.patterns() {
463 match routes_by_path.entry(path.clone()) {
464 Entry::Vacant(e) => e.insert(Vec::new()).push(route.name().clone()),
465 Entry::Occupied(mut e) => {
466 // If there is already a route with this path and method, then dispatch is
467 // ambiguous.
468 if let Some(ambiguous_name) = e
469 .get()
470 .iter()
471 .find(|name| routes[*name].method() == route.method())
472 {
473 return Err(ApiError::AmbiguousRoutes {
474 route1: route.name(),
475 route2: ambiguous_name.clone(),
476 });
477 }
478 e.get_mut().push(route.name());
479 }
480 }
481 }
482 }
483
484 // Parse description: the first line is a short description, to display when briefly
485 // describing this API in a list. The rest is the long description, to display on this API's
486 // own documentation page. Both are rendered to HTML via Markdown.
487 let blocks = markdown::tokenize(&meta.description);
488 let (short_description, long_description) = match blocks.split_first() {
489 Some((short, long)) => {
490 let render = |blocks| markdown::to_html(&markdown::generate_markdown(blocks));
491
492 let short = render(vec![short.clone()]);
493 let long = render(long.to_vec());
494
495 // The short description is only one block, and sometimes we would like to display
496 // it inline (as a `span`). Markdown automatically wraps blocks in `<p>`. We will
497 // strip this outer tag so that we can wrap it in either `<p>` or `<span>`,
498 // depending on the context.
499 let short = short.strip_prefix("<p>").unwrap_or(&short);
500 let short = short.strip_suffix("</p>").unwrap_or(short);
501 let short = short.to_string();
502
503 (short, long)
504 }
505 None => Default::default(),
506 };
507
508 Ok(Self {
509 inner: ApiInner {
510 name: meta.name.clone(),
511 meta,
512 routes,
513 routes_by_path,
514 health_check: Box::new(Self::default_health_check),
515 api_version: None,
516 error_handler: None,
517 version_handler: Arc::new(|accept, version| {
518 respond_with(accept, version, VER::instance())
519 }),
520 public: None,
521 short_description,
522 long_description,
523 },
524 _version: Default::default(),
525 })
526 }
527
528 /// Create an [Api] by reading a TOML specification from a file.
529 pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self, ApiError> {
530 let bytes = fs::read(path).map_err(|err| ApiError::CannotReadToml {
531 reason: err.to_string(),
532 })?;
533 let string = std::str::from_utf8(&bytes).map_err(|err| ApiError::CannotReadToml {
534 reason: err.to_string(),
535 })?;
536 Self::new(toml::from_str::<toml::Value>(string).map_err(|err| {
537 ApiError::CannotReadToml {
538 reason: err.to_string(),
539 }
540 })?)
541 }
542
543 /// Set the API version.
544 ///
545 /// The version information will automatically be included in responses to `GET /version`. This
546 /// version can also be used to serve multiple major versions of the same API simultaneously,
547 /// under a version prefix. For more information, see
548 /// [App::register_module](crate::App::register_module).
549 ///
550 /// This is the version of the application or sub-application which this instance of [Api]
551 /// represents. The versioning corresponds to the API specification passed to [new](Api::new),
552 /// and may be different from the version of the Rust crate implementing the route handlers for
553 /// the API.
554 pub fn with_version(&mut self, version: Version) -> &mut Self {
555 self.inner.api_version = Some(version);
556 self
557 }
558
559 /// Serve the contents of `dir` at the URL `/public/{{NAME}}`.
560 pub fn with_public(&mut self, dir: PathBuf) -> &mut Self {
561 self.inner.public = Some(dir);
562 self
563 }
564
565 /// Register a handler for a route.
566 ///
567 /// When the server receives a request whose URL matches the pattern of the route `name`,
568 /// `handler` will be invoked with the parameters of the request and a reference to the current
569 /// state, and the result will be serialized into a response.
570 ///
571 /// # Examples
572 ///
573 /// A simple getter route for a state object.
574 ///
575 /// `api.toml`
576 ///
577 /// ```toml
578 /// [route.getstate]
579 /// PATH = ["/getstate"]
580 /// DOC = "Gets the current state."
581 /// ```
582 ///
583 /// ```
584 /// use futures::FutureExt;
585 /// # use tide_disco::Api;
586 /// # use vbs::version::StaticVersion;
587 ///
588 /// type State = u64;
589 /// type StaticVer01 = StaticVersion<0, 1>;
590 ///
591 /// # fn ex(api: &mut Api<State, (), StaticVer01>) {
592 /// api.at("getstate", |req, state| async { Ok(*state) }.boxed());
593 /// # }
594 /// ```
595 ///
596 /// A counter endpoint which increments a mutable state. Notice how we use `METHOD = "POST"` to
597 /// ensure that the HTTP method for this route is compatible with mutable access.
598 ///
599 /// `api.toml`
600 ///
601 /// ```toml
602 /// [route.increment]
603 /// PATH = ["/increment"]
604 /// METHOD = "POST"
605 /// DOC = "Increment the current state and return the new value."
606 /// ```
607 ///
608 /// ```
609 /// use async_std::sync::Mutex;
610 /// use futures::FutureExt;
611 /// # use tide_disco::Api;
612 /// # use vbs::version::StaticVersion;
613 ///
614 /// type State = Mutex<u64>;
615 /// type StaticVer01 = StaticVersion<0, 1>;
616 ///
617 /// # fn ex(api: &mut Api<State, (), StaticVer01>) {
618 /// api.at("increment", |req, state| async {
619 /// let mut guard = state.lock().await;
620 /// *guard += 1;
621 /// Ok(*guard)
622 /// }.boxed());
623 /// # }
624 /// ```
625 ///
626 /// # Warnings
627 /// The route will use the HTTP method specified in the TOML specification for the named route
628 /// (or GET if the method is not specified). Some HTTP methods imply constraints on mutability.
629 /// For example, GET routes must be "pure", and not mutate any server state. Violating this
630 /// constraint may lead to confusing and unpredictable behavior. If the `State` type has
631 /// interior mutability (for instance, [RwLock](async_std::sync::RwLock)) it is up to the
632 /// `handler` not to use the interior mutability if the HTTP method suggests it shouldn't.
633 ///
634 /// If you know the HTTP method when you are registering the route, we recommend you use the
635 /// safer versions of this function, which enforce the appropriate mutability constraints. For
636 /// example,
637 /// * [get](Self::get)
638 /// * [post](Self::post)
639 /// * [put](Self::put)
640 /// * [delete](Self::delete)
641 ///
642 /// # Errors
643 ///
644 /// If the route `name` does not exist in the API specification, or if the route already has a
645 /// handler registered, an error is returned. Note that all routes are initialized with a
646 /// default handler that echoes parameters and shows documentation, but this default handler can
647 /// replaced by this function without raising [ApiError::HandlerAlreadyRegistered].
648 ///
649 /// If the route `name` exists, but it is not an HTTP route (for example, `METHOD = "SOCKET"`
650 /// was used when defining the route in the API specification), [ApiError::IncorrectMethod] is
651 /// returned.
652 ///
653 /// # Limitations
654 ///
655 /// [Like many function parameters](crate#boxed-futures) in [tide_disco](crate), the
656 /// handler function is required to return a [BoxFuture].
657 pub fn at<F, T>(&mut self, name: &str, handler: F) -> Result<&mut Self, ApiError>
658 where
659 F: 'static + Send + Sync + Fn(RequestParams, &State) -> BoxFuture<'_, Result<T, Error>>,
660 T: Serialize,
661 State: 'static + Send + Sync,
662 VER: 'static + Send + Sync,
663 {
664 let route = self
665 .inner
666 .routes
667 .get_mut(name)
668 .ok_or(ApiError::UndefinedRoute)?;
669 if route.has_handler() {
670 return Err(ApiError::HandlerAlreadyRegistered);
671 }
672
673 if !route.method().is_http() {
674 return Err(ApiError::IncorrectMethod {
675 // Just pick any HTTP method as the expected method.
676 expected: Method::get(),
677 actual: route.method(),
678 });
679 }
680
681 // `set_fn_handler` only fails if the route is not an HTTP route; since we have already
682 // checked that it is, this cannot fail.
683 route
684 .set_fn_handler(handler, VER::instance())
685 .unwrap_or_else(|_| panic!("unexpected failure in set_fn_handler"));
686
687 Ok(self)
688 }
689
690 fn method_immutable<F, T>(
691 &mut self,
692 method: Method,
693 name: &str,
694 handler: F,
695 ) -> Result<&mut Self, ApiError>
696 where
697 F: 'static
698 + Send
699 + Sync
700 + Fn(RequestParams, &<State as ReadState>::State) -> BoxFuture<'_, Result<T, Error>>,
701 T: Serialize,
702 State: 'static + Send + Sync + ReadState,
703 VER: 'static + Send + Sync + StaticVersionType,
704 {
705 assert!(method.is_http() && !method.is_mutable());
706 let route = self
707 .inner
708 .routes
709 .get_mut(name)
710 .ok_or(ApiError::UndefinedRoute)?;
711 if route.method() != method {
712 return Err(ApiError::IncorrectMethod {
713 expected: method,
714 actual: route.method(),
715 });
716 }
717 if route.has_handler() {
718 return Err(ApiError::HandlerAlreadyRegistered);
719 }
720 // `set_handler` only fails if the route is not an HTTP route; since we have already checked
721 // that it is, this cannot fail.
722 route
723 .set_handler(ReadHandler::<_, VER>::from(handler))
724 .unwrap_or_else(|_| panic!("unexpected failure in set_handler"));
725 Ok(self)
726 }
727
728 /// Register a handler for a GET route.
729 ///
730 /// When the server receives a GET request whose URL matches the pattern of the route `name`,
731 /// `handler` will be invoked with the parameters of the request and immutable access to the
732 /// current state, and the result will be serialized into a response.
733 ///
734 /// The [ReadState] trait is used to acquire immutable access to the state, so the state
735 /// reference passed to `handler` is actually [`<State as ReadState>::State`](ReadState::State).
736 /// For example, if `State` is `RwLock<T>`, the lock will automatically be acquired for reading,
737 /// and the handler will be passed a `&T`.
738 ///
739 /// # Examples
740 ///
741 /// A simple getter route for a state object.
742 ///
743 /// `api.toml`
744 ///
745 /// ```toml
746 /// [route.getstate]
747 /// PATH = ["/getstate"]
748 /// DOC = "Gets the current state."
749 /// ```
750 ///
751 /// ```
752 /// use async_std::sync::RwLock;
753 /// use futures::FutureExt;
754 /// # use tide_disco::Api;
755 /// # use vbs::{Serializer, version::StaticVersion};
756 ///
757 /// type State = RwLock<u64>;
758 /// type StaticVer01 = StaticVersion<0, 1>;
759 ///
760 /// # fn ex(api: &mut Api<State, (), StaticVer01>) {
761 /// api.get("getstate", |req, state| async { Ok(*state) }.boxed());
762 /// # }
763 /// ```
764 ///
765 /// # Errors
766 ///
767 /// If the route `name` does not exist in the API specification, or if the route already has a
768 /// handler registered, an error is returned. Note that all routes are initialized with a
769 /// default handler that echoes parameters and shows documentation, but this default handler can
770 /// replaced by this function without raising [ApiError::HandlerAlreadyRegistered].
771 ///
772 /// If the route `name` exists, but the method is not GET (that is, `METHOD = "M"` was used in
773 /// the route definition in `api.toml`, with `M` other than `GET`) the error
774 /// [IncorrectMethod](ApiError::IncorrectMethod) is returned.
775 ///
776 /// # Limitations
777 ///
778 /// [Like many function parameters](crate#boxed-futures) in [tide_disco](crate), the
779 /// handler function is required to return a [BoxFuture].
780 pub fn get<F, T>(&mut self, name: &str, handler: F) -> Result<&mut Self, ApiError>
781 where
782 F: 'static
783 + Send
784 + Sync
785 + Fn(RequestParams, &<State as ReadState>::State) -> BoxFuture<'_, Result<T, Error>>,
786 T: Serialize,
787 State: 'static + Send + Sync + ReadState,
788 VER: 'static + Send + Sync,
789 {
790 self.method_immutable(Method::get(), name, handler)
791 }
792
793 fn method_mutable<F, T>(
794 &mut self,
795 method: Method,
796 name: &str,
797 handler: F,
798 ) -> Result<&mut Self, ApiError>
799 where
800 F: 'static
801 + Send
802 + Sync
803 + Fn(RequestParams, &mut <State as ReadState>::State) -> BoxFuture<'_, Result<T, Error>>,
804 T: Serialize,
805 State: 'static + Send + Sync + WriteState,
806 VER: 'static + Send + Sync,
807 {
808 assert!(method.is_http() && method.is_mutable());
809 let route = self
810 .inner
811 .routes
812 .get_mut(name)
813 .ok_or(ApiError::UndefinedRoute)?;
814 if route.method() != method {
815 return Err(ApiError::IncorrectMethod {
816 expected: method,
817 actual: route.method(),
818 });
819 }
820 if route.has_handler() {
821 return Err(ApiError::HandlerAlreadyRegistered);
822 }
823
824 // `set_handler` only fails if the route is not an HTTP route; since we have already checked
825 // that it is, this cannot fail.
826 route
827 .set_handler(WriteHandler::<_, VER>::from(handler))
828 .unwrap_or_else(|_| panic!("unexpected failure in set_handler"));
829 Ok(self)
830 }
831
832 /// Register a handler for a POST route.
833 ///
834 /// When the server receives a POST request whose URL matches the pattern of the route `name`,
835 /// `handler` will be invoked with the parameters of the request and exclusive, mutable access
836 /// to the current state, and the result will be serialized into a response.
837 ///
838 /// The [WriteState] trait is used to acquire mutable access to the state, so the state
839 /// reference passed to `handler` is actually [`<State as ReadState>::State`](ReadState::State).
840 /// For example, if `State` is `RwLock<T>`, the lock will automatically be acquired for writing,
841 /// and the handler will be passed a `&mut T`.
842 ///
843 /// # Examples
844 ///
845 /// A counter endpoint which increments the state and returns the new state.
846 ///
847 /// `api.toml`
848 ///
849 /// ```toml
850 /// [route.increment]
851 /// PATH = ["/increment"]
852 /// METHOD = "POST"
853 /// DOC = "Increment the current state and return the new value."
854 /// ```
855 ///
856 /// ```
857 /// use async_std::sync::RwLock;
858 /// use futures::FutureExt;
859 /// # use tide_disco::Api;
860 /// # use vbs::version::StaticVersion;
861 ///
862 /// type State = RwLock<u64>;
863 /// type StaticVer01 = StaticVersion<0, 1>;
864 ///
865 /// # fn ex(api: &mut Api<State, (), StaticVer01>) {
866 /// api.post("increment", |req, state| async {
867 /// *state += 1;
868 /// Ok(*state)
869 /// }.boxed());
870 /// # }
871 /// ```
872 ///
873 /// # Errors
874 ///
875 /// If the route `name` does not exist in the API specification, or if the route already has a
876 /// handler registered, an error is returned. Note that all routes are initialized with a
877 /// default handler that echoes parameters and shows documentation, but this default handler can
878 /// replaced by this function without raising [ApiError::HandlerAlreadyRegistered].
879 ///
880 /// If the route `name` exists, but the method is not POST (that is, `METHOD = "M"` was used in
881 /// the route definition in `api.toml`, with `M` other than `POST`) the error
882 /// [IncorrectMethod](ApiError::IncorrectMethod) is returned.
883 ///
884 /// # Limitations
885 ///
886 /// [Like many function parameters](crate#boxed-futures) in [tide_disco](crate), the
887 /// handler function is required to return a [BoxFuture].
888 pub fn post<F, T>(&mut self, name: &str, handler: F) -> Result<&mut Self, ApiError>
889 where
890 F: 'static
891 + Send
892 + Sync
893 + Fn(RequestParams, &mut <State as ReadState>::State) -> BoxFuture<'_, Result<T, Error>>,
894 T: Serialize,
895 State: 'static + Send + Sync + WriteState,
896 VER: 'static + Send + Sync,
897 {
898 self.method_mutable(Method::post(), name, handler)
899 }
900
901 /// Register a handler for a PUT route.
902 ///
903 /// When the server receives a PUT request whose URL matches the pattern of the route `name`,
904 /// `handler` will be invoked with the parameters of the request and exclusive, mutable access
905 /// to the current state, and the result will be serialized into a response.
906 ///
907 /// The [WriteState] trait is used to acquire mutable access to the state, so the state
908 /// reference passed to `handler` is actually [`<State as ReadState>::State`](ReadState::State).
909 /// For example, if `State` is `RwLock<T>`, the lock will automatically be acquired for writing,
910 /// and the handler will be passed a `&mut T`.
911 ///
912 /// # Examples
913 ///
914 /// An endpoint which replaces the current state with a new value.
915 ///
916 /// `api.toml`
917 ///
918 /// ```toml
919 /// [route.replace]
920 /// PATH = ["/replace/:new_state"]
921 /// METHOD = "PUT"
922 /// ":new_state" = "Integer"
923 /// DOC = "Set the state to `:new_state`."
924 /// ```
925 ///
926 /// ```
927 /// use async_std::sync::RwLock;
928 /// use futures::FutureExt;
929 /// # use tide_disco::Api;
930 /// # use vbs::version::StaticVersion;
931 ///
932 /// type State = RwLock<u64>;
933 /// type StaticVer01 = StaticVersion<0, 1>;
934 ///
935 /// # fn ex(api: &mut Api<State, tide_disco::RequestError, StaticVer01>) {
936 /// api.post("replace", |req, state| async move {
937 /// *state = req.integer_param("new_state")?;
938 /// Ok(())
939 /// }.boxed());
940 /// # }
941 /// ```
942 ///
943 /// # Errors
944 ///
945 /// If the route `name` does not exist in the API specification, or if the route already has a
946 /// handler registered, an error is returned. Note that all routes are initialized with a
947 /// default handler that echoes parameters and shows documentation, but this default handler can
948 /// replaced by this function without raising [ApiError::HandlerAlreadyRegistered].
949 ///
950 /// If the route `name` exists, but the method is not PUT (that is, `METHOD = "M"` was used in
951 /// the route definition in `api.toml`, with `M` other than `PUT`) the error
952 /// [IncorrectMethod](ApiError::IncorrectMethod) is returned.
953 ///
954 /// # Limitations
955 ///
956 /// [Like many function parameters](crate#boxed-futures) in [tide_disco](crate), the
957 /// handler function is required to return a [BoxFuture].
958 pub fn put<F, T>(&mut self, name: &str, handler: F) -> Result<&mut Self, ApiError>
959 where
960 F: 'static
961 + Send
962 + Sync
963 + Fn(RequestParams, &mut <State as ReadState>::State) -> BoxFuture<'_, Result<T, Error>>,
964 T: Serialize,
965 State: 'static + Send + Sync + WriteState,
966 VER: 'static + Send + Sync,
967 {
968 self.method_mutable(Method::put(), name, handler)
969 }
970
971 /// Register a handler for a DELETE route.
972 ///
973 /// When the server receives a DELETE request whose URL matches the pattern of the route `name`,
974 /// `handler` will be invoked with the parameters of the request and exclusive, mutable access
975 /// to the current state, and the result will be serialized into a response.
976 ///
977 /// The [WriteState] trait is used to acquire mutable access to the state, so the state
978 /// reference passed to `handler` is actually [`<State as ReadState>::State`](ReadState::State).
979 /// For example, if `State` is `RwLock<T>`, the lock will automatically be acquired for writing,
980 /// and the handler will be passed a `&mut T`.
981 ///
982 /// # Examples
983 ///
984 /// An endpoint which clears the current state.
985 ///
986 /// `api.toml`
987 ///
988 /// ```toml
989 /// [route.state]
990 /// PATH = ["/state"]
991 /// METHOD = "DELETE"
992 /// DOC = "Clear the state."
993 /// ```
994 ///
995 /// ```
996 /// use async_std::sync::RwLock;
997 /// use futures::FutureExt;
998 /// # use tide_disco::Api;
999 /// # use vbs::version::StaticVersion;
1000 ///
1001 /// type State = RwLock<Option<u64>>;
1002 /// type StaticVer01 = StaticVersion<0, 1>;
1003 ///
1004 /// # fn ex(api: &mut Api<State, (), StaticVer01>) {
1005 /// api.delete("state", |req, state| async {
1006 /// *state = None;
1007 /// Ok(())
1008 /// }.boxed());
1009 /// # }
1010 /// ```
1011 ///
1012 /// # Errors
1013 ///
1014 /// If the route `name` does not exist in the API specification, or if the route already has a
1015 /// handler registered, an error is returned. Note that all routes are initialized with a
1016 /// default handler that echoes parameters and shows documentation, but this default handler can
1017 /// replaced by this function without raising [ApiError::HandlerAlreadyRegistered].
1018 ///
1019 /// If the route `name` exists, but the method is not DELETE (that is, `METHOD = "M"` was used
1020 /// in the route definition in `api.toml`, with `M` other than `DELETE`) the error
1021 /// [IncorrectMethod](ApiError::IncorrectMethod) is returned.
1022 ///
1023 /// # Limitations
1024 ///
1025 /// [Like many function parameters](crate#boxed-futures) in [tide_disco](crate), the
1026 /// handler function is required to return a [BoxFuture].
1027 pub fn delete<F, T>(&mut self, name: &str, handler: F) -> Result<&mut Self, ApiError>
1028 where
1029 F: 'static
1030 + Send
1031 + Sync
1032 + Fn(RequestParams, &mut <State as ReadState>::State) -> BoxFuture<'_, Result<T, Error>>,
1033 T: Serialize,
1034 State: 'static + Send + Sync + WriteState,
1035 VER: 'static + Send + Sync,
1036 {
1037 self.method_mutable(Method::delete(), name, handler)
1038 }
1039
1040 /// Register a handler for a SOCKET route.
1041 ///
1042 /// When the server receives any request whose URL matches the pattern for this route and which
1043 /// includes the WebSockets upgrade headers, the server will negotiate a protocol upgrade with
1044 /// the client, establishing a WebSockets connection, and then invoke `handler`. `handler` will
1045 /// be given the parameters of the request which initiated the connection and a reference to the
1046 /// application state, as well as a [Connection](socket::Connection) object which it can then
1047 /// use for asynchronous, bi-directional communication with the client.
1048 ///
1049 /// The server side of the connection will remain open as long as the future returned by
1050 /// `handler` is remains unresolved. The handler can terminate the connection by returning. If
1051 /// it returns an error, the error message will be included in the
1052 /// [CloseFrame](tide_websockets::tungstenite::protocol::CloseFrame) sent to the client when
1053 /// tearing down the connection.
1054 ///
1055 /// # Examples
1056 ///
1057 /// A socket endpoint which receives amounts from the client and returns a running sum.
1058 ///
1059 /// `api.toml`
1060 ///
1061 /// ```toml
1062 /// [route.sum]
1063 /// PATH = ["/sum"]
1064 /// METHOD = "SOCKET"
1065 /// DOC = "Stream a running sum."
1066 /// ```
1067 ///
1068 /// ```
1069 /// use futures::{FutureExt, SinkExt, StreamExt};
1070 /// use tide_disco::{error::ServerError, socket::Connection, Api};
1071 /// # use vbs::version::StaticVersion;
1072 ///
1073 /// # fn ex(api: &mut Api<(), ServerError, StaticVersion<0, 1>>) {
1074 /// api.socket("sum", |_req, mut conn: Connection<i32, i32, ServerError, StaticVersion<0, 1>>, _state| async move {
1075 /// let mut sum = 0;
1076 /// while let Some(amount) = conn.next().await {
1077 /// sum += amount?;
1078 /// conn.send(&sum).await?;
1079 /// }
1080 /// Ok(())
1081 /// }.boxed());
1082 /// # }
1083 /// ```
1084 ///
1085 /// In some cases, it may be desirable to handle messages to and from the client in separate
1086 /// tasks. There are two ways of doing this:
1087 ///
1088 /// ## Split the connection into separate stream and sink
1089 ///
1090 /// ```
1091 /// use async_std::task::spawn;
1092 /// use futures::{future::{join, FutureExt}, sink::SinkExt, stream::StreamExt};
1093 /// use tide_disco::{error::ServerError, socket::Connection, Api};
1094 /// # use vbs::version::StaticVersion;
1095 ///
1096 /// # fn ex(api: &mut Api<(), ServerError, StaticVersion<0, 1>>) {
1097 /// api.socket("endpoint", |_req, mut conn: Connection<i32, i32, ServerError, StaticVersion<0, 1>>, _state| async move {
1098 /// let (mut sink, mut stream) = conn.split();
1099 /// let recv = spawn(async move {
1100 /// while let Some(Ok(msg)) = stream.next().await {
1101 /// // Handle message from client.
1102 /// }
1103 /// });
1104 /// let send = spawn(async move {
1105 /// loop {
1106 /// let msg = // get message to send to client
1107 /// # 0;
1108 /// sink.send(msg).await;
1109 /// }
1110 /// });
1111 ///
1112 /// join(send, recv).await;
1113 /// Ok(())
1114 /// }.boxed());
1115 /// # }
1116 /// ```
1117 ///
1118 /// This approach requires messages to be sent to the client by value, consuming the message.
1119 /// This is because, if we were to use the `Sync<&ToClient>` implementation for `Connection`,
1120 /// the lifetime for `&ToClient` would be fixed after `split` is called, since the lifetime
1121 /// appears in the return type, `SplitSink<Connection<...>, &ToClient>`. Thus, this lifetime
1122 /// outlives any scoped local variables created after the `split` call, such as `msg` in the
1123 /// `loop`.
1124 ///
1125 /// If we want to use the message after sending it to the client, we would have to clone it,
1126 /// which may be inefficient or impossible. Thus, there is another approach:
1127 ///
1128 /// ## Clone the connection
1129 ///
1130 /// ```
1131 /// use async_std::task::spawn;
1132 /// use futures::{future::{join, FutureExt}, sink::SinkExt, stream::StreamExt};
1133 /// use tide_disco::{error::ServerError, socket::Connection, Api};
1134 /// # use vbs::version::StaticVersion;
1135 ///
1136 /// # fn ex(api: &mut Api<(), ServerError, StaticVersion<0, 1>>) {
1137 /// api.socket("endpoint", |_req, mut conn: Connection<i32, i32, ServerError, StaticVersion<0, 1>>, _state| async move {
1138 /// let recv = {
1139 /// let mut conn = conn.clone();
1140 /// spawn(async move {
1141 /// while let Some(Ok(msg)) = conn.next().await {
1142 /// // Handle message from client.
1143 /// }
1144 /// })
1145 /// };
1146 /// let send = spawn(async move {
1147 /// loop {
1148 /// let msg = // get message to send to client
1149 /// # 0;
1150 /// conn.send(&msg).await;
1151 /// // msg is still live at this point.
1152 /// drop(msg);
1153 /// }
1154 /// });
1155 ///
1156 /// join(send, recv).await;
1157 /// Ok(())
1158 /// }.boxed());
1159 /// # }
1160 /// ```
1161 ///
1162 /// Depending on the exact situation, this method may end up being more verbose than the
1163 /// previous example. But it allows us to retain the higher-ranked trait bound `conn: for<'a>
1164 /// Sink<&'a ToClient>` instead of fixing the lifetime, which can prevent an unnecessary clone
1165 /// in certain situations.
1166 ///
1167 /// # Errors
1168 ///
1169 /// If the route `name` does not exist in the API specification, or if the route already has a
1170 /// handler registered, an error is returned. Note that all routes are initialized with a
1171 /// default handler that echoes parameters and shows documentation, but this default handler can
1172 /// replaced by this function without raising [ApiError::HandlerAlreadyRegistered].
1173 ///
1174 /// If the route `name` exists, but the method is not SOCKET (that is, `METHOD = "M"` was used
1175 /// in the route definition in `api.toml`, with `M` other than `SOCKET`) the error
1176 /// [IncorrectMethod](ApiError::IncorrectMethod) is returned.
1177 ///
1178 /// # Limitations
1179 ///
1180 /// [Like many function parameters](crate#boxed-futures) in [tide_disco](crate), the
1181 /// handler function is required to return a [BoxFuture].
1182 pub fn socket<F, ToClient, FromClient>(
1183 &mut self,
1184 name: &str,
1185 handler: F,
1186 ) -> Result<&mut Self, ApiError>
1187 where
1188 F: 'static
1189 + Send
1190 + Sync
1191 + Fn(
1192 RequestParams,
1193 socket::Connection<ToClient, FromClient, Error, VER>,
1194 &State,
1195 ) -> BoxFuture<'_, Result<(), Error>>,
1196 ToClient: 'static + Serialize + ?Sized,
1197 FromClient: 'static + DeserializeOwned,
1198 State: 'static + Send + Sync,
1199 Error: 'static + Send + Display,
1200 {
1201 self.register_socket_handler(name, socket::handler(handler))
1202 }
1203
1204 /// Register a uni-directional handler for a SOCKET route.
1205 ///
1206 /// This function is very similar to [socket](Self::socket), but it permits the handler only to
1207 /// send messages to the client, not to receive messages back. As such, the handler does not
1208 /// take a [Connection](socket::Connection). Instead, it simply returns a stream of messages
1209 /// which are forwarded to the client as they are generated. If the stream ever yields an error,
1210 /// the error is propagated to the client and then the connection is closed.
1211 ///
1212 /// This function can be simpler to use than [socket](Self::socket) in case the handler does not
1213 /// need to receive messages from the client.
1214 pub fn stream<F, Msg>(&mut self, name: &str, handler: F) -> Result<&mut Self, ApiError>
1215 where
1216 F: 'static + Send + Sync + Fn(RequestParams, &State) -> BoxStream<Result<Msg, Error>>,
1217 Msg: 'static + Serialize + Send + Sync,
1218 State: 'static + Send + Sync,
1219 Error: 'static + Send + Display,
1220 VER: 'static + Send + Sync,
1221 {
1222 self.register_socket_handler(name, socket::stream_handler::<_, _, _, _, VER>(handler))
1223 }
1224
1225 fn register_socket_handler(
1226 &mut self,
1227 name: &str,
1228 handler: socket::Handler<State, Error>,
1229 ) -> Result<&mut Self, ApiError> {
1230 let route = self
1231 .inner
1232 .routes
1233 .get_mut(name)
1234 .ok_or(ApiError::UndefinedRoute)?;
1235 if route.method() != Method::Socket {
1236 return Err(ApiError::IncorrectMethod {
1237 expected: Method::Socket,
1238 actual: route.method(),
1239 });
1240 }
1241 if route.has_handler() {
1242 return Err(ApiError::HandlerAlreadyRegistered);
1243 }
1244
1245 // `set_handler` only fails if the route is not a socket route; since we have already
1246 // checked that it is, this cannot fail.
1247 route
1248 .set_socket_handler(handler)
1249 .unwrap_or_else(|_| panic!("unexpected failure in set_socket_handler"));
1250 Ok(self)
1251 }
1252
1253 /// Register a handler for a METRICS route.
1254 ///
1255 /// When the server receives any request whose URL matches the pattern for this route and whose
1256 /// headers indicate it is a request for metrics, the server will invoke this `handler` instead
1257 /// of the regular HTTP handler for the endpoint. Instead of returning a typed object to
1258 /// serialize, `handler` will return a [Metrics] object which will be serialized to plaintext
1259 /// using the Prometheus format.
1260 ///
1261 /// A request is considered a request for metrics, for the purpose of dispatching to this
1262 /// handler, if the method is GET and the `Accept` header specifies `text/plain` as a better
1263 /// response type than `application/json` and `application/octet-stream` (other Tide Disco
1264 /// handlers respond to the content types `application/json` or `application/octet-stream`). As
1265 /// a special case, a request with no `Accept` header or `Accept: *` will return metrics when
1266 /// there is a metrics route matching the request URL, since metrics are given priority over
1267 /// other content types when multiple routes match the URL.
1268 ///
1269 /// # Examples
1270 ///
1271 /// A metrics endpoint which keeps track of how many times it has been called.
1272 ///
1273 /// `api.toml`
1274 ///
1275 /// ```toml
1276 /// [route.metrics]
1277 /// PATH = ["/metrics"]
1278 /// METHOD = "METRICS"
1279 /// DOC = "Export Prometheus metrics."
1280 /// ```
1281 ///
1282 /// ```
1283 /// # use async_std::sync::Mutex;
1284 /// # use futures::FutureExt;
1285 /// # use tide_disco::{api::{Api, ApiError}, error::ServerError};
1286 /// # use std::borrow::Cow;
1287 /// # use vbs::version::StaticVersion;
1288 /// use prometheus::{Counter, Registry};
1289 ///
1290 /// struct State {
1291 /// counter: Counter,
1292 /// metrics: Registry,
1293 /// }
1294 /// type StaticVer01 = StaticVersion<0, 1>;
1295 ///
1296 /// # fn ex(_api: Api<Mutex<State>, ServerError, StaticVer01>) -> Result<(), ApiError> {
1297 /// let mut api: Api<Mutex<State>, ServerError, StaticVer01>;
1298 /// # api = _api;
1299 /// api.metrics("metrics", |_req, state| async move {
1300 /// state.counter.inc();
1301 /// Ok(Cow::Borrowed(&state.metrics))
1302 /// }.boxed())?;
1303 /// # Ok(())
1304 /// # }
1305 /// ```
1306 //
1307 /// # Errors
1308 ///
1309 /// If the route `name` does not exist in the API specification, or if the route already has a
1310 /// handler registered, an error is returned. Note that all routes are initialized with a
1311 /// default handler that echoes parameters and shows documentation, but this default handler can
1312 /// replaced by this function without raising [ApiError::HandlerAlreadyRegistered].
1313 ///
1314 /// If the route `name` exists, but the method is not METRICS (that is, `METHOD = "M"` was used
1315 /// in the route definition in `api.toml`, with `M` other than `METRICS`) the error
1316 /// [IncorrectMethod](ApiError::IncorrectMethod) is returned.
1317 ///
1318 /// # Limitations
1319 ///
1320 /// [Like many function parameters](crate#boxed-futures) in [tide_disco](crate), the
1321 /// handler function is required to return a [BoxFuture].
1322 pub fn metrics<F, T>(&mut self, name: &str, handler: F) -> Result<&mut Self, ApiError>
1323 where
1324 F: 'static
1325 + Send
1326 + Sync
1327 + Fn(RequestParams, &State::State) -> BoxFuture<Result<Cow<T>, Error>>,
1328 T: 'static + Clone + Metrics,
1329 State: 'static + Send + Sync + ReadState,
1330 Error: 'static,
1331 VER: 'static + Send + Sync,
1332 {
1333 let route = self
1334 .inner
1335 .routes
1336 .get_mut(name)
1337 .ok_or(ApiError::UndefinedRoute)?;
1338 if route.method() != Method::Metrics {
1339 return Err(ApiError::IncorrectMethod {
1340 expected: Method::Metrics,
1341 actual: route.method(),
1342 });
1343 }
1344 if route.has_handler() {
1345 return Err(ApiError::HandlerAlreadyRegistered);
1346 }
1347 // `set_metrics_handler` only fails if the route is not a metrics route; since we have
1348 // already checked that it is, this cannot fail.
1349 route
1350 .set_metrics_handler(handler)
1351 .unwrap_or_else(|_| panic!("unexpected failure in set_metrics_handler"));
1352 Ok(self)
1353 }
1354
1355 /// Set the health check handler for this API.
1356 ///
1357 /// This overrides the existing handler. If `health_check` has not yet been called, the default
1358 /// handler is one which simply returns `Health::default()`.
1359 pub fn with_health_check<H>(
1360 &mut self,
1361 handler: impl 'static + Send + Sync + Fn(&State) -> BoxFuture<H>,
1362 ) -> &mut Self
1363 where
1364 State: 'static + Send + Sync,
1365 H: 'static + HealthCheck,
1366 VER: 'static + Send + Sync,
1367 {
1368 self.inner.health_check = route::health_check_handler::<_, _, VER>(handler);
1369 self
1370 }
1371
1372 /// Create a new [Api] which is just like this one, except has a transformed `Error` type.
1373 pub(crate) fn map_err<Error2>(
1374 self,
1375 f: impl 'static + Clone + Send + Sync + Fn(Error) -> Error2,
1376 ) -> Api<State, Error2, VER>
1377 where
1378 Error: 'static + Send + Sync,
1379 Error2: 'static,
1380 State: 'static + Send + Sync,
1381 {
1382 Api {
1383 inner: ApiInner {
1384 meta: self.inner.meta,
1385 name: self.inner.name,
1386 routes: self
1387 .inner
1388 .routes
1389 .into_iter()
1390 .map(|(name, route)| (name, route.map_err(f.clone())))
1391 .collect(),
1392 routes_by_path: self.inner.routes_by_path,
1393 health_check: self.inner.health_check,
1394 api_version: self.inner.api_version,
1395 error_handler: None,
1396 version_handler: self.inner.version_handler,
1397 public: self.inner.public,
1398 short_description: self.inner.short_description,
1399 long_description: self.inner.long_description,
1400 },
1401 _version: Default::default(),
1402 }
1403 }
1404
1405 pub(crate) fn into_inner(mut self) -> ApiInner<State, Error>
1406 where
1407 Error: crate::Error,
1408 {
1409 // This `into_inner` finalizes the error type for the API. At this point, ensure
1410 // `error_handler` is set.
1411 self.inner.error_handler = Some(error_handler::<Error, VER>());
1412 self.inner
1413 }
1414
1415 fn default_health_check(req: RequestParams, _state: &State) -> BoxFuture<'_, tide::Response> {
1416 async move {
1417 // If there is no healthcheck handler registered, just return [HealthStatus::Available]
1418 // by default; after all, if this handler is getting hit at all, the service must be up.
1419 route::health_check_response::<_, VER>(
1420 &req.accept().unwrap_or_else(|_| {
1421 // The healthcheck endpoint is not allowed to fail, so just use the default
1422 // content type if we can't parse the Accept header.
1423 let mut accept = Accept::new();
1424 accept.set_wildcard(true);
1425 accept
1426 }),
1427 HealthStatus::Available,
1428 )
1429 }
1430 .boxed()
1431 }
1432}
1433
1434// `ReadHandler { handler }` essentially represents a handler function
1435// `move |req, state| async { state.read(|state| handler(req, state)).await.await }`. However, I
1436// cannot convince Rust that the future returned by this closure moves out of `req` while borrowing
1437// from `handler`, which is owned by the closure itself and thus outlives the closure body. This is
1438// partly due to the limitation where _all_ closure parameters must be captured either by value or
1439// by reference, and probably partly due to my lack of creativity. In any case, writing out the
1440// closure object and [Handler] implementation by hand seems to convince Rust that this code is
1441// memory safe.
1442struct ReadHandler<F, VER> {
1443 handler: F,
1444 _version: PhantomData<VER>,
1445}
1446
1447impl<F, VER> From<F> for ReadHandler<F, VER> {
1448 fn from(f: F) -> Self {
1449 Self {
1450 handler: f,
1451 _version: Default::default(),
1452 }
1453 }
1454}
1455
1456#[async_trait]
1457impl<State, Error, F, R, VER> Handler<State, Error> for ReadHandler<F, VER>
1458where
1459 F: 'static
1460 + Send
1461 + Sync
1462 + Fn(RequestParams, &<State as ReadState>::State) -> BoxFuture<'_, Result<R, Error>>,
1463 R: Serialize,
1464 State: 'static + Send + Sync + ReadState,
1465 VER: 'static + Send + Sync + StaticVersionType,
1466{
1467 async fn handle(
1468 &self,
1469 req: RequestParams,
1470 state: &State,
1471 ) -> Result<tide::Response, RouteError<Error>> {
1472 let accept = req.accept()?;
1473 response_from_result(
1474 &accept,
1475 state.read(|state| (self.handler)(req, state)).await,
1476 VER::instance(),
1477 )
1478 }
1479}
1480
1481// A manual closure that serves a similar purpose as [ReadHandler].
1482struct WriteHandler<F, VER> {
1483 handler: F,
1484 _version: PhantomData<VER>,
1485}
1486
1487impl<F, VER> From<F> for WriteHandler<F, VER> {
1488 fn from(f: F) -> Self {
1489 Self {
1490 handler: f,
1491 _version: Default::default(),
1492 }
1493 }
1494}
1495
1496#[async_trait]
1497impl<State, Error, F, R, VER> Handler<State, Error> for WriteHandler<F, VER>
1498where
1499 F: 'static
1500 + Send
1501 + Sync
1502 + Fn(RequestParams, &mut <State as ReadState>::State) -> BoxFuture<'_, Result<R, Error>>,
1503 R: Serialize,
1504 State: 'static + Send + Sync + WriteState,
1505 VER: 'static + Send + Sync + StaticVersionType,
1506{
1507 async fn handle(
1508 &self,
1509 req: RequestParams,
1510 state: &State,
1511 ) -> Result<tide::Response, RouteError<Error>> {
1512 let accept = req.accept()?;
1513 response_from_result(
1514 &accept,
1515 state.write(|state| (self.handler)(req, state)).await,
1516 VER::instance(),
1517 )
1518 }
1519}
1520
1521#[cfg(test)]
1522mod test {
1523 use crate::{
1524 App, StatusCode, Url,
1525 error::{Error, ServerError},
1526 healthcheck::HealthStatus,
1527 socket::Connection,
1528 testing::{Client, setup_test, test_ws_client, test_ws_client_with_headers},
1529 };
1530 use async_std::{sync::RwLock, task::spawn};
1531 use async_tungstenite::{
1532 WebSocketStream,
1533 tungstenite::{http::header::*, protocol::Message, protocol::frame::coding::CloseCode},
1534 };
1535 use futures::{
1536 AsyncRead, AsyncWrite, FutureExt, SinkExt, StreamExt,
1537 stream::{iter, once, repeat},
1538 };
1539 use portpicker::pick_unused_port;
1540 use prometheus::{Counter, Registry};
1541 use std::borrow::Cow;
1542 use toml::toml;
1543 use vbs::{
1544 BinarySerializer, Serializer,
1545 version::{StaticVersion, StaticVersionType},
1546 };
1547
1548 #[cfg(windows)]
1549 use async_tungstenite::tungstenite::Error as WsError;
1550 #[cfg(windows)]
1551 use std::io::ErrorKind;
1552
1553 type StaticVer01 = StaticVersion<0, 1>;
1554 type SerializerV01 = Serializer<StaticVersion<0, 1>>;
1555
1556 async fn check_stream_closed<S>(mut conn: WebSocketStream<S>)
1557 where
1558 S: AsyncRead + AsyncWrite + Unpin,
1559 {
1560 let msg = conn.next().await;
1561
1562 #[cfg(not(windows))]
1563 assert!(msg.is_none(), "{:?}", msg);
1564
1565 // Windows doesn't handle shutdown very gracefully.
1566 #[cfg(windows)]
1567 match msg {
1568 None => {}
1569 Some(Err(WsError::Io(err))) if err.kind() == ErrorKind::ConnectionAborted => {}
1570 msg => panic!(
1571 "expected end of stream or ConnectionAborted error, got {:?}",
1572 msg
1573 ),
1574 }
1575 }
1576
1577 #[async_std::test]
1578 async fn test_socket_endpoint() {
1579 setup_test();
1580
1581 let mut app = App::<_, ServerError>::with_state(RwLock::new(()));
1582 let api_toml = toml! {
1583 [meta]
1584 FORMAT_VERSION = "0.1.0"
1585
1586 [route.echo]
1587 PATH = ["/echo"]
1588 METHOD = "SOCKET"
1589
1590 [route.once]
1591 PATH = ["/once"]
1592 METHOD = "SOCKET"
1593
1594 [route.error]
1595 PATH = ["/error"]
1596 METHOD = "SOCKET"
1597 };
1598 {
1599 let mut api = app
1600 .module::<ServerError, StaticVer01>("mod", api_toml)
1601 .unwrap();
1602 api.socket(
1603 "echo",
1604 |_req, mut conn: Connection<String, String, _, StaticVer01>, _state| {
1605 async move {
1606 while let Some(msg) = conn.next().await {
1607 conn.send(&msg?).await?;
1608 }
1609 Ok(())
1610 }
1611 .boxed()
1612 },
1613 )
1614 .unwrap()
1615 .socket(
1616 "once",
1617 |_req, mut conn: Connection<str, (), _, StaticVer01>, _state| {
1618 async move {
1619 conn.send("msg").boxed().await?;
1620 Ok(())
1621 }
1622 .boxed()
1623 },
1624 )
1625 .unwrap()
1626 .socket(
1627 "error",
1628 |_req, _conn: Connection<(), (), _, StaticVer01>, _state| {
1629 async move {
1630 Err(ServerError::catch_all(
1631 StatusCode::INTERNAL_SERVER_ERROR,
1632 "an error message".to_string(),
1633 ))
1634 }
1635 .boxed()
1636 },
1637 )
1638 .unwrap();
1639 }
1640 let port = pick_unused_port().unwrap();
1641 let url: Url = format!("http://localhost:{}", port).parse().unwrap();
1642 spawn(app.serve(format!("0.0.0.0:{}", port), StaticVer01::instance()));
1643
1644 // Create a client that accepts JSON messages.
1645 let mut conn = test_ws_client_with_headers(
1646 url.join("mod/echo").unwrap(),
1647 &[(ACCEPT, "application/json")],
1648 )
1649 .await;
1650
1651 // Send a JSON message.
1652 conn.send(Message::Text(serde_json::to_string("hello").unwrap()))
1653 .await
1654 .unwrap();
1655 assert_eq!(
1656 conn.next().await.unwrap().unwrap(),
1657 Message::Text(serde_json::to_string("hello").unwrap())
1658 );
1659
1660 // Send a binary message.
1661 conn.send(Message::Binary(
1662 SerializerV01::serialize("goodbye").unwrap(),
1663 ))
1664 .await
1665 .unwrap();
1666 assert_eq!(
1667 conn.next().await.unwrap().unwrap(),
1668 Message::Text(serde_json::to_string("goodbye").unwrap())
1669 );
1670
1671 // Create a client that accepts binary messages.
1672 let mut conn = test_ws_client_with_headers(
1673 url.join("mod/echo").unwrap(),
1674 &[(ACCEPT, "application/octet-stream")],
1675 )
1676 .await;
1677
1678 // Send a JSON message.
1679 conn.send(Message::Text(serde_json::to_string("hello").unwrap()))
1680 .await
1681 .unwrap();
1682 assert_eq!(
1683 conn.next().await.unwrap().unwrap(),
1684 Message::Binary(SerializerV01::serialize("hello").unwrap())
1685 );
1686
1687 // Send a binary message.
1688 conn.send(Message::Binary(
1689 SerializerV01::serialize("goodbye").unwrap(),
1690 ))
1691 .await
1692 .unwrap();
1693 assert_eq!(
1694 conn.next().await.unwrap().unwrap(),
1695 Message::Binary(SerializerV01::serialize("goodbye").unwrap())
1696 );
1697
1698 // Test a stream that exits normally.
1699 let mut conn = test_ws_client(url.join("mod/once").unwrap()).await;
1700 assert_eq!(
1701 conn.next().await.unwrap().unwrap(),
1702 Message::Text(serde_json::to_string("msg").unwrap())
1703 );
1704 match conn.next().await.unwrap().unwrap() {
1705 Message::Close(None) => {}
1706 msg => panic!("expected normal close frame, got {:?}", msg),
1707 };
1708 check_stream_closed(conn).await;
1709
1710 // Test a stream that errors.
1711 let mut conn = test_ws_client(url.join("mod/error").unwrap()).await;
1712 match conn.next().await.unwrap().unwrap() {
1713 Message::Close(Some(frame)) => {
1714 assert_eq!(frame.code, CloseCode::Error);
1715 assert_eq!(frame.reason, "Error 500: an error message");
1716 }
1717 msg => panic!("expected error close frame, got {:?}", msg),
1718 }
1719 check_stream_closed(conn).await;
1720 }
1721
1722 #[async_std::test]
1723 async fn test_stream_endpoint() {
1724 setup_test();
1725
1726 let mut app = App::<_, ServerError>::with_state(RwLock::new(()));
1727 let api_toml = toml! {
1728 [meta]
1729 FORMAT_VERSION = "0.1.0"
1730
1731 [route.nat]
1732 PATH = ["/nat"]
1733 METHOD = "SOCKET"
1734
1735 [route.once]
1736 PATH = ["/once"]
1737 METHOD = "SOCKET"
1738
1739 [route.error]
1740 PATH = ["/error"]
1741 METHOD = "SOCKET"
1742 };
1743 {
1744 let mut api = app
1745 .module::<ServerError, StaticVer01>("mod", api_toml)
1746 .unwrap();
1747 api.stream("nat", |_req, _state| iter(0..).map(Ok).boxed())
1748 .unwrap()
1749 .stream("once", |_req, _state| once(async { Ok(0) }).boxed())
1750 .unwrap()
1751 .stream::<_, ()>("error", |_req, _state| {
1752 // We intentionally return a stream that never terminates, to check that simply
1753 // yielding an error causes the connection to terminate.
1754 repeat(Err(ServerError::catch_all(
1755 StatusCode::INTERNAL_SERVER_ERROR,
1756 "an error message".to_string(),
1757 )))
1758 .boxed()
1759 })
1760 .unwrap();
1761 }
1762 let port = pick_unused_port().unwrap();
1763 let url: Url = format!("http://localhost:{}", port).parse().unwrap();
1764 spawn(app.serve(format!("0.0.0.0:{}", port), StaticVer01::instance()));
1765
1766 // Consume the `nat` stream.
1767 let mut conn = test_ws_client(url.join("mod/nat").unwrap()).await;
1768 for i in 0..100 {
1769 assert_eq!(
1770 conn.next().await.unwrap().unwrap(),
1771 Message::Text(serde_json::to_string(&i).unwrap())
1772 );
1773 }
1774
1775 // Test a finite stream.
1776 let mut conn = test_ws_client(url.join("mod/once").unwrap()).await;
1777 assert_eq!(
1778 conn.next().await.unwrap().unwrap(),
1779 Message::Text(serde_json::to_string(&0).unwrap())
1780 );
1781 match conn.next().await.unwrap().unwrap() {
1782 Message::Close(None) => {}
1783 msg => panic!("expected normal close frame, got {:?}", msg),
1784 }
1785 check_stream_closed(conn).await;
1786
1787 // Test a stream that errors.
1788 let mut conn = test_ws_client(url.join("mod/error").unwrap()).await;
1789 match conn.next().await.unwrap().unwrap() {
1790 Message::Close(Some(frame)) => {
1791 assert_eq!(frame.code, CloseCode::Error);
1792 assert_eq!(frame.reason, "Error 500: an error message");
1793 }
1794 msg => panic!("expected error close frame, got {:?}", msg),
1795 }
1796 check_stream_closed(conn).await;
1797 }
1798
1799 #[async_std::test]
1800 async fn test_custom_healthcheck() {
1801 setup_test();
1802
1803 let mut app = App::<_, ServerError>::with_state(HealthStatus::Available);
1804 let api_toml = toml! {
1805 [meta]
1806 FORMAT_VERSION = "0.1.0"
1807
1808 [route.dummy]
1809 PATH = ["/dummy"]
1810 };
1811 {
1812 let mut api = app
1813 .module::<ServerError, StaticVer01>("mod", api_toml)
1814 .unwrap();
1815 api.with_health_check(|state| async move { *state }.boxed());
1816 }
1817 let port = pick_unused_port().unwrap();
1818 let url: Url = format!("http://localhost:{}", port).parse().unwrap();
1819 spawn(app.serve(format!("0.0.0.0:{}", port), StaticVer01::instance()));
1820 let client = Client::new(url).await;
1821
1822 let res = client.get("/mod/healthcheck").send().await.unwrap();
1823 assert_eq!(res.status(), StatusCode::OK);
1824 assert_eq!(
1825 res.json::<HealthStatus>().await.unwrap(),
1826 HealthStatus::Available
1827 );
1828 }
1829
1830 #[async_std::test]
1831 async fn test_metrics_endpoint() {
1832 setup_test();
1833
1834 struct State {
1835 metrics: Registry,
1836 counter: Counter,
1837 }
1838
1839 let counter = Counter::new(
1840 "counter",
1841 "count of how many times metrics have been exported",
1842 )
1843 .unwrap();
1844 let metrics = Registry::new();
1845 metrics.register(Box::new(counter.clone())).unwrap();
1846 let state = State { metrics, counter };
1847
1848 let mut app = App::<_, ServerError>::with_state(RwLock::new(state));
1849 let api_toml = toml! {
1850 [meta]
1851 FORMAT_VERSION = "0.1.0"
1852
1853 [route.metrics]
1854 PATH = ["/metrics"]
1855 METHOD = "METRICS"
1856 };
1857 {
1858 let mut api = app
1859 .module::<ServerError, StaticVer01>("mod", api_toml)
1860 .unwrap();
1861 api.metrics("metrics", |_req, state| {
1862 async move {
1863 state.counter.inc();
1864 Ok(Cow::Borrowed(&state.metrics))
1865 }
1866 .boxed()
1867 })
1868 .unwrap();
1869 }
1870 let port = pick_unused_port().unwrap();
1871 let url: Url = format!("http://localhost:{port}").parse().unwrap();
1872 spawn(app.serve(format!("0.0.0.0:{port}"), StaticVer01::instance()));
1873 let client = Client::new(url).await;
1874
1875 for i in 1..5 {
1876 tracing::info!("making metrics request {i}");
1877 let expected = format!(
1878 "# HELP counter count of how many times metrics have been exported\n# TYPE counter counter\ncounter {i}\n"
1879 );
1880 let res = client.get("mod/metrics").send().await.unwrap();
1881 assert_eq!(res.status(), StatusCode::OK);
1882 assert_eq!(res.text().await.unwrap(), expected);
1883 }
1884 }
1885}