Skip to main content

rustbasic_core/
view.rs

1/* ---------------------------------------------------------
2 * 📑 LABEL: VIEW ENGINE (config/view.rs)
3 * Mengatur template engine (Minijinja) dan fungsi render.
4 * --------------------------------------------------------- */
5
6use crate::router::{Html, IntoResponse, Response};
7use http::StatusCode;
8use minijinja::Environment;
9use chrono::DateTime;
10use chrono_tz::Tz;
11use std::sync::LazyLock;
12use crate::requests::Request as AppRequest;
13use crate::Config;
14use serde_json::{json, Value};
15
16use crate::tracing;
17
18// 1. Load Static Assets into Memory
19static HTMX_SRC: LazyLock<String> = LazyLock::new(|| {
20    include_str!("../resources/js/htmx.min.js").to_string()
21});
22
23static CSS_SRC: LazyLock<String> = LazyLock::new(|| {
24    include_str!("../resources/css/style.css").to_string()
25});
26
27
28static EMBEDDED_TEMPLATES_GET: std::sync::OnceLock<fn(&str) -> Option<rust_embed::EmbeddedFile>> = std::sync::OnceLock::new();
29
30pub fn set_embedded_templates(f: fn(&str) -> Option<rust_embed::EmbeddedFile>) {
31    EMBEDDED_TEMPLATES_GET.set(f).ok();
32}
33
34// 2. Setup Engine Template (Minijinja)
35pub static JINJA: LazyLock<Environment<'static>> = LazyLock::new(|| {
36    let mut env = Environment::new();
37    
38    // Default Loader: Mencari di disk (jika debug), lalu fallback ke memori
39    env.set_loader(|name| {
40        let cfg = Config::load();
41        if cfg.app_debug {
42            let path = format!("src/resources/views/{}", name);
43            if let Ok(content) = std::fs::read_to_string(&path) {
44                return Ok(Some(content));
45            }
46        }
47        
48        // Fallback ke embedded templates di memori
49        let file = EMBEDDED_TEMPLATES_GET.get().and_then(|f| f(name));
50        if let Some(file) = file {
51            if let Ok(content) = std::str::from_utf8(&file.data) {
52                return Ok(Some(content.to_string()));
53            }
54        }
55        
56        Ok(None)
57    });
58
59    // --- REGISTER CARBON-LIKE FILTERS ---
60
61    // Filter: {{ date | diff_for_humans }}
62    env.add_filter("diff_for_humans", |value: String| -> String {
63        if let Ok(dt) = DateTime::parse_from_rfc3339(&value) {
64            let now = chrono::Utc::now();
65            let dt_utc = dt.with_timezone(&chrono::Utc);
66            let duration = now.signed_duration_since(dt_utc);
67            let seconds = duration.num_seconds();
68            if seconds < 0 {
69                let seconds = -seconds;
70                if seconds < 60 {
71                    return "in a few seconds".to_string();
72                }
73                let minutes = seconds / 60;
74                if minutes < 60 {
75                    return format!("in {} minute{}", minutes, if minutes > 1 { "s" } else { "" });
76                }
77                let hours = minutes / 60;
78                if hours < 24 {
79                    return format!("in {} hour{}", hours, if hours > 1 { "s" } else { "" });
80                }
81                let days = hours / 24;
82                if days < 30 {
83                    return format!("in {} day{}", days, if days > 1 { "s" } else { "" });
84                }
85                let months = days / 30;
86                if months < 12 {
87                    return format!("in {} month{}", months, if months > 1 { "s" } else { "" });
88                }
89                let years = months / 12;
90                return format!("in {} year{}", years, if years > 1 { "s" } else { "" });
91            } else {
92                if seconds < 60 {
93                    return "a few seconds ago".to_string();
94                }
95                let minutes = seconds / 60;
96                if minutes < 60 {
97                    return format!("{} minute{} ago", minutes, if minutes > 1 { "s" } else { "" });
98                }
99                let hours = minutes / 60;
100                if hours < 24 {
101                    return format!("{} hour{} ago", hours, if hours > 1 { "s" } else { "" });
102                }
103                let days = hours / 24;
104                if days < 30 {
105                    return format!("{} day{} ago", days, if days > 1 { "s" } else { "" });
106                }
107                let months = days / 30;
108                if months < 12 {
109                    return format!("{} month{} ago", months, if months > 1 { "s" } else { "" });
110                }
111                let years = months / 12;
112                return format!("{} year{} ago", years, if years > 1 { "s" } else { "" });
113            }
114        }
115        value
116    });
117
118    // Filter: {{ date | format_date("%d %b %Y") }}
119    env.add_filter("format_date", |value: String, fmt: String| -> String {
120        let cfg = Config::load();
121        let tz_str = cfg.app_timezone.trim();
122        let tz: Tz = tz_str.parse().unwrap_or(chrono_tz::UTC);
123        
124        if let Ok(dt) = DateTime::parse_from_rfc3339(&value) {
125             return dt.with_timezone(&tz).format(&fmt).to_string();
126        }
127        value
128    });
129
130    // Global Function: {{ now() }}
131    env.add_function("now", || -> String {
132        let cfg = Config::load();
133        let tz_str = cfg.app_timezone.trim();
134        let tz: Tz = tz_str.parse().unwrap_or(chrono_tz::UTC);
135        
136        chrono::Utc::now().with_timezone(&tz).to_rfc3339()
137    });
138
139    // Global Function: {{ htmx_js() }}
140    env.add_function("htmx_js", || -> String {
141        HTMX_SRC.clone()
142    });
143
144    // Global Function: {{ app_css() }}
145    env.add_function("app_css", || -> String {
146        CSS_SRC.clone()
147    });
148
149    env
150});
151
152
153// 3. Fungsi Helper untuk Render HTML Statis
154pub fn render(template: &str, context: minijinja::Value) -> Response {
155    render_internal(template, context)
156}
157
158pub fn render_to_string(template: &str, context: minijinja::Value) -> String {
159    match JINJA.get_template(template) {
160        Ok(tmpl) => tmpl.render(context).unwrap_or_else(|e| format!("Render error: {}", e)),
161        Err(e) => format!("Template error: {}", e),
162    }
163}
164
165// 4. Fungsi Helper untuk Render dengan Session
166pub fn view(req: &AppRequest, template: &str, ctx: minijinja::Value) -> Response {
167    let mut ctx_value = serde_json::to_value(&ctx).unwrap_or_else(|_| json!({}));
168    
169    if !ctx_value.is_object() {
170        ctx_value = json!({});
171    }
172    
173    let obj = ctx_value.as_object_mut().unwrap();
174
175    // Default keys for flash messages
176    if !obj.contains_key("errors") { obj.insert("errors".to_string(), json!({})); }
177    if !obj.contains_key("old") { obj.insert("old".to_string(), json!({})); }
178    if !obj.contains_key("flash_success") { obj.insert("flash_success".to_string(), json!("")); }
179    if !obj.contains_key("flash_error") { obj.insert("flash_error".to_string(), json!("")); }
180
181    if let Some(success) = req.session.get::<String>("flash_success") {
182        obj.insert("flash_success".to_string(), json!(success));
183        req.session.remove("flash_success");
184    }
185    if let Some(error) = req.session.get::<String>("flash_error") {
186        obj.insert("flash_error".to_string(), json!(error));
187        req.session.remove("flash_error");
188    }
189    if let Some(errors) = req.session.get::<Value>("errors") {
190        obj.insert("errors".to_string(), errors);
191        req.session.remove("errors");
192    }
193    if let Some(old) = req.session.get::<Value>("old_input") {
194        obj.insert("old".to_string(), old);
195    }
196
197    if let Some(token) = req.session.get::<String>("_token") {
198        obj.insert("csrf_token".to_string(), json!(token));
199    }
200
201    let is_logged_in = req.session.get::<i64>("user_id").is_some();
202    obj.insert("auth".to_string(), json!(is_logged_in));
203
204    render_internal(template, minijinja::Value::from_serialize(obj))
205}
206
207fn render_internal(template: &str, context: minijinja::Value) -> Response {
208    let cfg = crate::Config::load();
209    tracing::debug!("Rendering template: {} (APP_DEBUG: {})", template, cfg.app_debug);
210
211    match JINJA.get_template(template) {
212        Ok(tmpl) => match tmpl.render(context.clone()) {
213            Ok(rendered) => {
214                Html(rendered).into_response()
215            },
216            Err(err) => {
217                tracing::error!("Gagal render template: {}", err);
218                
219                if cfg.app_debug {
220                    return (StatusCode::INTERNAL_SERVER_ERROR, format!("Render Error: {}", err)).into_response();
221                }
222
223                (StatusCode::INTERNAL_SERVER_ERROR, "Internal Server Error").into_response()
224            }
225        },
226        Err(err) => {
227            tracing::error!("Template tidak ditemukan: {}", err);
228
229            if cfg.app_debug {
230                return (StatusCode::NOT_FOUND, format!("Template Not Found: {}", err)).into_response();
231            }
232
233            (StatusCode::NOT_FOUND, "Not Found").into_response()
234        }
235    }
236}