spa_server/lib.rs
1//! spa-server is a library used to embed all the SPA web application files, and release as a single binary executable.
2//! it based-on [actix-web](https://crates.io/crates/actix-web) and [rust-embed](https://crates.io/crates/rust-embed)
3//!
4//! works in proc macro way, example:
5//! ```
6//! #[derive(SPAServer)]
7//! #[spa_server(
8//! static_files = "ui/dist/ui", # SPA dist dir, all the files will be packad into binary
9//! apis( # define apis that SPA application interacting with
10//! api( # define a api group
11//! prefix = "/api/v1", # prefix for this api group
12//! v1::foo, # api function
13//! v1::bar,
14//! ),
15//! api(
16//! prefix = "/api/v2",
17//! v2::foo,
18//! v2::bar,
19//! ),
20//! api(test), # api without prefix
21//! ),
22//! cors, # enable cors permissive for debug
23//! identity(name = "a", age = 30) # identity support, cookie name and age in minutes
24//! )]
25//! pub struct Server {
26//! data: String,
27//! num: u32,
28//! }
29//!
30//! mod v1 {
31//! use spa_server::re_export::*; # use actix-web symbol directly
32//!
33//! #[get("foo")] # match route /api/v1/foo
34//! async fn foo(s: web::Data<Server>) -> Result<HttpResponse> {
35//! let data = s.data; # web context stored in struct
36//! let num = s.num;
37//! Ok(HttpResponse::Ok().finish())
38//! }
39//! }
40//!
41//! #[spa_server::main] # replace actix_web::main with spa_server::main
42//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
43//! Server {
44//! data: String::new(), num: 1234
45//! }
46//! .run(8080) # listen at 0.0.0.0::8080
47//! .await?;
48//! Ok(())
49//! }
50//!
51//! ```
52//! access http://localhost:8080 will show the SPA index.html page
53
54/// re-export all the pub symbols from actix-web, no need to add additional
55/// [actix-web](https://crates.io/crates/actix-web) dependency in Cargo.toml.
56pub mod re_export {
57 pub use actix_cors::*;
58 pub use actix_files::*;
59 pub use actix_identity::*;
60 pub use actix_web::*;
61 pub use spa_server_derive::connect;
62 pub use spa_server_derive::delete;
63 pub use spa_server_derive::get;
64 pub use spa_server_derive::head;
65 pub use spa_server_derive::main;
66 pub use spa_server_derive::options;
67 pub use spa_server_derive::patch;
68 pub use spa_server_derive::post;
69 pub use spa_server_derive::put;
70 pub use spa_server_derive::trace;
71}
72
73#[doc(hidden)]
74pub use include_flate::flate;
75
76/// use spa_server::main replaced actix_web::main
77pub use spa_server_derive::main;
78
79/// convert actix_web error to 200 OK and json body
80/// ```
81/// #[error_to_json]
82/// #[get("/index")]
83/// async fn test() -> Result<HttpResponse> {
84/// Err(ErrorInternalServerError("some error"))
85/// }
86/// ```
87/// will convert to
88/// ```
89/// #[get("index")]
90/// async fn test() -> Result<HttpResponse> {
91/// Ok(HttpResponse::Ok().body(r#"{"errors":[{"detail": "some error"}]}"#))
92/// }
93/// ```
94pub use spa_server_derive::error_to_json;
95pub use spa_server_derive::SPAServer;
96
97#[doc(hidden)]
98pub use time::Duration;
99
100use log::{debug, warn};
101use rand::{distributions::Alphanumeric, thread_rng, Rng};
102use re_export::*;
103use serde::Serialize;
104use std::{
105 borrow::{Borrow, Cow},
106 collections::HashMap,
107 env::temp_dir,
108 fs::create_dir_all,
109 path::{Path, PathBuf},
110};
111
112#[doc(hidden)]
113#[actix_web::get("/{tail:[^\\.]+}")]
114async fn index(path: web::Data<PathBuf>) -> actix_web::Result<NamedFile> {
115 Ok(NamedFile::open(path.join("index.html"))?)
116}
117
118
119#[doc(hidden)]
120pub fn release_asset<T>() -> Result<PathBuf>
121where
122 T: Embed,
123{
124 let target_dir = temp_dir().join(
125 thread_rng()
126 .sample_iter(&Alphanumeric)
127 .take(8)
128 .map(char::from)
129 .collect::<String>(),
130 );
131 if !target_dir.exists() {
132 create_dir_all(&target_dir)?;
133 }
134
135 debug!("release asset target dir: {}", target_dir.to_string_lossy());
136
137 for file in T::iter() {
138 match T::get(file.borrow()) {
139 None => {
140 warn!("assert file {} not found", file);
141 }
142 Some(p) => {
143 if let Some(i) = Path::new(file.as_ref()).parent() {
144 let sub_dir = target_dir.join(i);
145 create_dir_all(sub_dir)?;
146 } else {
147 warn!("no parent part for file {}", file);
148 continue;
149 }
150
151 let path = target_dir.join(file.as_ref());
152 debug!("release asset file: {}", path.to_string_lossy());
153 if let Err(e) = std::fs::write(path, p) {
154 warn!("asset file {} write failed: {}", file, e);
155 }
156 }
157 }
158 }
159
160 Ok(target_dir)
161}
162#[doc(hidden)]
163pub trait Embed {
164 /// Given a relative path from the assets folder, returns the bytes if found.
165 ///
166 /// If the feature `debug-embed` is enabled or the binary is compiled in
167 /// release mode, the bytes have been embeded in the binary and a
168 /// `Cow::Borrowed(&'static [u8])` is returned.
169 ///
170 /// Otherwise, the bytes are read from the file system on each call and a
171 /// `Cow::Owned(Vec<u8>)` is returned.
172 fn get(file_path: &str) -> Option<Cow<'static, [u8]>>;
173
174 /// Iterates the files in this assets folder.
175 ///
176 /// If the feature `debug-embed` is enabled or the binary is compiled in
177 /// release mode, a static array to the list of relative paths to the files
178 /// is used.
179 ///
180 /// Otherwise, the files are listed from the file system on each call.
181 fn iter() -> Filenames;
182}
183#[doc(hidden)]
184pub struct Filenames(pub std::slice::Iter<'static, &'static str>);
185
186impl Iterator for Filenames {
187 type Item = Cow<'static, str>;
188
189 fn next(&mut self) -> Option<Self::Item> {
190 self.0.next().map(|x| Cow::from(*x))
191 }
192}
193#[doc(hidden)]
194#[derive(Serialize)]
195pub struct ErrorMsg {
196 errors: Vec<HashMap<String, String>>,
197}
198
199#[doc(hidden)]
200pub fn quick_err(msg: impl Into<String>) -> ErrorMsg {
201 let hm = [("detail".to_string(), msg.into())]
202 .iter()
203 .cloned()
204 .collect();
205 ErrorMsg { errors: vec![hm] }
206}