vertigo/driver_module/
driver.rs

1use crate::{
2    css::css_manager::CssManager,
3    fetch::request_builder::{RequestBody, RequestBuilder},
4    Context, Css, Dependencies, DropResource, FutureBox, Instant, JsJson, WebsocketMessage,
5};
6use std::cell::RefCell;
7use std::{future::Future, pin::Pin, rc::Rc};
8
9use crate::driver_module::dom::DriverDom;
10use crate::{driver_module::api::ApiImport, driver_module::utils::futures_spawn::spawn_local};
11
12use super::api::DomAccess;
13
14#[derive(Debug, Clone, Copy)]
15pub enum FetchMethod {
16    GET,
17    HEAD,
18    POST,
19    PUT,
20    DELETE,
21    CONNECT,
22    OPTIONS,
23    TRACE,
24    PATCH,
25}
26
27impl FetchMethod {
28    pub fn to_str(&self) -> String {
29        match self {
30            Self::GET => "GET",
31            Self::HEAD => "HEAD",
32            Self::POST => "POST",
33            Self::PUT => "PUT",
34            Self::DELETE => "DELETE",
35            Self::CONNECT => "CONNECT",
36            Self::OPTIONS => "OPTIONS",
37            Self::TRACE => "TRACE",
38            Self::PATCH => "PATCH",
39        }
40        .into()
41    }
42}
43
44type Executable = dyn Fn(Pin<Box<dyn Future<Output = ()> + 'static>>);
45type PlainHandler = dyn Fn(&str) -> Option<String>;
46
47pub struct DriverInner {
48    pub(crate) api: ApiImport,
49    pub(crate) dependencies: &'static Dependencies,
50    pub(crate) css_manager: CssManager,
51    pub(crate) dom: &'static DriverDom,
52    spawn_executor: Rc<Executable>,
53    _subscribe: DropResource,
54    _plains_handler: RefCell<Option<Rc<PlainHandler>>>,
55}
56
57impl DriverInner {
58    pub fn new() -> &'static Self {
59        let dependencies: &'static Dependencies = Box::leak(Box::default());
60
61        let api = ApiImport::default();
62
63        let spawn_executor = {
64            let api = api.clone();
65
66            Rc::new(move |fut: Pin<Box<dyn Future<Output = ()> + 'static>>| {
67                spawn_local(api.clone(), fut);
68            })
69        };
70
71        let dom = DriverDom::new(&api);
72        let css_manager = {
73            let driver_dom = dom;
74            CssManager::new(move |selector: &str, value: &str| {
75                driver_dom.insert_css(selector, value);
76            })
77        };
78
79        let subscribe = dependencies.hooks.on_after_transaction(move || {
80            dom.flush_dom_changes();
81        });
82
83        Box::leak(Box::new(DriverInner {
84            api,
85            dependencies,
86            css_manager,
87            dom,
88            spawn_executor,
89            _subscribe: subscribe,
90            _plains_handler: RefCell::new(None),
91        }))
92    }
93}
94
95/// Result from request made using [RequestBuilder].
96///
97/// Variants:
98/// - `Ok(status_code, response)` if request succeeded,
99/// - `Err(response)` if request failed (because of network error for example).
100pub type FetchResult = Result<(u32, RequestBody), String>;
101
102/// Main connection to vertigo facilities - dependencies and rendering client (the browser).
103#[derive(Clone, Copy)]
104pub struct Driver {
105    pub(crate) inner: &'static DriverInner,
106}
107
108impl Default for Driver {
109    fn default() -> Self {
110        let driver = DriverInner::new();
111
112        Driver { inner: driver }
113    }
114}
115
116impl Driver {
117    /// Gets a cookie by name
118    pub fn cookie_get(&self, cname: &str) -> String {
119        self.inner.api.cookie_get(cname)
120    }
121
122    /// Gets a JsJson cookie by name
123    pub fn cookie_get_json(&self, cname: &str) -> JsJson {
124        self.inner.api.cookie_get_json(cname)
125    }
126
127    /// Sets a cookie under provided name
128    pub fn cookie_set(&self, cname: &str, cvalue: &str, expires_in: u64) {
129        self.inner.api.cookie_set(cname, cvalue, expires_in)
130    }
131
132    /// Sets a cookie under provided name
133    pub fn cookie_set_json(&self, cname: &str, cvalue: JsJson, expires_in: u64) {
134        self.inner.api.cookie_set_json(cname, cvalue, expires_in)
135    }
136
137    /// Go back in client's (browser's) history
138    pub fn history_back(&self) {
139        self.inner.api.history_back();
140    }
141
142    /// Replace current location
143    pub fn history_replace(&self, new_url: &str) {
144        self.inner.api.replace_history_location(new_url)
145    }
146
147    /// Make `func` fire every `time` seconds.
148    #[must_use]
149    pub fn set_interval(&self, time: u32, func: impl Fn() + 'static) -> DropResource {
150        self.inner.api.interval_set(time, func)
151    }
152
153    /// Gets current value of monotonic clock.
154    pub fn now(&self) -> Instant {
155        Instant::now(self.inner.api.clone())
156    }
157
158    /// Gets current UTC timestamp
159    pub fn utc_now(&self) -> i64 {
160        self.inner.api.utc_now()
161    }
162
163    /// Gets browsers time zone offset in seconds
164    ///
165    /// Compatible with chrono's `FixedOffset::east_opt` method.
166    pub fn timezone_offset(&self) -> i32 {
167        self.inner.api.timezone_offset()
168    }
169
170    /// Create new RequestBuilder for GETs (more complex version of [fetch](struct.Driver.html#method.fetch))
171    #[must_use]
172    pub fn request_get(&self, url: impl Into<String>) -> RequestBuilder {
173        RequestBuilder::get(url)
174    }
175
176    /// Create new RequestBuilder for POSTs (more complex version of [fetch](struct.Driver.html#method.fetch))
177    #[must_use]
178    pub fn request_post(&self, url: impl Into<String>) -> RequestBuilder {
179        RequestBuilder::post(url)
180    }
181
182    #[must_use]
183    pub fn sleep(&self, time: u32) -> FutureBox<()> {
184        let (sender, future) = FutureBox::new();
185        self.inner.api.set_timeout_and_detach(time, move || {
186            sender.publish(());
187        });
188
189        future
190    }
191
192    pub fn get_random(&self, min: u32, max: u32) -> u32 {
193        self.inner.api.get_random(min, max)
194    }
195
196    pub fn get_random_from<K: Clone>(&self, list: &[K]) -> Option<K> {
197        let len = list.len();
198
199        if len < 1 {
200            return None;
201        }
202
203        let max_index = len - 1;
204
205        let index = self.get_random(0, max_index as u32);
206        Some(list[index as usize].clone())
207    }
208
209    /// Initiate a websocket connection. Provided callback should handle a single [WebsocketMessage].
210    #[must_use]
211    pub fn websocket<F: Fn(WebsocketMessage) + 'static>(
212        &self,
213        host: impl Into<String>,
214        callback: F,
215    ) -> DropResource {
216        self.inner.api.websocket(host, callback)
217    }
218
219    /// Spawn a future - thus allowing to fire async functions in, for example, event handler. Handy when fetching resources from internet.
220    pub fn spawn(&self, future: impl Future<Output = ()> + 'static) {
221        let future = Box::pin(future);
222        let spawn_executor = self.inner.spawn_executor.clone();
223        spawn_executor(future);
224    }
225
226    /// Fire provided function in a way that all changes in [dependency graph](struct.Dependencies.html) made by this function
227    /// will trigger only one run of updates, just like the changes were done all at once.
228    pub fn transaction<R, F: FnOnce(&Context) -> R>(&self, func: F) -> R {
229        self.inner.dependencies.transaction(func)
230    }
231
232    pub fn dom_access(&self) -> DomAccess {
233        self.inner.api.dom_access()
234    }
235
236    /// Function added for diagnostic purposes. It allows you to check whether a block with a transaction is missing somewhere.
237    pub fn on_after_transaction(&self, callback: impl Fn() + 'static) -> DropResource {
238        self.inner.dependencies.hooks.on_after_transaction(callback)
239    }
240
241    /// Return true if the code is executed client-side (in the browser).
242    ///
243    /// ```rust
244    /// use vertigo::{dom, get_driver};
245    ///
246    /// let component = if get_driver().is_browser() {
247    ///     dom! { <div>"My dynamic component"</div> }
248    /// } else {
249    ///     dom! { <div>"Loading... (if not loaded check if JavaScript is enabled)"</div> }
250    /// };
251    /// ```
252    pub fn is_browser(&self) -> bool {
253        self.inner.api.is_browser()
254    }
255
256    pub fn is_server(&self) -> bool {
257        !self.is_browser()
258    }
259
260    pub fn env(&self, name: impl Into<String>) -> Option<String> {
261        let name = name.into();
262        self.inner.api.get_env(name)
263    }
264
265    /// Register handler that intercepts defined urls and generates plaintext responses during SSR.
266    ///
267    /// Return None in the handler if regular HTML should be generated by the App.
268    ///
269    /// ```rust
270    /// use vertigo::get_driver;
271    ///
272    /// get_driver().plains(|url| {
273    ///    if url == "/robots.txt" {
274    ///       Some("User-Agent: *\nDisallow: /search".to_string())
275    ///    } else {
276    ///       None
277    ///    }
278    /// });
279    /// ```
280    pub fn plains(&mut self, callback: impl Fn(&str) -> Option<String> + 'static) {
281        let mut mut_plains = self.inner._plains_handler.borrow_mut();
282        *mut_plains = Some(Rc::new(callback));
283    }
284
285    pub fn try_get_plain(&self) {
286        if self.is_server() {
287            let url = self.inner.api.get_history_location();
288            match self.inner._plains_handler.try_borrow() {
289                Ok(callback_ref) => {
290                    if let Some(callback) = callback_ref.as_deref() {
291                        if let Some(body) = callback(&url) {
292                            self.inner.api.plain_response(body)
293                        }
294                    }
295                }
296                Err(err) => log::error!("Error invoking plains: {err}"),
297            }
298        } else {
299            log::info!("Browser mode, not invoking try_get_plain");
300        }
301    }
302
303    /// Allow to set custom HTTP status code during SSR
304    ///
305    /// ```rust
306    /// use vertigo::get_driver;
307    ///
308    /// get_driver().set_status(404)
309    /// ```
310    pub fn set_status(&self, status: u16) {
311        if self.is_server() {
312            self.inner.api.set_status(status);
313        }
314    }
315
316    /// Adds this CSS to manager producing a class name, which is returned
317    ///
318    /// There shouldn't be need to use it manually. It's used by `css!` macro.
319    pub fn class_name_for(&mut self, css: &Css) -> String {
320        self.inner.css_manager.get_class_name(css)
321    }
322}