mini-static 0.6.2

A secure, async static file server with streaming, traversal protection, and connection limits.
Documentation
use std::path::Path;

use bytes::Bytes;
use serde_json::json;

/// The type of change detected in a watched file.
///
/// Used by live-reload to determine what the browser should do when a file changes:
/// CSS stylesheets are hot-swapped, while scripts and HTML require a full page reload.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ChangeType {
	/// A CSS stylesheet changed.
	Css,
	/// A JavaScript module changed.
	Script,
	/// An HTML page changed.
	Html,
	/// Some other file changed.
	Other,
}

impl ChangeType {
	/// Determine the change type from a file path's extension.
	pub fn from_path(path: &Path) -> Self {
		match path.extension().and_then(|e| e.to_str()) {
			Some("css") => ChangeType::Css,
			Some("js" | "mjs") => ChangeType::Script,
			Some("html" | "htm") => ChangeType::Html,
			_ => ChangeType::Other,
		}
	}

	/// The string representation used in SSE event names and JSON.
	pub fn as_str(&self) -> &'static str {
		match self {
			ChangeType::Css => "css",
			ChangeType::Script => "script",
			ChangeType::Html => "html",
			ChangeType::Other => "other",
		}
	}
}

/// Encode a reload event as an SSE (Server-Sent Events) frame.
///
/// The frame follows the SSE text/event-stream format:
/// ```text
/// event: <change_type>
/// data: {"type": "<change_type>"}
///
/// ```
///
/// This is the single canonical place the SSE frame format is defined.
/// Callers that forward reload events over HTTP (e.g., mini-unified) call this
/// function rather than re-implementing the byte-for-byte format.
pub fn reload_event_frame(change_type: &ChangeType) -> Bytes {
	let data = json!({ "type": change_type.as_str() });
	let msg = format!(
		"event: {}\ndata: {}\n\n",
		change_type.as_str(),
		data
	);
	Bytes::from(msg)
}

#[cfg(test)]
mod tests {
	use super::*;

	#[test]
	fn reload_event_frame_formats_css_correctly() {
		let frame = reload_event_frame(&ChangeType::Css);
		let s = String::from_utf8(frame.to_vec()).unwrap();
		assert_eq!(s, "event: css\ndata: {\"type\":\"css\"}\n\n");
	}

	#[test]
	fn reload_event_frame_formats_script_correctly() {
		let frame = reload_event_frame(&ChangeType::Script);
		let s = String::from_utf8(frame.to_vec()).unwrap();
		assert_eq!(s, "event: script\ndata: {\"type\":\"script\"}\n\n");
	}

	#[test]
	fn reload_event_frame_formats_html_correctly() {
		let frame = reload_event_frame(&ChangeType::Html);
		let s = String::from_utf8(frame.to_vec()).unwrap();
		assert_eq!(s, "event: html\ndata: {\"type\":\"html\"}\n\n");
	}

	#[test]
	fn reload_event_frame_formats_other_correctly() {
		let frame = reload_event_frame(&ChangeType::Other);
		let s = String::from_utf8(frame.to_vec()).unwrap();
		assert_eq!(s, "event: other\ndata: {\"type\":\"other\"}\n\n");
	}

	#[test]
	fn change_type_from_path_css() {
		assert_eq!(ChangeType::from_path(Path::new("style.css")), ChangeType::Css);
		assert_eq!(ChangeType::from_path(Path::new("dir/main.css")), ChangeType::Css);
	}

	#[test]
	fn change_type_from_path_script() {
		assert_eq!(ChangeType::from_path(Path::new("app.js")), ChangeType::Script);
		assert_eq!(ChangeType::from_path(Path::new("mod.mjs")), ChangeType::Script);
		assert_eq!(ChangeType::from_path(Path::new("dir/lib.js")), ChangeType::Script);
	}

	#[test]
	fn change_type_from_path_html() {
		assert_eq!(ChangeType::from_path(Path::new("index.html")), ChangeType::Html);
		assert_eq!(ChangeType::from_path(Path::new("page.htm")), ChangeType::Html);
		assert_eq!(ChangeType::from_path(Path::new("dir/file.html")), ChangeType::Html);
	}

	#[test]
	fn change_type_from_path_other() {
		assert_eq!(ChangeType::from_path(Path::new("image.png")), ChangeType::Other);
		assert_eq!(ChangeType::from_path(Path::new("data.json")), ChangeType::Other);
		assert_eq!(ChangeType::from_path(Path::new("README")), ChangeType::Other);
	}
}