Skip to main content

topcoat_runtime/
signal.rs

1use std::{iter::empty, ops::Deref};
2
3use serde::{Deserialize, Serialize, de::DeserializeOwned};
4use topcoat_core::context::Cx;
5use topcoat_view::{NodeViewParts, PartsWriter};
6use uuid::Uuid;
7
8use crate::{Surrogate, Surrogated};
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
11#[serde(transparent)]
12pub struct SignalId(Uuid);
13
14impl SignalId {
15    #[inline]
16    #[must_use]
17    pub fn new() -> Self {
18        Self(Uuid::new_v4())
19    }
20}
21
22impl Default for SignalId {
23    #[inline]
24    fn default() -> Self {
25        Self::new()
26    }
27}
28
29impl std::fmt::Display for SignalId {
30    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
31        self.0.fmt(f)
32    }
33}
34
35#[derive(Debug)]
36pub struct Signal<T> {
37    id: SignalId,
38    value: T,
39}
40
41impl<T> Signal<T> {
42    #[inline]
43    pub fn new(value: T) -> Self {
44        Self {
45            id: SignalId::new(),
46            value,
47        }
48    }
49
50    pub(crate) fn id(&self) -> SignalId {
51        self.id
52    }
53
54    pub(crate) fn read(&self) -> &T {
55        &self.value
56    }
57}
58
59impl<T> Signal<T>
60where
61    T: Clone,
62{
63    pub(crate) fn get(&self) -> T {
64        self.value.clone()
65    }
66}
67
68pub struct SignalDeclaration<'a, T>(&'a Signal<T>);
69
70impl<'a, T> SignalDeclaration<'a, T> {
71    #[inline]
72    pub fn new(signal: &'a Signal<T>) -> Self {
73        Self(signal)
74    }
75}
76
77impl<T> NodeViewParts for SignalDeclaration<'_, T>
78where
79    for<'a> &'a T: Surrogated,
80    for<'a> <&'a T as Surrogated>::Surrogate: Serialize,
81{
82    fn into_view_parts(self, _cx: &Cx, parts: &mut PartsWriter<'_>) {
83        #[derive(Serialize)]
84        struct SignalDeclarationPayload<'a, V>
85        where
86            V: ?Sized,
87        {
88            t: &'static str,
89            id: std::string::String,
90            v: &'a V,
91        }
92
93        let value = (&self.0.value).into_surrogate();
94        let payload = SignalDeclarationPayload {
95            t: "signal",
96            id: self.0.id().to_string(),
97            v: &value,
98        };
99        let json = serde_json::to_string(&payload)
100            .expect("failed to serialize signal declaration payload");
101
102        parts.push_comment(|comment| {
103            // `json` is untrusted application data and must be escaped to prevent XSS attacks.
104            comment
105                .push_str_unescaped("::topcoat::signal(")
106                .push_str(json)
107                .push_str_unescaped(")");
108        });
109    }
110}
111
112#[derive(Debug, Clone)]
113pub struct ReadSignal<T> {
114    id: SignalId,
115    value: T,
116}
117
118impl<T> ReadSignal<T> {
119    pub fn new(signal: &Signal<T>) -> Self
120    where
121        T: Clone,
122    {
123        Self {
124            id: signal.id,
125            value: signal.value.clone(),
126        }
127    }
128
129    pub fn id(&self) -> SignalId {
130        self.id
131    }
132}
133
134impl<T> Deref for ReadSignal<T> {
135    type Target = T;
136
137    fn deref(&self) -> &Self::Target {
138        &self.value
139    }
140}
141
142impl<'de, T> Deserialize<'de> for ReadSignal<T>
143where
144    T: Surrogated,
145    T::Surrogate: Deserialize<'de>,
146{
147    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
148    where
149        D: serde::Deserializer<'de>,
150    {
151        #[derive(Deserialize)]
152        #[serde(deny_unknown_fields)]
153        struct EncodedReadSignal<S> {
154            id: SignalId,
155            value: S,
156        }
157
158        let encoded = EncodedReadSignal::<T::Surrogate>::deserialize(deserializer)?;
159        Ok(Self {
160            id: encoded.id,
161            value: encoded.value.into_real(),
162        })
163    }
164}
165
166pub trait Signals: Sized {
167    fn ids(&self) -> impl Iterator<Item = SignalId>;
168    fn decode(encoded_signals: EncodedSignals) -> Self;
169}
170
171impl Signals for () {
172    fn ids(&self) -> impl Iterator<Item = SignalId> {
173        empty()
174    }
175
176    fn decode(_encoded_signals: EncodedSignals) -> Self {}
177}
178
179macro_rules! impl_signals_for_tuple {
180    ($($n:tt $t:ident),+) => {
181        impl<$($t),+> Signals for ($(ReadSignal<$t>,)+)
182        where
183            $(
184                $t: Surrogated,
185                <$t as Surrogated>::Surrogate: DeserializeOwned,
186            )+
187        {
188            fn ids(&self) -> impl Iterator<Item = SignalId> {
189                [$(self.$n.id),+].into_iter()
190            }
191
192            fn decode(encoded_signals: EncodedSignals) -> Self {
193                serde_json::from_str(&encoded_signals.0).unwrap()
194            }
195        }
196    };
197}
198
199impl_signals_for_tuple!(0 T0);
200impl_signals_for_tuple!(0 T0, 1 T1);
201impl_signals_for_tuple!(0 T0, 1 T1, 2 T2);
202impl_signals_for_tuple!(0 T0, 1 T1, 2 T2, 3 T3);
203impl_signals_for_tuple!(0 T0, 1 T1, 2 T2, 3 T3, 4 T4);
204impl_signals_for_tuple!(0 T0, 1 T1, 2 T2, 3 T3, 4 T4, 5 T5);
205impl_signals_for_tuple!(0 T0, 1 T1, 2 T2, 3 T3, 4 T4, 5 T5, 6 T6);
206impl_signals_for_tuple!(0 T0, 1 T1, 2 T2, 3 T3, 4 T4, 5 T5, 6 T6, 7 T7);
207impl_signals_for_tuple!(0 T0, 1 T1, 2 T2, 3 T3, 4 T4, 5 T5, 6 T6, 7 T7, 8 T8);
208impl_signals_for_tuple!(0 T0, 1 T1, 2 T2, 3 T3, 4 T4, 5 T5, 6 T6, 7 T7, 8 T8, 9 T9);
209impl_signals_for_tuple!(0 T0, 1 T1, 2 T2, 3 T3, 4 T4, 5 T5, 6 T6, 7 T7, 8 T8, 9 T9, 10 T10);
210impl_signals_for_tuple!(0 T0, 1 T1, 2 T2, 3 T3, 4 T4, 5 T5, 6 T6, 7 T7, 8 T8, 9 T9, 10 T10, 11 T11);
211
212pub struct EncodedSignals(String);
213
214impl EncodedSignals {
215    pub fn new(inner: impl Into<String>) -> Self {
216        Self(inner.into())
217    }
218}
219
220#[cfg(test)]
221mod tests {
222    use topcoat_view::{HtmlContext, PartsWriter, View, ViewParts};
223
224    use super::*;
225
226    #[test]
227    fn payload_cannot_terminate_the_comment() {
228        // A value carrying `-->`, a quote, and an ampersand: the characters
229        // that could break out of the comment or corrupt its JSON payload.
230        let signal = Signal::new(String::from("a-->b\"c&d"));
231
232        let cx = Cx::default();
233        let mut parts = ViewParts::new();
234        SignalDeclaration::new(&signal)
235            .into_view_parts(&cx, &mut PartsWriter::new(&mut parts, HtmlContext::Text));
236        let html = View::new(parts).render(&cx);
237
238        // The comment context escaped `>`, so the only `-->` left is the
239        // marker's own terminator; the payload cannot end the comment early.
240        assert_eq!(html.matches("-->").count(), 1);
241        assert!(html.ends_with(") -->"));
242        assert!(html.contains("--&gt;"));
243        // The JSON's own quotes round-trip as entities the client decodes.
244        assert!(html.contains("&quot;"));
245    }
246}