lib.rsannotatedlib.rssource201 lines · 6.5 KB · raw

Datastar's wire, once: the events a server sends, the signals it reads, and the script a page loads.

No I/O and no runtime. An event is built here and written out as text ([PatchElements::sse], [PatchSignals::sse]); an adapter for a server (the Worker's, axum's) puts that text on a response. Datastar's own types are not used, so nothing here moves when that crate does.

7use serde::de::DeserializeOwned;
8use serde::Serialize;

The script a page loads. Datastar 1.0.4, the same bytes in every property.

11pub const SCRIPT: &str = include_str!("../datastar.js");

The query parameter a GET request carries its signals in, as JSON.

14pub const QUERY_PARAMETER: &str = "datastar";

The header every Datastar request carries, with the value true.

17pub const REQUEST_HEADER: &str = "datastar-request";

The content-type of a stream of events.

20pub const EVENT_STREAM: &str = "text/event-stream";

How patched elements meet the page.

23#[derive(Clone, Copy, Debug, PartialEq, Eq)]
24pub enum Mode {

Morph the element with the same id (Datastar's default).

26    Outer,

Morph the inside of the selected element.

28    Inner,
29    Replace,
30    Prepend,
31    Append,
32    Before,
33    After,
34    Remove,
35}
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}

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}
60impl PatchElements {

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    }

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    }
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    }

The event as stream text. Each line of the markup is its own elements line, so a newline in 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}

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}
108impl PatchSignals {

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    }

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    }
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}

The signals of a request: the datastar query parameter's value (already percent-decoded) for a 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}

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}

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}
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}