Skip to main content

autumn_plugin_markstream/
plugin.rs

1//! [`MarkstreamPlugin`]: installs the markstream assets in an Autumn app.
2//!
3//! The plugin gives [`MARKSTREAM_ASSETS`] to Autumn's
4//! `AppBuilder::plugin_assets` method. It adds no other routes and reads no
5//! configuration.
6
7use std::borrow::Cow;
8
9use autumn_web::app::AppBuilder;
10use autumn_web::plugin::Plugin;
11
12use crate::assets::MARKSTREAM_ASSETS;
13
14/// The plugin name in Autumn diagnostics.
15pub const PLUGIN_NAME: &str = "autumn-plugin-markstream";
16
17/// Installs the markstream assets in an Autumn app.
18///
19/// ```rust,no_run
20/// use autumn_plugin_markstream::MarkstreamPlugin;
21///
22/// # async fn run() {
23/// autumn_web::app()
24///     .plugin(MarkstreamPlugin::new())
25///     .run()
26///     .await;
27/// # }
28/// ```
29///
30/// Then put [`markstream_head`](crate::markstream_head) in the page head
31/// and render Markdown with [`Markstream`](crate::Markstream).
32#[derive(Debug, Default, Clone, Copy)]
33#[must_use]
34pub struct MarkstreamPlugin;
35
36impl MarkstreamPlugin {
37    /// Makes the plugin.
38    pub const fn new() -> Self {
39        Self
40    }
41}
42
43impl Plugin for MarkstreamPlugin {
44    fn name(&self) -> Cow<'static, str> {
45        Cow::Borrowed(PLUGIN_NAME)
46    }
47
48    fn build(self, app: AppBuilder) -> AppBuilder {
49        app.plugin_assets(&MARKSTREAM_ASSETS)
50    }
51}
52
53#[cfg(test)]
54mod tests {
55    use super::*;
56    use crate::assets::{CORE_JS, INIT_JS, MARKSTREAM_ASSETS, MARKSTREAM_ENTRY, PLUGIN_CSS};
57    use autumn_web::assets::{PLUGIN_ASSETS_ROUTE_MARKER, asset_url};
58    use autumn_web::plugin_conformance::{ConformanceConfig, run_conformance};
59    use autumn_web::route_listing::{RouteClassification, RouteSource};
60    use autumn_web::test::{TestApp, TestClient};
61
62    const JS: &str = "text/javascript; charset=utf-8";
63    const IMMUTABLE: &str = "public, max-age=31536000, immutable";
64    const REVALIDATE: &str = "public, max-age=0, must-revalidate";
65
66    fn client() -> TestClient {
67        TestApp::new().plugin(MarkstreamPlugin::new()).build()
68    }
69
70    #[tokio::test]
71    async fn init_module_serves_at_its_hashed_url() {
72        let response = client().get(&MARKSTREAM_ASSETS.url(INIT_JS)).send().await;
73        response
74            .assert_ok()
75            .assert_header("content-type", JS)
76            .assert_header("cache-control", IMMUTABLE);
77        assert!(response.text().contains("./core.js"));
78    }
79
80    #[tokio::test]
81    async fn relative_imports_resolve_to_plain_urls_that_serve() {
82        let client = client();
83        for path in [
84            CORE_JS,
85            MARKSTREAM_ENTRY,
86            "vendor/vue/vue.runtime.esm-browser.prod.js",
87        ] {
88            let plain = format!("/static/_plugins/markstream/{path}");
89            let response = client.get(&plain).send().await;
90            response
91                .assert_ok()
92                .assert_header("content-type", JS)
93                .assert_header("cache-control", REVALIDATE);
94            let etag = response.header("etag").expect("etag").to_owned();
95            client
96                .get(&plain)
97                .header("if-none-match", &etag)
98                .send()
99                .await
100                .assert_status(304);
101        }
102    }
103
104    #[tokio::test]
105    async fn stylesheets_serve() {
106        let client = client();
107        for path in [PLUGIN_CSS, "vendor/markstream-vue/index.css"] {
108            client
109                .get(&MARKSTREAM_ASSETS.url(path))
110                .send()
111                .await
112                .assert_ok()
113                .assert_header("content-type", "text/css; charset=utf-8");
114        }
115    }
116
117    #[tokio::test]
118    async fn provenance_and_unknown_paths_are_not_found() {
119        let client = client();
120        for path in [
121            "/static/_plugins/markstream/manifest.json",
122            "/static/_plugins/markstream/licenses/vue.LICENSE",
123            "/static/_plugins/markstream/init.00000000.js",
124            "/static/_plugins/markstream/vendor/markstream-vue/tailwind.js",
125            "/static/_plugins/markstream/../manifest.json",
126        ] {
127            client.get(path).send().await.assert_status(404);
128        }
129    }
130
131    #[tokio::test]
132    async fn asset_url_resolves_the_installed_bundle() {
133        let _client = client();
134        assert_eq!(
135            asset_url(&format!("_plugins/markstream/{INIT_JS}")),
136            MARKSTREAM_ASSETS.url(INIT_JS)
137        );
138    }
139
140    #[test]
141    fn bundle_routes_are_public_plugin_routes() {
142        let app = autumn_web::app().plugin(MarkstreamPlugin::new());
143        let infos = app.plugin_route_infos().expect("route infos");
144        let routes: Vec<_> = infos
145            .iter()
146            .filter(|i| i.path.starts_with("/static/_plugins/markstream/"))
147            .collect();
148        assert_eq!(routes.len(), MARKSTREAM_ASSETS.iter().count() * 2);
149        for info in routes {
150            assert_eq!(info.method, "GET");
151            assert_eq!(info.classification, RouteClassification::Public);
152            assert_eq!(info.middleware, [PLUGIN_ASSETS_ROUTE_MARKER]);
153            assert_eq!(info.source, RouteSource::Plugin(PLUGIN_NAME.to_owned()));
154        }
155    }
156
157    #[test]
158    fn plugin_passes_conformance() {
159        let app = autumn_web::app().plugin(MarkstreamPlugin::new());
160        let infos = app.plugin_route_infos().expect("route infos");
161        let report = run_conformance(&ConformanceConfig::new(PLUGIN_NAME), &infos);
162        assert!(report.passed(), "{}", report.to_text_report());
163    }
164
165    #[tokio::test]
166    async fn installing_the_plugin_twice_is_harmless() {
167        let client = TestApp::new()
168            .plugin(MarkstreamPlugin::new())
169            .plugin(MarkstreamPlugin::new())
170            .build();
171        client
172            .get(&MARKSTREAM_ASSETS.url(INIT_JS))
173            .send()
174            .await
175            .assert_ok();
176    }
177}