1//! Datastar's wire, once: the events a server sends, the signals it reads, and the script a page loads. 2//! 3//! No I/O and no runtime. An event is built here and written out as text ([`PatchElements::sse`], 4//! [`PatchSignals::sse`]); an adapter for a server (the Worker's, axum's) puts that text on a response. 5//! Datastar's own types are not used, so nothing here moves when that crate does. 6 7use serde::de::DeserializeOwned; 8use serde::Serialize; 9 10/// The script a page loads. Datastar 1.0.4, the same bytes in every property. 11pub const SCRIPT: &str = include_str!("../datastar.js"); 12 13/// The query parameter a GET request carries its signals in, as JSON. 14pub const QUERY_PARAMETER: &str = "datastar"; 15 16/// The header every Datastar request carries, with the value `true`. 17pub const REQUEST_HEADER: &str = "datastar-request"; 18 19/// The `content-type` of a stream of events. 20pub const EVENT_STREAM: &str = "text/event-stream"; 21 22/// How patched elements meet the page. 23#[derive(Clone, Copy, Debug, PartialEq, Eq)] 24pub enum Mode { 25 /// Morph the element with the same id (Datastar's default). 26 Outer, 27 /// Morph the inside of the selected element. 28 Inner, 29 Replace, 30 Prepend, 31 Append, 32 Before, 33 After, 34 Remove, 35} 36 37impl Mode { 38 fn word(self) -> &'static str { 39 match self { 40 Mode::Outer => "outer", 41 Mode::Inner => "inner", 42 Mode::Replace => "replace", 43 Mode::Prepend => "prepend", 44 Mode::Append => "append", 45 Mode::Before => "before", 46 Mode::After => "after", 47 Mode::Remove => "remove", 48 } 49 } 50} 51 52/// Elements to put into the page. 53#[derive(Clone, Debug, PartialEq, Eq)] 54pub struct PatchElements { 55 html: String, 56 selector: Option<String>, 57 mode: Option<Mode>, 58} 59 60impl PatchElements { 61 /// Elements that carry ids, which Datastar morphs in place. 62 pub fn new(html: impl Into<String>) -> PatchElements { 63 PatchElements { html: html.into(), selector: None, mode: None } 64 } 65 66 /// Takes away what the selector matches. 67 pub fn remove(selector: impl Into<String>) -> PatchElements { 68 PatchElements { html: String::new(), selector: Some(selector.into()), mode: Some(Mode::Remove) } 69 } 70 71 pub fn selector(mut self, selector: impl Into<String>) -> PatchElements { 72 self.selector = Some(selector.into()); 73 self 74 } 75 76 pub fn mode(mut self, mode: Mode) -> PatchElements { 77 self.mode = Some(mode); 78 self 79 } 80 81 /// The event as stream text. Each line of the markup is its own `elements` line, so a newline in 82 /// the markup (inside a `<pre>`) reaches the page as it was written. 83 pub fn sse(&self) -> String { 84 let mut out = String::from("event: datastar-patch-elements\n"); 85 if let Some(selector) = &self.selector { 86 out.push_str(&format!("data: selector {}\n", one_line(selector))); 87 } 88 if let Some(mode) = self.mode { 89 out.push_str(&format!("data: mode {}\n", mode.word())); 90 } 91 for line in self.html.lines() { 92 out.push_str("data: elements "); 93 out.push_str(line); 94 out.push('\n'); 95 } 96 out.push('\n'); 97 out 98 } 99} 100 101/// Signals to merge into the page's. 102#[derive(Clone, Debug, PartialEq, Eq)] 103pub struct PatchSignals { 104 json: String, 105 only_if_missing: bool, 106} 107 108impl PatchSignals { 109 /// Signals from anything that serializes to a JSON object. A `null` member removes that signal. 110 pub fn of(signals: &impl Serialize) -> Result<PatchSignals, serde_json::Error> { 111 Ok(PatchSignals { json: serde_json::to_string(signals)?, only_if_missing: false }) 112 } 113 114 /// Sets only the signals the page does not have yet. 115 pub fn only_if_missing(mut self) -> PatchSignals { 116 self.only_if_missing = true; 117 self 118 } 119 120 pub fn sse(&self) -> String { 121 let mut out = String::from("event: datastar-patch-signals\n"); 122 if self.only_if_missing { 123 out.push_str("data: onlyIfMissing true\n"); 124 } 125 out.push_str(&format!("data: signals {}\n\n", self.json)); 126 out 127 } 128} 129 130/// The signals of a request: the `datastar` query parameter's value (already percent-decoded) for a 131/// GET, the body for anything else. The same JSON either way. 132pub fn signals<T: DeserializeOwned>(json: &str) -> Result<T, serde_json::Error> { 133 serde_json::from_str(json) 134} 135 136/// What a page asks of a server with no signals: an empty object, so a field with a default reads. 137pub fn no_signals() -> &'static str { 138 "{}" 139} 140 141/// A selector or a signal name is a single line; a line break in one would end the `data` line. 142fn one_line(text: &str) -> String { 143 text.replace(['\n', '\r'], " ") 144} 145 146#[cfg(test)] 147mod tests { 148 use super::*; 149 use serde::Deserialize; 150 151 #[test] 152 fn a_patch_is_one_data_line_per_line_of_markup() { 153 let sse = PatchElements::new("<pre id=\"a\">x\n y</pre>").sse(); 154 assert_eq!(sse, "event: datastar-patch-elements\ndata: elements <pre id=\"a\">x\ndata: elements y</pre>\n\n"); 155 } 156 157 #[test] 158 fn selector_and_mode_come_before_the_markup() { 159 let sse = PatchElements::new("<li>1</li>").selector("#list").mode(Mode::Append).sse(); 160 assert_eq!( 161 sse, 162 "event: datastar-patch-elements\ndata: selector #list\ndata: mode append\ndata: elements <li>1</li>\n\n" 163 ); 164 } 165 166 #[test] 167 fn removing_names_only_the_selector() { 168 assert_eq!( 169 PatchElements::remove("#gone").sse(), 170 "event: datastar-patch-elements\ndata: selector #gone\ndata: mode remove\n\n" 171 ); 172 } 173 174 #[test] 175 fn a_selector_cannot_end_its_own_line() { 176 assert!(!PatchElements::new("<p></p>").selector("#a\ndata: elements <script>").sse().contains("\ndata: elements <script>")); 177 } 178 179 #[test] 180 fn signals_are_one_json_line() { 181 let sse = PatchSignals::of(&serde_json::json!({ "n": 1, "s": "a\nb" })).unwrap().only_if_missing().sse(); 182 assert_eq!(sse, "event: datastar-patch-signals\ndata: onlyIfMissing true\ndata: signals {\"n\":1,\"s\":\"a\\nb\"}\n\n"); 183 } 184 185 #[test] 186 fn signals_read_with_defaults() { 187 #[derive(Deserialize, Default, PartialEq, Debug)] 188 #[serde(default)] 189 struct Page { 190 name: String, 191 count: u32, 192 } 193 assert_eq!(signals::<Page>(r#"{"name":"a"}"#).unwrap(), Page { name: "a".into(), count: 0 }); 194 assert_eq!(signals::<Page>(no_signals()).unwrap(), Page::default()); 195 } 196 197 #[test] 198 fn the_script_is_datastar_one_zero_four() { 199 assert!(SCRIPT.contains("1.0.4")); 200 } 201}