1pub mod path;
2
3
4use rust_embed::RustEmbed;
5use thiserror::Error;
6use std::borrow::Cow;
7
8#[derive(Debug, Error)]
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)]
22pub struct SpaConfig {
23 pub base_path: String,
24 pub index_files: Vec<String>,
25}
26
27impl Default for SpaConfig {
28 fn default() -> Self {
29 SpaConfig {
30 base_path: "/".to_string(),
31 index_files: vec!["index.html".to_string()],
32 }
33 }
34}
35
36impl SpaConfig {
37 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 {
45 self.index_files = files.iter().map(|s| s.to_string()).collect();
46 self
47 }
48
49 pub fn add_index_file(mut self, file: &str) -> Self {
51 self.index_files.push(file.to_string());
52 self
53 }
54}
55
56pub struct SpaHandler<E: RustEmbed> {
58 config: SpaConfig,
59 _marker: std::marker::PhantomData<E>,
60}
61
62impl<E: RustEmbed> SpaHandler<E> {
63 pub fn new(config: SpaConfig) -> Self {
64 Self {
65 config,
66 _marker: std::marker::PhantomData,
67 }
68 }
69
70 pub fn get_file(&self, request_path: &str) -> Result<(Cow<'static, [u8]>, &'static str), SpaError> {
72 let normalized_path = crate::core::path::normalize_path(request_path)?;
74 let resource_path = crate::core::path::relative_to_base(&normalized_path, &self.config.base_path);
76 if let Some(content) = E::get(&resource_path) {
78 let mime = mime_guess::from_path(&resource_path)
79 .first_raw()
80 .ok_or(SpaError::MimeDetection)?;
81 return Ok((content.data, mime));
82 }
83
84 for index_file in &self.config.index_files {
86 if let Some(content) = E::get(index_file) {
87 return Ok((content.data, "text/html"));
88 }
89 }
90
91 if let Some(content) = E::get("index.html") {
93 return Ok((content.data, "text/html"));
94 }
95
96 Err(SpaError::IndexFileNotFound)
97 }
98}