Skip to main content

anycms_spa/core/
mod.rs

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/// SPA 配置
21#[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
54/// SPA 处理器
55pub 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    /// 获取嵌入的文件(考虑基础路径)
69    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        // SPA fallback:尝试索引文件
82        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}