1pub mod path;
2
3use rust_embed::RustEmbed;
4use thiserror::Error;
5use std::borrow::Cow;
6
7#[derive(Debug, Error)]
8#[non_exhaustive]
9pub enum SpaError {
10 #[error("Resource not found: {0}")]
11 NotFound(String),
12 #[error("MIME type detection failed")]
13 MimeDetection,
14 #[error("Path error: {0}")]
15 PathError(#[from] crate::core::path::PathError),
16 #[error("Index file not found")]
17 IndexFileNotFound,
18}
19
20#[derive(Clone)]
22#[non_exhaustive]
23pub struct SpaConfig {
24 pub base_path: String,
25 pub index_files: Vec<String>,
26}
27
28impl Default for SpaConfig {
29 fn default() -> Self {
30 SpaConfig {
31 base_path: "/".to_string(),
32 index_files: vec!["index.html".to_string()],
33 }
34 }
35}
36
37impl SpaConfig {
38 pub fn with_base_path(mut self, base_path: &str) -> Self {
39 self.base_path = base_path.to_string();
40 self
41 }
42
43 pub fn with_index_files(mut self, files: &[&str]) -> Self {
44 self.index_files = files.iter().map(|s| s.to_string()).collect();
45 self
46 }
47
48 pub fn add_index_file(mut self, file: &str) -> Self {
49 self.index_files.push(file.to_string());
50 self
51 }
52}
53
54pub struct SpaHandler<E: RustEmbed> {
56 config: SpaConfig,
57 _marker: std::marker::PhantomData<E>,
58}
59
60impl<E: RustEmbed> SpaHandler<E> {
61 pub fn new(config: SpaConfig) -> Self {
62 Self {
63 config,
64 _marker: std::marker::PhantomData,
65 }
66 }
67
68 pub fn get_file(&self, request_path: &str) -> Result<(Cow<'static, [u8]>, &'static str), SpaError> {
70 let clean_path = crate::core::path::collapse_slashes(request_path);
71 let normalized_path = crate::core::path::normalize_path(&clean_path)?;
72 let resource_path = crate::core::path::relative_to_base(&normalized_path, &self.config.base_path);
73
74 if let Some(content) = E::get(&resource_path) {
75 let mime = mime_guess::from_path(&resource_path)
76 .first_raw()
77 .ok_or(SpaError::MimeDetection)?;
78 return Ok((content.data, mime));
79 }
80
81 for index_file in &self.config.index_files {
83 if let Some(content) = E::get(index_file) {
84 return Ok((content.data, "text/html"));
85 }
86 }
87
88 Err(SpaError::IndexFileNotFound)
89 }
90}