penguin/lib.rs
1//! Penguin is a dev server with features like auto-reloading, a static file
2//! server, and proxy-support. It is available both, as an app and as a library.
3//! You are currently reading the library docs. If you are interested in the CLI
4//! app, see [the README](https://github.com/LukasKalbertodt/penguin#readme).
5//!
6//! This library essentially allows you to configure and then start an HTTP
7//! server. After starting the server you get a [`Controller`] which allows you
8//! to send commands to active browser sessions, like reloading the page or
9//! showing a message.
10//!
11//!
12//! # Quick start
13//!
14//! This should get you started as it shows almost everything this library has
15//! to offer:
16//!
17//! ```no_run
18//! use std::{path::Path, time::Duration};
19//! use penguin::Server;
20//!
21//! #[tokio::main]
22//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
23//! // Configure the server.
24//! let (server, controller) = Server::bind(([127, 0, 0, 1], 4090).into())
25//! .proxy("localhost:8000".parse()?)
26//! .add_mount("/assets", Path::new("./frontend/build"))?
27//! .build()?;
28//!
29//! // In some other task, you can control the browser sessions. This dummy
30//! // code just waits 5 seconds and then reloads all sessions.
31//! tokio::spawn(async move {
32//! tokio::time::sleep(Duration::from_secs(5)).await;
33//! controller.reload();
34//! });
35//!
36//! server.await?;
37//!
38//! Ok(())
39//! }
40//! ```
41//!
42//! # Routing
43//!
44//! Incoming requests are routed like this (from highest to lowest priority):
45//!
46//! - Requests to the control path (`/~~penguin` by default) are internally
47//! handled. This is used for establishing WS connections and to receive
48//! commands.
49//! - Requests with a path matching one of the mounts is served from that
50//! directory.
51//! - The most specific mount (i.e. the one with the longest URI path) is
52//! used. Consider there are two mounts: `/cat` -> `./foo` and `/cat/paw`
53//! -> `./bar`. Then a request to `/cat/paw/info.json` is replied to with
54//! `./bar/info.json` while a request to `/cat/style.css` is replied to
55//! with `./foo/style.css`
56//! - If a proxy is configured, then all remaining requests are forwarded to it
57//! and its reply is forwarded back to the initiator of the request. Otherwise
58//! (no proxy configured), all remaining requests are answered with 404.
59//!
60//!
61
62#![deny(missing_debug_implementations)]
63
64use std::{fmt, future::Future, net::SocketAddr, pin::Pin, task};
65
66use tokio::sync::broadcast::{self, Sender};
67
68mod config;
69mod inject;
70mod serve;
71pub mod util;
72mod ws;
73
74#[cfg(test)]
75mod tests;
76
77/// Reexport of `hyper` dependency (which includes `http`).
78pub extern crate hyper;
79
80pub use config::{
81 Builder, Config, ConfigError, DEFAULT_CONTROL_PATH, Mount, ProxyTarget, ProxyTargetParseError
82};
83
84/// Penguin server: the main type of this library.
85///
86/// This type implements `Future`, and can thus be `await`ed. If you do not
87/// `await` (or otherwise poll) this, the server will not start serving.
88#[must_use = "futures do nothing unless you `.await` or poll them"]
89pub struct Server {
90 // TODO: maybe avoid boxing this if possible?
91 future: Pin<Box<dyn Send + Future<Output = Result<(), hyper::Error>>>>,
92}
93
94impl Server {
95 /// Returns a builder to configure the server with the bind address of the
96 /// server being set to `addr`.
97 pub fn bind(addr: SocketAddr) -> Builder {
98 Builder::new(addr)
99 }
100
101 /// Builds a server and a controller from a configuration. Most of the time
102 /// you can use [`Builder::build`] instead of this method.
103 pub fn build(config: Config) -> (Self, Controller) {
104 let (sender, _) = broadcast::channel(ACTION_CHANNEL_SIZE);
105 let controller = Controller(sender.clone());
106 let future = Box::pin(serve::run(config, sender));
107
108 (Self { future }, controller)
109 }
110}
111
112impl Future for Server {
113 type Output = Result<(), hyper::Error>;
114
115 fn poll(mut self: Pin<&mut Self>, cx: &mut task::Context<'_>) -> task::Poll<Self::Output> {
116 self.future.as_mut().poll(cx)
117 }
118}
119
120impl fmt::Debug for Server {
121 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
122 f.pad("Server(_)")
123 }
124}
125
126const ACTION_CHANNEL_SIZE: usize = 64;
127
128/// A handle to send commands to the server.
129#[derive(Debug, Clone)]
130pub struct Controller(Sender<Action>);
131
132impl Controller {
133 /// Reloads all active browser sessions.
134 pub fn reload(&self) {
135 let _ = self.0.send(Action::Reload);
136 }
137
138 /// Shows a message as overlay in all active browser sessions. The given
139 /// string will be copied into the `innerHTML` of a `<div>` verbatim.
140 ///
141 /// This call will overwrite/hide all previous messages.
142 pub fn show_message(&self, msg: impl Into<String>) {
143 let _ = self.0.send(Action::Message(msg.into()));
144 }
145}
146
147#[derive(Debug, Clone)]
148enum Action {
149 Reload,
150 Message(String),
151}