Skip to main content

topcoat_runtime/surrogate/
signal.rs

1use std::ops::Deref;
2
3use ref_cast::RefCast;
4use serde::{Deserialize, Serialize, de};
5
6use crate::{
7    Signal, SignalId, StrSurrogate, Surrogate, Surrogated, impl_surrogate, impl_surrogate_mut,
8    impl_surrogate_ref,
9};
10
11#[derive(Debug, RefCast)]
12#[repr(transparent)]
13pub struct SignalSurrogate<T>(Signal<T>);
14
15impl<T> SignalSurrogate<T> {
16    #[inline]
17    pub(crate) const fn new(v: Signal<T>) -> Self {
18        Self(v)
19    }
20}
21
22impl<T> SignalSurrogate<T>
23where
24    for<'b> &'b T: Surrogated,
25{
26    /// Borrows the current value.
27    ///
28    /// Reads inside a runtime expression are the client-reactive path and
29    /// do not register a dependency on the server; the server-side
30    /// evaluation that produces the initial render reads untracked.
31    #[must_use]
32    pub fn read(&self) -> <&T as Surrogated>::Surrogate {
33        self.0.read_untracked().into_surrogate()
34    }
35}
36
37impl<T> SignalSurrogate<T>
38where
39    T: Surrogated + Clone,
40{
41    /// Clones the current value.
42    ///
43    /// Like [`read`](Self::read), this does not register a dependency on
44    /// the server.
45    #[must_use]
46    pub fn get(&self) -> <T as Surrogated>::Surrogate {
47        self.0.get_untracked().into_surrogate()
48    }
49}
50
51impl<T> SignalSurrogate<T>
52where
53    T: Surrogated,
54{
55    /// Writes a new value to the signal.
56    ///
57    /// # Panics
58    ///
59    /// Always panics; signal writes can only occur in client-side expressions.
60    #[track_caller]
61    pub fn set(&self, _v: T::Surrogate) {
62        write_in_browser_only();
63    }
64}
65
66impl SignalSurrogate<bool> {
67    /// Replaces the value with its negation.
68    ///
69    /// # Panics
70    ///
71    /// Always panics; signal writes can only occur in client-side expressions.
72    #[track_caller]
73    pub fn toggle(&self) {
74        write_in_browser_only();
75    }
76}
77
78impl SignalSurrogate<f64> {
79    /// Adds one to the value.
80    ///
81    /// # Panics
82    ///
83    /// Always panics; signal writes can only occur in client-side expressions.
84    #[track_caller]
85    pub fn increment(&self) {
86        write_in_browser_only();
87    }
88
89    /// Subtracts one from the value.
90    ///
91    /// # Panics
92    ///
93    /// Always panics; signal writes can only occur in client-side expressions.
94    #[track_caller]
95    pub fn decrement(&self) {
96        write_in_browser_only();
97    }
98}
99
100impl SignalSurrogate<String> {
101    /// Appends a string to the end of the value.
102    ///
103    /// The argument is anything that dereferences to a string, so both a
104    /// borrowed `&str` and an owned `String` work. The owned form is what an
105    /// event field yields: `Event::target.value` is a `String`, so
106    /// `message.push_str(e.target.value)` is the common call.
107    ///
108    /// # Panics
109    ///
110    /// Always panics; signal writes can only occur in client-side expressions.
111    #[track_caller]
112    pub fn push_str(&self, _s: impl Deref<Target = StrSurrogate>) {
113        write_in_browser_only();
114    }
115}
116
117/// The panic shared by every signal write evaluated on the server.
118#[track_caller]
119fn write_in_browser_only() -> ! {
120    panic!("expressions in which a signal is written to cannot be run server-side");
121}
122
123impl_surrogate!({T} Signal<T>, SignalSurrogate<T>);
124impl_surrogate_ref!({T} Signal<T>, SignalSurrogate<T>);
125impl_surrogate_mut!({T} Signal<T>, SignalSurrogate<T>);
126
127impl<T> Serialize for SignalSurrogate<T> {
128    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
129    where
130        S: serde::Serializer,
131    {
132        #[derive(Serialize)]
133        struct TaggedSignal {
134            t: &'static str,
135            id: std::string::String,
136        }
137
138        TaggedSignal {
139            t: "Signal",
140            id: self.0.id().to_string(),
141        }
142        .serialize(serializer)
143    }
144}
145
146/// A signal sent by the client, as an argument to a run: its id next to its
147/// current value.
148///
149/// The value is required. A run cannot read a signal it has no value for,
150/// so a client that sends only the id is rejected the same way as one that
151/// sends a value of the wrong shape.
152impl<'de, T> Deserialize<'de> for SignalSurrogate<T>
153where
154    T: Surrogated,
155    T::Surrogate: Deserialize<'de>,
156{
157    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
158    where
159        D: serde::Deserializer<'de>,
160    {
161        #[derive(Deserialize)]
162        #[serde(deny_unknown_fields, bound(deserialize = "V: Deserialize<'de>"))]
163        struct TaggedSignal<V> {
164            t: std::string::String,
165            id: SignalId,
166            v: V,
167        }
168
169        let tagged = TaggedSignal::<T::Surrogate>::deserialize(deserializer)?;
170        if tagged.t != "Signal" {
171            return Err(de::Error::invalid_value(
172                de::Unexpected::Str(&tagged.t),
173                &"Signal",
174            ));
175        }
176        Ok(Self(Signal::new(tagged.id, tagged.v.into_real())))
177    }
178}
179
180#[cfg(test)]
181mod tests {
182    use std::panic::Location;
183
184    use serde_json::json;
185
186    use super::*;
187
188    /// Builds a signal surrogate around a fresh signal holding `value`.
189    #[track_caller]
190    fn surrogate<T>(value: T) -> SignalSurrogate<T> {
191        SignalSurrogate::new(Signal::new(SignalId::derive(Location::caller()), value))
192    }
193
194    #[test]
195    fn serializes_as_its_id() {
196        let signal = surrogate(String::from("shoes"));
197
198        assert_eq!(
199            serde_json::to_value(&signal).unwrap(),
200            json!({ "t": "Signal", "id": signal.0.id().to_string() })
201        );
202    }
203
204    #[test]
205    fn deserializes_from_its_id_and_value() {
206        let id = SignalId::derive(Location::caller());
207
208        let signal: SignalSurrogate<String> =
209            serde_json::from_value(json!({ "t": "Signal", "id": id.to_string(), "v": "shoes" }))
210                .unwrap();
211
212        assert_eq!(signal.0.id(), id);
213        assert_eq!(*signal.0.read_untracked(), "shoes");
214    }
215
216    #[test]
217    fn deserializes_a_value_through_its_surrogate() {
218        let id = SignalId::derive(Location::caller());
219
220        let signal: SignalSurrogate<Option<f64>> = serde_json::from_value(json!({
221            "t": "Signal",
222            "id": id.to_string(),
223            "v": { "t": "Option", "v": 5.0 },
224        }))
225        .unwrap();
226
227        assert_eq!(*signal.0.read_untracked(), Some(5.0));
228    }
229
230    #[test]
231    fn rejects_another_tag() {
232        let id = SignalId::derive(Location::caller());
233
234        let error = serde_json::from_value::<SignalSurrogate<String>>(
235            json!({ "t": "Procedure", "id": id.to_string(), "v": "shoes" }),
236        )
237        .unwrap_err();
238
239        assert!(error.to_string().contains("expected Signal"), "{error}");
240    }
241
242    #[test]
243    fn rejects_a_missing_value() {
244        let id = SignalId::derive(Location::caller());
245
246        let error = serde_json::from_value::<SignalSurrogate<String>>(
247            json!({ "t": "Signal", "id": id.to_string() }),
248        )
249        .unwrap_err();
250
251        assert!(error.to_string().contains("missing field `v`"), "{error}");
252    }
253
254    #[test]
255    fn rejects_a_value_of_another_type() {
256        let id = SignalId::derive(Location::caller());
257
258        serde_json::from_value::<SignalSurrogate<f64>>(json!({
259            "t": "Signal",
260            "id": id.to_string(),
261            "v": "shoes",
262        }))
263        .unwrap_err();
264    }
265
266    #[test]
267    fn rejects_a_malformed_id() {
268        serde_json::from_value::<SignalSurrogate<String>>(json!({
269            "t": "Signal",
270            "id": "not hex",
271            "v": "shoes",
272        }))
273        .unwrap_err();
274    }
275
276    #[test]
277    #[should_panic(expected = "cannot be run server-side")]
278    fn toggle_panics_server_side() {
279        surrogate(false).toggle();
280    }
281
282    #[test]
283    #[should_panic(expected = "cannot be run server-side")]
284    fn increment_panics_server_side() {
285        surrogate(0.0).increment();
286    }
287
288    #[test]
289    #[should_panic(expected = "cannot be run server-side")]
290    fn decrement_panics_server_side() {
291        surrogate(0.0).decrement();
292    }
293
294    #[test]
295    #[should_panic(expected = "cannot be run server-side")]
296    fn push_str_panics_server_side() {
297        surrogate(String::new()).push_str(StrSurrogate::ref_cast(""));
298    }
299}