rocket_sass_fairing/
lib.rs

1//! Easily compile and serve a sass/scss style sheet through Rocket.
2//! ```rust
3//! use rocket::{launch, get, routes};
4//! use rocket_sass_fairing::SassSheet;
5//!
6//! #[launch]
7//! fn rocket() -> _ {
8//!     rocket::build()
9//!         .attach(SassSheet::fairing())
10//!         .mount("/", routes![style])
11//! }
12//!
13//! #[get("/assets/style.css")]
14//! async fn style(sheet: &SassSheet) -> &SassSheet { sheet }
15//! #
16//! # use rocket::local::blocking::Client;
17//! # use rocket::http::Status;
18//! # let client = Client::tracked(rocket()).expect("valid rocket instance");
19//! # let response = client.get("/assets/style.css").dispatch();
20//! # assert_eq!(response.status(), Status::Ok);
21//! # assert_eq!(response.into_string().unwrap(), "a b{color:a b}");
22//! ```
23use normpath::PathExt;
24use rocket::{
25    error,
26    fairing::{self, Fairing, Info, Kind},
27    http::ContentType,
28    info, info_,
29    outcome::IntoOutcome,
30    request::{self, FromRequest, Request},
31    response::{self, Responder, Response},
32    Build, Orbit, Rocket,
33};
34use std::path::PathBuf;
35
36pub struct SassSheet {
37    content: String,
38    cache_max_age: i32,
39    path: PathBuf,
40}
41
42impl SassSheet {
43    pub fn fairing() -> impl Fairing {
44        SassSheetFairing
45    }
46}
47
48struct SassSheetFairing;
49
50#[rocket::async_trait]
51impl Fairing for SassSheetFairing {
52    fn info(&self) -> Info {
53        Info {
54            kind: Kind::Ignite | Kind::Liftoff,
55            name: "Sass Sheet",
56        }
57    }
58
59    async fn on_ignite(&self, rocket: Rocket<Build>) -> fairing::Result {
60        use rocket::figment::value::magic::RelativePathBuf;
61
62        let configured_path = rocket
63            .figment()
64            .extract_inner::<RelativePathBuf>("sass_sheet_path")
65            .map(|path| path.relative());
66
67        let relative_path = match configured_path {
68            Ok(path) => path,
69            Err(e) if e.missing() => "assets/style.scss".into(),
70            Err(e) => {
71                rocket::config::pretty_print_error(e);
72                return Err(rocket);
73            }
74        };
75
76        let path = match relative_path.normalize() {
77            Ok(path) => path.into_path_buf(),
78            Err(e) => {
79                error!(
80                    "Invalid sass sheet file '{}': {}.",
81                    relative_path.display(),
82                    e
83                );
84                return Err(rocket);
85            }
86        };
87
88        info!("Compiling sass file '{}'...", relative_path.display());
89        let options = grass::Options::default().style(grass::OutputStyle::Compressed);
90        let compiled_css = match grass::from_path(&path.to_string_lossy(), &options) {
91            Ok(css) => css,
92            Err(e) => {
93                error!("Couldn't compile sass: {}", e);
94                return Err(rocket);
95            }
96        };
97
98        let cache_max_age = rocket
99            .figment()
100            .extract_inner::<i32>("assets_max_age")
101            .unwrap_or(86400);
102
103        Ok(rocket.manage(SassSheet {
104            content: compiled_css,
105            cache_max_age,
106            path,
107        }))
108    }
109
110    async fn on_liftoff(&self, rocket: &Rocket<Orbit>) {
111        use rocket::{figment::Source, log::PaintExt, yansi::Paint};
112
113        let state = rocket
114            .state::<SassSheet>()
115            .expect("SassSheet registered in on_ignite");
116
117        info!("{}{}:", Paint::emoji("📐 "), Paint::magenta("Assets"));
118        info_!("sheet path: {}", Paint::white(Source::from(&*state.path)));
119        info_!("cache max age: {}", Paint::white(state.cache_max_age));
120    }
121}
122
123#[rocket::async_trait]
124impl<'r> FromRequest<'r> for &'r SassSheet {
125    type Error = ();
126    async fn from_request(req: &'r Request<'_>) -> request::Outcome<Self, ()> {
127        req.rocket().state::<SassSheet>().or_forward(())
128    }
129}
130impl<'r, 'o: 'r> Responder<'r, 'o> for &'o SassSheet {
131    fn respond_to(self, req: &'r Request<'_>) -> response::Result<'o> {
132        let content: &str = self.content.as_ref();
133        Response::build_from(content.respond_to(req)?)
134            .header(ContentType::CSS)
135            .raw_header("Cache-control", format!("max-age={}", self.cache_max_age))
136            .ok()
137    }
138}