Skip to content

Commit 11fe5f5

Browse files
add IPC PWA example
1 parent 6bfe341 commit 11fe5f5

File tree

6 files changed

+438
-0
lines changed

6 files changed

+438
-0
lines changed
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
[package]
2+
name = "pwa-actix"
3+
version = "0.1.0"
4+
edition = "2021"
5+
6+
[dependencies]
7+
neutralipcrs = "1.3.0-beta1"
8+
actix-web = "4.4"
9+
actix-files = "0.6"
10+
serde = { version = "1.0", features = ["derive"] }
11+
serde_json = "1.0"
12+
tokio = { version = "1.0", features = ["full"] }
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
Neutral TS example with Actix Web
2+
=================================
3+
4+
Example of using Neutral Template System with Web APP and different themes.
5+
6+
Navigate to the examples/rust/pwa-actix directory and then:
7+
8+
```
9+
cargo run --release
10+
```
11+
12+
A server will be available on port 9090
13+
14+
```
15+
http://127.0.0.1:9090/
16+
```
17+
18+
Links
19+
-----
20+
21+
Neutral TS template engine.
22+
23+
- [Template docs](https://franbarinstance.github.io/neutralts-docs/docs/neutralts/doc/)
24+
- [Repository](https://github.com/FranBarInstance/neutralts)
25+
- [Crate](https://crates.io/crates/neutralts)
26+
- [Examples](https://github.com/FranBarInstance/neutralts-docs/tree/master/examples)
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
//! Constants
2+
//! See: https://github.com/FranBarInstance/neutralts-docs
3+
4+
// Use cached templates
5+
pub const TEMPLATE_ROUTER: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/../../neutral/tpl/cache.ntpl");
6+
7+
// Uncomment for no cached templates
8+
// pub const TEMPLATE_ROUTER: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/../../neutral/tpl/index.ntpl");
9+
10+
// HTTP errors template
11+
pub const TEMPLATE_ERROR: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/../../neutral/tpl/cache_error.ntpl");
12+
13+
// static files
14+
pub const STATIC_FOLDER: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/../../neutral/static");
15+
16+
// Default schema
17+
pub const DEFAULT_SCHEMA: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/../../neutral/data/schema.json");
18+
19+
pub const LANG_KEY: &str = "lang";
20+
pub const THEME_KEY: &str = "theme";
21+
pub const SIMULATE_SECRET_KEY: &str = "69bdd1e4b4047d8f4e3";
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
//! Neutral TS example with Actix-web
2+
//! See: https://github.com/FranBarInstance/neutralts-docs
3+
4+
use actix_web::{web, App, HttpRequest, HttpResponse, HttpServer, Result};
5+
use actix_files::NamedFile;
6+
use serde_json::Value;
7+
use std::path::Path;
8+
use std::collections::HashMap;
9+
10+
mod constants;
11+
mod schema;
12+
mod template;
13+
14+
use constants::{STATIC_FOLDER, SIMULATE_SECRET_KEY};
15+
use schema::Schema;
16+
use template::Template;
17+
18+
/// Catch all route handler - serves static files and dynamic content (GET only)
19+
///
20+
/// To prevent arbitrary routes contents-[route]-snippets.ntpl must exist,
21+
/// to create a simple view/route you wouldn't need a function or view, just create
22+
/// the contents-[route]-snippets.ntpl template file.
23+
///
24+
/// Following routes do not have a handler and are dispatched here:
25+
/// /simulate-...
26+
/// /help
27+
/// /login
28+
///
29+
/// /login is a container for /form-login (/form-login is loaded via ajax)
30+
async fn catch_all_get(req: HttpRequest, path: web::Path<String>) -> Result<HttpResponse> {
31+
let route = path.into_inner();
32+
33+
// Serve static files directly
34+
let file_path = Path::new(STATIC_FOLDER).join(&route);
35+
if file_path.exists() && file_path.is_file() {
36+
return Ok(NamedFile::open(file_path)?.into_response(&req));
37+
}
38+
39+
// Serve dynamic content
40+
let schema = Schema::new(&req, &route);
41+
let template = Template::new(schema.get().clone());
42+
template.render().map_err(|e| actix_web::error::ErrorInternalServerError(e))
43+
}
44+
45+
/// Display form login (GET)
46+
async fn form_login_get(req: HttpRequest) -> Result<HttpResponse> {
47+
let route = "form-login";
48+
let schema = Schema::new(&req, route);
49+
50+
let template = Template::new(schema.get().clone());
51+
template.render().map_err(|e| actix_web::error::ErrorInternalServerError(e))
52+
}
53+
54+
/// Process login form in POST (Fake login)
55+
async fn form_login_post(req: HttpRequest, form: web::Form<HashMap<String, String>>) -> Result<HttpResponse> {
56+
let route = "form-login";
57+
let mut schema = Schema::new_with_post(&req, route, form);
58+
let current_schema = schema.get_mut();
59+
60+
current_schema["data"]["send_form_login"] = Value::Number(serde_json::Number::from(1));
61+
62+
// Fake login, any user, password: 1234
63+
let passwd = current_schema["data"]["CONTEXT"]["POST"]["passwd"]
64+
.as_str()
65+
.unwrap();
66+
67+
if passwd == "1234" {
68+
current_schema["data"]["send_form_login_fails"] = Value::Null;
69+
current_schema["data"]["CONTEXT"]["SESSION"] = Value::String(SIMULATE_SECRET_KEY.to_string());
70+
} else {
71+
current_schema["data"]["send_form_login_fails"] = Value::Number(serde_json::Number::from(1));
72+
}
73+
74+
let template = Template::new(schema.get().clone());
75+
template.render().map_err(|e| actix_web::error::ErrorInternalServerError(e))
76+
}
77+
78+
// Home GET
79+
async fn home_get(req: HttpRequest) -> Result<HttpResponse> {
80+
let route = "home";
81+
let schema = Schema::new(&req, route);
82+
let template = Template::new(schema.get().clone());
83+
template.render().map_err(|e| actix_web::error::ErrorInternalServerError(e))
84+
}
85+
86+
// Home POST
87+
async fn home_post(req: HttpRequest, form: web::Form<HashMap<String, String>>) -> Result<HttpResponse> {
88+
let route = "home";
89+
let schema = Schema::new_with_post(&req, route, form);
90+
let template = Template::new(schema.get().clone());
91+
template.render().map_err(|e| actix_web::error::ErrorInternalServerError(e))
92+
}
93+
94+
#[actix_web::main]
95+
async fn main() -> std::io::Result<()> {
96+
println!("Starting Neutral TS PWA Actix server on http://127.0.0.1:9090");
97+
98+
HttpServer::new(|| {
99+
App::new()
100+
.route("/", web::get().to(home_get))
101+
.route("/", web::post().to(home_post))
102+
.route("/form-login", web::get().to(form_login_get))
103+
.route("/form-login", web::post().to(form_login_post))
104+
.route("/{path:.*}", web::get().to(catch_all_get))
105+
})
106+
.bind("127.0.0.1:9090")?
107+
.run()
108+
.await
109+
}
Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
//! Schema
2+
//! See: https://github.com/FranBarInstance/neutralts-docs
3+
4+
use actix_web::HttpRequest;
5+
use actix_web::web;
6+
use serde_json::{json, Value};
7+
use std::fs;
8+
use std::collections::HashMap;
9+
use crate::constants::{DEFAULT_SCHEMA, LANG_KEY, THEME_KEY};
10+
11+
// It is important to distinguish between data coming from the user and data
12+
// coming from the application. "CONTEXT" has some security measures such as escaping.
13+
pub struct Schema {
14+
req: HttpRequest,
15+
route: String,
16+
schema: Value,
17+
}
18+
19+
impl Schema {
20+
pub fn new(req: &HttpRequest, route: &str) -> Self {
21+
let mut schema = Self {
22+
req: req.clone(),
23+
route: route.trim_matches('/').to_string(),
24+
schema: Value::Null,
25+
};
26+
schema.default();
27+
schema.populate_context(None);
28+
schema.negotiate_language();
29+
schema.set_theme();
30+
schema
31+
}
32+
33+
pub fn new_with_post(req: &HttpRequest, route: &str, form: web::Form<HashMap<String, String>>) -> Self {
34+
let mut schema = Self {
35+
req: req.clone(),
36+
route: route.trim_matches('/').to_string(),
37+
schema: Value::Null,
38+
};
39+
schema.default();
40+
// Extraer el HashMap interno del Form
41+
schema.populate_context(Some(form.into_inner()));
42+
schema.negotiate_language();
43+
schema.set_theme();
44+
schema
45+
}
46+
47+
fn default(&mut self) {
48+
let schema_content = fs::read_to_string(DEFAULT_SCHEMA)
49+
.expect("Failed to read default schema");
50+
self.schema = serde_json::from_str(&schema_content)
51+
.expect("Failed to parse default schema");
52+
53+
// Ensure required structure exists
54+
if self.schema.get("data").is_none() {
55+
self.schema["data"] = json!({});
56+
}
57+
if self.schema["data"].get("CONTEXT").is_none() {
58+
self.schema["data"]["CONTEXT"] = json!({});
59+
}
60+
if self.schema["data"]["CONTEXT"].get("GET").is_none() {
61+
self.schema["data"]["CONTEXT"]["GET"] = json!({});
62+
}
63+
if self.schema["data"]["CONTEXT"].get("POST").is_none() {
64+
self.schema["data"]["CONTEXT"]["POST"] = json!({});
65+
}
66+
if self.schema["data"]["CONTEXT"].get("COOKIES").is_none() {
67+
self.schema["data"]["CONTEXT"]["COOKIES"] = json!({});
68+
}
69+
if self.schema["data"]["CONTEXT"].get("HEADERS").is_none() {
70+
self.schema["data"]["CONTEXT"]["HEADERS"] = json!({});
71+
}
72+
}
73+
74+
fn populate_context(&mut self, post_data: Option<HashMap<String, String>>) {
75+
self.schema["data"]["CONTEXT"]["ROUTE"] = json!(self.route);
76+
77+
// Get Host header
78+
if let Some(host) = self.req.headers().get("Host") {
79+
if let Ok(host_str) = host.to_str() {
80+
self.schema["data"]["CONTEXT"]["HEADERS"]["HOST"] = json!(host_str);
81+
}
82+
}
83+
84+
// Parse query parameters (GET)
85+
if let Some(query) = self.req.uri().query() {
86+
let params: Vec<&str> = query.split('&').collect();
87+
for param in params {
88+
if let Some(eq_pos) = param.find('=') {
89+
let key = &param[..eq_pos];
90+
let value = &param[eq_pos + 1..];
91+
self.schema["data"]["CONTEXT"]["GET"][key] = json!(value);
92+
}
93+
}
94+
}
95+
96+
// Parse POST data if available
97+
if let Some(post_params) = post_data {
98+
for (key, value) in post_params {
99+
self.schema["data"]["CONTEXT"]["POST"][key] = json!(value);
100+
}
101+
}
102+
103+
// Parse headers
104+
for (key, value) in self.req.headers() {
105+
if let Ok(value_str) = value.to_str() {
106+
self.schema["data"]["CONTEXT"]["HEADERS"][key.as_str()] = json!(value_str);
107+
}
108+
}
109+
110+
// Parse cookies
111+
if let Some(cookie_header) = self.req.headers().get("Cookie") {
112+
if let Ok(cookie_str) = cookie_header.to_str() {
113+
for cookie in cookie_str.split(';') {
114+
let cookie = cookie.trim();
115+
if let Some(eq_pos) = cookie.find('=') {
116+
let key = &cookie[..eq_pos];
117+
let value = &cookie[eq_pos + 1..];
118+
self.schema["data"]["CONTEXT"]["COOKIES"][key] = json!(value);
119+
}
120+
}
121+
}
122+
}
123+
124+
// Fake session
125+
let session = self.schema["data"]["CONTEXT"]["COOKIES"]
126+
.get("SESSION")
127+
.and_then(|v| v.as_str())
128+
.map(|s| s.to_string());
129+
if session.is_some() {
130+
self.schema["data"]["CONTEXT"]["SESSION"] = json!(session.unwrap());
131+
}
132+
}
133+
134+
fn negotiate_language(&mut self) {
135+
let languages = self.schema["data"]["site"]["validLanguages"].clone();
136+
let empty_vec = vec![];
137+
let languages_array = languages.as_array().unwrap_or(&empty_vec);
138+
let languages_vec: Vec<String> = languages_array
139+
.iter()
140+
.filter_map(|v| v.as_str().map(|s| s.to_string()))
141+
.collect();
142+
143+
// Get language from query params, cookies, or default to first language
144+
let lang_from_get = self.schema["data"]["CONTEXT"]["GET"]
145+
.get(LANG_KEY)
146+
.and_then(|v| v.as_str())
147+
.map(|s| s.to_string());
148+
149+
let lang_from_cookies = self.schema["data"]["CONTEXT"]["COOKIES"]
150+
.get(LANG_KEY)
151+
.and_then(|v| v.as_str())
152+
.map(|s| s.to_string());
153+
154+
let current_lang = lang_from_get
155+
.or(lang_from_cookies)
156+
.filter(|lang| languages_vec.contains(lang))
157+
.unwrap_or_else(|| languages_vec.first().cloned().unwrap_or_default());
158+
159+
self.schema["inherit"]["locale"]["current"] = json!(current_lang);
160+
}
161+
162+
fn set_theme(&mut self) {
163+
let theme_from_get = self.schema["data"]["CONTEXT"]["GET"]
164+
.get(THEME_KEY)
165+
.and_then(|v| v.as_str())
166+
.map(|s| s.to_string());
167+
168+
let theme_from_cookies = self.schema["data"]["CONTEXT"]["COOKIES"]
169+
.get(THEME_KEY)
170+
.and_then(|v| v.as_str())
171+
.map(|s| s.to_string());
172+
173+
let valid_themes = self.schema["data"]["site"]["validThemes"].clone();
174+
let empty_vec = vec![];
175+
let valid_themes_array = valid_themes.as_array().unwrap_or(&empty_vec);
176+
let default_theme = valid_themes_array
177+
.first()
178+
.and_then(|v| v.as_str())
179+
.unwrap_or("default");
180+
181+
let current_theme = theme_from_get
182+
.or(theme_from_cookies)
183+
.filter(|theme| valid_themes_array.contains(&json!(theme)))
184+
.unwrap_or_else(|| default_theme.to_string());
185+
186+
self.schema["data"]["site"]["theme"] = json!(current_theme);
187+
}
188+
189+
pub fn get_mut(&mut self) -> &mut Value {
190+
&mut self.schema
191+
}
192+
193+
pub fn get(&self) -> &Value {
194+
&self.schema
195+
}
196+
}

0 commit comments

Comments
 (0)