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 only records whether the expression needs a browser binding.
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
78macro_rules! numeric_signal {
79    ($($number:ty),+ $(,)?) => {
80        $(impl SignalSurrogate<$number> {
81            /// Adds one to the value.
82            ///
83            /// # Panics
84            ///
85            /// Always panics on the server. Integer overflow panics in the browser.
86            #[track_caller]
87            pub fn increment(&self) {
88                write_in_browser_only();
89            }
90
91            /// Subtracts one from the value.
92            ///
93            /// # Panics
94            ///
95            /// Always panics on the server. Integer overflow panics in the browser.
96            #[track_caller]
97            pub fn decrement(&self) {
98                write_in_browser_only();
99            }
100        })+
101    };
102}
103
104numeric_signal!(
105    f64, u8, u16, u32, u64, u128, usize, i8, i16, i32, i64, i128, isize
106);
107
108impl SignalSurrogate<String> {
109    /// Appends a string to the end of the value.
110    ///
111    /// Accepts borrowed or owned strings, including an event target's value.
112    ///
113    /// # Panics
114    ///
115    /// Always panics; signal writes can only occur in client-side expressions.
116    #[track_caller]
117    pub fn push_str(&self, _s: impl Deref<Target = StrSurrogate>) {
118        write_in_browser_only();
119    }
120}
121
122/// The panic shared by every signal write evaluated on the server.
123#[track_caller]
124fn write_in_browser_only() -> ! {
125    panic!("expressions in which a signal is written to cannot be run server-side");
126}
127
128impl_surrogate!({T} Signal<T>, SignalSurrogate<T>);
129impl_surrogate_ref!({T} Signal<T>, SignalSurrogate<T>);
130impl_surrogate_mut!({T} Signal<T>, SignalSurrogate<T>);
131
132impl<T> Serialize for SignalSurrogate<T> {
133    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
134    where
135        S: serde::Serializer,
136    {
137        #[derive(Serialize)]
138        struct TaggedSignal {
139            t: &'static str,
140            id: std::string::String,
141        }
142
143        TaggedSignal {
144            t: "Signal",
145            id: self.0.id().to_string(),
146        }
147        .serialize(serializer)
148    }
149}
150
151/// A signal sent by the client, as an argument to a run: its id next to its
152/// current value.
153///
154/// The value is required. A run cannot read a signal it has no value for,
155/// so a client that sends only the id is rejected the same way as one that
156/// sends a value of the wrong shape.
157impl<'de, T> Deserialize<'de> for SignalSurrogate<T>
158where
159    T: Surrogated,
160    T::Surrogate: Deserialize<'de>,
161{
162    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
163    where
164        D: serde::Deserializer<'de>,
165    {
166        #[derive(Deserialize)]
167        #[serde(deny_unknown_fields, bound(deserialize = "V: Deserialize<'de>"))]
168        struct TaggedSignal<V> {
169            t: std::string::String,
170            id: SignalId,
171            v: V,
172        }
173
174        let tagged = TaggedSignal::<T::Surrogate>::deserialize(deserializer)?;
175        if tagged.t != "Signal" {
176            return Err(de::Error::invalid_value(
177                de::Unexpected::Str(&tagged.t),
178                &"Signal",
179            ));
180        }
181        Ok(Self(Signal::new(tagged.id, tagged.v.into_real())))
182    }
183}
184
185#[cfg(test)]
186mod tests {
187    use std::panic::Location;
188
189    use serde_json::json;
190    use topcoat_core::identity::Identity;
191
192    use super::*;
193
194    /// Builds a signal surrogate around a fresh signal holding `value`.
195    #[track_caller]
196    fn surrogate<T>(value: T) -> SignalSurrogate<T> {
197        SignalSurrogate::new(Signal::new(
198            SignalId::derive(Identity::ROOT, Location::caller()),
199            value,
200        ))
201    }
202
203    #[test]
204    fn serializes_as_its_id() {
205        let signal = surrogate(String::from("shoes"));
206
207        assert_eq!(
208            serde_json::to_value(&signal).unwrap(),
209            json!({ "t": "Signal", "id": signal.0.id().to_string() })
210        );
211    }
212
213    #[test]
214    fn deserializes_from_its_id_and_value() {
215        let id = SignalId::derive(Identity::ROOT, Location::caller());
216
217        let signal: SignalSurrogate<String> =
218            serde_json::from_value(json!({ "t": "Signal", "id": id.to_string(), "v": "shoes" }))
219                .unwrap();
220
221        assert_eq!(signal.0.id(), id);
222        assert_eq!(*signal.0.read_untracked(), "shoes");
223    }
224
225    #[test]
226    fn deserializes_a_value_through_its_surrogate() {
227        let id = SignalId::derive(Identity::ROOT, Location::caller());
228
229        let signal: SignalSurrogate<Option<f64>> = serde_json::from_value(json!({
230            "t": "Signal",
231            "id": id.to_string(),
232            "v": { "t": "Option", "v": 5.0 },
233        }))
234        .unwrap();
235
236        assert_eq!(*signal.0.read_untracked(), Some(5.0));
237    }
238
239    #[test]
240    fn rejects_another_tag() {
241        let id = SignalId::derive(Identity::ROOT, Location::caller());
242
243        let error = serde_json::from_value::<SignalSurrogate<String>>(
244            json!({ "t": "Procedure", "id": id.to_string(), "v": "shoes" }),
245        )
246        .unwrap_err();
247
248        assert!(error.to_string().contains("expected Signal"), "{error}");
249    }
250
251    #[test]
252    fn rejects_a_missing_value() {
253        let id = SignalId::derive(Identity::ROOT, Location::caller());
254
255        let error = serde_json::from_value::<SignalSurrogate<String>>(
256            json!({ "t": "Signal", "id": id.to_string() }),
257        )
258        .unwrap_err();
259
260        assert!(error.to_string().contains("missing field `v`"), "{error}");
261    }
262
263    #[test]
264    fn rejects_a_value_of_another_type() {
265        let id = SignalId::derive(Identity::ROOT, Location::caller());
266
267        serde_json::from_value::<SignalSurrogate<f64>>(json!({
268            "t": "Signal",
269            "id": id.to_string(),
270            "v": "shoes",
271        }))
272        .unwrap_err();
273    }
274
275    #[test]
276    fn rejects_a_malformed_id() {
277        serde_json::from_value::<SignalSurrogate<String>>(json!({
278            "t": "Signal",
279            "id": "not hex",
280            "v": "shoes",
281        }))
282        .unwrap_err();
283    }
284
285    #[test]
286    #[should_panic(expected = "cannot be run server-side")]
287    fn toggle_panics_server_side() {
288        surrogate(false).toggle();
289    }
290
291    #[test]
292    #[should_panic(expected = "cannot be run server-side")]
293    fn increment_panics_server_side() {
294        surrogate(0.0).increment();
295    }
296
297    #[test]
298    #[should_panic(expected = "cannot be run server-side")]
299    fn decrement_panics_server_side() {
300        surrogate(0.0).decrement();
301    }
302
303    #[test]
304    #[should_panic(expected = "cannot be run server-side")]
305    fn push_str_panics_server_side() {
306        surrogate(String::new()).push_str(StrSurrogate::ref_cast(""));
307    }
308}